Merge lp:~camptocamp-reviewer/banking-addons/fix-type-and-account-in-bk-st-line into lp:banking-addons/bank-statement-reconcile-70

Proposed by Nicolas Bessi - Camptocamp
Status: Merged
Merged at revision: 90
Proposed branch: lp:~camptocamp-reviewer/banking-addons/fix-type-and-account-in-bk-st-line
Merge into: lp:banking-addons/bank-statement-reconcile-70
Diff against target: 1631 lines (+575/-420)
9 files modified
account_statement_base_completion/__openerp__.py (+4/-0)
account_statement_base_completion/statement.py (+217/-203)
account_statement_base_import/parser/generic_file_parser.py (+7/-8)
account_statement_base_import/statement.py (+121/-54)
account_statement_ext/__openerp__.py (+4/-7)
account_statement_ext/i18n/fr.po (+2/-2)
account_statement_ext/statement.py (+184/-99)
account_statement_transactionid_completion/statement.py (+24/-29)
account_statement_transactionid_import/parser/transactionid_file_parser.py (+12/-18)
To merge this branch: bzr merge lp:~camptocamp-reviewer/banking-addons/fix-type-and-account-in-bk-st-line
Reviewer Review Type Date Requested Status
Alexandre Fayolle - camptocamp code review, no test Approve
Vincent Renaville@camptocamp Pending
Nicolas Bessi - Camptocamp Pending
Review via email: mp+160591@code.launchpad.net

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

Description of the change

Major refactoring of statement import and completion:

Fix the way to look default account on partner:
- If master account is provided in profile it will be forced
 - If the customer checkbox is checked on the found partner, type and account will be customer and receivable
 - If the supplier checkbox is checked on the found partner, type and account will be supplier and payable
 - If both checkbox are checked or none of them, it'll be based on the amount :
    If amount is positif the type and account will be customer and receivable,
    If amount is negativ, the type and account will be supplier and payable

Completion:
-Fix and refactor the the invoices lookup for completion
-Various fixes in completion rules
- Non matches lines are not mark as completed.

Optimisation:

Refactoring of statement import:
We by pass ORM to increase performances.
TODO support of sparse field

Refactoring of completion.
We have done some structural changes in order to avoid a lot of un needed call to ORM.
Bypass orm when writing to database.

These merge is required in order to fix transaction id completion rules and import +
Handle the new semantic change in OpenERP partner model

To post a comment you must log in.
Revision history for this message
Vincent Renaville@camptocamp (vrenaville-c2c) wrote : Posted in a previous version of this proposal

Hello,

just a little typo error at line 23, remove q letter before tag

review: Needs Fixing
116. By Nicolas Bessi - Camptocamp

[FIX] transaction_id completion to support optimization

117. By Nicolas Bessi - Camptocamp

[FIX] transaction id import styling

118. By Nicolas Bessi - Camptocamp

[FIX] incoherence in type lookup behavior

Revision history for this message
Alexandre Fayolle - camptocamp (alexandre-fayolle-c2c) wrote :

General: no space before "!" "?" ":" or ";" in english -> fix needed for the error messages

line 148-149: I find this message confusing, since we are not searching by partner. This message is repeated in several places in the code.

line 418: is there not a security rules bypass here ? Eg in a multicompany context with non shares res.partners, I can imagine the query returning several rows, yet only one partner being availalble to the current company.

line 424: the else False part will never execute : the function has returned on line 419.
line 425: this test is always true (the execution of line 424 sets res to an non empty dictionary, which is True)

line 487-492: there is still a little cleanup to be done here: the previous logic was handling several lines in a loop (hence the need for error_stack). The current logic deals with a single line, so error_stack can be removed and the exception raised in the except block. In the same function, I'd change the "return res" statements to "return {}" as res is never changed.

line 519: use log_line.insert(0, value) to insert a single element at the beginning of the line (I have not seen anything indicating the the RHS arg is multi element list).

line 598: leftover print statement

line 874: no batch update is performed. Docstring update required :-)

line 952: any reason for doing this in a loop rather than using UPDATE account_bank_statement_line SET sequence = account_bank_statement_line.id + 1 WHERE statement_id in %(list_of_ids)s" ?

review: Needs Fixing (code review, no test)
Revision history for this message
Nicolas Bessi - Camptocamp (nbessi-c2c-deactivatedaccount) wrote :

Hello,

Many thanks for your review.

Line 148-149 the message is done this way because even if the lookup is done on invoice, it's the partner we try to complete. But that right we can add the name of the rule inside the message.

For the rest I agree with you I will do the fixes ASAP.

119. By Nicolas Bessi - Camptocamp

[IMP] completion messages

120. By Nicolas Bessi - Camptocamp

[FIX] by partner name completion rule if statement and wrong variable

121. By Nicolas Bessi - Camptocamp

[FIX] dead if statement

122. By Nicolas Bessi - Camptocamp

[FIX] dead error management code

123. By Nicolas Bessi - Camptocamp

[IMP] return value

124. By Nicolas Bessi - Camptocamp

[IMP] log code cleanup

125. By Nicolas Bessi - Camptocamp

[FIX] comment and forgotten print statement

126. By Nicolas Bessi - Camptocamp

[IMP] no loop when computing sequence

127. By Nicolas Bessi - Camptocamp

[TYPO]

128. By Nicolas Bessi - Camptocamp

[FIX] partner name lookup now respect security rules

129. By Nicolas Bessi - Camptocamp

[TYPO]

130. By Nicolas Bessi - Camptocamp

[FIX] po file

131. By Nicolas Bessi - Camptocamp

[IMP] message logging

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

Corrections applied.

Alexandre please feel free to do an english correction round. This way I can regenerate PO files.

Regards

Nicolas

132. By Alexandre Fayolle - camptocamp

[IMP] fix a few docstrings

small code improvement in account_statement_transactionid_completion/statement.py AccountStatementCompletionRule.get_from_transaction_id_and_so)

Revision history for this message
Alexandre Fayolle - camptocamp (alexandre-fayolle-c2c) wrote :

account_statement_base_completion/statement.py, AccountStatementProfil._find_values_from_rules

* the docstring does not match the arguments of the method (no st_line, and calls and line are not described)
* the "if not calls branch" will crash, it uses 'id' which is a Python built in function, not shadowed by a local variable here

account_statement_base_completion/statement.py, AccountStatementCompletionRule.get_from_label_and_partner_name:

* security rule bypass still there AFAICT

I've pushed a few changes and improvements, don't forget to pull before fixing the points above)

review: Needs Fixing (code review, no test)
133. By Nicolas Bessi - Camptocamp

[FIX] wrong argument id instead of line['profile_id']

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

Hello,

Many thanks for the attentive review. I have pushed the last fixes

Normally get_from_label_and_partner_name should not bypass security rules as the memoizer partner set is limited by the first search statement:
 partner_ids = partner_obj.search(cr,
                                  uid,
                                  [('bank_statement_label', '!=', False)])

Revision history for this message
Alexandre Fayolle - camptocamp (alexandre-fayolle-c2c) wrote :

LGTM.

review: Approve (code review, no test)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'account_statement_base_completion/__openerp__.py'
2--- account_statement_base_completion/__openerp__.py 2013-01-10 15:09:55 +0000
3+++ account_statement_base_completion/__openerp__.py 2013-04-25 11:52:27 +0000
4@@ -52,6 +52,10 @@
5
6 You can use it with our account_advanced_reconcile module to automatize the reconciliation process.
7
8+
9+ TODO: The rules that look for invoices to find out the partner should take back the payable / receivable
10+ account from there directly instead of retrieving it from partner properties !
11+
12 """,
13 'website': 'http://www.camptocamp.com',
14 'init_xml': [],
15
16=== modified file 'account_statement_base_completion/statement.py'
17--- account_statement_base_completion/statement.py 2013-03-22 08:31:27 +0000
18+++ account_statement_base_completion/statement.py 2013-04-25 11:52:27 +0000
19@@ -18,15 +18,21 @@
20 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 #
22 ##############################################################################
23+# TODO replace customer supplier by package constant
24+import traceback
25+import sys
26+import logging
27+
28 from collections import defaultdict
29 import re
30-
31 from tools.translate import _
32-from openerp.osv.orm import Model, fields
33+from openerp.osv import osv, orm, fields
34 from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT
35 from operator import attrgetter
36 import datetime
37
38+_logger = logging.getLogger(__name__)
39+
40
41 class ErrorTooManyPartner(Exception):
42 """
43@@ -40,7 +46,7 @@
44 return repr(self.value)
45
46
47-class AccountStatementProfil(Model):
48+class AccountStatementProfil(orm.Model):
49 """
50 Extend the class to add rules per profile that will match at least the partner,
51 but it could also be used to match other values as well.
52@@ -49,11 +55,11 @@
53 _inherit = "account.statement.profile"
54
55 _columns = {
56- # @Akretion : For now, we don't implement this features, but this would probably be there:
57+ # @Akretion: For now, we don't implement this features, but this would probably be there:
58 # 'auto_completion': fields.text('Auto Completion'),
59 # 'transferts_account_id':fields.many2one('account.account', 'Transferts Account'),
60 # => You can implement it in a module easily, we design it with your needs in mind
61- # as well !
62+ # as well!
63
64 'rule_ids': fields.many2many(
65 'account.statement.completion.rule',
66@@ -61,36 +67,45 @@
67 rel='as_rul_st_prof_rel'),
68 }
69
70- def find_values_from_rules(self, cr, uid, id, line_id, context=None):
71+ def _get_callable(self, cr, uid, profile, context=None):
72+ if isinstance(profile, (int, long)):
73+ prof = self.browse(cr, uid, profile, context=context)
74+ else:
75+ prof = profile
76+ # We need to respect the sequence order
77+ sorted_array = sorted(prof.rule_ids, key=attrgetter('sequence'))
78+ return tuple((x.function_to_call for x in sorted_array))
79+
80+ def _find_values_from_rules(self, cr, uid, calls, line, context=None):
81 """
82 This method will execute all related rules, in their sequence order,
83 to retrieve all the values returned by the first rules that will match.
84-
85- :param int/long line_id: id of the concerned account.bank.statement.line
86+ :param calls: list of lookup function name available in rules
87+ :param dict line: read of the concerned account.bank.statement.line
88 :return:
89 A dict of value that can be passed directly to the write method of
90 the statement line or {}
91 {'partner_id': value,
92- 'account_id' : value,
93+ 'account_id: value,
94
95 ...}
96 """
97 if context is None:
98 context = {}
99- res = {}
100+ if not calls:
101+ calls = self._get_callable(cr, uid, line['profile_id'], context=context)
102 rule_obj = self.pool.get('account.statement.completion.rule')
103- profile = self.browse(cr, uid, id, context=context)
104- # We need to respect the sequence order
105- sorted_array = sorted(profile.rule_ids, key=attrgetter('sequence'))
106- for rule in sorted_array:
107- method_to_call = getattr(rule_obj, rule.function_to_call)
108- result = method_to_call(cr, uid, line_id, context)
109+
110+ for call in calls:
111+ method_to_call = getattr(rule_obj, call)
112+ result = method_to_call(cr, uid, line, context)
113 if result:
114+ result['already_completed'] = True
115 return result
116- return res
117-
118-
119-class AccountStatementCompletionRule(Model):
120+ return None
121+
122+
123+class AccountStatementCompletionRule(orm.Model):
124 """
125 This will represent all the completion method that we can have to
126 fullfill the bank statement lines. You'll be able to extend them in you own module
127@@ -109,12 +124,11 @@
128 List of available methods for rules. Override this to add you own.
129 """
130 return [
131- ('get_from_ref_and_invoice', 'From line reference (based on invoice number)'),
132+ ('get_from_ref_and_invoice', 'From line reference (based on customer invoice number)'),
133 ('get_from_ref_and_supplier_invoice', 'From line reference (based on supplier invoice number)'),
134 ('get_from_ref_and_so', 'From line reference (based on SO number)'),
135 ('get_from_label_and_partner_field', 'From line label (based on partner field)'),
136- ('get_from_label_and_partner_name', 'From line label (based on partner name)'),
137- ]
138+ ('get_from_label_and_partner_name', 'From line label (based on partner name)')]
139
140 _columns = {
141 'sequence': fields.integer('Sequence', help="Lower means parsed first."),
142@@ -126,134 +140,129 @@
143 'function_to_call': fields.selection(_get_functions, 'Method'),
144 }
145
146- def get_from_ref_and_supplier_invoice(self, cr, uid, line_id, context=None):
147+ def _find_invoice(self, cr, uid, st_line, inv_type, context=None):
148+ """Find invoice related to statement line"""
149+ inv_obj = self.pool.get('account.invoice')
150+ if inv_type == 'supplier':
151+ type_domain = ('in_invoice', 'in_refund')
152+ number_field = 'supplier_invoice_number'
153+ elif inv_type == 'customer':
154+ type_domain = ('out_invoice', 'out_refund')
155+ number_field = 'number'
156+ else:
157+ raise osv.except_osv(_('System error'),
158+ _('Invalid invoice type for completion: %') % inv_type)
159+
160+ inv_id = inv_obj.search(cr, uid,
161+ [(number_field, '=', st_line['ref'].strip()),
162+ ('type', 'in', type_domain)],
163+ context=context)
164+ if inv_id:
165+ if len(inv_id) == 1:
166+ inv = inv_obj.browse(cr, uid, inv_id[0], context=context)
167+ else:
168+ raise ErrorTooManyPartner(_('Line named "%s" (Ref:%s) was matched by more '
169+ 'than one partner while looking on %s invoices') %
170+ (st_line['name'], st_line['ref'], inv_type))
171+ return inv
172+ return False
173+
174+ def _from_invoice(self, cr, uid, line, inv_type, context):
175+ """Populate statement line values"""
176+ if not inv_type in ('supplier', 'customer'):
177+ raise osv.except_osv(_('System error'),
178+ _('Invalid invoice type for completion: %') % inv_type)
179+ res = {}
180+ inv = self._find_invoice(cr, uid, line, inv_type, context=context)
181+ if inv:
182+ res = {'partner_id': inv.partner_id.id,
183+ 'account_id': inv.account_id.id,
184+ 'type': inv_type}
185+ override_acc = line['master_account_id']
186+ if override_acc:
187+ res['account_id'] = override_acc
188+ return res
189+
190+ # Should be private but data are initialised with no update XML
191+ def get_from_ref_and_supplier_invoice(self, cr, uid, line, context=None):
192 """
193 Match the partner based on the invoice supplier invoice number and the reference of the statement
194 line. Then, call the generic get_values_for_line method to complete other values.
195 If more than one partner matched, raise the ErrorTooManyPartner error.
196
197- :param int/long line_id: id of the concerned account.bank.statement.line
198+ :param dict line: read of the concerned account.bank.statement.line
199 :return:
200 A dict of value that can be passed directly to the write method of
201 the statement line or {}
202 {'partner_id': value,
203- 'account_id' : value,
204+ 'account_id': value,
205
206 ...}
207 """
208- st_obj = self.pool['account.bank.statement.line']
209- st_line = st_obj.browse(cr, uid, line_id, context=context)
210- res = {}
211- inv_obj = self.pool.get('account.invoice')
212- if st_line:
213- inv_id = inv_obj.search(cr,
214- uid,
215- [('supplier_invoice_number', '=', st_line.ref),
216- ('type', 'in', ('in_invoice', 'in_refund'))],
217- context=context)
218- if inv_id:
219- if len(inv_id) == 1:
220- inv = inv_obj.browse(cr, uid, inv_id[0], context=context)
221- res['partner_id'] = inv.partner_id.id
222- else:
223- raise ErrorTooManyPartner(_('Line named "%s" (Ref:%s) was matched by more '
224- 'than one partner.') % (st_line.name, st_line.ref))
225- st_vals = st_obj.get_values_for_line(cr,
226- uid,
227- profile_id=st_line.statement_id.profile_id.id,
228- partner_id=res.get('partner_id', False),
229- line_type="supplier",
230- amount=st_line.amount,
231- context=context)
232- res.update(st_vals)
233- return res
234+ return self._from_invoice(cr, uid, line, 'supplier', context=context)
235
236- def get_from_ref_and_invoice(self, cr, uid, line_id, context=None):
237+ # Should be private but data are initialised with no update XML
238+ def get_from_ref_and_invoice(self, cr, uid, line, context=None):
239 """
240 Match the partner based on the invoice number and the reference of the statement
241 line. Then, call the generic get_values_for_line method to complete other values.
242 If more than one partner matched, raise the ErrorTooManyPartner error.
243
244- :param int/long line_id: id of the concerned account.bank.statement.line
245+ :param dict line: read of the concerned account.bank.statement.line
246 :return:
247 A dict of value that can be passed directly to the write method of
248 the statement line or {}
249 {'partner_id': value,
250- 'account_id' : value,
251+ 'account_id': value,
252 ...}
253 """
254- st_obj = self.pool.get('account.bank.statement.line')
255- st_line = st_obj.browse(cr, uid, line_id, context=context)
256- res = {}
257- if st_line:
258- inv_obj = self.pool.get('account.invoice')
259- inv_id = inv_obj.search(cr,
260- uid,
261- [('number', '=', st_line.ref)],
262- context=context)
263- if inv_id:
264- if len(inv_id) == 1:
265- inv = inv_obj.browse(cr, uid, inv_id[0], context=context)
266- res['partner_id'] = inv.partner_id.id
267- else:
268- raise ErrorTooManyPartner(_('Line named "%s" (Ref:%s) was matched by more '
269- 'than one partner.') % (st_line.name, st_line.ref))
270- st_vals = st_obj.get_values_for_line(cr,
271- uid,
272- profile_id=st_line.statement_id.profile_id.id,
273- partner_id=res.get('partner_id', False),
274- line_type=st_line.type,
275- amount=st_line.amount,
276- context=context)
277- res.update(st_vals)
278- return res
279+ return self._from_invoice(cr, uid, line, 'customer', context=context)
280
281- def get_from_ref_and_so(self, cr, uid, line_id, context=None):
282+ # Should be private but data are initialised with no update XML
283+ def get_from_ref_and_so(self, cr, uid, st_line, context=None):
284 """
285 Match the partner based on the SO number and the reference of the statement
286 line. Then, call the generic get_values_for_line method to complete other values.
287 If more than one partner matched, raise the ErrorTooManyPartner error.
288
289- :param int/long line_id: id of the concerned account.bank.statement.line
290+ :param int/long st_line: read of the concerned account.bank.statement.line
291 :return:
292 A dict of value that can be passed directly to the write method of
293 the statement line or {}
294 {'partner_id': value,
295- 'account_id' : value,
296+ 'account_id': value,
297
298 ...}
299 """
300 st_obj = self.pool.get('account.bank.statement.line')
301- st_line = st_obj.browse(cr, uid, line_id, context=context)
302 res = {}
303 if st_line:
304 so_obj = self.pool.get('sale.order')
305- so_id = so_obj.search(
306- cr,
307- uid,
308- [('name', '=', st_line.ref)],
309- context=context)
310+ so_id = so_obj.search(cr,
311+ uid,
312+ [('name', '=', st_line['ref'])],
313+ context=context)
314 if so_id:
315 if so_id and len(so_id) == 1:
316 so = so_obj.browse(cr, uid, so_id[0], context=context)
317 res['partner_id'] = so.partner_id.id
318 elif so_id and len(so_id) > 1:
319- raise ErrorTooManyPartner(
320- _('Line named "%s" (Ref:%s) was matched by more '
321- 'than one partner.') %
322- (st_line.name, st_line.ref))
323- st_vals = st_obj.get_values_for_line(
324- cr,
325- uid,
326- profile_id=st_line.statement_id.profile_id.id,
327- partner_id=res.get('partner_id', False),
328- line_type=st_line.type,
329- amount=st_line.amount,
330- context=context)
331+ raise ErrorTooManyPartner(_('Line named "%s" (Ref:%s) was matched by more '
332+ 'than one partner while looking on SO by ref.') %
333+ (st_line['name'], st_line['ref']))
334+ st_vals = st_obj.get_values_for_line(cr,
335+ uid,
336+ profile_id=st_line['profile_id'],
337+ master_account_id=st_line['master_account_id'],
338+ partner_id=res.get('partner_id', False),
339+ line_type='customer',
340+ amount=st_line['amount'] if st_line['amount'] else 0.0,
341+ context=context)
342 res.update(st_vals)
343 return res
344
345- def get_from_label_and_partner_field(self, cr, uid, line_id, context=None):
346+ # Should be private but data are initialised with no update XML
347+ def get_from_label_and_partner_field(self, cr, uid, st_line, context=None):
348 """
349 Match the partner based on the label field of the statement line
350 and the text defined in the 'bank_statement_label' field of the partner.
351@@ -261,12 +270,12 @@
352 get_values_for_line method to complete other values.
353 If more than one partner matched, raise the ErrorTooManyPartner error.
354
355- :param int/long line_id: id of the concerned account.bank.statement.line
356+ :param dict st_line: read of the concerned account.bank.statement.line
357 :return:
358 A dict of value that can be passed directly to the write method of
359 the statement line or {}
360 {'partner_id': value,
361- 'account_id' : value,
362+ 'account_id': value,
363
364 ...}
365 """
366@@ -283,7 +292,7 @@
367 partner_ids = partner_obj.search(cr,
368 uid,
369 [('bank_statement_label', '!=', False)])
370- line_ids = tuple(x.id for x in context.get('line_ids', []))
371+ line_ids = context.get('line_ids', [])
372 for partner in partner_obj.browse(cr, uid, partner_ids, context=context):
373 vals = '|'.join(re.escape(x.strip()) for x in partner.bank_statement_label.split(';'))
374 or_regex = ".*%s*." % vals
375@@ -294,70 +303,71 @@
376 pairs = cr.fetchall()
377 for pair in pairs:
378 context['label_memoizer'][pair[0]].append(partner)
379- st_line = st_obj.browse(cr, uid, line_id, context=context)
380- if st_line and st_line.id in context['label_memoizer']:
381- found_partner = context['label_memoizer'][st_line.id]
382+ if st_line['id'] in context['label_memoizer']:
383+ found_partner = context['label_memoizer'][st_line['id']]
384 if len(found_partner) > 1:
385 raise ErrorTooManyPartner(_('Line named "%s" (Ref:%s) was matched by '
386- 'more than one partner.') %
387- (st_line.name, st_line.ref))
388+ 'more than one partner while looking on partner label') %
389+ (st_line['name'], st_line['ref']))
390 res['partner_id'] = found_partner[0].id
391 st_vals = st_obj.get_values_for_line(cr,
392 uid,
393- profile_id=st_line.statement_id.profile_id.id,
394+ profile_id=st_line['profile_id'],
395+ master_account_id=st_line['master_account_id'],
396 partner_id=found_partner[0].id,
397- line_type=st_line.type,
398- amount=st_line.amount,
399+ line_type=False,
400+ amount=st_line['amount'] if st_line['amount'] else 0.0,
401 context=context)
402 res.update(st_vals)
403 return res
404
405- def get_from_label_and_partner_name(self, cr, uid, line_id, context=None):
406+ def get_from_label_and_partner_name(self, cr, uid, st_line, context=None):
407 """
408 Match the partner based on the label field of the statement line
409 and the name of the partner.
410 Then, call the generic get_values_for_line method to complete other values.
411 If more than one partner matched, raise the ErrorTooManyPartner error.
412
413- :param int/long line_id: id of the concerned account.bank.statement.line
414+ :param dict st_line: read of the concerned account.bank.statement.line
415 :return:
416 A dict of value that can be passed directly to the write method of
417 the statement line or {}
418 {'partner_id': value,
419- 'account_id' : value,
420+ 'account_id': value,
421
422 ...}
423 """
424- # This Method has not been tested yet !
425 res = {}
426+ # We memoize allowed partner
427+ if not context.get('partner_memoizer'):
428+ context['partner_memoizer'] = tuple(self.pool['res.partner'].search(cr, uid, []))
429+ if not context['partner_memoizer']:
430+ return res
431 st_obj = self.pool.get('account.bank.statement.line')
432- st_line = st_obj.browse(cr, uid, line_id, context=context)
433- if st_line:
434- sql = "SELECT id FROM res_partner WHERE name ~* %s"
435- pattern = ".*%s.*" % re.escape(st_line.label)
436- cr.execute(sql, (pattern,))
437- result = cr.fetchall()
438- if not result:
439- return res
440- if len(result) > 1:
441- raise ErrorTooManyPartner(_('Line named "%s" (Ref:%s) was matched by more '
442- 'than one partner.') %
443- (st_line.name, st_line.ref))
444- res['partner_id'] = result[0][0] if result else False
445- if res:
446- st_vals = st_obj.get_values_for_line(
447- cr,
448- uid,
449- profile_id=st_line.statement_id.profile_id.id,
450- partner_id=res['partner_id'],
451- line_type=st_line.type,
452- amount=st_line.amount,
453- context=context)
454- res.update(st_vals)
455+ sql = "SELECT id FROM res_partner WHERE name ~* %s and id in %s"
456+ pattern = ".*%s.*" % re.escape(st_line['name'])
457+ cr.execute(sql, (pattern, context['partner_memoizer']))
458+ result = cr.fetchall()
459+ if not result:
460+ return res
461+ if len(result) > 1:
462+ raise ErrorTooManyPartner(_('Line named "%s" (Ref:%s) was matched by more '
463+ 'than one partner while looking on partner by name') %
464+ (st_line['name'], st_line['ref']))
465+ res['partner_id'] = result[0][0]
466+ st_vals = st_obj.get_values_for_line(cr,
467+ uid,
468+ profile_id=st_line['porfile_id'],
469+ master_account_id=st_line['master_account_id'],
470+ partner_id=res['partner_id'],
471+ line_type=False,
472+ amount=st_line['amount'] if st_line['amount'] else 0.0,
473+ context=context)
474+ res.update(st_vals)
475 return res
476
477
478-class AccountStatementLine(Model):
479+class AccountStatementLine(orm.Model):
480 """
481 Add sparse field on the statement line to allow to store all the
482 bank infos that are given by a bank/office. You can then add you own in your
483@@ -390,7 +400,7 @@
484 'already_completed': False,
485 }
486
487- def get_line_values_from_rules(self, cr, uid, ids, context=None):
488+ def _get_line_values_from_rules(self, cr, uid, line, rules, context=None):
489 """
490 We'll try to find out the values related to the line based on rules setted on
491 the profile.. We will ignore line for which already_completed is ticked.
492@@ -401,36 +411,17 @@
493 {117009: {'partner_id': 100997, 'account_id': 489L}}
494 """
495 profile_obj = self.pool.get('account.statement.profile')
496- st_obj = self.pool.get('account.bank.statement.line')
497- res = {}
498- errors_stack = []
499- for line in self.browse(cr, uid, ids, context=context):
500- if line.already_completed:
501- continue
502- try:
503- # Take the default values
504- res[line.id] = st_obj.get_values_for_line(
505- cr,
506- uid,
507- profile_id=line.statement_id.profile_id.id,
508- line_type=line.type,
509- amount=line.amount,
510- context=context)
511- # Ask the rule
512- vals = profile_obj.find_values_from_rules(
513- cr, uid, line.statement_id.profile_id.id, line.id, context)
514- # Merge the result
515- res[line.id].update(vals)
516- except ErrorTooManyPartner, exc:
517- msg = "Line ID %s had following error: %s" % (line.id, exc.value)
518- errors_stack.append(msg)
519- if errors_stack:
520- msg = u"\n".join(errors_stack)
521- raise ErrorTooManyPartner(msg)
522- return res
523-
524-
525-class AccountBankSatement(Model):
526+ if line.get('already_completed'):
527+ return {}
528+ # Ask the rule
529+ vals = profile_obj._find_values_from_rules(cr, uid, rules, line, context)
530+ if vals:
531+ vals['id'] = line['id']
532+ return vals
533+ return {}
534+
535+
536+class AccountBankSatement(orm.Model):
537 """
538 We add a basic button and stuff to support the auto-completion
539 of the bank statement once line have been imported or manually fullfill.
540@@ -450,59 +441,82 @@
541 :param int/long stat_id: ID of the account.bank.statement
542 :param char error_msg: Message to add
543 :number_imported int/long: Number of lines that have been completed
544- :return : True
545+ :return True
546 """
547- error_log = ""
548- user_name = self.pool.get('res.users').read(
549- cr, uid, uid, ['name'], context=context)['name']
550- log = self.read(
551- cr, uid, stat_id, ['completion_logs'], context=context)['completion_logs']
552- log_line = log and log.split("\n") or []
553+ user_name = self.pool.get('res.users').read(cr, uid, uid,
554+ ['name'], context=context)['name']
555+
556+ log = self.read(cr, uid, stat_id, ['completion_logs'],
557+ context=context)['completion_logs']
558+ log = log if log else ""
559+
560 completion_date = datetime.datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT)
561- if error_msg:
562- error_log = error_msg
563- log_line[0:0] = [completion_date + ' : '
564- + _("Bank Statement ID %s has %s lines completed by %s") % (stat_id, number_imported, user_name)
565- + "\n" + error_log + "-------------" + "\n"]
566- log = "\n".join(log_line)
567- self.write(cr, uid, [stat_id], {'completion_logs': log}, context=context)
568- self.message_post(
569- cr, uid,
570- [stat_id],
571- body=_('Statement ID %s auto-completed for %s lines completed') % (stat_id, number_imported),
572- context=context)
573+ message = (_("%s Bank Statement ID %s has %s lines completed by %s \n%s\n") %
574+ (completion_date, stat_id, number_imported, user_name, log))
575+ self.write(cr, uid, [stat_id], {'completion_logs': message}, context=context)
576+
577+ body = (_('Statement ID %s auto-completed for %s lines completed') %
578+ (stat_id, number_imported)),
579+ self.message_post(cr, uid,
580+ [stat_id],
581+ body=body,
582+ context=context)
583 return True
584
585 def button_auto_completion(self, cr, uid, ids, context=None):
586 """
587 Complete line with values given by rules and tic the already_completed
588- checkbox so we won't compute them again unless the user untick them !
589+ checkbox so we won't compute them again unless the user untick them!
590 """
591 if context is None:
592 context = {}
593- stat_line_obj = self.pool.get('account.bank.statement.line')
594+ stat_line_obj = self.pool['account.bank.statement.line']
595+ profile_obj = self.pool.get('account.statement.profile')
596 compl_lines = 0
597+ stat_line_obj.check_access_rule(cr, uid, [], 'create')
598+ stat_line_obj.check_access_rights(cr, uid, 'create', raise_exception=True)
599 for stat in self.browse(cr, uid, ids, context=context):
600 msg_lines = []
601 ctx = context.copy()
602- ctx['line_ids'] = stat.line_ids
603- for line in stat.line_ids:
604- res = {}
605+ ctx['line_ids'] = tuple((x.id for x in stat.line_ids))
606+ b_profile = stat.profile_id
607+ rules = profile_obj._get_callable(cr, uid, b_profile, context=context)
608+ profile_id = b_profile.id # Only for perfo even it gains almost nothing
609+ master_account_id = b_profile.receivable_account_id
610+ master_account_id = master_account_id.id if master_account_id else False
611+ res = False
612+ for line in stat_line_obj.read(cr, uid, ctx['line_ids']):
613 try:
614- res = stat_line_obj.get_line_values_from_rules(
615- cr, uid, [line.id], context=ctx)
616+ # performance trick
617+ line['master_account_id'] = master_account_id
618+ line['profile_id'] = profile_id
619+ res = stat_line_obj._get_line_values_from_rules(cr, uid, line,
620+ rules, context=ctx)
621 if res:
622 compl_lines += 1
623 except ErrorTooManyPartner, exc:
624 msg_lines.append(repr(exc))
625 except Exception, exc:
626 msg_lines.append(repr(exc))
627- # vals = res and res.keys() or False
628+ error_type, error_value, trbk = sys.exc_info()
629+ st = "Error: %s\nDescription: %s\nTraceback:" % (error_type.__name__, error_value)
630+ st += ''.join(traceback.format_tb(trbk, 30))
631+ _logger.error(st)
632 if res:
633- vals = res[line.id]
634- vals['already_completed'] = True
635- stat_line_obj.write(cr, uid, [line.id], vals, context=ctx)
636+ #stat_line_obj.write(cr, uid, [line.id], vals, context=ctx)
637+ try:
638+ stat_line_obj._update_line(cr, uid, res, context=context)
639+ except Exception as exc:
640+ msg_lines.append(repr(exc))
641+ error_type, error_value, trbk = sys.exc_info()
642+ st = "Error: %s\nDescription: %s\nTraceback:" % (error_type.__name__, error_value)
643+ st += ''.join(traceback.format_tb(trbk, 30))
644+ _logger.error(st)
645+ # we can commit as it is not needed to be atomic
646+ # commiting here adds a nice perfo boost
647+ if not compl_lines % 500:
648+ cr.commit()
649 msg = u'\n'.join(msg_lines)
650 self.write_completion_log(cr, uid, stat.id,
651- msg, compl_lines, context=context)
652+ msg, compl_lines, context=context)
653 return True
654
655=== modified file 'account_statement_base_import/parser/generic_file_parser.py'
656--- account_statement_base_import/parser/generic_file_parser.py 2013-02-26 08:22:25 +0000
657+++ account_statement_base_import/parser/generic_file_parser.py 2013-04-25 11:52:27 +0000
658@@ -29,6 +29,7 @@
659 except:
660 raise Exception(_('Please install python lib xlrd'))
661
662+
663 def float_or_zero(val):
664 """ Conversion function used to manage
665 empty string into float usecase"""
666@@ -82,14 +83,12 @@
667 In this generic parser, the commission is given for every line, so we store it
668 for each one.
669 """
670- return {
671- 'name': line.get('label', line.get('ref', '/')),
672- 'date': line.get('date', datetime.datetime.now().date()),
673- 'amount': line.get('amount', 0.0),
674- 'ref': line.get('ref', '/'),
675- 'label': line.get('label', ''),
676- 'commission_amount': line.get('commission_amount', 0.0),
677- }
678+ return {'name': line.get('label', line.get('ref', '/')),
679+ 'date': line.get('date', datetime.datetime.now().date()),
680+ 'amount': line.get('amount', 0.0),
681+ 'ref': line.get('ref', '/'),
682+ 'label': line.get('label', ''),
683+ 'commission_amount': line.get('commission_amount', 0.0)}
684
685 def _post(self, *args, **kwargs):
686 """
687
688=== modified file 'account_statement_base_import/statement.py'
689--- account_statement_base_import/statement.py 2013-02-26 08:45:03 +0000
690+++ account_statement_base_import/statement.py 2013-04-25 11:52:27 +0000
691@@ -18,14 +18,16 @@
692 # along with this program. If not, see <http://www.gnu.org/licenses/>.
693 #
694 ##############################################################################
695+import sys
696+import traceback
697+
698+import psycopg2
699
700 from openerp.tools.translate import _
701 import datetime
702 from openerp.osv.orm import Model
703 from openerp.osv import fields, osv
704 from parser import new_bank_statement_parser
705-import sys
706-import traceback
707
708
709 class AccountStatementProfil(Model):
710@@ -43,7 +45,8 @@
711 help="Tic that box to automatically launch the completion "
712 "on each imported file using this profile."),
713 'last_import_date': fields.datetime("Last Import Date"),
714- 'rec_log': fields.text('log', readonly=True, deprecated=True),
715+ # we remove deprecated as it floods logs in standard/warning level sob...
716+ 'rec_log': fields.text('log', readonly=True), # Deprecated
717 'import_type': fields.selection(
718 get_import_type_selection,
719 'Type of import',
720@@ -64,7 +67,8 @@
721 self.message_post(cr,
722 uid,
723 ids,
724- body=_('Statement ID %s have been imported with %s lines.') % (statement_id, num_lines),
725+ body=_('Statement ID %s have been imported with %s lines.') %
726+ (statement_id, num_lines),
727 context=context)
728 return True
729
730@@ -79,7 +83,7 @@
731 :param: browse_record of the current parser
732 :param: result_row_list: [{'key':value}]
733 :param: profile: browserecord of account.statement.profile
734- :param: statement_id : int/long of the current importing statement ID
735+ :param: statement_id: int/long of the current importing statement ID
736 :param: context: global context
737 return: dict of vals that will be passed to create method of statement line.
738 """
739@@ -98,7 +102,7 @@
740 'account_id': commission_account_id,
741 'ref': 'commission',
742 'analytic_account_id': commission_analytic_id,
743- # !! We set the already_completed so auto-completion will not update those values !
744+ # !! We set the already_completed so auto-completion will not update those values!
745 'already_completed': True,
746 }
747 return comm_values
748@@ -116,18 +120,32 @@
749 :param int/long account_payable: ID of the receivable account to use
750 :param int/long account_receivable: ID of the payable account to use
751 :param int/long statement_id: ID of the concerned account.bank.statement
752- :return : dict of vals that will be passed to create method of statement line.
753+ :return: dict of vals that will be passed to create method of statement line.
754 """
755 statement_obj = self.pool.get('account.bank.statement')
756 values = parser_vals
757 values['statement_id'] = statement_id
758- values['account_id'] = statement_obj.get_account_for_counterpart(
759- cr,
760- uid,
761- parser_vals['amount'],
762- account_receivable,
763- account_payable
764- )
765+ values['account_id'] = statement_obj.get_account_for_counterpart(cr,
766+ uid,
767+ parser_vals['amount'],
768+ account_receivable,
769+ account_payable)
770+
771+ date = values.get('date')
772+ period_memoizer = context.get('period_memoizer')
773+ if not period_memoizer:
774+ period_memoizer = {}
775+ context['period_memoizer'] = period_memoizer
776+ if period_memoizer.get(date):
777+ values['period_id'] = period_memoizer[date]
778+ else:
779+ # This is awfully slow...
780+ periods = self.pool.get('account.period').find(cr, uid,
781+ dt=values.get('date'),
782+ context=context)
783+ values['period_id'] = periods[0]
784+ period_memoizer[date] = periods[0]
785+ values['type'] = 'general'
786 return values
787
788 def statement_import(self, cr, uid, ids, profile_id, file_stream, ftype="csv", context=None):
789@@ -148,75 +166,85 @@
790 attachment_obj = self.pool.get('ir.attachment')
791 prof_obj = self.pool.get("account.statement.profile")
792 if not profile_id:
793- raise osv.except_osv(
794- _("No Profile !"),
795- _("You must provide a valid profile to import a bank statement !"))
796+ raise osv.except_osv(_("No Profile!"),
797+ _("You must provide a valid profile to import a bank statement!"))
798 prof = prof_obj.browse(cr, uid, profile_id, context=context)
799
800 parser = new_bank_statement_parser(prof.import_type, ftype=ftype)
801 result_row_list = parser.parse(file_stream)
802- # Check all key are present in account.bank.statement.line !!
803+ # Check all key are present in account.bank.statement.line!!
804+ if not result_row_list:
805+ raise osv.except_osv(_("Nothing to import"),
806+ _("The file is empty"))
807 parsed_cols = parser.get_st_line_vals(result_row_list[0]).keys()
808 for col in parsed_cols:
809 if col not in statement_line_obj._columns:
810- raise osv.except_osv(
811- _("Missing column !"),
812- _("Column %s you try to import is not "
813- "present in the bank statement line !") % col)
814+ raise osv.except_osv(_("Missing column!"),
815+ _("Column %s you try to import is not "
816+ "present in the bank statement line!") % col)
817
818- statement_id = statement_obj.create(
819- cr, uid, {'profile_id': prof.id}, context=context)
820- account_receivable, account_payable = statement_obj.get_default_pay_receiv_accounts(
821- cr, uid, context)
822+ statement_id = statement_obj.create(cr, uid,
823+ {'profile_id': prof.id},
824+ context=context)
825+ if prof.receivable_account_id:
826+ account_receivable = account_payable = prof.receivable_account_id.id
827+ else:
828+ account_receivable, account_payable = statement_obj.get_default_pay_receiv_accounts(
829+ cr, uid, context)
830 try:
831 # Record every line in the bank statement and compute the global commission
832 # based on the commission_amount column
833 statement_store = []
834 for line in result_row_list:
835 parser_vals = parser.get_st_line_vals(line)
836- values = self.prepare_statetement_lines_vals(
837- cr, uid, parser_vals, account_payable,
838- account_receivable, statement_id, context)
839- # we finally create the line in system
840- statement_store.append((0, 0, values))
841+ values = self.prepare_statetement_lines_vals(cr, uid, parser_vals, account_payable,
842+ account_receivable, statement_id, context)
843+ statement_store.append(values)
844+ # Hack to bypass ORM poor perfomance. Sob...
845+ statement_line_obj._insert_lines(cr, uid, statement_store, context=context)
846+
847 # Build and create the global commission line for the whole statement
848- statement_obj.write(cr, uid, [statement_id],
849- {'line_ids': statement_store}, context=context)
850 comm_vals = self.prepare_global_commission_line_vals(cr, uid, parser, result_row_list,
851 prof, statement_id, context)
852 if comm_vals:
853 statement_line_obj.create(cr, uid, comm_vals, context=context)
854-
855- attachment_obj.create(
856- cr,
857- uid,
858- {
859- 'name': 'statement file',
860- 'datas': file_stream,
861- 'datas_fname': "%s.%s" % (
862- datetime.datetime.now().date(),
863- ftype),
864- 'res_model': 'account.bank.statement',
865- 'res_id': statement_id,
866- },
867- context=context
868- )
869- # If user ask to launch completion at end of import, do it !
870+ else:
871+ # Trigger store field computation if someone has better idea
872+ start_bal = statement_obj.read(cr, uid, statement_id,
873+ ['balance_start'],
874+ context=context)
875+ start_bal = start_bal['balance_start']
876+ statement_obj.write(cr, uid, [statement_id],
877+ {'balance_start': start_bal})
878+
879+ attachment_obj.create(cr,
880+ uid,
881+ {'name': 'statement file',
882+ 'datas': file_stream,
883+ 'datas_fname': "%s.%s" % (
884+ datetime.datetime.now().date(),
885+ ftype),
886+ 'res_model': 'account.bank.statement',
887+ 'res_id': statement_id},
888+ context=context)
889+
890+ # If user ask to launch completion at end of import, do it!
891 if prof.launch_import_completion:
892 statement_obj.button_auto_completion(cr, uid, [statement_id], context)
893
894 # Write the needed log infos on profile
895- self.write_logs_after_import(
896- cr, uid, prof.id, statement_id, len(result_row_list), context)
897+ self.write_logs_after_import(cr, uid, prof.id,
898+ statement_id,
899+ len(result_row_list),
900+ context)
901
902 except Exception:
903 statement_obj.unlink(cr, uid, [statement_id], context=context)
904 error_type, error_value, trbk = sys.exc_info()
905 st = "Error: %s\nDescription: %s\nTraceback:" % (error_type.__name__, error_value)
906 st += ''.join(traceback.format_tb(trbk, 30))
907- raise osv.except_osv(
908- _("Statement import error"),
909- _("The statement cannot be created : %s") % st)
910+ raise osv.except_osv(_("Statement import error"),
911+ _("The statement cannot be created: %s") % st)
912 return statement_id
913
914
915@@ -228,6 +256,45 @@
916 """
917 _inherit = "account.bank.statement.line"
918
919+ def _get_available_columns(self, statement_store):
920+ """Return writeable by SQL columns"""
921+ statement_line_obj = self.pool['account.bank.statement.line']
922+ model_cols = statement_line_obj._columns
923+ avail = [k for k, col in model_cols.iteritems() if not hasattr(col, '_fnct')]
924+ keys = [k for k in statement_store[0].keys() if k in avail]
925+ keys.sort()
926+ return keys
927+
928+ def _insert_lines(self, cr, uid, statement_store, context=None):
929+ """ Do raw insert into database because ORM is awfully slow
930+ when doing batch write. It is a shame that batch function
931+ does not exist"""
932+ statement_line_obj = self.pool['account.bank.statement.line']
933+ statement_line_obj.check_access_rule(cr, uid, [], 'create')
934+ statement_line_obj.check_access_rights(cr, uid, 'create', raise_exception=True)
935+ cols = self._get_available_columns(statement_store)
936+ tmp_vals = (', '.join(cols), ', '.join(['%%(%s)s' % i for i in cols]))
937+ sql = "INSERT INTO account_bank_statement_line (%s) VALUES (%s);" % tmp_vals
938+ try:
939+ cr.executemany(sql, tuple(statement_store))
940+ except psycopg2.Error as sql_err:
941+ cr.rollback()
942+ raise osv.except_osv(_("ORM bypass error"),
943+ sql_err.pgerror)
944+
945+ def _update_line(self, cr, uid, vals, context=None):
946+ """ Do raw update into database because ORM is awfully slow
947+ when cheking security."""
948+ cols = self._get_available_columns([vals])
949+ tmp_vals = (', '.join(['%s = %%(%s)s' % (i, i) for i in cols]))
950+ sql = "UPDATE account_bank_statement_line SET %s where id = %%(id)s;" % tmp_vals
951+ try:
952+ cr.execute(sql, vals)
953+ except psycopg2.Error as sql_err:
954+ cr.rollback()
955+ raise osv.except_osv(_("ORM bypass error"),
956+ sql_err.pgerror)
957+
958 _columns = {
959 'commission_amount': fields.sparse(
960 type='float',
961
962=== modified file 'account_statement_ext/__openerp__.py'
963--- account_statement_ext/__openerp__.py 2013-02-14 08:13:49 +0000
964+++ account_statement_ext/__openerp__.py 2013-04-25 11:52:27 +0000
965@@ -25,11 +25,9 @@
966 'maintainer': 'Camptocamp',
967 'category': 'Finance',
968 'complexity': 'normal',
969- 'depends': [
970- 'account',
971- 'report_webkit',
972- 'account_voucher'
973- ],
974+ 'depends': ['account',
975+ 'report_webkit',
976+ 'account_voucher'],
977 'description': """
978 Improve the basic bank statement, by adding various new features,
979 and help dealing with huge volume of reconciliation through payment offices such as Paypal, Lazer,
980@@ -65,7 +63,6 @@
981 4) Remove the period on the bank statement, and compute it for each line based on their date instead.
982 It also adds this feature in the voucher in order to compute the period correctly.
983
984-
985 5) Cancelling a bank statement is much more easy and will cancel all related entries, unreconcile them,
986 and finally delete them.
987
988@@ -88,4 +85,4 @@
989 'auto_install': False,
990 'license': 'AGPL-3',
991 'active': False,
992-}
993+ }
994
995=== modified file 'account_statement_ext/i18n/fr.po'
996--- account_statement_ext/i18n/fr.po 2012-12-13 13:57:29 +0000
997+++ account_statement_ext/i18n/fr.po 2013-04-25 11:52:27 +0000
998@@ -41,7 +41,7 @@
999 #. module: account_statement_ext
1000 #: code:addons/account_statement_ext/statement.py:361
1001 #, python-format
1002-msgid "Configuration Error !"
1003+msgid "Configuration Error!"
1004 msgstr "Erreur de configuration !"
1005
1006 #. module: account_statement_ext
1007@@ -64,7 +64,7 @@
1008 #: code:addons/account_statement_ext/statement.py:307
1009 #: code:addons/account_statement_ext/statement.py:372
1010 #, python-format
1011-msgid "Error !"
1012+msgid "Error!"
1013 msgstr "Erreur !"
1014
1015 #. module: account_statement_ext
1016
1017=== modified file 'account_statement_ext/statement.py'
1018--- account_statement_ext/statement.py 2013-03-01 14:33:32 +0000
1019+++ account_statement_ext/statement.py 2013-04-25 11:52:27 +0000
1020@@ -1,4 +1,4 @@
1021-# -*- coding: utf-8 -*-
1022+#-*- coding: utf-8 -*-
1023 ##############################################################################
1024 #
1025 # Author: Nicolas Bessi, Joel Grand-Guillaume
1026@@ -18,12 +18,26 @@
1027 # along with this program. If not, see <http://www.gnu.org/licenses/>.
1028 #
1029 ##############################################################################
1030-
1031+import openerp.addons.account.account_bank_statement as stat_mod
1032 from openerp.osv.orm import Model
1033 from openerp.osv import fields, osv
1034 from openerp.tools.translate import _
1035
1036
1037+# Monkey patch to fix bad write implementation...
1038+def fixed_write(self, cr, uid, ids, vals, context=None):
1039+ """ Fix performance desing of original function
1040+ Ideally we should use a real PostgreSQL sequence or serial fields.
1041+ I will do it when I have time."""
1042+ res = super(stat_mod.account_bank_statement, self).write(cr, uid, ids,
1043+ vals, context=context)
1044+ cr.execute("UPDATE account_bank_statement_line"
1045+ " SET sequence = account_bank_statement_line.id + 1"
1046+ " where statement_id in %s", (tuple(ids),))
1047+ return res
1048+stat_mod.account_bank_statement.write = fixed_write
1049+
1050+
1051 class AccountStatementProfil(Model):
1052 """
1053 A Profile will contain all infos related to the type of
1054@@ -44,38 +58,45 @@
1055 "commission move (and optionaly on the counterpart "
1056 "of the intermediate/banking move if you tick the "
1057 "corresponding checkbox)."),
1058+
1059 'journal_id': fields.many2one(
1060 'account.journal',
1061 'Financial journal to use for transaction',
1062 required=True),
1063+
1064 'commission_account_id': fields.many2one(
1065 'account.account',
1066 'Commission account',
1067 required=True),
1068+
1069 'commission_analytic_id': fields.many2one(
1070 'account.analytic.account',
1071 'Commission analytic account'),
1072+
1073 'receivable_account_id': fields.many2one(
1074 'account.account',
1075 'Force Receivable/Payable Account',
1076 help="Choose a receivable account to force the default "
1077 "debit/credit account (eg. an intermediat bank account "
1078 "instead of default debitors)."),
1079+
1080 'force_partner_on_bank': fields.boolean(
1081 'Force partner on bank move',
1082 help="Tick that box if you want to use the credit "
1083 "institute partner in the counterpart of the "
1084 "intermediate/banking move."),
1085+
1086 'balance_check': fields.boolean(
1087 'Balance check',
1088 help="Tick that box if you want OpenERP to control "
1089 "the start/end balance before confirming a bank statement. "
1090- "If don't ticked, no balance control will be done."
1091- ),
1092- 'bank_statement_prefix': fields.char(
1093- 'Bank Statement Prefix', size=32),
1094- 'bank_statement_ids': fields.one2many(
1095- 'account.bank.statement', 'profile_id', 'Bank Statement Imported'),
1096+ "If don't ticked, no balance control will be done."),
1097+
1098+ 'bank_statement_prefix': fields.char('Bank Statement Prefix', size=32),
1099+
1100+ 'bank_statement_ids': fields.one2many('account.bank.statement',
1101+ 'profile_id',
1102+ 'Bank Statement Imported'),
1103 'company_id': fields.many2one('res.company', 'Company'),
1104 }
1105
1106@@ -86,7 +107,7 @@
1107 return True
1108
1109 _constraints = [
1110- (_check_partner, "You need to put a partner if you tic the 'Force partner on bank move' !", []),
1111+ (_check_partner, "You need to put a partner if you tic the 'Force partner on bank move'!", []),
1112 ]
1113
1114
1115@@ -166,11 +187,11 @@
1116 """
1117 for statement in self.browse(cr, uid, ids, context=context):
1118 if (statement.period_id and
1119- statement.company_id.id != statement.period_id.company_id.id):
1120+ statement.company_id.id != statement.period_id.company_id.id):
1121 return False
1122 for line in statement.line_ids:
1123 if (line.period_id and
1124- statement.company_id.id != line.period_id.company_id.id):
1125+ statement.company_id.id != line.period_id.company_id.id):
1126 return False
1127 return True
1128
1129@@ -244,8 +265,10 @@
1130 create the move from.
1131 :return: int/long of the res.partner to use as counterpart
1132 """
1133- bank_partner_id = super(AccountBankSatement, self).\
1134- _get_counter_part_partner(cr, uid, st_line, context=context)
1135+ bank_partner_id = super(AccountBankSatement, self)._get_counter_part_partner(cr,
1136+ uid,
1137+ st_line,
1138+ context=context)
1139 # get the right partner according to the chosen profil
1140 if st_line.statement_id.profile_id.force_partner_on_bank:
1141 bank_partner_id = st_line.statement_id.profile_id.partner_id.id
1142@@ -285,7 +308,7 @@
1143 We have to copy paste a big block of code, changing the error
1144 stack + managing period from date.
1145
1146- TODO: Log the error in a bank statement field instead of using a popup !
1147+ TODO: Log the error in a bank statement field instead of using a popup!
1148 """
1149 for st in self.browse(cr, uid, ids, context=context):
1150
1151@@ -297,8 +320,8 @@
1152 self.balance_check(cr, uid, st.id, journal_type=j_type, context=context)
1153 if (not st.journal_id.default_credit_account_id) \
1154 or (not st.journal_id.default_debit_account_id):
1155- raise osv.except_osv(_('Configuration Error !'),
1156- _('Please verify that an account is defined in the journal.'))
1157+ raise osv.except_osv(_('Configuration Error!'),
1158+ _('Please verify that an account is defined in the journal.'))
1159
1160 if not st.name == '/':
1161 st_number = st.name
1162@@ -308,20 +331,24 @@
1163 # End Changes
1164 for line in st.move_line_ids:
1165 if line.state != 'valid':
1166- raise osv.except_osv(_('Error !'),
1167- _('The account entries lines are not in valid state.'))
1168+ raise osv.except_osv(_('Error!'),
1169+ _('The account entries lines are not in valid state.'))
1170 # begin changes
1171 errors_stack = []
1172 for st_line in st.line_ids:
1173 try:
1174 if st_line.analytic_account_id:
1175 if not st.journal_id.analytic_journal_id:
1176- raise osv.except_osv(_('No Analytic Journal !'),
1177- _("You have to assign an analytic journal on the '%s' journal!") % (st.journal_id.name,))
1178+ raise osv.except_osv(_('No Analytic Journal!'),
1179+ _("You have to assign an analytic"
1180+ " journal on the '%s' journal!") % st.journal_id.name)
1181 if not st_line.amount:
1182 continue
1183 st_line_number = self.get_next_st_line_number(cr, uid, st_number, st_line, context)
1184- self.create_move_from_st_line(cr, uid, st_line.id, company_currency_id, st_line_number, context)
1185+ self.create_move_from_st_line(cr, uid, st_line.id,
1186+ company_currency_id,
1187+ st_line_number,
1188+ context)
1189 except osv.except_osv, exc:
1190 msg = "Line ID %s with ref %s had following error: %s" % (st_line.id, st_line.ref, exc.value)
1191 errors_stack.append(msg)
1192@@ -332,37 +359,97 @@
1193 msg = u"\n".join(errors_stack)
1194 raise osv.except_osv(_('Error'), msg)
1195 #end changes
1196- self.write(cr, uid, [st.id], {
1197- 'name': st_number,
1198- 'balance_end_real': st.balance_end
1199- }, context=context)
1200- self.message_post(cr, uid, [st.id], body=_('Statement %s confirmed, journal items were created.') % (st_number,), context=context)
1201+ self.write(cr, uid, [st.id],
1202+ {'name': st_number,
1203+ 'balance_end_real': st.balance_end},
1204+ context=context)
1205+ body = _('Statement %s confirmed, journal items were created.') % st_number
1206+ self.message_post(cr, uid, [st.id],
1207+ body,
1208+ context=context)
1209 return self.write(cr, uid, ids, {'state': 'confirm'}, context=context)
1210
1211- def get_account_for_counterpart(
1212- self, cr, uid, amount, account_receivable, account_payable):
1213+ def get_account_for_counterpart(self, cr, uid, amount, account_receivable, account_payable):
1214+ """For backward compatibility."""
1215+ account_id, type = self.get_account_and_type_for_counterpart(cr, uid, amount,
1216+ account_receivable,
1217+ account_payable)
1218+ return account_id
1219+
1220+ def _compute_type_from_partner_profile(self, cr, uid, partner_id,
1221+ default_type, context=None):
1222+ """Compute the statement line type
1223+ from partner profile (customer, supplier)"""
1224+ obj_partner = self.pool.get('res.partner')
1225+ part = obj_partner.browse(cr, uid, partner_id, context=context)
1226+ if part.supplier == part.customer:
1227+ return default_type
1228+ if part.supplier:
1229+ return 'supplier'
1230+ else:
1231+ return 'customer'
1232+
1233+ def _compute_type_from_amount(self, cr, uid, amount):
1234+ """Compute the statement type based on amount"""
1235+ if amount in (None, False):
1236+ return 'general'
1237+ if amount < 0:
1238+ return 'supplier'
1239+ return 'customer'
1240+
1241+ def get_type_for_counterpart(self, cr, uid, amount, partner_id=False):
1242+ """Give the amount and receive the type to use for the line.
1243+ The rules are:
1244+ - If the customer checkbox is checked on the found partner, type customer
1245+ - If the supplier checkbox is checked on the found partner, typewill be supplier
1246+ - If both checkbox are checked or none of them, it'll be based on the amount :
1247+ If amount is positif the type customer,
1248+ If amount is negativ, the type supplier
1249+ :param float: amount of the line
1250+ :param int/long: partner_id the partner id
1251+ :return: type as string: the default type to use: 'customer' or 'supplier'.
1252+ """
1253+ s_line_type = self._compute_type_from_amount(cr, uid, amount)
1254+ if partner_id:
1255+ s_line_type = self._compute_type_from_partner_profile(cr, uid,
1256+ partner_id, s_line_type)
1257+ return s_line_type
1258+
1259+ def get_account_and_type_for_counterpart(self, cr, uid, amount, account_receivable,
1260+ account_payable, partner_id=False):
1261 """
1262 Give the amount, payable and receivable account (that can be found using
1263 get_default_pay_receiv_accounts method) and receive the one to use. This method
1264 should be use when there is no other way to know which one to take.
1265+ The rules are:
1266+ - If the customer checkbox is checked on the found partner, type and account will be customer and receivable
1267+ - If the supplier checkbox is checked on the found partner, type and account will be supplier and payable
1268+ - If both checkbox are checked or none of them, it'll be based on the amount :
1269+ If amount is positive, the type and account will be customer and receivable,
1270+ If amount is negative, the type and account will be supplier and payable
1271+ Note that we return the payable or receivable account from agrs and not from the optional partner_id
1272+ given!
1273
1274 :param float: amount of the line
1275 :param int/long: account_receivable the receivable account
1276 :param int/long: account_payable the payable account
1277- :return: int/long :the default account to be used by statement line as the counterpart
1278- of the journal account depending on the amount.
1279+ :param int/long: partner_id the partner id
1280+ :return: dict with [account_id as int/long,type as string]: the default account to be used by
1281+ statement line as the counterpart of the journal account depending on the amount and the type
1282+ as 'customer' or 'supplier'.
1283 """
1284 account_id = False
1285- if amount >= 0:
1286+ ltype = self.get_type_for_counterpart(cr, uid, amount, partner_id=partner_id)
1287+ if ltype == 'supplier':
1288+ account_id = account_payable
1289+ else:
1290 account_id = account_receivable
1291- else:
1292- account_id = account_payable
1293 if not account_id:
1294 raise osv.except_osv(
1295 _('Can not determine account'),
1296 _('Please ensure that minimal properties are set')
1297 )
1298- return account_id
1299+ return [account_id, ltype]
1300
1301 def get_default_pay_receiv_accounts(self, cr, uid, context=None):
1302 """
1303@@ -386,14 +473,11 @@
1304 ('model', '=', 'res.partner')],
1305 context=context
1306 )
1307- property_ids = property_obj.search(
1308- cr,
1309- uid,
1310- [('fields_id', 'in', model_fields_ids),
1311- ('res_id', '=', False),
1312- ],
1313- context=context
1314- )
1315+ property_ids = property_obj.search(cr,
1316+ uid,
1317+ [('fields_id', 'in', model_fields_ids),
1318+ ('res_id', '=', False)],
1319+ context=context)
1320
1321 for erp_property in property_obj.browse(
1322 cr, uid, property_ids, context=context):
1323@@ -433,13 +517,10 @@
1324 journal_id = import_config.journal_id.id
1325 account_id = import_config.journal_id.default_debit_account_id.id
1326 credit_partner_id = import_config.partner_id and import_config.partner_id.id or False
1327- return {'value':
1328- {'journal_id': journal_id,
1329- 'account_id': account_id,
1330- 'balance_check': import_config.balance_check,
1331- 'credit_partner_id': credit_partner_id,
1332- }
1333- }
1334+ return {'value': {'journal_id': journal_id,
1335+ 'account_id': account_id,
1336+ 'balance_check': import_config.balance_check,
1337+ 'credit_partner_id': credit_partner_id}}
1338
1339
1340 class AccountBankSatementLine(Model):
1341@@ -472,18 +553,24 @@
1342 'account_id': _get_default_account,
1343 }
1344
1345- def get_values_for_line(self, cr, uid, profile_id=False, partner_id=False, line_type=False, amount=False, context=None):
1346+ def get_values_for_line(self, cr, uid, profile_id=False, partner_id=False, line_type=False, amount=False, master_account_id=None, context=None):
1347 """
1348 Return the account_id to be used in the line of a bank statement. It'll base the result as follow:
1349 - If a receivable_account_id is set in the profile, return this value and type = general
1350- - Elif line_type is given, take the partner receivable/payable property (payable if type= supplier, receivable
1351+ # TODO
1352+ - Elif how_get_type_account is set to force_supplier or force_customer, will take respectively payable and type=supplier,
1353+ receivable and type=customer otherwise
1354+ # END TODO
1355+ - Elif line_type is given, take the partner receivable/payable property (payable if type=supplier, receivable
1356 otherwise)
1357- - Elif amount is given, take the partner receivable/payable property (receivable if amount >= 0.0,
1358- payable otherwise). In that case, we also fullfill the type (receivable = customer, payable = supplier)
1359- so it is easier for the accountant to know why the receivable/payable has been chosen
1360+ - Elif amount is given:
1361+ - If the customer checkbox is checked on the found partner, type and account will be customer and receivable
1362+ - If the supplier checkbox is checked on the found partner, type and account will be supplier and payable
1363+ - If both checkbox are checked or none of them, it'll be based on the amount :
1364+ If amount is positive, the type and account will be customer and receivable,
1365+ If amount is negative, the type and account will be supplier an payable
1366 - Then, if no partner are given we look and take the property from the company so we always give a value
1367 for account_id. Note that in that case, we return the receivable one.
1368-
1369 :param int/long profile_id of the related bank statement
1370 :param int/long partner_id of the line
1371 :param char line_type: a value from: 'general', 'supplier', 'customer'
1372@@ -499,79 +586,77 @@
1373 res = {}
1374 obj_partner = self.pool.get('res.partner')
1375 obj_stat = self.pool.get('account.bank.statement')
1376- line_type = receiv_account = pay_account = account_id = False
1377+ receiv_account = pay_account = account_id = False
1378 # If profile has a receivable_account_id, we return it in any case
1379- if profile_id:
1380+ if master_account_id:
1381+ res['account_id'] = master_account_id
1382+ # We return general as default instead of get_type_for_counterpart
1383+ # for perfomance reasons as line_type is not a meaningfull value
1384+ # as account is forced
1385+ res['type'] = line_type if line_type else 'general'
1386+ return res
1387+ # To optimize we consider passing false means there is no account
1388+ # on profile
1389+ if profile_id and master_account_id is None:
1390 profile = self.pool.get("account.statement.profile").browse(
1391- cr, uid, profile_id, context=context)
1392+ cr, uid, profile_id, context=context)
1393 if profile.receivable_account_id:
1394- account_id = profile.receivable_account_id.id
1395- line_type = 'general'
1396+ res['account_id'] = profile.receivable_account_id.id
1397+ # We return general as default instead of get_type_for_counterpart
1398+ # for perfomance reasons as line_type is not a meaningfull value
1399+ # as account is forced
1400+ res['type'] = line_type if line_type else 'general'
1401 return res
1402- # If partner -> take from him
1403+ # If no account is available on profile you have to do the lookup
1404+ # This can be quite a performance killer as we read ir.properity fields
1405 if partner_id:
1406 part = obj_partner.browse(cr, uid, partner_id, context=context)
1407 pay_account = part.property_account_payable.id
1408 receiv_account = part.property_account_receivable.id
1409 # If no value, look on the default company property
1410 if not pay_account or not receiv_account:
1411- receiv_account, pay_account = obj_stat.get_default_pay_receiv_accounts(
1412- cr, uid, context=None)
1413- # Now we have both pay and receive account, choose the one to use
1414- # based on line_type first, then amount, otherwise take receivable one.
1415- if line_type is not False:
1416- if line_type == 'supplier':
1417- account_id = pay_account
1418- elif amount is not False:
1419- if amount >= 0:
1420- account_id = receiv_account
1421- line_type = 'customer'
1422- else:
1423- account_id = pay_account
1424- line_type = 'supplier'
1425+ receiv_account, pay_account = obj_stat.get_default_pay_receiv_accounts(cr, uid, context=None)
1426+ account_id, comp_line_type = obj_stat.get_account_and_type_for_counterpart(cr, uid, amount,
1427+ receiv_account, pay_account,
1428+ partner_id=partner_id)
1429 res['account_id'] = account_id if account_id else receiv_account
1430- res['type'] = line_type
1431+ res['type'] = line_type if line_type else comp_line_type
1432 return res
1433
1434 def onchange_partner_id(self, cr, uid, ids, partner_id, profile_id=None, context=None):
1435 """
1436 Override of the basic method as we need to pass the profile_id in the on_change_type
1437 call.
1438+ Moreover, we now call the get_account_and_type_for_counterpart method now to get the
1439+ type to use.
1440 """
1441- obj_partner = self.pool.get('res.partner')
1442+ obj_stat = self.pool.get('account.bank.statement')
1443 if not partner_id:
1444 return {}
1445- part = obj_partner.browse(cr, uid, partner_id, context=context)
1446- if not part.supplier and not part.customer:
1447- type = 'general'
1448- elif part.supplier and part.customer:
1449- type = 'general'
1450- else:
1451- if part.supplier == True:
1452- type = 'supplier'
1453- if part.customer == True:
1454- type = 'customer'
1455- res_type = self.onchange_type(cr, uid, ids, partner_id, type, profile_id, context=context) # Chg
1456+ line_type = obj_stat.get_type_for_counterpart(cr, uid, 0.0, partner_id=partner_id)
1457+ res_type = self.onchange_type(cr, uid, ids, partner_id, line_type, profile_id, context=context)
1458 if res_type['value'] and res_type['value'].get('account_id', False):
1459- return {'value': {'type': type,
1460+ return {'value': {'type': line_type,
1461 'account_id': res_type['value']['account_id'],
1462 'voucher_id': False}}
1463- return {'value': {'type': type}}
1464+ return {'value': {'type': line_type}}
1465
1466- def onchange_type(self, cr, uid, line_id, partner_id, type, profile_id, context=None):
1467+ def onchange_type(self, cr, uid, line_id, partner_id, line_type, profile_id, context=None):
1468 """
1469 Keep the same features as in standard and call super. If an account is returned,
1470 call the method to compute line values.
1471 """
1472- res = super(AccountBankSatementLine, self).onchange_type(
1473- cr, uid, line_id, partner_id, type, context=context)
1474+ res = super(AccountBankSatementLine, self).onchange_type(cr, uid,
1475+ line_id,
1476+ partner_id,
1477+ line_type,
1478+ context=context)
1479 if 'account_id' in res['value']:
1480- result = self.get_values_for_line(
1481- cr, uid,
1482- profile_id=profile_id,
1483- partner_id=partner_id,
1484- line_type=type,
1485- context=context)
1486+ result = self.get_values_for_line(cr, uid,
1487+ profile_id=profile_id,
1488+ partner_id=partner_id,
1489+ line_type=line_type,
1490+ context=context)
1491 if result:
1492 res['value'].update({'account_id': result['account_id']})
1493 return res
1494
1495=== modified file 'account_statement_transactionid_completion/statement.py'
1496--- account_statement_transactionid_completion/statement.py 2012-12-20 13:37:01 +0000
1497+++ account_statement_transactionid_completion/statement.py 2013-04-25 11:52:27 +0000
1498@@ -32,7 +32,7 @@
1499
1500 def _get_functions(self, cr, uid, context=None):
1501 res = super(AccountStatementCompletionRule, self)._get_functions(
1502- cr, uid, context=context)
1503+ cr, uid, context=context)
1504 res.append(('get_from_transaction_id_and_so',
1505 'From line reference (based on SO transaction ID)'))
1506 return res
1507@@ -41,12 +41,12 @@
1508 'function_to_call': fields.selection(_get_functions, 'Method'),
1509 }
1510
1511- def get_from_transaction_id_and_so(self, cr, uid, line_id, context=None):
1512+ def get_from_transaction_id_and_so(self, cr, uid, st_line, context=None):
1513 """
1514 Match the partner based on the transaction ID field of the SO.
1515 Then, call the generic st_line method to complete other values.
1516 In that case, we always fullfill the reference of the line with the SO name.
1517- :param int/long line_id: ID of the concerned account.bank.statement.line
1518+ :param dict st_line: read of the concerned account.bank.statement.line
1519 :return:
1520 A dict of value that can be passed directly to the write method of
1521 the statement line or {}
1522@@ -55,33 +55,28 @@
1523 ...}
1524 """
1525 st_obj = self.pool.get('account.bank.statement.line')
1526- st_line = st_obj.browse(cr, uid, line_id, context=context)
1527 res = {}
1528- if st_line:
1529- so_obj = self.pool.get('sale.order')
1530- so_id = so_obj.search(
1531- cr,
1532- uid,
1533- [('transaction_id', '=', st_line.transaction_id)],
1534- context=context)
1535- if so_id and len(so_id) == 1:
1536- so = so_obj.browse(cr, uid, so_id[0], context=context)
1537- res['partner_id'] = so.partner_id.id
1538- res['ref'] = so.name
1539- elif so_id and len(so_id) > 1:
1540- raise ErrorTooManyPartner(
1541- _('Line named "%s" (Ref:%s) was matched by more than '
1542- 'one partner.') % (st_line.name, st_line.ref))
1543- if so_id:
1544- st_vals = st_obj.get_values_for_line(
1545- cr,
1546- uid,
1547- profile_id=st_line.statement_id.profile_id.id,
1548- partner_id=res.get('partner_id', False),
1549- line_type=st_line.type,
1550- amount=st_line.amount,
1551- context=context)
1552- res.update(st_vals)
1553+ so_obj = self.pool.get('sale.order')
1554+ so_id = so_obj.search(cr,
1555+ uid,
1556+ [('transaction_id', '=', st_line['transaction_id'])],
1557+ context=context)
1558+ if len(so_id) > 1:
1559+ raise ErrorTooManyPartner(_('Line named "%s" (Ref:%s) was matched by more than '
1560+ 'one partner.') % (st_line['name'], st_line['ref']))
1561+ if len(so_id) == 1:
1562+ so = so_obj.browse(cr, uid, so_id[0], context=context)
1563+ res['partner_id'] = so.partner_id.id
1564+ res['ref'] = so.name
1565+ st_vals = st_obj.get_values_for_line(cr,
1566+ uid,
1567+ profile_id=st_line['profile_id'],
1568+ master_account_id=st_line['master_account_id'],
1569+ partner_id=res.get('partner_id', False),
1570+ line_type=st_line['type'],
1571+ amount=st_line['amount'] if st_line['amount'] else 0.0,
1572+ context=context)
1573+ res.update(st_vals)
1574 return res
1575
1576
1577
1578=== modified file 'account_statement_transactionid_import/parser/transactionid_file_parser.py'
1579--- account_statement_transactionid_import/parser/transactionid_file_parser.py 2013-02-26 08:19:44 +0000
1580+++ account_statement_transactionid_import/parser/transactionid_file_parser.py 2013-04-25 11:52:27 +0000
1581@@ -17,8 +17,6 @@
1582 # along with this program. If not, see <http://www.gnu.org/licenses/>.
1583 #
1584 ##############################################################################
1585-
1586-from openerp.tools.translate import _
1587 import datetime
1588 from account_statement_base_import.parser.file_parser import FileParser
1589
1590@@ -30,13 +28,11 @@
1591 """
1592
1593 def __init__(self, parse_name, ftype='csv'):
1594- conversion_dict = {
1595- 'transaction_id': unicode,
1596- 'label': unicode,
1597- 'date': datetime.datetime,
1598- 'amount': float,
1599- 'commission_amount': float
1600- }
1601+ conversion_dict = {'transaction_id': unicode,
1602+ 'label': unicode,
1603+ 'date': datetime.datetime,
1604+ 'amount': float,
1605+ 'commission_amount': float}
1606 # Order of cols does not matter but first row of the file has to be header
1607 keys_to_validate = ['transaction_id', 'label', 'date', 'amount', 'commission_amount']
1608 super(TransactionIDFileParser, self).__init__(parse_name, keys_to_validate=keys_to_validate,
1609@@ -70,15 +66,13 @@
1610 In this generic parser, the commission is given for every line, so we store it
1611 for each one.
1612 """
1613- return {
1614- 'name': line.get('label', line.get('ref', '/')),
1615- 'date': line.get('date', datetime.datetime.now().date()),
1616- 'amount': line.get('amount', 0.0),
1617- 'ref': line.get('transaction_id', '/'),
1618- 'label': line.get('label', ''),
1619- 'transaction_id': line.get('transaction_id', '/'),
1620- 'commission_amount': line.get('commission_amount', 0.0),
1621- }
1622+ return {'name': line.get('label', line.get('ref', '/')),
1623+ 'date': line.get('date', datetime.datetime.now().date()),
1624+ 'amount': line.get('amount', 0.0),
1625+ 'ref': line.get('transaction_id', '/'),
1626+ 'label': line.get('label', ''),
1627+ 'transaction_id': line.get('transaction_id', '/'),
1628+ 'commission_amount': line.get('commission_amount', 0.0)}
1629
1630 def _post(self, *args, **kwargs):
1631 """

Subscribers

People subscribed via source and target branches