X-Git-Url: https://git.cyclocoop.org/?a=blobdiff_plain;f=report%2Faged_partner_balance.py;fp=report%2Faged_partner_balance.py;h=ef81d326cf833659ecf0b94a3def7dcf98ceda9d;hb=cddef38e342c880228694525c45df3f7dd8c2148;hp=0000000000000000000000000000000000000000;hpb=bffd331c220385010fe9abad9e228fc2004a293a;p=burette%2Faccount_financial_report_webkit.git diff --git a/report/aged_partner_balance.py b/report/aged_partner_balance.py new file mode 100644 index 0000000..ef81d32 --- /dev/null +++ b/report/aged_partner_balance.py @@ -0,0 +1,403 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Nicolas Bessi +# Copyright 2014 Camptocamp SA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +from __future__ import division +from datetime import datetime + +from openerp import pooler +from openerp.tools import DEFAULT_SERVER_DATE_FORMAT +from openerp.tools.translate import _ +from .open_invoices import PartnersOpenInvoicesWebkit +from .webkit_parser_header_fix import HeaderFooterTextWebKitParser + + +def make_ranges(top, offset): + """Return sorted days ranges + + :param top: maximum overdue day + :param offset: offset for ranges + + :returns: list of sorted ranges tuples in days + eg. [(-100000, 0), (0, offset), (offset, n*offset), ... (top, 100000)] + """ + ranges = [(n, min(n + offset, top)) for n in xrange(0, top, offset)] + ranges.insert(0, (-100000000000, 0)) + ranges.append((top, 100000000000)) + return ranges + +#list of overdue ranges +RANGES = make_ranges(120, 30) + + +def make_ranges_titles(): + """Generates title to be used by mako""" + titles = [_('Due')] + titles += [_(u'Overdue ≤ %s d.') % x[1] for x in RANGES[1:-1]] + titles.append(_('Older')) + return titles + +#list of overdue ranges title +RANGES_TITLES = make_ranges_titles() +#list of payable journal types +REC_PAY_TYPE = ('purchase', 'sale') +#list of refund payable type +REFUND_TYPE = ('purchase_refund', 'sale_refund') +INV_TYPE = REC_PAY_TYPE + REFUND_TYPE + + +class AccountAgedTrialBalanceWebkit(PartnersOpenInvoicesWebkit): + """Compute Aged Partner Balance based on result of Open Invoices""" + + def __init__(self, cursor, uid, name, context=None): + """Constructor, refer to :class:`openerp.report.report_sxw.rml_parse`""" + super(AccountAgedTrialBalanceWebkit, self).__init__(cursor, uid, name, + context=context) + self.pool = pooler.get_pool(self.cr.dbname) + self.cursor = self.cr + company = self.pool.get('res.users').browse(self.cr, uid, uid, + context=context).company_id + + header_report_name = ' - '.join((_('Aged Partner Balance'), + company.currency_id.name)) + + footer_date_time = self.formatLang(str(datetime.today()), + date_time=True) + + self.localcontext.update({ + 'cr': cursor, + 'uid': uid, + 'company': company, + 'ranges': self._get_ranges(), + 'ranges_titles': self._get_ranges_titles(), + 'report_name': _('Aged Partner Balance'), + 'additional_args': [ + ('--header-font-name', 'Helvetica'), + ('--footer-font-name', 'Helvetica'), + ('--header-font-size', '10'), + ('--footer-font-size', '6'), + ('--header-left', header_report_name), + ('--header-spacing', '2'), + ('--footer-left', footer_date_time), + ('--footer-right', ' '.join((_('Page'), '[page]', _('of'), '[topage]'))), + ('--footer-line',), + ], + }) + + def _get_ranges(self): + """:returns: :cons:`RANGES`""" + return RANGES + + def _get_ranges_titles(self): + """:returns: :cons: `RANGES_TITLES`""" + return RANGES_TITLES + + def set_context(self, objects, data, ids, report_type=None): + """Populate aged_lines, aged_balance, aged_percents attributes + + on each account browse record that will be used by mako template + The browse record are store in :attr:`objects` + + The computation are based on the ledger_lines attribute set on account + contained by :attr:`objects` + + :attr:`objects` values were previously set by parent class + :class: `.open_invoices.PartnersOpenInvoicesWebkit` + + :returns: parent :class:`.open_invoices.PartnersOpenInvoicesWebkit` + call to set_context + + """ + res = super(AccountAgedTrialBalanceWebkit, self).set_context( + objects, + data, + ids, + report_type=report_type + ) + + for acc in self.objects: + acc.aged_lines = {} + acc.agged_totals = {} + acc.agged_percents = {} + for part_id, partner_lines in acc.ledger_lines.items(): + aged_lines = self.compute_aged_lines(part_id, + partner_lines, + data) + if aged_lines: + acc.aged_lines[part_id] = aged_lines + acc.aged_totals = totals = self.compute_totals(acc.aged_lines.values()) + acc.aged_percents = self.compute_percents(totals) + #Free some memory + del(acc.ledger_lines) + return res + + def compute_aged_lines(self, partner_id, ledger_lines, data): + """Add property aged_lines to accounts browse records + + contained in :attr:`objects` for a given partner + + :param: partner_id: current partner + :param ledger_lines: generated by parent + :class:`.open_invoices.PartnersOpenInvoicesWebkit` + + :returns: dict of computed aged lines + eg {'balance': 1000.0, + 'aged_lines': {(90, 120): 0.0, ...} + + """ + lines_to_age = self.filter_lines(partner_id, ledger_lines) + res = {} + end_date = self._get_end_date(data) + aged_lines = dict.fromkeys(RANGES, 0.0) + reconcile_lookup = self.get_reconcile_count_lookup(lines_to_age) + res['aged_lines'] = aged_lines + for line in lines_to_age: + compute_method = self.get_compute_method(reconcile_lookup, + partner_id, + line) + delay = compute_method(line, end_date, ledger_lines) + classification = self.classify_line(partner_id, delay) + aged_lines[classification] += line['debit'] - line['credit'] + self.compute_balance(res, aged_lines) + return res + + def _get_end_date(self, data): + """Retrieve end date to be used to compute delay. + + :param data: data dict send to report contains form dict + + :returns: end date to be used to compute overdue delay + + """ + end_date = None + date_to = data['form']['date_to'] + period_to_id = data['form']['period_to'] + fiscal_to_id = data['form']['fiscalyear_id'] + if date_to: + end_date = date_to + elif period_to_id: + period_to = self.pool['account.period'].browse(self.cr, + self.uid, + period_to_id) + end_date = period_to.date_stop + elif fiscal_to_id: + fiscal_to = self.pool['account.fiscalyear'].browse(self.cr, + self.uid, + fiscal_to_id) + end_date = fiscal_to.date_stop + else: + raise ValueError('End date and end period not available') + return end_date + + def _compute_delay_from_key(self, key, line, end_date): + """Compute overdue delay delta in days for line using attribute in key + + delta = end_date - date of key + + :param line: current ledger line + :param key: date key to be used to compute delta + :param end_date: end_date computed for wizard data + + :returns: delta in days + """ + from_date = datetime.strptime(line[key], DEFAULT_SERVER_DATE_FORMAT) + end_date = datetime.strptime(end_date, DEFAULT_SERVER_DATE_FORMAT) + delta = end_date - from_date + return delta.days + + def compute_delay_from_maturity(self, line, end_date, ledger_lines): + """Compute overdue delay delta in days for line using attribute in key + + delta = end_date - maturity date + + :param line: current ledger line + :param end_date: end_date computed for wizard data + :param ledger_lines: generated by parent + :class:`.open_invoices.PartnersOpenInvoicesWebkit` + + :returns: delta in days + """ + return self._compute_delay_from_key('date_maturity', + line, + end_date) + + def compute_delay_from_date(self, line, end_date, ledger_lines): + """Compute overdue delay delta in days for line using attribute in key + + delta = end_date - date + + :param line: current ledger line + :param end_date: end_date computed for wizard data + :param ledger_lines: generated by parent + :class:`.open_invoices.PartnersOpenInvoicesWebkit` + + :returns: delta in days + """ + return self._compute_delay_from_key('ldate', + line, + end_date) + + def compute_delay_from_partial_rec(self, line, end_date, ledger_lines): + """Compute overdue delay delta in days for the case where move line + + is related to a partial reconcile with more than one reconcile line + + :param line: current ledger line + :param end_date: end_date computed for wizard data + :param ledger_lines: generated by parent + :class:`.open_invoices.PartnersOpenInvoicesWebkit` + + :returns: delta in days + """ + sale_lines = [x for x in ledger_lines if x['jtype'] in REC_PAY_TYPE and + line['rec_id'] == x['rec_id']] + refund_lines = [x for x in ledger_lines if x['jtype'] in REFUND_TYPE and + line['rec_id'] == x['rec_id']] + if len(sale_lines) == 1: + reference_line = sale_lines[0] + elif len(refund_lines) == 1: + reference_line = refund_lines[0] + else: + reference_line = line + key = 'date_maturity' if reference_line.get('date_maturity') else 'ldate' + return self._compute_delay_from_key(key, + reference_line, + end_date) + + def get_compute_method(self, reconcile_lookup, partner_id, line): + """Get the function that should compute the delay for a given line + + :param reconcile_lookup: dict of reconcile group by id and count + {rec_id: count of line related to reconcile} + :param partner_id: current partner_id + :param line: current ledger line generated by parent + :class:`.open_invoices.PartnersOpenInvoicesWebkit` + + :returns: function bounded to :class:`.AccountAgedTrialBalanceWebkit` + + """ + if reconcile_lookup.get(line['rec_id'], 0.0) > 1: + return self.compute_delay_from_partial_rec + elif line['jtype'] in INV_TYPE and line.get('date_maturity'): + return self.compute_delay_from_maturity + else: + return self.compute_delay_from_date + + def line_is_valid(self, partner_id, line): + """Predicate hook that allows to filter line to be treated + + :param partner_id: current partner_id + :param line: current ledger line generated by parent + :class:`.open_invoices.PartnersOpenInvoicesWebkit` + + :returns: boolean True if line is allowed + """ + return True + + def filter_lines(self, partner_id, lines): + """Filter ledger lines that have to be treated + + :param partner_id: current partner_id + :param lines: ledger_lines related to current partner + and generated by parent + :class:`.open_invoices.PartnersOpenInvoicesWebkit` + + :returns: list of allowed lines + + """ + return [x for x in lines if self.line_is_valid(partner_id, x)] + + def classify_line(self, partner_id, overdue_days): + """Return the overdue range for a given delay + + We loop from smaller range to higher + This should be the most effective solution as generaly + customer tend to have one or two month of delay + + :param overdue_days: delay in days + :param partner_id: current partner_id + + :returns: the correct range in :const:`RANGES` + + """ + for drange in RANGES: + if overdue_days <= drange[1]: + return drange + return drange + + def compute_balance(self, res, aged_lines): + """Compute the total balance of aged line + for given account""" + res['balance'] = sum(aged_lines.values()) + + def compute_totals(self, aged_lines): + """Compute the totals for an account + + :param aged_lines: dict of aged line taken from the + property added to account record + + :returns: dict of total {'balance':1000.00, (30, 60): 3000,...} + + """ + totals = {} + totals['balance'] = sum(x.get('balance', 0.0) for + x in aged_lines) + aged_ranges = [x.get('aged_lines', {}) for x in aged_lines] + for drange in RANGES: + totals[drange] = sum(x.get(drange, 0.0) for x in aged_ranges) + return totals + + def compute_percents(self, totals): + percents = {} + base = totals['balance'] or 1.0 + for drange in RANGES: + percents[drange] = (totals[drange] / base) * 100.0 + return percents + + def get_reconcile_count_lookup(self, lines): + """Compute an lookup dict + + It contains has partial reconcile id as key and the count of lines + related to the reconcile id + + :param: a list of ledger lines generated by parent + :class:`.open_invoices.PartnersOpenInvoicesWebkit` + + :retuns: lookup dict {ṛec_id: count} + + """ + # possible bang if l_ids is really long. + # We have the same weakness in common_report ... + # but it seems not really possible for a partner + # So I'll keep that option. + l_ids = tuple(x['id'] for x in lines) + sql = ("SELECT reconcile_partial_id, COUNT(*) FROM account_move_line" + " WHERE reconcile_partial_id IS NOT NULL" + " AND id in %s" + " GROUP BY reconcile_partial_id") + self.cr.execute(sql, (l_ids,)) + res = self.cr.fetchall() + return dict((x[0], x[1]) for x in res) + +HeaderFooterTextWebKitParser( + 'report.account.account_aged_trial_balance_webkit', + 'account.account', + 'addons/account_financial_report_webkit/report/templates/aged_trial_webkit.mako', + parser=AccountAgedTrialBalanceWebkit, +)