Browse Source
[ADD] core of aged partner balance report
[ADD] core of aged partner balance report
partial reconciliation support is not now implementedpull/7/merge
Nicolas Bessi
11 years ago
13 changed files with 587 additions and 18 deletions
-
3account_financial_report_webkit/__openerp__.py
-
1account_financial_report_webkit/report/__init__.py
-
243account_financial_report_webkit/report/aged_partner_balance.py
-
14account_financial_report_webkit/report/common_reports.py
-
2account_financial_report_webkit/report/open_invoices.py
-
45account_financial_report_webkit/report/report.xml
-
155account_financial_report_webkit/report/templates/aged_trial_webkit.mako
-
4account_financial_report_webkit/report/templates/open_invoices_inclusion.mako.html
-
1account_financial_report_webkit/report/webkit_parser_header_fix.py
-
4account_financial_report_webkit/report_menus.xml
-
1account_financial_report_webkit/wizard/__init__.py
-
38account_financial_report_webkit/wizard/aged_partner_balance_wizard.py
-
94account_financial_report_webkit/wizard/aged_partner_balance_wizard.xml
@ -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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
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, |
|||
) |
@ -0,0 +1,155 @@ |
|||
## -*- coding: utf-8 -*- |
|||
<!DOCTYPE html SYSTEM |
|||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> |
|||
<html xmlns="http://www.w3.org/1999/xhtml"> |
|||
<head> |
|||
<style type="text/css"> |
|||
.overflow_ellipsis { |
|||
text-overflow: ellipsis; |
|||
overflow: hidden; |
|||
white-space: nowrap; |
|||
} |
|||
|
|||
.open_invoice_previous_line { |
|||
font-style: italic; |
|||
} |
|||
|
|||
.percent_line { |
|||
font-style: italic; |
|||
} |
|||
|
|||
.amount { |
|||
text-align:right; |
|||
} |
|||
|
|||
.classif_title { |
|||
text-align:right; |
|||
} |
|||
|
|||
.classif{ |
|||
width: ${700/len(ranges)}px; |
|||
} |
|||
.total{ |
|||
font-weight:bold; |
|||
} |
|||
${css} |
|||
</style> |
|||
</head> |
|||
|
|||
<%! |
|||
def amount(text): |
|||
# replace by a non-breaking hyphen (it will not word-wrap between hyphen and numbers) |
|||
return text.replace('-', '‑') |
|||
%> |
|||
<body> |
|||
<%setLang(user.lang)%> |
|||
|
|||
<div class="act_as_table data_table"> |
|||
<div class="act_as_row labels"> |
|||
<div class="act_as_cell">${_('Chart of Account')}</div> |
|||
<div class="act_as_cell">${_('Fiscal Year')}</div> |
|||
<div class="act_as_cell"> |
|||
%if filter_form(data) == 'filter_date': |
|||
${_('Dates Filter')} |
|||
%else: |
|||
${_('Periods Filter')} |
|||
%endif |
|||
</div> |
|||
<div class="act_as_cell">${_('Clearance Date')}</div> |
|||
<div class="act_as_cell">${_('Accounts Filter')}</div> |
|||
<div class="act_as_cell">${_('Target Moves')}</div> |
|||
|
|||
</div> |
|||
<div class="act_as_row"> |
|||
<div class="act_as_cell">${ chart_account.name }</div> |
|||
<div class="act_as_cell">${ fiscalyear.name if fiscalyear else '-' }</div> |
|||
<div class="act_as_cell"> |
|||
${_('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 |
|||
</div> |
|||
<div class="act_as_cell">${ formatLang(date_until, date=True) }</div> |
|||
<div class="act_as_cell"> |
|||
%if partner_ids: |
|||
${_('Custom Filter')} |
|||
%else: |
|||
${ display_partner_account(data) } |
|||
%endif |
|||
</div> |
|||
<div class="act_as_cell">${ display_target_move(data) }</div> |
|||
</div> |
|||
</div> |
|||
%for acc in objects: |
|||
%if acc.aged_lines: |
|||
<div class="account_title bg" style="width: 1080px; margin-top: 20px; font-size: 12px;">${acc.code} - ${acc.name}</div> |
|||
|
|||
|
|||
|
|||
<div class="act_as_table list_table" style="margin-top: 5px;"> |
|||
<div class="act_as_thead"> |
|||
<div class="act_as_row labels"> |
|||
## partner |
|||
<div class="act_as_cell first_column" style="width: 60px;">${_('Partner')}</div> |
|||
## code |
|||
<div class="act_as_cell" style="width: 70px;">${_('code')}</div> |
|||
## balance |
|||
<div class="act_as_cell classif_title" style="width: 70px;">${_('balance')}</div> |
|||
## Classifications |
|||
%for title in ranges_titles: |
|||
<div class="act_as_cell classif classif_title">${title}</div> |
|||
%endfor |
|||
</div> |
|||
</div> |
|||
<div class="act_as_tbody"> |
|||
%for partner_name, p_id, p_ref, p_name in acc.partners_order: |
|||
%if acc.aged_lines.get(p_id): |
|||
<div class="act_as_row lines"> |
|||
<%line = acc.aged_lines[p_id]%> |
|||
<%percents = acc.aged_percents%> |
|||
<%totals = acc.aged_totals%> |
|||
<div class="act_as_cell first_column">${partner_name}</div> |
|||
<div class="act_as_cell">TOSEPC</div> |
|||
|
|||
<div class="act_as_cell amount">${formatLang(line.get('balance') or 0.0) | amount}</div> |
|||
%for classif in ranges: |
|||
<div class="act_as_cell classif amount"> |
|||
${formatLang(line['aged_lines'][classif] or 0.0) | amount} |
|||
</div> |
|||
%endfor |
|||
</div> |
|||
%endif |
|||
%endfor |
|||
<div class="act_as_row labels"> |
|||
<div class="act_as_cell total">${_('Total')}</div> |
|||
<div class="act_as_cell"></div> |
|||
<div class="act_as_cell amount classif total">${formatLang(totals['balance']) | amount}</div> |
|||
%for classif in ranges: |
|||
<div class="act_as_cell amount classif total">${formatLang(totals[classif]) | amount}</div> |
|||
%endfor |
|||
</div> |
|||
|
|||
<div class="act_as_row"> |
|||
<div class="act_as_cell"><b>${_('Percents')}</b></div> |
|||
<div class="act_as_cell"></div> |
|||
<div class="act_as_cell"></div> |
|||
%for classif in ranges: |
|||
<div class="act_as_cell amount percent_line classif">${formatLang(percents[classif]) | amount}%</div> |
|||
%endfor |
|||
</div> |
|||
</div> |
|||
<br/> |
|||
|
|||
%endif |
|||
%endfor |
|||
</div> |
|||
</body> |
|||
</html> |
@ -0,0 +1,38 @@ |
|||
# -*- 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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
from openerp.osv import orm, fields |
|||
|
|||
class AccountAgedTrialBalance(orm.TransientModel): |
|||
"""Will launch age partner balance report. |
|||
This report is based on Open Invoice Report |
|||
and share a lot of knowledge with him |
|||
""" |
|||
|
|||
_inherit = "open.invoices.webkit" |
|||
_name = "account.aged.trial.balance.webkit" |
|||
_description = "Aged partner balanced" |
|||
|
|||
def _print_report(self, cr, uid, ids, data, context=None): |
|||
# we update form with display account value |
|||
data = self.pre_print_report(cr, uid, ids, data, context=context) |
|||
return {'type': 'ir.actions.report.xml', |
|||
'report_name': 'account.account_aged_trial_balance_webkit', |
|||
'datas': data} |
@ -0,0 +1,94 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<openerp> |
|||
<data> |
|||
|
|||
<record id="account_aged_trial_balance_webkit" model="ir.ui.view"> |
|||
<field name="name">Aged Partner Balance Report</field> |
|||
<field name="model">account.aged.trial.balance.webkit</field> |
|||
<field name="inherit_id" ref="account.account_common_report_view"/> |
|||
<field name="arch" type="xml"> |
|||
<data> |
|||
|
|||
<xpath expr="/form/label[@string='']" position="replace"> |
|||
<separator string="Aged Partner Balance" colspan="4"/> |
|||
<label nolabel="1" |
|||
colspan="4" |
|||
string="This report list partner open balances and indicate when payment is (or was) supposed to be completed"/> |
|||
</xpath> |
|||
|
|||
<field name="chart_account_id" position='attributes'> |
|||
<attribute name="colspan">4</attribute> |
|||
</field> |
|||
|
|||
<xpath expr="//field[@name='target_move']" position="after"> |
|||
<newline/> |
|||
<field name="result_selection" colspan="4"/> |
|||
</xpath> |
|||
|
|||
<xpath expr="/form/notebook[1]" position="after"> |
|||
<separator string="Clearance Analysis Options" colspan="4"/> |
|||
<newline/> |
|||
<field name="until_date"/> |
|||
</xpath> |
|||
|
|||
<field name="filter" position="attributes"> |
|||
<attribute name="invisible">1</attribute> |
|||
</field> |
|||
|
|||
<page name="filters" position="after"> |
|||
<page string="Partners Filters" name="partners"> |
|||
<separator string="Print only" colspan="4"/> |
|||
<field name="partner_ids" colspan="4" nolabel="1"/> |
|||
</page> |
|||
</page> |
|||
|
|||
<page name="journal_ids" position="attributes"> |
|||
<attribute name="invisible">True</attribute> |
|||
</page> |
|||
|
|||
<field name="fiscalyear_id" position="attributes"> |
|||
<attribute name="on_change">onchange_fiscalyear(fiscalyear_id, period_to, date_to, until_date)</attribute> |
|||
</field> |
|||
|
|||
<field name="date_from" position="attributes"> |
|||
<attribute name="invisible">1</attribute> |
|||
</field> |
|||
|
|||
<field name="date_to" position="attributes"> |
|||
<attribute name="invisible">1</attribute> |
|||
</field> |
|||
|
|||
<group string="Dates" position="attributes"> |
|||
<attribute name="invisible">1</attribute> |
|||
<attribute name="attrs"></attribute> |
|||
</group> |
|||
|
|||
<group string="Periods" position="attributes"> |
|||
<attribute name="invisible">0</attribute> |
|||
<attribute name="attrs"></attribute> |
|||
</group> |
|||
|
|||
<field name="period_from" position="attributes"> |
|||
<attribute name="domain">[('fiscalyear_id', '=', fiscalyear_id), ('special', '=', False)]</attribute> |
|||
</field> |
|||
|
|||
<field name="period_to" position="attributes"> |
|||
<attribute name="domain">[('fiscalyear_id', '=', fiscalyear_id), ('special', '=', False)]</attribute> |
|||
</field> |
|||
|
|||
</data> |
|||
</field> |
|||
</record> |
|||
|
|||
<record id="action_account_aged_trial_balance_menu_webkit" |
|||
model="ir.actions.act_window"> |
|||
<field name="name">Aged partner balance</field> |
|||
<field name="type">ir.actions.act_window</field> |
|||
<field name="res_model">account.aged.trial.balance.webkit</field> |
|||
<field name="view_type">form</field> |
|||
<field name="view_mode">form</field> |
|||
<field name="view_id" ref="account_aged_trial_balance_webkit"/> |
|||
<field name="target">new</field> |
|||
</record> |
|||
</data> |
|||
</openerp> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue