From 85199370049c355fe6f7694ff6111530ecc5ddc0 Mon Sep 17 00:00:00 2001 From: mreficent Date: Fri, 24 Mar 2017 13:09:46 +0100 Subject: [PATCH] Customer Outstanding Statement --- customer_outstanding_statement/README.rst | 72 ++++ customer_outstanding_statement/__init__.py | 7 + customer_outstanding_statement/__openerp__.py | 23 ++ .../report/__init__.py | 6 + .../report/customer_outstanding_statement.py | 323 ++++++++++++++++++ .../description/Outstanding_Statement.png | Bin 0 -> 25021 bytes .../static/description/icon.png | Bin 0 -> 9455 bytes .../static/description/index.html | 75 ++++ .../tests/__init__.py | 6 + .../test_customer_outstanding_statement.py | 67 ++++ .../views/statement.xml | 204 +++++++++++ .../wizard/__init__.py | 6 + .../customer_outstanding_statement_wizard.py | 51 +++ .../customer_outstanding_statement_wizard.xml | 49 +++ 14 files changed, 889 insertions(+) create mode 100644 customer_outstanding_statement/README.rst create mode 100644 customer_outstanding_statement/__init__.py create mode 100644 customer_outstanding_statement/__openerp__.py create mode 100644 customer_outstanding_statement/report/__init__.py create mode 100644 customer_outstanding_statement/report/customer_outstanding_statement.py create mode 100644 customer_outstanding_statement/static/description/Outstanding_Statement.png create mode 100644 customer_outstanding_statement/static/description/icon.png create mode 100644 customer_outstanding_statement/static/description/index.html create mode 100644 customer_outstanding_statement/tests/__init__.py create mode 100644 customer_outstanding_statement/tests/test_customer_outstanding_statement.py create mode 100644 customer_outstanding_statement/views/statement.xml create mode 100644 customer_outstanding_statement/wizard/__init__.py create mode 100644 customer_outstanding_statement/wizard/customer_outstanding_statement_wizard.py create mode 100644 customer_outstanding_statement/wizard/customer_outstanding_statement_wizard.xml diff --git a/customer_outstanding_statement/README.rst b/customer_outstanding_statement/README.rst new file mode 100644 index 00000000..6ff702eb --- /dev/null +++ b/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 +`_. 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 `_. + +Contributors +------------ + +* Miquel Raïch + +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. diff --git a/customer_outstanding_statement/__init__.py b/customer_outstanding_statement/__init__.py new file mode 100644 index 00000000..7e6f294d --- /dev/null +++ b/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 diff --git a/customer_outstanding_statement/__openerp__.py b/customer_outstanding_statement/__openerp__.py new file mode 100644 index 00000000..3e5aa4fc --- /dev/null +++ b/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, +} diff --git a/customer_outstanding_statement/report/__init__.py b/customer_outstanding_statement/report/__init__.py new file mode 100644 index 00000000..eca82e78 --- /dev/null +++ b/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 diff --git a/customer_outstanding_statement/report/customer_outstanding_statement.py b/customer_outstanding_statement/report/customer_outstanding_statement.py new file mode 100644 index 00000000..fe4ed5ca --- /dev/null +++ b/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) diff --git a/customer_outstanding_statement/static/description/Outstanding_Statement.png b/customer_outstanding_statement/static/description/Outstanding_Statement.png new file mode 100644 index 0000000000000000000000000000000000000000..eeba68298613ed0db87f466a84a3b81447c28f58 GIT binary patch literal 25021 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 z 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 as(;gdWG-*r;V@wBYSewsdwIJ9IR62gRYT_h literal 0 HcmV?d00001 diff --git a/customer_outstanding_statement/static/description/icon.png b/customer_outstanding_statement/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/customer_outstanding_statement/static/description/index.html b/customer_outstanding_statement/static/description/index.html new file mode 100644 index 00000000..3f3faf37 --- /dev/null +++ b/customer_outstanding_statement/static/description/index.html @@ -0,0 +1,75 @@ +
+
+
+

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

+
+
+

To configure this module, you need to: +

    +
  • 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
  • +
+

+
+
+
+ +
+
+
+

Credits

+
+
+

Contributors

+ +
+
+

Maintainer

+

+ 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 http://odoo-community.org.
+ +

+
+
+
\ No newline at end of file diff --git a/customer_outstanding_statement/tests/__init__.py b/customer_outstanding_statement/tests/__init__.py new file mode 100644 index 00000000..8e6a88de --- /dev/null +++ b/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 diff --git a/customer_outstanding_statement/tests/test_customer_outstanding_statement.py b/customer_outstanding_statement/tests/test_customer_outstanding_statement.py new file mode 100644 index 00000000..f961466e --- /dev/null +++ b/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("", report, + "There was an error while compiling the report.") diff --git a/customer_outstanding_statement/views/statement.xml b/customer_outstanding_statement/views/statement.xml new file mode 100644 index 00000000..32a14e51 --- /dev/null +++ b/customer_outstanding_statement/views/statement.xml @@ -0,0 +1,204 @@ + + + + + + + + diff --git a/customer_outstanding_statement/wizard/__init__.py b/customer_outstanding_statement/wizard/__init__.py new file mode 100644 index 00000000..1044a1d8 --- /dev/null +++ b/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 diff --git a/customer_outstanding_statement/wizard/customer_outstanding_statement_wizard.py b/customer_outstanding_statement/wizard/customer_outstanding_statement_wizard.py new file mode 100644 index 00000000..2278ddcb --- /dev/null +++ b/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) diff --git a/customer_outstanding_statement/wizard/customer_outstanding_statement_wizard.xml b/customer_outstanding_statement/wizard/customer_outstanding_statement_wizard.xml new file mode 100644 index 00000000..d64cf601 --- /dev/null +++ b/customer_outstanding_statement/wizard/customer_outstanding_statement_wizard.xml @@ -0,0 +1,49 @@ + + + + + + + + + Customer Outstanding Statement Wizard + customer.outstanding.statement.wizard + +
+
+

+ + + + + + + + + + + + + + +
+
+
+
+