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..a5cc926e --- /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': 'Reports/pdf', + 'summary': 'OCA Financial Reports', + 'author': "Eficent, Odoo Community Association (OCA)", + 'website': 'https://github.com/OCA/account-financial-reporting', + 'license': 'AGPL-3', + 'depends': [ + 'account', + ], + 'data': [ + 'views/statement.xml', + 'wizard/customer_outstanding_statement_wizard.xml', + ], + 'installable': True, + 'application': False, +} 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..8d23117f --- /dev/null +++ b/customer_outstanding_statement/report/customer_outstanding_statement.py @@ -0,0 +1,315 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Eficent Business and IT Consulting Services S.L. +# (http://www.eficent.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from datetime import datetime, timedelta +from openerp.tools import DEFAULT_SERVER_DATE_FORMAT +from openerp import api, fields, models + + +class CustomerOutstandingStatement(models.AbstractModel): + """Model of Customer Outstanding Statement""" + + _name = 'report.customer_outstanding_statement.statement' + + def _format_date_to_partner_lang(self, str_date, partner_id): + lang_code = self.env['res.partner'].browse(partner_id).lang + lang_id = self.env['res.lang']._lang_get(lang_code) + lang = self.env['res.lang'].browse(lang_id) + date = datetime.strptime(str_date, DEFAULT_SERVER_DATE_FORMAT).date() + return date.strftime(lang.date_format) + + def _display_lines_sql_q1(self, partners, date_end): + return """ + SELECT m.name as move_id, l.partner_id, l.date, l.name, + l.ref, l.blocked, l.currency_id, l.company_id, + CASE WHEN (l.currency_id is not null AND l.amount_currency > 0.0) + THEN sum(l.amount_currency) + ELSE sum(l.debit) + END as debit, + CASE WHEN (l.currency_id is not null AND l.amount_currency < 0.0) + THEN sum(l.amount_currency * (-1)) + ELSE sum(l.credit) + END as credit, + CASE WHEN l.balance > 0.0 + THEN l.balance - sum(coalesce(pd.amount, 0.0)) + ELSE l.balance + sum(coalesce(pc.amount, 0.0)) + END AS open_amount, + CASE WHEN l.balance > 0.0 + THEN l.amount_currency - sum(coalesce(pd.amount_currency, 0.0)) + ELSE l.amount_currency + sum(coalesce(pc.amount_currency, 0.0)) + END AS open_amount_currency, + CASE WHEN l.date_maturity is null + THEN l.date + ELSE l.date_maturity + END as date_maturity + FROM account_move_line l + JOIN account_account_type at ON (at.id = l.user_type_id) + JOIN account_move m ON (l.move_id = m.id) + LEFT JOIN (SELECT pr.* + FROM account_partial_reconcile pr + INNER JOIN account_move_line l2 + ON pr.credit_move_id = l2.id + WHERE l2.date <= '%s' + ) as pd ON pd.debit_move_id = l.id + LEFT JOIN (SELECT pr.* + FROM account_partial_reconcile pr + INNER JOIN account_move_line l2 + ON pr.debit_move_id = l2.id + WHERE l2.date <= '%s' + ) as pc ON pc.credit_move_id = l.id + WHERE l.partner_id IN (%s) AND at.type = 'receivable' + AND not l.reconciled AND l.date <= '%s' + GROUP BY l.partner_id, m.name, l.date, l.date_maturity, l.name, + l.ref, l.blocked, l.currency_id, + l.balance, l.amount_currency, l.company_id + """ % (date_end, date_end, partners, date_end) + + def _display_lines_sql_q2(self): + return """ + SELECT partner_id, currency_id, move_id, date, date_maturity, + debit, credit, name, ref, blocked, company_id, + CASE WHEN currency_id is not null + THEN open_amount_currency + ELSE open_amount + END as open_amount + FROM Q1 + """ + + def _display_lines_sql_q3(self, company_id): + return """ + SELECT Q2.partner_id, move_id, date, date_maturity, Q2.name, ref, + debit, credit, debit-credit AS amount, blocked, + COALESCE(Q2.currency_id, c.currency_id) AS currency_id, open_amount + FROM Q2 + JOIN res_company c ON (c.id = Q2.company_id) + WHERE c.id = %s + """ % company_id + + def _get_account_display_lines(self, company_id, partner_ids, date_end): + res = dict(map(lambda x: (x, []), partner_ids)) + partners = ', '.join([str(i) for i in partner_ids]) + date_end = datetime.strptime( + date_end, DEFAULT_SERVER_DATE_FORMAT).date() + self.env.cr.execute("""WITH Q1 AS (%s), Q2 AS (%s), Q3 AS (%s) + SELECT partner_id, currency_id, move_id, date, date_maturity, debit, + credit, amount, open_amount, name, ref, blocked + FROM Q3 + ORDER BY date, date_maturity, move_id""" % ( + self._display_lines_sql_q1(partners, date_end), + self._display_lines_sql_q2(), + self._display_lines_sql_q3(company_id))) + for row in self.env.cr.dictfetchall(): + res[row.pop('partner_id')].append(row) + return res + + def _show_buckets_sql_q1(self, partners, date_end): + return """ + SELECT l.partner_id, l.currency_id, l.company_id, l.move_id, + l.balance - sum(coalesce(pr.amount, 0.0)) as open_due, + l.amount_currency - sum(coalesce(pr.amount_currency, 0.0)) + AS open_due_currency, + CASE WHEN l.date_maturity is null + THEN l.date + ELSE l.date_maturity + END as date_maturity + FROM account_move_line l + JOIN account_account_type at ON (at.id = l.user_type_id) + JOIN account_move m ON (l.move_id = m.id) + LEFT JOIN ( + SELECT pr.* + FROM account_partial_reconcile pr + INNER JOIN account_move_line l2 + ON pr.credit_move_id = l2.id + WHERE l2.date <= '%s' + ) as pr + ON pr.debit_move_id = l.id + WHERE l.partner_id IN (%s) AND at.type = 'receivable' + AND not l.reconciled AND not l.blocked + AND l.balance > 0.0 + GROUP BY l.partner_id, l.currency_id, l.date, l.date_maturity, + l.amount_currency, l.balance, l.move_id, + l.company_id + """ % (date_end, partners) + + def _show_buckets_sql_q2(self, today, minus_30, minus_60, minus_90, + minus_120): + return """ + SELECT partner_id, currency_id, date_maturity, open_due, + open_due_currency, move_id, company_id, + CASE + WHEN '%s' <= date_maturity AND currency_id is null + THEN open_due + WHEN '%s' <= date_maturity AND currency_id is not null + THEN open_due_currency + ELSE 0.0 + END as current, + CASE + WHEN '%s' < date_maturity AND date_maturity < '%s' + AND currency_id is null THEN open_due + WHEN '%s' < date_maturity AND date_maturity < '%s' + AND currency_id is not null + THEN open_due_currency + ELSE 0.0 + END as b_1_30, + CASE + WHEN '%s' < date_maturity AND date_maturity <= '%s' + AND currency_id is null THEN open_due + WHEN '%s' < date_maturity AND date_maturity <= '%s' + AND currency_id is not null + THEN open_due_currency + ELSE 0.0 + END as b_30_60, + CASE + WHEN '%s' < date_maturity AND date_maturity <= '%s' + AND currency_id is null THEN open_due + WHEN '%s' < date_maturity AND date_maturity <= '%s' + AND currency_id is not null + THEN open_due_currency + ELSE 0.0 + END as b_60_90, + CASE + WHEN '%s' < date_maturity AND date_maturity <= '%s' + AND currency_id is null THEN open_due + WHEN '%s' < date_maturity AND date_maturity <= '%s' + AND currency_id is not null + THEN open_due_currency + ELSE 0.0 + END as b_90_120, + CASE + WHEN date_maturity <= '%s' AND currency_id is null + THEN open_due + WHEN date_maturity <= '%s' AND currency_id is not null + THEN open_due_currency + ELSE 0.0 + END as b_over_120 + FROM Q1 + GROUP BY partner_id, currency_id, date_maturity, open_due, + open_due_currency, move_id, company_id + """ % (today, today, minus_30, today, minus_30, today, minus_60, + minus_30, minus_60, minus_30, minus_90, minus_60, minus_90, + minus_60, minus_120, minus_90, minus_120, minus_90, minus_120, + minus_120) + + def _show_buckets_sql_q3(self, company_id): + return """ + SELECT Q2.partner_id, current, b_1_30, b_30_60, b_60_90, b_90_120, + b_over_120, + COALESCE(Q2.currency_id, c.currency_id) AS currency_id + FROM Q2 + JOIN res_company c ON (c.id = Q2.company_id) + WHERE c.id = %s + """ % company_id + + def _show_buckets_sql_q4(self): + return """ + SELECT partner_id, currency_id, sum(current) as current, + sum(b_1_30) as b_1_30, + sum(b_30_60) as b_30_60, + sum(b_60_90) as b_60_90, + sum(b_90_120) as b_90_120, + sum(b_over_120) as b_over_120 + FROM Q3 + GROUP BY partner_id, currency_id + """ + + _bucket_dates = { + 'today': fields.date.today(), + 'minus_30': fields.date.today() - timedelta(days=30), + 'minus_60': fields.date.today() - timedelta(days=60), + 'minus_90': fields.date.today() - timedelta(days=90), + 'minus_120': fields.date.today() - timedelta(days=120), + } + + def _get_account_show_buckets(self, company_id, partner_ids, date_end): + res = dict(map(lambda x: (x, []), partner_ids)) + partners = ', '.join([str(i) for i in partner_ids]) + date_end = datetime.strptime( + date_end, DEFAULT_SERVER_DATE_FORMAT).date() + self.env.cr.execute("""WITH Q1 AS (%s), Q2 AS (%s), + Q3 AS (%s), Q4 AS (%s) + SELECT partner_id, currency_id, current, b_1_30, b_30_60, b_60_90, + b_90_120, b_over_120, + current+b_1_30+b_30_60+b_60_90+b_90_120+b_over_120 + AS balance + FROM Q4 + GROUP BY partner_id, currency_id, current, b_1_30, b_30_60, b_60_90, + b_90_120, b_over_120""" % ( + self._show_buckets_sql_q1(partners, date_end), + self._show_buckets_sql_q2( + self._bucket_dates['today'], + self._bucket_dates['minus_30'], + self._bucket_dates['minus_60'], + self._bucket_dates['minus_90'], + self._bucket_dates['minus_120']), + self._show_buckets_sql_q3(company_id), + self._show_buckets_sql_q4())) + for row in self.env.cr.dictfetchall(): + res[row.pop('partner_id')].append(row) + return res + + @api.multi + def render_html(self, data): + company_id = data['company_id'] + partner_ids = data['partner_ids'] + date_end = data['date_end'] + today = fields.Date.today() + + buckets_to_display = {} + lines_to_display, amount_due = {}, {} + currency_to_display = {} + today_display, date_end_display = {}, {} + + lines = self._get_account_display_lines( + company_id, partner_ids, date_end) + + for partner_id in partner_ids: + lines_to_display[partner_id], amount_due[partner_id] = {}, {} + currency_to_display[partner_id] = {} + today_display[partner_id] = self._format_date_to_partner_lang( + today, partner_id) + date_end_display[partner_id] = self._format_date_to_partner_lang( + date_end, partner_id) + for line in lines[partner_id]: + currency = self.env['res.currency'].browse(line['currency_id']) + if currency not in lines_to_display[partner_id]: + lines_to_display[partner_id][currency] = [] + currency_to_display[partner_id][currency] = currency + amount_due[partner_id][currency] = 0.0 + if not line['blocked']: + amount_due[partner_id][currency] += line['open_amount'] + line['balance'] = amount_due[partner_id][currency] + line['date'] = self._format_date_to_partner_lang( + line['date'], partner_id) + line['date_maturity'] = self._format_date_to_partner_lang( + line['date_maturity'], partner_id) + lines_to_display[partner_id][currency].append(line) + + if data['show_aging_buckets']: + buckets = self._get_account_show_buckets( + company_id, partner_ids, date_end) + for partner_id in partner_ids: + buckets_to_display[partner_id] = {} + for line in buckets[partner_id]: + currency = self.env['res.currency'].browse( + line['currency_id']) + if currency not in buckets_to_display[partner_id]: + buckets_to_display[partner_id][currency] = [] + buckets_to_display[partner_id][currency] = line + + docargs = { + 'doc_ids': partner_ids, + 'doc_model': 'res.partner', + 'docs': self.env['res.partner'].browse(partner_ids), + 'Amount_Due': amount_due, + 'Lines': lines_to_display, + 'Buckets': buckets_to_display, + 'Currencies': currency_to_display, + 'Show_Buckets': data['show_aging_buckets'], + 'Filter_non_due_partners': data['filter_non_due_partners'], + 'Date_end': date_end_display, + 'Date': today_display, + } + return self.env['report'].render( + 'customer_outstanding_statement.statement', values=docargs) 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 00000000..eeba6829 Binary files /dev/null and b/customer_outstanding_statement/static/description/Outstanding_Statement.png differ diff --git a/customer_outstanding_statement/static/description/icon.png b/customer_outstanding_statement/static/description/icon.png new file mode 100644 index 00000000..3a0328b5 Binary files /dev/null and b/customer_outstanding_statement/static/description/icon.png differ 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 + +
+
+

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