# -*- 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 days 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 += [_(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): 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): """: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 browse record that will be used by mako template The computation are based on the ledger_lines attribute set on account contained by :attr:`objects` self.object 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 :params: partner_id current partner :params: ledger_lines generated by parent :class:`.open_invoices.PartnersOpenInvoicesWebkit` :return computed ledger lines """ 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 overdur 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): 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): return self._compute_delay_from_key('date_maturity', line, end_date) def compute_delay_from_date(self, line, end_date, ledger_lines): return self._compute_delay_from_key('ldate', line, end_date) def compute_delay_from_partial_rec(self, line, end_date, ledger_lines): sale_lines = [x for x in ledger_lines if x['jtype'] in REC_PAY_TYPE] refund_lines = [x for x in ledger_lines if x['jtype'] in REFUND_TYPE] reference_line = line if len(sale_lines) == 1: reference_line = sale_lines[0] elif len(refund_lines) == 1: reference_line = refund_lines[0] key = 'date_maturity' if 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): 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) 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 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, )