mreficent
8 years ago
14 changed files with 934 additions and 0 deletions
-
74customer_activity_statement/README.rst
-
7customer_activity_statement/__init__.py
-
23customer_activity_statement/__openerp__.py
-
6customer_activity_statement/report/__init__.py
-
358customer_activity_statement/report/customer_activity_statement.py
-
BINcustomer_activity_statement/static/description/Activity_Statement.png
-
BINcustomer_activity_statement/static/description/icon.png
-
77customer_activity_statement/static/description/index.html
-
6customer_activity_statement/tests/__init__.py
-
67customer_activity_statement/tests/test_customer_activity_statement.py
-
204customer_activity_statement/views/statement.xml
-
6customer_activity_statement/wizard/__init__.py
-
55customer_activity_statement/wizard/customer_activity_statement_wizard.py
-
51customer_activity_statement/wizard/customer_activity_statement_wizard.xml
@ -0,0 +1,74 @@ |
|||
.. 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 Activity Statement |
|||
================================= |
|||
|
|||
The activity statement provides details of all activity on the customer receivables |
|||
between two selected dates. This includes all invoices, refunds and payments. |
|||
Any outstanding balance dated prior to the chosen statement period will appear |
|||
as a forward balance at the top of the statement. 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 Activity 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 Activity Statement', |
|||
'version': '9.0.1.0.0', |
|||
'category': 'Accounting & Finance', |
|||
'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_activity_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_activity_statement |
@ -0,0 +1,358 @@ |
|||
# -*- 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 CustomerActivityStatement(models.AbstractModel): |
|||
"""Model of Customer Activity Statement""" |
|||
|
|||
_name = 'report.customer_activity_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 _initial_balance_sql_q1(self, partners, date_start): |
|||
return """ |
|||
SELECT l.partner_id, 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 |
|||
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) |
|||
WHERE l.partner_id IN (%s) AND at.type = 'receivable' |
|||
AND l.date <= '%s' AND not l.blocked |
|||
GROUP BY l.partner_id, l.currency_id, l.amount_currency, |
|||
l.company_id |
|||
""" % (partners, date_start) |
|||
|
|||
def _initial_balance_sql_q2(self, company_id): |
|||
return """ |
|||
SELECT Q1.partner_id, debit-credit AS balance, |
|||
COALESCE(Q1.currency_id, c.currency_id) AS currency_id |
|||
FROM Q1 |
|||
JOIN res_company c ON (c.id = Q1.company_id) |
|||
WHERE c.id = %s |
|||
""" % company_id |
|||
|
|||
def _get_account_initial_balance(self, company_id, partner_ids, |
|||
date_start): |
|||
res = dict(map(lambda x: (x, []), partner_ids)) |
|||
partners = ', '.join([str(i) for i in partner_ids]) |
|||
date_start = datetime.strptime( |
|||
date_start, DEFAULT_SERVER_DATE_FORMAT).date() |
|||
self.env.cr.execute("""WITH Q1 AS (%s), Q2 AS (%s) |
|||
SELECT partner_id, currency_id, balance |
|||
FROM Q2""" % (self._initial_balance_sql_q1(partners, date_start), |
|||
self._initial_balance_sql_q2(company_id))) |
|||
for row in self.env.cr.dictfetchall(): |
|||
res[row.pop('partner_id')].append(row) |
|||
return res |
|||
|
|||
def _display_lines_sql_q1(self, partners, date_start, 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.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) |
|||
WHERE l.partner_id IN (%s) AND at.type = 'receivable' |
|||
AND '%s' < l.date 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.amount_currency, l.company_id |
|||
""" % (partners, date_start, date_end) |
|||
|
|||
def _display_lines_sql_q2(self, company_id): |
|||
return """ |
|||
SELECT Q1.partner_id, move_id, date, date_maturity, Q1.name, ref, |
|||
debit, credit, debit-credit as amount, blocked, |
|||
COALESCE(Q1.currency_id, c.currency_id) AS currency_id |
|||
FROM Q1 |
|||
JOIN res_company c ON (c.id = Q1.company_id) |
|||
WHERE c.id = %s |
|||
""" % company_id |
|||
|
|||
def _get_account_display_lines(self, company_id, partner_ids, date_start, |
|||
date_end): |
|||
res = dict(map(lambda x: (x, []), partner_ids)) |
|||
partners = ', '.join([str(i) for i in partner_ids]) |
|||
date_start = datetime.strptime( |
|||
date_start, DEFAULT_SERVER_DATE_FORMAT).date() |
|||
date_end = datetime.strptime( |
|||
date_end, DEFAULT_SERVER_DATE_FORMAT).date() |
|||
self.env.cr.execute("""WITH Q1 AS (%s), Q2 AS (%s) |
|||
SELECT partner_id, move_id, date, date_maturity, name, ref, debit, |
|||
credit, amount, blocked, currency_id |
|||
FROM Q2 |
|||
ORDER BY date, date_maturity, move_id""" % ( |
|||
self._display_lines_sql_q1(partners, date_start, date_end), |
|||
self._display_lines_sql_q2(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, |
|||
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_due, |
|||
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_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 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 not l.blocked |
|||
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, 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_start = data['date_start'] |
|||
date_end = data['date_end'] |
|||
today = fields.Date.today() |
|||
|
|||
balance_start_to_display, buckets_to_display = {}, {} |
|||
lines_to_display, amount_due = {}, {} |
|||
currency_to_display = {} |
|||
today_display, date_start_display, date_end_display = {}, {}, {} |
|||
|
|||
balance_start = self._get_account_initial_balance( |
|||
company_id, partner_ids, date_start) |
|||
|
|||
for partner_id in partner_ids: |
|||
balance_start_to_display[partner_id] = {} |
|||
for line in balance_start[partner_id]: |
|||
currency = self.env['res.currency'].browse(line['currency_id']) |
|||
if currency not in balance_start_to_display[partner_id]: |
|||
balance_start_to_display[partner_id][currency] = [] |
|||
balance_start_to_display[partner_id][currency] = \ |
|||
line['balance'] |
|||
|
|||
lines = self._get_account_display_lines( |
|||
company_id, partner_ids, date_start, 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_start_display[partner_id] = self._format_date_to_partner_lang( |
|||
date_start, 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 |
|||
if currency in balance_start_to_display[partner_id]: |
|||
amount_due[partner_id][currency] = \ |
|||
balance_start_to_display[partner_id][currency] |
|||
else: |
|||
amount_due[partner_id][currency] = 0.0 |
|||
if not line['blocked']: |
|||
amount_due[partner_id][currency] += line['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, |
|||
'Balance_forward': balance_start_to_display, |
|||
'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_start': date_start_display, |
|||
'Date_end': date_end_display, |
|||
'Date': today_display, |
|||
} |
|||
return self.env['report'].render( |
|||
'customer_activity_statement.statement', values=docargs) |
After Width: 662 | Height: 514 | Size: 34 KiB |
After Width: 128 | Height: 128 | Size: 9.2 KiB |
@ -0,0 +1,77 @@ |
|||
<section class="oe_container"> |
|||
<div class="oe_row oe_spaced"> |
|||
<div class="oe_span12"> |
|||
<h2 class="oe_slogan">Customer Activity Statement</h2> |
|||
</div> |
|||
<div class="oe_span6"> |
|||
<div class="oe_demo oe_picture oe_screenshot"> |
|||
<img src="Activity_Statement.png"> |
|||
</div> |
|||
</div> |
|||
<div class="oe_span6"> |
|||
<p class="oe_mt32"><div style="text-align:justify">The activity statement provides |
|||
details of all activity on the customer receivables between two selected dates. This |
|||
includes all invoices, refunds and payments. Any outstanding balance dated prior to |
|||
the chosen statement period will appear as a forward balance at the top of the statement. |
|||
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 Activity 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_activity_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 TestCustomerActivityStatement(TransactionCase): |
|||
""" |
|||
Tests for Customer Activity Statement. |
|||
""" |
|||
def setUp(self): |
|||
super(TestCustomerActivityStatement, 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_activity_statement.statement'] |
|||
self.wiz = self.env['customer.activity.statement.wizard'] |
|||
self.report_name = 'customer_activity_statement.statement' |
|||
self.report_title = 'Customer Activity 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_activity_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_activity_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,204 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<odoo> |
|||
<template id="customer_activity_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> |
|||
<h4 style="padding-left:20em"> |
|||
Activity Statement |
|||
</h4> |
|||
<p> |
|||
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/> |
|||
<t t-foreach="Lines[o.id]" t-as="currency"> |
|||
<br t-if="not currency_first" /> |
|||
<p> |
|||
Activity 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 style="border-right: 1px solid black;">Description</th> |
|||
<th class="text-right" style="border-right: 1px solid black;">Amount</th> |
|||
<th class="text-right" style="border-right: 1px solid black;">Balance</th> |
|||
</tr> |
|||
</thead> |
|||
<tr> |
|||
<td style="border-right: 1px solid black;"></td> |
|||
<td style="border-right: 1px solid black;"> |
|||
<span t-esc="Date_start[o.id]"/> |
|||
</td> |
|||
<td style="border-right: 1px solid black;"> |
|||
Balance Forward |
|||
</td> |
|||
<td style="border-right: 1px solid black;"></td> |
|||
<td class="text-right" t-if="currency in Balance_forward[o.id]" style="border-right: 1px solid black;"> |
|||
<span t-esc="Balance_forward[o.id][currency]" t-esc-options='{"widget": "monetary", "display_currency": "currency"}'/> |
|||
</td> |
|||
<td class="text-right" t-if="currency not in Balance_forward[o.id]" style="border-right: 1px solid black;"> |
|||
<span t-esc="0.0" t-esc-options='{"widget": "monetary", "display_currency": "currency"}'/> |
|||
</td> |
|||
</tr> |
|||
<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;"> |
|||
<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['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;"> |
|||
<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['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;"> |
|||
Ending Balance |
|||
</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_activity_statement.statement_document" t-lang="o.lang"/> |
|||
</t> |
|||
</t> |
|||
</t> |
|||
</template> |
|||
|
|||
<report id="action_print_customer_activity_statement" |
|||
model="res.partner" |
|||
report_type="qweb-pdf" |
|||
menu="False" |
|||
string="Statement Action to PDF" |
|||
name="customer_activity_statement.statement" |
|||
file="customer_activity_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_activity_statement_wizard |
@ -0,0 +1,55 @@ |
|||
# -*- 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, timedelta |
|||
from openerp import api, fields, models |
|||
|
|||
|
|||
class CustomerActivityStatementWizard(models.TransientModel): |
|||
"""Customer Activity Statement wizard.""" |
|||
|
|||
_name = 'customer.activity.statement.wizard' |
|||
_description = 'Customer Activity Statement Wizard' |
|||
|
|||
company_id = fields.Many2one( |
|||
comodel_name='res.company', |
|||
default=lambda self: self.env.user.company_id, |
|||
string='Company' |
|||
) |
|||
|
|||
date_start = fields.Date(required=True, |
|||
default=fields.Date.to_string( |
|||
date.today()-timedelta(days=120))) |
|||
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_activity_statement(self): |
|||
self.ensure_one() |
|||
return { |
|||
'date_start': self.date_start, |
|||
'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_activity_statement() |
|||
return self.env['report'].with_context(landscape=True).get_action( |
|||
self, 'customer_activity_statement.statement', data=data) |
@ -0,0 +1,51 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<openerp> |
|||
<data> |
|||
<!-- wizard action on res.partner --> |
|||
<act_window id="customer_activity_statement_wizard_action" |
|||
name="Customer Activity Statement" |
|||
src_model="res.partner" |
|||
res_model="customer.activity.statement.wizard" |
|||
view_type="form" view_mode="form" |
|||
key2="client_action_multi" target="new" |
|||
groups="account.group_account_user"/> |
|||
|
|||
<!-- wizard view --> |
|||
<record id="customer_activity_statement_wizard_view" model="ir.ui.view"> |
|||
<field name="name">Customer Activity Statement Wizard</field> |
|||
<field name="model">customer.activity.statement.wizard</field> |
|||
<field name="arch" type="xml"> |
|||
<form name="Report Options"> |
|||
<div style="text-align:justify"> |
|||
<label string="The activity statement provides details of all activity on |
|||
the customer receivables between two selected dates. This includes all invoices, |
|||
refunds and payments. Any outstanding balance dated prior to the chosen statement |
|||
period will appear as a forward balance at the top of the statement. 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_start"/> |
|||
<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