From b9b4f1e67f5f0eb1008b8f342a871fa61d05c381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miquel=20Ra=C3=AFch?= Date: Fri, 16 Dec 2022 10:29:17 +0100 Subject: [PATCH] [IMP] partner_statement: add Detailed Activity report --- partner_statement/__manifest__.py | 2 + partner_statement/readme/DESCRIPTION.rst | 7 +- partner_statement/readme/HISTORY.rst | 5 + partner_statement/report/__init__.py | 2 + .../report/activity_statement.py | 231 +++++-- .../report/activity_statement_xlsx.py | 93 ++- .../report/detailed_activity_statement.py | 43 ++ .../detailed_activity_statement_xlsx.py | 568 ++++++++++++++++++ .../report/outstanding_statement.py | 69 ++- .../report/outstanting_statement_xlsx.py | 76 ++- .../report/report_statement_common.py | 147 ++++- .../security/ir.model.access.csv | 1 + .../static/src/scss/layout_statement.scss | 8 + .../views/activity_statement.xml | 220 +++---- .../views/detailed_activity_statement.xml | 112 ++++ .../views/outstanding_statement.xml | 204 ++++--- partner_statement/wizard/__init__.py | 1 + .../wizard/activity_statement_wizard.py | 7 +- .../detailed_activity_statement_wizard.py | 40 ++ .../wizard/outstanding_statement_wizard.py | 9 + .../wizard/res_config_settings.py | 3 +- partner_statement/wizard/statement_wizard.xml | 34 ++ 22 files changed, 1537 insertions(+), 345 deletions(-) create mode 100644 partner_statement/report/detailed_activity_statement.py create mode 100644 partner_statement/report/detailed_activity_statement_xlsx.py create mode 100644 partner_statement/views/detailed_activity_statement.xml create mode 100644 partner_statement/wizard/detailed_activity_statement_wizard.py diff --git a/partner_statement/__manifest__.py b/partner_statement/__manifest__.py index 048bfb2e..7d5e7fbd 100644 --- a/partner_statement/__manifest__.py +++ b/partner_statement/__manifest__.py @@ -7,6 +7,7 @@ "category": "Accounting & Finance", "summary": "OCA Financial Reports", "author": "ForgeFlow, Odoo Community Association (OCA)", + "maintainers": ["MiquelRForgeFlow"], "website": "https://github.com/OCA/account-financial-reporting", "license": "AGPL-3", "depends": ["account", "report_xlsx", "report_xlsx_helper"], @@ -15,6 +16,7 @@ "security/statement_security.xml", "views/activity_statement.xml", "views/outstanding_statement.xml", + "views/detailed_activity_statement.xml", "views/aging_buckets.xml", "views/res_config_settings.xml", "wizard/statement_wizard.xml", diff --git a/partner_statement/readme/DESCRIPTION.rst b/partner_statement/readme/DESCRIPTION.rst index 007ebf3b..981ca8f1 100644 --- a/partner_statement/readme/DESCRIPTION.rst +++ b/partner_statement/readme/DESCRIPTION.rst @@ -1,6 +1,6 @@ This module extends the functionality of Invoicing to support the printing of customer and vendor statements. -There are two types of statements, Activity and Outstanding. Aging details can be shown in the reports, expressed in aging buckets, -so the customer or vendor can review how much is open, due or overdue. +There are three types of statements: Activity, Detailed Activity, and Outstanding. Aging details can be shown +in the reports, expressed in aging buckets, so the customer or vendor can review how much is open, due or overdue. The activity statement provides details of all activity on the partner receivables or payables between two selected dates. This includes all invoices, refunds and payments. @@ -8,6 +8,9 @@ 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. +The detailed activity statement is an extension of the previous statement, and intends to explain the transactions +that have happened during the period, also providing with a Prior Balance section and an Ending Balance section. + The outstanding statement provides details of all outstanding partner receivables or payables 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. diff --git a/partner_statement/readme/HISTORY.rst b/partner_statement/readme/HISTORY.rst index dee6c04a..a1bcf6d7 100644 --- a/partner_statement/readme/HISTORY.rst +++ b/partner_statement/readme/HISTORY.rst @@ -5,3 +5,8 @@ * [ADD] New features. * Age by months or days * Filter negative balances + +14.0.2.0.0 (2022-12-16) +~~~~~~~~~~~~~~~~~~~~~~~ + +* [ADD] Detailed Activity Statement. diff --git a/partner_statement/report/__init__.py b/partner_statement/report/__init__.py index fd311a65..f1b003ff 100644 --- a/partner_statement/report/__init__.py +++ b/partner_statement/report/__init__.py @@ -1,5 +1,7 @@ from . import report_statement_common from . import activity_statement +from . import detailed_activity_statement from . import outstanding_statement from . import activity_statement_xlsx +from . import detailed_activity_statement_xlsx from . import outstanting_statement_xlsx diff --git a/partner_statement/report/activity_statement.py b/partner_statement/report/activity_statement.py index 270ec1d4..eea4dcbf 100644 --- a/partner_statement/report/activity_statement.py +++ b/partner_statement/report/activity_statement.py @@ -5,6 +5,8 @@ from collections import defaultdict from odoo import api, models +from .outstanding_statement import OutstandingStatement + class ActivityStatement(models.AbstractModel): """Model of Activity Statement""" @@ -17,40 +19,74 @@ class ActivityStatement(models.AbstractModel): return str( self._cr.mogrify( """ - SELECT l.partner_id, l.currency_id, l.company_id, - sum(CASE WHEN l.currency_id is not null AND l.amount_currency > 0.0 - THEN l.amount_currency - ELSE l.debit - END) as debit, - sum(CASE WHEN l.currency_id is not null AND l.amount_currency < 0.0 - THEN l.amount_currency * (-1) - ELSE l.credit - END) as credit + SELECT l.partner_id, l.currency_id, l.company_id, l.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_amount, + CASE WHEN l.balance > 0.0 + THEN l.amount_currency - sum(coalesce(pd.debit_amount_currency, 0.0)) + ELSE l.amount_currency + sum(coalesce(pc.credit_amount_currency, 0.0)) + END AS open_amount_currency FROM account_move_line l JOIN account_account aa ON (aa.id = l.account_id) JOIN account_account_type at ON (at.id = aa.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 < %(date_start)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 < %(date_start)s + ) as pc ON pc.credit_move_id = l.id WHERE l.partner_id IN %(partners)s AND at.type = %(account_type)s AND l.date < %(date_start)s AND not l.blocked AND m.state IN ('posted') - GROUP BY l.partner_id, l.currency_id, l.company_id + AND ( + (pd.id IS NOT NULL AND + pd.max_date < %(date_start)s) OR + (pc.id IS NOT NULL AND + pc.max_date < %(date_start)s) OR + (pd.id IS NULL AND pc.id IS NULL) + ) + GROUP BY l.partner_id, l.currency_id, l.company_id, l.balance, l.id """, locals(), ), "utf-8", ) - def _initial_balance_sql_q2(self, company_id): + def _initial_balance_sql_q2(self, sub): return str( self._cr.mogrify( - """ - 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 = %(company_id)s - """, + f""" + SELECT {sub}.partner_id, {sub}.currency_id, + sum(CASE WHEN {sub}.currency_id is not null + THEN {sub}.open_amount_currency + ELSE {sub}.open_amount + END) as balance, {sub}.company_id + FROM {sub} + GROUP BY {sub}.partner_id, {sub}.currency_id, {sub}.company_id""", + locals(), + ), + "utf-8", + ) + + def _initial_balance_sql_q3(self, sub, company_id): + return str( + self._cr.mogrify( + f""" + SELECT {sub}.partner_id, {sub}.balance, + COALESCE({sub}.currency_id, c.currency_id) AS currency_id + FROM {sub} + JOIN res_company c ON (c.id = {sub}.company_id) + WHERE c.id = %(company_id)s""", locals(), ), "utf-8", @@ -63,23 +99,30 @@ class ActivityStatement(models.AbstractModel): partners = tuple(partner_ids) # pylint: disable=E8103 self.env.cr.execute( - """WITH Q1 AS (%s), Q2 AS (%s) - SELECT partner_id, currency_id, balance - FROM Q2""" + """WITH Q1 AS (%s), + Q2 AS (%s), + Q3 AS (%s) + SELECT partner_id, currency_id, sum(balance) as balance + FROM Q3 + GROUP BY partner_id, currency_id""" % ( self._initial_balance_sql_q1(partners, date_start, account_type), - self._initial_balance_sql_q2(company_id), + self._initial_balance_sql_q2("Q1"), + self._initial_balance_sql_q3("Q2", company_id), ) ) for row in self.env.cr.dictfetchall(): balance_start[row.pop("partner_id")].append(row) return balance_start - def _display_lines_sql_q1(self, partners, date_start, date_end, account_type): + def _display_activity_lines_sql_q1( + self, partners, date_start, date_end, account_type + ): return str( self._cr.mogrify( """ SELECT m.name AS move_id, l.partner_id, l.date, + array_agg(l.id ORDER BY l.id) as ids, CASE WHEN (aj.type IN ('sale', 'purchase')) THEN l.name ELSE '/' @@ -92,7 +135,7 @@ class ActivityStatement(models.AbstractModel): WHEN (aj.type in ('bank', 'cash')) THEN 'Payment' ELSE '' - END as ref, + END as case_ref, l.blocked, l.currency_id, l.company_id, sum(CASE WHEN (l.currency_id is not null AND l.amount_currency > 0.0) THEN l.amount_currency @@ -120,33 +163,23 @@ class ActivityStatement(models.AbstractModel): CASE WHEN (aj.type IN ('sale', 'purchase')) THEN l.name ELSE '/' - END, - CASE - WHEN (aj.type IN ('sale', 'purchase')) AND l.name IS NOT NULL - THEN l.ref - WHEN aj.type IN ('sale', 'purchase') AND l.name IS NULL - THEN m.ref - WHEN (aj.type in ('bank', 'cash')) - THEN 'Payment' - ELSE '' - END, - l.blocked, l.currency_id, l.company_id + END, case_ref, l.blocked, l.currency_id, l.company_id """, locals(), ), "utf-8", ) - def _display_lines_sql_q2(self, company_id): + def _display_activity_lines_sql_q2(self, sub, company_id): return str( self._cr.mogrify( - """ - SELECT Q1.partner_id, Q1.move_id, Q1.date, Q1.date_maturity, - Q1.name, Q1.ref, Q1.debit, Q1.credit, - Q1.debit-Q1.credit as amount, Q1.blocked, - COALESCE(Q1.currency_id, c.currency_id) AS currency_id - FROM Q1 - JOIN res_company c ON (c.id = Q1.company_id) + f""" + SELECT {sub}.partner_id, {sub}.move_id, {sub}.date, {sub}.date_maturity, + {sub}.name, {sub}.case_ref as ref, {sub}.debit, {sub}.credit, {sub}.ids, + {sub}.debit-{sub}.credit as amount, {sub}.blocked, + COALESCE({sub}.currency_id, c.currency_id) AS currency_id + FROM {sub} + JOIN res_company c ON (c.id = {sub}.company_id) WHERE c.id = %(company_id)s """, locals(), @@ -165,21 +198,117 @@ class ActivityStatement(models.AbstractModel): """ WITH Q1 AS (%s), Q2 AS (%s) - SELECT partner_id, move_id, date, date_maturity, name, ref, debit, - credit, amount, blocked, currency_id + SELECT partner_id, move_id, date, date_maturity, ids, + COALESCE(name, '') as name, COALESCE(ref, '') as ref, + debit, credit, amount, blocked, currency_id FROM Q2 ORDER BY date, date_maturity, move_id""" % ( - self._display_lines_sql_q1( + self._display_activity_lines_sql_q1( partners, date_start, date_end, account_type ), - self._display_lines_sql_q2(company_id), + self._display_activity_lines_sql_q2("Q1", company_id), ) ) for row in self.env.cr.dictfetchall(): res[row.pop("partner_id")].append(row) return res + def _display_activity_reconciled_lines_sql_q1(self, sub): + return str( + self._cr.mogrify( + f""" + SELECT unnest(ids) as id + FROM {sub} + """, + locals(), + ), + "utf-8", + ) + + def _display_activity_reconciled_lines_sql_q2(self, sub, date_end): + return str( + self._cr.mogrify( + f""" + SELECT l.id as rel_id, m.name AS move_id, l.partner_id, l.date, l.name, + l.blocked, l.currency_id, l.company_id, {sub}.id, + CASE WHEN l.ref IS NOT NULL + THEN l.ref + ELSE m.ref + END as ref, + CASE WHEN (l.currency_id is not null AND l.amount_currency > 0.0) + THEN avg(l.amount_currency) + ELSE avg(l.debit) + END as debit, + CASE WHEN (l.currency_id is not null AND l.amount_currency < 0.0) + THEN avg(l.amount_currency * (-1)) + ELSE avg(l.credit) + END as credit, + CASE WHEN l.balance > 0.0 + THEN sum(coalesce(pc.amount, 0.0)) + ELSE -sum(coalesce(pd.amount, 0.0)) + END AS open_amount, + CASE WHEN l.balance > 0.0 + THEN sum(coalesce(pc.debit_amount_currency, 0.0)) + ELSE -sum(coalesce(pd.credit_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 {sub} + LEFT JOIN account_partial_reconcile pd ON ( + pd.debit_move_id = {sub}.id AND pd.max_date <= %(date_end)s) + LEFT JOIN account_partial_reconcile pc ON ( + pc.credit_move_id = {sub}.id AND pc.max_date <= %(date_end)s) + LEFT JOIN account_move_line l ON ( + pd.credit_move_id = l.id OR pc.debit_move_id = l.id) + LEFT JOIN account_move m ON (l.move_id = m.id) + WHERE l.date <= %(date_end)s AND m.state IN ('posted') + GROUP BY l.id, l.partner_id, m.name, l.date, l.date_maturity, l.name, + CASE WHEN l.ref IS NOT NULL + THEN l.ref + ELSE m.ref + END, {sub}.id, + l.blocked, l.currency_id, l.balance, l.amount_currency, l.company_id + """, + locals(), + ), + "utf-8", + ) + + def _get_account_display_reconciled_lines( + self, company_id, partner_ids, date_start, date_end, account_type + ): + partners = tuple(partner_ids) + + # pylint: disable=E8103 + self.env.cr.execute( + """ + WITH Q1 AS (%s), + Q2 AS (%s), + Q3 AS (%s), + Q4 AS (%s), + Q5 AS (%s), + Q6 AS (%s) + SELECT partner_id, currency_id, move_id, date, date_maturity, debit, + credit, amount, open_amount, COALESCE(name, '') as name, + COALESCE(ref, '') as ref, blocked, id + FROM Q6 + ORDER BY date, date_maturity, move_id""" + % ( + self._display_activity_lines_sql_q1( + partners, date_start, date_end, account_type + ), + self._display_activity_lines_sql_q2("Q1", company_id), + self._display_activity_reconciled_lines_sql_q1("Q2"), + self._display_activity_reconciled_lines_sql_q2("Q3", date_end), + self._display_outstanding_lines_sql_q2("Q4"), + self._display_outstanding_lines_sql_q3("Q5", company_id), + ) + ) + return self.env.cr.dictfetchall() + @api.model def _get_report_values(self, docids, data=None): if not data: @@ -191,3 +320,11 @@ class ActivityStatement(models.AbstractModel): data.update(wiz.create({})._prepare_statement()) data["amount_field"] = "amount" return super()._get_report_values(docids, data) + + +ActivityStatement._display_outstanding_lines_sql_q2 = ( + OutstandingStatement._display_outstanding_lines_sql_q2 +) +ActivityStatement._display_outstanding_lines_sql_q3 = ( + OutstandingStatement._display_outstanding_lines_sql_q3 +) diff --git a/partner_statement/report/activity_statement_xlsx.py b/partner_statement/report/activity_statement_xlsx.py index 4f43625d..01e61630 100644 --- a/partner_statement/report/activity_statement_xlsx.py +++ b/partner_statement/report/activity_statement_xlsx.py @@ -2,11 +2,23 @@ # Copyright 2021 ForgeFlow S.L. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo import _, fields, models +from odoo import _, models from odoo.addons.report_xlsx_helper.report.report_xlsx_format import FORMATS +def copy_format(book, fmt): + properties = [f[4:] for f in dir(fmt) if f[0:4] == "set_"] + dft_fmt = book.add_format() + return book.add_format( + { + k: v + for k, v in fmt.__dict__.items() + if k in properties and dft_fmt.__dict__[k] != v + } + ) + + class ActivityStatementXslx(models.AbstractModel): _name = "report.p_s.report_activity_statement_xlsx" _description = "Activity Statement XLSL Report" @@ -36,7 +48,7 @@ class ActivityStatementXslx(models.AbstractModel): } sheet.merge_range( - row_pos, 0, row_pos, 6, statement_header, FORMATS["format_right_bold"] + row_pos, 0, row_pos, 6, statement_header, FORMATS["format_left_bold"] ) row_pos += 1 sheet.write( @@ -47,20 +59,25 @@ class ActivityStatementXslx(models.AbstractModel): row_pos, 2, row_pos, - 4, + 3, _("Description"), FORMATS["format_theader_yellow_center"], ) sheet.write( - row_pos, 5, _("Open Amount"), FORMATS["format_theader_yellow_center"] + row_pos, 4, _("Original Amount"), FORMATS["format_theader_yellow_center"] + ) + sheet.write( + row_pos, 5, _("Applied Amount"), FORMATS["format_theader_yellow_center"] + ) + sheet.write( + row_pos, 6, _("Open Amount"), FORMATS["format_theader_yellow_center"] ) - sheet.write(row_pos, 6, _("Balance"), FORMATS["format_theader_yellow_center"]) row_pos += 1 sheet.write( - row_pos, 1, partner_data.get("start"), FORMATS["format_tcell_date_left"] + row_pos, 1, partner_data.get("prior_day"), FORMATS["format_tcell_date_left"] ) sheet.merge_range( - row_pos, 2, row_pos, 4, _("Balance Forward"), FORMATS["format_tcell_left"] + row_pos, 2, row_pos, 5, _("Balance Forward"), FORMATS["format_tcell_left"] ) sheet.write( row_pos, @@ -68,12 +85,21 @@ class ActivityStatementXslx(models.AbstractModel): currency_data.get("balance_forward"), FORMATS["current_money_format"], ) + format_tcell_left = FORMATS["format_tcell_left"] + format_tcell_date_left = FORMATS["format_tcell_date_left"] + format_distributed = FORMATS["format_distributed"] + current_money_format = FORMATS["current_money_format"] for line in currency_data.get("lines"): + if line.get("blocked"): + format_tcell_left = FORMATS["format_tcell_left_blocked"] + format_tcell_date_left = FORMATS["format_tcell_date_left_blocked"] + format_distributed = FORMATS["format_distributed_blocked"] + current_money_format = FORMATS["current_money_format_blocked"] row_pos += 1 name_to_show = ( line.get("name", "") == "/" or not line.get("name", "") ) and line.get("ref", "") - if line.get("name", "") != "/": + if line.get("name", "") and line.get("name", "") != "/": if not line.get("ref", ""): name_to_show = line.get("name", "") else: @@ -83,30 +109,26 @@ class ActivityStatementXslx(models.AbstractModel): name_to_show = line.get("name", "") elif line.get("ref", "") not in line.get("name", ""): name_to_show = line.get("ref", "") + sheet.write(row_pos, 0, line.get("move_id", ""), format_tcell_left) + sheet.write(row_pos, 1, line.get("date", ""), format_tcell_date_left) + sheet.merge_range(row_pos, 2, row_pos, 3, name_to_show, format_distributed) + sheet.write(row_pos, 4, line.get("amount", ""), current_money_format) sheet.write( - row_pos, 0, line.get("move_id", ""), FORMATS["format_tcell_left"] - ) - sheet.write( - row_pos, 1, line.get("date", ""), FORMATS["format_tcell_date_left"] - ) - sheet.merge_range( - row_pos, 2, row_pos, 4, name_to_show, FORMATS["format_distributed"] - ) - sheet.write( - row_pos, 5, line.get("amount", ""), FORMATS["current_money_format"] - ) - sheet.write( - row_pos, 6, line.get("balance", ""), FORMATS["current_money_format"] + row_pos, 5, line.get("applied_amount", ""), current_money_format ) + sheet.write(row_pos, 6, line.get("open_amount", ""), current_money_format) row_pos += 1 sheet.write( row_pos, 1, partner_data.get("end"), FORMATS["format_tcell_date_left"] ) sheet.merge_range( - row_pos, 2, row_pos, 4, _("Ending Balance"), FORMATS["format_tcell_left"] + row_pos, 2, row_pos, 5, _("Ending Balance"), FORMATS["format_tcell_left"] ) sheet.write( - row_pos, 6, currency_data.get("amount_due"), FORMATS["current_money_format"] + row_pos, + 6, + currency_data.get("amount_due"), + FORMATS["current_money_format"], ) return row_pos @@ -180,7 +202,7 @@ class ActivityStatementXslx(models.AbstractModel): ) return row_pos - def _size_columns(self, sheet): + def _size_columns(self, sheet, data): for i in range(7): sheet.set_column(0, i, 20) @@ -203,7 +225,7 @@ class ActivityStatementXslx(models.AbstractModel): 0, row_pos, 6, - _("Statement of Account from %s") % (company.display_name), + _("Statement of Account from %s") % (company.display_name,), FORMATS["format_ws_title"], ) row_pos += 1 @@ -211,10 +233,10 @@ class ActivityStatementXslx(models.AbstractModel): sheet.write( row_pos, 2, - fields.Date.from_string(data.get("date_end")), + data.get("data", {}).get(partners.ids[0], {}).get("today"), FORMATS["format_date_left"], ) - self._size_columns(sheet) + self._size_columns(sheet, data) for partner in partners: invoice_address = data.get( "get_inv_addr", lambda x: self.env["res.partner"] @@ -286,6 +308,23 @@ class ActivityStatementXslx(models.AbstractModel): FORMATS["current_money_format"] = workbook.add_format( {"align": "right", "num_format": money_string} ) + bg_grey = "#CCCCCC" + FORMATS["format_tcell_left_blocked"] = copy_format( + workbook, FORMATS["format_tcell_left"] + ) + FORMATS["format_tcell_left_blocked"].set_bg_color(bg_grey) + FORMATS["format_tcell_date_left_blocked"] = copy_format( + workbook, FORMATS["format_tcell_date_left"] + ) + FORMATS["format_tcell_date_left_blocked"].set_bg_color(bg_grey) + FORMATS["format_distributed_blocked"] = copy_format( + workbook, FORMATS["format_distributed"] + ) + FORMATS["format_distributed_blocked"].set_bg_color(bg_grey) + FORMATS["current_money_format_blocked"] = copy_format( + workbook, FORMATS["current_money_format"] + ) + FORMATS["current_money_format_blocked"].set_bg_color(bg_grey) row_pos = self._write_currency_lines( row_pos, sheet, partner, currency, data ) diff --git a/partner_statement/report/detailed_activity_statement.py b/partner_statement/report/detailed_activity_statement.py new file mode 100644 index 00000000..7507dd4d --- /dev/null +++ b/partner_statement/report/detailed_activity_statement.py @@ -0,0 +1,43 @@ +# Copyright 2022 ForgeFlow, S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import models + +from .outstanding_statement import OutstandingStatement + + +class DetailedActivityStatement(models.AbstractModel): + """Model of Detailed Activity Statement""" + + _inherit = "report.partner_statement.activity_statement" + _name = "report.partner_statement.detailed_activity_statement" + _description = "Partner Detailed Activity Statement" + + def _get_account_display_prior_lines( + self, company_id, partner_ids, date_start, date_end, account_type + ): + return self._get_account_display_lines2( + company_id, partner_ids, date_start, date_end, account_type + ) + + def _get_account_display_ending_lines( + self, company_id, partner_ids, date_start, date_end, account_type + ): + return self._get_account_display_lines2( + company_id, partner_ids, date_start, date_end, account_type + ) + + def _add_currency_prior_line(self, line, currency): + return self._add_currency_line2(line, currency) + + def _add_currency_ending_line(self, line, currency): + return self._add_currency_line2(line, currency) + + +DetailedActivityStatement._get_account_display_lines2 = ( + OutstandingStatement._get_account_display_lines +) +DetailedActivityStatement._display_outstanding_lines_sql_q1 = ( + OutstandingStatement._display_outstanding_lines_sql_q1 +) +DetailedActivityStatement._add_currency_line2 = OutstandingStatement._add_currency_line diff --git a/partner_statement/report/detailed_activity_statement_xlsx.py b/partner_statement/report/detailed_activity_statement_xlsx.py new file mode 100644 index 00000000..614e9362 --- /dev/null +++ b/partner_statement/report/detailed_activity_statement_xlsx.py @@ -0,0 +1,568 @@ +# Author: Miquel Raïch +# Copyright 2022 ForgeFlow S.L. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import _, models + +from odoo.addons.report_xlsx_helper.report.report_xlsx_format import FORMATS + + +def copy_format(book, fmt): + properties = [f[4:] for f in dir(fmt) if f[0:4] == "set_"] + dft_fmt = book.add_format() + return book.add_format( + { + k: v + for k, v in fmt.__dict__.items() + if k in properties and dft_fmt.__dict__[k] != v + } + ) + + +class DetailedActivityStatementXslx(models.AbstractModel): + _name = "report.p_s.report_detailed_activity_statement_xlsx" + _description = "Detailed Activity Statement XLSL Report" + _inherit = "report.p_s.report_activity_statement_xlsx" + + def _get_report_name(self, report, data=False): + company_id = data.get("company_id", False) + report_name = _("Detailed Activity Statement") + if company_id: + company = self.env["res.company"].browse(company_id) + suffix = " - {} - {}".format(company.name, company.currency_id.name) + report_name = report_name + suffix + return report_name + + def _write_currency_lines(self, row_pos, sheet, partner, currency, data): + partner_data = data.get("data", {}).get(partner.id, {}) + currency_data = partner_data.get("currencies", {}).get(currency.id) + account_type = data.get("account_type", False) + row_pos += 2 + statement_header = _( + "Detailed %(payable)sStatement between %(start)s and %(end)s in %(currency)s" + ) % { + "payable": account_type == "payable" and _("Supplier ") or "", + "start": partner_data.get("start"), + "end": partner_data.get("end"), + "currency": currency.display_name, + } + sheet.merge_range( + row_pos, + 0, + row_pos, + 6, + statement_header, + FORMATS["format_left_bold"], + ) + row_pos += 1 + sheet.write( + row_pos, 0, _("Reference Number"), FORMATS["format_theader_yellow_center"] + ) + sheet.write(row_pos, 1, _("Date"), FORMATS["format_theader_yellow_center"]) + sheet.merge_range( + row_pos, + 2, + row_pos, + 3, + _("Description"), + FORMATS["format_theader_yellow_center"], + ) + sheet.write( + row_pos, 4, _("Original Amount"), FORMATS["format_theader_yellow_center"] + ) + sheet.write( + row_pos, 5, _("Applied Amount"), FORMATS["format_theader_yellow_center"] + ) + sheet.write( + row_pos, 6, _("Open Amount"), FORMATS["format_theader_yellow_center"] + ) + row_pos += 1 + sheet.write( + row_pos, 1, partner_data.get("prior_day"), FORMATS["format_tcell_date_left"] + ) + sheet.merge_range( + row_pos, + 2, + row_pos, + 5, + _("Initial Balance"), + FORMATS["format_tcell_left"], + ) + sheet.write( + row_pos, + 6, + currency_data.get("balance_forward"), + FORMATS["current_money_format"], + ) + for line in currency_data.get("lines"): + if line.get("blocked") and not line.get("reconciled_line"): + format_tcell_left = FORMATS["format_tcell_left_blocked"] + format_tcell_date_left = FORMATS["format_tcell_date_left_blocked"] + format_distributed = FORMATS["format_distributed_blocked"] + current_money_format = FORMATS["current_money_format_blocked"] + elif line.get("reconciled_line") and not line.get("blocked"): + format_tcell_left = FORMATS["format_tcell_left_reconciled"] + format_tcell_date_left = FORMATS["format_tcell_date_left_reconciled"] + format_distributed = FORMATS["format_distributed_reconciled"] + current_money_format = FORMATS["current_money_format_reconciled"] + elif line.get("blocked") and line.get("reconciled_line"): + format_tcell_left = FORMATS["format_tcell_left_blocked_reconciled"] + format_tcell_date_left = FORMATS[ + "format_tcell_date_left_blocked_reconciled" + ] + format_distributed = FORMATS["format_distributed_blocked_reconciled"] + current_money_format = FORMATS[ + "current_money_format_blocked_reconciled" + ] + else: + format_tcell_left = FORMATS["format_tcell_left"] + format_tcell_date_left = FORMATS["format_tcell_date_left"] + format_distributed = FORMATS["format_distributed"] + current_money_format = FORMATS["current_money_format"] + row_pos += 1 + name_to_show = ( + line.get("name", "") == "/" or not line.get("name", "") + ) and line.get("ref", "") + if line.get("name", "") and line.get("name", "") != "/": + if not line.get("ref", ""): + name_to_show = line.get("name", "") + else: + if (line.get("name", "") in line.get("ref", "")) or ( + line.get("name", "") == line.get("ref", "") + ): + name_to_show = line.get("name", "") + elif line.get("ref", "") not in line.get("name", ""): + name_to_show = line.get("ref", "") + sheet.write(row_pos, 0, line.get("move_id", ""), format_tcell_left) + sheet.write(row_pos, 1, line.get("date", ""), format_tcell_date_left) + sheet.merge_range(row_pos, 2, row_pos, 3, name_to_show, format_distributed) + sheet.write( + row_pos, + 4, + line.get("amount", "") if not line.get("reconciled_line") else "", + current_money_format, + ) + sheet.write( + row_pos, 5, line.get("applied_amount", ""), current_money_format + ) + sheet.write( + row_pos, + 6, + line.get("open_amount", "") if not line.get("reconciled_line") else "", + current_money_format, + ) + row_pos += 1 + sheet.write( + row_pos, 1, partner_data.get("end"), FORMATS["format_tcell_date_left"] + ) + sheet.merge_range( + row_pos, + 2, + row_pos, + 5, + _("Ending Balance"), + FORMATS["format_tcell_left"], + ) + sheet.write( + row_pos, + 6, + currency_data.get("amount_due"), + FORMATS["current_money_format"], + ) + return row_pos + + def _write_currency_prior_lines(self, row_pos, sheet, partner, currency, data): + partner_data = data.get("data", {}).get(partner.id, {}) + currency_data = partner_data.get("currencies", {}).get(currency.id) + account_type = data.get("account_type", False) + row_pos += 2 + statement_header = _( + "%(payable)sStatement up to %(prior_day)s in %(currency)s" + ) % { + "payable": account_type == "payable" and _("Supplier ") or "", + "prior_day": partner_data.get("prior_day"), + "currency": currency.display_name, + } + sheet.merge_range( + row_pos, + 0, + row_pos, + 6, + statement_header, + FORMATS["format_left_bold"], + ) + row_pos += 1 + sheet.write( + row_pos, 0, _("Reference Number"), FORMATS["format_theader_yellow_center"] + ) + sheet.write(row_pos, 1, _("Date"), FORMATS["format_theader_yellow_center"]) + sheet.write(row_pos, 2, _("Due Date"), FORMATS["format_theader_yellow_center"]) + sheet.write( + row_pos, + 3, + _("Description"), + FORMATS["format_theader_yellow_center"], + ) + sheet.write(row_pos, 4, _("Original"), FORMATS["format_theader_yellow_center"]) + sheet.write( + row_pos, 5, _("Open Amount"), FORMATS["format_theader_yellow_center"] + ) + sheet.write(row_pos, 6, _("Balance"), FORMATS["format_theader_yellow_center"]) + format_tcell_left = FORMATS["format_tcell_left"] + format_tcell_date_left = FORMATS["format_tcell_date_left"] + format_distributed = FORMATS["format_distributed"] + current_money_format = FORMATS["current_money_format"] + for line in currency_data.get("prior_lines"): + if line.get("blocked") and not line.get("reconciled_line"): + format_tcell_left = FORMATS["format_tcell_left_blocked"] + format_tcell_date_left = FORMATS["format_tcell_date_left_blocked"] + format_distributed = FORMATS["format_distributed_blocked"] + current_money_format = FORMATS["current_money_format_blocked"] + elif line.get("reconciled_line") and not line.get("blocked"): + format_tcell_left = FORMATS["format_tcell_left_reconciled"] + format_tcell_date_left = FORMATS["format_tcell_date_left_reconciled"] + format_distributed = FORMATS["format_distributed_reconciled"] + current_money_format = FORMATS["current_money_format_reconciled"] + elif line.get("blocked") and line.get("reconciled_line"): + format_tcell_left = FORMATS["format_tcell_left_blocked_reconciled"] + format_tcell_date_left = FORMATS[ + "format_tcell_date_left_blocked_reconciled" + ] + format_distributed = FORMATS["format_distributed_blocked_reconciled"] + current_money_format = FORMATS[ + "current_money_format_blocked_reconciled" + ] + row_pos += 1 + name_to_show = ( + line.get("name", "") == "/" or not line.get("name", "") + ) and line.get("ref", "") + if line.get("name", "") and line.get("name", "") != "/": + if not line.get("ref", ""): + name_to_show = line.get("name", "") + else: + if (line.get("ref", "") in line.get("name", "")) or ( + line.get("name", "") == line.get("ref", "") + ): + name_to_show = line.get("name", "") + else: + name_to_show = line.get("ref", "") + sheet.write(row_pos, 0, line.get("move_id", ""), format_tcell_left) + sheet.write(row_pos, 1, line.get("date", ""), format_tcell_date_left) + sheet.write( + row_pos, + 2, + line.get("date_maturity", ""), + format_tcell_date_left, + ) + sheet.write(row_pos, 3, name_to_show, format_distributed) + sheet.write(row_pos, 4, line.get("amount", ""), current_money_format) + sheet.write(row_pos, 5, line.get("open_amount", ""), current_money_format) + sheet.write(row_pos, 6, line.get("balance", ""), current_money_format) + row_pos += 1 + sheet.write( + row_pos, 1, partner_data.get("prior_day"), FORMATS["format_tcell_date_left"] + ) + sheet.merge_range( + row_pos, + 2, + row_pos, + 5, + _("Ending Balance"), + FORMATS["format_tcell_left"], + ) + sheet.write( + row_pos, + 6, + currency_data.get("balance_forward"), + FORMATS["current_money_format"], + ) + return row_pos + + def _write_currency_ending_lines(self, row_pos, sheet, partner, currency, data): + partner_data = data.get("data", {}).get(partner.id, {}) + currency_data = partner_data.get("currencies", {}).get(currency.id) + account_type = data.get("account_type", False) + row_pos += 2 + statement_header = _("%(payable)sStatement up to %(end)s in %(currency)s") % { + "payable": account_type == "payable" and _("Supplier ") or "", + "end": partner_data.get("end"), + "currency": currency.display_name, + } + sheet.merge_range( + row_pos, + 0, + row_pos, + 6, + statement_header, + FORMATS["format_left_bold"], + ) + row_pos += 1 + sheet.write( + row_pos, 0, _("Reference Number"), FORMATS["format_theader_yellow_center"] + ) + sheet.write(row_pos, 1, _("Date"), FORMATS["format_theader_yellow_center"]) + sheet.write(row_pos, 2, _("Due Date"), FORMATS["format_theader_yellow_center"]) + sheet.write( + row_pos, + 3, + _("Description"), + FORMATS["format_theader_yellow_center"], + ) + sheet.write(row_pos, 4, _("Original"), FORMATS["format_theader_yellow_center"]) + sheet.write( + row_pos, 5, _("Open Amount"), FORMATS["format_theader_yellow_center"] + ) + sheet.write(row_pos, 6, _("Balance"), FORMATS["format_theader_yellow_center"]) + format_tcell_left = FORMATS["format_tcell_left"] + format_tcell_date_left = FORMATS["format_tcell_date_left"] + format_distributed = FORMATS["format_distributed"] + current_money_format = FORMATS["current_money_format"] + for line in currency_data.get("ending_lines"): + if line.get("blocked") and not line.get("reconciled_line"): + format_tcell_left = FORMATS["format_tcell_left_blocked"] + format_tcell_date_left = FORMATS["format_tcell_date_left_blocked"] + format_distributed = FORMATS["format_distributed_blocked"] + current_money_format = FORMATS["current_money_format_blocked"] + elif line.get("reconciled_line") and not line.get("blocked"): + format_tcell_left = FORMATS["format_tcell_left_reconciled"] + format_tcell_date_left = FORMATS["format_tcell_date_left_reconciled"] + format_distributed = FORMATS["format_distributed_reconciled"] + current_money_format = FORMATS["current_money_format_reconciled"] + elif line.get("blocked") and line.get("reconciled_line"): + format_tcell_left = FORMATS["format_tcell_left_blocked_reconciled"] + format_tcell_date_left = FORMATS[ + "format_tcell_date_left_blocked_reconciled" + ] + format_distributed = FORMATS["format_distributed_blocked_reconciled"] + current_money_format = FORMATS[ + "current_money_format_blocked_reconciled" + ] + row_pos += 1 + name_to_show = ( + line.get("name", "") == "/" or not line.get("name", "") + ) and line.get("ref", "") + if line.get("name", "") and line.get("name", "") != "/": + if not line.get("ref", ""): + name_to_show = line.get("name", "") + else: + if (line.get("ref", "") in line.get("name", "")) or ( + line.get("name", "") == line.get("ref", "") + ): + name_to_show = line.get("name", "") + else: + name_to_show = line.get("ref", "") + sheet.write(row_pos, 0, line.get("move_id", ""), format_tcell_left) + sheet.write(row_pos, 1, line.get("date", ""), format_tcell_date_left) + sheet.write( + row_pos, + 2, + line.get("date_maturity", ""), + format_tcell_date_left, + ) + sheet.write(row_pos, 3, name_to_show, format_distributed) + sheet.write(row_pos, 4, line.get("amount", ""), current_money_format) + sheet.write(row_pos, 5, line.get("open_amount", ""), current_money_format) + sheet.write(row_pos, 6, line.get("balance", ""), current_money_format) + row_pos += 1 + sheet.write( + row_pos, 1, partner_data.get("end"), FORMATS["format_tcell_date_left"] + ) + sheet.merge_range( + row_pos, + 2, + row_pos, + 5, + _("Ending Balance"), + FORMATS["format_tcell_left"], + ) + sheet.write( + row_pos, + 6, + currency_data.get("amount_due"), + FORMATS["current_money_format"], + ) + return row_pos + + def _size_columns(self, sheet, data): + for i in range(7): + sheet.set_column(0, i, 20) + + def generate_xlsx_report(self, workbook, data, objects): + report_model = self.env["report.partner_statement.detailed_activity_statement"] + self._define_formats(workbook) + FORMATS["format_distributed"] = workbook.add_format({"align": "vdistributed"}) + company_id = data.get("company_id", False) + if company_id: + company = self.env["res.company"].browse(company_id) + else: + company = self.env.user.company_id + data.update(report_model._get_report_values(data.get("partner_ids"), data)) + partners = self.env["res.partner"].browse(data.get("partner_ids")) + sheet = workbook.add_worksheet(_("Detailed Activity Statement")) + sheet.set_landscape() + row_pos = 0 + sheet.merge_range( + row_pos, + 0, + row_pos, + 6, + _("Statement of Account from %s") % (company.display_name,), + FORMATS["format_ws_title"], + ) + row_pos += 1 + sheet.write(row_pos, 1, _("Date:"), FORMATS["format_theader_yellow_right"]) + sheet.write( + row_pos, + 2, + data.get("data", {}).get(partners.ids[0], {}).get("today"), + FORMATS["format_date_left"], + ) + self._size_columns(sheet, data) + for partner in partners: + invoice_address = data.get( + "get_inv_addr", lambda x: self.env["res.partner"] + )(partner) + row_pos += 3 + sheet.write( + row_pos, 1, _("Statement to:"), FORMATS["format_theader_yellow_right"] + ) + sheet.merge_range( + row_pos, + 2, + row_pos, + 3, + invoice_address.display_name, + FORMATS["format_left"], + ) + if invoice_address.vat: + sheet.write( + row_pos, + 4, + _("VAT:"), + FORMATS["format_theader_yellow_right"], + ) + sheet.write( + row_pos, + 5, + invoice_address.vat, + FORMATS["format_left"], + ) + row_pos += 1 + sheet.write( + row_pos, 1, _("Statement from:"), FORMATS["format_theader_yellow_right"] + ) + sheet.merge_range( + row_pos, + 2, + row_pos, + 3, + company.partner_id.display_name, + FORMATS["format_left"], + ) + if company.vat: + sheet.write( + row_pos, + 4, + _("VAT:"), + FORMATS["format_theader_yellow_right"], + ) + sheet.write( + row_pos, + 5, + company.vat, + FORMATS["format_left"], + ) + partner_data = data.get("data", {}).get(partner.id) + currencies = partner_data.get("currencies", {}).keys() + if currencies: + row_pos += 1 + for currency_id in currencies: + currency = self.env["res.currency"].browse(currency_id) + if currency.position == "after": + money_string = "#,##0.%s " % ( + "0" * currency.decimal_places + ) + "[${}]".format(currency.symbol) + elif currency.position == "before": + money_string = "[${}]".format(currency.symbol) + " #,##0.%s" % ( + "0" * currency.decimal_places + ) + FORMATS["current_money_format"] = workbook.add_format( + {"align": "right", "num_format": money_string} + ) + bg_grey = "#CCCCCC" + FORMATS["format_tcell_left_blocked"] = copy_format( + workbook, FORMATS["format_tcell_left"] + ) + FORMATS["format_tcell_left_blocked"].set_bg_color(bg_grey) + FORMATS["format_tcell_date_left_blocked"] = copy_format( + workbook, FORMATS["format_tcell_date_left"] + ) + FORMATS["format_tcell_date_left_blocked"].set_bg_color(bg_grey) + FORMATS["format_distributed_blocked"] = copy_format( + workbook, FORMATS["format_distributed"] + ) + FORMATS["format_distributed_blocked"].set_bg_color(bg_grey) + FORMATS["current_money_format_blocked"] = copy_format( + workbook, FORMATS["current_money_format"] + ) + FORMATS["current_money_format_blocked"].set_bg_color(bg_grey) + FORMATS["format_tcell_left_reconciled"] = copy_format( + workbook, FORMATS["format_tcell_left"] + ) + FORMATS["format_tcell_left_reconciled"].set_italic(True) + FORMATS["format_tcell_left_reconciled"].set_font_size(10) + FORMATS["format_tcell_left_reconciled"].set_indent(1) + FORMATS["format_tcell_date_left_reconciled"] = copy_format( + workbook, FORMATS["format_tcell_date_left"] + ) + FORMATS["format_tcell_date_left_reconciled"].set_italic(True) + FORMATS["format_tcell_date_left_reconciled"].set_font_size(10) + FORMATS["format_distributed_reconciled"] = copy_format( + workbook, FORMATS["format_distributed"] + ) + FORMATS["format_distributed_reconciled"].set_italic(True) + FORMATS["format_distributed_reconciled"].set_font_size(10) + FORMATS["current_money_format_reconciled"] = copy_format( + workbook, FORMATS["current_money_format"] + ) + FORMATS["current_money_format_reconciled"].set_italic(True) + FORMATS["current_money_format_reconciled"].set_font_size(10) + FORMATS["format_tcell_left_blocked_reconciled"] = copy_format( + workbook, FORMATS["format_tcell_left"] + ) + FORMATS["format_tcell_left_blocked_reconciled"].set_bg_color(bg_grey) + FORMATS["format_tcell_left_blocked_reconciled"].set_italic(True) + FORMATS["format_tcell_left_blocked_reconciled"].set_font_size(10) + FORMATS["format_tcell_left_blocked_reconciled"].set_indent(1) + FORMATS["format_tcell_date_left_blocked_reconciled"] = copy_format( + workbook, FORMATS["format_tcell_date_left"] + ) + FORMATS["format_tcell_date_left_blocked_reconciled"].set_bg_color( + bg_grey + ) + FORMATS["format_tcell_date_left_blocked_reconciled"].set_italic(True) + FORMATS["format_tcell_date_left_blocked_reconciled"].set_font_size(10) + FORMATS["format_distributed_blocked_reconciled"] = copy_format( + workbook, FORMATS["format_distributed"] + ) + FORMATS["format_distributed_blocked_reconciled"].set_bg_color(bg_grey) + FORMATS["format_distributed_blocked_reconciled"].set_italic(True) + FORMATS["format_distributed_blocked_reconciled"].set_font_size(10) + FORMATS["current_money_format_blocked_reconciled"] = copy_format( + workbook, FORMATS["current_money_format"] + ) + FORMATS["current_money_format_blocked_reconciled"].set_bg_color(bg_grey) + FORMATS["current_money_format_blocked_reconciled"].set_italic(True) + FORMATS["current_money_format_blocked_reconciled"].set_font_size(10) + row_pos = self._write_currency_prior_lines( + row_pos, sheet, partner, currency, data + ) + row_pos = self._write_currency_lines( + row_pos, sheet, partner, currency, data + ) + row_pos = self._write_currency_ending_lines( + row_pos, sheet, partner, currency, data + ) + row_pos = self._write_currency_buckets( + row_pos, sheet, partner, currency, data + ) diff --git a/partner_statement/report/outstanding_statement.py b/partner_statement/report/outstanding_statement.py index d011428e..a4d69a3c 100644 --- a/partner_statement/report/outstanding_statement.py +++ b/partner_statement/report/outstanding_statement.py @@ -12,13 +12,13 @@ class OutstandingStatement(models.AbstractModel): _name = "report.partner_statement.outstanding_statement" _description = "Partner Outstanding Statement" - def _display_lines_sql_q1(self, partners, date_end, account_type): + def _display_outstanding_lines_sql_q1(self, partners, date_end, account_type): partners = tuple(partners) return str( self._cr.mogrify( """ SELECT l.id, m.name AS move_id, l.partner_id, l.date, l.name, - l.blocked, l.currency_id, l.company_id, + l.blocked, l.currency_id, l.company_id, CASE WHEN l.ref IS NOT NULL THEN l.ref ELSE m.ref @@ -60,13 +60,13 @@ class OutstandingStatement(models.AbstractModel): WHERE l2.date <= %(date_end)s ) as pc ON pc.credit_move_id = l.id WHERE l.partner_id IN %(partners)s AND at.type = %(account_type)s - AND ( - (pd.id IS NOT NULL AND - pd.max_date <= %(date_end)s) OR - (pc.id IS NOT NULL AND - pc.max_date <= %(date_end)s) OR - (pd.id IS NULL AND pc.id IS NULL) - ) AND l.date <= %(date_end)s AND m.state IN ('posted') + AND ( + (pd.id IS NOT NULL AND + pd.max_date <= %(date_end)s) OR + (pc.id IS NOT NULL AND + pc.max_date <= %(date_end)s) OR + (pd.id IS NULL AND pc.id IS NULL) + ) AND l.date <= %(date_end)s AND m.state IN ('posted') GROUP BY l.id, l.partner_id, m.name, l.date, l.date_maturity, l.name, CASE WHEN l.ref IS NOT NULL THEN l.ref @@ -79,36 +79,36 @@ class OutstandingStatement(models.AbstractModel): "utf-8", ) - def _display_lines_sql_q2(self): + def _display_outstanding_lines_sql_q2(self, sub): return str( self._cr.mogrify( - """ - SELECT Q1.partner_id, Q1.currency_id, Q1.move_id, - Q1.date, Q1.date_maturity, Q1.debit, Q1.credit, - Q1.name, Q1.ref, Q1.blocked, Q1.company_id, - CASE WHEN Q1.currency_id is not null - THEN Q1.open_amount_currency - ELSE Q1.open_amount - END as open_amount - FROM Q1 + f""" + SELECT {sub}.partner_id, {sub}.currency_id, {sub}.move_id, + {sub}.date, {sub}.date_maturity, {sub}.debit, {sub}.credit, + {sub}.name, {sub}.ref, {sub}.blocked, {sub}.company_id, + CASE WHEN {sub}.currency_id is not null + THEN {sub}.open_amount_currency + ELSE {sub}.open_amount + END as open_amount, {sub}.id + FROM {sub} """, locals(), ), "utf-8", ) - def _display_lines_sql_q3(self, company_id): + def _display_outstanding_lines_sql_q3(self, sub, company_id): return str( self._cr.mogrify( - """ - SELECT Q2.partner_id, Q2.move_id, Q2.date, Q2.date_maturity, - Q2.name, Q2.ref, Q2.debit, Q2.credit, - Q2.debit-Q2.credit AS amount, blocked, - COALESCE(Q2.currency_id, c.currency_id) AS currency_id, - Q2.open_amount - FROM Q2 - JOIN res_company c ON (c.id = Q2.company_id) - WHERE c.id = %(company_id)s AND Q2.open_amount != 0.0 + f""" + SELECT {sub}.partner_id, {sub}.move_id, {sub}.date, + {sub}.date_maturity, {sub}.name, {sub}.ref, {sub}.debit, + {sub}.credit, {sub}.debit-{sub}.credit AS amount, + COALESCE({sub}.currency_id, c.currency_id) AS currency_id, + {sub}.open_amount, {sub}.blocked, {sub}.id + FROM {sub} + JOIN res_company c ON (c.id = {sub}.company_id) + WHERE c.id = %(company_id)s AND {sub}.open_amount != 0.0 """, locals(), ), @@ -127,13 +127,16 @@ class OutstandingStatement(models.AbstractModel): Q2 AS (%s), Q3 AS (%s) SELECT partner_id, currency_id, move_id, date, date_maturity, debit, - credit, amount, open_amount, name, ref, blocked + credit, amount, open_amount, COALESCE(name, '') as name, + COALESCE(ref, '') as ref, blocked, id FROM Q3 ORDER BY date, date_maturity, move_id""" % ( - self._display_lines_sql_q1(partners, date_end, account_type), - self._display_lines_sql_q2(), - self._display_lines_sql_q3(company_id), + self._display_outstanding_lines_sql_q1( + partners, date_end, account_type + ), + self._display_outstanding_lines_sql_q2("Q1"), + self._display_outstanding_lines_sql_q3("Q2", company_id), ) ) for row in self.env.cr.dictfetchall(): diff --git a/partner_statement/report/outstanting_statement_xlsx.py b/partner_statement/report/outstanting_statement_xlsx.py index e1e82566..e29b7f37 100644 --- a/partner_statement/report/outstanting_statement_xlsx.py +++ b/partner_statement/report/outstanting_statement_xlsx.py @@ -2,11 +2,23 @@ # Copyright 2021 ForgeFlow S.L. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo import _, fields, models +from odoo import _, models from odoo.addons.report_xlsx_helper.report.report_xlsx_format import FORMATS +def copy_format(book, fmt): + properties = [f[4:] for f in dir(fmt) if f[0:4] == "set_"] + dft_fmt = book.add_format() + return book.add_format( + { + k: v + for k, v in fmt.__dict__.items() + if k in properties and dft_fmt.__dict__[k] != v + } + ) + + class OutstandingStatementXslx(models.AbstractModel): _name = "report.p_s.report_outstanding_statement_xlsx" _description = "Outstanding Statement XLSL Report" @@ -33,7 +45,7 @@ class OutstandingStatementXslx(models.AbstractModel): } sheet.merge_range( - row_pos, 0, row_pos, 6, statement_header, FORMATS["format_right_bold"] + row_pos, 0, row_pos, 6, statement_header, FORMATS["format_left_bold"] ) row_pos += 1 sheet.write( @@ -49,12 +61,21 @@ class OutstandingStatementXslx(models.AbstractModel): row_pos, 5, _("Open Amount"), FORMATS["format_theader_yellow_center"] ) sheet.write(row_pos, 6, _("Balance"), FORMATS["format_theader_yellow_center"]) + format_tcell_left = FORMATS["format_tcell_left"] + format_tcell_date_left = FORMATS["format_tcell_date_left"] + format_distributed = FORMATS["format_distributed"] + current_money_format = FORMATS["current_money_format"] for line in currency_data.get("lines"): + if line.get("blocked"): + format_tcell_left = FORMATS["format_tcell_left_blocked"] + format_tcell_date_left = FORMATS["format_tcell_date_left_blocked"] + format_distributed = FORMATS["format_distributed_blocked"] + current_money_format = FORMATS["current_money_format_blocked"] row_pos += 1 name_to_show = ( line.get("name", "") == "/" or not line.get("name", "") ) and line.get("ref", "") - if line.get("name", "") != "/": + if line.get("name", "") and line.get("name", "") != "/": if not line.get("ref", ""): name_to_show = line.get("name", "") else: @@ -64,28 +85,18 @@ class OutstandingStatementXslx(models.AbstractModel): name_to_show = line.get("name", "") else: name_to_show = line.get("ref", "") - sheet.write( - row_pos, 0, line.get("move_id", ""), FORMATS["format_tcell_left"] - ) - sheet.write( - row_pos, 1, line.get("date", ""), FORMATS["format_tcell_date_left"] - ) + sheet.write(row_pos, 0, line.get("move_id", ""), format_tcell_left) + sheet.write(row_pos, 1, line.get("date", ""), format_tcell_date_left) sheet.write( row_pos, 2, line.get("date_maturity", ""), - FORMATS["format_tcell_date_left"], - ) - sheet.write(row_pos, 3, name_to_show, FORMATS["format_distributed"]) - sheet.write( - row_pos, 4, line.get("amount", ""), FORMATS["current_money_format"] - ) - sheet.write( - row_pos, 5, line.get("open_amount", ""), FORMATS["current_money_format"] - ) - sheet.write( - row_pos, 6, line.get("balance", ""), FORMATS["current_money_format"] + format_tcell_date_left, ) + sheet.write(row_pos, 3, name_to_show, format_distributed) + sheet.write(row_pos, 4, line.get("amount", ""), current_money_format) + sheet.write(row_pos, 5, line.get("open_amount", ""), current_money_format) + sheet.write(row_pos, 6, line.get("balance", ""), current_money_format) row_pos += 1 sheet.write( row_pos, 1, partner_data.get("end"), FORMATS["format_tcell_date_left"] @@ -169,7 +180,7 @@ class OutstandingStatementXslx(models.AbstractModel): ) return row_pos - def _size_columns(self, sheet): + def _size_columns(self, sheet, data): for i in range(7): sheet.set_column(0, i, 20) @@ -192,7 +203,7 @@ class OutstandingStatementXslx(models.AbstractModel): 0, row_pos, 6, - _("Statement of Account from %s") % (company.display_name), + _("Statement of Account from %s") % (company.display_name,), FORMATS["format_ws_title"], ) row_pos += 1 @@ -200,10 +211,10 @@ class OutstandingStatementXslx(models.AbstractModel): sheet.write( row_pos, 2, - fields.Date.from_string(data.get("date_end")), + data.get("data", {}).get(partners.ids[0], {}).get("today"), FORMATS["format_date_left"], ) - self._size_columns(sheet) + self._size_columns(sheet, data) for partner in partners: invoice_address = data.get( "get_inv_addr", lambda x: self.env["res.partner"] @@ -275,6 +286,23 @@ class OutstandingStatementXslx(models.AbstractModel): FORMATS["current_money_format"] = workbook.add_format( {"align": "right", "num_format": money_string} ) + bg_grey = "#CCCCCC" + FORMATS["format_tcell_left_blocked"] = copy_format( + workbook, FORMATS["format_tcell_left"] + ) + FORMATS["format_tcell_left_blocked"].set_bg_color(bg_grey) + FORMATS["format_tcell_date_left_blocked"] = copy_format( + workbook, FORMATS["format_tcell_date_left"] + ) + FORMATS["format_tcell_date_left_blocked"].set_bg_color(bg_grey) + FORMATS["format_distributed_blocked"] = copy_format( + workbook, FORMATS["format_distributed"] + ) + FORMATS["format_distributed_blocked"].set_bg_color(bg_grey) + FORMATS["current_money_format_blocked"] = copy_format( + workbook, FORMATS["current_money_format"] + ) + FORMATS["current_money_format_blocked"].set_bg_color(bg_grey) row_pos = self._write_currency_lines( row_pos, sheet, partner, currency, data ) diff --git a/partner_statement/report/report_statement_common.py b/partner_statement/report/report_statement_common.py index f4db9028..297bf9fc 100644 --- a/partner_statement/report/report_statement_common.py +++ b/partner_statement/report/report_statement_common.py @@ -34,6 +34,21 @@ class ReportStatementCommon(models.AbstractModel): ): return {} + def _get_account_display_prior_lines( + self, company_id, partner_ids, date_start, date_end, account_type + ): + return {} + + def _get_account_display_reconciled_lines( + self, company_id, partner_ids, date_start, date_end, account_type + ): + return {} + + def _get_account_display_ending_lines( + self, company_id, partner_ids, date_start, date_end, account_type + ): + return {} + def _show_buckets_sql_q1(self, partners, date_end, account_type): return str( self._cr.mogrify( @@ -273,16 +288,21 @@ class ReportStatementCommon(models.AbstractModel): _("Total"), ] - def _get_line_currency_defaults(self, currency_id, currencies, balance_forward): + def _get_line_currency_defaults( + self, currency_id, currencies, balance_forward, amount_due + ): if currency_id not in currencies: # This will only happen if currency is inactive currencies[currency_id] = self.env["res.currency"].browse(currency_id) return ( { + "prior_lines": [], "lines": [], + "ending_lines": [], "buckets": [], "balance_forward": balance_forward, - "amount_due": balance_forward, + "amount_due": amount_due, + "ending_balance": 0.0, }, currencies, ) @@ -290,6 +310,12 @@ class ReportStatementCommon(models.AbstractModel): def _add_currency_line(self, line, currency): return [line] + def _add_currency_prior_line(self, line, currency): + return [line] + + def _add_currency_ending_line(self, line, currency): + return [line] + @api.model def _get_report_values(self, docids, data=None): # flake8: noqa: C901 @@ -325,6 +351,8 @@ class ReportStatementCommon(models.AbstractModel): date_end = datetime.strptime(date_end, DEFAULT_SERVER_DATE_FORMAT).date() account_type = data["account_type"] aging_type = data["aging_type"] + is_activity = data.get("is_activity") + is_detailed = data.get("is_detailed") today = fields.Date.today() amount_field = data.get("amount_field", "amount") @@ -344,9 +372,31 @@ class ReportStatementCommon(models.AbstractModel): res = {} # get base data + prior_day = date_start - timedelta(days=1) if date_start else None + prior_lines = ( + self._get_account_display_prior_lines( + company_id, partner_ids, prior_day, prior_day, account_type + ) + if is_detailed + else {} + ) lines = self._get_account_display_lines( company_id, partner_ids, date_start, date_end, account_type ) + ending_lines = ( + self._get_account_display_ending_lines( + company_id, partner_ids, date_start, date_end, account_type + ) + if is_detailed + else {} + ) + reconciled_lines = ( + self._get_account_display_reconciled_lines( + company_id, partner_ids, date_start, date_end, account_type + ) + if is_activity + else {} + ) balances_forward = self._get_account_initial_balance( company_id, partner_ids, date_start, account_type ) @@ -359,7 +409,7 @@ class ReportStatementCommon(models.AbstractModel): else: bucket_labels = {} - # organise and format for report + # organize and format for report format_date = self._format_date_to_partner_lang partners_to_remove = set() for partner_id in partner_ids: @@ -369,6 +419,9 @@ class ReportStatementCommon(models.AbstractModel): date_start, date_formats.get(partner_id, default_fmt) ), "end": format_date(date_end, date_formats.get(partner_id, default_fmt)), + "prior_day": format_date( + prior_day, date_formats.get(partner_id, default_fmt) + ), "currencies": {}, } currency_dict = res[partner_id]["currencies"] @@ -378,7 +431,32 @@ class ReportStatementCommon(models.AbstractModel): currency_dict[line["currency_id"]], currencies, ) = self._get_line_currency_defaults( - line["currency_id"], currencies, line["balance"] + line["currency_id"], + currencies, + line["balance"], + 0.0 if is_detailed else line["balance"], + ) + + for line in prior_lines.get(partner_id, []): + if line["currency_id"] not in currency_dict: + ( + currency_dict[line["currency_id"]], + currencies, + ) = self._get_line_currency_defaults( + line["currency_id"], currencies, 0.0, 0.0 + ) + line_currency = currency_dict[line["currency_id"]] + if not line["blocked"]: + line_currency["amount_due"] += line["open_amount"] + line["balance"] = line_currency["amount_due"] + line["date"] = format_date( + line["date"], date_formats.get(partner_id, default_fmt) + ) + line["date_maturity"] = format_date( + line["date_maturity"], date_formats.get(partner_id, default_fmt) + ) + line_currency["prior_lines"].extend( + self._add_currency_prior_line(line, currencies[line["currency_id"]]) ) for line in lines[partner_id]: @@ -387,11 +465,59 @@ class ReportStatementCommon(models.AbstractModel): currency_dict[line["currency_id"]], currencies, ) = self._get_line_currency_defaults( - line["currency_id"], currencies, 0.0 + line["currency_id"], currencies, 0.0, 0.0 ) line_currency = currency_dict[line["currency_id"]] if not line["blocked"]: - line_currency["amount_due"] += line[amount_field] + if not is_activity: + line_currency["amount_due"] += line[amount_field] + line["balance"] = line_currency["amount_due"] + else: + line_currency["ending_balance"] += line[amount_field] + line["balance"] = line_currency["ending_balance"] + line["date"] = format_date( + line["date"], date_formats.get(partner_id, default_fmt) + ) + line["date_maturity"] = format_date( + line["date_maturity"], date_formats.get(partner_id, default_fmt) + ) + line["reconciled_line"] = False + if is_activity: + line["open_amount"] = 0.0 + line["applied_amount"] = 0.0 + line_currency["lines"].extend( + self._add_currency_line(line, currencies[line["currency_id"]]) + ) + for line2 in reconciled_lines: + if line2["id"] in line["ids"]: + line2["date"] = format_date( + line2["date"], date_formats.get(partner_id, default_fmt) + ) + line2["date_maturity"] = format_date( + line2["date_maturity"], + date_formats.get(partner_id, default_fmt), + ) + line2["reconciled_line"] = True + line2["applied_amount"] = line2["open_amount"] + line["applied_amount"] += line2["open_amount"] + if is_detailed: + line_currency["lines"].extend( + self._add_currency_line( + line2, currencies[line["currency_id"]] + ) + ) + if is_activity: + line["open_amount"] = line["amount"] + line["applied_amount"] + line_currency["amount_due"] += line["open_amount"] + + if is_detailed: + for line_currency in currency_dict.values(): + line_currency["amount_due"] = 0.0 + + for line in ending_lines.get(partner_id, []): + line_currency = currency_dict[line["currency_id"]] + if not line["blocked"]: + line_currency["amount_due"] += line["open_amount"] line["balance"] = line_currency["amount_due"] line["date"] = format_date( line["date"], date_formats.get(partner_id, default_fmt) @@ -399,8 +525,10 @@ class ReportStatementCommon(models.AbstractModel): line["date_maturity"] = format_date( line["date_maturity"], date_formats.get(partner_id, default_fmt) ) - line_currency["lines"].extend( - self._add_currency_line(line, currencies[line["currency_id"]]) + line_currency["ending_lines"].extend( + self._add_currency_ending_line( + line, currencies[line["currency_id"]] + ) ) if data["show_aging_buckets"]: @@ -410,7 +538,7 @@ class ReportStatementCommon(models.AbstractModel): currency_dict[line["currency_id"]], currencies, ) = self._get_line_currency_defaults( - line["currency_id"], currencies, 0.0 + line["currency_id"], currencies, 0.0, 0.0 ) line_currency = currency_dict[line["currency_id"]] line_currency["buckets"] = line @@ -439,6 +567,7 @@ class ReportStatementCommon(models.AbstractModel): "company": self.env["res.company"].browse(company_id), "Currencies": currencies, "account_type": account_type, + "is_detailed": is_detailed, "bucket_labels": bucket_labels, "get_inv_addr": self._get_invoice_address, } diff --git a/partner_statement/security/ir.model.access.csv b/partner_statement/security/ir.model.access.csv index f3a19dcc..18c24142 100644 --- a/partner_statement/security/ir.model.access.csv +++ b/partner_statement/security/ir.model.access.csv @@ -1,3 +1,4 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_activity_statement_wizard,access_activity_statement_wizard,model_activity_statement_wizard,account.group_account_invoice,1,1,1,0 access_outstanding_statement_wizard,access_outstanding_statement_wizard,model_outstanding_statement_wizard,account.group_account_invoice,1,1,1,0 +access_detailed_activity_statement_wizard,access_detailed_activity_statement_wizard,model_detailed_activity_statement_wizard,account.group_account_invoice,1,1,1,0 diff --git a/partner_statement/static/src/scss/layout_statement.scss b/partner_statement/static/src/scss/layout_statement.scss index b5851829..c0c84e0d 100644 --- a/partner_statement/static/src/scss/layout_statement.scss +++ b/partner_statement/static/src/scss/layout_statement.scss @@ -20,3 +20,11 @@ background-color: $gray-500 !important; } } + +.statement-reconciled { + font-size: smaller; + font-style: italic !important; + td:last-child { + font-style: italic !important; + } +} diff --git a/partner_statement/views/activity_statement.xml b/partner_statement/views/activity_statement.xml index 2c900223..c8c371f7 100644 --- a/partner_statement/views/activity_statement.xml +++ b/partner_statement/views/activity_statement.xml @@ -2,6 +2,119 @@ +