Merge lp:~therp-nl/additional-discount/fix_lp1102075_move_lines into lp:additional-discount

Proposed by Ronald Portier (Therp)
Status: Needs review
Proposed branch: lp:~therp-nl/additional-discount/fix_lp1102075_move_lines
Merge into: lp:additional-discount
Diff against target: 270 lines (+163/-22)
5 files modified
additional_discount/__init__.py (+2/-3)
additional_discount/__openerp__.py (+3/-1)
additional_discount/model/__init__.py (+13/-0)
additional_discount/model/common.py (+6/-5)
additional_discount/model/invoice.py (+139/-13)
To merge this branch: bzr merge lp:~therp-nl/additional-discount/fix_lp1102075_move_lines
Reviewer Review Type Date Requested Status
Georges Racinet Pending
Anybox Pending
Stefan Rijnhart (Opener) Pending
Review via email: mp+161957@code.launchpad.net

This proposal supersedes a proposal from 2013-05-01.

Description of the change

The original merge proposal to generate additional move lines for the discount has been extensively reworked:

- Adjust the original move lines instead of creating additional ones. In this way the line that should be paid will have an amount equal to an incoming or outgoing payment. This makes for easier reconciliation.

- Rounding had been made more consistent all over

- Nevertheless an catch all test has been added to prevent the additional discount from ever creating an unbalanced move

To post a comment you must log in.
Revision history for this message
Stefan Rijnhart (Opener) (stefan-opener) wrote : Posted in a previous version of this proposal

It is probably a good idea to have a look at rounding issues now. They can get ugly really easily. At least make sure that the amount to pay is the same as on the invoice that goes out to the customer.

Another thing to consider: if the move line's account is set to reconcile, would it be a good idea to do a partial reconciliation with its discount line at this point? This would require some work in the method cancelling the invoice because you would have to undo any move internal reconciliation before the move line object protests against unlinking reconciled items.

review: Needs Fixing
Revision history for this message
Christophe Combelles (ccomb) wrote : Posted in a previous version of this proposal

Hi, thanks for the work on this package,
I currently have no time for reviewing, I've changed the project settings to allow you (or someone else) to merge or push another branch in ~additional-discount (actually I hope I succeeded in doing so)

Unmerged revisions

31. By Ronald Portier (Therp)

[FIX] Previous revision moved view xml, but forgot to adapt __openerp__.py

30. By Ronald Portier (Therp)

[RFR] Added standard organization to module
[FIX] Rounding issues
[FIX] Prevent additional discount from creating unbalanced moves

29. By Ronald Portier (Therp)

[IMP] Generate move-lines for discount.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'additional_discount/__init__.py'
2--- additional_discount/__init__.py 2011-05-04 10:37:12 +0000
3+++ additional_discount/__init__.py 2013-05-01 21:40:37 +0000
4@@ -1,3 +1,2 @@
5-import sale
6-import purchase
7-import invoice
8\ No newline at end of file
9+import model
10+
11
12=== modified file 'additional_discount/__openerp__.py'
13--- additional_discount/__openerp__.py 2012-09-04 07:09:37 +0000
14+++ additional_discount/__openerp__.py 2013-05-01 21:40:37 +0000
15@@ -10,7 +10,9 @@
16 Additional discount is fully integrated between sales, purchase and invoices.
17 """,
18 "init_xml": [],
19- 'update_xml': ['additional_discount_view.xml'],
20+ 'update_xml': [
21+ 'view/additional_discount_view.xml'
22+ ],
23 'demo_xml': [],
24 'test': [
25 'test/scenario1.yml',
26
27=== added directory 'additional_discount/i18n'
28=== added directory 'additional_discount/model'
29=== added file 'additional_discount/model/__init__.py'
30--- additional_discount/model/__init__.py 1970-01-01 00:00:00 +0000
31+++ additional_discount/model/__init__.py 2013-05-01 21:40:37 +0000
32@@ -0,0 +1,13 @@
33+# -*- coding: UTF-8 -*-
34+'''
35+Created on 16 mrt. 2013
36+
37+@author: Ronald Portier, Therp
38+
39+rportier@therp.nl
40+http://www.therp.nl
41+
42+'''
43+import sale
44+import purchase
45+import invoice
46
47=== renamed file 'additional_discount/common.py' => 'additional_discount/model/common.py'
48--- additional_discount/common.py 2012-09-10 16:22:19 +0000
49+++ additional_discount/model/common.py 2013-05-01 21:40:37 +0000
50@@ -47,14 +47,15 @@
51 amount_untaxed = sum(line.price_subtotal
52 for line in getattr(record, self._line_column))
53 add_disc = record.add_disc
54- add_disc_amt = cur_round(amount_untaxed * add_disc / 100)
55- o_res['add_disc_amt'] = add_disc_amt
56- o_res['amount_net'] = o_res['amount_untaxed'] - add_disc_amt
57+ discount_factor = 1 - (add_disc / 100)
58+ o_res['amount_net'] = cur_round(
59+ o_res['amount_untaxed'] * discount_factor)
60+ o_res['add_disc_amt'] = (
61+ o_res['amount_untaxed'] - o_res['amount_net'])
62
63 if self._amount_all_discount_taxes:
64- # XXX We might have rounding issue
65 o_res['amount_tax'] = cur_round(
66- o_res['amount_tax'] * (100.0 - (add_disc or 0.0))/100.0)
67+ o_res['amount_tax'] * discount_factor)
68 o_res['amount_total'] = o_res['amount_net'] + o_res['amount_tax']
69
70 return res
71
72=== renamed file 'additional_discount/invoice.py' => 'additional_discount/model/invoice.py'
73--- additional_discount/invoice.py 2012-09-10 16:22:19 +0000
74+++ additional_discount/model/invoice.py 2013-05-01 21:40:37 +0000
75@@ -1,17 +1,26 @@
76+# -*- coding: utf-8 -*-
77+import logging
78+
79 import decimal_precision as dp
80-from osv import fields, osv
81+
82+from openerp.osv import fields
83+from openerp.osv import orm
84+from openerp.tools.translate import _
85+from openerp.tools.float_utils import float_round
86+
87 from common import AdditionalDiscountable
88-from tools.translate import _
89-
90-class account_invoice(AdditionalDiscountable, osv.Model):
91-
92- _inherit = "account.invoice"
93+
94+class account_invoice(AdditionalDiscountable, orm.Model):
95+
96+ _inherit = 'account.invoice'
97 _description = 'Invoice'
98
99 _line_column = 'invoice_line'
100 _tax_column = 'invoice_line_tax_id'
101 _amount_all_discount_taxes = False # done and written on tax lines
102
103+ __logger = logging.getLogger(_inherit)
104+
105 def record_currency(self, record):
106 """Return currency browse record from an invoice record (override)."""
107 return record.currency_id
108@@ -32,6 +41,118 @@
109 for tax in self.pool.get('account.invoice.tax').browse(cr, uid, ids, context=context):
110 result[tax.invoice_id.id] = True
111 return result.keys()
112+
113+
114+ def finalize_invoice_move_lines(self, cr, uid, invoice_browse, move_lines):
115+ """finalize_invoice_move_lines(cr, uid, invoice, move_lines) -> move_lines
116+ Hook method to be overridden in additional modules to verify and possibly alter the
117+ move lines to be created by an invoice, for special cases.
118+ :param invoice_browse: browsable record of the invoice that is generating the move lines
119+ :param move_lines: list of dictionaries with the account.move.lines (as for create())
120+ :return: the (possibly updated) final move_lines to create for this invoice
121+
122+ Actually move_lines is a list op tuples with each tuple having three
123+ elements. The third element is the value dictionary used to create one
124+ line. So move_lines will look like this:
125+ [(0, 0, {'debit': ......}), (0, 0, {<values for second line>}), ..]
126+
127+ Taxes are a special problem, because when this function is called,
128+ the tax move lines have the discount already applied. There is no
129+ easy way to recogize these lines, therefore we retrieve all tax
130+ account_id's used and exclude them from being discounted (again).
131+ """
132+ discount = invoice_browse.add_disc
133+ if not discount:
134+ return move_lines
135+ # Only if there is a additional discount will we loop
136+ # over all lines to adjust the amounts
137+ # We assume a line will nly contain either credit, or debit,
138+ # never both.
139+ precision_model = self.pool.get('decimal.precision')
140+ precision = precision_model.precision_get(cr, uid, 'Account')
141+ discount_factor = 1.0 - (discount / 100.0)
142+ balance_credit = True
143+ total_credit = 0.0
144+ total_debit = 0.0
145+ # Get taxt account id's
146+ tax_account_ids = []
147+ ait_model = self.pool.get('account.invoice.tax')
148+ ait_lines = ait_model.move_line_get(cr, uid, invoice_browse.id)
149+ for ait_line in ait_lines:
150+ tax_account_ids.append(ait_line['account_id'])
151+ for move_line in move_lines:
152+ # Check format
153+ assert len(move_line) > 2, (
154+ _('Invalid move line %s') % str(move_line))
155+ vals = move_line[2]
156+ # Do not change tax lines (but include them in totals):
157+ if vals['account_id'] in tax_account_ids:
158+ if vals['debit']:
159+ total_debit += vals['debit']
160+ if vals['credit']:
161+ total_credit += vals['credit']
162+ continue
163+ # Handle debtor/creditor
164+ # We don't want to recompute to make sure open amount will
165+ # be exactly the same as on invoice.
166+ if invoice_browse.account_id.id == vals['account_id']:
167+ if vals['debit']:
168+ assert not vals['credit'], (
169+ _('Can not have credit and debit in same move line'))
170+ vals['debit'] = invoice_browse.amount_total
171+ total_debit += vals['debit']
172+ else:
173+ assert not vals['debit'], (
174+ _('Can not have debit and credit in same move line'))
175+ vals['credit'] = invoice_browse.amount_total
176+ total_credit += vals['credit']
177+ balance_credit = False
178+ else:
179+ if vals['credit']:
180+ vals['credit'] = float_round(
181+ vals['credit'] * discount_factor, precision)
182+ total_credit += vals['credit']
183+ if vals['debit']:
184+ vals['debit'] = float_round(
185+ vals['debit'] * discount_factor, precision)
186+ total_debit += vals['debit']
187+ if vals['amount_currency']:
188+ vals['amount_currency'] = float_round(
189+ vals['amount_currency'] * discount_factor, precision)
190+ if vals['tax_amount']:
191+ vals['tax_amount'] = float_round(
192+ vals['tax_amount'] * discount_factor, precision)
193+ # Check balance
194+ difference = total_debit - total_credit
195+ if abs(difference) > 10 ** -4:
196+ # Find largest credit or debit amount and adjust for rounding:
197+ largest_vals = None
198+ largest_amount = 0.0
199+ for move_line in move_lines:
200+ vals = move_line[2]
201+ if balance_credit:
202+ if vals['credit'] and vals['credit'] > largest_amount:
203+ largest_vals = vals
204+ else:
205+ if vals['debit'] and vals['debit'] > largest_amount:
206+ largest_vals = vals
207+ assert largest_vals, _('No largest debit or credit found')
208+ if balance_credit:
209+ largest_vals['credit'] = (
210+ largest_vals['credit'] + difference)
211+ if largest_vals['tax_amount']:
212+ largest_vals['tax_amount'] = (
213+ largest_vals['tax_amount'] + difference)
214+ else:
215+ largest_vals['debit'] = (
216+ largest_vals['debit'] - difference)
217+ if largest_vals['tax_amount']:
218+ largest_vals['tax_amount'] = (
219+ largest_vals['tax_amount'] - difference)
220+ self.__logger.info(
221+ _('Modified move line %s to prevent unbalanced move with'
222+ ' difference %d') % (str(largest_vals), difference))
223+ return move_lines
224
225 _columns={
226 'add_disc':fields.float('Additional Discount(%)',digits=(4,2),readonly=True, states={'draft':[('readonly',False)]}),
227@@ -68,12 +189,15 @@
228 }
229
230
231-class account_invoice_tax(osv.Model):
232+class account_invoice_tax(orm.Model):
233
234 _inherit = 'account.invoice.tax'
235
236 def compute(self, cr, uid, invoice_id, context=None):
237- tax_grouped = super(account_invoice_tax, self).compute(cr, uid, invoice_id, context)
238+ precision_model = self.pool.get('decimal.precision')
239+ precision = precision_model.precision_get(cr, uid, 'Account')
240+ tax_grouped = super(
241+ account_invoice_tax, self).compute(cr, uid, invoice_id, context)
242 tax_pool = self.pool.get('account.tax')
243 cur_pool = self.pool.get('res.currency')
244 tax_ids = set([key[0] for key in tax_grouped])
245@@ -82,13 +206,15 @@
246 raise osv.except_osv(_('Discount error'),
247 _('Unable (for now) to compute a global '
248 'discount with non percent-type taxes'))
249- invoice = self.pool.get('account.invoice').browse(cr, uid, invoice_id, context=context)
250+ invoice = self.pool.get('account.invoice').browse(
251+ cr, uid, invoice_id, context=context)
252 add_disc = invoice.add_disc
253- cur = invoice.currency_id
254+ discount_factor = 1.0 - (add_disc / 100.0)
255 for line in tax_grouped:
256- for key in ('tax_amount', 'base_amount', 'amount', 'base'): #FIXME?
257+ for key in ('tax_amount', 'base_amount', 'amount', 'base'):
258 val = tax_grouped[line][key]
259- tax_grouped[line][key] = cur_pool.round(cr, uid, cur, val * (1.0 - add_disc / 100.0))
260- print tax_grouped
261+ tax_grouped[line][key] = float_round(
262+ val * discount_factor, precision)
263+ # print tax_grouped
264 return tax_grouped
265
266
267=== renamed file 'additional_discount/purchase.py' => 'additional_discount/model/purchase.py'
268=== renamed file 'additional_discount/sale.py' => 'additional_discount/model/sale.py'
269=== added directory 'additional_discount/view'
270=== renamed file 'additional_discount/additional_discount_view.xml' => 'additional_discount/view/additional_discount_view.xml'

Subscribers

People subscribed via source and target branches

to all changes: