You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

453 lines
17 KiB

# -*- 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 __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 = [_('Current')]
titles += [_(u'Age ≤ %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_lines_by_invoice, 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
)
self.localcontext.update({
'aging_method': data['form']['aging_method']
})
for acc in self.objects:
acc.aged_lines = {}
acc.aged_lines_by_invoice = {}
acc.agged_totals = {}
acc.agged_percents = {}
for part_id, partner_lines in acc.ledger_lines.items():
partner_aged_lines, invoice_aged_lines = \
self.compute_aged_lines(part_id,
partner_lines,
data)
if partner_aged_lines:
acc.aged_lines[part_id] = partner_aged_lines
if data['form']['detailed_by_invoice']:
acc.aged_lines_by_invoice[part_id] = invoice_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, ...}
}
- dict of computed aged lines by invoice
eg {'SAJ/2015/003': {'balance: 700.0
'aged_lines': {(90, 120): 0.0, ...},
'SAJ/2015/004': {'balance: 300.0
'aged_lines': {(90, 120): 300.0, ...},
}
"""
detailed_by_invoice = data['form']['detailed_by_invoice']
aging_method = data['form']['aging_method']
lines_to_age = self.filter_lines(partner_id, ledger_lines)
res_by_partner = {}
res_by_invoice = {}
end_date = self._get_end_date(data)
partner_aged_lines = dict.fromkeys(RANGES, 0.0)
reconcile_lookup = self.get_reconcile_count_lookup(lines_to_age)
res_by_partner['aged_lines'] = partner_aged_lines
for line in lines_to_age:
compute_method = self.get_compute_method(reconcile_lookup,
partner_id,
line,
aging_method)
delay = compute_method(line, end_date, ledger_lines)
classification = self.classify_line(partner_id, delay)
amount = line['debit'] - line['credit']
partner_aged_lines[classification] += amount
# Populate the aged_lines_by_invoice dictionary if the option has
# been chosen in the wizard
if detailed_by_invoice:
invoice = line['invoice_number']
if invoice not in res_by_invoice:
res_by_invoice[invoice] = {
'aged_lines': dict.fromkeys(RANGES, 0.0),
}
res_by_invoice[invoice][
'aged_lines'][classification] += amount
if detailed_by_invoice:
for invoice in res_by_invoice:
self.compute_balance(
res_by_invoice[invoice],
res_by_invoice[invoice]['aged_lines']
)
self.compute_balance(res_by_partner, partner_aged_lines)
return res_by_partner, res_by_invoice
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,
aging_method):
"""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') \
and aging_method == 'due_date':
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,
)