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