From 9d7825495633d8ce6a2ece742b073707711074f1 Mon Sep 17 00:00:00 2001 From: mreficent Date: Thu, 23 Mar 2017 15:31:18 +0100 Subject: [PATCH] Customer Activity Statement --- customer_activity_statement/README.rst | 74 ++++ customer_activity_statement/__init__.py | 7 + customer_activity_statement/__openerp__.py | 23 ++ .../report/__init__.py | 6 + .../report/customer_activity_statement.py | 358 ++++++++++++++++++ .../static/description/Activity_Statement.png | Bin 0 -> 35033 bytes .../static/description/icon.png | Bin 0 -> 9455 bytes .../static/description/index.html | 77 ++++ customer_activity_statement/tests/__init__.py | 6 + .../tests/test_customer_activity_statement.py | 67 ++++ .../views/statement.xml | 204 ++++++++++ .../wizard/__init__.py | 6 + .../customer_activity_statement_wizard.py | 55 +++ .../customer_activity_statement_wizard.xml | 51 +++ 14 files changed, 934 insertions(+) create mode 100644 customer_activity_statement/README.rst create mode 100644 customer_activity_statement/__init__.py create mode 100644 customer_activity_statement/__openerp__.py create mode 100644 customer_activity_statement/report/__init__.py create mode 100644 customer_activity_statement/report/customer_activity_statement.py create mode 100644 customer_activity_statement/static/description/Activity_Statement.png create mode 100644 customer_activity_statement/static/description/icon.png create mode 100644 customer_activity_statement/static/description/index.html create mode 100644 customer_activity_statement/tests/__init__.py create mode 100644 customer_activity_statement/tests/test_customer_activity_statement.py create mode 100644 customer_activity_statement/views/statement.xml create mode 100644 customer_activity_statement/wizard/__init__.py create mode 100644 customer_activity_statement/wizard/customer_activity_statement_wizard.py create mode 100644 customer_activity_statement/wizard/customer_activity_statement_wizard.xml diff --git a/customer_activity_statement/README.rst b/customer_activity_statement/README.rst new file mode 100644 index 00000000..23d318ef --- /dev/null +++ b/customer_activity_statement/README.rst @@ -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 +`_. 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_activity_statement/__init__.py b/customer_activity_statement/__init__.py new file mode 100644 index 00000000..7e6f294d --- /dev/null +++ b/customer_activity_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_activity_statement/__openerp__.py b/customer_activity_statement/__openerp__.py new file mode 100644 index 00000000..6e8479c2 --- /dev/null +++ b/customer_activity_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 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, +} diff --git a/customer_activity_statement/report/__init__.py b/customer_activity_statement/report/__init__.py new file mode 100644 index 00000000..8f6b4ad9 --- /dev/null +++ b/customer_activity_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_activity_statement diff --git a/customer_activity_statement/report/customer_activity_statement.py b/customer_activity_statement/report/customer_activity_statement.py new file mode 100644 index 00000000..9b2ec147 --- /dev/null +++ b/customer_activity_statement/report/customer_activity_statement.py @@ -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) diff --git a/customer_activity_statement/static/description/Activity_Statement.png b/customer_activity_statement/static/description/Activity_Statement.png new file mode 100644 index 0000000000000000000000000000000000000000..e730c3a7a58bf9a5c8f7ab24066085d84b48a2bc GIT binary patch literal 35033 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

KLNAFO0a-q~glk2u0M9DN5(_)#IX;HSqFH12Lq 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/customer_activity_statement/static/description/index.html b/customer_activity_statement/static/description/index.html new file mode 100644 index 00000000..9d218b04 --- /dev/null +++ b/customer_activity_statement/static/description/index.html @@ -0,0 +1,77 @@ +

+
+
+

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

+
+
+

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 Activity 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_activity_statement/tests/__init__.py b/customer_activity_statement/tests/__init__.py new file mode 100644 index 00000000..1b672b5e --- /dev/null +++ b/customer_activity_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_activity_statement diff --git a/customer_activity_statement/tests/test_customer_activity_statement.py b/customer_activity_statement/tests/test_customer_activity_statement.py new file mode 100644 index 00000000..700bbbdc --- /dev/null +++ b/customer_activity_statement/tests/test_customer_activity_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 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("", report, + "There was an error while compiling the report.") diff --git a/customer_activity_statement/views/statement.xml b/customer_activity_statement/views/statement.xml new file mode 100644 index 00000000..1701cf4c --- /dev/null +++ b/customer_activity_statement/views/statement.xml @@ -0,0 +1,204 @@ + + + + + + + + diff --git a/customer_activity_statement/wizard/__init__.py b/customer_activity_statement/wizard/__init__.py new file mode 100644 index 00000000..8f76a2a2 --- /dev/null +++ b/customer_activity_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_activity_statement_wizard diff --git a/customer_activity_statement/wizard/customer_activity_statement_wizard.py b/customer_activity_statement/wizard/customer_activity_statement_wizard.py new file mode 100644 index 00000000..a2a48c98 --- /dev/null +++ b/customer_activity_statement/wizard/customer_activity_statement_wizard.py @@ -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) diff --git a/customer_activity_statement/wizard/customer_activity_statement_wizard.xml b/customer_activity_statement/wizard/customer_activity_statement_wizard.xml new file mode 100644 index 00000000..8e381221 --- /dev/null +++ b/customer_activity_statement/wizard/customer_activity_statement_wizard.xml @@ -0,0 +1,51 @@ + + + + + + + + + Customer Activity Statement Wizard + customer.activity.statement.wizard + +
+
+

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