Browse Source

Customer Outstanding Statement

pull/324/head
mreficent 7 years ago
parent
commit
78213f0d0e
  1. 72
      customer_outstanding_statement/README.rst
  2. 7
      customer_outstanding_statement/__init__.py
  3. 23
      customer_outstanding_statement/__openerp__.py
  4. 6
      customer_outstanding_statement/report/__init__.py
  5. 323
      customer_outstanding_statement/report/customer_outstanding_statement.py
  6. BIN
      customer_outstanding_statement/static/description/Outstanding_Statement.png
  7. BIN
      customer_outstanding_statement/static/description/icon.png
  8. 75
      customer_outstanding_statement/static/description/index.html
  9. 6
      customer_outstanding_statement/tests/__init__.py
  10. 67
      customer_outstanding_statement/tests/test_customer_outstanding_statement.py
  11. 204
      customer_outstanding_statement/views/statement.xml
  12. 6
      customer_outstanding_statement/wizard/__init__.py
  13. 51
      customer_outstanding_statement/wizard/customer_outstanding_statement_wizard.py
  14. 49
      customer_outstanding_statement/wizard/customer_outstanding_statement_wizard.xml

72
customer_outstanding_statement/README.rst

@ -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.

7
customer_outstanding_statement/__init__.py

@ -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

23
customer_outstanding_statement/__openerp__.py

@ -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': '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_outstanding_statement_wizard.xml',
],
'installable': True,
'application': False,
}

6
customer_outstanding_statement/report/__init__.py

@ -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

323
customer_outstanding_statement/report/customer_outstanding_statement.py

@ -0,0 +1,323 @@
# -*- 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,
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_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)

BIN
customer_outstanding_statement/static/description/Outstanding_Statement.png

After

Width: 660  |  Height: 512  |  Size: 24 KiB

BIN
customer_outstanding_statement/static/description/icon.png

After

Width: 128  |  Height: 128  |  Size: 9.2 KiB

75
customer_outstanding_statement/static/description/index.html

@ -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&iuml;ch &lt;<a href="mailto:miquel.raich@eficent.com">miquel.raich@eficent.com</a>&gt;</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>

6
customer_outstanding_statement/tests/__init__.py

@ -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

67
customer_outstanding_statement/tests/test_customer_outstanding_statement.py

@ -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.")

204
customer_outstanding_statement/views/statement.xml

@ -0,0 +1,204 @@
<?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', '&lt;br&gt;')"/>
<span t-field="o.vat"/>
</div>
<h4 style="padding-left:20em">
Outstanding 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>
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>

6
customer_outstanding_statement/wizard/__init__.py

@ -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

51
customer_outstanding_statement/wizard/customer_outstanding_statement_wizard.py

@ -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)

49
customer_outstanding_statement/wizard/customer_outstanding_statement_wizard.xml

@ -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>
Loading…
Cancel
Save