From 980f872c67cb28a40069776937a153f95dbf3712 Mon Sep 17 00:00:00 2001 From: Nicolas Bessi Date: Fri, 14 Mar 2014 12:25:09 +0100 Subject: [PATCH 01/25] [ADD] core of aged partner balance report partial reconciliation support is not now implemented --- .../__openerp__.py | 3 +- .../report/__init__.py | 1 + .../report/aged_partner_balance.py | 243 ++++++++++++++++++ .../report/common_reports.py | 14 +- .../report/open_invoices.py | 2 +- .../report/report.xml | 45 +++- .../report/templates/aged_trial_webkit.mako | 155 +++++++++++ .../open_invoices_inclusion.mako.html | 4 +- .../report/webkit_parser_header_fix.py | 1 - .../report_menus.xml | 4 + .../wizard/__init__.py | 1 + .../wizard/aged_partner_balance_wizard.py | 38 +++ .../wizard/aged_partner_balance_wizard.xml | 94 +++++++ 13 files changed, 587 insertions(+), 18 deletions(-) create mode 100644 account_financial_report_webkit/report/aged_partner_balance.py create mode 100644 account_financial_report_webkit/report/templates/aged_trial_webkit.mako create mode 100644 account_financial_report_webkit/wizard/aged_partner_balance_wizard.py create mode 100644 account_financial_report_webkit/wizard/aged_partner_balance_wizard.xml diff --git a/account_financial_report_webkit/__openerp__.py b/account_financial_report_webkit/__openerp__.py index 605798a6..d0fffefe 100644 --- a/account_financial_report_webkit/__openerp__.py +++ b/account_financial_report_webkit/__openerp__.py @@ -30,7 +30,7 @@ This module adds or replaces the following standard OpenERP financial reports: - Partner ledger - Partner balance - Open invoices report - + - Aged Partner Balance Main improvements per report: ----------------------------- @@ -147,6 +147,7 @@ wkhtmltopdf. The texts are defined inside the report classes. 'wizard/trial_balance_wizard_view.xml', 'wizard/partner_balance_wizard_view.xml', 'wizard/open_invoices_wizard_view.xml', + 'wizard/aged_partner_balance_wizard.xml', 'wizard/print_journal_view.xml', 'report_menus.xml', ], diff --git a/account_financial_report_webkit/report/__init__.py b/account_financial_report_webkit/report/__init__.py index f8bc59fb..e597e39d 100644 --- a/account_financial_report_webkit/report/__init__.py +++ b/account_financial_report_webkit/report/__init__.py @@ -9,3 +9,4 @@ from . import trial_balance from . import partner_balance from . import open_invoices from . import print_journal +from . import aged_partner_balance diff --git a/account_financial_report_webkit/report/aged_partner_balance.py b/account_financial_report_webkit/report/aged_partner_balance.py new file mode 100644 index 00000000..b599f50a --- /dev/null +++ b/account_financial_report_webkit/report/aged_partner_balance.py @@ -0,0 +1,243 @@ +# -*- 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 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 day ranges""" + 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 += [_('Overdue since %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): + + def __init__(self, cursor, uid, name, context=None): + 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): + return RANGES + + def _get_ranges_titles(self): + return RANGES_TITLES + + def set_context(self, objects, data, ids, report_type=None): + """Populate aged_lines, aged_balance, aged_percents attributes + on each browse record that will be used by mako template + """ + 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, lines, data): + lines_to_age = self.filter_lines(partner_id, lines) + res = {} + period_to_id = data['form']['period_to'] + + period_to = self.pool['account.period'].browse(self.cr, + self.uid, + period_to_id) + end_date = period_to.date_stop + 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) + classification = self.classify_line(partner_id, delay) + aged_lines[classification] += line['debit'] - line['credit'] + self.compute_balance(res, aged_lines) + return res + + def _compute_delay_from_key(self, key, line, end_date): + 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): + return self._compute_delay_from_key('date_maturity', + line, + end_date) + + def compute_delay_from_date(self, line, end_date): + return self._compute_delay_from_key('date', + line, + end_date) + + def compute_delay_from_partial_rec(self, line, end_date): + return 25 + + def get_compute_method(self, reconcile_lookup, partner_id, line): + if reconcile_lookup.get(line['rec_id'], 0.0) > 1: + return self.compute_delay_from_partial_rec + if line['jtype'] in INV_TYPE: + if line.get('date_maturity'): + return self.compute_delay_from_maturity + return self.compute_delay_from_date + else: + return self.compute_delay_from_date + + def line_is_valid(self, partner_id, line): + """Predicate that tells if line has to be treated""" + # waiting some spec here maybe dead code + return True + + def filter_lines(self, partner_id, lines): + # vaiting specs + return [x for x in lines if self.line_is_valid(partner_id, x)] + + def classify_line(self, partner_id, overdue_days): + """Return the range index for a number of overdue days + + 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: int representing the lenght in days of delay + :returns: the index of the correct range in ´´RANGES´´ + """ + for drange in RANGES: + if overdue_days <= drange[1]: + return drange + return drange + + def compute_balance(self, res, aged_lines): + res['balance'] = sum(aged_lines.values()) + + def compute_totals(self, aged_lines): + 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) + print totals + return totals + + def compute_percents(self, totals): + percents = {} + base = float(totals['balance']) or 1.0 + for drange in RANGES: + percents[drange] = (float(totals[drange]) / base) * 100.0 + print percents + return percents + + def get_reconcile_count_lookup(self, lines): + # 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 \n" + " WHERE reconcile_partial_id IS NOT NULL \n" + " AND id in %s \n" + " GROUP BY reconcile_partial_id") + self.cr.execute(sql, (l_ids,)) + res = self.cr.fetchall() + if res: + return dict((x[0], x[1]) for x in res) + return {} + +HeaderFooterTextWebKitParser( + 'report.account.account_aged_trial_balance_webkit', + 'account.account', + 'addons/account_financial_report_webkit/report/templates/aged_trial_webkit.mako', + parser=AccountAgedTrialBalanceWebkit, +) diff --git a/account_financial_report_webkit/report/common_reports.py b/account_financial_report_webkit/report/common_reports.py index 3a0a2a15..617cc97e 100644 --- a/account_financial_report_webkit/report/common_reports.py +++ b/account_financial_report_webkit/report/common_reports.py @@ -30,7 +30,7 @@ from openerp.addons.account.report.common_report_header import common_report_hea _logger = logging.getLogger('financial.reports.webkit') - +MAX_MONSTER_SLICE = 50000 class CommonReportHeaderWebkit(common_report_header): """Define common helper for financial report""" @@ -433,6 +433,14 @@ class CommonReportHeaderWebkit(common_report_header): raise osv.except_osv(_('No valid filter'), _('Please set a valid time filter')) def _get_move_line_datas(self, move_line_ids, order='per.special DESC, l.date ASC, per.date_start ASC, m.name ASC'): + # Possible bang if move_line_ids is too long + # We can not slice here as we have to do the sort. + # If slice has to be done it means that we have to reorder in python + # after all is finished. That quite crapy... + # We have a defective desing here (mea culpa) that should be fixed + # + # TODO improve that by making a better domain or if not possible + # by using python sort if not move_line_ids: return [] if not isinstance(move_line_ids, list): @@ -441,6 +449,7 @@ class CommonReportHeaderWebkit(common_report_header): SELECT l.id AS id, l.date AS ldate, j.code AS jcode , + j.type AS jtype, l.currency_id, l.account_id, l.amount_currency, @@ -455,7 +464,8 @@ SELECT l.id AS id, l.partner_id AS lpartner_id, p.name AS partner_name, m.name AS move_name, - COALESCE(partialrec.name, fullrec.name, '') AS rec_name, + COALESCE(partialrec.name, fullrec.name, '') AS rec_name, + COALESCE(partialrec.id, fullrec.id, NULL) AS rec_id, m.id AS move_id, c.name AS currency_code, i.id AS invoice_id, diff --git a/account_financial_report_webkit/report/open_invoices.py b/account_financial_report_webkit/report/open_invoices.py index 237f9e4f..0cda47e8 100644 --- a/account_financial_report_webkit/report/open_invoices.py +++ b/account_financial_report_webkit/report/open_invoices.py @@ -93,7 +93,7 @@ class PartnersOpenInvoicesWebkit(report_sxw.rml_parse, CommonPartnersReportHeade """Populate a ledger_lines attribute on each browse record that will be used by mako template""" new_ids = data['form']['chart_account_id'] - + # import pdb; pdb.set_trace() # Account initial balance memoizer init_balance_memoizer = {} # Reading form diff --git a/account_financial_report_webkit/report/report.xml b/account_financial_report_webkit/report/report.xml index 6fa33a25..c239faa0 100644 --- a/account_financial_report_webkit/report/report.xml +++ b/account_financial_report_webkit/report/report.xml @@ -14,23 +14,16 @@ General Ledger Webkit account_financial_report_webkit/report/templates/account_report_general_ledger.mako account_financial_report_webkit/report/templates/account_report_general_ledger.mako - + + account_report_general_ledger_webkit - - - + + webkit account.account_report_partners_ledger_webkit @@ -44,6 +37,7 @@ account_financial_report_webkit/report/templates/account_report_partners_ledger.mako account_financial_report_webkit/report/templates/account_report_partners_ledger.mako + account_report_partners_ledger_webkit @@ -64,6 +58,7 @@ account_financial_report_webkit/report/templates/account_report_trial_balance.mako account_financial_report_webkit/report/templates/account_report_trial_balance.mako + account_report_trial_balance_webkit @@ -84,6 +79,7 @@ account_financial_report_webkit/report/templates/account_report_partner_balance.mako account_financial_report_webkit/report/templates/account_report_partner_balance.mako + account_report_partner_balance_webkit @@ -104,6 +100,7 @@ account_financial_report_webkit/report/templates/account_report_open_invoices.mako account_financial_report_webkit/report/templates/account_report_open_invoices.mako + account_report_open_invoices_webkit @@ -111,6 +108,31 @@ + + webkit + account.account_aged_trial_balance_webkit + + + + + account.account + ir.actions.report.xml + Aged Partner Palance + account_financial_report_webkit/report/templates/aged_trial_webkit.mako + account_financial_report_webkit/report/templates/aged_trial_webkit.mako + + + + account_aged_trial_balance_webkit + + + + + webkit account.account_report_print_journal_webkit @@ -124,6 +146,7 @@ account_financial_report_webkit/report/templates/account_report_print_journal.mako account_financial_report_webkit/report/templates/account_report_print_journal.mako + account_report_print_journal_webkit diff --git a/account_financial_report_webkit/report/templates/aged_trial_webkit.mako b/account_financial_report_webkit/report/templates/aged_trial_webkit.mako new file mode 100644 index 00000000..d4720e53 --- /dev/null +++ b/account_financial_report_webkit/report/templates/aged_trial_webkit.mako @@ -0,0 +1,155 @@ +## -*- coding: utf-8 -*- + + + + + + + <%! + def amount(text): + # replace by a non-breaking hyphen (it will not word-wrap between hyphen and numbers) + return text.replace('-', '‑') + %> + + <%setLang(user.lang)%> + +
+
+
${_('Chart of Account')}
+
${_('Fiscal Year')}
+
+ %if filter_form(data) == 'filter_date': + ${_('Dates Filter')} + %else: + ${_('Periods Filter')} + %endif +
+
${_('Clearance Date')}
+
${_('Accounts Filter')}
+
${_('Target Moves')}
+ +
+
+
${ chart_account.name }
+
${ fiscalyear.name if fiscalyear else '-' }
+
+ ${_('From:')} + %if filter_form(data) == 'filter_date': + ${formatLang(start_date, date=True) if start_date else u'' } + %else: + ${start_period.name if start_period else u''} + %endif + ${_('To:')} + %if filter_form(data) == 'filter_date': + ${ formatLang(stop_date, date=True) if stop_date else u'' } + %else: + ${stop_period.name if stop_period else u'' } + %endif +
+
${ formatLang(date_until, date=True) }
+
+ %if partner_ids: + ${_('Custom Filter')} + %else: + ${ display_partner_account(data) } + %endif +
+
${ display_target_move(data) }
+
+
+ %for acc in objects: + %if acc.aged_lines: + + + + +
+
+
+ ## partner +
${_('Partner')}
+ ## code +
${_('code')}
+ ## balance +
${_('balance')}
+ ## Classifications + %for title in ranges_titles: +
${title}
+ %endfor +
+
+
+ %for partner_name, p_id, p_ref, p_name in acc.partners_order: + %if acc.aged_lines.get(p_id): +
+ <%line = acc.aged_lines[p_id]%> + <%percents = acc.aged_percents%> + <%totals = acc.aged_totals%> +
${partner_name}
+
TOSEPC
+ +
${formatLang(line.get('balance') or 0.0) | amount}
+ %for classif in ranges: +
+ ${formatLang(line['aged_lines'][classif] or 0.0) | amount} +
+ %endfor +
+ %endif + %endfor +
+
${_('Total')}
+
+
${formatLang(totals['balance']) | amount}
+ %for classif in ranges: +
${formatLang(totals[classif]) | amount}
+ %endfor +
+ +
+
${_('Percents')}
+
+
+ %for classif in ranges: +
${formatLang(percents[classif]) | amount}%
+ %endfor +
+
+
+ + %endif + %endfor +
+ + diff --git a/account_financial_report_webkit/report/templates/open_invoices_inclusion.mako.html b/account_financial_report_webkit/report/templates/open_invoices_inclusion.mako.html index d868cb9b..20be358b 100644 --- a/account_financial_report_webkit/report/templates/open_invoices_inclusion.mako.html +++ b/account_financial_report_webkit/report/templates/open_invoices_inclusion.mako.html @@ -9,7 +9,7 @@ %> - + %for partner_name, p_id, p_ref, p_name in account.partners_order: <% total_debit = 0.0 @@ -18,7 +18,7 @@ cumul_balance_curr = 0.0 part_cumul_balance = 0.0 - part_cumul_balance_curr = 0.0 + part_cumul_balance_curr = 0.0 %>