Merge lp:~camptocamp/sale-financial/7.0-port-sale_markup into lp:~sale-core-editors/sale-financial/7.0
- 7.0-port-sale_markup
- Merge into 7.0
Status: | Needs review | ||||
---|---|---|---|---|---|
Proposed branch: | lp:~camptocamp/sale-financial/7.0-port-sale_markup | ||||
Merge into: | lp:~sale-core-editors/sale-financial/7.0 | ||||
Diff against target: |
1380 lines (+840/-267) 9 files modified
sale_markup/__openerp__.py (+21/-12) sale_markup/i18n/fr.po (+76/-0) sale_markup/i18n/sale_markup.pot (+76/-0) sale_markup/product_markup.py (+105/-91) sale_markup/product_view.xml (+3/-5) sale_markup/sale_markup.py (+310/-131) sale_markup/sale_view.xml (+31/-28) sale_markup/tests/__init__.py (+21/-0) sale_markup/tests/test_sale_markup.py (+197/-0) |
||||
To merge this branch: | bzr merge lp:~camptocamp/sale-financial/7.0-port-sale_markup | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Alexandre Fayolle - camptocamp | Needs Resubmitting | ||
Alexis de Lattre | Pending | ||
Review via email: mp+214061@code.launchpad.net |
Commit message
Portage to v7 of module sale_markup
Description of the change
Portage of module sale_markup
On changes were rewritten to use web_context_tunnel and ensure there are no infinite chained on_change calls as in V7 returned values of on change will trigger next on_change and with rounding this can go wild.
Requires sale_line_watcher ported in
https:/
- 31. By Yannick Vaucher @ Camptocamp
-
Fix markup computation method on product
- 32. By Yannick Vaucher @ Camptocamp
-
improve view inheritance by using more specific paths and add more context to product_id_change
- 33. By Yannick Vaucher @ Camptocamp
-
fix onchange margin, use new_discount instead of discount rate
- 34. By Yannick Vaucher @ Camptocamp
-
[FIX] imports of decimal_precision
- 35. By Yannick Vaucher @ Camptocamp
-
Improve check on super returned values as it can be an empty dict
- 36. By Yannick Vaucher @ Camptocamp
-
Fix using sums multi group fails as the registred function for amount totals is the super one. Here I separate the computationto ensure it will be triggered.
- 37. By Yannick Vaucher @ Camptocamp
-
add tests
- 38. By Guewen Baconnier @ Camptocamp
-
Creation of a sale.order.line must trigger the sale_order.
markup_ rate function field
Alexandre Fayolle - camptocamp (alexandre-fayolle-c2c) wrote : | # |
Vincent Renaville@camptocamp (vrenaville-c2c) wrote : | # |
PR submit on LP:
Vincent Renaville@camptocamp (vrenaville-c2c) wrote : | # |
Resubmit due to conflicts :
Unmerged revisions
- 38. By Guewen Baconnier @ Camptocamp
-
Creation of a sale.order.line must trigger the sale_order.
markup_ rate function field - 37. By Yannick Vaucher @ Camptocamp
-
add tests
- 36. By Yannick Vaucher @ Camptocamp
-
Fix using sums multi group fails as the registred function for amount totals is the super one. Here I separate the computationto ensure it will be triggered.
- 35. By Yannick Vaucher @ Camptocamp
-
Improve check on super returned values as it can be an empty dict
- 34. By Yannick Vaucher @ Camptocamp
-
[FIX] imports of decimal_precision
- 33. By Yannick Vaucher @ Camptocamp
-
fix onchange margin, use new_discount instead of discount rate
- 32. By Yannick Vaucher @ Camptocamp
-
improve view inheritance by using more specific paths and add more context to product_id_change
- 31. By Yannick Vaucher @ Camptocamp
-
Fix markup computation method on product
- 30. By Yannick Vaucher @ Camptocamp
-
hide break on change fields
- 29. By Yannick Vaucher @ Camptocamp
-
fixes missing on change context on discount and add checks to not break on_changes which won't be triggered as same value is set
Preview Diff
1 | === modified file 'sale_markup/__openerp__.py' |
2 | --- sale_markup/__openerp__.py 2013-11-14 08:16:22 +0000 |
3 | +++ sale_markup/__openerp__.py 2014-06-10 15:19:47 +0000 |
4 | @@ -23,19 +23,28 @@ |
5 | 'author' : 'Camptocamp', |
6 | 'maintainer': 'Camptocamp', |
7 | 'category': 'version', |
8 | - 'complexity': "normal", # easy, normal, expert |
9 | - 'depends' : ['base', |
10 | - 'product_get_cost_field', |
11 | - 'mrp', |
12 | - 'sale', |
13 | - 'sale_floor_price'], |
14 | - 'description': """display the product and sale markup in the appropriate views""", |
15 | + 'complexity': "normal", |
16 | + 'depends' : [ |
17 | + 'base', |
18 | + 'product_get_cost_field', |
19 | + 'mrp', |
20 | + 'sale', |
21 | + 'web_context_tunnel', |
22 | + 'sale_line_watcher', |
23 | + ], |
24 | + 'description': """ |
25 | +Markup rate on product and sales |
26 | +================================ |
27 | + |
28 | +Display the product and sale markup in the appropriate views |
29 | +""", |
30 | 'website': 'http://www.camptocamp.com/', |
31 | - 'init_xml': [], |
32 | - 'update_xml': ['sale_view.xml', 'product_view.xml'], |
33 | - 'demo_xml': [], |
34 | - 'tests': [], |
35 | - 'installable': False, |
36 | + 'data': [ |
37 | + 'sale_view.xml', |
38 | + 'product_view.xml', |
39 | + ], |
40 | + 'test': [], |
41 | + 'installable': True, |
42 | 'auto_install': False, |
43 | 'license': 'AGPL-3', |
44 | 'application': True} |
45 | |
46 | === added file 'sale_markup/i18n/fr.po' |
47 | --- sale_markup/i18n/fr.po 1970-01-01 00:00:00 +0000 |
48 | +++ sale_markup/i18n/fr.po 2014-06-10 15:19:47 +0000 |
49 | @@ -0,0 +1,76 @@ |
50 | +# Translation of OpenERP Server. |
51 | +# This file contains the translation of the following modules: |
52 | +# * sale_markup |
53 | +# |
54 | +msgid "" |
55 | +msgstr "" |
56 | +"Project-Id-Version: OpenERP Server 7.0\n" |
57 | +"Report-Msgid-Bugs-To: \n" |
58 | +"POT-Creation-Date: 2014-04-03 14:11+0000\n" |
59 | +"PO-Revision-Date: 2014-04-03 16:32+0100\n" |
60 | +"Last-Translator: Yannick Vaucher <yannick.vaucher@camptocamp.com>\n" |
61 | +"Language-Team: \n" |
62 | +"MIME-Version: 1.0\n" |
63 | +"Content-Type: text/plain; charset=UTF-8\n" |
64 | +"Content-Transfer-Encoding: 8bit\n" |
65 | +"Plural-Forms: \n" |
66 | + |
67 | +#. module: sale_markup |
68 | +#: field:product.product,cost_price:0 |
69 | +msgid "Cost Price" |
70 | +msgstr "Coût de revient" |
71 | + |
72 | +#. module: sale_markup |
73 | +#: field:sale.order.line,cost_price:0 |
74 | +msgid "Historical Cost Price" |
75 | +msgstr "Coût de revient historique" |
76 | + |
77 | +#. module: sale_markup |
78 | +#: field:product.product,commercial_margin:0 |
79 | +#: field:sale.order.line,commercial_margin:0 |
80 | +msgid "Margin" |
81 | +msgstr "Marge" |
82 | + |
83 | +#. module: sale_markup |
84 | +#: help:product.product,commercial_margin:0 |
85 | +msgid "Margin is [ sale_price - cost_price ] (not based on historical values)" |
86 | +msgstr "La marge est calculée ainsi [ prix_vente - prix_achat ] (n'est pas basé sur l'historique des valeurs)" |
87 | + |
88 | +#. module: sale_markup |
89 | +#: help:sale.order.line,commercial_margin:0 |
90 | +msgid "Margin is [ sale_price - cost_price ], changing it will update the discount" |
91 | +msgstr "La marge est calculée ainsi [ prix_vente - prix_achat ], changer la marge adapte la remise." |
92 | + |
93 | +#. module: sale_markup |
94 | +#: help:sale.order.line,markup_rate:0 |
95 | +msgid "Markup is [ margin / sale_price ], changing it will update the discount" |
96 | +msgstr "Le taux de marque est calculé ainsi [ marge / prix_vente ], changer le taux de marque adapte la remise." |
97 | + |
98 | +#. module: sale_markup |
99 | +#: view:sale.order:0 |
100 | +#: field:sale.order,markup_rate:0 |
101 | +#: field:sale.order.line,markup_rate:0 |
102 | +#: field:product.product,markup_rate:0 |
103 | +msgid "Markup" |
104 | +msgstr "Taux de marque" |
105 | + |
106 | +#. module: sale_markup |
107 | +#: help:product.product,markup_rate:0 |
108 | +msgid "Markup is [ margin / sale_price ] (not based on historical values)" |
109 | +msgstr "Le taux de marque est [ marge / prix_vente ] (n'est pas basé sur l'historique des valeurs)" |
110 | + |
111 | +#. module: sale_markup |
112 | +#: help:product.product,cost_price:0 |
113 | +msgid "The cost price is the standard price unless you install the product_cost_incl_bom module." |
114 | +msgstr "Le coût de revient est le prix standard à moins que le module product_cost_incl_bom ne soit installé." |
115 | + |
116 | +#. module: sale_markup |
117 | +#: help:sale.order.line,cost_price:0 |
118 | +msgid "The cost price of the product at the time of the creation of the sale order" |
119 | +msgstr "Le coût de revient du produit au moment de la création de la commande client." |
120 | + |
121 | +#. module: sale_markup |
122 | +#: view:sale.order:0 |
123 | +msgid "product_id_change(parent.pricelist_id,product_id,product_uom_qty,product_uom,product_uos_qty,product_uos,name,parent.partner_id,False,True,parent.date_order,product_packaging,parent.fiscal_position,False,price_unit,discount,context)" |
124 | +msgstr "product_id_change(parent.pricelist_id,product_id,product_uom_qty,product_uom,product_uos_qty,product_uos,name,parent.partner_id,False,True,parent.date_order,product_packaging,parent.fiscal_position,False,price_unit,discount,context)" |
125 | + |
126 | |
127 | === added file 'sale_markup/i18n/sale_markup.pot' |
128 | --- sale_markup/i18n/sale_markup.pot 1970-01-01 00:00:00 +0000 |
129 | +++ sale_markup/i18n/sale_markup.pot 2014-06-10 15:19:47 +0000 |
130 | @@ -0,0 +1,76 @@ |
131 | +# Translation of OpenERP Server. |
132 | +# This file contains the translation of the following modules: |
133 | +# * sale_markup |
134 | +# |
135 | +msgid "" |
136 | +msgstr "" |
137 | +"Project-Id-Version: OpenERP Server 7.0\n" |
138 | +"Report-Msgid-Bugs-To: \n" |
139 | +"POT-Creation-Date: 2014-04-03 14:11+0000\n" |
140 | +"PO-Revision-Date: 2014-04-03 14:11+0000\n" |
141 | +"Last-Translator: <>\n" |
142 | +"Language-Team: \n" |
143 | +"MIME-Version: 1.0\n" |
144 | +"Content-Type: text/plain; charset=UTF-8\n" |
145 | +"Content-Transfer-Encoding: \n" |
146 | +"Plural-Forms: \n" |
147 | + |
148 | +#. module: sale_markup |
149 | +#: field:product.product,cost_price:0 |
150 | +msgid "Cost Price" |
151 | +msgstr "" |
152 | + |
153 | +#. module: sale_markup |
154 | +#: field:sale.order.line,cost_price:0 |
155 | +msgid "Historical Cost Price" |
156 | +msgstr "" |
157 | + |
158 | +#. module: sale_markup |
159 | +#: field:product.product,commercial_margin:0 |
160 | +#: field:sale.order.line,commercial_margin:0 |
161 | +msgid "Margin" |
162 | +msgstr "" |
163 | + |
164 | +#. module: sale_markup |
165 | +#: help:product.product,commercial_margin:0 |
166 | +msgid "Margin is [ sale_price - cost_price ] (not based on historical values)" |
167 | +msgstr "" |
168 | + |
169 | +#. module: sale_markup |
170 | +#: help:sale.order.line,commercial_margin:0 |
171 | +msgid "Margin is [ sale_price - cost_price ], changing it will update the discount" |
172 | +msgstr "" |
173 | + |
174 | +#. module: sale_markup |
175 | +#: help:sale.order.line,markup_rate:0 |
176 | +msgid "Markup is [ margin / sale_price ], changing it will update the discount" |
177 | +msgstr "" |
178 | + |
179 | +#. module: sale_markup |
180 | +#: view:sale.order:0 |
181 | +#: field:sale.order,markup_rate:0 |
182 | +#: field:sale.order.line,markup_rate:0 |
183 | +#: field:product.product,markup_rate:0 |
184 | +msgid "Markup" |
185 | +msgstr "" |
186 | + |
187 | +#. module: sale_markup |
188 | +#: help:product.product,markup_rate:0 |
189 | +msgid "Markup is [ margin / sale_price ] (not based on historical values)" |
190 | +msgstr "" |
191 | + |
192 | +#. module: sale_markup |
193 | +#: help:product.product,cost_price:0 |
194 | +msgid "The cost price is the standard price unless you install the product_cost_incl_bom module." |
195 | +msgstr "" |
196 | + |
197 | +#. module: sale_markup |
198 | +#: help:sale.order.line,cost_price:0 |
199 | +msgid "The cost price of the product at the time of the creation of the sale order" |
200 | +msgstr "" |
201 | + |
202 | +#. module: sale_markup |
203 | +#: view:sale.order:0 |
204 | +msgid "product_id_change(parent.pricelist_id,product_id,product_uom_qty,product_uom,product_uos_qty,product_uos,name,parent.partner_id,False,True,parent.date_order,product_packaging,parent.fiscal_position,False,price_unit,discount,context)" |
205 | +msgstr "" |
206 | + |
207 | |
208 | === modified file 'sale_markup/product_markup.py' |
209 | --- sale_markup/product_markup.py 2013-04-30 12:33:48 +0000 |
210 | +++ sale_markup/product_markup.py 2014-06-10 15:19:47 +0000 |
211 | @@ -1,10 +1,8 @@ |
212 | # -*- coding: utf-8 -*- |
213 | ############################################################################## |
214 | # |
215 | -# Copyright (c) 2011 Camptocamp SA (http://www.camptocamp.com) |
216 | -# All Right Reserved |
217 | -# |
218 | -# Author : Yannick Vaucher, Joel Grand-Guillaume |
219 | +# Author: Yannick Vaucher, Joel Grand-Guillaume |
220 | +# Copyright 2011 Camptocamp SA |
221 | # |
222 | # This program is free software: you can redistribute it and/or modify |
223 | # it under the terms of the GNU Affero General Public License as |
224 | @@ -21,80 +19,98 @@ |
225 | # |
226 | ############################################################################## |
227 | |
228 | -from osv import fields |
229 | -from osv.orm import Model |
230 | -import decimal_precision as dp |
231 | - |
232 | - |
233 | -class Product(Model): |
234 | +from openerp.osv import orm, fields |
235 | +from openerp.addons import decimal_precision as dp |
236 | + |
237 | + |
238 | +class Product(orm.Model): |
239 | _inherit = 'product.product' |
240 | |
241 | - def _convert_to_foreign_currency(self, cursor, user, pricelist, amount_dict, context=None): |
242 | - if context is None: |
243 | - context = {} |
244 | + def _convert_to_foreign_currency(self, cr, uid, pricelist, |
245 | + amount_dict, context=None): |
246 | + """ |
247 | + Apply purchase pricelist |
248 | + """ |
249 | if not pricelist: |
250 | return amount_dict |
251 | - pricelist_obj = self.pool.get('product.pricelist') |
252 | - currency_obj = self.pool.get('res.currency') |
253 | - user_obj = self.pool.get('res.users') |
254 | - pricelist = pricelist_obj.browse(cursor, user, pricelist) |
255 | - company_currency_id = user_obj.browse(cursor, user, user).company_id.currency_id.id |
256 | - converted_price = {} |
257 | + pricelist_obj = self.pool['product.pricelist'] |
258 | + currency_obj = self.pool['res.currency'] |
259 | + user_obj = self.pool['res.users'] |
260 | + pricelist = pricelist_obj.browse(cr, uid, pricelist, context=context) |
261 | + user = user_obj.browse(cr, uid, uid, context=context) |
262 | + company_currency_id = user.company_id.currency_id.id |
263 | + converted_prices = {} |
264 | for product_id, amount in amount_dict.iteritems(): |
265 | - converted_price[product_id] = currency_obj.compute(cursor, user, |
266 | - company_currency_id, |
267 | - pricelist.currency_id.id, |
268 | - amount, |
269 | - round=False) |
270 | - return converted_price |
271 | - |
272 | - def compute_markup(self, cursor, user, ids, |
273 | - product_uom = None, |
274 | - pricelist = None, |
275 | - sale_price = None, |
276 | - properties = None, |
277 | - context = None): |
278 | - ''' |
279 | + converted_prices[product_id] = currency_obj.compute( |
280 | + cr, uid, company_currency_id, pricelist.currency_id.id, amount, |
281 | + round=False) |
282 | + return converted_prices |
283 | + |
284 | + @staticmethod |
285 | + def _compute_markup(sale_price, purchase_price): |
286 | + """ |
287 | + Return markup as a rate |
288 | + |
289 | + Markup = SP - PP / SP |
290 | + |
291 | + Where SP = Sale price |
292 | + PP = Purchase price |
293 | + """ |
294 | + if not sale_price: |
295 | + return 0.0 |
296 | + return (sale_price - purchase_price) / sale_price * 100 |
297 | + |
298 | + def compute_markup(self, cr, uid, ids, |
299 | + product_uom=None, pricelist=None, sale_price=None, |
300 | + properties=None, context=None): |
301 | + """ |
302 | compute markup |
303 | |
304 | If properties, pricelist and sale_price arguments are set, it |
305 | will be used to compute all results |
306 | - ''' |
307 | + """ |
308 | properties = properties or [] |
309 | pricelist = pricelist or [] |
310 | if context is None: |
311 | context = {} |
312 | if isinstance(ids, (int, long)): |
313 | - ids = [ids] |
314 | + ids = [ids] |
315 | res = {} |
316 | |
317 | - # cost_price_context will be used by product_get_cost_field if it is installed |
318 | - cost_price_context = context.copy().update({'produc_uom': product_uom, |
319 | - 'properties': properties}) |
320 | - purchase_prices = self.get_cost_field(cursor, user, ids, cost_price_context) |
321 | + # cost_price_context will be used by product_get_cost_field if it is |
322 | + # installed |
323 | + cost_price_context = context.copy() |
324 | + cost_price_context.update({ |
325 | + 'product_uom': product_uom, |
326 | + 'properties': properties}) |
327 | + purchase_prices = self.get_cost_field(cr, uid, ids, cost_price_context) |
328 | # if purchase prices failed returned a dict of default values |
329 | - if not purchase_prices: return dict([(id, {'commercial_margin': 0.0, |
330 | - 'markup_rate': 0.0, |
331 | - 'cost_price': 0.0,}) for id in ids]) |
332 | + if not purchase_prices: |
333 | + return dict([(id, {'commercial_margin': 0.0, |
334 | + 'markup_rate': 0.0, |
335 | + 'cost_price': 0.0, |
336 | + }) for id in ids]) |
337 | |
338 | - purchase_price = self._convert_to_foreign_currency(cursor, user, pricelist, purchase_prices) |
339 | - for pr in self.browse(cursor, user, ids): |
340 | + purchase_prices = self._convert_to_foreign_currency(cr, uid, pricelist, |
341 | + purchase_prices, |
342 | + context=context) |
343 | + for pr in self.browse(cr, uid, ids, context=context): |
344 | res[pr.id] = {} |
345 | if sale_price is None: |
346 | catalog_price = pr.list_price |
347 | else: |
348 | catalog_price = sale_price |
349 | |
350 | - res[pr.id]['commercial_margin'] = catalog_price - purchase_prices[pr.id] |
351 | - |
352 | - res[pr.id]['markup_rate'] = (catalog_price and |
353 | - (catalog_price - purchase_prices[pr.id]) / catalog_price * 100 or 0.0) |
354 | - |
355 | - res[pr.id]['cost_price'] = purchase_prices[pr.id] |
356 | + res[pr.id].update({ |
357 | + 'commercial_margin': catalog_price - purchase_prices[pr.id], |
358 | + 'markup_rate': self._compute_markup(catalog_price, |
359 | + purchase_prices[pr.id]), |
360 | + 'cost_price': purchase_prices[pr.id] |
361 | + }) |
362 | |
363 | return res |
364 | |
365 | - def _get_bom_product(self,cursor, user, ids, context=None): |
366 | + def _get_bom_product(self, cr, uid, ids, context=None): |
367 | """return ids of modified product and ids of all product that use |
368 | as sub-product one of this ids. Ex: |
369 | BoM : |
370 | @@ -103,63 +119,61 @@ |
371 | - Product C |
372 | => If we change standard_price of product B, we want to update Product |
373 | A as well...""" |
374 | - if context is None: |
375 | - context = {} |
376 | def _get_parent_bom(bom_record): |
377 | """Recursvely find the parent bom""" |
378 | - result=[] |
379 | + result = [] |
380 | if bom_record.bom_id: |
381 | result.append(bom_record.bom_id.id) |
382 | result.extend(_get_parent_bom(bom_record.bom_id)) |
383 | return result |
384 | res = [] |
385 | - bom_obj = self.pool.get('mrp.bom') |
386 | - bom_ids = bom_obj.search(cursor, user, [('product_id','in',ids)]) |
387 | - for bom in bom_obj.browse(cursor, user, bom_ids): |
388 | + bom_obj = self.pool['mrp.bom'] |
389 | + bom_ids = bom_obj.search(cr, uid, [('product_id', 'in', ids)], |
390 | + context=context) |
391 | + for bom in bom_obj.browse(cr, uid, bom_ids, context=context): |
392 | res += _get_parent_bom(bom) |
393 | final_bom_ids = list(set(res + bom_ids)) |
394 | - return list(set(ids + self._get_product(cursor, user, final_bom_ids, context))) |
395 | + return list(set(ids + self._get_product(cr, uid, final_bom_ids, |
396 | + context=context))) |
397 | |
398 | - def _get_product(self, cursor, user, ids, context = None): |
399 | - if context is None: |
400 | - context = {} |
401 | - bom_obj = self.pool.get('mrp.bom') |
402 | + def _get_product(self, cr, uid, ids, context=None): |
403 | + bom_obj = self.pool['mrp.bom'] |
404 | |
405 | res = {} |
406 | - for bom in bom_obj.browse(cursor, user, ids, context=context): |
407 | + for bom in bom_obj.browse(cr, uid, ids, context=context): |
408 | res[bom.product_id.id] = True |
409 | return res.keys() |
410 | |
411 | - def _compute_all_markup(self, cursor, user, ids, field_name, arg, context = None): |
412 | - ''' |
413 | + def _compute_all_markup(self, cr, uid, ids, field_name, arg, |
414 | + context=None): |
415 | + """ |
416 | method for product function field on multi 'markup' |
417 | - ''' |
418 | - if context is None: |
419 | - context = {} |
420 | - res = self.compute_markup(cursor, user, ids, context=context) |
421 | - return res |
422 | + """ |
423 | + return self.compute_markup(cr, uid, ids, context=context) |
424 | |
425 | - _store_cfg = {'product.product': (_get_bom_product, ['list_price' ,'standard_price'], 20), |
426 | + _store_cfg = {'product.product': (_get_bom_product, |
427 | + ['list_price', 'standard_price'], 20), |
428 | 'mrp.bom': (_get_product, |
429 | - ['bom_id', 'bom_lines', 'product_id', 'product_uom', |
430 | - 'product_qty', 'product_uos', 'product_uos_qty', |
431 | - 'property_ids'], 20)} |
432 | - |
433 | - |
434 | + ['bom_id', 'bom_lines', 'product_id', |
435 | + 'product_uom', 'product_qty', 'product_uos', |
436 | + 'product_uos_qty', 'property_ids'], 20) |
437 | + } |
438 | |
439 | _columns = { |
440 | - 'commercial_margin' : fields.function(_compute_all_markup, |
441 | - method=True, |
442 | - string='Margin', |
443 | - digits_compute=dp.get_precision('Sale Price'), |
444 | - store =_store_cfg, |
445 | - multi ='markup', |
446 | - help='Margin is [ sale_price - cost_price ] (not based on historical values)'), |
447 | - 'markup_rate' : fields.function(_compute_all_markup, |
448 | - method=True, |
449 | - string='Markup rate (%)', |
450 | - digits_compute=dp.get_precision('Sale Price'), |
451 | - store=_store_cfg, |
452 | - multi='markup', |
453 | - help='Markup rate is [ margin / sale_price ] (not based on historical values)'), |
454 | - } |
455 | + 'commercial_margin': fields.function( |
456 | + _compute_all_markup, |
457 | + string='Margin', |
458 | + digits_compute=dp.get_precision('Sale Price'), |
459 | + store=_store_cfg, |
460 | + multi='markup', |
461 | + help='Margin is [ sale_price - cost_price ] (not based on ' |
462 | + 'historical values)'), |
463 | + 'markup_rate': fields.function( |
464 | + _compute_all_markup, |
465 | + string='Markup', |
466 | + digits_compute=dp.get_precision('Sale Price'), |
467 | + store=_store_cfg, |
468 | + multi='markup', |
469 | + help='Markup is [ margin / sale_price ] (not based on ' |
470 | + 'historical values)'), |
471 | + } |
472 | |
473 | === modified file 'sale_markup/product_view.xml' |
474 | --- sale_markup/product_view.xml 2012-05-15 15:56:19 +0000 |
475 | +++ sale_markup/product_view.xml 2014-06-10 15:19:47 +0000 |
476 | @@ -3,28 +3,26 @@ |
477 | <data> |
478 | <record model="ir.ui.view" id="sale_markup_product_form"> |
479 | <field name="name">product.markup.view.form</field> |
480 | - <field name="type">form</field> |
481 | <field name="model">product.product</field> |
482 | <field name="inherit_id" ref="product.product_normal_form_view" /> |
483 | <field name="arch" type="xml"> |
484 | <field name="list_price" position="after"> |
485 | <field name="markup_rate" |
486 | - groups="sale.group_sale_manager"/> |
487 | + groups="base.group_sale_manager"/> |
488 | <field name="commercial_margin" |
489 | - groups="sale.group_sale_manager"/> |
490 | + groups="base.group_sale_manager"/> |
491 | </field> |
492 | </field> |
493 | </record> |
494 | |
495 | <record model="ir.ui.view" id="sale_markup_product_tree"> |
496 | <field name="name">product.markup.view.tree</field> |
497 | - <field name="type">tree</field> |
498 | <field name="model">product.product</field> |
499 | <field name="inherit_id" ref="product.product_product_tree_view" /> |
500 | <field name="arch" type="xml"> |
501 | <field name="standard_price" position="after"> |
502 | <field name="markup_rate" |
503 | - groups="sale.group_sale_manager"/> |
504 | + groups="base.group_sale_manager"/> |
505 | </field> |
506 | </field> |
507 | </record> |
508 | |
509 | === modified file 'sale_markup/sale_markup.py' |
510 | --- sale_markup/sale_markup.py 2012-07-12 13:56:17 +0000 |
511 | +++ sale_markup/sale_markup.py 2014-06-10 15:19:47 +0000 |
512 | @@ -1,10 +1,8 @@ |
513 | # -*- coding: utf-8 -*- |
514 | ############################################################################## |
515 | # |
516 | -# Copyright (c) 2011 Camptocamp SA (http://www.camptocamp.com) |
517 | -# All Right Reserved |
518 | -# |
519 | -# Author : Yannick Vaucher, Joel Grand-Guillaume |
520 | +# Author: Yannick Vaucher, Joel Grand-Guillaume |
521 | +# Copyright 2011 Camptocamp SA |
522 | # |
523 | # This program is free software: you can redistribute it and/or modify |
524 | # it under the terms of the GNU Affero General Public License as |
525 | @@ -22,202 +20,383 @@ |
526 | ############################################################################## |
527 | |
528 | from openerp.osv.orm import Model, fields |
529 | -import decimal_precision as dp |
530 | +from openerp.addons import decimal_precision as dp |
531 | +from openerp.tools import float_compare |
532 | + |
533 | |
534 | def _prec(obj, cr, uid, mode=None): |
535 | # This function use orm cache it should be efficient |
536 | mode = mode or 'Sale Price' |
537 | - return obj.pool.get('decimal.precision').precision_get(cr, uid, mode) |
538 | + return obj.pool['decimal.precision'].precision_get(cr, uid, mode) |
539 | + |
540 | |
541 | class SaleOrder(Model): |
542 | _inherit = 'sale.order' |
543 | |
544 | - def _amount_all(self, cursor, user, ids, field_name, arg, context = None): |
545 | - '''Calculate the markup rate based on sums''' |
546 | - |
547 | - if context is None: |
548 | - context = {} |
549 | - res = {} |
550 | - res = super(SaleOrder, self)._amount_all(cursor, user, ids, field_name, arg, context) |
551 | - |
552 | - for sale_order in self.browse(cursor, user, ids): |
553 | + def _amount_markup(self, cr, user, ids, field_name, arg, context=None): |
554 | + """Calculate the markup rate based on sums""" |
555 | + |
556 | + res = dict.fromkeys(ids, {}) |
557 | + |
558 | + for sale_order in self.browse(cr, user, ids): |
559 | cost_sum = 0.0 |
560 | sale_sum = 0.0 |
561 | for line in sale_order.order_line: |
562 | cost_sum += line.cost_price |
563 | sale_sum += line.price_unit * (100 - line.discount) / 100.0 |
564 | - res[sale_order.id]['markup_rate'] = sale_sum and (sale_sum - cost_sum) / sale_sum * 100 or 0.0 |
565 | + markup_rate = ((sale_sum - cost_sum) / sale_sum * 100 if sale_sum |
566 | + else 0.0) |
567 | + res[sale_order.id]['markup_rate'] = markup_rate |
568 | return res |
569 | |
570 | - |
571 | def _get_order(self, cr, uid, ids, context=None): |
572 | - if context is None: |
573 | - context = {} |
574 | + sale_order_line_obj = self.pool['sale.order.line'] |
575 | + sale_order_lines = sale_order_line_obj.browse(cr, uid, ids, |
576 | + context=context) |
577 | result = set() |
578 | - for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context): |
579 | + for line in sale_order_lines: |
580 | result.add(line.order_id.id) |
581 | return list(result) |
582 | |
583 | _store_sums = { |
584 | - 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10), |
585 | - 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty', |
586 | - 'product_id','commercial_margin', 'markup_rate'], 10)} |
587 | - |
588 | - |
589 | - _columns = {'markup_rate': fields.function(_amount_all, |
590 | - method = True, |
591 | - string = 'Markup Rate', |
592 | - digits_compute=dp.get_precision('Sale Price'), |
593 | - store = _store_sums, |
594 | - multi='sums')} |
595 | + 'sale.order': (lambda self, cr, uid, ids, c=None: ids, |
596 | + ['order_line'], 10), |
597 | + 'sale.order.line': (_get_order, |
598 | + ['price_unit', 'tax_id', 'discount', |
599 | + 'product_uom_qty', 'product_id', 'order_id', |
600 | + 'commercial_margin', 'markup_rate'], 10) |
601 | + } |
602 | + |
603 | + _columns = { |
604 | + 'markup_rate': fields.function( |
605 | + _amount_markup, |
606 | + string='Markup', |
607 | + digits_compute=dp.get_precision('Sale Price'), |
608 | + store=_store_sums, |
609 | + multi='sums_markup'), |
610 | + } |
611 | |
612 | |
613 | class SaleOrderLine(Model): |
614 | |
615 | _inherit = 'sale.order.line' |
616 | |
617 | - _columns = {'commercial_margin': fields.float('Margin', |
618 | - digits_compute=dp.get_precision('Sale Price'), |
619 | - help='Margin is [ sale_price - cost_price ],' |
620 | - ' changing it will update the discount'), |
621 | - 'markup_rate': fields.float('Markup Rate (%)', |
622 | - digits_compute=dp.get_precision('Sale Price'), |
623 | - help='Margin rate is [ margin / sale_price ],' |
624 | - 'changing it will update the discount'), |
625 | - 'cost_price': fields.float('Historical Cost Price', |
626 | - digits_compute=dp.get_precision('Sale Price'), |
627 | - help="The cost price of the product at the time of the creation of the sale order"), |
628 | - } |
629 | - |
630 | - |
631 | - |
632 | - def onchange_price_unit(self, cursor, uid, ids, price_unit, product_id, discount, |
633 | - product_uom, pricelist, **kwargs): |
634 | - ''' |
635 | - If price unit change, compute the new markup rate. |
636 | - ''' |
637 | - res = super(SaleOrderLine,self).onchange_price_unit(cursor, uid, ids, |
638 | - price_unit, |
639 | - product_id, |
640 | - discount, |
641 | - product_uom, |
642 | - pricelist) |
643 | - |
644 | + def _get_break(self, cr, uid, ids, field_name, arg, context=None): |
645 | + return dict.fromkeys(ids, False) |
646 | + |
647 | + _columns = { |
648 | + 'commercial_margin': fields.float( |
649 | + 'Margin', |
650 | + digits_compute=dp.get_precision('Sale Price'), |
651 | + help='Margin is [ sale_price - cost_price ], changing it will ' |
652 | + 'update the discount'), |
653 | + 'markup_rate': fields.float( |
654 | + 'Markup', |
655 | + digits_compute=dp.get_precision('Sale Price'), |
656 | + help='Markup is [ margin / sale_price ], changing it will ' |
657 | + 'update the discount'), |
658 | + 'cost_price': fields.float( |
659 | + 'Historical Cost Price', |
660 | + digits_compute=dp.get_precision('Sale Price'), |
661 | + help='The cost price of the product at the time of the creation ' |
662 | + 'of the sale order'), |
663 | + # boolean fields to skip onchange loop |
664 | + 'break_onchange_discount': fields.function( |
665 | + _get_break, |
666 | + string='Break onchange', type='boolean'), |
667 | + 'break_onchange_markup_rate': fields.function( |
668 | + _get_break, |
669 | + string='Break onchange', type='boolean'), |
670 | + 'break_onchange_commercial_margin': fields.function( |
671 | + _get_break, |
672 | + string='Break onchange', type='boolean'), |
673 | + } |
674 | + |
675 | + def onchange_price_unit(self, cr, uid, ids, context=None, **kwargs): |
676 | + """ |
677 | + If price unit changes, compute the new markup rate and |
678 | + commercial margin |
679 | + |
680 | + context arguments: |
681 | + price_unit |
682 | + product_id |
683 | + discount |
684 | + product_uom |
685 | + pricelist |
686 | + markup_rate |
687 | + commercial_margin |
688 | + |
689 | + Will change: |
690 | + markup_rate |
691 | + commercial_margin |
692 | + """ |
693 | + if context is None: |
694 | + context = {} |
695 | + res = super(SaleOrderLine, self |
696 | + ).onchange_price_unit(cr, uid, ids, context=context) |
697 | + product_id = context.get('product_id') |
698 | if product_id: |
699 | - product_obj = self.pool.get('product.product') |
700 | - if res['value'].has_key('price_unit'): |
701 | + price_unit = context.get('price_unit') |
702 | + discount = context.get('discount') |
703 | + product_uom = context.get('product_uom') |
704 | + pricelist = context.get('pricelist') |
705 | + product_obj = self.pool['product.product'] |
706 | + markup = context.get('markup_rate', 0.0) |
707 | + margin = context.get('commercial_margin', 0.0) |
708 | + if not 'value' in res: |
709 | + res['value'] = {} |
710 | + if 'price_unit' in res['value']: |
711 | price_unit = res['value']['price_unit'] |
712 | sale_price = price_unit * (100 - discount) / 100.0 |
713 | - markup_res = product_obj.compute_markup(cursor, uid, |
714 | + markup_res = product_obj.compute_markup(cr, uid, |
715 | product_id, |
716 | product_uom, |
717 | pricelist, |
718 | sale_price)[product_id] |
719 | |
720 | - |
721 | - res['value']['commercial_margin'] = round(markup_res['commercial_margin'], _prec(self, cursor, uid)) |
722 | - res['value']['markup_rate'] = round(markup_res['markup_rate'], _prec(self, cursor, uid)) |
723 | + new_margin = round(markup_res['commercial_margin'], |
724 | + _prec(self, cr, uid)) |
725 | + if not float_compare(margin, new_margin, |
726 | + precision_digits=_prec(self, cr, uid)) == 0: |
727 | + res['value'].update({ |
728 | + 'commercial_margin': new_margin, |
729 | + 'break_onchange_commercial_margin': True, |
730 | + }) |
731 | + new_markup = round(markup_res['markup_rate'], |
732 | + _prec(self, cr, uid)) |
733 | + if not float_compare(markup, new_markup, |
734 | + precision_digits=_prec(self, cr, uid)) == 0: |
735 | + res['value'].update({ |
736 | + 'markup_rate': new_markup, |
737 | + 'break_onchange_markup_rate': True, |
738 | + }) |
739 | return res |
740 | |
741 | - |
742 | - |
743 | - def onchange_discount(self, cursor, uid, ids, |
744 | - price_unit, product_id, discount, product_uom, pricelist, **kwargs): |
745 | - ''' |
746 | - If discount change, compute the new markup rate |
747 | - ''' |
748 | - res = super(SaleOrderLine,self).onchange_discount(cursor, uid, ids, |
749 | - price_unit, |
750 | - product_id, |
751 | - discount, |
752 | - product_uom, |
753 | - pricelist) |
754 | - |
755 | - if product_id: |
756 | - product_obj = self.pool.get('product.product') |
757 | - if res['value'].has_key('price_unit'): |
758 | + def onchange_discount(self, cr, uid, ids, context=None, **kwargs): |
759 | + """ |
760 | + If discount changes, compute the new markup rate and commercial margin. |
761 | + |
762 | + context arguments: |
763 | + product_id |
764 | + price_unit |
765 | + discount |
766 | + product_uom |
767 | + pricelist |
768 | + markup_rate |
769 | + commercial_margin |
770 | + break_onchange |
771 | + |
772 | + Will change: |
773 | + markup_rate |
774 | + commercial_margin |
775 | + """ |
776 | + if context is None: |
777 | + context = {} |
778 | + res = super(SaleOrderLine, self |
779 | + ).onchange_discount(cr, uid, ids, context=context) |
780 | + product_id = context.get('product_id') |
781 | + if context.get('break_onchange'): |
782 | + res['value']['break_onchange_discount'] = False |
783 | + elif product_id: |
784 | + price_unit = context.get('price_unit') |
785 | + discount = context.get('discount') |
786 | + product_uom = context.get('product_uom') |
787 | + pricelist = context.get('pricelist') |
788 | + markup = context.get('markup_rate', 0.0) |
789 | + margin = context.get('commercial_margin', 0.0) |
790 | + |
791 | + product_obj = self.pool['product.product'] |
792 | + if not 'value' in res: |
793 | + res['value'] = {} |
794 | + if 'price_unit' in res['value']: |
795 | price_unit = res['value']['price_unit'] |
796 | - if res['value'].has_key('discount'): |
797 | + if 'discount' in res['value']: |
798 | discount = res['value']['discount'] |
799 | sale_price = price_unit * (100 - discount) / 100.0 |
800 | - markup_res = product_obj.compute_markup(cursor, uid, |
801 | + markup_res = product_obj.compute_markup(cr, uid, |
802 | product_id, |
803 | product_uom, |
804 | pricelist, |
805 | sale_price)[product_id] |
806 | |
807 | - |
808 | - res['value']['commercial_margin'] = round(markup_res['commercial_margin'] , _prec(self, cursor, uid)) |
809 | - res['value']['markup_rate'] = round(markup_res['markup_rate'], _prec(self, cursor, uid)) |
810 | + new_margin = round(markup_res['commercial_margin'], |
811 | + _prec(self, cr, uid)) |
812 | + if not float_compare(margin, new_margin, |
813 | + precision_digits=_prec(self, cr, uid)) == 0: |
814 | + res['value'].update({ |
815 | + 'commercial_margin': new_margin, |
816 | + 'break_onchange_commercial_margin': True, |
817 | + }) |
818 | + new_markup = round(markup_res['markup_rate'], |
819 | + _prec(self, cr, uid)) |
820 | + if not float_compare(markup, new_markup, |
821 | + precision_digits=_prec(self, cr, uid)) == 0: |
822 | + res['value'].update({ |
823 | + 'markup_rate': new_markup, |
824 | + 'break_onchange_markup_rate': True, |
825 | + }) |
826 | return res |
827 | |
828 | - |
829 | - def product_id_change(self, cursor, uid, ids, pricelist, product, qty=0, |
830 | - uom=False, qty_uos=0, uos=False, name='', partner_id=False, |
831 | - lang=False, update_tax=True, date_order=False, packaging=False, |
832 | - fiscal_position=False, flag=False, discount=None, price_unit=None, context=None): |
833 | - ''' |
834 | + def product_id_change(self, cr, uid, ids, pricelist, product, qty=0, |
835 | + uom=False, qty_uos=0, uos=False, name='', |
836 | + partner_id=False, lang=False, update_tax=True, |
837 | + date_order=False, packaging=False, |
838 | + fiscal_position=False, flag=False, |
839 | + context=None): |
840 | + """ |
841 | Overload method |
842 | - If product change, compute the new markup. |
843 | - Added params : - price_unit, |
844 | + If product changes, compute the new markup, cost_price and |
845 | + commercial_margin. |
846 | + Added params : - price_unit |
847 | - discount |
848 | - - properties |
849 | - ''' |
850 | + - markup_rate |
851 | + - commercial_margin |
852 | + |
853 | + Will change: |
854 | + markup_rate |
855 | + commercial_margin |
856 | + """ |
857 | if context is None: |
858 | context = {} |
859 | - discount = discount or 0.0 |
860 | - price_unit = price_unit or 0.0 |
861 | - res = {} |
862 | - res = super(SaleOrderLine, self).product_id_change(cursor, uid, ids, pricelist, product, qty, |
863 | - uom, qty_uos, uos, name, partner_id, |
864 | - lang, update_tax, date_order, packaging, |
865 | - fiscal_position, flag, context) |
866 | + res = super(SaleOrderLine, self |
867 | + ).product_id_change(cr, uid, ids, pricelist, product, qty, |
868 | + uom, qty_uos, uos, name, partner_id, |
869 | + lang, update_tax, date_order, |
870 | + packaging, fiscal_position, flag, |
871 | + context=context) |
872 | |
873 | if product: |
874 | - if res['value'].has_key('price_unit'): |
875 | + price_unit = context.get('price_unit', 0.0) |
876 | + discount = context.get('discount', 0.0) |
877 | + markup = context.get('markup_rate', 0.0) |
878 | + margin = context.get('commercial_margin', 0.0) |
879 | + |
880 | + if not 'value' in res: |
881 | + res['value'] = {} |
882 | + if 'price_unit' in res['value']: |
883 | price_unit = res['value']['price_unit'] |
884 | sale_price = price_unit * (100 - discount) / 100.0 |
885 | |
886 | - product_obj = self.pool.get('product.product') |
887 | - markup_res = product_obj.compute_markup(cursor, uid, |
888 | + product_obj = self.pool['product.product'] |
889 | + markup_res = product_obj.compute_markup(cr, uid, |
890 | product, |
891 | uom, |
892 | pricelist, |
893 | sale_price)[product] |
894 | |
895 | - res['value']['commercial_margin'] = round(markup_res['commercial_margin'], _prec(self, cursor, uid)) |
896 | - res['value']['markup_rate'] = round(int(markup_res['markup_rate'] * 100) / 100.0, _prec(self, cursor, uid)) |
897 | - res['value']['cost_price'] = round(markup_res['cost_price'], _prec(self, cursor, uid)) |
898 | + res['value']['cost_price'] = round(markup_res['cost_price'], |
899 | + _prec(self, cr, uid)) |
900 | + new_margin = round(markup_res['commercial_margin'], |
901 | + _prec(self, cr, uid)) |
902 | + if not float_compare(margin, new_margin, |
903 | + precision_digits=_prec(self, cr, uid)) == 0: |
904 | + res['value'].update({ |
905 | + 'commercial_margin': new_margin, |
906 | + 'break_onchange_commercial_margin': True, |
907 | + }) |
908 | + new_markup = round(int(markup_res['markup_rate'] * 100) / 100.0, |
909 | + _prec(self, cr, uid)) |
910 | + if not float_compare(markup, new_markup, |
911 | + precision_digits=_prec(self, cr, uid)) == 0: |
912 | + res['value'].update({ |
913 | + 'markup_rate': new_markup, |
914 | + 'break_onchange_markup_rate': True, |
915 | + }) |
916 | |
917 | return res |
918 | |
919 | - |
920 | - def onchange_markup_rate(self, cursor, uid, ids, |
921 | - markup, cost_price, price_unit, context=None): |
922 | - ''' If markup rate change compute the discount ''' |
923 | + def onchange_markup_rate(self, cr, uid, ids, context=None): |
924 | + """ If markup rate changes compute the discount |
925 | + |
926 | + context arguments: |
927 | + markup_rate |
928 | + commercial_margin |
929 | + cost_price |
930 | + price_unit |
931 | + break_onchange |
932 | + |
933 | + Will change: |
934 | + discount |
935 | + commercial_margin |
936 | + """ |
937 | if context is None: |
938 | context = {} |
939 | - res = {} |
940 | - res['value'] = {} |
941 | + res = {'value': {}} |
942 | + if context.get('break_onchange'): |
943 | + res['value']['break_onchange_markup_rate'] = False |
944 | + return res |
945 | + |
946 | + markup = context.get('markup_rate') |
947 | + price_unit = context.get('price_unit') |
948 | markup = markup / 100.0 |
949 | - if not price_unit or markup == 1: return {'value': {}} |
950 | - discount = 1 + cost_price / (markup - 1) / price_unit |
951 | - sale_price = price_unit * (1 - discount) |
952 | - res['value']['discount'] = round(discount * 100, _prec(self, cursor, uid)) |
953 | - res['value']['commercial_margin'] = round(sale_price - cost_price, _prec(self, cursor, uid)) |
954 | + if price_unit and not markup == 1: |
955 | + cost_price = context.get('cost_price') |
956 | + margin = context.get('commercial_margin') |
957 | + discount = context.get('discount') |
958 | + |
959 | + new_discount = 1 + cost_price / (markup - 1) / price_unit |
960 | + sale_price = price_unit * (1 - new_discount) |
961 | + |
962 | + new_discount = round(new_discount * 100, _prec(self, cr, uid)) |
963 | + if not float_compare(discount, new_discount, |
964 | + precision_digits=_prec(self, cr, uid)) == 0: |
965 | + res['value'].update({ |
966 | + 'discount': new_discount, |
967 | + 'break_onchange_discount': True, |
968 | + }) |
969 | + |
970 | + new_margin = round(sale_price - cost_price, _prec(self, cr, uid)) |
971 | + if not float_compare(margin, new_margin, |
972 | + precision_digits=_prec(self, cr, uid)) == 0: |
973 | + res['value'].update({ |
974 | + 'commercial_margin': new_margin, |
975 | + 'break_onchange_commercial_margin': True, |
976 | + }) |
977 | return res |
978 | |
979 | - |
980 | - def onchange_commercial_margin(self, cursor, uid, ids, |
981 | - margin, cost_price, price_unit, context=None): |
982 | - ''' If markup rate change compute the discount ''' |
983 | + def onchange_commercial_margin(self, cr, uid, ids, context=None): |
984 | + """ If commercial margin changes compute the discount |
985 | + |
986 | + context arguments: |
987 | + commercial_margin |
988 | + markup_rate |
989 | + discount |
990 | + cost_price |
991 | + price_unit |
992 | + break_onchange |
993 | + |
994 | + Will change: |
995 | + discount |
996 | + markup_rate |
997 | + """ |
998 | if context is None: |
999 | context = {} |
1000 | - res = {} |
1001 | - res['value'] = {} |
1002 | - if not price_unit: return {'value': {}} |
1003 | - discount = 1 - ((cost_price + margin) / price_unit) |
1004 | - sale_price = price_unit * (1 - discount) |
1005 | - res['value']['discount'] = round(discount * 100, _prec(self, cursor, uid)) |
1006 | - res['value']['markup_rate'] = round(margin / (sale_price or 1.0) * 100, _prec(self, cursor, uid)) |
1007 | + res = {'value': {}} |
1008 | + price_unit = context.get('price_unit') |
1009 | + if context.get('break_onchange'): |
1010 | + res['value']['break_onchange_commercial_margin'] = False |
1011 | + elif price_unit: |
1012 | + margin = context.get('commercial_margin') |
1013 | + markup = context.get('markup_rate') |
1014 | + cost_price = context.get('cost_price') |
1015 | + discount = context.get('discount') |
1016 | + |
1017 | + new_discount = 1 - ((cost_price + margin) / price_unit) |
1018 | + sale_price = price_unit * (1 - new_discount) |
1019 | + |
1020 | + new_discount = round(new_discount * 100, _prec(self, cr, uid)) |
1021 | + if not float_compare(discount, new_discount, |
1022 | + precision_digits=_prec(self, cr, uid)) == 0: |
1023 | + res['value'].update({ |
1024 | + 'discount': new_discount, |
1025 | + 'break_onchange_discount': True, |
1026 | + }) |
1027 | + new_markup = round(margin / (sale_price or 1.0) * 100, |
1028 | + _prec(self, cr, uid)) |
1029 | + if not float_compare(markup, new_markup, |
1030 | + precision_digits=_prec(self, cr, uid)) == 0: |
1031 | + res['value'].update({ |
1032 | + 'markup_rate': new_markup, |
1033 | + 'break_onchange_markup_rate': True, |
1034 | + }) |
1035 | return res |
1036 | |
1037 | === modified file 'sale_markup/sale_view.xml' |
1038 | --- sale_markup/sale_view.xml 2012-07-23 12:01:18 +0000 |
1039 | +++ sale_markup/sale_view.xml 2014-06-10 15:19:47 +0000 |
1040 | @@ -7,13 +7,15 @@ |
1041 | <!-- Add markup rate of the order in form view after Compute button --> |
1042 | <record model="ir.ui.view" id="sale_markup_sale_order_view"> |
1043 | <field name="name">sale.order.markup.view.form</field> |
1044 | - <field name="type">form</field> |
1045 | <field name="model">sale.order</field> |
1046 | <field name="inherit_id" ref="sale.view_order_form" /> |
1047 | <field name="arch" type="xml"> |
1048 | - <xpath expr="//button[@name='button_dummy']" position="before"> |
1049 | - <field name="markup_rate" |
1050 | - groups="base.group_sale_manager"/> |
1051 | + <xpath expr="//group[@name='sale_total']" position="after"> |
1052 | + <group class="oe_subtotal_footer oe_right" colspan="2" name="sale_markup" |
1053 | + groups="base.group_sale_manager"> |
1054 | + <field name="markup_rate" |
1055 | + groups="base.group_sale_manager"/> |
1056 | + </group> |
1057 | </xpath> |
1058 | </field> |
1059 | </record> |
1060 | @@ -21,7 +23,6 @@ |
1061 | <!-- Add markup rate of the order in tree view after state field --> |
1062 | <record model="ir.ui.view" id="sale_markup_sale_order_tree"> |
1063 | <field name="name">sale.order.markup.view.tree</field> |
1064 | - <field name="type">tree</field> |
1065 | <field name="model">sale.order</field> |
1066 | <field name="inherit_id" ref="sale.view_order_tree" /> |
1067 | <field name="arch" type="xml"> |
1068 | @@ -34,7 +35,6 @@ |
1069 | |
1070 | <record model="ir.ui.view" id="sale_markup_sale_order_line_tree"> |
1071 | <field name="name">sale.order.line.markup.view.tree</field> |
1072 | - <field name="type">tree</field> |
1073 | <field name="model">sale.order.line</field> |
1074 | <field name="inherit_id" ref="sale.view_order_line_tree" /> |
1075 | <field name="arch" type="xml"> |
1076 | @@ -52,37 +52,41 @@ |
1077 | <!-- Add onchanges on unit price and discount to update margin and markup --> |
1078 | <record model="ir.ui.view" id="sale_markup_sale_order_form_line_form"> |
1079 | <field name="name">sale.order.markup.view.form2</field> |
1080 | - <field name="type">form</field> |
1081 | <field name="model">sale.order</field> |
1082 | <field name="inherit_id" ref="sale.view_order_form" /> |
1083 | <field name="arch" type="xml"> |
1084 | - <xpath expr="//field[@name='product_id']" |
1085 | - position="replace"> |
1086 | - <field colspan="4" |
1087 | - name="product_id" |
1088 | - context="{'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'shop':parent.shop_id, 'uom':product_uom}" |
1089 | - on_change="product_id_change(parent.pricelist_id,product_id,product_uom_qty,product_uom,product_uos_qty,product_uos,name,parent.partner_id, False, True, parent.date_order, product_packaging, parent.fiscal_position, False, price_unit, discount, context)" /> |
1090 | - <newline/> |
1091 | - </xpath> |
1092 | - <xpath expr="/form/notebook/page/field[@name='order_line']/form/notebook/page/group/field[@name='name']" |
1093 | - position="attributes"> |
1094 | - <attribute name="colspan">5</attribute> |
1095 | - </xpath> |
1096 | - <xpath expr="/form/notebook/page/field[@name='order_line']/form/notebook/page/group/field[@name='name']" |
1097 | + <xpath expr="//field[@name='order_line']/form//field[@name='product_id']" |
1098 | + position="attributes"> |
1099 | + <attribute name="context_markup">{'discount': discount, 'price_unit': price_unit, 'markup_rate': markup_rate, 'commercial_margin': commercial_margin}</attribute> |
1100 | + </xpath> |
1101 | + <xpath expr="//field[@name='order_line']/form//field[@name='price_unit']" |
1102 | + position="attributes"> |
1103 | + <attribute name="context_markup">{'price_unit': price_unit, 'product_id': product_id, 'discount': discount, 'product_uom': product_uom, 'pricelist': parent.pricelist_id, 'markup_rate': markup_rate, 'commercial_margin': commercial_margin}</attribute> |
1104 | + </xpath> |
1105 | + <xpath expr="//field[@name='order_line']/form//field[@name='discount']" |
1106 | + position="attributes"> |
1107 | + <attribute name="context_markup">{'product_id': product_id, 'price_unit': price_unit, 'discount': discount, 'product_uom': product_uom, 'pricelist': parent.pricelist_id, 'markup_rate': markup_rate, 'commercial_margin': commercial_margin, 'break_onchange': break_onchange_discount}</attribute> |
1108 | + </xpath> |
1109 | + <xpath expr="//field[@name='order_line']/form//field[@name='name']" |
1110 | position="after"> |
1111 | - <separator string="Markup" colspan="5" groups="base.group_sale_manager"/> |
1112 | + <group string="Markup" groups="base.group_sale_manager"> |
1113 | <field name="commercial_margin" |
1114 | - on_change="onchange_commercial_margin(commercial_margin, cost_price, price_unit)" |
1115 | + context="{'commercial_margin': commercial_margin, 'markup_rate': markup_rate, 'discount': discount, 'cost_price': cost_price, 'price_unit': price_unit, 'break_onchange': break_onchange_commercial_margin}" |
1116 | + on_change="onchange_commercial_margin(context)" |
1117 | groups="base.group_sale_manager"/> |
1118 | <field name="markup_rate" |
1119 | - on_change="onchange_markup_rate(markup_rate, cost_price, price_unit)" |
1120 | + context="{'markup_rate': markup_rate, 'commercial_margin': commercial_margin, 'discount': discount, 'cost_price': cost_price, 'price_unit': price_unit, 'break_onchange': break_onchange_markup_rate}" |
1121 | + on_change="onchange_markup_rate(context)" |
1122 | groups="base.group_sale_manager"/> |
1123 | <field name="cost_price" |
1124 | groups="base.group_sale_manager" invisible="1"/> |
1125 | - <newline/> |
1126 | + <field name="break_onchange_commercial_margin" invisible="1"/> |
1127 | + <field name="break_onchange_markup_rate" invisible="1"/> |
1128 | + <field name="break_onchange_discount" invisible="1"/> |
1129 | + </group> |
1130 | </xpath> |
1131 | - <xpath expr="//field[@name='product_uom_qty']" position="attributes"> |
1132 | - <attribute name="on_change">product_id_change(parent.pricelist_id,product_id,product_uom_qty,product_uom,product_uos_qty,product_uos,name,parent.partner_id,False,True,parent.date_order,product_packaging,parent.fiscal_position,False,price_unit,discount,context)</attribute> |
1133 | + <xpath expr="//field[@name='order_line']/form//field[@name='product_uom_qty']" position="attributes"> |
1134 | + <attribute name="context_markup">{'discount': discount, 'price_unit': price_unit, 'markup_rate': markup_rate, 'commercial_margin': commercial_margin}</attribute> |
1135 | </xpath> |
1136 | </field> |
1137 | </record> |
1138 | @@ -90,12 +94,11 @@ |
1139 | <!-- Add Markup in Sales Orders form's Sale order lines tree --> |
1140 | <record model="ir.ui.view" id="sale_markup_sale_order_form_line_tree"> |
1141 | <field name="name">sale.order.markup.view.form</field> |
1142 | - <field name="type">form</field> |
1143 | <field name="model">sale.order</field> |
1144 | <field name="inherit_id" ref="sale.view_order_form" /> |
1145 | <field name="arch" type="xml"> |
1146 | <xpath expr="//field[@name='order_line']/tree/field[@name='price_subtotal']" |
1147 | - position="after"> |
1148 | + position="after"> |
1149 | <field name="cost_price" |
1150 | groups="base.group_sale_manager"/> |
1151 | <field name="markup_rate" |
1152 | |
1153 | === added directory 'sale_markup/tests' |
1154 | === added file 'sale_markup/tests/__init__.py' |
1155 | --- sale_markup/tests/__init__.py 1970-01-01 00:00:00 +0000 |
1156 | +++ sale_markup/tests/__init__.py 2014-06-10 15:19:47 +0000 |
1157 | @@ -0,0 +1,21 @@ |
1158 | +# -*- coding: utf-8 -*- |
1159 | +############################################################################## |
1160 | +# |
1161 | +# Author: Yannick Vaucher |
1162 | +# Copyright 2014 Camptocamp SA |
1163 | +# |
1164 | +# This program is free software: you can redistribute it and/or modify |
1165 | +# it under the terms of the GNU Affero General Public License as |
1166 | +# published by the Free Software Foundation, either version 3 of the |
1167 | +# License, or (at your option) any later version. |
1168 | +# |
1169 | +# This program is distributed in the hope that it will be useful, |
1170 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1171 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1172 | +# GNU Affero General Public License for more details. |
1173 | +# |
1174 | +# You should have received a copy of the GNU Affero General Public License |
1175 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1176 | +# |
1177 | +############################################################################## |
1178 | +from . import test_sale_markup |
1179 | |
1180 | === added file 'sale_markup/tests/test_sale_markup.py' |
1181 | --- sale_markup/tests/test_sale_markup.py 1970-01-01 00:00:00 +0000 |
1182 | +++ sale_markup/tests/test_sale_markup.py 2014-06-10 15:19:47 +0000 |
1183 | @@ -0,0 +1,197 @@ |
1184 | +# -*- coding: utf-8 -*- |
1185 | +############################################################################## |
1186 | +# |
1187 | +# Author: Yannick Vaucher |
1188 | +# Copyright 2014 Camptocamp SA |
1189 | +# |
1190 | +# This program is free software: you can redistribute it and/or modify |
1191 | +# it under the terms of the GNU Affero General Public License as |
1192 | +# published by the Free Software Foundation, either version 3 of the |
1193 | +# License, or (at your option) any later version. |
1194 | +# |
1195 | +# This program is distributed in the hope that it will be useful, |
1196 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1197 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1198 | +# GNU Affero General Public License for more details. |
1199 | +# |
1200 | +# You should have received a copy of the GNU Affero General Public License |
1201 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1202 | +# |
1203 | +############################################################################## |
1204 | +import openerp.tests.common as common |
1205 | +from openerp.addons import get_module_resource |
1206 | + |
1207 | + |
1208 | +def _trigger_on_changes(self, cr, uid, sale_order, view_values, changed_values): |
1209 | + triggered_on_changes = [] |
1210 | + while [i for i in changed_values.keys() if i not in triggered_on_changes]: |
1211 | + res_values = {} |
1212 | + for field in view_values.keys(): |
1213 | + if field in triggered_on_changes: |
1214 | + continue |
1215 | + triggered_on_changes.append(field) |
1216 | + if field == 'product_id': |
1217 | + res_values = self.SaleOrderLine.product_id_change( |
1218 | + cr, uid, False, |
1219 | + sale_order.pricelist_id.id, |
1220 | + view_values.get('product_id'), |
1221 | + qty=view_values.get('product_uom_qty'), |
1222 | + uom=False, |
1223 | + qty_uos=view_values.get('product_uos_qty'), |
1224 | + uos=False, |
1225 | + name='sol_test_1', |
1226 | + partner_id=sale_order.partner_id.id, |
1227 | + lang=False, |
1228 | + update_tax=True, |
1229 | + date_order=sale_order.date_order, |
1230 | + packaging=False, |
1231 | + fiscal_position=sale_order.fiscal_position.id, |
1232 | + flag=False, |
1233 | + context=view_values) |
1234 | + break |
1235 | + elif field == 'price_unit': |
1236 | + res_values = self.SaleOrderLine.onchange_price_unit(cr, uid, False, context=view_values) |
1237 | + break |
1238 | + elif field == 'discount': |
1239 | + res_values = self.SaleOrderLine.onchange_discount(cr, uid, False, context=view_values) |
1240 | + break |
1241 | + elif field == 'commercial_margin': |
1242 | + res_values = self.SaleOrderLine.onchange_commercial_margin(cr, uid, False, context=view_values) |
1243 | + break |
1244 | + elif field == 'markup_rate': |
1245 | + res_values = self.SaleOrderLine.onchange_markup_rate(cr, uid, False, context=view_values) |
1246 | + break |
1247 | + if res_values: |
1248 | + changed_values.update(res_values['value']) |
1249 | + view_values.update(res_values['value']) |
1250 | + return view_values |
1251 | + |
1252 | +class test_sale_markup(common.TransactionCase): |
1253 | + """ Test the wizard for delivery carrier label generation """ |
1254 | + |
1255 | + def setUp(self): |
1256 | + super(test_sale_markup, self).setUp() |
1257 | + cr, uid = self.cr, self.uid |
1258 | + |
1259 | + self.SaleOrder = self.registry('sale.order') |
1260 | + self.SaleOrderLine = self.registry('sale.order.line') |
1261 | + self.Product = self.registry('product.product') |
1262 | + self.product_33 = self.Product.browse( |
1263 | + cr, uid, self.ref('product.product_product_33')) |
1264 | + self.ResPartner = self.registry('res.partner') |
1265 | + self.partner_12 = self.ResPartner.browse( |
1266 | + cr, uid, self.ref('base.res_partner_12')) |
1267 | + |
1268 | + def test_00_create_sale_order(self): |
1269 | + """ Check markup computing in sale order |
1270 | + |
1271 | + And check each on_changes |
1272 | + """ |
1273 | + cr, uid = self.cr, self.uid |
1274 | + |
1275 | + so_data = {'partner_id': self.partner_12.id, |
1276 | + } |
1277 | + res = self.SaleOrder.onchange_partner_id(cr, uid, False, self.partner_12.id) |
1278 | + so_data.update(res['value']) |
1279 | + |
1280 | + so_1_id = self.SaleOrder.create(cr, uid, so_data) |
1281 | + so_1 = self.SaleOrder.browse(cr, uid, so_1_id) |
1282 | + |
1283 | + ctx = {'product_id': self.product_33.id, |
1284 | + 'product_uom': self.ref('product.product_uom_unit'), |
1285 | + 'discount': 0.0, |
1286 | + 'product_uom_qty': 1, |
1287 | + 'product_uos_qty': 1, |
1288 | + 'sequence': 10, |
1289 | + 'state': 'draft', |
1290 | + 'type': 'make_to_stock', |
1291 | + 'price_unit': 0.0, |
1292 | + 'order_id': so_1_id, |
1293 | + } |
1294 | + |
1295 | + # I set product A on sale order and trigger the on_change on product. |
1296 | + res = self.SaleOrderLine.product_id_change( |
1297 | + cr, uid, False, |
1298 | + so_1.pricelist_id.id, |
1299 | + ctx.get('product_id'), |
1300 | + qty=ctx.get('product_uom_qty'), |
1301 | + uom=False, |
1302 | + qty_uos=ctx.get('product_uos_qty'), |
1303 | + uos=False, |
1304 | + name='sol_test_1', |
1305 | + partner_id=so_1.partner_id.id, |
1306 | + lang=False, |
1307 | + update_tax=True, |
1308 | + date_order=so_1.date_order, |
1309 | + packaging=False, |
1310 | + fiscal_position=so_1.fiscal_position.id, |
1311 | + flag=False, |
1312 | + context=ctx) |
1313 | + |
1314 | + ctx.update(res['value']) |
1315 | + res = _trigger_on_changes(self, cr, uid, so_1, ctx, res['value']) |
1316 | + # cost_price should be set and equal to product cost price. |
1317 | + assert abs(res.get('cost_price') - self.Product.get_cost_field(cr, uid, self.product_33.id)[self.product_33.id]) < 0.01 |
1318 | + |
1319 | + # commercial_margin should be updated and equal to price_unit * (1 - (discount / 100.0)) - cost_price |
1320 | + commercial_margin = (ctx.get('price_unit') * (1 - (ctx.get('discount') / 100.0)) - ctx.get('cost_price')) |
1321 | + assert abs(ctx.get('commercial_margin') - commercial_margin) < 0.01, "Commercial margin is %s instead of %s after update of product_id" % (ctx.get('commercial_margin'), commercial_margin) |
1322 | + # markup_rate should be updated and equal to commercial_margin / (price_unit * (1 - (discount / 100.0))) |
1323 | + markup_rate = (ctx.get('commercial_margin') / (ctx.get('price_unit') * (1 - (ctx.get('discount') / 100.0))) * 100.0) |
1324 | + assert abs(ctx.get('markup_rate') - markup_rate) < 0.01, "Markup rate is %s instead of %s after update of product_id" % (ctx.get('markup_rate'), markup_rate) |
1325 | + |
1326 | + # I add 1 percent to discount and trigger the on_change on discount. |
1327 | + ctx['discount'] = 1.0 |
1328 | + res = self.SaleOrderLine.onchange_discount(cr, uid, False, context=ctx) |
1329 | + ctx.update(res['value']) |
1330 | + ctx = _trigger_on_changes(self, cr, uid, so_1, ctx, res['value']) |
1331 | + |
1332 | + # commercial_margin should be updated and equal to price_unit * (1 - (discount / 100.0)) - cost_price |
1333 | + commercial_margin = (ctx.get('price_unit') * (1 - (ctx.get('discount') / 100.0)) - ctx.get('cost_price')) |
1334 | + assert abs(ctx.get('commercial_margin') - commercial_margin) < 0.01, "Commercial margin is %s instead of %s after update of discount" % (ctx.get('commercial_margin'), commercial_margin) |
1335 | + |
1336 | + # markup_rate should be updated and equal to commercial_margin / (price_unit * (1 - (discount / 100.0))) |
1337 | + markup_rate = (ctx.get('commercial_margin') / (ctx.get('price_unit') * (1 - (ctx.get('discount') / 100.0))) * 100.0) |
1338 | + assert abs(ctx.get('markup_rate') - markup_rate) < 0.01, "Markup rate is %s instead of %s after update of discount" % (ctx.get('markup_rate'), markup_rate) |
1339 | + |
1340 | + # I change the markup rate to 20.0 and trigger the on_change on markup_rate. |
1341 | + ctx['markup_rate'] = 20.0 |
1342 | + res = self.SaleOrderLine.onchange_markup_rate(cr, uid, False, context=ctx) |
1343 | + ctx.update(res['value']) |
1344 | + ctx = _trigger_on_changes(self, cr, uid, so_1, ctx, res['value']) |
1345 | + |
1346 | + # commercial_margin should be updated and equal to price_unit * (1 - (discount / 100.0)) - cost_price |
1347 | + commercial_margin = (ctx.get('price_unit') * (1 - (ctx.get('discount') / 100.0)) - ctx.get('cost_price')) |
1348 | + assert abs(ctx.get('commercial_margin') - commercial_margin) < 0.01, "Commercial margin is %s instead of %s after update of markup_rate" % (ctx.get('commercial_margin'), commercial_margin) |
1349 | + |
1350 | + # markup_rate should be updated and equal to commercial_margin / (price_unit * (1 - (discount / 100.0))) |
1351 | + markup_rate = (ctx.get('commercial_margin') / (ctx.get('price_unit') * (1 - (ctx.get('discount') / 100.0))) * 100.0) |
1352 | + assert abs(ctx.get('markup_rate') - markup_rate) < 0.01, "Markup rate is %s instead of %s after update of markup_rate" % (ctx.get('markup_rate'), markup_rate) |
1353 | + |
1354 | + |
1355 | + # I change the price unit to 2000.0 and trigger the on_change on price_unit. |
1356 | + ctx['price_unit'] = 2000.0 |
1357 | + res = self.SaleOrderLine.onchange_price_unit(cr, uid, False, context=ctx) |
1358 | + ctx.update(res['value']) |
1359 | + ctx = _trigger_on_changes(self, cr, uid, so_1, ctx, res['value']) |
1360 | + |
1361 | + # commercial_margin should be updated and equal to price_unit * (1 - (discount / 100.0)) - cost_price |
1362 | + commercial_margin = (ctx.get('price_unit') * (1 - (ctx.get('discount') / 100.0)) - ctx.get('cost_price')) |
1363 | + assert abs(ctx.get('commercial_margin') - commercial_margin) < 0.01, "Commercial margin is %s instead of %s after update of price_unit" % (ctx.get('commercial_margin'), commercial_margin) |
1364 | + |
1365 | + # markup_rate should be updated and equal to commercial_margin / (price_unit * (1 - (discount / 100.0))) |
1366 | + markup_rate = (ctx.get('commercial_margin') / (ctx.get('price_unit') * (1 - (ctx.get('discount') / 100.0))) * 100.0) |
1367 | + assert abs(ctx.get('markup_rate') - markup_rate) < 0.01, "Markup rate is %s instead of %s after update of price_unit" % (ctx.get('markup_rate'), markup_rate) |
1368 | + |
1369 | + sol_data = ctx |
1370 | + |
1371 | + # I create the sale order line for the sale order. |
1372 | + sol_1_id = self.SaleOrderLine.create( |
1373 | + cr, uid, |
1374 | + sol_data |
1375 | + ) |
1376 | + |
1377 | + so_1.refresh() |
1378 | + assert so_1.markup_rate |
1379 | + # as we have only one line it should be equal to our last line markup |
1380 | + assert abs(so_1.markup_rate - markup_rate) < 0.01 |
Hello,
The management of the project has moved to Github: https:/ /github. com/OCA/ sale-financial
Please migrate your merge proposal to Github. You may want to check https:/ /github. com/OCA/ maintainers- tools/wiki/ How-to- move-a- Merge-Proposal- to-GitHub for an explanation on how to proceed.
Thanks for contributing to the project