Merge lp:~camptocamp/purchase-wkfl/7.0-add_framework_agreement-nbi into lp:~purchase-core-editors/purchase-wkfl/7.0

Proposed by Nicolas Bessi - Camptocamp
Status: Merged
Merged at revision: 24
Proposed branch: lp:~camptocamp/purchase-wkfl/7.0-add_framework_agreement-nbi
Merge into: lp:~purchase-core-editors/purchase-wkfl/7.0
Diff against target: 2182 lines (+2066/-0)
22 files modified
framework_agreement/__init__.py (+22/-0)
framework_agreement/__openerp__.py (+67/-0)
framework_agreement/data.xml (+19/-0)
framework_agreement/model/__init__.py (+25/-0)
framework_agreement/model/company.py (+32/-0)
framework_agreement/model/framework_agreement.py (+734/-0)
framework_agreement/model/pricelist.py (+80/-0)
framework_agreement/model/product.py (+40/-0)
framework_agreement/model/purchase.py (+155/-0)
framework_agreement/security/ir.model.access.csv (+5/-0)
framework_agreement/security/multicompany.xml (+11/-0)
framework_agreement/tests/__init__.py (+30/-0)
framework_agreement/tests/common.py (+97/-0)
framework_agreement/tests/test_framework_agreement_consumed_qty.py (+74/-0)
framework_agreement/tests/test_framework_agreement_dates_and_constraints.py (+135/-0)
framework_agreement/tests/test_framework_agreement_on_change.py (+166/-0)
framework_agreement/tests/test_framework_agreement_price_list.py (+73/-0)
framework_agreement/utils.py (+29/-0)
framework_agreement/view/company_view.xml (+17/-0)
framework_agreement/view/framework_agreement_view.xml (+118/-0)
framework_agreement/view/product_view.xml (+86/-0)
framework_agreement/view/purchase_view.xml (+51/-0)
To merge this branch: bzr merge lp:~camptocamp/purchase-wkfl/7.0-add_framework_agreement-nbi
Reviewer Review Type Date Requested Status
Romain Deheele - Camptocamp (community) code review Approve
Joël Grand-Guillaume @ camptocamp code review + test Approve
Review via email: mp+196681@code.launchpad.net

Description of the change

Add framework agreement

To post a comment you must log in.
33. By Nicolas Bessi - Camptocamp

[FIX] PO onchange

34. By Nicolas Bessi - Camptocamp

[FIX] sequence.get is deprecated

Revision history for this message
Romain Deheele - Camptocamp (romaindeheele) wrote :

Hello,

I see 3 points:

- integrity error on framework agreement deletion

- import pdb in def open_agreement (maybe voluntary)

- change price in a FA doesn't change price in existing purchase order lines, is it voluntary?

Romain

review: Needs Information (code review, tests)
35. By Nicolas Bessi - Camptocamp

[RM] forgotten pdb

Revision history for this message
Nicolas Bessi - Camptocamp (nbessi-c2c-deactivatedaccount) wrote :

Hello,

thanks for the review

>
> I see 3 points:
>
> - integrity error on framework agreement deletion
As discussed a better message will be nice but nice to have.
>
> - import pdb in def open_agreement (maybe voluntary)>
Fixed
> - change price in a FA doesn't change price in existing purchase order lines,
> is it voluntary?
Yes but we should add a warning on change of price if related to a PO

Regards

Nicolas

36. By Nicolas Bessi - Camptocamp

[IMP] better docstring

37. By Nicolas Bessi - Camptocamp

[ADD] missing security access

38. By Nicolas Bessi - Camptocamp

[FIX] open agreement button attrs

39. By Nicolas Bessi - Camptocamp

[FIX] available quantity that was buggy because of 3 reasons:
First the related field on po_line po was not updated
Second There was missind an and close on product_id
Third we do not manage correct trigger when we remove LTA from a PO action that was first impossible

40. By Nicolas Bessi - Camptocamp

[FIX] abusive consumption of agreement sequence

41. By Romain Deheele - Camptocamp

[MRG] missing domain on supplier field

Revision history for this message
Joël Grand-Guillaume @ camptocamp (jgrandguillaume-c2c) wrote :

Hi,

This LGTM. I just have the little remark I already made:

 * Adding a LTA on a product should add a supplierinfo entry as well.

A part from that, LGTM. I don't want to block it for that. Just to let you know.

Regards,

review: Approve (code review + test)
Revision history for this message
Romain Deheele - Camptocamp (romaindeheele) wrote :

LGTM,

Regards,

Romain

review: Approve (code review)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added directory 'framework_agreement'
=== added file 'framework_agreement/__init__.py'
--- framework_agreement/__init__.py 1970-01-01 00:00:00 +0000
+++ framework_agreement/__init__.py 2014-02-06 10:10:57 +0000
@@ -0,0 +1,22 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Nicolas Bessi
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21from . import model
22from . import utils
023
=== added file 'framework_agreement/__openerp__.py'
--- framework_agreement/__openerp__.py 1970-01-01 00:00:00 +0000
+++ framework_agreement/__openerp__.py 2014-02-06 10:10:57 +0000
@@ -0,0 +1,67 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Nicolas Bessi
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21{'name': 'Simple Framework Agreement',
22 'version': '0.1',
23 'author': 'Camptocamp',
24 'maintainer': 'Camptocamp',
25 'category': 'Purchase Management',
26 'complexity': 'normal',
27 'depends': ['stock', 'procurement', 'purchase'],
28 'description': """
29Long Term Agreement (or Framework Agreement) on price.
30======================================================
31
32Agreements are defined by a product, a date range , a supplier, a price, a lead time
33and agreed quantity.
34
35Agreements are set on a product view or using a menu in the product configuration.
36
37There can be only one agreement for the same supplier and product at the same time, even
38if we may have different prices depending on lead time/qty.
39
40There is an option on company to restrict one agreement per product at same time.
41
42If an agreement is running its price will be automatically used in PO.
43A warning will be raised in case of exhaustion of override of agreement price.
44
45**Technical aspect**
46
47The module provide an observalbe mixin to enable generic on_change management on various model
48related to agreements.
49
50The framework agreement is by default related to purchase order but the addon
51provides a library to integrate it with any other model easily
52""",
53 'website': 'http://www.camptocamp.com',
54 'data': ['data.xml',
55 'view/product_view.xml',
56 'view/framework_agreement_view.xml',
57 'view/purchase_view.xml',
58 'view/company_view.xml',
59 'security/multicompany.xml',
60 'security/ir.model.access.csv'],
61 'demo': [],
62 'test': [],
63 'installable': True,
64 'auto_install': False,
65 'license': 'AGPL-3',
66 'application': False,
67 }
068
=== added file 'framework_agreement/data.xml'
--- framework_agreement/data.xml 1970-01-01 00:00:00 +0000
+++ framework_agreement/data.xml 2014-02-06 10:10:57 +0000
@@ -0,0 +1,19 @@
1<?xml version="1.0" encoding="utf-8"?>
2<openerp>
3 <data>
4 <record id="framework_agreement_sequence_type" model="ir.sequence.type">
5 <field name="name">Framework Agreement</field>
6 <field name="code">framework.agreement</field>
7 </record>
8 <record id="seq_mrp_repair" model="ir.sequence">
9 <field name="name">framework.agreement</field>
10 <field name="code">framework.agreement</field>
11 <field name="prefix">LTA</field>
12 </record>
13
14 <record id="framework_agreement_currency_type" model="res.currency.rate.type">
15 <field name="name">Framework Agreement</field>
16 </record>
17
18 </data>
19</openerp>
020
=== added directory 'framework_agreement/i18n'
=== added directory 'framework_agreement/model'
=== added file 'framework_agreement/model/__init__.py'
--- framework_agreement/model/__init__.py 1970-01-01 00:00:00 +0000
+++ framework_agreement/model/__init__.py 2014-02-06 10:10:57 +0000
@@ -0,0 +1,25 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Nicolas Bessi
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21from . import pricelist
22from . import product
23from . import framework_agreement
24from . import purchase
25from . import company
026
=== added file 'framework_agreement/model/company.py'
--- framework_agreement/model/company.py 1970-01-01 00:00:00 +0000
+++ framework_agreement/model/company.py 2014-02-06 10:10:57 +0000
@@ -0,0 +1,32 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Nicolas Bessi
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21from openerp.osv import orm, fields
22
23
24class res_Company(orm.Model):
25 """Add a field on company"""
26
27 _inherit = "res.company"
28 _columns = {'one_agreement_per_product': fields.boolean('One agreement per product',
29 help='If checked you can have only'
30 ' one framework agreement '
31 ' per product at the same time')}
32 # TODO add check on activation deactivation of check box
033
=== added file 'framework_agreement/model/framework_agreement.py'
--- framework_agreement/model/framework_agreement.py 1970-01-01 00:00:00 +0000
+++ framework_agreement/model/framework_agreement.py 2014-02-06 10:10:57 +0000
@@ -0,0 +1,734 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Nicolas Bessi
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21from operator import attrgetter
22from collections import namedtuple
23from datetime import datetime
24from openerp.osv import orm, fields
25from openerp.osv.orm import except_orm
26from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
27from openerp.tools.translate import _
28import openerp.addons.decimal_precision as dp
29
30AGR_PO_STATE = ('confirmed', 'approved',
31 'done', 'except_picking', 'except_invoice')
32
33
34class framework_agreement(orm.Model):
35 """Long term agreement on product price with a supplier"""
36
37 _name = 'framework.agreement'
38 _description = 'Agreement on price'
39
40 def _check_running_date(self, cr, agreement, context=None):
41 """ Returns agreement state based on date.
42
43 Available qty is ignored in this method
44
45 :param agreement: an agreement record
46
47 :returns: a string - "running" if now is between,
48 - "future" if agreement is in future,
49 - "closed" if agreement is outdated
50
51 """
52 now, start, end = self._get_dates(agreement, context=context)
53 if start > now:
54 return 'future'
55 elif end < now:
56 return 'closed'
57 elif start <= now <= end:
58 return 'running'
59 else:
60 raise ValueError('Agreement start/end dates are incorrect')
61
62 def _get_dates(self, agreement, context=None):
63 """Return current time, start date and end date of agreement
64
65 Boiler plate as OpenERP returns string instead of date/time objects...
66
67 :param agreement: agreement record
68
69 :returns: (now, start, end)
70
71 """
72 AGDates = namedtuple('AGDates', ['now', 'start', 'end'])
73 now = datetime.strptime(fields.date.today(),
74 DEFAULT_SERVER_DATE_FORMAT)
75 start = datetime.strptime(agreement.start_date,
76 DEFAULT_SERVER_DATE_FORMAT)
77 end = datetime.strptime(agreement.end_date,
78 DEFAULT_SERVER_DATE_FORMAT)
79 return AGDates(now, start, end)
80
81 def date_valid(self, cr, uid, agreement_id, date, context=None):
82 """Predicate that checks that date is in agreement
83
84 :param date: date to validate
85
86 :returns: True if date is valid
87
88 """
89
90 if isinstance(agreement_id, (list, tuple)):
91 assert len(agreement_id) == 1
92 agreement_id = agreement_id[0]
93 current = self.browse(cr, uid, agreement_id, context=context)
94 now, start, end = self._get_dates(current, context=context)
95 pdate = datetime.strptime(date,
96 DEFAULT_SERVER_DATE_FORMAT)
97 return start <= pdate <= end
98
99 def _get_self(self, cr, uid, ids, context=None):
100 """ Store field function to get current ids
101
102 :returns: list of current ids
103
104 """
105 return ids
106
107 def _compute_state(self, cr, uid, ids, field_name, arg, context=None):
108 """ Compute current state of agreement based on date and consumption
109
110 Please refer to function field documentation for more details.
111
112 """
113 res = {}
114 for agreement in self.browse(cr, uid, ids, context=context):
115 if (agreement.draft or not agreement.start_date or
116 not agreement.end_date):
117 res[agreement.id] = 'draft'
118 continue
119 dates_state = self._check_running_date(cr, agreement,
120 context=context)
121 if dates_state == 'running':
122 if agreement.available_quantity <= 0:
123 res[agreement.id] = 'consumed'
124 else:
125 res[agreement.id] = 'running'
126 else:
127 res[agreement.id] = dates_state
128 return res
129
130 def _search_state(self, cr, uid, obj, name, args, context=None):
131 """Implement search on state function field.
132
133 Only support "and" mode.
134 supported opperators are =, in, not in, <>.
135 For more information please refer to fnct_search OpenERP documentation.
136
137 """
138 if not args:
139 return []
140 ids = self.search(cr, uid, [], context=context)
141 # this can be problematic in term of performace but the
142 # state field can be changed by values and time evolution
143 # In a business point of view there should be around 30 yearly LTA
144
145 found_ids = []
146 res = self.read(cr, uid, ids, ['state'], context=context)
147 for field, operator, value in args:
148 assert field == name
149 if operator == '=':
150 found_ids += [frm['id'] for frm in res if frm['state'] in value]
151 elif operator == 'in' and isinstance(value, list):
152 found_ids += [frm['id'] for frm in res if frm['state'] in value]
153 elif operator in ("!=", "<>"):
154 found_ids += [frm['id'] for frm in res if frm['state'] != value]
155 elif operator == 'not in'and isinstance(value, list):
156 found_ids += [frm['id'] for frm in res if frm['state'] not in value]
157 else:
158 raise NotImplementedError('Search operator %s not implemented'
159 ' for value %s'
160 % (operator, value))
161 to_return = set(found_ids)
162 return [('id', 'in', [x['id'] for x in to_return])]
163
164 def _compute_available_qty(self, cr, uid, ids, field_name, arg,
165 context=None):
166 """Compute available qty of current agreements.
167
168 Consumption is based on confirmed po lines.
169 Please refer to function field documentation for more details.
170
171 """
172 company_id = self._company_get(cr, uid, context=None)
173 res = {}
174 for agreement in self.browse(cr, uid, ids, context=context):
175 sql = """SELECT SUM(po_line.product_qty) FROM purchase_order_line AS po_line
176 LEFT JOIN purchase_order AS po ON po_line.order_id = po.id
177 WHERE po_line.framework_agreement_id = %s
178 AND po_line.product_id = %s
179 AND po.partner_id = %s
180 AND po.state IN %s
181 AND po.company_id = %s"""
182 cr.execute(sql, (agreement.id,
183 agreement.product_id.id,
184 agreement.supplier_id.id,
185 AGR_PO_STATE,
186 company_id))
187 amount = cr.fetchone()[0]
188 if amount is None:
189 amount = 0
190 res[agreement.id] = agreement.quantity - amount
191 return res
192
193 def _get_available_qty(self, cr, uid, ids, field_name, arg, context=None):
194 """Compute available qty of current agreements.
195
196 Consumption is based on confirmed po lines.
197 Please refer to function field documentation for more details.
198
199 """
200 return self._compute_available_qty(cr, uid, ids, field_name, arg,
201 context=context)
202
203 def _get_state(self, cr, uid, ids, field_name, arg, context=None):
204 """ Compute current state of agreement based on date and consumption
205
206 Please refer to function field documentation for more details.
207
208 """
209 return self._compute_state(cr, uid, ids, field_name, arg,
210 context=context)
211
212 def open_agreement(self, cr, uid, ids, context=None):
213 """Open agreement
214
215 Agreement goes from state draft to X
216
217 """
218 if isinstance(ids, (int, long)):
219 ids = [ids]
220 for agr in self.browse(cr, uid, ids, context=context):
221 mandatory = [agr.start_date,
222 agr.end_date,
223 agr.framework_agreement_pricelist_id]
224 if not all(mandatory):
225 raise orm.except_orm(_('Data are missing'),
226 _('Please enter dates'
227 ' and price informations'))
228 self.write(cr, uid, ids, {'draft': False}, context=context)
229
230 def _get_po_store(self, cr, uid, ids, context=None):
231 res = set()
232 agr_obj = self.pool['framework.agreement']
233 po_obj = self.pool['purchase.order']
234 for row in po_obj.browse(cr, uid, ids, context=context):
235 if row.framework_agreement_id:
236 res.update([row.framework_agreement_id.id])
237 else:
238 product_ids = [x.product_id.id for x in row.order_line
239 if x.product_id]
240 f_ids = agr_obj.search(cr, uid,
241 [('product_id', 'in', product_ids)],
242 context=context)
243 res.update(f_ids)
244
245 return res
246
247 def _get_po_line_store(self, cr, uid, ids, context=None):
248 # TODO DRY with _get_po_store
249 res = set()
250 pol_obj = self.pool.get('purchase.order.line')
251 for row in pol_obj.browse(cr, uid, ids, context=context):
252 if row.framework_agreement_id:
253 res.update([row.framework_agreement_id.id])
254 return res
255
256 _store_tuple = (lambda self, cr, uid, ids, c={}: ids, ['quantity'], 10)
257 _po_store_tuple = (_get_po_store, ['framework_agreement_id', 'state'], 20)
258 _po_line_store_tuple = (_get_po_line_store, [], 20)
259
260 _columns = {'name': fields.char('Number',
261 readonly=True),
262 'supplier_id': fields.many2one('res.partner',
263 'Supplier',
264 required=True),
265 'product_id': fields.many2one('product.product',
266 'Product',
267 required=True),
268 'origin': fields.char('Origin'),
269 'start_date': fields.date('Begin of Agreement'),
270 'end_date': fields.date('End of Agreement'),
271 'delay': fields.integer('Lead time in days'),
272 'quantity': fields.integer('Negociated quantity',
273 required=True),
274 'framework_agreement_pricelist_ids': fields.one2many('framework.agreement.pricelist',
275 'framework_agreement_id',
276 'Price lists'),
277 'available_quantity': fields.function(_get_available_qty,
278 type='integer',
279 string='Available quantity',
280 readonly=True,
281 store={'framework.agreement': _store_tuple,
282 'purchase.order': _po_store_tuple,
283 'purchase.order.line': _po_line_store_tuple}),
284 'state': fields.function(_get_state,
285 fnct_search=_search_state,
286 string='state',
287 type='selection',
288 selection=[('draft', 'Draft'),
289 ('future', 'Future'),
290 ('running', 'Running'),
291 ('consumed', 'Consumed'),
292 ('closed', 'Closed')],
293 readonly=True),
294 'company_id': fields.many2one('res.company',
295 'Company'),
296 'draft': fields.boolean('Is draft'),
297 }
298
299 def _company_get(self, cr, uid, context=None):
300 return self.pool['res.company']._company_default_get(cr, uid,
301 'framework.agreement',
302 context=context)
303
304 def create(self, cr, uid, vals, context=None):
305 """We want to have increment sequence only at creation
306
307 When set by a default in a o2m form default consume sequence.
308 But we do not want to use no_gap sequence
309
310 """
311 vals['name'] = self.pool['ir.sequence'].next_by_code(cr, uid,
312 'framework.agreement')
313 return super(framework_agreement, self).create(cr, uid, vals,
314 context=context)
315
316 def _check_overlap(self, cr, uid, ids, context=None):
317 """Constraint to check that no agreements for same product/supplier overlap.
318
319 One agreement per product limit is checked if one_agreement_per_product
320 is set to True on company
321
322 """
323 comp_obj = self.pool['res.company']
324 company_id = self._company_get(cr, uid, context=context)
325 strict = comp_obj.read(cr, uid, company_id,
326 ['one_agreement_per_product'],
327 context=context)['one_agreement_per_product']
328 for agreement in self.browse(cr, uid, ids, context=context):
329 # we do not add current id in domain for readability reasons
330 # indent is not PEP8 compliant but more readable.
331 overlap = self.search(cr, uid,
332 ['&',
333 ('draft', '=', False),
334 ('product_id', '=', agreement.product_id.id),
335 '|',
336 '&',
337 ('start_date', '>=', agreement.start_date),
338 ('start_date', '<=', agreement.end_date),
339 '&',
340 ('end_date', '>=', agreement.start_date),
341 ('end_date', '<=', agreement.end_date),
342 ])
343 # we also look for the one that includes current offer
344 overlap += self.search(cr, uid, [('start_date', '<=', agreement.start_date),
345 ('end_date', '>=', agreement.end_date),
346 ('id', '!=', agreement.id),
347 ('product_id', '=', agreement.product_id.id)])
348 overlap = self.browse(cr, uid,
349 [x for x in overlap if x != agreement.id],
350 context=context)
351 # we ensure that there is only one agreement at time per product
352 # if strict agreement is set on company
353 if strict and overlap:
354 return False
355 # We ensure that there are not multiple agreements for same supplier at same time
356 if any((x.supplier_id.id == agreement.supplier_id.id) for x in overlap):
357 return False
358 return True
359
360 def check_overlap(self, cr, uid, ids, context=None):
361 """Constraint to check that no agreements for same product/supplier overlap.
362
363 One agreement per product limit is checked if one_agreement_per_product
364 is set to True on company
365
366 """
367 return self._check_overlap(cr, uid, ids, context=context)
368
369 _defaults = {'company_id': _company_get,
370 'draft': True}
371
372 _sql_constraints = [('date_priority',
373 'check(start_date < end_date)',
374 'Start/end date inversion')]
375
376 _constraints = [(check_overlap,
377 "You can not have overlapping dates for same supplier and product",
378 ('start_date', 'end_date'))]
379
380 def get_all_product_agreements(self, cr, uid, product_id, lookup_dt, qty=None, context=None):
381 """Get the all the active agreement of a given product at a given date
382
383 :param product_id: product id of the product
384 :param lookup_dt: date string of the lookup date
385 :param qty: quantity that should be available if parameter is
386 passed and qty is insuffisant no agreement would be returned
387
388 :returns: a list of corresponding agreements or None
389
390 """
391 search_args = [('product_id', '=', product_id),
392 ('start_date', '<=', lookup_dt),
393 ('end_date', '>=', lookup_dt),
394 ('draft', '=', False)]
395 if qty:
396 search_args.append(('available_quantity', '>=', qty))
397 agreement_ids = self.search(cr, uid, search_args)
398 if agreement_ids:
399 return self.browse(cr, uid, agreement_ids, context=context)
400 return None
401
402 def get_cheapest_agreement_for_qty(self, cr, uid, product_id, date, qty,
403 currency=None, context=None):
404 """Return the cheapest agreement that has enough available qty.
405
406 If not enough quantity fallback on the cheapest agreement available
407 for quantity.
408
409 :param product_id:
410 :param date:
411 :param qty:
412 :param currency: currency record to make price convertion
413
414 returns (cheapest agreement, enough qty)
415
416 """
417 Cheapest = namedtuple('Cheapest', ['cheapest_agreement', 'enough'])
418 agreements = self.get_all_product_agreements(cr, uid, product_id,
419 date, qty, context=context)
420 if not agreements:
421 return Cheapest(None, None)
422 agreements.sort(key=lambda x: x.get_price(qty, currency=currency))
423 enough = True
424 cheapest_agreement = None
425 for agr in agreements:
426 if agr.available_quantity >= qty:
427 cheapest_agreement = agr
428 break
429 if not cheapest_agreement:
430 cheapest_agreement = agreements[0]
431 enough = False
432 return Cheapest(cheapest_agreement, enough)
433
434 def get_product_agreement(self, cr, uid, product_id, supplier_id,
435 lookup_dt, qty=None, context=None):
436 """Get the matching agreement for a given product/supplier at date
437 :param product_id: product id of the product
438 :param supplier_id: supplier to look for agreement
439 :param lookup_dt: date string of the lookup date
440 :param qty: quantity that should be available if parameter is
441 passed and qty is insuffisant no aggrement would be returned
442
443 :returns: a corresponding agreement or None
444
445 """
446 search_args = [('product_id', '=', product_id),
447 ('supplier_id', '=', supplier_id),
448 ('start_date', '<=', lookup_dt),
449 ('end_date', '>=', lookup_dt),
450 ('draft', '=', False)]
451 if qty:
452 search_args.append(('available_quantity', '>=', qty))
453 agreement_ids = self.search(cr, uid, search_args)
454 if len(agreement_ids) > 1:
455 raise except_orm(_('Many agreements found for the product with id %s'
456 ' at date %s') % (product_id, lookup_dt),
457 _('Please contact your ERP administrator'))
458 if agreement_ids:
459 agreement = self.browse(cr, uid, agreement_ids[0], context=context)
460 return agreement
461 return None
462
463 def has_currency(self, cr, uid, agr_id, currency, context=None):
464 """Predicate that check that agreement has a given currency pricelist
465
466 :returns: boolean (True if a price list in given currency is present)
467
468 """
469 if isinstance(agr_id, (list, tuple)):
470 assert len(agr_id) == 1
471 agr_id = agr_id[0]
472 agreement = self.browse(cr, uid, agr_id, context=context)
473 plists = agreement.framework_agreement_pricelist_ids
474 return any(x for x in plists if x.currency_id == currency)
475
476 def _get_pricelist_lines(self, cr, uid, agreement,
477 currency, context=None):
478 plists = agreement.framework_agreement_pricelist_ids
479 # we do not use has_agreement for performance reason
480 # Python cookbook idiom
481 plist = next((x for x in plists if x.currency_id == currency), None)
482 if not plist:
483 raise orm.except_orm(_('Missing Agreement price list'),
484 _('Please set a price list in currency %s for agreement %s') %
485 (currency.name, agreement.name))
486 return plist.framework_agreement_line_ids
487
488 def get_price(self, cr, uid, agreement_id, qty=0,
489 currency=None, context=None):
490 """Return price negociated for quantity
491
492 :param currency: currency record
493 :param qty: qty to lookup
494
495
496 :returns: price float
497
498 """
499 if isinstance(agreement_id, list):
500 assert len(agreement_id) == 1
501 agreement_id = agreement_id[0]
502 current = self.browse(cr, uid, agreement_id, context=context)
503 if not currency:
504 comp_obj = self.pool['res.company']
505 comp_id = self._company_get(cr, uid, context=context)
506 currency = comp_obj.browse(cr, uid, comp_id, context=context).currency_id
507 lines = self._get_pricelist_lines(cr, uid, current, currency,
508 context=context)
509 lines.sort(key=attrgetter('quantity'), reverse=True)
510 for line in lines:
511 if qty >= line.quantity:
512 return line.price
513 return lines[-1].price
514
515 def _get_currency(self, cr, uid, supplier_id, pricelist_id, context=None):
516 """Helper to retrieve correct currency.
517
518 It will look for currency on supplied pricelist if availwichable
519 else it will look for partner pricelist currency
520
521 :param supplier_id: supplier of agreement
522 :param pricelist_id: primary price list
523
524 :returns: currency browse record
525
526 """
527
528 plist_obj = self.pool['product.pricelist']
529 partner_obj = self.pool['res.partner']
530 if pricelist_id:
531 plist = plist_obj.browse(cr, uid, pricelist_id, context=context)
532 return plist.currency_id
533 partner = partner_obj.browse(cr, uid, supplier_id, context=context)
534 if not partner.property_product_pricelist_purchase:
535 raise orm.except_orm(_('No pricelist found'),
536 _('Please set a pricelist on PO or supplier %s') % partner.name)
537 return partner.property_product_pricelist_purchase.currency_id
538
539
540class framework_agreement_pricelist(orm.Model):
541 """Price list container"""
542
543 _name = "framework.agreement.pricelist"
544 _rec_name = 'currency_id'
545 _columns = {'framework_agreement_id': fields.many2one('framework.agreement',
546 'Agreement',
547 required=True),
548 'currency_id': fields.many2one('res.currency',
549 'Currency',
550 required=True),
551 'framework_agreement_line_ids': fields.one2many('framework.agreement.line',
552 'framework_agreement_pricelist_id',
553 'Price lines',
554 required=True)}
555
556
557class framework_agreement_line(orm.Model):
558 """Price list line of framework agreement
559 that contains price and qty"""
560
561 _name = 'framework.agreement.line'
562 _description = 'Framework agreement line'
563 _rec_name = "quantity"
564 _order = "quantity"
565
566 _columns = {'framework_agreement_pricelist_id': fields.many2one('framework.agreement.pricelist',
567 'Price list',
568 required=True),
569 'quantity': fields.integer('Quantity',
570 required=True),
571
572 'price': fields.float('Price', 'Negociated price',
573 required=True,
574 digits_compute=dp.get_precision('Product Price'))}
575
576
577class FrameworkAgreementObservable(object):
578 """Base functions for model that have to be (pseudo) observable
579 by framework agreement using OpenERP on_change mechanism"""
580
581 def _currency_get(self, cr, uid, pricelist_id, context=None):
582 return self.pool['product.pricelist'].browse(cr, uid,
583 pricelist_id,
584 context=context).currency_id
585
586 def onchange_price_obs(self, cr, uid, ids, price, agreement_id,
587 currency=None, qty=0, context=None):
588 """Raise a warning if a agreed price is changed on observed object"""
589 if context is None:
590 context = {}
591 if not agreement_id or context.get('no_chained'):
592 return {}
593 agr_obj = self.pool['framework.agreement']
594 agreement = agr_obj.browse(cr, uid, agreement_id, context=context)
595 if agreement.get_price(qty, currency=currency) != price:
596 msg = _("You have set the price to %s \n"
597 " but there is a running agreement"
598 " with price %s") % (price, agreement.get_price(qty, currency=currency))
599 return {'warning': {'title': _('Agreement Warning!'),
600 'message': msg}}
601 return {}
602
603 def onchange_quantity_obs(self, cr, uid, ids, qty, date,
604 product_id, currency=None,
605 supplier_id=None,
606 price_field='price', context=None):
607 """Raise a warning if agreed qty is not sufficient when changed on observed object
608
609 :param qty: requested quantity
610 :param currency: currency to get price
611 :param price field: key on which we should return price
612
613 :returns: on change dict
614
615 """
616 res = {'value': {'framework_agreement_id': False}}
617 agreement, status = self._get_agreement_and_qty_status(cr, uid, ids, qty, date,
618 product_id,
619 supplier_id=supplier_id,
620 currency=currency,
621 context=context)
622 if agreement:
623 res['value'] = {price_field: agreement.get_price(qty, currency=currency),
624 'framework_agreement_id': agreement.id}
625 if status:
626 res['warning'] = {'title': _('Agreement Warning!'),
627 'message': status}
628 return res
629
630 def _get_agreement_and_qty_status(self, cr, uid, ids, qty, date,
631 product_id, supplier_id,
632 currency=None, context=None):
633 """Lookup for agreement and return (matching_agreement, status)
634
635 Agreement or status can be None.
636
637 :param qty: requested quantity
638 :param date: date to look for agreement
639 :param supplier_id: supplier id who has signed an agreement
640 :param product_id: product id to look for an agreement
641 :param price field: key on which we should return price
642
643 :returns: (agreement record, status)
644
645 """
646 FoundAgreement = namedtuple('FoundAgreement', ['Agreement', 'message'])
647 agreement_obj = self.pool['framework.agreement']
648 if supplier_id:
649 agreement = agreement_obj.get_product_agreement(cr, uid, product_id,
650 supplier_id, date,
651 context=context)
652 else:
653 agreement, enough = agreement_obj.get_cheapest_agreement_for_qty(cr,
654 uid,
655 product_id,
656 date,
657 qty,
658 currency=currency,
659 context=context)
660 if agreement is None:
661 return FoundAgreement(None, None)
662 msg = None
663 if agreement.available_quantity < qty:
664 msg = _("You have ask for a quantity of %s \n"
665 " but there is only %s available"
666 " for current agreement") % (qty, agreement.available_quantity)
667 return FoundAgreement(agreement, msg)
668
669 def onchange_product_id_obs(self, cr, uid, ids, qty, date,
670 supplier_id, product_id, pricelist_id=None,
671 currency=None, price_field='price', context=None):
672 """
673 Lookup for agreement corresponding to product or return None.
674
675 It will raise a warning if not enough available qty.
676
677 :param qty: requested quantity
678 :param date: date to look for agreement
679 :param supplier_id: supplier id who has signed an agreement
680 :param pricelist_id: if of prefered pricelist
681 :param product_id: product id to look for an agreement
682 :param price field: key on which we should return price
683
684 :returns: on change dict
685
686 """
687 if context is None:
688 context = {}
689 res = {'value': {'framework_agreement_id': False}}
690 if not supplier_id or not product_id:
691 return res
692 agreement, status = self._get_agreement_and_qty_status(cr, uid, ids, qty, date,
693 product_id,
694 supplier_id=supplier_id,
695 currency=currency,
696 context=context)
697 # agr_obj = self.pool['framework.agreement']
698 # currency = agr_obj._get_currency(cr, uid, supplier_id,
699 # pricelist_id, context=context)
700 if agreement:
701 res['value'] = {price_field: agreement.get_price(qty, currency=currency),
702 'framework_agreement_id': agreement.id}
703 if status:
704 res['warning'] = {'title': _('Agreement Warning!'),
705 'message': status}
706 if not agreement:
707 context['no_chained'] = True
708 return res
709
710 def onchange_agreement_obs(self, cr, uid, ids, agreement_id, qty, date, product_id,
711 supplier_id=None, currency=None, price_field='price',
712 context=None):
713 res = {}
714 if not agreement_id or not product_id:
715 return res
716 agr_obj = self.pool['framework.agreement']
717 agreement = agr_obj.browse(cr, uid, agreement_id, context=context)
718 if not agreement.date_valid(date, context=context):
719 raise orm.except_orm(_('Invalid date'),
720 _('Agreement and purchase date does not match'))
721 if agreement.product_id.id != product_id:
722 raise orm.except_orm(_('User Error'),
723 _('Wrong product for choosen agreement'))
724 if supplier_id and agreement.supplier_id.id != supplier_id:
725 raise orm.except_orm(_('User Error'),
726 _('Wrong supplier for choosen agreement'))
727 res['value'] = {price_field: agreement.get_price(qty, currency=currency)}
728 if qty and agreement.available_quantity < qty:
729 msg = _("You have ask for a quantity of %s \n"
730 " but there is only %s available"
731 " for current agreement") % (qty, agreement.available_quantity)
732 res['warning'] = {'title': _('Agreement Warning!'),
733 'message': msg}
734 return res
0735
=== added file 'framework_agreement/model/pricelist.py'
--- framework_agreement/model/pricelist.py 1970-01-01 00:00:00 +0000
+++ framework_agreement/model/pricelist.py 2014-02-06 10:10:57 +0000
@@ -0,0 +1,80 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Nicolas Bessi
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21from datetime import datetime
22from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
23from openerp.osv import orm, fields
24
25
26class product_pricelist(orm.Model):
27 """Add framework agreement behavior on pricelist"""
28
29 _inherit = "product.pricelist"
30
31 def _plist_is_agreement(self, cr, uid, pricelist_id, context=None):
32 """Check that a price list can be subject to agreement.
33
34 :param pricelist_id: the price list to be validated
35
36 :returns: a boolean (True if agreement is applicable)
37
38 """
39 p_list = self.browse(cr, uid, pricelist_id, context=context)
40 if p_list.type == 'purchase':
41 return True
42 return False
43
44 def price_get(self, cr, uid, ids, prod_id, qty, partner=None, context=None):
45 """Override of price retrival function in order to support framework agreement.
46
47 If it is a supplier price list agrreement will be taken in account
48 and use the price of the agreement if required.
49
50 If there is not enough available qty on agreement, standard price will be used.
51
52 This is mabye a faulty design and we should use on_change override
53
54 """
55 if context is None:
56 context = {}
57 agreement_obj = self.pool['framework.agreement']
58 res = super(product_pricelist, self).price_get(cr, uid, ids, prod_id, qty,
59 partner=partner, context=context)
60 if not partner:
61 return res
62 for pricelist_id in res:
63 if (pricelist_id == 'item_id' or not
64 self._plist_is_agreement(cr, uid, pricelist_id, context=context)):
65 continue
66 now = datetime.strptime(fields.date.today(),
67 DEFAULT_SERVER_DATE_FORMAT)
68 date = context.get('date') or context.get('date_order') or now
69 if context.get('from_agreement_id'):
70 agreement = agreement_obj.browse(cr, uid, context['from_agreement_id'],
71 context=context)
72 else:
73 agreement = agreement_obj.get_product_agreement(cr, uid, prod_id,
74 partner, date,
75 qty=qty, context=context)
76 if agreement is not None:
77 currency = agreement_obj._get_currency(cr, uid, partner, pricelist_id,
78 context=context)
79 res[pricelist_id] = agreement.get_price(qty, currency=currency)
80 return res
081
=== added file 'framework_agreement/model/product.py'
--- framework_agreement/model/product.py 1970-01-01 00:00:00 +0000
+++ framework_agreement/model/product.py 2014-02-06 10:10:57 +0000
@@ -0,0 +1,40 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Nicolas Bessi
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21from openerp.osv import orm, fields
22
23
24class product_product(orm.Model):
25 """Add relation to framework agreement"""
26
27 _inherit = "product.product"
28 _columns = {'framework_agreement_ids': fields.one2many('framework.agreement',
29 'product_id',
30 'Framework Agreements (LTA)')
31 }
32
33 def copy(self, cr, uid, id, default=None, context=None):
34 """Override of copy in order not to copy agreements"""
35 if not default:
36 default = {}
37 default['framework_agreement_ids'] = False
38 return super(product_product, self).copy(cr, uid, id,
39 default=default,
40 context=context)
041
=== added file 'framework_agreement/model/purchase.py'
--- framework_agreement/model/purchase.py 1970-01-01 00:00:00 +0000
+++ framework_agreement/model/purchase.py 2014-02-06 10:10:57 +0000
@@ -0,0 +1,155 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Nicolas Bessi
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21from openerp.osv import orm, fields
22from openerp.tools.translate import _
23from openerp.addons.framework_agreement.model.framework_agreement import FrameworkAgreementObservable
24
25
26class purchase_order_line(orm.Model, FrameworkAgreementObservable):
27 """Add on change on price to raise a warning if line is subject to
28 an agreement"""
29
30 _inherit = "purchase.order.line"
31
32 def _get_po_store(self, cr, uid, ids, context=None):
33 res = set()
34 po_obj = self.pool.get('purchase.order')
35 for row in po_obj.browse(cr, uid, ids, context=context):
36 res.update([x.id for x in row.order_line])
37 return res
38
39 _store_tuple = (_get_po_store, ['framework_agreement_id'], 20)
40
41 _columns = {'framework_agreement_id': fields.related('order_id',
42 'framework_agreement_id',
43 type='many2one',
44 readonly=True,
45 store={'purchase.order': _store_tuple},
46 relation='framework.agreement',
47 string='Agreement')}
48
49 def onchange_price(self, cr, uid, ids, price, agreement_id, qty, pricelist_id,
50 product_id, context=None):
51 """Raise a warning if a agreed price is changed"""
52 if not product_id or not agreement_id:
53 return {}
54 currency = self._currency_get(cr, uid, pricelist_id, context=context)
55 product = self.pool['product.product'].browse(cr, uid, product_id, context=context)
56 if product.type == 'service':
57 return {}
58 return self.onchange_price_obs(cr, uid, ids, price, agreement_id, currency=currency,
59 qty=qty, context=None)
60
61 def onchange_product_id(self, cr, uid, ids, pricelist_id, product_id, qty, uom_id,
62 partner_id, date_order=False, fiscal_position_id=False,
63 date_planned=False, name=False, price_unit=False,
64 context=None, agreement_id=False, **kwargs):
65 """ We override this function to check qty change (I know...)
66
67 The price retrieval is managed by the override of product.pricelist.price_get
68 that is overidden to support agreement.
69 This is mabye a faulty design as it has a low level impact
70
71 """
72 # rock n'roll
73 if context is None:
74 context = {}
75 if agreement_id:
76 context['from_agreement_id'] = agreement_id
77 res = super(purchase_order_line, self).onchange_product_id(
78 cr, uid, ids, pricelist_id, product_id, qty, uom_id,
79 partner_id, date_order=date_order, fiscal_position_id=fiscal_position_id,
80 date_planned=date_planned, name=name, price_unit=price_unit, context=context, **kwargs)
81 if not product_id or not agreement_id:
82 return res
83 product = self.pool['product.product'].browse(cr, uid, product_id, context=context)
84 if product.type != 'service' and agreement_id:
85 agreement = self.pool['framework.agreement'].browse(cr, uid,
86 agreement_id,
87 context=context)
88 if agreement.product_id.id != product_id:
89 return {'warning': _('Product not in agreement')}
90 currency = self._currency_get(cr, uid, pricelist_id, context=context)
91 res['value']['price_unit'] = agreement.get_price(qty, currency=currency)
92 return res
93
94
95class purchase_order(orm.Model):
96 """Oveeride on change to raise warning"""
97
98 _inherit = "purchase.order"
99
100 _columns = {'framework_agreement_id': fields.many2one('framework.agreement',
101 'Agreement')}
102
103 def onchange_agreement(self, cr, uid, ids, agreement_id, partner_id, date, context=None):
104 res = {}
105 agr_obj = self.pool['framework.agreement']
106 if agreement_id:
107 agreement = agr_obj.browse(cr, uid, agreement_id, context=context)
108 if not agreement.date_valid(date, context=context):
109 raise orm.except_orm(_('Invalid date'),
110 _('Agreement and purchase date does not match'))
111 if agreement.supplier_id.id != partner_id:
112 raise orm.except_orm(_('Invalid agreement'),
113 _('Agreement and supplier does not match'))
114
115 warning = {'title': _('Agreement Warning!'),
116 'message': _('If you change the agreement of this order'
117 ' (and eventually the currency),'
118 ' existing order lines will not be updated.')}
119 res['warning'] = warning
120 return res
121
122 def onchange_pricelist(self, cr, uid, ids, pricelist_id, line_ids, context=None):
123 res = super(purchase_order, self).onchange_pricelist(cr, uid, ids, pricelist_id,
124 context=context)
125 if not pricelist_id or not line_ids:
126 return res
127
128
129 warning = {'title': _('Pricelist Warning!'),
130 'message': _('If you change the pricelist of this order'
131 ' (and eventually the currency),'
132 ' prices of existing order lines will not be updated.')}
133 res['warning'] = warning
134 return res
135
136 def _date_valid(self, cr, uid, agreement_id, date, context=None):
137 """predicate that check that date of invoice is in agreement"""
138 agr_model = self.pool['framework.agreement']
139 return agr_model.date_valid(cr, uid, agreement_id, date, context=context)
140
141 def onchange_date(self, cr, uid, ids, agreement_id, date, context=None):
142 """Check that date is in agreement"""
143 if agreement_id and not self._date_valid(cr, uid, agreement_id, date, context=context):
144 raise orm.except_orm(_('Invalid date'),
145 _('Agreement and purchase date does not match'))
146 return {}
147
148 # no context in original def...
149 def onchange_partner_id(self, cr, uid, ids, partner_id, agreement_id):
150 """Override to ensure that partner can not be changed if agreement"""
151 res = super(purchase_order, self).onchange_partner_id(cr, uid, ids, partner_id)
152 if agreement_id:
153 raise orm.except_orm(_('You can not change supplier'),
154 _('PO is linked to an agreement'))
155 return res
0156
=== added directory 'framework_agreement/security'
=== added file 'framework_agreement/security/ir.model.access.csv'
--- framework_agreement/security/ir.model.access.csv 1970-01-01 00:00:00 +0000
+++ framework_agreement/security/ir.model.access.csv 2014-02-06 10:10:57 +0000
@@ -0,0 +1,5 @@
1id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
2access_agreement,framework.agreement.user,model_framework_agreement,purchase.group_purchase_user,1,0,0,0
3access_agreement,framework.agreement.user,model_framework_agreement,purchase.group_purchase_manager,1,1,1,1
4access_framework_agreement_pricelist,access_framework_agreement_pricelist,model_framework_agreement_pricelist,purchase.group_purchase_manager,1,1,1,1
5access_framework_agreement_line,access_framework_agreement_line,model_framework_agreement_line,purchase.group_purchase_manager,1,1,1,1
0\ No newline at end of file6\ No newline at end of file
17
=== added file 'framework_agreement/security/multicompany.xml'
--- framework_agreement/security/multicompany.xml 1970-01-01 00:00:00 +0000
+++ framework_agreement/security/multicompany.xml 2014-02-06 10:10:57 +0000
@@ -0,0 +1,11 @@
1<?xml version="1.0" encoding="utf-8"?>
2<openerp>
3 <data noupdate="1">
4 <record model="ir.rule" id="framework_agreement_mc_rule">
5 <field name="name">Framework Agreement company rule</field>
6 <field name="model_id" ref="model_framework_agreement"/>
7 <field name="global" eval="True"/>
8 <field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
9 </record>
10 </data>
11</openerp>
012
=== added directory 'framework_agreement/tests'
=== added file 'framework_agreement/tests/__init__.py'
--- framework_agreement/tests/__init__.py 1970-01-01 00:00:00 +0000
+++ framework_agreement/tests/__init__.py 2014-02-06 10:10:57 +0000
@@ -0,0 +1,30 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Nicolas Bessi
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21from . import common
22from . import test_framework_agreement_dates_and_constraints
23from . import test_framework_agreement_consumed_qty
24from . import test_framework_agreement_on_change
25from . import test_framework_agreement_price_list
26
27checks = [test_framework_agreement_dates_and_constraints,
28 test_framework_agreement_consumed_qty,
29 test_framework_agreement_on_change,
30 test_framework_agreement_price_list]
031
=== added file 'framework_agreement/tests/common.py'
--- framework_agreement/tests/common.py 1970-01-01 00:00:00 +0000
+++ framework_agreement/tests/common.py 2014-02-06 10:10:57 +0000
@@ -0,0 +1,97 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Nicolas Bessi
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21from datetime import datetime, timedelta
22from openerp.osv import fields
23from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
24
25
26class BaseAgreementTestMixin(object):
27 """Class that contain common behavior for all agreement related unit test classes.
28
29 We use Mixin because we want to have those behaviors on the various
30 unit test subclasses provided by OpenERP in test common.
31
32 """
33
34 def commonsetUp(self):
35 cr, uid = self.cr, self.uid
36 self.agreement_model = self.registry('framework.agreement')
37 self.agreement_pl_model = self.registry('framework.agreement.pricelist')
38 self.agreement_line_model = self.registry('framework.agreement.line')
39 self.now = datetime.strptime(fields.date.today(),
40 DEFAULT_SERVER_DATE_FORMAT)
41 self.product_id = self.registry('product.product').create(cr, uid,
42 {'name': 'test_1',
43 'type': 'product',
44 'list_price': 10.00})
45 self.supplier_id = self.registry('res.partner').create(cr, uid, {'name': 'toto',
46 'supplier': 'True'})
47
48 def _map_agreement_to_po(self, agreement, delta_days):
49 """Map agreement to dict to be used by PO create"""
50 supplier = agreement.supplier_id
51 add = self.browse_ref('base.res_partner_3')
52 term = supplier.property_supplier_payment_term
53 term = term.id if term else False
54 start_date = datetime.strptime(agreement.start_date, DEFAULT_SERVER_DATE_FORMAT)
55 date = start_date + timedelta(days=delta_days)
56 data = {}
57 data['partner_id'] = supplier.id
58 data['pricelist_id'] = supplier.property_product_pricelist_purchase.id
59 data['dest_address_id'] = add.id
60 data['location_id'] = add.property_stock_customer.id
61 data['payment_term_id'] = term
62 data['origin'] = agreement.name
63 data['date_order'] = date.strftime(DEFAULT_SERVER_DATE_FORMAT)
64 data['name'] = agreement.name
65 data['framework_agreement_id'] = agreement.id
66 return data
67
68 def _map_agreement_to_po_line(self, agreement, qty, order_id):
69 """Map agreement to dict to be used by PO line create"""
70 data = {}
71 supplier = agreement.supplier_id
72 data['product_qty'] = qty
73 data['product_id'] = agreement.product_id.id
74 data['product_uom'] = agreement.product_id.uom_id.id
75 currency = supplier.property_product_pricelist_purchase.currency_id
76 data['price_unit'] = agreement.get_price(qty, currency=currency)
77 data['name'] = agreement.product_id.name
78 data['order_id'] = order_id
79 data['date_planned'] = self.now
80 return data
81
82 def make_po_from_agreement(self, agreement, qty=0, delta_days=1):
83 """Create a purchase order from an agreement
84
85 :param agreement: origin agreement browse record
86 :param qty: qty to be used on po line
87 :delta days: set date of po to agreement start date + delta
88
89 :returns: purchase order browse record
90
91 """
92 cr, uid = self.cr, self.uid
93 po_model = self.registry('purchase.order')
94 po_line_model = self.registry('purchase.order.line')
95 po_id = po_model.create(cr, uid, self._map_agreement_to_po(agreement, delta_days))
96 po_line_model.create(cr, uid, self._map_agreement_to_po_line(agreement, qty, po_id))
97 return po_model.browse(cr, uid, po_id)
098
=== added file 'framework_agreement/tests/test_framework_agreement_consumed_qty.py'
--- framework_agreement/tests/test_framework_agreement_consumed_qty.py 1970-01-01 00:00:00 +0000
+++ framework_agreement/tests/test_framework_agreement_consumed_qty.py 2014-02-06 10:10:57 +0000
@@ -0,0 +1,74 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Nicolas Bessi
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21from datetime import timedelta
22from openerp import netsvc
23from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
24import openerp.tests.common as test_common
25from .common import BaseAgreementTestMixin
26from ..model.framework_agreement import AGR_PO_STATE
27
28
29class TestAvailabeQty(test_common.TransactionCase, BaseAgreementTestMixin):
30 """Test the function fields available_quantity"""
31
32 def setUp(self):
33 """ Create a default agreement"""
34 super(TestAvailabeQty, self).setUp()
35 self.commonsetUp()
36 cr, uid = self.cr, self.uid
37 start_date = self.now + timedelta(days=10)
38 start_date = start_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
39 end_date = self.now + timedelta(days=20)
40 end_date = end_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
41
42 agr_id = self.agreement_model.create(cr, uid,
43 {'supplier_id': self.supplier_id,
44 'product_id': self.product_id,
45 'start_date': start_date,
46 'end_date': end_date,
47 'price': 77,
48 'delay': 5,
49 'quantity': 200})
50 pl_id = self.agreement_pl_model.create(cr, uid,
51 {'framework_agreement_id': agr_id,
52 'currency_id': self.ref('base.EUR')})
53
54 self.agreement_line_model.create(cr, uid,
55 {'framework_agreement_pricelist_id': pl_id,
56 'quantity': 0,
57 'price': 77.0})
58 self.agreement = self.agreement_model.browse(cr, uid, agr_id)
59 self.agreement.open_agreement()
60
61 def test_00_noting_consumed(self):
62 """Test non consumption"""
63 self.assertEqual(self.agreement.available_quantity, 200)
64
65 def test_01_150_consumed(self):
66 """ test consumption of 150 units"""
67 cr, uid = self.cr, self.uid
68 po = self.make_po_from_agreement(self.agreement, qty=150, delta_days=5)
69 wf_service = netsvc.LocalService("workflow")
70 wf_service.trg_validate(uid, 'purchase.order', po.id, 'purchase_confirm', cr)
71 po.refresh()
72 self.assertIn(po.state, AGR_PO_STATE)
73 self.agreement.refresh()
74 self.assertEqual(self.agreement.available_quantity, 50)
075
=== added file 'framework_agreement/tests/test_framework_agreement_dates_and_constraints.py'
--- framework_agreement/tests/test_framework_agreement_dates_and_constraints.py 1970-01-01 00:00:00 +0000
+++ framework_agreement/tests/test_framework_agreement_dates_and_constraints.py 2014-02-06 10:10:57 +0000
@@ -0,0 +1,135 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Nicolas Bessi
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21from datetime import timedelta
22from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
23import openerp.tests.common as test_common
24from .common import BaseAgreementTestMixin
25
26
27class TestAgreementState(test_common.TransactionCase, BaseAgreementTestMixin):
28
29 def setUp(self):
30 super(TestAgreementState, self).setUp()
31 self.commonsetUp()
32
33 def test_00_future(self):
34 """Test state of a future agreement"""
35 cr, uid = self.cr, self.uid
36 start_date = self.now + timedelta(days=10)
37 start_date = start_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
38 end_date = self.now + timedelta(days=20)
39 end_date = end_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
40
41 agr_id = self.agreement_model.create(cr, uid,
42 {'supplier_id': self.supplier_id,
43 'product_id': self.product_id,
44 'start_date': start_date,
45 'end_date': end_date,
46 'delay': 5,
47 'quantity': 20})
48
49 agreement = self.agreement_model.browse(cr, uid, agr_id)
50 agreement.open_agreement()
51 self.assertEqual(agreement.state, 'future')
52
53 def test_01_past(self):
54 """Test state of a past agreement"""
55 cr, uid = self.cr, self.uid
56 start_date = self.now - timedelta(days=20)
57 start_date = start_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
58 end_date = self.now - timedelta(days=10)
59 end_date = end_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
60
61 agr_id = self.agreement_model.create(cr, uid,
62 {'supplier_id': self.supplier_id,
63 'product_id': self.product_id,
64 'start_date': start_date,
65 'end_date': end_date,
66 'delay': 5,
67 'quantity': 20})
68 agreement = self.agreement_model.browse(cr, uid, agr_id)
69 agreement.open_agreement()
70 self.assertEqual(agreement.state, 'closed')
71
72 def test_02_running(self):
73 """Test state of a running agreement"""
74 cr, uid = self.cr, self.uid
75 start_date = self.now - timedelta(days=2)
76 start_date = start_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
77 end_date = self.now + timedelta(days=2)
78 end_date = end_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
79
80 agr_id = self.agreement_model.create(cr, uid,
81 {'supplier_id': self.supplier_id,
82 'product_id': self.product_id,
83 'start_date': start_date,
84 'end_date': end_date,
85 'delay': 5,
86 'quantity': 20})
87 agreement = self.agreement_model.browse(cr, uid, agr_id)
88 agreement.open_agreement()
89 self.assertEqual(agreement.state, 'running')
90
91 def test_03_date_orderconstraint(self):
92 """Test that date order is checked"""
93 cr, uid = self.cr, self.uid
94 start_date = self.now - timedelta(days=40)
95 start_date = start_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
96 end_date = self.now + timedelta(days=30)
97 end_date = end_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
98 with self.assertRaises(Exception) as constraint:
99 self.agreement_model.create(cr, uid,
100 {'supplier_id': self.supplier_id,
101 'product_id': self.product_id,
102 'start_date': end_date,
103 'end_date': start_date,
104 'draft': False,
105 'delay': 5,
106 'quantity': 20})
107
108 def test_04_test_overlapp(self):
109 """Test overlapping agreement for same supplier constraint"""
110 cr, uid = self.cr, self.uid
111 start_date = self.now - timedelta(days=10)
112 start_date = start_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
113 end_date = self.now + timedelta(days=10)
114 end_date = end_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
115 self.agreement_model.create(cr, uid,
116 {'supplier_id': self.supplier_id,
117 'product_id': self.product_id,
118 'start_date': start_date,
119 'end_date': end_date,
120 'draft': False,
121 'delay': 5,
122 'quantity': 20})
123 start_date = self.now - timedelta(days=2)
124 start_date = start_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
125 end_date = self.now + timedelta(days=2)
126 end_date = end_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
127 with self.assertRaises(Exception) as constraint:
128 self.agreement_model.create(cr, uid,
129 {'supplier_id': self.supplier_id,
130 'product_id': self.product_id,
131 'start_date': start_date,
132 'end_date': end_date,
133 'draft': False,
134 'delay': 5,
135 'quantity': 20})
0136
=== added file 'framework_agreement/tests/test_framework_agreement_on_change.py'
--- framework_agreement/tests/test_framework_agreement_on_change.py 1970-01-01 00:00:00 +0000
+++ framework_agreement/tests/test_framework_agreement_on_change.py 2014-02-06 10:10:57 +0000
@@ -0,0 +1,166 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Nicolas Bessi
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21from datetime import timedelta
22from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
23import openerp.tests.common as test_common
24from .common import BaseAgreementTestMixin
25from ..model.framework_agreement import FrameworkAgreementObservable
26
27
28class TestAgreementOnChange(test_common.TransactionCase, BaseAgreementTestMixin):
29 """Test observer on change and purchase order on chnage"""
30
31 def setUp(self):
32 """ Create a default agreement
33 with 3 price line
34 qty 0 price 70
35 qty 200 price 60
36 """
37 super(TestAgreementOnChange, self).setUp()
38 self.commonsetUp()
39 cr, uid = self.cr, self.uid
40 start_date = self.now + timedelta(days=10)
41 start_date = start_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
42 end_date = self.now + timedelta(days=20)
43 end_date = end_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
44 agr_id = self.agreement_model.create(cr, uid,
45 {'supplier_id': self.supplier_id,
46 'product_id': self.product_id,
47 'start_date': start_date,
48 'end_date': end_date,
49 'delay': 5,
50 'draft': False,
51 'quantity': 1500})
52 pl_id = self.agreement_pl_model.create(cr, uid,
53 {'framework_agreement_id': agr_id,
54 'currency_id': self.ref('base.EUR')})
55 self.agreement_line_model.create(cr, uid,
56 {'framework_agreement_pricelist_id': pl_id,
57 'quantity': 0,
58 'price': 70})
59 self.agreement_line_model.create(cr, uid,
60 {'framework_agreement_pricelist_id': pl_id,
61 'quantity': 200,
62 'price': 60})
63 self.agreement = self.agreement_model.browse(cr, uid, agr_id)
64 self.po_line_model = self.registry('purchase.order.line')
65 self.assertTrue(issubclass(type(self.po_line_model),
66 FrameworkAgreementObservable))
67
68 def test_00_observe_price_change(self):
69 """Ensure that on change price observer raise correct warning
70
71 Warning must be rose if there is a running price agreement
72
73 """
74 cr, uid = self.cr, self.uid
75 res = self.po_line_model.onchange_price_obs(cr, uid, False, 20.0,
76 self.agreement.id,
77 currency=self.browse_ref('base.EUR'),
78 qty=100)
79 self.assertTrue(res.get('warning'))
80
81 def test_01_onchange_quantity_obs(self):
82 """Ensure that on change quantity will raise warning or return price"""
83 cr, uid = self.cr, self.uid
84 res = self.po_line_model.onchange_quantity_obs(cr, uid, False, 200.0,
85 self.agreement.start_date,
86 self.agreement.product_id.id,
87 supplier_id=self.agreement.supplier_id.id,
88 currency=self.browse_ref('base.EUR'))
89 self.assertFalse(res.get('warning'))
90 self.assertEqual(res.get('value', {}).get('price'), 60)
91 # test there is a warning if agreement has not enought quantity
92 res = self.po_line_model.onchange_quantity_obs(cr, uid, False, 20000.0,
93 self.agreement.start_date,
94 self.agreement.product_id.id,
95 supplier_id=self.agreement.supplier_id.id,
96 currency=self.browse_ref('base.EUR'))
97 self.assertTrue(res.get('warning'))
98
99 res = self.po_line_model.onchange_quantity_obs(cr, uid, False, 20000.0,
100 self.now.strftime(DEFAULT_SERVER_DATE_FORMAT),
101 self.agreement.product_id.id,
102 supplier_id=self.agreement.supplier_id.id,
103 currency=self.browse_ref('base.EUR'))
104 self.assertFalse(res.get('warning'))
105
106 def test_02_onchange_product_obs(self):
107 """Check that change of product has correct behavior"""
108 cr, uid = self.cr, self.uid
109 res = self.po_line_model.onchange_product_id_obs(cr, uid, False, 180.0,
110 self.agreement.start_date,
111 self.agreement.supplier_id.id,
112 self.agreement.product_id.id)
113 self.assertFalse(res.get('warning'))
114 self.assertEqual(res.get('value', {}).get('price'), 70)
115
116 res = self.po_line_model.onchange_product_id_obs(cr, uid, False, 20000.0,
117 self.agreement.start_date,
118 self.agreement.supplier_id.id,
119 self.agreement.product_id.id)
120 self.assertTrue(res.get('warning'))
121 self.assertEqual(res.get('value', {}).get('price'), 60)
122
123 res = self.po_line_model.onchange_product_id_obs(cr, uid, False, 20000.0,
124 self.now.strftime(DEFAULT_SERVER_DATE_FORMAT),
125 self.agreement.supplier_id.id,
126 self.agreement.product_id.id)
127 self.assertFalse(res.get('warning'))
128
129 # we do the test on non agreement product
130
131 res = self.po_line_model.onchange_product_id_obs(cr, uid, False, 20000.0,
132 self.agreement.start_date,
133 self.ref('product.product_product_33'),
134 self.agreement.product_id.id)
135 self.assertEqual(res, {'value': {'framework_agreement_id': False}})
136
137 def test_03_price_observer_bindings(self):
138 """Check that change of price has correct behavior"""
139 cr, uid = self.cr, self.uid
140 plist = self.agreement.supplier_id.property_product_pricelist_purchase
141 res = self.po_line_model.onchange_price(cr, uid, False, 20.0,
142 self.agreement.id,
143 200,
144 plist.id,
145 self.agreement.product_id.id)
146 self.assertTrue(res.get('warning'))
147
148 def test_04_product_observer_bindings(self):
149 """Check that change of product has correct behavior"""
150 cr, uid = self.cr, self.uid
151 pl = self.agreement.supplier_id.property_product_pricelist_purchase.id,
152
153 res = self.po_line_model.onchange_product_id(cr, uid, False,
154 pl,
155 self.agreement.product_id.id,
156 200,
157 self.agreement.product_id.uom_id.id,
158 self.agreement.supplier_id.id,
159 date_order=self.agreement.start_date[0:10],
160 fiscal_position_id=False,
161 date_planned=False,
162 name=False,
163 price_unit=False,
164 context={},
165 agreement_id=self.agreement.id)
166 self.assertFalse(res.get('warning'))
0167
=== added file 'framework_agreement/tests/test_framework_agreement_price_list.py'
--- framework_agreement/tests/test_framework_agreement_price_list.py 1970-01-01 00:00:00 +0000
+++ framework_agreement/tests/test_framework_agreement_price_list.py 2014-02-06 10:10:57 +0000
@@ -0,0 +1,73 @@
1from datetime import timedelta
2from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
3from openerp.osv import orm
4import openerp.tests.common as test_common
5from .common import BaseAgreementTestMixin
6
7
8class TestAgreementPriceList(test_common.TransactionCase, BaseAgreementTestMixin):
9 """Test observer on change and purchase order on chnage"""
10
11 def setUp(self):
12 """ Create a default agreement
13 with 3 price line
14 qty 0 price 70
15 qty 200 price 60
16 qty 500 price 50
17 qty 1000 price 45
18 """
19 super(TestAgreementPriceList, self).setUp()
20 self.commonsetUp()
21 cr, uid = self.cr, self.uid
22 start_date = self.now + timedelta(days=10)
23 start_date = start_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
24 end_date = self.now + timedelta(days=20)
25 end_date = end_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
26 agr_id = self.agreement_model.create(cr, uid,
27 {'supplier_id': self.supplier_id,
28 'product_id': self.product_id,
29 'start_date': start_date,
30 'end_date': end_date,
31 'delay': 5,
32 'draft': False,
33 'quantity': 1500})
34
35 pl_id = self.agreement_pl_model.create(cr, uid,
36 {'framework_agreement_id': agr_id,
37 'currency_id': self.ref('base.EUR')})
38
39 self.agreement_line_model.create(cr, uid,
40 {'framework_agreement_pricelist_id': pl_id,
41 'quantity': 0,
42 'price': 70.0})
43 self.agreement_line_model.create(cr, uid,
44 {'framework_agreement_pricelist_id': pl_id,
45 'quantity': 200,
46 'price': 60.0})
47 self.agreement_line_model.create(cr, uid,
48 {'framework_agreement_pricelist_id': pl_id,
49 'quantity': 500,
50 'price': 50.0})
51 self.agreement_line_model.create(cr, uid,
52 {'framework_agreement_pricelist_id': pl_id,
53 'quantity': 1000,
54 'price': 45.0})
55 self.agreement = self.agreement_model.browse(cr, uid, agr_id)
56
57 def test_00_test_qty(self):
58 """Test if barem retrieval is correct"""
59 self.assertEqual(self.agreement.get_price(0, currency=self.browse_ref('base.EUR')), 70.0)
60 self.assertEqual(self.agreement.get_price(100, currency=self.browse_ref('base.EUR')), 70.0)
61 self.assertEqual(self.agreement.get_price(200, currency=self.browse_ref('base.EUR')), 60.0)
62 self.assertEqual(self.agreement.get_price(210, currency=self.browse_ref('base.EUR')), 60.0)
63 self.assertEqual(self.agreement.get_price(500, currency=self.browse_ref('base.EUR')), 50.0)
64 self.assertEqual(self.agreement.get_price(800, currency=self.browse_ref('base.EUR')), 50.0)
65 self.assertEqual(self.agreement.get_price(999, currency=self.browse_ref('base.EUR')), 50.0)
66 self.assertEqual(self.agreement.get_price(1000, currency=self.browse_ref('base.EUR')), 45.0)
67 self.assertEqual(self.agreement.get_price(10000, currency=self.browse_ref('base.EUR')), 45.0)
68 self.assertEqual(self.agreement.get_price(-10, currency=self.browse_ref('base.EUR')), 70.0)
69
70 def test_01_failed_wrong_currency(self):
71 """Tests that wrong currency raise an exception"""
72 with self.assertRaises(orm.except_orm) as error:
73 self.agreement.get_price(0, currency=self.browse_ref('base.USD'))
074
=== added file 'framework_agreement/utils.py'
--- framework_agreement/utils.py 1970-01-01 00:00:00 +0000
+++ framework_agreement/utils.py 2014-02-06 10:10:57 +0000
@@ -0,0 +1,29 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# Author: Nicolas Bessi
5# Copyright 2013 Camptocamp SA
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU Affero General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Affero General Public License for more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19#
20##############################################################################
21def id_boilerplate(fun):
22 """Ensure that id agrument passed to on change is not a list"""
23 def wrapper(*args, **kwargs):
24 if isinstance(args[3], (list, tuple)):
25 args = list(args)
26 args[3] = args[3][0] if args[3] else False
27 args = tuple(args)
28 return fun(*args, **kwargs)
29 return wrapper
030
=== added directory 'framework_agreement/view'
=== added file 'framework_agreement/view/company_view.xml'
--- framework_agreement/view/company_view.xml 1970-01-01 00:00:00 +0000
+++ framework_agreement/view/company_view.xml 2014-02-06 10:10:57 +0000
@@ -0,0 +1,17 @@
1<?xml version="1.0" encoding="utf-8"?>
2<openerp>
3 <data>
4 <record id="add agreement setting on company" model="ir.ui.view">
5 <field name="name">add agreement setting on company</field>
6 <field name="model">res.company</field>
7 <field name="priority">19</field>
8 <field name="inherit_id" ref="base.view_company_form"/>
9 <field name="arch" type="xml">
10 <xpath expr="//group[@name='logistics_grp']" position="inside">
11 <field name="one_agreement_per_product"/>
12 </xpath>
13 </field>
14 </record>
15
16 </data>
17</openerp>
018
=== added file 'framework_agreement/view/framework_agreement_view.xml'
--- framework_agreement/view/framework_agreement_view.xml 1970-01-01 00:00:00 +0000
+++ framework_agreement/view/framework_agreement_view.xml 2014-02-06 10:10:57 +0000
@@ -0,0 +1,118 @@
1<?xml version="1.0" encoding="utf-8"?>
2<openerp>
3 <data>
4 <record id="framework_agreement_list_view" model="ir.ui.view">
5 <field name="name">framework agreement list view</field>
6 <field name="model">framework.agreement</field>
7 <field name="arch" type="xml">
8 <tree version="7.0" string="Framework Agreement (LTA)"> <!-- editable="bottom" -->
9 <field name="name" />
10 <field name="company_id" groups="base.group_multi_company" widget="selection"/>
11 <field name="product_id"/>
12 <field name="supplier_id"/>
13 <field name="delay"/>
14 <field name="quantity"/>
15 <field name="available_quantity"/>
16 <field name="start_date"/>
17 <field name="end_date"/>
18 <field name="state"/>
19 </tree>
20 </field>
21 </record>
22
23 <record id="framework_agreement_form_view" model="ir.ui.view">
24 <field name="name">framework agreement form</field>
25 <field name="model">framework.agreement</field>
26 <field name="arch" type="xml">
27 <form version="7.0" string="Framework Agreement">
28 <header>
29 <button name="open_agreement"
30 context="{}"
31 string="Open Agreement"
32 type="object"
33 attrs="{'invisible': ['|', ('draft', '=', False), '|', ('start_date', '=', False), '|', ('end_date', '=', False), '|', ('framework_agreement_pricelist_ids', '=', False)]}"/>
34 <field name="state"
35 widget="statusbar"
36 nolabel="1"
37 statusbar_visible="draft,future,running,consumed,closed"
38 statusbar_colors='{"draft":"blue","future": "blue", "closed": "blue", "running": "green", "consumed": "red"}'/>
39 </header>
40 <sheet>
41 <group>
42 <field name="name"/>
43 <field name="origin"/>
44 <field name="draft" invisible="1"/>
45 </group>
46 <group>
47 <group>
48 <field name="company_id" groups="base.group_multi_company" widget="selection"/>
49 <field name="supplier_id" context="{'default_supplier': True, 'default_customer': False}"/>
50 <field name="product_id"/>
51 </group>
52 <group>
53 <field name="delay"/>
54 <field name="quantity"/>
55 <field name="available_quantity"/>
56 </group>
57 </group>
58
59 <group string="Dates">
60 <field name="start_date"
61 required="1"/>
62 <field name="end_date"
63 required="1"/>
64 </group>
65 <notebook>
66 <page string="Negociated price lists" colspan="4">
67 <field name="framework_agreement_pricelist_ids"
68 required="1">
69 <tree type="7.0" string="Price list">
70 <field name="currency_id"/>
71 </tree>
72 <form type="7.0" string="Price list">
73 <group>
74 <field name="currency_id"/>
75 </group>
76 <newline/>
77 <notebook>
78 <page string="Price lines" colspan="4">
79 <field name="framework_agreement_line_ids"
80 nolabel="1">
81 <tree type="7.0"
82 string="Price line"
83 editable="top">
84 <field name="quantity"/>
85 <field name="price"/>
86 </tree>
87 </field>
88 </page>
89 </notebook>
90 </form>
91 </field>
92 </page>
93 </notebook>
94 </sheet>
95 </form>
96 </field>
97 </record>
98
99
100 <record model="ir.actions.act_window" id="action_framework_agreement">
101 <field name="name">Framework Agreement</field>
102 <field name="type">ir.actions.act_window</field>
103 <field name="res_model">framework.agreement</field>
104 <field name="domain"></field>
105 <field name="view_type">form</field>
106 <field name="view_mode">tree,form</field>
107 <field name="view_id" ref="framework_agreement_list_view"/>
108 </record>
109
110
111 <menuitem
112 name="Framework Agreement"
113 parent="purchase.menu_purchase_config_pricelist"
114 action="action_framework_agreement"
115 id="action_framework_agreement_menu"/>
116
117 </data>
118</openerp>
0119
=== added file 'framework_agreement/view/product_view.xml'
--- framework_agreement/view/product_view.xml 1970-01-01 00:00:00 +0000
+++ framework_agreement/view/product_view.xml 2014-02-06 10:10:57 +0000
@@ -0,0 +1,86 @@
1<?xml version="1.0" encoding="utf-8"?>
2<openerp>
3 <data>
4 <record id="agreement in product view" model="ir.ui.view">
5 <field name="name">agreement in product view</field>
6 <field name="model">product.product</field>
7 <field name="inherit_id" ref="product.product_normal_form_view"/>
8 <field name="arch" type="xml">
9 <group string="Purchase" position="after">
10 <group string="Framework agreements (LTA)" colspan="4">
11 <field name="framework_agreement_ids" nolabel="1">
12 <form version="7.0" string="Framework Agreement">
13 <header>
14 <button name="open_agreement"
15 context="{}"
16 string="Open Agreement"
17 type="object"
18 attrs="{'invisible': ['|', ('draft', '=', False), ('start_date', '=', False), ('end_date', '=', False), ('framework_agreement_pricelist_ids', '=', False)]}"/>
19 <field name="state"
20 widget="statusbar"
21 nolabel="1"
22 statusbar_visible="draft,future,running,consumed,closed"
23 statusbar_colors='{"draft":"blue","future": "blue", "closed": "blue", "running": "green", "consumed": "red"}'/>
24 </header>
25 <sheet>
26 <group>
27 <field name="name"/>
28 <field name="draft" invisible="1"/>
29 <field name="origin"/>
30 </group>
31 <group>
32 <group>
33 <field name="company_id" groups="base.group_multi_company" widget="selection"/>
34 <field name="supplier_id" context="{'default_supplier': True, 'default_customer': False}"/>
35 </group>
36 <group>
37 <field name="delay"/>
38 <field name="quantity"/>
39 <field name="available_quantity"/>
40 </group>
41 </group>
42
43 <group string="Dates">
44 <field name="start_date"
45 required="1"/>
46 <field name="end_date"
47 required="1"/>
48 </group>
49 <notebook>
50 <page string="Negociated price lists" colspan="4">
51 <field name="framework_agreement_pricelist_ids"
52 attrs="{'required': [('draft', '=', False)]}">
53 <tree type="7.0" string="Price list">
54 <field name="currency_id"/>
55 </tree>
56 <form type="7.0" string="Price list">
57 <group>
58 <field name="currency_id"/>
59 </group>
60 <newline/>
61 <notebook>
62 <page string="Price lines" colspan="4">
63 <field name="framework_agreement_line_ids"
64 nolabel="1">
65 <tree type="7.0"
66 string="Price line"
67 editable="top">
68 <field name="quantity"/>
69 <field name="price"/>
70 </tree>
71 </field>
72 </page>
73 </notebook>
74 </form>
75 </field>
76 </page>
77 </notebook>
78 </sheet>
79 </form>
80 </field>
81 </group>
82 </group>
83 </field>
84 </record>
85 </data>
86</openerp>
087
=== added file 'framework_agreement/view/purchase_view.xml'
--- framework_agreement/view/purchase_view.xml 1970-01-01 00:00:00 +0000
+++ framework_agreement/view/purchase_view.xml 2014-02-06 10:10:57 +0000
@@ -0,0 +1,51 @@
1<?xml version="1.0" encoding="utf-8"?>
2<openerp>
3 <data>
4 <record id="add_onchange_on_pruchase_order_form" model="ir.ui.view">
5 <field name="name">add onchange on pruchase form</field>
6 <field name="model">purchase.order</field>
7 <field name="inherit_id" ref="purchase.purchase_order_form" />
8 <field name="arch" type="xml">
9 <field name="price_unit" position="attributes">
10 <attribute name="on_change">onchange_price(price_unit, parent.framework_agreement_id, product_qty, parent.pricelist_id, product_id)</attribute>
11 </field>
12 <field name="pricelist_id" position="after">
13 <field name="framework_agreement_id"
14 domain="[('draft', '=', False)]"
15 on_change="onchange_agreement(framework_agreement_id, partner_id, date_order)"/>
16 </field>
17 <field name="pricelist_id"
18 position="attributes">
19 <attribute name="on_change">onchange_pricelist(pricelist_id, order_line)</attribute>
20 </field>
21 <field name="date_order"
22 position="attributes">
23 <attribute name="on_change">onchange_date(framework_agreement_id, date_order)</attribute>
24 </field>
25 <field name="partner_id"
26 position="attributes">
27 <attribute name="on_change">onchange_partner_id(partner_id, framework_agreement_id)</attribute>
28 </field>
29 <field name="product_id"
30 position="attributes">
31 <attribute name="on_change">onchange_product_id(parent.pricelist_id,product_id,product_qty,product_uom,parent.partner_id,parent.date_order,parent.fiscal_position,date_planned,name,price_unit,context,parent.framework_agreement_id)</attribute>
32 </field>
33 <field name="product_qty"
34 position="attributes">
35 <attribute name="on_change">onchange_product_id(parent.pricelist_id,product_id,product_qty,product_uom,parent.partner_id,parent.date_order,parent.fiscal_position,date_planned,name,price_unit,context,parent.framework_agreement_id)</attribute>
36 </field>
37 </field>
38 </record>
39
40 <record id="add_onchange_on_pruchase_order_line_form_standalone" model="ir.ui.view">
41 <field name="name">add onchange on pruchase order line form standalone</field>
42 <field name="model">purchase.order.line</field>
43 <field name="inherit_id" ref="purchase.purchase_order_line_form" />
44 <field name="arch" type="xml">
45 <field name="price_unit" position="attributes">
46 <attribute name="on_change">on_change="onchange_price(price_unit, parent.framework_agreement_id, product_qty, parent.pricelist_id)</attribute>
47 </field>
48 </field>
49 </record>
50 </data>
51</openerp>

Subscribers

People subscribed via source and target branches