1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # Author: Nicolas Bessi
5 # Copyright 2014 Camptocamp SA
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
21 from __future__
import division
22 from datetime
import datetime
24 from openerp
import pooler
25 from openerp
.tools
import DEFAULT_SERVER_DATE_FORMAT
26 from openerp
.tools
.translate
import _
27 from .open_invoices
import PartnersOpenInvoicesWebkit
28 from .webkit_parser_header_fix
import HeaderFooterTextWebKitParser
31 def make_ranges(top
, offset
):
32 """Return sorted days ranges
34 :param top: maximum overdue day
35 :param offset: offset for ranges
37 :returns: list of sorted ranges tuples in days
38 eg. [(-100000, 0), (0, offset), (offset, n*offset), ... (top, 100000)]
40 ranges
= [(n
, min(n
+ offset
, top
)) for n
in xrange(0, top
, offset
)]
41 ranges
.insert(0, (-100000000000, 0))
42 ranges
.append((top
, 100000000000))
45 #list of overdue ranges
46 RANGES
= make_ranges(120, 30)
49 def make_ranges_titles():
50 """Generates title to be used by mako"""
52 titles
+= [_(u
'Overdue ≤ %s d.') % x
[1] for x
in RANGES
[1:-1]]
53 titles
.append(_('Older'))
56 #list of overdue ranges title
57 RANGES_TITLES
= make_ranges_titles()
58 #list of payable journal types
59 REC_PAY_TYPE
= ('purchase', 'sale')
60 #list of refund payable type
61 REFUND_TYPE
= ('purchase_refund', 'sale_refund')
62 INV_TYPE
= REC_PAY_TYPE
+ REFUND_TYPE
65 class AccountAgedTrialBalanceWebkit(PartnersOpenInvoicesWebkit
):
66 """Compute Aged Partner Balance based on result of Open Invoices"""
68 def __init__(self
, cursor
, uid
, name
, context
=None):
69 """Constructor, refer to :class:`openerp.report.report_sxw.rml_parse`"""
70 super(AccountAgedTrialBalanceWebkit
, self
).__init
__(cursor
, uid
, name
,
72 self
.pool
= pooler
.get_pool(self
.cr
.dbname
)
74 company
= self
.pool
.get('res.users').browse(self
.cr
, uid
, uid
,
75 context
=context
).company_id
77 header_report_name
= ' - '.join((_('Aged Partner Balance'),
78 company
.currency_id
.name
))
80 footer_date_time
= self
.formatLang(str(datetime
.today()),
83 self
.localcontext
.update({
87 'ranges': self
._get
_ranges
(),
88 'ranges_titles': self
._get
_ranges
_titles
(),
89 'report_name': _('Aged Partner Balance'),
91 ('--header-font-name', 'Helvetica'),
92 ('--footer-font-name', 'Helvetica'),
93 ('--header-font-size', '10'),
94 ('--footer-font-size', '6'),
95 ('--header-left', header_report_name
),
96 ('--header-spacing', '2'),
97 ('--footer-left', footer_date_time
),
98 ('--footer-right', ' '.join((_('Page'), '[page]', _('of'), '[topage]'))),
103 def _get_ranges(self
):
104 """:returns: :cons:`RANGES`"""
107 def _get_ranges_titles(self
):
108 """:returns: :cons: `RANGES_TITLES`"""
111 def set_context(self
, objects
, data
, ids
, report_type
=None):
112 """Populate aged_lines, aged_balance, aged_percents attributes
114 on each account browse record that will be used by mako template
115 The browse record are store in :attr:`objects`
117 The computation are based on the ledger_lines attribute set on account
118 contained by :attr:`objects`
120 :attr:`objects` values were previously set by parent class
121 :class: `.open_invoices.PartnersOpenInvoicesWebkit`
123 :returns: parent :class:`.open_invoices.PartnersOpenInvoicesWebkit`
127 res
= super(AccountAgedTrialBalanceWebkit
, self
).set_context(
131 report_type
=report_type
134 for acc
in self
.objects
:
136 acc
.agged_totals
= {}
137 acc
.agged_percents
= {}
138 for part_id
, partner_lines
in acc
.ledger_lines
.items():
139 aged_lines
= self
.compute_aged_lines(part_id
,
143 acc
.aged_lines
[part_id
] = aged_lines
144 acc
.aged_totals
= totals
= self
.compute_totals(acc
.aged_lines
.values())
145 acc
.aged_percents
= self
.compute_percents(totals
)
147 del(acc
.ledger_lines
)
150 def compute_aged_lines(self
, partner_id
, ledger_lines
, data
):
151 """Add property aged_lines to accounts browse records
153 contained in :attr:`objects` for a given partner
155 :param: partner_id: current partner
156 :param ledger_lines: generated by parent
157 :class:`.open_invoices.PartnersOpenInvoicesWebkit`
159 :returns: dict of computed aged lines
160 eg {'balance': 1000.0,
161 'aged_lines': {(90, 120): 0.0, ...}
164 lines_to_age
= self
.filter_lines(partner_id
, ledger_lines
)
166 end_date
= self
._get
_end
_date
(data
)
167 aged_lines
= dict.fromkeys(RANGES
, 0.0)
168 reconcile_lookup
= self
.get_reconcile_count_lookup(lines_to_age
)
169 res
['aged_lines'] = aged_lines
170 for line
in lines_to_age
:
171 compute_method
= self
.get_compute_method(reconcile_lookup
,
174 delay
= compute_method(line
, end_date
, ledger_lines
)
175 classification
= self
.classify_line(partner_id
, delay
)
176 aged_lines
[classification
] += line
['debit'] - line
['credit']
177 self
.compute_balance(res
, aged_lines
)
180 def _get_end_date(self
, data
):
181 """Retrieve end date to be used to compute delay.
183 :param data: data dict send to report contains form dict
185 :returns: end date to be used to compute overdue delay
189 date_to
= data
['form']['date_to']
190 period_to_id
= data
['form']['period_to']
191 fiscal_to_id
= data
['form']['fiscalyear_id']
195 period_to
= self
.pool
['account.period'].browse(self
.cr
,
198 end_date
= period_to
.date_stop
200 fiscal_to
= self
.pool
['account.fiscalyear'].browse(self
.cr
,
203 end_date
= fiscal_to
.date_stop
205 raise ValueError('End date and end period not available')
208 def _compute_delay_from_key(self
, key
, line
, end_date
):
209 """Compute overdue delay delta in days for line using attribute in key
211 delta = end_date - date of key
213 :param line: current ledger line
214 :param key: date key to be used to compute delta
215 :param end_date: end_date computed for wizard data
217 :returns: delta in days
219 from_date
= datetime
.strptime(line
[key
], DEFAULT_SERVER_DATE_FORMAT
)
220 end_date
= datetime
.strptime(end_date
, DEFAULT_SERVER_DATE_FORMAT
)
221 delta
= end_date
- from_date
224 def compute_delay_from_maturity(self
, line
, end_date
, ledger_lines
):
225 """Compute overdue delay delta in days for line using attribute in key
227 delta = end_date - maturity date
229 :param line: current ledger line
230 :param end_date: end_date computed for wizard data
231 :param ledger_lines: generated by parent
232 :class:`.open_invoices.PartnersOpenInvoicesWebkit`
234 :returns: delta in days
236 return self
._compute
_delay
_from
_key
('date_maturity',
240 def compute_delay_from_date(self
, line
, end_date
, ledger_lines
):
241 """Compute overdue delay delta in days for line using attribute in key
243 delta = end_date - date
245 :param line: current ledger line
246 :param end_date: end_date computed for wizard data
247 :param ledger_lines: generated by parent
248 :class:`.open_invoices.PartnersOpenInvoicesWebkit`
250 :returns: delta in days
252 return self
._compute
_delay
_from
_key
('ldate',
256 def compute_delay_from_partial_rec(self
, line
, end_date
, ledger_lines
):
257 """Compute overdue delay delta in days for the case where move line
259 is related to a partial reconcile with more than one reconcile line
261 :param line: current ledger line
262 :param end_date: end_date computed for wizard data
263 :param ledger_lines: generated by parent
264 :class:`.open_invoices.PartnersOpenInvoicesWebkit`
266 :returns: delta in days
268 sale_lines
= [x
for x
in ledger_lines
if x
['jtype'] in REC_PAY_TYPE
and
269 line
['rec_id'] == x
['rec_id']]
270 refund_lines
= [x
for x
in ledger_lines
if x
['jtype'] in REFUND_TYPE
and
271 line
['rec_id'] == x
['rec_id']]
272 if len(sale_lines
) == 1:
273 reference_line
= sale_lines
[0]
274 elif len(refund_lines
) == 1:
275 reference_line
= refund_lines
[0]
277 reference_line
= line
278 key
= 'date_maturity' if reference_line
.get('date_maturity') else 'ldate'
279 return self
._compute
_delay
_from
_key
(key
,
283 def get_compute_method(self
, reconcile_lookup
, partner_id
, line
):
284 """Get the function that should compute the delay for a given line
286 :param reconcile_lookup: dict of reconcile group by id and count
287 {rec_id: count of line related to reconcile}
288 :param partner_id: current partner_id
289 :param line: current ledger line generated by parent
290 :class:`.open_invoices.PartnersOpenInvoicesWebkit`
292 :returns: function bounded to :class:`.AccountAgedTrialBalanceWebkit`
295 if reconcile_lookup
.get(line
['rec_id'], 0.0) > 1:
296 return self
.compute_delay_from_partial_rec
297 elif line
['jtype'] in INV_TYPE
and line
.get('date_maturity'):
298 return self
.compute_delay_from_maturity
300 return self
.compute_delay_from_date
302 def line_is_valid(self
, partner_id
, line
):
303 """Predicate hook that allows to filter line to be treated
305 :param partner_id: current partner_id
306 :param line: current ledger line generated by parent
307 :class:`.open_invoices.PartnersOpenInvoicesWebkit`
309 :returns: boolean True if line is allowed
313 def filter_lines(self
, partner_id
, lines
):
314 """Filter ledger lines that have to be treated
316 :param partner_id: current partner_id
317 :param lines: ledger_lines related to current partner
318 and generated by parent
319 :class:`.open_invoices.PartnersOpenInvoicesWebkit`
321 :returns: list of allowed lines
324 return [x
for x
in lines
if self
.line_is_valid(partner_id
, x
)]
326 def classify_line(self
, partner_id
, overdue_days
):
327 """Return the overdue range for a given delay
329 We loop from smaller range to higher
330 This should be the most effective solution as generaly
331 customer tend to have one or two month of delay
333 :param overdue_days: delay in days
334 :param partner_id: current partner_id
336 :returns: the correct range in :const:`RANGES`
339 for drange
in RANGES
:
340 if overdue_days
<= drange
[1]:
344 def compute_balance(self
, res
, aged_lines
):
345 """Compute the total balance of aged line
347 res
['balance'] = sum(aged_lines
.values())
349 def compute_totals(self
, aged_lines
):
350 """Compute the totals for an account
352 :param aged_lines: dict of aged line taken from the
353 property added to account record
355 :returns: dict of total {'balance':1000.00, (30, 60): 3000,...}
359 totals
['balance'] = sum(x
.get('balance', 0.0) for
361 aged_ranges
= [x
.get('aged_lines', {}) for x
in aged_lines
]
362 for drange
in RANGES
:
363 totals
[drange
] = sum(x
.get(drange
, 0.0) for x
in aged_ranges
)
366 def compute_percents(self
, totals
):
368 base
= totals
['balance'] or 1.0
369 for drange
in RANGES
:
370 percents
[drange
] = (totals
[drange
] / base
) * 100.0
373 def get_reconcile_count_lookup(self
, lines
):
374 """Compute an lookup dict
376 It contains has partial reconcile id as key and the count of lines
377 related to the reconcile id
379 :param: a list of ledger lines generated by parent
380 :class:`.open_invoices.PartnersOpenInvoicesWebkit`
382 :retuns: lookup dict {ṛec_id: count}
385 # possible bang if l_ids is really long.
386 # We have the same weakness in common_report ...
387 # but it seems not really possible for a partner
388 # So I'll keep that option.
389 l_ids
= tuple(x
['id'] for x
in lines
)
390 sql
= ("SELECT reconcile_partial_id, COUNT(*) FROM account_move_line"
391 " WHERE reconcile_partial_id IS NOT NULL"
393 " GROUP BY reconcile_partial_id")
394 self
.cr
.execute(sql
, (l_ids
,))
395 res
= self
.cr
.fetchall()
396 return dict((x
[0], x
[1]) for x
in res
)
398 HeaderFooterTextWebKitParser(
399 'report.account.account_aged_trial_balance_webkit',
401 'addons/account_financial_report_webkit/report/templates/aged_trial_webkit.mako',
402 parser
=AccountAgedTrialBalanceWebkit
,