Merge lp:~camptocamp/purchase-wkfl/7.0-add_framework_agreement-nbi into lp:~purchase-core-editors/purchase-wkfl/7.0
- 7.0-add_framework_agreement-nbi
- Merge into 7.0
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 |
Related bugs: |
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 |
Commit message
Description of the change
Add framework agreement
- 33. By Nicolas Bessi - Camptocamp
-
[FIX] PO onchange
- 34. By Nicolas Bessi - Camptocamp
-
[FIX] sequence.get is deprecated
Romain Deheele - Camptocamp (romaindeheele) wrote : | # |
- 35. By Nicolas Bessi - Camptocamp
-
[RM] forgotten pdb
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
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,
Romain Deheele - Camptocamp (romaindeheele) wrote : | # |
LGTM,
Regards,
Romain
Preview Diff
1 | === added directory 'framework_agreement' |
2 | === added file 'framework_agreement/__init__.py' |
3 | --- framework_agreement/__init__.py 1970-01-01 00:00:00 +0000 |
4 | +++ framework_agreement/__init__.py 2014-02-06 10:10:57 +0000 |
5 | @@ -0,0 +1,22 @@ |
6 | +# -*- coding: utf-8 -*- |
7 | +############################################################################## |
8 | +# |
9 | +# Author: Nicolas Bessi |
10 | +# Copyright 2013 Camptocamp SA |
11 | +# |
12 | +# This program is free software: you can redistribute it and/or modify |
13 | +# it under the terms of the GNU Affero General Public License as |
14 | +# published by the Free Software Foundation, either version 3 of the |
15 | +# License, or (at your option) any later version. |
16 | +# |
17 | +# This program is distributed in the hope that it will be useful, |
18 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
19 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
20 | +# GNU Affero General Public License for more details. |
21 | +# |
22 | +# You should have received a copy of the GNU Affero General Public License |
23 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
24 | +# |
25 | +############################################################################## |
26 | +from . import model |
27 | +from . import utils |
28 | |
29 | === added file 'framework_agreement/__openerp__.py' |
30 | --- framework_agreement/__openerp__.py 1970-01-01 00:00:00 +0000 |
31 | +++ framework_agreement/__openerp__.py 2014-02-06 10:10:57 +0000 |
32 | @@ -0,0 +1,67 @@ |
33 | +# -*- coding: utf-8 -*- |
34 | +############################################################################## |
35 | +# |
36 | +# Author: Nicolas Bessi |
37 | +# Copyright 2013 Camptocamp SA |
38 | +# |
39 | +# This program is free software: you can redistribute it and/or modify |
40 | +# it under the terms of the GNU Affero General Public License as |
41 | +# published by the Free Software Foundation, either version 3 of the |
42 | +# License, or (at your option) any later version. |
43 | +# |
44 | +# This program is distributed in the hope that it will be useful, |
45 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
46 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
47 | +# GNU Affero General Public License for more details. |
48 | +# |
49 | +# You should have received a copy of the GNU Affero General Public License |
50 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
51 | +# |
52 | +############################################################################## |
53 | +{'name': 'Simple Framework Agreement', |
54 | + 'version': '0.1', |
55 | + 'author': 'Camptocamp', |
56 | + 'maintainer': 'Camptocamp', |
57 | + 'category': 'Purchase Management', |
58 | + 'complexity': 'normal', |
59 | + 'depends': ['stock', 'procurement', 'purchase'], |
60 | + 'description': """ |
61 | +Long Term Agreement (or Framework Agreement) on price. |
62 | +====================================================== |
63 | + |
64 | +Agreements are defined by a product, a date range , a supplier, a price, a lead time |
65 | +and agreed quantity. |
66 | + |
67 | +Agreements are set on a product view or using a menu in the product configuration. |
68 | + |
69 | +There can be only one agreement for the same supplier and product at the same time, even |
70 | +if we may have different prices depending on lead time/qty. |
71 | + |
72 | +There is an option on company to restrict one agreement per product at same time. |
73 | + |
74 | +If an agreement is running its price will be automatically used in PO. |
75 | +A warning will be raised in case of exhaustion of override of agreement price. |
76 | + |
77 | +**Technical aspect** |
78 | + |
79 | +The module provide an observalbe mixin to enable generic on_change management on various model |
80 | +related to agreements. |
81 | + |
82 | +The framework agreement is by default related to purchase order but the addon |
83 | +provides a library to integrate it with any other model easily |
84 | +""", |
85 | + 'website': 'http://www.camptocamp.com', |
86 | + 'data': ['data.xml', |
87 | + 'view/product_view.xml', |
88 | + 'view/framework_agreement_view.xml', |
89 | + 'view/purchase_view.xml', |
90 | + 'view/company_view.xml', |
91 | + 'security/multicompany.xml', |
92 | + 'security/ir.model.access.csv'], |
93 | + 'demo': [], |
94 | + 'test': [], |
95 | + 'installable': True, |
96 | + 'auto_install': False, |
97 | + 'license': 'AGPL-3', |
98 | + 'application': False, |
99 | + } |
100 | |
101 | === added file 'framework_agreement/data.xml' |
102 | --- framework_agreement/data.xml 1970-01-01 00:00:00 +0000 |
103 | +++ framework_agreement/data.xml 2014-02-06 10:10:57 +0000 |
104 | @@ -0,0 +1,19 @@ |
105 | +<?xml version="1.0" encoding="utf-8"?> |
106 | +<openerp> |
107 | + <data> |
108 | + <record id="framework_agreement_sequence_type" model="ir.sequence.type"> |
109 | + <field name="name">Framework Agreement</field> |
110 | + <field name="code">framework.agreement</field> |
111 | + </record> |
112 | + <record id="seq_mrp_repair" model="ir.sequence"> |
113 | + <field name="name">framework.agreement</field> |
114 | + <field name="code">framework.agreement</field> |
115 | + <field name="prefix">LTA</field> |
116 | + </record> |
117 | + |
118 | + <record id="framework_agreement_currency_type" model="res.currency.rate.type"> |
119 | + <field name="name">Framework Agreement</field> |
120 | + </record> |
121 | + |
122 | + </data> |
123 | +</openerp> |
124 | |
125 | === added directory 'framework_agreement/i18n' |
126 | === added directory 'framework_agreement/model' |
127 | === added file 'framework_agreement/model/__init__.py' |
128 | --- framework_agreement/model/__init__.py 1970-01-01 00:00:00 +0000 |
129 | +++ framework_agreement/model/__init__.py 2014-02-06 10:10:57 +0000 |
130 | @@ -0,0 +1,25 @@ |
131 | +# -*- coding: utf-8 -*- |
132 | +############################################################################## |
133 | +# |
134 | +# Author: Nicolas Bessi |
135 | +# Copyright 2013 Camptocamp SA |
136 | +# |
137 | +# This program is free software: you can redistribute it and/or modify |
138 | +# it under the terms of the GNU Affero General Public License as |
139 | +# published by the Free Software Foundation, either version 3 of the |
140 | +# License, or (at your option) any later version. |
141 | +# |
142 | +# This program is distributed in the hope that it will be useful, |
143 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
144 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
145 | +# GNU Affero General Public License for more details. |
146 | +# |
147 | +# You should have received a copy of the GNU Affero General Public License |
148 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
149 | +# |
150 | +############################################################################## |
151 | +from . import pricelist |
152 | +from . import product |
153 | +from . import framework_agreement |
154 | +from . import purchase |
155 | +from . import company |
156 | |
157 | === added file 'framework_agreement/model/company.py' |
158 | --- framework_agreement/model/company.py 1970-01-01 00:00:00 +0000 |
159 | +++ framework_agreement/model/company.py 2014-02-06 10:10:57 +0000 |
160 | @@ -0,0 +1,32 @@ |
161 | +# -*- coding: utf-8 -*- |
162 | +############################################################################## |
163 | +# |
164 | +# Author: Nicolas Bessi |
165 | +# Copyright 2013 Camptocamp SA |
166 | +# |
167 | +# This program is free software: you can redistribute it and/or modify |
168 | +# it under the terms of the GNU Affero General Public License as |
169 | +# published by the Free Software Foundation, either version 3 of the |
170 | +# License, or (at your option) any later version. |
171 | +# |
172 | +# This program is distributed in the hope that it will be useful, |
173 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
174 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
175 | +# GNU Affero General Public License for more details. |
176 | +# |
177 | +# You should have received a copy of the GNU Affero General Public License |
178 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
179 | +# |
180 | +############################################################################## |
181 | +from openerp.osv import orm, fields |
182 | + |
183 | + |
184 | +class res_Company(orm.Model): |
185 | + """Add a field on company""" |
186 | + |
187 | + _inherit = "res.company" |
188 | + _columns = {'one_agreement_per_product': fields.boolean('One agreement per product', |
189 | + help='If checked you can have only' |
190 | + ' one framework agreement ' |
191 | + ' per product at the same time')} |
192 | + # TODO add check on activation deactivation of check box |
193 | |
194 | === added file 'framework_agreement/model/framework_agreement.py' |
195 | --- framework_agreement/model/framework_agreement.py 1970-01-01 00:00:00 +0000 |
196 | +++ framework_agreement/model/framework_agreement.py 2014-02-06 10:10:57 +0000 |
197 | @@ -0,0 +1,734 @@ |
198 | +# -*- coding: utf-8 -*- |
199 | +############################################################################## |
200 | +# |
201 | +# Author: Nicolas Bessi |
202 | +# Copyright 2013 Camptocamp SA |
203 | +# |
204 | +# This program is free software: you can redistribute it and/or modify |
205 | +# it under the terms of the GNU Affero General Public License as |
206 | +# published by the Free Software Foundation, either version 3 of the |
207 | +# License, or (at your option) any later version. |
208 | +# |
209 | +# This program is distributed in the hope that it will be useful, |
210 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
211 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
212 | +# GNU Affero General Public License for more details. |
213 | +# |
214 | +# You should have received a copy of the GNU Affero General Public License |
215 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
216 | +# |
217 | +############################################################################## |
218 | +from operator import attrgetter |
219 | +from collections import namedtuple |
220 | +from datetime import datetime |
221 | +from openerp.osv import orm, fields |
222 | +from openerp.osv.orm import except_orm |
223 | +from openerp.tools import DEFAULT_SERVER_DATE_FORMAT |
224 | +from openerp.tools.translate import _ |
225 | +import openerp.addons.decimal_precision as dp |
226 | + |
227 | +AGR_PO_STATE = ('confirmed', 'approved', |
228 | + 'done', 'except_picking', 'except_invoice') |
229 | + |
230 | + |
231 | +class framework_agreement(orm.Model): |
232 | + """Long term agreement on product price with a supplier""" |
233 | + |
234 | + _name = 'framework.agreement' |
235 | + _description = 'Agreement on price' |
236 | + |
237 | + def _check_running_date(self, cr, agreement, context=None): |
238 | + """ Returns agreement state based on date. |
239 | + |
240 | + Available qty is ignored in this method |
241 | + |
242 | + :param agreement: an agreement record |
243 | + |
244 | + :returns: a string - "running" if now is between, |
245 | + - "future" if agreement is in future, |
246 | + - "closed" if agreement is outdated |
247 | + |
248 | + """ |
249 | + now, start, end = self._get_dates(agreement, context=context) |
250 | + if start > now: |
251 | + return 'future' |
252 | + elif end < now: |
253 | + return 'closed' |
254 | + elif start <= now <= end: |
255 | + return 'running' |
256 | + else: |
257 | + raise ValueError('Agreement start/end dates are incorrect') |
258 | + |
259 | + def _get_dates(self, agreement, context=None): |
260 | + """Return current time, start date and end date of agreement |
261 | + |
262 | + Boiler plate as OpenERP returns string instead of date/time objects... |
263 | + |
264 | + :param agreement: agreement record |
265 | + |
266 | + :returns: (now, start, end) |
267 | + |
268 | + """ |
269 | + AGDates = namedtuple('AGDates', ['now', 'start', 'end']) |
270 | + now = datetime.strptime(fields.date.today(), |
271 | + DEFAULT_SERVER_DATE_FORMAT) |
272 | + start = datetime.strptime(agreement.start_date, |
273 | + DEFAULT_SERVER_DATE_FORMAT) |
274 | + end = datetime.strptime(agreement.end_date, |
275 | + DEFAULT_SERVER_DATE_FORMAT) |
276 | + return AGDates(now, start, end) |
277 | + |
278 | + def date_valid(self, cr, uid, agreement_id, date, context=None): |
279 | + """Predicate that checks that date is in agreement |
280 | + |
281 | + :param date: date to validate |
282 | + |
283 | + :returns: True if date is valid |
284 | + |
285 | + """ |
286 | + |
287 | + if isinstance(agreement_id, (list, tuple)): |
288 | + assert len(agreement_id) == 1 |
289 | + agreement_id = agreement_id[0] |
290 | + current = self.browse(cr, uid, agreement_id, context=context) |
291 | + now, start, end = self._get_dates(current, context=context) |
292 | + pdate = datetime.strptime(date, |
293 | + DEFAULT_SERVER_DATE_FORMAT) |
294 | + return start <= pdate <= end |
295 | + |
296 | + def _get_self(self, cr, uid, ids, context=None): |
297 | + """ Store field function to get current ids |
298 | + |
299 | + :returns: list of current ids |
300 | + |
301 | + """ |
302 | + return ids |
303 | + |
304 | + def _compute_state(self, cr, uid, ids, field_name, arg, context=None): |
305 | + """ Compute current state of agreement based on date and consumption |
306 | + |
307 | + Please refer to function field documentation for more details. |
308 | + |
309 | + """ |
310 | + res = {} |
311 | + for agreement in self.browse(cr, uid, ids, context=context): |
312 | + if (agreement.draft or not agreement.start_date or |
313 | + not agreement.end_date): |
314 | + res[agreement.id] = 'draft' |
315 | + continue |
316 | + dates_state = self._check_running_date(cr, agreement, |
317 | + context=context) |
318 | + if dates_state == 'running': |
319 | + if agreement.available_quantity <= 0: |
320 | + res[agreement.id] = 'consumed' |
321 | + else: |
322 | + res[agreement.id] = 'running' |
323 | + else: |
324 | + res[agreement.id] = dates_state |
325 | + return res |
326 | + |
327 | + def _search_state(self, cr, uid, obj, name, args, context=None): |
328 | + """Implement search on state function field. |
329 | + |
330 | + Only support "and" mode. |
331 | + supported opperators are =, in, not in, <>. |
332 | + For more information please refer to fnct_search OpenERP documentation. |
333 | + |
334 | + """ |
335 | + if not args: |
336 | + return [] |
337 | + ids = self.search(cr, uid, [], context=context) |
338 | + # this can be problematic in term of performace but the |
339 | + # state field can be changed by values and time evolution |
340 | + # In a business point of view there should be around 30 yearly LTA |
341 | + |
342 | + found_ids = [] |
343 | + res = self.read(cr, uid, ids, ['state'], context=context) |
344 | + for field, operator, value in args: |
345 | + assert field == name |
346 | + if operator == '=': |
347 | + found_ids += [frm['id'] for frm in res if frm['state'] in value] |
348 | + elif operator == 'in' and isinstance(value, list): |
349 | + found_ids += [frm['id'] for frm in res if frm['state'] in value] |
350 | + elif operator in ("!=", "<>"): |
351 | + found_ids += [frm['id'] for frm in res if frm['state'] != value] |
352 | + elif operator == 'not in'and isinstance(value, list): |
353 | + found_ids += [frm['id'] for frm in res if frm['state'] not in value] |
354 | + else: |
355 | + raise NotImplementedError('Search operator %s not implemented' |
356 | + ' for value %s' |
357 | + % (operator, value)) |
358 | + to_return = set(found_ids) |
359 | + return [('id', 'in', [x['id'] for x in to_return])] |
360 | + |
361 | + def _compute_available_qty(self, cr, uid, ids, field_name, arg, |
362 | + context=None): |
363 | + """Compute available qty of current agreements. |
364 | + |
365 | + Consumption is based on confirmed po lines. |
366 | + Please refer to function field documentation for more details. |
367 | + |
368 | + """ |
369 | + company_id = self._company_get(cr, uid, context=None) |
370 | + res = {} |
371 | + for agreement in self.browse(cr, uid, ids, context=context): |
372 | + sql = """SELECT SUM(po_line.product_qty) FROM purchase_order_line AS po_line |
373 | + LEFT JOIN purchase_order AS po ON po_line.order_id = po.id |
374 | + WHERE po_line.framework_agreement_id = %s |
375 | + AND po_line.product_id = %s |
376 | + AND po.partner_id = %s |
377 | + AND po.state IN %s |
378 | + AND po.company_id = %s""" |
379 | + cr.execute(sql, (agreement.id, |
380 | + agreement.product_id.id, |
381 | + agreement.supplier_id.id, |
382 | + AGR_PO_STATE, |
383 | + company_id)) |
384 | + amount = cr.fetchone()[0] |
385 | + if amount is None: |
386 | + amount = 0 |
387 | + res[agreement.id] = agreement.quantity - amount |
388 | + return res |
389 | + |
390 | + def _get_available_qty(self, cr, uid, ids, field_name, arg, context=None): |
391 | + """Compute available qty of current agreements. |
392 | + |
393 | + Consumption is based on confirmed po lines. |
394 | + Please refer to function field documentation for more details. |
395 | + |
396 | + """ |
397 | + return self._compute_available_qty(cr, uid, ids, field_name, arg, |
398 | + context=context) |
399 | + |
400 | + def _get_state(self, cr, uid, ids, field_name, arg, context=None): |
401 | + """ Compute current state of agreement based on date and consumption |
402 | + |
403 | + Please refer to function field documentation for more details. |
404 | + |
405 | + """ |
406 | + return self._compute_state(cr, uid, ids, field_name, arg, |
407 | + context=context) |
408 | + |
409 | + def open_agreement(self, cr, uid, ids, context=None): |
410 | + """Open agreement |
411 | + |
412 | + Agreement goes from state draft to X |
413 | + |
414 | + """ |
415 | + if isinstance(ids, (int, long)): |
416 | + ids = [ids] |
417 | + for agr in self.browse(cr, uid, ids, context=context): |
418 | + mandatory = [agr.start_date, |
419 | + agr.end_date, |
420 | + agr.framework_agreement_pricelist_id] |
421 | + if not all(mandatory): |
422 | + raise orm.except_orm(_('Data are missing'), |
423 | + _('Please enter dates' |
424 | + ' and price informations')) |
425 | + self.write(cr, uid, ids, {'draft': False}, context=context) |
426 | + |
427 | + def _get_po_store(self, cr, uid, ids, context=None): |
428 | + res = set() |
429 | + agr_obj = self.pool['framework.agreement'] |
430 | + po_obj = self.pool['purchase.order'] |
431 | + for row in po_obj.browse(cr, uid, ids, context=context): |
432 | + if row.framework_agreement_id: |
433 | + res.update([row.framework_agreement_id.id]) |
434 | + else: |
435 | + product_ids = [x.product_id.id for x in row.order_line |
436 | + if x.product_id] |
437 | + f_ids = agr_obj.search(cr, uid, |
438 | + [('product_id', 'in', product_ids)], |
439 | + context=context) |
440 | + res.update(f_ids) |
441 | + |
442 | + return res |
443 | + |
444 | + def _get_po_line_store(self, cr, uid, ids, context=None): |
445 | + # TODO DRY with _get_po_store |
446 | + res = set() |
447 | + pol_obj = self.pool.get('purchase.order.line') |
448 | + for row in pol_obj.browse(cr, uid, ids, context=context): |
449 | + if row.framework_agreement_id: |
450 | + res.update([row.framework_agreement_id.id]) |
451 | + return res |
452 | + |
453 | + _store_tuple = (lambda self, cr, uid, ids, c={}: ids, ['quantity'], 10) |
454 | + _po_store_tuple = (_get_po_store, ['framework_agreement_id', 'state'], 20) |
455 | + _po_line_store_tuple = (_get_po_line_store, [], 20) |
456 | + |
457 | + _columns = {'name': fields.char('Number', |
458 | + readonly=True), |
459 | + 'supplier_id': fields.many2one('res.partner', |
460 | + 'Supplier', |
461 | + required=True), |
462 | + 'product_id': fields.many2one('product.product', |
463 | + 'Product', |
464 | + required=True), |
465 | + 'origin': fields.char('Origin'), |
466 | + 'start_date': fields.date('Begin of Agreement'), |
467 | + 'end_date': fields.date('End of Agreement'), |
468 | + 'delay': fields.integer('Lead time in days'), |
469 | + 'quantity': fields.integer('Negociated quantity', |
470 | + required=True), |
471 | + 'framework_agreement_pricelist_ids': fields.one2many('framework.agreement.pricelist', |
472 | + 'framework_agreement_id', |
473 | + 'Price lists'), |
474 | + 'available_quantity': fields.function(_get_available_qty, |
475 | + type='integer', |
476 | + string='Available quantity', |
477 | + readonly=True, |
478 | + store={'framework.agreement': _store_tuple, |
479 | + 'purchase.order': _po_store_tuple, |
480 | + 'purchase.order.line': _po_line_store_tuple}), |
481 | + 'state': fields.function(_get_state, |
482 | + fnct_search=_search_state, |
483 | + string='state', |
484 | + type='selection', |
485 | + selection=[('draft', 'Draft'), |
486 | + ('future', 'Future'), |
487 | + ('running', 'Running'), |
488 | + ('consumed', 'Consumed'), |
489 | + ('closed', 'Closed')], |
490 | + readonly=True), |
491 | + 'company_id': fields.many2one('res.company', |
492 | + 'Company'), |
493 | + 'draft': fields.boolean('Is draft'), |
494 | + } |
495 | + |
496 | + def _company_get(self, cr, uid, context=None): |
497 | + return self.pool['res.company']._company_default_get(cr, uid, |
498 | + 'framework.agreement', |
499 | + context=context) |
500 | + |
501 | + def create(self, cr, uid, vals, context=None): |
502 | + """We want to have increment sequence only at creation |
503 | + |
504 | + When set by a default in a o2m form default consume sequence. |
505 | + But we do not want to use no_gap sequence |
506 | + |
507 | + """ |
508 | + vals['name'] = self.pool['ir.sequence'].next_by_code(cr, uid, |
509 | + 'framework.agreement') |
510 | + return super(framework_agreement, self).create(cr, uid, vals, |
511 | + context=context) |
512 | + |
513 | + def _check_overlap(self, cr, uid, ids, context=None): |
514 | + """Constraint to check that no agreements for same product/supplier overlap. |
515 | + |
516 | + One agreement per product limit is checked if one_agreement_per_product |
517 | + is set to True on company |
518 | + |
519 | + """ |
520 | + comp_obj = self.pool['res.company'] |
521 | + company_id = self._company_get(cr, uid, context=context) |
522 | + strict = comp_obj.read(cr, uid, company_id, |
523 | + ['one_agreement_per_product'], |
524 | + context=context)['one_agreement_per_product'] |
525 | + for agreement in self.browse(cr, uid, ids, context=context): |
526 | + # we do not add current id in domain for readability reasons |
527 | + # indent is not PEP8 compliant but more readable. |
528 | + overlap = self.search(cr, uid, |
529 | + ['&', |
530 | + ('draft', '=', False), |
531 | + ('product_id', '=', agreement.product_id.id), |
532 | + '|', |
533 | + '&', |
534 | + ('start_date', '>=', agreement.start_date), |
535 | + ('start_date', '<=', agreement.end_date), |
536 | + '&', |
537 | + ('end_date', '>=', agreement.start_date), |
538 | + ('end_date', '<=', agreement.end_date), |
539 | + ]) |
540 | + # we also look for the one that includes current offer |
541 | + overlap += self.search(cr, uid, [('start_date', '<=', agreement.start_date), |
542 | + ('end_date', '>=', agreement.end_date), |
543 | + ('id', '!=', agreement.id), |
544 | + ('product_id', '=', agreement.product_id.id)]) |
545 | + overlap = self.browse(cr, uid, |
546 | + [x for x in overlap if x != agreement.id], |
547 | + context=context) |
548 | + # we ensure that there is only one agreement at time per product |
549 | + # if strict agreement is set on company |
550 | + if strict and overlap: |
551 | + return False |
552 | + # We ensure that there are not multiple agreements for same supplier at same time |
553 | + if any((x.supplier_id.id == agreement.supplier_id.id) for x in overlap): |
554 | + return False |
555 | + return True |
556 | + |
557 | + def check_overlap(self, cr, uid, ids, context=None): |
558 | + """Constraint to check that no agreements for same product/supplier overlap. |
559 | + |
560 | + One agreement per product limit is checked if one_agreement_per_product |
561 | + is set to True on company |
562 | + |
563 | + """ |
564 | + return self._check_overlap(cr, uid, ids, context=context) |
565 | + |
566 | + _defaults = {'company_id': _company_get, |
567 | + 'draft': True} |
568 | + |
569 | + _sql_constraints = [('date_priority', |
570 | + 'check(start_date < end_date)', |
571 | + 'Start/end date inversion')] |
572 | + |
573 | + _constraints = [(check_overlap, |
574 | + "You can not have overlapping dates for same supplier and product", |
575 | + ('start_date', 'end_date'))] |
576 | + |
577 | + def get_all_product_agreements(self, cr, uid, product_id, lookup_dt, qty=None, context=None): |
578 | + """Get the all the active agreement of a given product at a given date |
579 | + |
580 | + :param product_id: product id of the product |
581 | + :param lookup_dt: date string of the lookup date |
582 | + :param qty: quantity that should be available if parameter is |
583 | + passed and qty is insuffisant no agreement would be returned |
584 | + |
585 | + :returns: a list of corresponding agreements or None |
586 | + |
587 | + """ |
588 | + search_args = [('product_id', '=', product_id), |
589 | + ('start_date', '<=', lookup_dt), |
590 | + ('end_date', '>=', lookup_dt), |
591 | + ('draft', '=', False)] |
592 | + if qty: |
593 | + search_args.append(('available_quantity', '>=', qty)) |
594 | + agreement_ids = self.search(cr, uid, search_args) |
595 | + if agreement_ids: |
596 | + return self.browse(cr, uid, agreement_ids, context=context) |
597 | + return None |
598 | + |
599 | + def get_cheapest_agreement_for_qty(self, cr, uid, product_id, date, qty, |
600 | + currency=None, context=None): |
601 | + """Return the cheapest agreement that has enough available qty. |
602 | + |
603 | + If not enough quantity fallback on the cheapest agreement available |
604 | + for quantity. |
605 | + |
606 | + :param product_id: |
607 | + :param date: |
608 | + :param qty: |
609 | + :param currency: currency record to make price convertion |
610 | + |
611 | + returns (cheapest agreement, enough qty) |
612 | + |
613 | + """ |
614 | + Cheapest = namedtuple('Cheapest', ['cheapest_agreement', 'enough']) |
615 | + agreements = self.get_all_product_agreements(cr, uid, product_id, |
616 | + date, qty, context=context) |
617 | + if not agreements: |
618 | + return Cheapest(None, None) |
619 | + agreements.sort(key=lambda x: x.get_price(qty, currency=currency)) |
620 | + enough = True |
621 | + cheapest_agreement = None |
622 | + for agr in agreements: |
623 | + if agr.available_quantity >= qty: |
624 | + cheapest_agreement = agr |
625 | + break |
626 | + if not cheapest_agreement: |
627 | + cheapest_agreement = agreements[0] |
628 | + enough = False |
629 | + return Cheapest(cheapest_agreement, enough) |
630 | + |
631 | + def get_product_agreement(self, cr, uid, product_id, supplier_id, |
632 | + lookup_dt, qty=None, context=None): |
633 | + """Get the matching agreement for a given product/supplier at date |
634 | + :param product_id: product id of the product |
635 | + :param supplier_id: supplier to look for agreement |
636 | + :param lookup_dt: date string of the lookup date |
637 | + :param qty: quantity that should be available if parameter is |
638 | + passed and qty is insuffisant no aggrement would be returned |
639 | + |
640 | + :returns: a corresponding agreement or None |
641 | + |
642 | + """ |
643 | + search_args = [('product_id', '=', product_id), |
644 | + ('supplier_id', '=', supplier_id), |
645 | + ('start_date', '<=', lookup_dt), |
646 | + ('end_date', '>=', lookup_dt), |
647 | + ('draft', '=', False)] |
648 | + if qty: |
649 | + search_args.append(('available_quantity', '>=', qty)) |
650 | + agreement_ids = self.search(cr, uid, search_args) |
651 | + if len(agreement_ids) > 1: |
652 | + raise except_orm(_('Many agreements found for the product with id %s' |
653 | + ' at date %s') % (product_id, lookup_dt), |
654 | + _('Please contact your ERP administrator')) |
655 | + if agreement_ids: |
656 | + agreement = self.browse(cr, uid, agreement_ids[0], context=context) |
657 | + return agreement |
658 | + return None |
659 | + |
660 | + def has_currency(self, cr, uid, agr_id, currency, context=None): |
661 | + """Predicate that check that agreement has a given currency pricelist |
662 | + |
663 | + :returns: boolean (True if a price list in given currency is present) |
664 | + |
665 | + """ |
666 | + if isinstance(agr_id, (list, tuple)): |
667 | + assert len(agr_id) == 1 |
668 | + agr_id = agr_id[0] |
669 | + agreement = self.browse(cr, uid, agr_id, context=context) |
670 | + plists = agreement.framework_agreement_pricelist_ids |
671 | + return any(x for x in plists if x.currency_id == currency) |
672 | + |
673 | + def _get_pricelist_lines(self, cr, uid, agreement, |
674 | + currency, context=None): |
675 | + plists = agreement.framework_agreement_pricelist_ids |
676 | + # we do not use has_agreement for performance reason |
677 | + # Python cookbook idiom |
678 | + plist = next((x for x in plists if x.currency_id == currency), None) |
679 | + if not plist: |
680 | + raise orm.except_orm(_('Missing Agreement price list'), |
681 | + _('Please set a price list in currency %s for agreement %s') % |
682 | + (currency.name, agreement.name)) |
683 | + return plist.framework_agreement_line_ids |
684 | + |
685 | + def get_price(self, cr, uid, agreement_id, qty=0, |
686 | + currency=None, context=None): |
687 | + """Return price negociated for quantity |
688 | + |
689 | + :param currency: currency record |
690 | + :param qty: qty to lookup |
691 | + |
692 | + |
693 | + :returns: price float |
694 | + |
695 | + """ |
696 | + if isinstance(agreement_id, list): |
697 | + assert len(agreement_id) == 1 |
698 | + agreement_id = agreement_id[0] |
699 | + current = self.browse(cr, uid, agreement_id, context=context) |
700 | + if not currency: |
701 | + comp_obj = self.pool['res.company'] |
702 | + comp_id = self._company_get(cr, uid, context=context) |
703 | + currency = comp_obj.browse(cr, uid, comp_id, context=context).currency_id |
704 | + lines = self._get_pricelist_lines(cr, uid, current, currency, |
705 | + context=context) |
706 | + lines.sort(key=attrgetter('quantity'), reverse=True) |
707 | + for line in lines: |
708 | + if qty >= line.quantity: |
709 | + return line.price |
710 | + return lines[-1].price |
711 | + |
712 | + def _get_currency(self, cr, uid, supplier_id, pricelist_id, context=None): |
713 | + """Helper to retrieve correct currency. |
714 | + |
715 | + It will look for currency on supplied pricelist if availwichable |
716 | + else it will look for partner pricelist currency |
717 | + |
718 | + :param supplier_id: supplier of agreement |
719 | + :param pricelist_id: primary price list |
720 | + |
721 | + :returns: currency browse record |
722 | + |
723 | + """ |
724 | + |
725 | + plist_obj = self.pool['product.pricelist'] |
726 | + partner_obj = self.pool['res.partner'] |
727 | + if pricelist_id: |
728 | + plist = plist_obj.browse(cr, uid, pricelist_id, context=context) |
729 | + return plist.currency_id |
730 | + partner = partner_obj.browse(cr, uid, supplier_id, context=context) |
731 | + if not partner.property_product_pricelist_purchase: |
732 | + raise orm.except_orm(_('No pricelist found'), |
733 | + _('Please set a pricelist on PO or supplier %s') % partner.name) |
734 | + return partner.property_product_pricelist_purchase.currency_id |
735 | + |
736 | + |
737 | +class framework_agreement_pricelist(orm.Model): |
738 | + """Price list container""" |
739 | + |
740 | + _name = "framework.agreement.pricelist" |
741 | + _rec_name = 'currency_id' |
742 | + _columns = {'framework_agreement_id': fields.many2one('framework.agreement', |
743 | + 'Agreement', |
744 | + required=True), |
745 | + 'currency_id': fields.many2one('res.currency', |
746 | + 'Currency', |
747 | + required=True), |
748 | + 'framework_agreement_line_ids': fields.one2many('framework.agreement.line', |
749 | + 'framework_agreement_pricelist_id', |
750 | + 'Price lines', |
751 | + required=True)} |
752 | + |
753 | + |
754 | +class framework_agreement_line(orm.Model): |
755 | + """Price list line of framework agreement |
756 | + that contains price and qty""" |
757 | + |
758 | + _name = 'framework.agreement.line' |
759 | + _description = 'Framework agreement line' |
760 | + _rec_name = "quantity" |
761 | + _order = "quantity" |
762 | + |
763 | + _columns = {'framework_agreement_pricelist_id': fields.many2one('framework.agreement.pricelist', |
764 | + 'Price list', |
765 | + required=True), |
766 | + 'quantity': fields.integer('Quantity', |
767 | + required=True), |
768 | + |
769 | + 'price': fields.float('Price', 'Negociated price', |
770 | + required=True, |
771 | + digits_compute=dp.get_precision('Product Price'))} |
772 | + |
773 | + |
774 | +class FrameworkAgreementObservable(object): |
775 | + """Base functions for model that have to be (pseudo) observable |
776 | + by framework agreement using OpenERP on_change mechanism""" |
777 | + |
778 | + def _currency_get(self, cr, uid, pricelist_id, context=None): |
779 | + return self.pool['product.pricelist'].browse(cr, uid, |
780 | + pricelist_id, |
781 | + context=context).currency_id |
782 | + |
783 | + def onchange_price_obs(self, cr, uid, ids, price, agreement_id, |
784 | + currency=None, qty=0, context=None): |
785 | + """Raise a warning if a agreed price is changed on observed object""" |
786 | + if context is None: |
787 | + context = {} |
788 | + if not agreement_id or context.get('no_chained'): |
789 | + return {} |
790 | + agr_obj = self.pool['framework.agreement'] |
791 | + agreement = agr_obj.browse(cr, uid, agreement_id, context=context) |
792 | + if agreement.get_price(qty, currency=currency) != price: |
793 | + msg = _("You have set the price to %s \n" |
794 | + " but there is a running agreement" |
795 | + " with price %s") % (price, agreement.get_price(qty, currency=currency)) |
796 | + return {'warning': {'title': _('Agreement Warning!'), |
797 | + 'message': msg}} |
798 | + return {} |
799 | + |
800 | + def onchange_quantity_obs(self, cr, uid, ids, qty, date, |
801 | + product_id, currency=None, |
802 | + supplier_id=None, |
803 | + price_field='price', context=None): |
804 | + """Raise a warning if agreed qty is not sufficient when changed on observed object |
805 | + |
806 | + :param qty: requested quantity |
807 | + :param currency: currency to get price |
808 | + :param price field: key on which we should return price |
809 | + |
810 | + :returns: on change dict |
811 | + |
812 | + """ |
813 | + res = {'value': {'framework_agreement_id': False}} |
814 | + agreement, status = self._get_agreement_and_qty_status(cr, uid, ids, qty, date, |
815 | + product_id, |
816 | + supplier_id=supplier_id, |
817 | + currency=currency, |
818 | + context=context) |
819 | + if agreement: |
820 | + res['value'] = {price_field: agreement.get_price(qty, currency=currency), |
821 | + 'framework_agreement_id': agreement.id} |
822 | + if status: |
823 | + res['warning'] = {'title': _('Agreement Warning!'), |
824 | + 'message': status} |
825 | + return res |
826 | + |
827 | + def _get_agreement_and_qty_status(self, cr, uid, ids, qty, date, |
828 | + product_id, supplier_id, |
829 | + currency=None, context=None): |
830 | + """Lookup for agreement and return (matching_agreement, status) |
831 | + |
832 | + Agreement or status can be None. |
833 | + |
834 | + :param qty: requested quantity |
835 | + :param date: date to look for agreement |
836 | + :param supplier_id: supplier id who has signed an agreement |
837 | + :param product_id: product id to look for an agreement |
838 | + :param price field: key on which we should return price |
839 | + |
840 | + :returns: (agreement record, status) |
841 | + |
842 | + """ |
843 | + FoundAgreement = namedtuple('FoundAgreement', ['Agreement', 'message']) |
844 | + agreement_obj = self.pool['framework.agreement'] |
845 | + if supplier_id: |
846 | + agreement = agreement_obj.get_product_agreement(cr, uid, product_id, |
847 | + supplier_id, date, |
848 | + context=context) |
849 | + else: |
850 | + agreement, enough = agreement_obj.get_cheapest_agreement_for_qty(cr, |
851 | + uid, |
852 | + product_id, |
853 | + date, |
854 | + qty, |
855 | + currency=currency, |
856 | + context=context) |
857 | + if agreement is None: |
858 | + return FoundAgreement(None, None) |
859 | + msg = None |
860 | + if agreement.available_quantity < qty: |
861 | + msg = _("You have ask for a quantity of %s \n" |
862 | + " but there is only %s available" |
863 | + " for current agreement") % (qty, agreement.available_quantity) |
864 | + return FoundAgreement(agreement, msg) |
865 | + |
866 | + def onchange_product_id_obs(self, cr, uid, ids, qty, date, |
867 | + supplier_id, product_id, pricelist_id=None, |
868 | + currency=None, price_field='price', context=None): |
869 | + """ |
870 | + Lookup for agreement corresponding to product or return None. |
871 | + |
872 | + It will raise a warning if not enough available qty. |
873 | + |
874 | + :param qty: requested quantity |
875 | + :param date: date to look for agreement |
876 | + :param supplier_id: supplier id who has signed an agreement |
877 | + :param pricelist_id: if of prefered pricelist |
878 | + :param product_id: product id to look for an agreement |
879 | + :param price field: key on which we should return price |
880 | + |
881 | + :returns: on change dict |
882 | + |
883 | + """ |
884 | + if context is None: |
885 | + context = {} |
886 | + res = {'value': {'framework_agreement_id': False}} |
887 | + if not supplier_id or not product_id: |
888 | + return res |
889 | + agreement, status = self._get_agreement_and_qty_status(cr, uid, ids, qty, date, |
890 | + product_id, |
891 | + supplier_id=supplier_id, |
892 | + currency=currency, |
893 | + context=context) |
894 | + # agr_obj = self.pool['framework.agreement'] |
895 | + # currency = agr_obj._get_currency(cr, uid, supplier_id, |
896 | + # pricelist_id, context=context) |
897 | + if agreement: |
898 | + res['value'] = {price_field: agreement.get_price(qty, currency=currency), |
899 | + 'framework_agreement_id': agreement.id} |
900 | + if status: |
901 | + res['warning'] = {'title': _('Agreement Warning!'), |
902 | + 'message': status} |
903 | + if not agreement: |
904 | + context['no_chained'] = True |
905 | + return res |
906 | + |
907 | + def onchange_agreement_obs(self, cr, uid, ids, agreement_id, qty, date, product_id, |
908 | + supplier_id=None, currency=None, price_field='price', |
909 | + context=None): |
910 | + res = {} |
911 | + if not agreement_id or not product_id: |
912 | + return res |
913 | + agr_obj = self.pool['framework.agreement'] |
914 | + agreement = agr_obj.browse(cr, uid, agreement_id, context=context) |
915 | + if not agreement.date_valid(date, context=context): |
916 | + raise orm.except_orm(_('Invalid date'), |
917 | + _('Agreement and purchase date does not match')) |
918 | + if agreement.product_id.id != product_id: |
919 | + raise orm.except_orm(_('User Error'), |
920 | + _('Wrong product for choosen agreement')) |
921 | + if supplier_id and agreement.supplier_id.id != supplier_id: |
922 | + raise orm.except_orm(_('User Error'), |
923 | + _('Wrong supplier for choosen agreement')) |
924 | + res['value'] = {price_field: agreement.get_price(qty, currency=currency)} |
925 | + if qty and agreement.available_quantity < qty: |
926 | + msg = _("You have ask for a quantity of %s \n" |
927 | + " but there is only %s available" |
928 | + " for current agreement") % (qty, agreement.available_quantity) |
929 | + res['warning'] = {'title': _('Agreement Warning!'), |
930 | + 'message': msg} |
931 | + return res |
932 | |
933 | === added file 'framework_agreement/model/pricelist.py' |
934 | --- framework_agreement/model/pricelist.py 1970-01-01 00:00:00 +0000 |
935 | +++ framework_agreement/model/pricelist.py 2014-02-06 10:10:57 +0000 |
936 | @@ -0,0 +1,80 @@ |
937 | +# -*- coding: utf-8 -*- |
938 | +############################################################################## |
939 | +# |
940 | +# Author: Nicolas Bessi |
941 | +# Copyright 2013 Camptocamp SA |
942 | +# |
943 | +# This program is free software: you can redistribute it and/or modify |
944 | +# it under the terms of the GNU Affero General Public License as |
945 | +# published by the Free Software Foundation, either version 3 of the |
946 | +# License, or (at your option) any later version. |
947 | +# |
948 | +# This program is distributed in the hope that it will be useful, |
949 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
950 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
951 | +# GNU Affero General Public License for more details. |
952 | +# |
953 | +# You should have received a copy of the GNU Affero General Public License |
954 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
955 | +# |
956 | +############################################################################## |
957 | +from datetime import datetime |
958 | +from openerp.tools import DEFAULT_SERVER_DATE_FORMAT |
959 | +from openerp.osv import orm, fields |
960 | + |
961 | + |
962 | +class product_pricelist(orm.Model): |
963 | + """Add framework agreement behavior on pricelist""" |
964 | + |
965 | + _inherit = "product.pricelist" |
966 | + |
967 | + def _plist_is_agreement(self, cr, uid, pricelist_id, context=None): |
968 | + """Check that a price list can be subject to agreement. |
969 | + |
970 | + :param pricelist_id: the price list to be validated |
971 | + |
972 | + :returns: a boolean (True if agreement is applicable) |
973 | + |
974 | + """ |
975 | + p_list = self.browse(cr, uid, pricelist_id, context=context) |
976 | + if p_list.type == 'purchase': |
977 | + return True |
978 | + return False |
979 | + |
980 | + def price_get(self, cr, uid, ids, prod_id, qty, partner=None, context=None): |
981 | + """Override of price retrival function in order to support framework agreement. |
982 | + |
983 | + If it is a supplier price list agrreement will be taken in account |
984 | + and use the price of the agreement if required. |
985 | + |
986 | + If there is not enough available qty on agreement, standard price will be used. |
987 | + |
988 | + This is mabye a faulty design and we should use on_change override |
989 | + |
990 | + """ |
991 | + if context is None: |
992 | + context = {} |
993 | + agreement_obj = self.pool['framework.agreement'] |
994 | + res = super(product_pricelist, self).price_get(cr, uid, ids, prod_id, qty, |
995 | + partner=partner, context=context) |
996 | + if not partner: |
997 | + return res |
998 | + for pricelist_id in res: |
999 | + if (pricelist_id == 'item_id' or not |
1000 | + self._plist_is_agreement(cr, uid, pricelist_id, context=context)): |
1001 | + continue |
1002 | + now = datetime.strptime(fields.date.today(), |
1003 | + DEFAULT_SERVER_DATE_FORMAT) |
1004 | + date = context.get('date') or context.get('date_order') or now |
1005 | + if context.get('from_agreement_id'): |
1006 | + agreement = agreement_obj.browse(cr, uid, context['from_agreement_id'], |
1007 | + context=context) |
1008 | + else: |
1009 | + agreement = agreement_obj.get_product_agreement(cr, uid, prod_id, |
1010 | + partner, date, |
1011 | + qty=qty, context=context) |
1012 | + if agreement is not None: |
1013 | + currency = agreement_obj._get_currency(cr, uid, partner, pricelist_id, |
1014 | + context=context) |
1015 | + res[pricelist_id] = agreement.get_price(qty, currency=currency) |
1016 | + return res |
1017 | |
1018 | === added file 'framework_agreement/model/product.py' |
1019 | --- framework_agreement/model/product.py 1970-01-01 00:00:00 +0000 |
1020 | +++ framework_agreement/model/product.py 2014-02-06 10:10:57 +0000 |
1021 | @@ -0,0 +1,40 @@ |
1022 | +# -*- coding: utf-8 -*- |
1023 | +############################################################################## |
1024 | +# |
1025 | +# Author: Nicolas Bessi |
1026 | +# Copyright 2013 Camptocamp SA |
1027 | +# |
1028 | +# This program is free software: you can redistribute it and/or modify |
1029 | +# it under the terms of the GNU Affero General Public License as |
1030 | +# published by the Free Software Foundation, either version 3 of the |
1031 | +# License, or (at your option) any later version. |
1032 | +# |
1033 | +# This program is distributed in the hope that it will be useful, |
1034 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1035 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1036 | +# GNU Affero General Public License for more details. |
1037 | +# |
1038 | +# You should have received a copy of the GNU Affero General Public License |
1039 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1040 | +# |
1041 | +############################################################################## |
1042 | +from openerp.osv import orm, fields |
1043 | + |
1044 | + |
1045 | +class product_product(orm.Model): |
1046 | + """Add relation to framework agreement""" |
1047 | + |
1048 | + _inherit = "product.product" |
1049 | + _columns = {'framework_agreement_ids': fields.one2many('framework.agreement', |
1050 | + 'product_id', |
1051 | + 'Framework Agreements (LTA)') |
1052 | + } |
1053 | + |
1054 | + def copy(self, cr, uid, id, default=None, context=None): |
1055 | + """Override of copy in order not to copy agreements""" |
1056 | + if not default: |
1057 | + default = {} |
1058 | + default['framework_agreement_ids'] = False |
1059 | + return super(product_product, self).copy(cr, uid, id, |
1060 | + default=default, |
1061 | + context=context) |
1062 | |
1063 | === added file 'framework_agreement/model/purchase.py' |
1064 | --- framework_agreement/model/purchase.py 1970-01-01 00:00:00 +0000 |
1065 | +++ framework_agreement/model/purchase.py 2014-02-06 10:10:57 +0000 |
1066 | @@ -0,0 +1,155 @@ |
1067 | +# -*- coding: utf-8 -*- |
1068 | +############################################################################## |
1069 | +# |
1070 | +# Author: Nicolas Bessi |
1071 | +# Copyright 2013 Camptocamp SA |
1072 | +# |
1073 | +# This program is free software: you can redistribute it and/or modify |
1074 | +# it under the terms of the GNU Affero General Public License as |
1075 | +# published by the Free Software Foundation, either version 3 of the |
1076 | +# License, or (at your option) any later version. |
1077 | +# |
1078 | +# This program is distributed in the hope that it will be useful, |
1079 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1080 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1081 | +# GNU Affero General Public License for more details. |
1082 | +# |
1083 | +# You should have received a copy of the GNU Affero General Public License |
1084 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1085 | +# |
1086 | +############################################################################## |
1087 | +from openerp.osv import orm, fields |
1088 | +from openerp.tools.translate import _ |
1089 | +from openerp.addons.framework_agreement.model.framework_agreement import FrameworkAgreementObservable |
1090 | + |
1091 | + |
1092 | +class purchase_order_line(orm.Model, FrameworkAgreementObservable): |
1093 | + """Add on change on price to raise a warning if line is subject to |
1094 | + an agreement""" |
1095 | + |
1096 | + _inherit = "purchase.order.line" |
1097 | + |
1098 | + def _get_po_store(self, cr, uid, ids, context=None): |
1099 | + res = set() |
1100 | + po_obj = self.pool.get('purchase.order') |
1101 | + for row in po_obj.browse(cr, uid, ids, context=context): |
1102 | + res.update([x.id for x in row.order_line]) |
1103 | + return res |
1104 | + |
1105 | + _store_tuple = (_get_po_store, ['framework_agreement_id'], 20) |
1106 | + |
1107 | + _columns = {'framework_agreement_id': fields.related('order_id', |
1108 | + 'framework_agreement_id', |
1109 | + type='many2one', |
1110 | + readonly=True, |
1111 | + store={'purchase.order': _store_tuple}, |
1112 | + relation='framework.agreement', |
1113 | + string='Agreement')} |
1114 | + |
1115 | + def onchange_price(self, cr, uid, ids, price, agreement_id, qty, pricelist_id, |
1116 | + product_id, context=None): |
1117 | + """Raise a warning if a agreed price is changed""" |
1118 | + if not product_id or not agreement_id: |
1119 | + return {} |
1120 | + currency = self._currency_get(cr, uid, pricelist_id, context=context) |
1121 | + product = self.pool['product.product'].browse(cr, uid, product_id, context=context) |
1122 | + if product.type == 'service': |
1123 | + return {} |
1124 | + return self.onchange_price_obs(cr, uid, ids, price, agreement_id, currency=currency, |
1125 | + qty=qty, context=None) |
1126 | + |
1127 | + def onchange_product_id(self, cr, uid, ids, pricelist_id, product_id, qty, uom_id, |
1128 | + partner_id, date_order=False, fiscal_position_id=False, |
1129 | + date_planned=False, name=False, price_unit=False, |
1130 | + context=None, agreement_id=False, **kwargs): |
1131 | + """ We override this function to check qty change (I know...) |
1132 | + |
1133 | + The price retrieval is managed by the override of product.pricelist.price_get |
1134 | + that is overidden to support agreement. |
1135 | + This is mabye a faulty design as it has a low level impact |
1136 | + |
1137 | + """ |
1138 | + # rock n'roll |
1139 | + if context is None: |
1140 | + context = {} |
1141 | + if agreement_id: |
1142 | + context['from_agreement_id'] = agreement_id |
1143 | + res = super(purchase_order_line, self).onchange_product_id( |
1144 | + cr, uid, ids, pricelist_id, product_id, qty, uom_id, |
1145 | + partner_id, date_order=date_order, fiscal_position_id=fiscal_position_id, |
1146 | + date_planned=date_planned, name=name, price_unit=price_unit, context=context, **kwargs) |
1147 | + if not product_id or not agreement_id: |
1148 | + return res |
1149 | + product = self.pool['product.product'].browse(cr, uid, product_id, context=context) |
1150 | + if product.type != 'service' and agreement_id: |
1151 | + agreement = self.pool['framework.agreement'].browse(cr, uid, |
1152 | + agreement_id, |
1153 | + context=context) |
1154 | + if agreement.product_id.id != product_id: |
1155 | + return {'warning': _('Product not in agreement')} |
1156 | + currency = self._currency_get(cr, uid, pricelist_id, context=context) |
1157 | + res['value']['price_unit'] = agreement.get_price(qty, currency=currency) |
1158 | + return res |
1159 | + |
1160 | + |
1161 | +class purchase_order(orm.Model): |
1162 | + """Oveeride on change to raise warning""" |
1163 | + |
1164 | + _inherit = "purchase.order" |
1165 | + |
1166 | + _columns = {'framework_agreement_id': fields.many2one('framework.agreement', |
1167 | + 'Agreement')} |
1168 | + |
1169 | + def onchange_agreement(self, cr, uid, ids, agreement_id, partner_id, date, context=None): |
1170 | + res = {} |
1171 | + agr_obj = self.pool['framework.agreement'] |
1172 | + if agreement_id: |
1173 | + agreement = agr_obj.browse(cr, uid, agreement_id, context=context) |
1174 | + if not agreement.date_valid(date, context=context): |
1175 | + raise orm.except_orm(_('Invalid date'), |
1176 | + _('Agreement and purchase date does not match')) |
1177 | + if agreement.supplier_id.id != partner_id: |
1178 | + raise orm.except_orm(_('Invalid agreement'), |
1179 | + _('Agreement and supplier does not match')) |
1180 | + |
1181 | + warning = {'title': _('Agreement Warning!'), |
1182 | + 'message': _('If you change the agreement of this order' |
1183 | + ' (and eventually the currency),' |
1184 | + ' existing order lines will not be updated.')} |
1185 | + res['warning'] = warning |
1186 | + return res |
1187 | + |
1188 | + def onchange_pricelist(self, cr, uid, ids, pricelist_id, line_ids, context=None): |
1189 | + res = super(purchase_order, self).onchange_pricelist(cr, uid, ids, pricelist_id, |
1190 | + context=context) |
1191 | + if not pricelist_id or not line_ids: |
1192 | + return res |
1193 | + |
1194 | + |
1195 | + warning = {'title': _('Pricelist Warning!'), |
1196 | + 'message': _('If you change the pricelist of this order' |
1197 | + ' (and eventually the currency),' |
1198 | + ' prices of existing order lines will not be updated.')} |
1199 | + res['warning'] = warning |
1200 | + return res |
1201 | + |
1202 | + def _date_valid(self, cr, uid, agreement_id, date, context=None): |
1203 | + """predicate that check that date of invoice is in agreement""" |
1204 | + agr_model = self.pool['framework.agreement'] |
1205 | + return agr_model.date_valid(cr, uid, agreement_id, date, context=context) |
1206 | + |
1207 | + def onchange_date(self, cr, uid, ids, agreement_id, date, context=None): |
1208 | + """Check that date is in agreement""" |
1209 | + if agreement_id and not self._date_valid(cr, uid, agreement_id, date, context=context): |
1210 | + raise orm.except_orm(_('Invalid date'), |
1211 | + _('Agreement and purchase date does not match')) |
1212 | + return {} |
1213 | + |
1214 | + # no context in original def... |
1215 | + def onchange_partner_id(self, cr, uid, ids, partner_id, agreement_id): |
1216 | + """Override to ensure that partner can not be changed if agreement""" |
1217 | + res = super(purchase_order, self).onchange_partner_id(cr, uid, ids, partner_id) |
1218 | + if agreement_id: |
1219 | + raise orm.except_orm(_('You can not change supplier'), |
1220 | + _('PO is linked to an agreement')) |
1221 | + return res |
1222 | |
1223 | === added directory 'framework_agreement/security' |
1224 | === added file 'framework_agreement/security/ir.model.access.csv' |
1225 | --- framework_agreement/security/ir.model.access.csv 1970-01-01 00:00:00 +0000 |
1226 | +++ framework_agreement/security/ir.model.access.csv 2014-02-06 10:10:57 +0000 |
1227 | @@ -0,0 +1,5 @@ |
1228 | +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink |
1229 | +access_agreement,framework.agreement.user,model_framework_agreement,purchase.group_purchase_user,1,0,0,0 |
1230 | +access_agreement,framework.agreement.user,model_framework_agreement,purchase.group_purchase_manager,1,1,1,1 |
1231 | +access_framework_agreement_pricelist,access_framework_agreement_pricelist,model_framework_agreement_pricelist,purchase.group_purchase_manager,1,1,1,1 |
1232 | +access_framework_agreement_line,access_framework_agreement_line,model_framework_agreement_line,purchase.group_purchase_manager,1,1,1,1 |
1233 | \ No newline at end of file |
1234 | |
1235 | === added file 'framework_agreement/security/multicompany.xml' |
1236 | --- framework_agreement/security/multicompany.xml 1970-01-01 00:00:00 +0000 |
1237 | +++ framework_agreement/security/multicompany.xml 2014-02-06 10:10:57 +0000 |
1238 | @@ -0,0 +1,11 @@ |
1239 | +<?xml version="1.0" encoding="utf-8"?> |
1240 | +<openerp> |
1241 | + <data noupdate="1"> |
1242 | + <record model="ir.rule" id="framework_agreement_mc_rule"> |
1243 | + <field name="name">Framework Agreement company rule</field> |
1244 | + <field name="model_id" ref="model_framework_agreement"/> |
1245 | + <field name="global" eval="True"/> |
1246 | + <field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field> |
1247 | + </record> |
1248 | + </data> |
1249 | +</openerp> |
1250 | |
1251 | === added directory 'framework_agreement/tests' |
1252 | === added file 'framework_agreement/tests/__init__.py' |
1253 | --- framework_agreement/tests/__init__.py 1970-01-01 00:00:00 +0000 |
1254 | +++ framework_agreement/tests/__init__.py 2014-02-06 10:10:57 +0000 |
1255 | @@ -0,0 +1,30 @@ |
1256 | +# -*- coding: utf-8 -*- |
1257 | +############################################################################## |
1258 | +# |
1259 | +# Author: Nicolas Bessi |
1260 | +# Copyright 2013 Camptocamp SA |
1261 | +# |
1262 | +# This program is free software: you can redistribute it and/or modify |
1263 | +# it under the terms of the GNU Affero General Public License as |
1264 | +# published by the Free Software Foundation, either version 3 of the |
1265 | +# License, or (at your option) any later version. |
1266 | +# |
1267 | +# This program is distributed in the hope that it will be useful, |
1268 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1269 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1270 | +# GNU Affero General Public License for more details. |
1271 | +# |
1272 | +# You should have received a copy of the GNU Affero General Public License |
1273 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1274 | +# |
1275 | +############################################################################## |
1276 | +from . import common |
1277 | +from . import test_framework_agreement_dates_and_constraints |
1278 | +from . import test_framework_agreement_consumed_qty |
1279 | +from . import test_framework_agreement_on_change |
1280 | +from . import test_framework_agreement_price_list |
1281 | + |
1282 | +checks = [test_framework_agreement_dates_and_constraints, |
1283 | + test_framework_agreement_consumed_qty, |
1284 | + test_framework_agreement_on_change, |
1285 | + test_framework_agreement_price_list] |
1286 | |
1287 | === added file 'framework_agreement/tests/common.py' |
1288 | --- framework_agreement/tests/common.py 1970-01-01 00:00:00 +0000 |
1289 | +++ framework_agreement/tests/common.py 2014-02-06 10:10:57 +0000 |
1290 | @@ -0,0 +1,97 @@ |
1291 | +# -*- coding: utf-8 -*- |
1292 | +############################################################################## |
1293 | +# |
1294 | +# Author: Nicolas Bessi |
1295 | +# Copyright 2013 Camptocamp SA |
1296 | +# |
1297 | +# This program is free software: you can redistribute it and/or modify |
1298 | +# it under the terms of the GNU Affero General Public License as |
1299 | +# published by the Free Software Foundation, either version 3 of the |
1300 | +# License, or (at your option) any later version. |
1301 | +# |
1302 | +# This program is distributed in the hope that it will be useful, |
1303 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1304 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1305 | +# GNU Affero General Public License for more details. |
1306 | +# |
1307 | +# You should have received a copy of the GNU Affero General Public License |
1308 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1309 | +# |
1310 | +############################################################################## |
1311 | +from datetime import datetime, timedelta |
1312 | +from openerp.osv import fields |
1313 | +from openerp.tools import DEFAULT_SERVER_DATE_FORMAT |
1314 | + |
1315 | + |
1316 | +class BaseAgreementTestMixin(object): |
1317 | + """Class that contain common behavior for all agreement related unit test classes. |
1318 | + |
1319 | + We use Mixin because we want to have those behaviors on the various |
1320 | + unit test subclasses provided by OpenERP in test common. |
1321 | + |
1322 | + """ |
1323 | + |
1324 | + def commonsetUp(self): |
1325 | + cr, uid = self.cr, self.uid |
1326 | + self.agreement_model = self.registry('framework.agreement') |
1327 | + self.agreement_pl_model = self.registry('framework.agreement.pricelist') |
1328 | + self.agreement_line_model = self.registry('framework.agreement.line') |
1329 | + self.now = datetime.strptime(fields.date.today(), |
1330 | + DEFAULT_SERVER_DATE_FORMAT) |
1331 | + self.product_id = self.registry('product.product').create(cr, uid, |
1332 | + {'name': 'test_1', |
1333 | + 'type': 'product', |
1334 | + 'list_price': 10.00}) |
1335 | + self.supplier_id = self.registry('res.partner').create(cr, uid, {'name': 'toto', |
1336 | + 'supplier': 'True'}) |
1337 | + |
1338 | + def _map_agreement_to_po(self, agreement, delta_days): |
1339 | + """Map agreement to dict to be used by PO create""" |
1340 | + supplier = agreement.supplier_id |
1341 | + add = self.browse_ref('base.res_partner_3') |
1342 | + term = supplier.property_supplier_payment_term |
1343 | + term = term.id if term else False |
1344 | + start_date = datetime.strptime(agreement.start_date, DEFAULT_SERVER_DATE_FORMAT) |
1345 | + date = start_date + timedelta(days=delta_days) |
1346 | + data = {} |
1347 | + data['partner_id'] = supplier.id |
1348 | + data['pricelist_id'] = supplier.property_product_pricelist_purchase.id |
1349 | + data['dest_address_id'] = add.id |
1350 | + data['location_id'] = add.property_stock_customer.id |
1351 | + data['payment_term_id'] = term |
1352 | + data['origin'] = agreement.name |
1353 | + data['date_order'] = date.strftime(DEFAULT_SERVER_DATE_FORMAT) |
1354 | + data['name'] = agreement.name |
1355 | + data['framework_agreement_id'] = agreement.id |
1356 | + return data |
1357 | + |
1358 | + def _map_agreement_to_po_line(self, agreement, qty, order_id): |
1359 | + """Map agreement to dict to be used by PO line create""" |
1360 | + data = {} |
1361 | + supplier = agreement.supplier_id |
1362 | + data['product_qty'] = qty |
1363 | + data['product_id'] = agreement.product_id.id |
1364 | + data['product_uom'] = agreement.product_id.uom_id.id |
1365 | + currency = supplier.property_product_pricelist_purchase.currency_id |
1366 | + data['price_unit'] = agreement.get_price(qty, currency=currency) |
1367 | + data['name'] = agreement.product_id.name |
1368 | + data['order_id'] = order_id |
1369 | + data['date_planned'] = self.now |
1370 | + return data |
1371 | + |
1372 | + def make_po_from_agreement(self, agreement, qty=0, delta_days=1): |
1373 | + """Create a purchase order from an agreement |
1374 | + |
1375 | + :param agreement: origin agreement browse record |
1376 | + :param qty: qty to be used on po line |
1377 | + :delta days: set date of po to agreement start date + delta |
1378 | + |
1379 | + :returns: purchase order browse record |
1380 | + |
1381 | + """ |
1382 | + cr, uid = self.cr, self.uid |
1383 | + po_model = self.registry('purchase.order') |
1384 | + po_line_model = self.registry('purchase.order.line') |
1385 | + po_id = po_model.create(cr, uid, self._map_agreement_to_po(agreement, delta_days)) |
1386 | + po_line_model.create(cr, uid, self._map_agreement_to_po_line(agreement, qty, po_id)) |
1387 | + return po_model.browse(cr, uid, po_id) |
1388 | |
1389 | === added file 'framework_agreement/tests/test_framework_agreement_consumed_qty.py' |
1390 | --- framework_agreement/tests/test_framework_agreement_consumed_qty.py 1970-01-01 00:00:00 +0000 |
1391 | +++ framework_agreement/tests/test_framework_agreement_consumed_qty.py 2014-02-06 10:10:57 +0000 |
1392 | @@ -0,0 +1,74 @@ |
1393 | +# -*- coding: utf-8 -*- |
1394 | +############################################################################## |
1395 | +# |
1396 | +# Author: Nicolas Bessi |
1397 | +# Copyright 2013 Camptocamp SA |
1398 | +# |
1399 | +# This program is free software: you can redistribute it and/or modify |
1400 | +# it under the terms of the GNU Affero General Public License as |
1401 | +# published by the Free Software Foundation, either version 3 of the |
1402 | +# License, or (at your option) any later version. |
1403 | +# |
1404 | +# This program is distributed in the hope that it will be useful, |
1405 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1406 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1407 | +# GNU Affero General Public License for more details. |
1408 | +# |
1409 | +# You should have received a copy of the GNU Affero General Public License |
1410 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1411 | +# |
1412 | +############################################################################## |
1413 | +from datetime import timedelta |
1414 | +from openerp import netsvc |
1415 | +from openerp.tools import DEFAULT_SERVER_DATE_FORMAT |
1416 | +import openerp.tests.common as test_common |
1417 | +from .common import BaseAgreementTestMixin |
1418 | +from ..model.framework_agreement import AGR_PO_STATE |
1419 | + |
1420 | + |
1421 | +class TestAvailabeQty(test_common.TransactionCase, BaseAgreementTestMixin): |
1422 | + """Test the function fields available_quantity""" |
1423 | + |
1424 | + def setUp(self): |
1425 | + """ Create a default agreement""" |
1426 | + super(TestAvailabeQty, self).setUp() |
1427 | + self.commonsetUp() |
1428 | + cr, uid = self.cr, self.uid |
1429 | + start_date = self.now + timedelta(days=10) |
1430 | + start_date = start_date.strftime(DEFAULT_SERVER_DATE_FORMAT) |
1431 | + end_date = self.now + timedelta(days=20) |
1432 | + end_date = end_date.strftime(DEFAULT_SERVER_DATE_FORMAT) |
1433 | + |
1434 | + agr_id = self.agreement_model.create(cr, uid, |
1435 | + {'supplier_id': self.supplier_id, |
1436 | + 'product_id': self.product_id, |
1437 | + 'start_date': start_date, |
1438 | + 'end_date': end_date, |
1439 | + 'price': 77, |
1440 | + 'delay': 5, |
1441 | + 'quantity': 200}) |
1442 | + pl_id = self.agreement_pl_model.create(cr, uid, |
1443 | + {'framework_agreement_id': agr_id, |
1444 | + 'currency_id': self.ref('base.EUR')}) |
1445 | + |
1446 | + self.agreement_line_model.create(cr, uid, |
1447 | + {'framework_agreement_pricelist_id': pl_id, |
1448 | + 'quantity': 0, |
1449 | + 'price': 77.0}) |
1450 | + self.agreement = self.agreement_model.browse(cr, uid, agr_id) |
1451 | + self.agreement.open_agreement() |
1452 | + |
1453 | + def test_00_noting_consumed(self): |
1454 | + """Test non consumption""" |
1455 | + self.assertEqual(self.agreement.available_quantity, 200) |
1456 | + |
1457 | + def test_01_150_consumed(self): |
1458 | + """ test consumption of 150 units""" |
1459 | + cr, uid = self.cr, self.uid |
1460 | + po = self.make_po_from_agreement(self.agreement, qty=150, delta_days=5) |
1461 | + wf_service = netsvc.LocalService("workflow") |
1462 | + wf_service.trg_validate(uid, 'purchase.order', po.id, 'purchase_confirm', cr) |
1463 | + po.refresh() |
1464 | + self.assertIn(po.state, AGR_PO_STATE) |
1465 | + self.agreement.refresh() |
1466 | + self.assertEqual(self.agreement.available_quantity, 50) |
1467 | |
1468 | === added file 'framework_agreement/tests/test_framework_agreement_dates_and_constraints.py' |
1469 | --- framework_agreement/tests/test_framework_agreement_dates_and_constraints.py 1970-01-01 00:00:00 +0000 |
1470 | +++ framework_agreement/tests/test_framework_agreement_dates_and_constraints.py 2014-02-06 10:10:57 +0000 |
1471 | @@ -0,0 +1,135 @@ |
1472 | +# -*- coding: utf-8 -*- |
1473 | +############################################################################## |
1474 | +# |
1475 | +# Author: Nicolas Bessi |
1476 | +# Copyright 2013 Camptocamp SA |
1477 | +# |
1478 | +# This program is free software: you can redistribute it and/or modify |
1479 | +# it under the terms of the GNU Affero General Public License as |
1480 | +# published by the Free Software Foundation, either version 3 of the |
1481 | +# License, or (at your option) any later version. |
1482 | +# |
1483 | +# This program is distributed in the hope that it will be useful, |
1484 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1485 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1486 | +# GNU Affero General Public License for more details. |
1487 | +# |
1488 | +# You should have received a copy of the GNU Affero General Public License |
1489 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1490 | +# |
1491 | +############################################################################## |
1492 | +from datetime import timedelta |
1493 | +from openerp.tools import DEFAULT_SERVER_DATE_FORMAT |
1494 | +import openerp.tests.common as test_common |
1495 | +from .common import BaseAgreementTestMixin |
1496 | + |
1497 | + |
1498 | +class TestAgreementState(test_common.TransactionCase, BaseAgreementTestMixin): |
1499 | + |
1500 | + def setUp(self): |
1501 | + super(TestAgreementState, self).setUp() |
1502 | + self.commonsetUp() |
1503 | + |
1504 | + def test_00_future(self): |
1505 | + """Test state of a future agreement""" |
1506 | + cr, uid = self.cr, self.uid |
1507 | + start_date = self.now + timedelta(days=10) |
1508 | + start_date = start_date.strftime(DEFAULT_SERVER_DATE_FORMAT) |
1509 | + end_date = self.now + timedelta(days=20) |
1510 | + end_date = end_date.strftime(DEFAULT_SERVER_DATE_FORMAT) |
1511 | + |
1512 | + agr_id = self.agreement_model.create(cr, uid, |
1513 | + {'supplier_id': self.supplier_id, |
1514 | + 'product_id': self.product_id, |
1515 | + 'start_date': start_date, |
1516 | + 'end_date': end_date, |
1517 | + 'delay': 5, |
1518 | + 'quantity': 20}) |
1519 | + |
1520 | + agreement = self.agreement_model.browse(cr, uid, agr_id) |
1521 | + agreement.open_agreement() |
1522 | + self.assertEqual(agreement.state, 'future') |
1523 | + |
1524 | + def test_01_past(self): |
1525 | + """Test state of a past agreement""" |
1526 | + cr, uid = self.cr, self.uid |
1527 | + start_date = self.now - timedelta(days=20) |
1528 | + start_date = start_date.strftime(DEFAULT_SERVER_DATE_FORMAT) |
1529 | + end_date = self.now - timedelta(days=10) |
1530 | + end_date = end_date.strftime(DEFAULT_SERVER_DATE_FORMAT) |
1531 | + |
1532 | + agr_id = self.agreement_model.create(cr, uid, |
1533 | + {'supplier_id': self.supplier_id, |
1534 | + 'product_id': self.product_id, |
1535 | + 'start_date': start_date, |
1536 | + 'end_date': end_date, |
1537 | + 'delay': 5, |
1538 | + 'quantity': 20}) |
1539 | + agreement = self.agreement_model.browse(cr, uid, agr_id) |
1540 | + agreement.open_agreement() |
1541 | + self.assertEqual(agreement.state, 'closed') |
1542 | + |
1543 | + def test_02_running(self): |
1544 | + """Test state of a running agreement""" |
1545 | + cr, uid = self.cr, self.uid |
1546 | + start_date = self.now - timedelta(days=2) |
1547 | + start_date = start_date.strftime(DEFAULT_SERVER_DATE_FORMAT) |
1548 | + end_date = self.now + timedelta(days=2) |
1549 | + end_date = end_date.strftime(DEFAULT_SERVER_DATE_FORMAT) |
1550 | + |
1551 | + agr_id = self.agreement_model.create(cr, uid, |
1552 | + {'supplier_id': self.supplier_id, |
1553 | + 'product_id': self.product_id, |
1554 | + 'start_date': start_date, |
1555 | + 'end_date': end_date, |
1556 | + 'delay': 5, |
1557 | + 'quantity': 20}) |
1558 | + agreement = self.agreement_model.browse(cr, uid, agr_id) |
1559 | + agreement.open_agreement() |
1560 | + self.assertEqual(agreement.state, 'running') |
1561 | + |
1562 | + def test_03_date_orderconstraint(self): |
1563 | + """Test that date order is checked""" |
1564 | + cr, uid = self.cr, self.uid |
1565 | + start_date = self.now - timedelta(days=40) |
1566 | + start_date = start_date.strftime(DEFAULT_SERVER_DATE_FORMAT) |
1567 | + end_date = self.now + timedelta(days=30) |
1568 | + end_date = end_date.strftime(DEFAULT_SERVER_DATE_FORMAT) |
1569 | + with self.assertRaises(Exception) as constraint: |
1570 | + self.agreement_model.create(cr, uid, |
1571 | + {'supplier_id': self.supplier_id, |
1572 | + 'product_id': self.product_id, |
1573 | + 'start_date': end_date, |
1574 | + 'end_date': start_date, |
1575 | + 'draft': False, |
1576 | + 'delay': 5, |
1577 | + 'quantity': 20}) |
1578 | + |
1579 | + def test_04_test_overlapp(self): |
1580 | + """Test overlapping agreement for same supplier constraint""" |
1581 | + cr, uid = self.cr, self.uid |
1582 | + start_date = self.now - timedelta(days=10) |
1583 | + start_date = start_date.strftime(DEFAULT_SERVER_DATE_FORMAT) |
1584 | + end_date = self.now + timedelta(days=10) |
1585 | + end_date = end_date.strftime(DEFAULT_SERVER_DATE_FORMAT) |
1586 | + self.agreement_model.create(cr, uid, |
1587 | + {'supplier_id': self.supplier_id, |
1588 | + 'product_id': self.product_id, |
1589 | + 'start_date': start_date, |
1590 | + 'end_date': end_date, |
1591 | + 'draft': False, |
1592 | + 'delay': 5, |
1593 | + 'quantity': 20}) |
1594 | + start_date = self.now - timedelta(days=2) |
1595 | + start_date = start_date.strftime(DEFAULT_SERVER_DATE_FORMAT) |
1596 | + end_date = self.now + timedelta(days=2) |
1597 | + end_date = end_date.strftime(DEFAULT_SERVER_DATE_FORMAT) |
1598 | + with self.assertRaises(Exception) as constraint: |
1599 | + self.agreement_model.create(cr, uid, |
1600 | + {'supplier_id': self.supplier_id, |
1601 | + 'product_id': self.product_id, |
1602 | + 'start_date': start_date, |
1603 | + 'end_date': end_date, |
1604 | + 'draft': False, |
1605 | + 'delay': 5, |
1606 | + 'quantity': 20}) |
1607 | |
1608 | === added file 'framework_agreement/tests/test_framework_agreement_on_change.py' |
1609 | --- framework_agreement/tests/test_framework_agreement_on_change.py 1970-01-01 00:00:00 +0000 |
1610 | +++ framework_agreement/tests/test_framework_agreement_on_change.py 2014-02-06 10:10:57 +0000 |
1611 | @@ -0,0 +1,166 @@ |
1612 | +# -*- coding: utf-8 -*- |
1613 | +############################################################################## |
1614 | +# |
1615 | +# Author: Nicolas Bessi |
1616 | +# Copyright 2013 Camptocamp SA |
1617 | +# |
1618 | +# This program is free software: you can redistribute it and/or modify |
1619 | +# it under the terms of the GNU Affero General Public License as |
1620 | +# published by the Free Software Foundation, either version 3 of the |
1621 | +# License, or (at your option) any later version. |
1622 | +# |
1623 | +# This program is distributed in the hope that it will be useful, |
1624 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1625 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1626 | +# GNU Affero General Public License for more details. |
1627 | +# |
1628 | +# You should have received a copy of the GNU Affero General Public License |
1629 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1630 | +# |
1631 | +############################################################################## |
1632 | +from datetime import timedelta |
1633 | +from openerp.tools import DEFAULT_SERVER_DATE_FORMAT |
1634 | +import openerp.tests.common as test_common |
1635 | +from .common import BaseAgreementTestMixin |
1636 | +from ..model.framework_agreement import FrameworkAgreementObservable |
1637 | + |
1638 | + |
1639 | +class TestAgreementOnChange(test_common.TransactionCase, BaseAgreementTestMixin): |
1640 | + """Test observer on change and purchase order on chnage""" |
1641 | + |
1642 | + def setUp(self): |
1643 | + """ Create a default agreement |
1644 | + with 3 price line |
1645 | + qty 0 price 70 |
1646 | + qty 200 price 60 |
1647 | + """ |
1648 | + super(TestAgreementOnChange, self).setUp() |
1649 | + self.commonsetUp() |
1650 | + cr, uid = self.cr, self.uid |
1651 | + start_date = self.now + timedelta(days=10) |
1652 | + start_date = start_date.strftime(DEFAULT_SERVER_DATE_FORMAT) |
1653 | + end_date = self.now + timedelta(days=20) |
1654 | + end_date = end_date.strftime(DEFAULT_SERVER_DATE_FORMAT) |
1655 | + agr_id = self.agreement_model.create(cr, uid, |
1656 | + {'supplier_id': self.supplier_id, |
1657 | + 'product_id': self.product_id, |
1658 | + 'start_date': start_date, |
1659 | + 'end_date': end_date, |
1660 | + 'delay': 5, |
1661 | + 'draft': False, |
1662 | + 'quantity': 1500}) |
1663 | + pl_id = self.agreement_pl_model.create(cr, uid, |
1664 | + {'framework_agreement_id': agr_id, |
1665 | + 'currency_id': self.ref('base.EUR')}) |
1666 | + self.agreement_line_model.create(cr, uid, |
1667 | + {'framework_agreement_pricelist_id': pl_id, |
1668 | + 'quantity': 0, |
1669 | + 'price': 70}) |
1670 | + self.agreement_line_model.create(cr, uid, |
1671 | + {'framework_agreement_pricelist_id': pl_id, |
1672 | + 'quantity': 200, |
1673 | + 'price': 60}) |
1674 | + self.agreement = self.agreement_model.browse(cr, uid, agr_id) |
1675 | + self.po_line_model = self.registry('purchase.order.line') |
1676 | + self.assertTrue(issubclass(type(self.po_line_model), |
1677 | + FrameworkAgreementObservable)) |
1678 | + |
1679 | + def test_00_observe_price_change(self): |
1680 | + """Ensure that on change price observer raise correct warning |
1681 | + |
1682 | + Warning must be rose if there is a running price agreement |
1683 | + |
1684 | + """ |
1685 | + cr, uid = self.cr, self.uid |
1686 | + res = self.po_line_model.onchange_price_obs(cr, uid, False, 20.0, |
1687 | + self.agreement.id, |
1688 | + currency=self.browse_ref('base.EUR'), |
1689 | + qty=100) |
1690 | + self.assertTrue(res.get('warning')) |
1691 | + |
1692 | + def test_01_onchange_quantity_obs(self): |
1693 | + """Ensure that on change quantity will raise warning or return price""" |
1694 | + cr, uid = self.cr, self.uid |
1695 | + res = self.po_line_model.onchange_quantity_obs(cr, uid, False, 200.0, |
1696 | + self.agreement.start_date, |
1697 | + self.agreement.product_id.id, |
1698 | + supplier_id=self.agreement.supplier_id.id, |
1699 | + currency=self.browse_ref('base.EUR')) |
1700 | + self.assertFalse(res.get('warning')) |
1701 | + self.assertEqual(res.get('value', {}).get('price'), 60) |
1702 | + # test there is a warning if agreement has not enought quantity |
1703 | + res = self.po_line_model.onchange_quantity_obs(cr, uid, False, 20000.0, |
1704 | + self.agreement.start_date, |
1705 | + self.agreement.product_id.id, |
1706 | + supplier_id=self.agreement.supplier_id.id, |
1707 | + currency=self.browse_ref('base.EUR')) |
1708 | + self.assertTrue(res.get('warning')) |
1709 | + |
1710 | + res = self.po_line_model.onchange_quantity_obs(cr, uid, False, 20000.0, |
1711 | + self.now.strftime(DEFAULT_SERVER_DATE_FORMAT), |
1712 | + self.agreement.product_id.id, |
1713 | + supplier_id=self.agreement.supplier_id.id, |
1714 | + currency=self.browse_ref('base.EUR')) |
1715 | + self.assertFalse(res.get('warning')) |
1716 | + |
1717 | + def test_02_onchange_product_obs(self): |
1718 | + """Check that change of product has correct behavior""" |
1719 | + cr, uid = self.cr, self.uid |
1720 | + res = self.po_line_model.onchange_product_id_obs(cr, uid, False, 180.0, |
1721 | + self.agreement.start_date, |
1722 | + self.agreement.supplier_id.id, |
1723 | + self.agreement.product_id.id) |
1724 | + self.assertFalse(res.get('warning')) |
1725 | + self.assertEqual(res.get('value', {}).get('price'), 70) |
1726 | + |
1727 | + res = self.po_line_model.onchange_product_id_obs(cr, uid, False, 20000.0, |
1728 | + self.agreement.start_date, |
1729 | + self.agreement.supplier_id.id, |
1730 | + self.agreement.product_id.id) |
1731 | + self.assertTrue(res.get('warning')) |
1732 | + self.assertEqual(res.get('value', {}).get('price'), 60) |
1733 | + |
1734 | + res = self.po_line_model.onchange_product_id_obs(cr, uid, False, 20000.0, |
1735 | + self.now.strftime(DEFAULT_SERVER_DATE_FORMAT), |
1736 | + self.agreement.supplier_id.id, |
1737 | + self.agreement.product_id.id) |
1738 | + self.assertFalse(res.get('warning')) |
1739 | + |
1740 | + # we do the test on non agreement product |
1741 | + |
1742 | + res = self.po_line_model.onchange_product_id_obs(cr, uid, False, 20000.0, |
1743 | + self.agreement.start_date, |
1744 | + self.ref('product.product_product_33'), |
1745 | + self.agreement.product_id.id) |
1746 | + self.assertEqual(res, {'value': {'framework_agreement_id': False}}) |
1747 | + |
1748 | + def test_03_price_observer_bindings(self): |
1749 | + """Check that change of price has correct behavior""" |
1750 | + cr, uid = self.cr, self.uid |
1751 | + plist = self.agreement.supplier_id.property_product_pricelist_purchase |
1752 | + res = self.po_line_model.onchange_price(cr, uid, False, 20.0, |
1753 | + self.agreement.id, |
1754 | + 200, |
1755 | + plist.id, |
1756 | + self.agreement.product_id.id) |
1757 | + self.assertTrue(res.get('warning')) |
1758 | + |
1759 | + def test_04_product_observer_bindings(self): |
1760 | + """Check that change of product has correct behavior""" |
1761 | + cr, uid = self.cr, self.uid |
1762 | + pl = self.agreement.supplier_id.property_product_pricelist_purchase.id, |
1763 | + |
1764 | + res = self.po_line_model.onchange_product_id(cr, uid, False, |
1765 | + pl, |
1766 | + self.agreement.product_id.id, |
1767 | + 200, |
1768 | + self.agreement.product_id.uom_id.id, |
1769 | + self.agreement.supplier_id.id, |
1770 | + date_order=self.agreement.start_date[0:10], |
1771 | + fiscal_position_id=False, |
1772 | + date_planned=False, |
1773 | + name=False, |
1774 | + price_unit=False, |
1775 | + context={}, |
1776 | + agreement_id=self.agreement.id) |
1777 | + self.assertFalse(res.get('warning')) |
1778 | |
1779 | === added file 'framework_agreement/tests/test_framework_agreement_price_list.py' |
1780 | --- framework_agreement/tests/test_framework_agreement_price_list.py 1970-01-01 00:00:00 +0000 |
1781 | +++ framework_agreement/tests/test_framework_agreement_price_list.py 2014-02-06 10:10:57 +0000 |
1782 | @@ -0,0 +1,73 @@ |
1783 | +from datetime import timedelta |
1784 | +from openerp.tools import DEFAULT_SERVER_DATE_FORMAT |
1785 | +from openerp.osv import orm |
1786 | +import openerp.tests.common as test_common |
1787 | +from .common import BaseAgreementTestMixin |
1788 | + |
1789 | + |
1790 | +class TestAgreementPriceList(test_common.TransactionCase, BaseAgreementTestMixin): |
1791 | + """Test observer on change and purchase order on chnage""" |
1792 | + |
1793 | + def setUp(self): |
1794 | + """ Create a default agreement |
1795 | + with 3 price line |
1796 | + qty 0 price 70 |
1797 | + qty 200 price 60 |
1798 | + qty 500 price 50 |
1799 | + qty 1000 price 45 |
1800 | + """ |
1801 | + super(TestAgreementPriceList, self).setUp() |
1802 | + self.commonsetUp() |
1803 | + cr, uid = self.cr, self.uid |
1804 | + start_date = self.now + timedelta(days=10) |
1805 | + start_date = start_date.strftime(DEFAULT_SERVER_DATE_FORMAT) |
1806 | + end_date = self.now + timedelta(days=20) |
1807 | + end_date = end_date.strftime(DEFAULT_SERVER_DATE_FORMAT) |
1808 | + agr_id = self.agreement_model.create(cr, uid, |
1809 | + {'supplier_id': self.supplier_id, |
1810 | + 'product_id': self.product_id, |
1811 | + 'start_date': start_date, |
1812 | + 'end_date': end_date, |
1813 | + 'delay': 5, |
1814 | + 'draft': False, |
1815 | + 'quantity': 1500}) |
1816 | + |
1817 | + pl_id = self.agreement_pl_model.create(cr, uid, |
1818 | + {'framework_agreement_id': agr_id, |
1819 | + 'currency_id': self.ref('base.EUR')}) |
1820 | + |
1821 | + self.agreement_line_model.create(cr, uid, |
1822 | + {'framework_agreement_pricelist_id': pl_id, |
1823 | + 'quantity': 0, |
1824 | + 'price': 70.0}) |
1825 | + self.agreement_line_model.create(cr, uid, |
1826 | + {'framework_agreement_pricelist_id': pl_id, |
1827 | + 'quantity': 200, |
1828 | + 'price': 60.0}) |
1829 | + self.agreement_line_model.create(cr, uid, |
1830 | + {'framework_agreement_pricelist_id': pl_id, |
1831 | + 'quantity': 500, |
1832 | + 'price': 50.0}) |
1833 | + self.agreement_line_model.create(cr, uid, |
1834 | + {'framework_agreement_pricelist_id': pl_id, |
1835 | + 'quantity': 1000, |
1836 | + 'price': 45.0}) |
1837 | + self.agreement = self.agreement_model.browse(cr, uid, agr_id) |
1838 | + |
1839 | + def test_00_test_qty(self): |
1840 | + """Test if barem retrieval is correct""" |
1841 | + self.assertEqual(self.agreement.get_price(0, currency=self.browse_ref('base.EUR')), 70.0) |
1842 | + self.assertEqual(self.agreement.get_price(100, currency=self.browse_ref('base.EUR')), 70.0) |
1843 | + self.assertEqual(self.agreement.get_price(200, currency=self.browse_ref('base.EUR')), 60.0) |
1844 | + self.assertEqual(self.agreement.get_price(210, currency=self.browse_ref('base.EUR')), 60.0) |
1845 | + self.assertEqual(self.agreement.get_price(500, currency=self.browse_ref('base.EUR')), 50.0) |
1846 | + self.assertEqual(self.agreement.get_price(800, currency=self.browse_ref('base.EUR')), 50.0) |
1847 | + self.assertEqual(self.agreement.get_price(999, currency=self.browse_ref('base.EUR')), 50.0) |
1848 | + self.assertEqual(self.agreement.get_price(1000, currency=self.browse_ref('base.EUR')), 45.0) |
1849 | + self.assertEqual(self.agreement.get_price(10000, currency=self.browse_ref('base.EUR')), 45.0) |
1850 | + self.assertEqual(self.agreement.get_price(-10, currency=self.browse_ref('base.EUR')), 70.0) |
1851 | + |
1852 | + def test_01_failed_wrong_currency(self): |
1853 | + """Tests that wrong currency raise an exception""" |
1854 | + with self.assertRaises(orm.except_orm) as error: |
1855 | + self.agreement.get_price(0, currency=self.browse_ref('base.USD')) |
1856 | |
1857 | === added file 'framework_agreement/utils.py' |
1858 | --- framework_agreement/utils.py 1970-01-01 00:00:00 +0000 |
1859 | +++ framework_agreement/utils.py 2014-02-06 10:10:57 +0000 |
1860 | @@ -0,0 +1,29 @@ |
1861 | +# -*- coding: utf-8 -*- |
1862 | +############################################################################## |
1863 | +# |
1864 | +# Author: Nicolas Bessi |
1865 | +# Copyright 2013 Camptocamp SA |
1866 | +# |
1867 | +# This program is free software: you can redistribute it and/or modify |
1868 | +# it under the terms of the GNU Affero General Public License as |
1869 | +# published by the Free Software Foundation, either version 3 of the |
1870 | +# License, or (at your option) any later version. |
1871 | +# |
1872 | +# This program is distributed in the hope that it will be useful, |
1873 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1874 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1875 | +# GNU Affero General Public License for more details. |
1876 | +# |
1877 | +# You should have received a copy of the GNU Affero General Public License |
1878 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1879 | +# |
1880 | +############################################################################## |
1881 | +def id_boilerplate(fun): |
1882 | + """Ensure that id agrument passed to on change is not a list""" |
1883 | + def wrapper(*args, **kwargs): |
1884 | + if isinstance(args[3], (list, tuple)): |
1885 | + args = list(args) |
1886 | + args[3] = args[3][0] if args[3] else False |
1887 | + args = tuple(args) |
1888 | + return fun(*args, **kwargs) |
1889 | + return wrapper |
1890 | |
1891 | === added directory 'framework_agreement/view' |
1892 | === added file 'framework_agreement/view/company_view.xml' |
1893 | --- framework_agreement/view/company_view.xml 1970-01-01 00:00:00 +0000 |
1894 | +++ framework_agreement/view/company_view.xml 2014-02-06 10:10:57 +0000 |
1895 | @@ -0,0 +1,17 @@ |
1896 | +<?xml version="1.0" encoding="utf-8"?> |
1897 | +<openerp> |
1898 | + <data> |
1899 | + <record id="add agreement setting on company" model="ir.ui.view"> |
1900 | + <field name="name">add agreement setting on company</field> |
1901 | + <field name="model">res.company</field> |
1902 | + <field name="priority">19</field> |
1903 | + <field name="inherit_id" ref="base.view_company_form"/> |
1904 | + <field name="arch" type="xml"> |
1905 | + <xpath expr="//group[@name='logistics_grp']" position="inside"> |
1906 | + <field name="one_agreement_per_product"/> |
1907 | + </xpath> |
1908 | + </field> |
1909 | + </record> |
1910 | + |
1911 | + </data> |
1912 | +</openerp> |
1913 | |
1914 | === added file 'framework_agreement/view/framework_agreement_view.xml' |
1915 | --- framework_agreement/view/framework_agreement_view.xml 1970-01-01 00:00:00 +0000 |
1916 | +++ framework_agreement/view/framework_agreement_view.xml 2014-02-06 10:10:57 +0000 |
1917 | @@ -0,0 +1,118 @@ |
1918 | +<?xml version="1.0" encoding="utf-8"?> |
1919 | +<openerp> |
1920 | + <data> |
1921 | + <record id="framework_agreement_list_view" model="ir.ui.view"> |
1922 | + <field name="name">framework agreement list view</field> |
1923 | + <field name="model">framework.agreement</field> |
1924 | + <field name="arch" type="xml"> |
1925 | + <tree version="7.0" string="Framework Agreement (LTA)"> <!-- editable="bottom" --> |
1926 | + <field name="name" /> |
1927 | + <field name="company_id" groups="base.group_multi_company" widget="selection"/> |
1928 | + <field name="product_id"/> |
1929 | + <field name="supplier_id"/> |
1930 | + <field name="delay"/> |
1931 | + <field name="quantity"/> |
1932 | + <field name="available_quantity"/> |
1933 | + <field name="start_date"/> |
1934 | + <field name="end_date"/> |
1935 | + <field name="state"/> |
1936 | + </tree> |
1937 | + </field> |
1938 | + </record> |
1939 | + |
1940 | + <record id="framework_agreement_form_view" model="ir.ui.view"> |
1941 | + <field name="name">framework agreement form</field> |
1942 | + <field name="model">framework.agreement</field> |
1943 | + <field name="arch" type="xml"> |
1944 | + <form version="7.0" string="Framework Agreement"> |
1945 | + <header> |
1946 | + <button name="open_agreement" |
1947 | + context="{}" |
1948 | + string="Open Agreement" |
1949 | + type="object" |
1950 | + attrs="{'invisible': ['|', ('draft', '=', False), '|', ('start_date', '=', False), '|', ('end_date', '=', False), '|', ('framework_agreement_pricelist_ids', '=', False)]}"/> |
1951 | + <field name="state" |
1952 | + widget="statusbar" |
1953 | + nolabel="1" |
1954 | + statusbar_visible="draft,future,running,consumed,closed" |
1955 | + statusbar_colors='{"draft":"blue","future": "blue", "closed": "blue", "running": "green", "consumed": "red"}'/> |
1956 | + </header> |
1957 | + <sheet> |
1958 | + <group> |
1959 | + <field name="name"/> |
1960 | + <field name="origin"/> |
1961 | + <field name="draft" invisible="1"/> |
1962 | + </group> |
1963 | + <group> |
1964 | + <group> |
1965 | + <field name="company_id" groups="base.group_multi_company" widget="selection"/> |
1966 | + <field name="supplier_id" context="{'default_supplier': True, 'default_customer': False}"/> |
1967 | + <field name="product_id"/> |
1968 | + </group> |
1969 | + <group> |
1970 | + <field name="delay"/> |
1971 | + <field name="quantity"/> |
1972 | + <field name="available_quantity"/> |
1973 | + </group> |
1974 | + </group> |
1975 | + |
1976 | + <group string="Dates"> |
1977 | + <field name="start_date" |
1978 | + required="1"/> |
1979 | + <field name="end_date" |
1980 | + required="1"/> |
1981 | + </group> |
1982 | + <notebook> |
1983 | + <page string="Negociated price lists" colspan="4"> |
1984 | + <field name="framework_agreement_pricelist_ids" |
1985 | + required="1"> |
1986 | + <tree type="7.0" string="Price list"> |
1987 | + <field name="currency_id"/> |
1988 | + </tree> |
1989 | + <form type="7.0" string="Price list"> |
1990 | + <group> |
1991 | + <field name="currency_id"/> |
1992 | + </group> |
1993 | + <newline/> |
1994 | + <notebook> |
1995 | + <page string="Price lines" colspan="4"> |
1996 | + <field name="framework_agreement_line_ids" |
1997 | + nolabel="1"> |
1998 | + <tree type="7.0" |
1999 | + string="Price line" |
2000 | + editable="top"> |
2001 | + <field name="quantity"/> |
2002 | + <field name="price"/> |
2003 | + </tree> |
2004 | + </field> |
2005 | + </page> |
2006 | + </notebook> |
2007 | + </form> |
2008 | + </field> |
2009 | + </page> |
2010 | + </notebook> |
2011 | + </sheet> |
2012 | + </form> |
2013 | + </field> |
2014 | + </record> |
2015 | + |
2016 | + |
2017 | + <record model="ir.actions.act_window" id="action_framework_agreement"> |
2018 | + <field name="name">Framework Agreement</field> |
2019 | + <field name="type">ir.actions.act_window</field> |
2020 | + <field name="res_model">framework.agreement</field> |
2021 | + <field name="domain"></field> |
2022 | + <field name="view_type">form</field> |
2023 | + <field name="view_mode">tree,form</field> |
2024 | + <field name="view_id" ref="framework_agreement_list_view"/> |
2025 | + </record> |
2026 | + |
2027 | + |
2028 | + <menuitem |
2029 | + name="Framework Agreement" |
2030 | + parent="purchase.menu_purchase_config_pricelist" |
2031 | + action="action_framework_agreement" |
2032 | + id="action_framework_agreement_menu"/> |
2033 | + |
2034 | + </data> |
2035 | +</openerp> |
2036 | |
2037 | === added file 'framework_agreement/view/product_view.xml' |
2038 | --- framework_agreement/view/product_view.xml 1970-01-01 00:00:00 +0000 |
2039 | +++ framework_agreement/view/product_view.xml 2014-02-06 10:10:57 +0000 |
2040 | @@ -0,0 +1,86 @@ |
2041 | +<?xml version="1.0" encoding="utf-8"?> |
2042 | +<openerp> |
2043 | + <data> |
2044 | + <record id="agreement in product view" model="ir.ui.view"> |
2045 | + <field name="name">agreement in product view</field> |
2046 | + <field name="model">product.product</field> |
2047 | + <field name="inherit_id" ref="product.product_normal_form_view"/> |
2048 | + <field name="arch" type="xml"> |
2049 | + <group string="Purchase" position="after"> |
2050 | + <group string="Framework agreements (LTA)" colspan="4"> |
2051 | + <field name="framework_agreement_ids" nolabel="1"> |
2052 | + <form version="7.0" string="Framework Agreement"> |
2053 | + <header> |
2054 | + <button name="open_agreement" |
2055 | + context="{}" |
2056 | + string="Open Agreement" |
2057 | + type="object" |
2058 | + attrs="{'invisible': ['|', ('draft', '=', False), ('start_date', '=', False), ('end_date', '=', False), ('framework_agreement_pricelist_ids', '=', False)]}"/> |
2059 | + <field name="state" |
2060 | + widget="statusbar" |
2061 | + nolabel="1" |
2062 | + statusbar_visible="draft,future,running,consumed,closed" |
2063 | + statusbar_colors='{"draft":"blue","future": "blue", "closed": "blue", "running": "green", "consumed": "red"}'/> |
2064 | + </header> |
2065 | + <sheet> |
2066 | + <group> |
2067 | + <field name="name"/> |
2068 | + <field name="draft" invisible="1"/> |
2069 | + <field name="origin"/> |
2070 | + </group> |
2071 | + <group> |
2072 | + <group> |
2073 | + <field name="company_id" groups="base.group_multi_company" widget="selection"/> |
2074 | + <field name="supplier_id" context="{'default_supplier': True, 'default_customer': False}"/> |
2075 | + </group> |
2076 | + <group> |
2077 | + <field name="delay"/> |
2078 | + <field name="quantity"/> |
2079 | + <field name="available_quantity"/> |
2080 | + </group> |
2081 | + </group> |
2082 | + |
2083 | + <group string="Dates"> |
2084 | + <field name="start_date" |
2085 | + required="1"/> |
2086 | + <field name="end_date" |
2087 | + required="1"/> |
2088 | + </group> |
2089 | + <notebook> |
2090 | + <page string="Negociated price lists" colspan="4"> |
2091 | + <field name="framework_agreement_pricelist_ids" |
2092 | + attrs="{'required': [('draft', '=', False)]}"> |
2093 | + <tree type="7.0" string="Price list"> |
2094 | + <field name="currency_id"/> |
2095 | + </tree> |
2096 | + <form type="7.0" string="Price list"> |
2097 | + <group> |
2098 | + <field name="currency_id"/> |
2099 | + </group> |
2100 | + <newline/> |
2101 | + <notebook> |
2102 | + <page string="Price lines" colspan="4"> |
2103 | + <field name="framework_agreement_line_ids" |
2104 | + nolabel="1"> |
2105 | + <tree type="7.0" |
2106 | + string="Price line" |
2107 | + editable="top"> |
2108 | + <field name="quantity"/> |
2109 | + <field name="price"/> |
2110 | + </tree> |
2111 | + </field> |
2112 | + </page> |
2113 | + </notebook> |
2114 | + </form> |
2115 | + </field> |
2116 | + </page> |
2117 | + </notebook> |
2118 | + </sheet> |
2119 | + </form> |
2120 | + </field> |
2121 | + </group> |
2122 | + </group> |
2123 | + </field> |
2124 | + </record> |
2125 | + </data> |
2126 | +</openerp> |
2127 | |
2128 | === added file 'framework_agreement/view/purchase_view.xml' |
2129 | --- framework_agreement/view/purchase_view.xml 1970-01-01 00:00:00 +0000 |
2130 | +++ framework_agreement/view/purchase_view.xml 2014-02-06 10:10:57 +0000 |
2131 | @@ -0,0 +1,51 @@ |
2132 | +<?xml version="1.0" encoding="utf-8"?> |
2133 | +<openerp> |
2134 | + <data> |
2135 | + <record id="add_onchange_on_pruchase_order_form" model="ir.ui.view"> |
2136 | + <field name="name">add onchange on pruchase form</field> |
2137 | + <field name="model">purchase.order</field> |
2138 | + <field name="inherit_id" ref="purchase.purchase_order_form" /> |
2139 | + <field name="arch" type="xml"> |
2140 | + <field name="price_unit" position="attributes"> |
2141 | + <attribute name="on_change">onchange_price(price_unit, parent.framework_agreement_id, product_qty, parent.pricelist_id, product_id)</attribute> |
2142 | + </field> |
2143 | + <field name="pricelist_id" position="after"> |
2144 | + <field name="framework_agreement_id" |
2145 | + domain="[('draft', '=', False)]" |
2146 | + on_change="onchange_agreement(framework_agreement_id, partner_id, date_order)"/> |
2147 | + </field> |
2148 | + <field name="pricelist_id" |
2149 | + position="attributes"> |
2150 | + <attribute name="on_change">onchange_pricelist(pricelist_id, order_line)</attribute> |
2151 | + </field> |
2152 | + <field name="date_order" |
2153 | + position="attributes"> |
2154 | + <attribute name="on_change">onchange_date(framework_agreement_id, date_order)</attribute> |
2155 | + </field> |
2156 | + <field name="partner_id" |
2157 | + position="attributes"> |
2158 | + <attribute name="on_change">onchange_partner_id(partner_id, framework_agreement_id)</attribute> |
2159 | + </field> |
2160 | + <field name="product_id" |
2161 | + position="attributes"> |
2162 | + <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> |
2163 | + </field> |
2164 | + <field name="product_qty" |
2165 | + position="attributes"> |
2166 | + <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> |
2167 | + </field> |
2168 | + </field> |
2169 | + </record> |
2170 | + |
2171 | + <record id="add_onchange_on_pruchase_order_line_form_standalone" model="ir.ui.view"> |
2172 | + <field name="name">add onchange on pruchase order line form standalone</field> |
2173 | + <field name="model">purchase.order.line</field> |
2174 | + <field name="inherit_id" ref="purchase.purchase_order_line_form" /> |
2175 | + <field name="arch" type="xml"> |
2176 | + <field name="price_unit" position="attributes"> |
2177 | + <attribute name="on_change">on_change="onchange_price(price_unit, parent.framework_agreement_id, product_qty, parent.pricelist_id)</attribute> |
2178 | + </field> |
2179 | + </field> |
2180 | + </record> |
2181 | + </data> |
2182 | +</openerp> |
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