mreficent
8 years ago
14 changed files with 879 additions and 0 deletions
-
72customer_outstanding_statement/README.rst
-
7customer_outstanding_statement/__init__.py
-
23customer_outstanding_statement/__openerp__.py
-
6customer_outstanding_statement/report/__init__.py
-
315customer_outstanding_statement/report/customer_outstanding_statement.py
-
BINcustomer_outstanding_statement/static/description/Outstanding_Statement.png
-
BINcustomer_outstanding_statement/static/description/icon.png
-
75customer_outstanding_statement/static/description/index.html
-
6customer_outstanding_statement/tests/__init__.py
-
67customer_outstanding_statement/tests/test_customer_outstanding_statement.py
-
202customer_outstanding_statement/views/statement.xml
-
6customer_outstanding_statement/wizard/__init__.py
-
51customer_outstanding_statement/wizard/customer_outstanding_statement_wizard.py
-
49customer_outstanding_statement/wizard/customer_outstanding_statement_wizard.xml
@ -0,0 +1,72 @@ |
|||||
|
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg |
||||
|
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html |
||||
|
:alt: License: AGPL-3 |
||||
|
|
||||
|
==================================== |
||||
|
Print Customer Outstanding Statement |
||||
|
==================================== |
||||
|
|
||||
|
The outstanding statement provides details of all outstanding customer receivables |
||||
|
up to a particular date. This includes all unpaid invoices, unclaimed refunds and |
||||
|
outstanding payments. The list is displayed in chronological order and is split by currencies. |
||||
|
|
||||
|
Aging details can be shown in the report, expressed in aging buckets (30 days |
||||
|
due, ...), so the customer can review how much is open, due or overdue. |
||||
|
|
||||
|
Configuration |
||||
|
============= |
||||
|
|
||||
|
Users willing to access to this report should have proper Accounting & Finance rights: |
||||
|
|
||||
|
#. Go to *Settings / Users* and edit your user to add the corresponding access rights as follows. |
||||
|
#. In *Application / Accounting & Finance*, select *Accountant* or *Adviser* options. |
||||
|
|
||||
|
Usage |
||||
|
===== |
||||
|
|
||||
|
To use this module, you need to: |
||||
|
|
||||
|
#. Go to Customers and select one or more |
||||
|
#. Press 'Action > Customer Outstanding Statement' |
||||
|
#. Indicate if you want to display aging buckets |
||||
|
|
||||
|
|
||||
|
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas |
||||
|
:alt: Try me on Runbot |
||||
|
:target: https://runbot.odoo-community.org/runbot/91/9.0 |
||||
|
|
||||
|
Bug Tracker |
||||
|
=========== |
||||
|
|
||||
|
Bugs are tracked on `GitHub Issues |
||||
|
<https://github.com/OCA/account-financial-reporting/issues>`_. In case of trouble, |
||||
|
please check there if your issue has already been reported. If you spotted it |
||||
|
first, help us smash it by providing detailed and welcomed feedback. |
||||
|
|
||||
|
Credits |
||||
|
======= |
||||
|
|
||||
|
Images |
||||
|
------ |
||||
|
|
||||
|
* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_. |
||||
|
|
||||
|
Contributors |
||||
|
------------ |
||||
|
|
||||
|
* Miquel Raïch <miquel.raich@eficent.com> |
||||
|
|
||||
|
Maintainer |
||||
|
---------- |
||||
|
|
||||
|
.. image:: https://odoo-community.org/logo.png |
||||
|
:alt: Odoo Community Association |
||||
|
:target: https://odoo-community.org |
||||
|
|
||||
|
This module is maintained by the OCA. |
||||
|
|
||||
|
OCA, or the Odoo Community Association, is a nonprofit organization whose |
||||
|
mission is to support the collaborative development of Odoo features and |
||||
|
promote its widespread use. |
||||
|
|
||||
|
To contribute to this module, please visit https://odoo-community.org. |
@ -0,0 +1,7 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Copyright 2017 Eficent Business and IT Consulting Services S.L. |
||||
|
# (http://www.eficent.com) |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
|
from . import report |
||||
|
from . import wizard |
@ -0,0 +1,23 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Copyright 2017 Eficent Business and IT Consulting Services S.L. |
||||
|
# (http://www.eficent.com) |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
|
{ |
||||
|
'name': 'Customer Outstanding Statement', |
||||
|
'version': '9.0.1.0.0', |
||||
|
'category': 'Reports/pdf', |
||||
|
'summary': 'OCA Financial Reports', |
||||
|
'author': "Eficent, Odoo Community Association (OCA)", |
||||
|
'website': 'https://github.com/OCA/account-financial-reporting', |
||||
|
'license': 'AGPL-3', |
||||
|
'depends': [ |
||||
|
'account', |
||||
|
], |
||||
|
'data': [ |
||||
|
'views/statement.xml', |
||||
|
'wizard/customer_outstanding_statement_wizard.xml', |
||||
|
], |
||||
|
'installable': True, |
||||
|
'application': False, |
||||
|
} |
@ -0,0 +1,6 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Copyright 2017 Eficent Business and IT Consulting Services S.L. |
||||
|
# (http://www.eficent.com) |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
|
from . import customer_outstanding_statement |
@ -0,0 +1,315 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Copyright 2017 Eficent Business and IT Consulting Services S.L. |
||||
|
# (http://www.eficent.com) |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
|
from datetime import datetime, timedelta |
||||
|
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT |
||||
|
from openerp import api, fields, models |
||||
|
|
||||
|
|
||||
|
class CustomerOutstandingStatement(models.AbstractModel): |
||||
|
"""Model of Customer Outstanding Statement""" |
||||
|
|
||||
|
_name = 'report.customer_outstanding_statement.statement' |
||||
|
|
||||
|
def _format_date_to_partner_lang(self, str_date, partner_id): |
||||
|
lang_code = self.env['res.partner'].browse(partner_id).lang |
||||
|
lang_id = self.env['res.lang']._lang_get(lang_code) |
||||
|
lang = self.env['res.lang'].browse(lang_id) |
||||
|
date = datetime.strptime(str_date, DEFAULT_SERVER_DATE_FORMAT).date() |
||||
|
return date.strftime(lang.date_format) |
||||
|
|
||||
|
def _display_lines_sql_q1(self, partners, date_end): |
||||
|
return """ |
||||
|
SELECT m.name as move_id, l.partner_id, l.date, l.name, |
||||
|
l.ref, l.blocked, l.currency_id, l.company_id, |
||||
|
CASE WHEN (l.currency_id is not null AND l.amount_currency > 0.0) |
||||
|
THEN sum(l.amount_currency) |
||||
|
ELSE sum(l.debit) |
||||
|
END as debit, |
||||
|
CASE WHEN (l.currency_id is not null AND l.amount_currency < 0.0) |
||||
|
THEN sum(l.amount_currency * (-1)) |
||||
|
ELSE sum(l.credit) |
||||
|
END as credit, |
||||
|
CASE WHEN l.balance > 0.0 |
||||
|
THEN l.balance - sum(coalesce(pd.amount, 0.0)) |
||||
|
ELSE l.balance + sum(coalesce(pc.amount, 0.0)) |
||||
|
END AS open_amount, |
||||
|
CASE WHEN l.balance > 0.0 |
||||
|
THEN l.amount_currency - sum(coalesce(pd.amount_currency, 0.0)) |
||||
|
ELSE l.amount_currency + sum(coalesce(pc.amount_currency, 0.0)) |
||||
|
END AS open_amount_currency, |
||||
|
CASE WHEN l.date_maturity is null |
||||
|
THEN l.date |
||||
|
ELSE l.date_maturity |
||||
|
END as date_maturity |
||||
|
FROM account_move_line l |
||||
|
JOIN account_account_type at ON (at.id = l.user_type_id) |
||||
|
JOIN account_move m ON (l.move_id = m.id) |
||||
|
LEFT JOIN (SELECT pr.* |
||||
|
FROM account_partial_reconcile pr |
||||
|
INNER JOIN account_move_line l2 |
||||
|
ON pr.credit_move_id = l2.id |
||||
|
WHERE l2.date <= '%s' |
||||
|
) as pd ON pd.debit_move_id = l.id |
||||
|
LEFT JOIN (SELECT pr.* |
||||
|
FROM account_partial_reconcile pr |
||||
|
INNER JOIN account_move_line l2 |
||||
|
ON pr.debit_move_id = l2.id |
||||
|
WHERE l2.date <= '%s' |
||||
|
) as pc ON pc.credit_move_id = l.id |
||||
|
WHERE l.partner_id IN (%s) AND at.type = 'receivable' |
||||
|
AND not l.reconciled AND l.date <= '%s' |
||||
|
GROUP BY l.partner_id, m.name, l.date, l.date_maturity, l.name, |
||||
|
l.ref, l.blocked, l.currency_id, |
||||
|
l.balance, l.amount_currency, l.company_id |
||||
|
""" % (date_end, date_end, partners, date_end) |
||||
|
|
||||
|
def _display_lines_sql_q2(self): |
||||
|
return """ |
||||
|
SELECT partner_id, currency_id, move_id, date, date_maturity, |
||||
|
debit, credit, name, ref, blocked, company_id, |
||||
|
CASE WHEN currency_id is not null |
||||
|
THEN open_amount_currency |
||||
|
ELSE open_amount |
||||
|
END as open_amount |
||||
|
FROM Q1 |
||||
|
""" |
||||
|
|
||||
|
def _display_lines_sql_q3(self, company_id): |
||||
|
return """ |
||||
|
SELECT Q2.partner_id, move_id, date, date_maturity, Q2.name, ref, |
||||
|
debit, credit, debit-credit AS amount, blocked, |
||||
|
COALESCE(Q2.currency_id, c.currency_id) AS currency_id, open_amount |
||||
|
FROM Q2 |
||||
|
JOIN res_company c ON (c.id = Q2.company_id) |
||||
|
WHERE c.id = %s |
||||
|
""" % company_id |
||||
|
|
||||
|
def _get_account_display_lines(self, company_id, partner_ids, date_end): |
||||
|
res = dict(map(lambda x: (x, []), partner_ids)) |
||||
|
partners = ', '.join([str(i) for i in partner_ids]) |
||||
|
date_end = datetime.strptime( |
||||
|
date_end, DEFAULT_SERVER_DATE_FORMAT).date() |
||||
|
self.env.cr.execute("""WITH Q1 AS (%s), Q2 AS (%s), Q3 AS (%s) |
||||
|
SELECT partner_id, currency_id, move_id, date, date_maturity, debit, |
||||
|
credit, amount, open_amount, name, ref, blocked |
||||
|
FROM Q3 |
||||
|
ORDER BY date, date_maturity, move_id""" % ( |
||||
|
self._display_lines_sql_q1(partners, date_end), |
||||
|
self._display_lines_sql_q2(), |
||||
|
self._display_lines_sql_q3(company_id))) |
||||
|
for row in self.env.cr.dictfetchall(): |
||||
|
res[row.pop('partner_id')].append(row) |
||||
|
return res |
||||
|
|
||||
|
def _show_buckets_sql_q1(self, partners, date_end): |
||||
|
return """ |
||||
|
SELECT l.partner_id, l.currency_id, l.company_id, l.move_id, |
||||
|
l.balance - sum(coalesce(pr.amount, 0.0)) as open_due, |
||||
|
l.amount_currency - sum(coalesce(pr.amount_currency, 0.0)) |
||||
|
AS open_due_currency, |
||||
|
CASE WHEN l.date_maturity is null |
||||
|
THEN l.date |
||||
|
ELSE l.date_maturity |
||||
|
END as date_maturity |
||||
|
FROM account_move_line l |
||||
|
JOIN account_account_type at ON (at.id = l.user_type_id) |
||||
|
JOIN account_move m ON (l.move_id = m.id) |
||||
|
LEFT JOIN ( |
||||
|
SELECT pr.* |
||||
|
FROM account_partial_reconcile pr |
||||
|
INNER JOIN account_move_line l2 |
||||
|
ON pr.credit_move_id = l2.id |
||||
|
WHERE l2.date <= '%s' |
||||
|
) as pr |
||||
|
ON pr.debit_move_id = l.id |
||||
|
WHERE l.partner_id IN (%s) AND at.type = 'receivable' |
||||
|
AND not l.reconciled AND not l.blocked |
||||
|
AND l.balance > 0.0 |
||||
|
GROUP BY l.partner_id, l.currency_id, l.date, l.date_maturity, |
||||
|
l.amount_currency, l.balance, l.move_id, |
||||
|
l.company_id |
||||
|
""" % (date_end, partners) |
||||
|
|
||||
|
def _show_buckets_sql_q2(self, today, minus_30, minus_60, minus_90, |
||||
|
minus_120): |
||||
|
return """ |
||||
|
SELECT partner_id, currency_id, date_maturity, open_due, |
||||
|
open_due_currency, move_id, company_id, |
||||
|
CASE |
||||
|
WHEN '%s' <= date_maturity AND currency_id is null |
||||
|
THEN open_due |
||||
|
WHEN '%s' <= date_maturity AND currency_id is not null |
||||
|
THEN open_due_currency |
||||
|
ELSE 0.0 |
||||
|
END as current, |
||||
|
CASE |
||||
|
WHEN '%s' < date_maturity AND date_maturity < '%s' |
||||
|
AND currency_id is null THEN open_due |
||||
|
WHEN '%s' < date_maturity AND date_maturity < '%s' |
||||
|
AND currency_id is not null |
||||
|
THEN open_due_currency |
||||
|
ELSE 0.0 |
||||
|
END as b_1_30, |
||||
|
CASE |
||||
|
WHEN '%s' < date_maturity AND date_maturity <= '%s' |
||||
|
AND currency_id is null THEN open_due |
||||
|
WHEN '%s' < date_maturity AND date_maturity <= '%s' |
||||
|
AND currency_id is not null |
||||
|
THEN open_due_currency |
||||
|
ELSE 0.0 |
||||
|
END as b_30_60, |
||||
|
CASE |
||||
|
WHEN '%s' < date_maturity AND date_maturity <= '%s' |
||||
|
AND currency_id is null THEN open_due |
||||
|
WHEN '%s' < date_maturity AND date_maturity <= '%s' |
||||
|
AND currency_id is not null |
||||
|
THEN open_due_currency |
||||
|
ELSE 0.0 |
||||
|
END as b_60_90, |
||||
|
CASE |
||||
|
WHEN '%s' < date_maturity AND date_maturity <= '%s' |
||||
|
AND currency_id is null THEN open_due |
||||
|
WHEN '%s' < date_maturity AND date_maturity <= '%s' |
||||
|
AND currency_id is not null |
||||
|
THEN open_due_currency |
||||
|
ELSE 0.0 |
||||
|
END as b_90_120, |
||||
|
CASE |
||||
|
WHEN date_maturity <= '%s' AND currency_id is null |
||||
|
THEN open_due |
||||
|
WHEN date_maturity <= '%s' AND currency_id is not null |
||||
|
THEN open_due_currency |
||||
|
ELSE 0.0 |
||||
|
END as b_over_120 |
||||
|
FROM Q1 |
||||
|
GROUP BY partner_id, currency_id, date_maturity, open_due, |
||||
|
open_due_currency, move_id, company_id |
||||
|
""" % (today, today, minus_30, today, minus_30, today, minus_60, |
||||
|
minus_30, minus_60, minus_30, minus_90, minus_60, minus_90, |
||||
|
minus_60, minus_120, minus_90, minus_120, minus_90, minus_120, |
||||
|
minus_120) |
||||
|
|
||||
|
def _show_buckets_sql_q3(self, company_id): |
||||
|
return """ |
||||
|
SELECT Q2.partner_id, current, b_1_30, b_30_60, b_60_90, b_90_120, |
||||
|
b_over_120, |
||||
|
COALESCE(Q2.currency_id, c.currency_id) AS currency_id |
||||
|
FROM Q2 |
||||
|
JOIN res_company c ON (c.id = Q2.company_id) |
||||
|
WHERE c.id = %s |
||||
|
""" % company_id |
||||
|
|
||||
|
def _show_buckets_sql_q4(self): |
||||
|
return """ |
||||
|
SELECT partner_id, currency_id, sum(current) as current, |
||||
|
sum(b_1_30) as b_1_30, |
||||
|
sum(b_30_60) as b_30_60, |
||||
|
sum(b_60_90) as b_60_90, |
||||
|
sum(b_90_120) as b_90_120, |
||||
|
sum(b_over_120) as b_over_120 |
||||
|
FROM Q3 |
||||
|
GROUP BY partner_id, currency_id |
||||
|
""" |
||||
|
|
||||
|
_bucket_dates = { |
||||
|
'today': fields.date.today(), |
||||
|
'minus_30': fields.date.today() - timedelta(days=30), |
||||
|
'minus_60': fields.date.today() - timedelta(days=60), |
||||
|
'minus_90': fields.date.today() - timedelta(days=90), |
||||
|
'minus_120': fields.date.today() - timedelta(days=120), |
||||
|
} |
||||
|
|
||||
|
def _get_account_show_buckets(self, company_id, partner_ids, date_end): |
||||
|
res = dict(map(lambda x: (x, []), partner_ids)) |
||||
|
partners = ', '.join([str(i) for i in partner_ids]) |
||||
|
date_end = datetime.strptime( |
||||
|
date_end, DEFAULT_SERVER_DATE_FORMAT).date() |
||||
|
self.env.cr.execute("""WITH Q1 AS (%s), Q2 AS (%s), |
||||
|
Q3 AS (%s), Q4 AS (%s) |
||||
|
SELECT partner_id, currency_id, current, b_1_30, b_30_60, b_60_90, |
||||
|
b_90_120, b_over_120, |
||||
|
current+b_1_30+b_30_60+b_60_90+b_90_120+b_over_120 |
||||
|
AS balance |
||||
|
FROM Q4 |
||||
|
GROUP BY partner_id, currency_id, current, b_1_30, b_30_60, b_60_90, |
||||
|
b_90_120, b_over_120""" % ( |
||||
|
self._show_buckets_sql_q1(partners, date_end), |
||||
|
self._show_buckets_sql_q2( |
||||
|
self._bucket_dates['today'], |
||||
|
self._bucket_dates['minus_30'], |
||||
|
self._bucket_dates['minus_60'], |
||||
|
self._bucket_dates['minus_90'], |
||||
|
self._bucket_dates['minus_120']), |
||||
|
self._show_buckets_sql_q3(company_id), |
||||
|
self._show_buckets_sql_q4())) |
||||
|
for row in self.env.cr.dictfetchall(): |
||||
|
res[row.pop('partner_id')].append(row) |
||||
|
return res |
||||
|
|
||||
|
@api.multi |
||||
|
def render_html(self, data): |
||||
|
company_id = data['company_id'] |
||||
|
partner_ids = data['partner_ids'] |
||||
|
date_end = data['date_end'] |
||||
|
today = fields.Date.today() |
||||
|
|
||||
|
buckets_to_display = {} |
||||
|
lines_to_display, amount_due = {}, {} |
||||
|
currency_to_display = {} |
||||
|
today_display, date_end_display = {}, {} |
||||
|
|
||||
|
lines = self._get_account_display_lines( |
||||
|
company_id, partner_ids, date_end) |
||||
|
|
||||
|
for partner_id in partner_ids: |
||||
|
lines_to_display[partner_id], amount_due[partner_id] = {}, {} |
||||
|
currency_to_display[partner_id] = {} |
||||
|
today_display[partner_id] = self._format_date_to_partner_lang( |
||||
|
today, partner_id) |
||||
|
date_end_display[partner_id] = self._format_date_to_partner_lang( |
||||
|
date_end, partner_id) |
||||
|
for line in lines[partner_id]: |
||||
|
currency = self.env['res.currency'].browse(line['currency_id']) |
||||
|
if currency not in lines_to_display[partner_id]: |
||||
|
lines_to_display[partner_id][currency] = [] |
||||
|
currency_to_display[partner_id][currency] = currency |
||||
|
amount_due[partner_id][currency] = 0.0 |
||||
|
if not line['blocked']: |
||||
|
amount_due[partner_id][currency] += line['open_amount'] |
||||
|
line['balance'] = amount_due[partner_id][currency] |
||||
|
line['date'] = self._format_date_to_partner_lang( |
||||
|
line['date'], partner_id) |
||||
|
line['date_maturity'] = self._format_date_to_partner_lang( |
||||
|
line['date_maturity'], partner_id) |
||||
|
lines_to_display[partner_id][currency].append(line) |
||||
|
|
||||
|
if data['show_aging_buckets']: |
||||
|
buckets = self._get_account_show_buckets( |
||||
|
company_id, partner_ids, date_end) |
||||
|
for partner_id in partner_ids: |
||||
|
buckets_to_display[partner_id] = {} |
||||
|
for line in buckets[partner_id]: |
||||
|
currency = self.env['res.currency'].browse( |
||||
|
line['currency_id']) |
||||
|
if currency not in buckets_to_display[partner_id]: |
||||
|
buckets_to_display[partner_id][currency] = [] |
||||
|
buckets_to_display[partner_id][currency] = line |
||||
|
|
||||
|
docargs = { |
||||
|
'doc_ids': partner_ids, |
||||
|
'doc_model': 'res.partner', |
||||
|
'docs': self.env['res.partner'].browse(partner_ids), |
||||
|
'Amount_Due': amount_due, |
||||
|
'Lines': lines_to_display, |
||||
|
'Buckets': buckets_to_display, |
||||
|
'Currencies': currency_to_display, |
||||
|
'Show_Buckets': data['show_aging_buckets'], |
||||
|
'Filter_non_due_partners': data['filter_non_due_partners'], |
||||
|
'Date_end': date_end_display, |
||||
|
'Date': today_display, |
||||
|
} |
||||
|
return self.env['report'].render( |
||||
|
'customer_outstanding_statement.statement', values=docargs) |
After Width: 660 | Height: 512 | Size: 24 KiB |
After Width: 128 | Height: 128 | Size: 9.2 KiB |
@ -0,0 +1,75 @@ |
|||||
|
<section class="oe_container"> |
||||
|
<div class="oe_row oe_spaced"> |
||||
|
<div class="oe_span12"> |
||||
|
<h2 class="oe_slogan">Customer Outstanding Statement</h2> |
||||
|
</div> |
||||
|
<div class="oe_span6"> |
||||
|
<div class="oe_demo oe_picture oe_screenshot"> |
||||
|
<img src="Outstanding_Statement.png"> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="oe_span6"> |
||||
|
<p class="oe_mt32"><div style="text-align:justify">The outstanding statement provides details of all outstanding |
||||
|
customer receivables up to a particular date. This includes all unpaid invoices, unclaimed |
||||
|
refunds and outstanding payments. The list is displayed in chronological order and is split |
||||
|
by currencies.<br><br>Aging details can be shown in the report, expressed in aging buckets (30 days |
||||
|
due, ...), so the customer can review how much is open, due or overdue.</div></p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</section> |
||||
|
|
||||
|
<section class="oe_container"> |
||||
|
<div class="oe_row oe_spaced"> |
||||
|
<div class="oe_span12"> |
||||
|
<h2 class="oe_slogan">Configuration</h2> |
||||
|
</div> |
||||
|
<div class="oe_span12"> |
||||
|
<p class="oe_mt32">To configure this module, you need to: |
||||
|
<ul> |
||||
|
<li>Go to <code>Settings / Users</code> and edit your user to add the corresponding access rights as follows.</li> |
||||
|
<li>In <code>Application / Accounting & Finance</code>, select <code>Accountant</code> or <code>Adviser</code> options.</li> |
||||
|
</ul> |
||||
|
</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</section> |
||||
|
|
||||
|
<section class="oe_container oe_dark"> |
||||
|
<div class="oe_row oe_spaced"> |
||||
|
<div class="oe_span12"> |
||||
|
<h2 class="oe_slogan">Usage</h2> |
||||
|
</div> |
||||
|
<div class="oe_span12"> |
||||
|
<p class="oe_mt32">To use this module, you need to: |
||||
|
<ul> |
||||
|
<li>Go to <code>Customers</code> and select one or more</li> |
||||
|
<li>Press '<code>Action > Customer Outstanding Statement</code>'</li> |
||||
|
<li>Indicate if you want to display aging buckets</li> |
||||
|
</ul> |
||||
|
</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</section> |
||||
|
|
||||
|
<section class="oe_container oe_dark"> |
||||
|
<div class="oe_row"> |
||||
|
<div class="oe_span12"> |
||||
|
<h2 class="oe_slogan">Credits</h2> |
||||
|
</div> |
||||
|
<div class="oe_span12"> |
||||
|
<h3>Contributors</h3> |
||||
|
<ul> |
||||
|
<li>Miquel Raïch <<a href="mailto:miquel.raich@eficent.com">miquel.raich@eficent.com</a>></li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
<div class="oe_span12"> |
||||
|
<h3>Maintainer</h3> |
||||
|
<p> |
||||
|
This module is maintained by the OCA.<br/> |
||||
|
OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.<br/> |
||||
|
To contribute to this module, please visit <a href="http://odoo-community.org">http://odoo-community.org</a>.<br/> |
||||
|
<a href="http://odoo-community.org"><img class="oe_picture oe_centered" src="http://odoo-community.org/logo.png"></a> |
||||
|
</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</section> |
@ -0,0 +1,6 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Copyright 2017 Eficent Business and IT Consulting Services S.L. |
||||
|
# (http://www.eficent.com) |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
|
from . import test_customer_outstanding_statement |
@ -0,0 +1,67 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Copyright 2017 Eficent Business and IT Consulting Services S.L. |
||||
|
# (http://www.eficent.com) |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
|
from openerp.tests.common import TransactionCase |
||||
|
|
||||
|
|
||||
|
class TestCustomerOutstandingStatement(TransactionCase): |
||||
|
""" |
||||
|
Tests for Customer Outstanding Statement. |
||||
|
""" |
||||
|
def setUp(self): |
||||
|
super(TestCustomerOutstandingStatement, self).setUp() |
||||
|
|
||||
|
self.res_users_model = self.env['res.users'] |
||||
|
self.company = self.env.ref('base.main_company') |
||||
|
self.partner1 = self.env.ref('base.res_partner_1') |
||||
|
self.partner2 = self.env.ref('base.res_partner_2') |
||||
|
self.g_account_user = self.env.ref('account.group_account_user') |
||||
|
|
||||
|
self.user = self._create_user('user_1', [self.g_account_user], |
||||
|
self.company).id |
||||
|
|
||||
|
self.statement_model = \ |
||||
|
self.env['report.customer_outstanding_statement.statement'] |
||||
|
self.wiz = self.env['customer.outstanding.statement.wizard'] |
||||
|
self.report_name = 'customer_outstanding_statement.statement' |
||||
|
self.report_title = 'Customer Outstanding Statement' |
||||
|
|
||||
|
def _create_user(self, login, groups, company): |
||||
|
group_ids = [group.id for group in groups] |
||||
|
user = self.res_users_model.create({ |
||||
|
'name': login, |
||||
|
'login': login, |
||||
|
'password': 'demo', |
||||
|
'email': 'example@yourcompany.com', |
||||
|
'company_id': company.id, |
||||
|
'company_ids': [(4, company.id)], |
||||
|
'groups_id': [(6, 0, group_ids)] |
||||
|
}) |
||||
|
return user |
||||
|
|
||||
|
def test_customer_outstanding_statement(self): |
||||
|
|
||||
|
wiz_id = self.wiz.with_context( |
||||
|
active_ids=[self.partner1.id, self.partner2.id], |
||||
|
).create({}) |
||||
|
|
||||
|
statement = wiz_id.button_export_pdf() |
||||
|
|
||||
|
self.assertDictContainsSubset( |
||||
|
{ |
||||
|
'type': 'ir.actions.report.xml', |
||||
|
'report_name': self.report_name, |
||||
|
'report_type': 'qweb-pdf', |
||||
|
}, |
||||
|
statement, |
||||
|
'There was an error and the PDF report was not generated.' |
||||
|
) |
||||
|
|
||||
|
data = wiz_id._prepare_outstanding_statement() |
||||
|
report = self.statement_model.render_html(data) |
||||
|
self.assertIsInstance(report, str, |
||||
|
"There was an error while compiling the report.") |
||||
|
self.assertIn("<!DOCTYPE html>", report, |
||||
|
"There was an error while compiling the report.") |
@ -0,0 +1,202 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<odoo> |
||||
|
<template id="customer_outstanding_statement.statement_document"> |
||||
|
<t t-call="report.external_layout"> |
||||
|
<div class="page"> |
||||
|
<div class="row"> |
||||
|
<div class="col-xs-5 col-xs-offset-7"> |
||||
|
<span t-field="o.name"/><br/> |
||||
|
<span t-raw="o._address_display(None, None)[o.id].replace('\n\n', '\n').replace('\n', '<br>')"/> |
||||
|
<span t-field="o.vat"/> |
||||
|
</div> |
||||
|
<p> |
||||
|
Document: Customer outstanding statement<br/> |
||||
|
Date: <span t-esc="Date[o.id]" /><br/> |
||||
|
<t t-if="o.ref">Partner ref: <span t-field="o.ref"/></t> |
||||
|
</p> |
||||
|
|
||||
|
<t t-if="Lines[o.id]"> |
||||
|
<br/> <br/> |
||||
|
<t t-foreach="Lines[o.id]" t-as="currency"> |
||||
|
<br t-if="not currency_first" /> |
||||
|
<p> |
||||
|
Outstanding Statement in <span t-esc="Currencies[o.id][currency].name"/>: |
||||
|
</p> |
||||
|
<table class="table table-condensed" style="border: 1px solid black; border-collapse: collapse;"> |
||||
|
<thead> |
||||
|
<tr> |
||||
|
<th style="border-right: 1px solid black;">Reference number</th> |
||||
|
<th class="text-center" style="border-right: 1px solid black;">Date</th> |
||||
|
<th class="text-center" style="border-right: 1px solid black;">Due Date</th> |
||||
|
<th style="border-right: 1px solid black;">Description</th> |
||||
|
<th class="text-right" style="border-right: 1px solid black;">Original Amount</th> |
||||
|
<th class="text-right" style="border-right: 1px solid black;">Open Amount</th> |
||||
|
<th class="text-right" style="border-right: 1px solid black;">Balance</th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tr t-foreach="Lines[o.id][currency]" t-as="line"> |
||||
|
<t t-if="not line['blocked']"> |
||||
|
<td style="border-right: 1px solid black;"> |
||||
|
<span t-esc="line['move_id']"/> |
||||
|
</td> |
||||
|
<td style="border-right: 1px solid black;"> |
||||
|
<span t-esc="line['date']"/> |
||||
|
</td> |
||||
|
<td style="border-right: 1px solid black;"> |
||||
|
<span t-esc="line['date_maturity']"/> |
||||
|
</td> |
||||
|
<td style="border-right: 1px solid black;"> |
||||
|
<t t-if="line['name'] != '/'"> |
||||
|
<t t-if="not line['ref']"><span t-esc="line['name']"/></t> |
||||
|
<t t-if="line['ref'] and line['name']"> |
||||
|
<t t-if="line['name'] not in line['ref']"><span t-esc="line['name']"/></t> |
||||
|
<t t-if="line['ref'] not in line['name']"><span t-esc="line['ref']"/></t> |
||||
|
</t> |
||||
|
</t> |
||||
|
<t t-if="line['name'] == '/'"><span t-esc="line['ref']"/></t> |
||||
|
</td> |
||||
|
<td class="text-right" style="border-right: 1px solid black;"> |
||||
|
<span t-esc="line['amount']" t-esc-options='{"widget": "monetary", "display_currency": "currency"}'/> |
||||
|
</td> |
||||
|
<td class="text-right" style="border-right: 1px solid black;"> |
||||
|
<span t-esc="line['open_amount']" t-esc-options='{"widget": "monetary", "display_currency": "currency"}'/> |
||||
|
</td> |
||||
|
<td class="text-right" style="border-right: 1px solid black;"> |
||||
|
<span t-esc="line['balance']" t-esc-options='{"widget": "monetary", "display_currency": "currency"}'/> |
||||
|
</td> |
||||
|
</t> |
||||
|
<t t-if="line['blocked']"> |
||||
|
<td style="border-right: 1px solid black; background-color: grey;"> |
||||
|
<span t-esc="line['move_id']"/> |
||||
|
</td> |
||||
|
<td style="border-right: 1px solid black; background-color: grey;"> |
||||
|
<span t-esc="line['date']"/> |
||||
|
</td> |
||||
|
<td style="border-right: 1px solid black; background-color: grey;"> |
||||
|
<span t-esc="line['date_maturity']"/> |
||||
|
</td> |
||||
|
<td style="border-right: 1px solid black; background-color: grey;"> |
||||
|
<t t-if="line['name'] != '/'"> |
||||
|
<t t-if="not line['ref']"><span t-esc="line['name']"/></t> |
||||
|
<t t-if="line['ref'] and line['name']"> |
||||
|
<t t-if="line['name'] not in line['ref']"><span t-esc="line['name']"/></t> |
||||
|
<t t-if="line['ref'] not in line['name']"><span t-esc="line['ref']"/></t> |
||||
|
</t> |
||||
|
</t> |
||||
|
<t t-if="line['name'] == '/'"><span t-esc="line['ref']"/></t> |
||||
|
</td> |
||||
|
<td class="text-right" style="border-right: 1px solid black; background-color: grey;"> |
||||
|
<span t-esc="line['amount']" t-esc-options='{"widget": "monetary", "display_currency": "currency"}'/> |
||||
|
</td> |
||||
|
<td class="text-right" style="border-right: 1px solid black; background-color: grey;"> |
||||
|
<span t-esc="line['open_amount']" t-esc-options='{"widget": "monetary", "display_currency": "currency"}'/> |
||||
|
</td> |
||||
|
<td class="text-right" style="border-right: 1px solid black; background-color: grey;"> |
||||
|
<span t-esc="line['balance']" t-esc-options='{"widget": "monetary", "display_currency": "currency"}'/> |
||||
|
</td> |
||||
|
</t> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<td style="border-right: 1px solid black;"></td> |
||||
|
<td style="border-right: 1px solid black;"> |
||||
|
<span t-esc="Date_end[o.id]"/> |
||||
|
</td> |
||||
|
<td style="border-right: 1px solid black;"></td> |
||||
|
<td style="border-right: 1px solid black;"> |
||||
|
Ending Balance |
||||
|
</td> |
||||
|
<td style="border-right: 1px solid black;"></td> |
||||
|
<td style="border-right: 1px solid black;"></td> |
||||
|
<td class="text-right" style="border-right: 1px solid black;"> |
||||
|
<span t-esc="Amount_Due[o.id][currency]" t-esc-options='{"widget": "monetary", "display_currency": "currency"}'/> |
||||
|
</td> |
||||
|
</tr> |
||||
|
</table> |
||||
|
<table class="table table-condensed" t-if="Show_Buckets" style="border: 1px solid black; border-collapse: collapse;"> |
||||
|
<thead> |
||||
|
<tr> |
||||
|
<th class="text-center" style="border-right: 1px solid black;">Current Due</th> |
||||
|
<th class="text-center" style="border-right: 1px solid black;">1-30 Days Due</th> |
||||
|
<th class="text-center" style="border-right: 1px solid black;">30-60 Days Due</th> |
||||
|
<th class="text-center" style="border-right: 1px solid black;">60-90 Days Due</th> |
||||
|
<th class="text-center" style="border-right: 1px solid black;">90-120 Days Due</th> |
||||
|
<th class="text-center" style="border-right: 1px solid black;">+120 Days Due</th> |
||||
|
<th class="text-right" style="border-right: 1px solid black;">Balance Due</th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tr t-if="currency in Buckets[o.id]"> |
||||
|
<td class="text-right" style="border-right: 1px solid black;"> |
||||
|
<span t-esc="Buckets[o.id][currency]['current']" t-esc-options='{"widget": "monetary", "display_currency": "currency"}'/> |
||||
|
</td> |
||||
|
<td class="text-right" style="border-right: 1px solid black;"> |
||||
|
<span t-esc="Buckets[o.id][currency]['b_1_30']" t-esc-options='{"widget": "monetary", "display_currency": "currency"}'/> |
||||
|
</td> |
||||
|
<td class="text-right" style="border-right: 1px solid black;"> |
||||
|
<span t-esc="Buckets[o.id][currency]['b_30_60']" t-esc-options='{"widget": "monetary", "display_currency": "currency"}'/> |
||||
|
</td> |
||||
|
<td class="text-right" style="border-right: 1px solid black;"> |
||||
|
<span t-esc="Buckets[o.id][currency]['b_60_90']" t-esc-options='{"widget": "monetary", "display_currency": "currency"}'/> |
||||
|
</td> |
||||
|
<td class="text-right" style="border-right: 1px solid black;"> |
||||
|
<span t-esc="Buckets[o.id][currency]['b_90_120']" t-esc-options='{"widget": "monetary", "display_currency": "currency"}'/> |
||||
|
</td> |
||||
|
<td class="text-right" style="border-right: 1px solid black;"> |
||||
|
<span t-esc="Buckets[o.id][currency]['b_over_120']" t-esc-options='{"widget": "monetary", "display_currency": "currency"}'/> |
||||
|
</td> |
||||
|
<td class="text-right" style="border-right: 1px solid black;"> |
||||
|
<span t-esc="Buckets[o.id][currency]['balance']" t-esc-options='{"widget": "monetary", "display_currency": "currency"}'/> |
||||
|
</td> |
||||
|
</tr> |
||||
|
<tr t-if="currency not in Buckets[o.id]"> |
||||
|
<td class="text-right" style="border-right: 1px solid black;"> |
||||
|
<span t-esc="0.0" t-esc-options='{"widget": "monetary", "display_currency": "currency"}'/> |
||||
|
</td> |
||||
|
<td class="text-right" style="border-right: 1px solid black;"> |
||||
|
<span t-esc="0.0" t-esc-options='{"widget": "monetary", "display_currency": "currency"}'/> |
||||
|
</td> |
||||
|
<td class="text-right" style="border-right: 1px solid black;"> |
||||
|
<span t-esc="0.0" t-esc-options='{"widget": "monetary", "display_currency": "currency"}'/> |
||||
|
</td> |
||||
|
<td class="text-right" style="border-right: 1px solid black;"> |
||||
|
<span t-esc="0.0" t-esc-options='{"widget": "monetary", "display_currency": "currency"}'/> |
||||
|
</td> |
||||
|
<td class="text-right" style="border-right: 1px solid black;"> |
||||
|
<span t-esc="0.0" t-esc-options='{"widget": "monetary", "display_currency": "currency"}'/> |
||||
|
</td> |
||||
|
<td class="text-right" style="border-right: 1px solid black;"> |
||||
|
<span t-esc="0.0" t-esc-options='{"widget": "monetary", "display_currency": "currency"}'/> |
||||
|
</td> |
||||
|
<td class="text-right" style="border-right: 1px solid black;"> |
||||
|
<span t-esc="0.0" t-esc-options='{"widget": "monetary", "display_currency": "currency"}'/> |
||||
|
</td> |
||||
|
</tr> |
||||
|
</table> |
||||
|
</t> |
||||
|
</t> |
||||
|
<p t-if="not Lines[o.id]"> |
||||
|
<strong>The partner doesn't have due entries.</strong> |
||||
|
</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</t> |
||||
|
</template> |
||||
|
|
||||
|
<template id="statement"> |
||||
|
<t t-call="report.html_container"> |
||||
|
<t t-foreach="docs" t-as="o"> |
||||
|
<t t-if="not (Filter_non_due_partners and (not Lines[o.id]) and (len(doc_ids) > 1))"> |
||||
|
<t t-call="customer_outstanding_statement.statement_document" t-lang="o.lang"/> |
||||
|
</t> |
||||
|
</t> |
||||
|
</t> |
||||
|
</template> |
||||
|
|
||||
|
<report id="action_print_customer_outstanding_statement" |
||||
|
model="res.partner" |
||||
|
report_type="qweb-pdf" |
||||
|
menu="False" |
||||
|
string="Statement Action to PDF" |
||||
|
name="customer_outstanding_statement.statement" |
||||
|
file="customer_outstanding_statement.statement" |
||||
|
/> |
||||
|
</odoo> |
@ -0,0 +1,6 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Copyright 2017 Eficent Business and IT Consulting Services S.L. |
||||
|
# (http://www.eficent.com) |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
|
from . import customer_outstanding_statement_wizard |
@ -0,0 +1,51 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Copyright 2017 Eficent Business and IT Consulting Services S.L. |
||||
|
# (http://www.eficent.com) |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
|
from datetime import date |
||||
|
from openerp import api, fields, models |
||||
|
|
||||
|
|
||||
|
class CustomerOutstandingStatementWizard(models.TransientModel): |
||||
|
"""Customer Outstanding Statement wizard.""" |
||||
|
|
||||
|
_name = 'customer.outstanding.statement.wizard' |
||||
|
_description = 'Customer Outstanding Statement Wizard' |
||||
|
|
||||
|
company_id = fields.Many2one( |
||||
|
comodel_name='res.company', |
||||
|
default=lambda self: self.env.user.company_id, |
||||
|
string='Company' |
||||
|
) |
||||
|
|
||||
|
date_end = fields.Date(required=True, |
||||
|
default=fields.Date.to_string(date.today())) |
||||
|
show_aging_buckets = fields.Boolean(string='Include Aging Buckets', |
||||
|
default=True) |
||||
|
number_partner_ids = fields.Integer( |
||||
|
default=lambda self: len(self._context['active_ids']) |
||||
|
) |
||||
|
filter_partners_non_due = fields.Boolean( |
||||
|
string='Don\'t show partners with no due entries', default=True) |
||||
|
|
||||
|
@api.multi |
||||
|
def button_export_pdf(self): |
||||
|
self.ensure_one() |
||||
|
return self._export() |
||||
|
|
||||
|
def _prepare_outstanding_statement(self): |
||||
|
self.ensure_one() |
||||
|
return { |
||||
|
'date_end': self.date_end, |
||||
|
'company_id': self.company_id.id, |
||||
|
'partner_ids': self._context['active_ids'], |
||||
|
'show_aging_buckets': self.show_aging_buckets, |
||||
|
'filter_non_due_partners': self.filter_partners_non_due, |
||||
|
} |
||||
|
|
||||
|
def _export(self): |
||||
|
"""Export to PDF.""" |
||||
|
data = self._prepare_outstanding_statement() |
||||
|
return self.env['report'].with_context(landscape=True).get_action( |
||||
|
self, 'customer_outstanding_statement.statement', data=data) |
@ -0,0 +1,49 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<openerp> |
||||
|
<data> |
||||
|
<!-- wizard action on res.partner --> |
||||
|
<act_window id="customer_outstanding_statement_wizard_action" |
||||
|
name="Customer Outstanding Statement" |
||||
|
src_model="res.partner" |
||||
|
res_model="customer.outstanding.statement.wizard" |
||||
|
view_type="form" view_mode="form" |
||||
|
key2="client_action_multi" target="new" |
||||
|
groups="account.group_account_user"/> |
||||
|
|
||||
|
<!-- wizard view --> |
||||
|
<record id="customer_outstanding_statement_wizard_view" model="ir.ui.view"> |
||||
|
<field name="name">Customer Outstanding Statement Wizard</field> |
||||
|
<field name="model">customer.outstanding.statement.wizard</field> |
||||
|
<field name="arch" type="xml"> |
||||
|
<form name="Report Options"> |
||||
|
<div style="text-align:justify"> |
||||
|
<label string="The outstanding statement provides details of all outstanding |
||||
|
customer receivables up to a particular date. This includes all unpaid invoices, unclaimed |
||||
|
refunds and outstanding payments. The list is displayed in chronological order and is |
||||
|
split by currencies."/><br/><br/> |
||||
|
<label string="Aging details can be shown in the report, expressed in aging |
||||
|
buckets (30 days due, ...), so the customer can review how much is open, due or overdue."/> |
||||
|
</div><hr/> |
||||
|
<group name="main_info"> |
||||
|
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/> |
||||
|
</group> |
||||
|
<group name="dates"> |
||||
|
<field name="date_end"/> |
||||
|
</group> |
||||
|
<group name="aging_report"> |
||||
|
<field name="show_aging_buckets"/> |
||||
|
</group> |
||||
|
<group name="multiple_partners"> |
||||
|
<field name="number_partner_ids" readonly="1" invisible="1"/> |
||||
|
<field name="filter_partners_non_due" attrs="{'invisible': [('number_partner_ids', '=', 1)]}"/> |
||||
|
</group> |
||||
|
<footer> |
||||
|
<button name="button_export_pdf" string="Export PDF" type="object" default_focus="1" class="oe_highlight"/> |
||||
|
or |
||||
|
<button string="Cancel" class="oe_link" special="cancel" /> |
||||
|
</footer> |
||||
|
</form> |
||||
|
</field> |
||||
|
</record> |
||||
|
</data> |
||||
|
</openerp> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue