diff --git a/partner_statement/__manifest__.py b/partner_statement/__manifest__.py index ec542e87..92df4103 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/assets.xml", "views/aging_buckets.xml", "views/res_config_settings.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..cb1f8b2b 100644 --- a/partner_statement/report/activity_statement.py +++ b/partner_statement/report/activity_statement.py @@ -75,11 +75,14 @@ class ActivityStatement(models.AbstractModel): 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 +95,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 @@ -116,37 +119,24 @@ class ActivityStatement(models.AbstractModel): AND %(date_start)s <= l.date AND l.date <= %(date_end)s AND m.state IN ('posted') - GROUP BY l.partner_id, m.name, l.date, l.date_maturity, - 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 + GROUP BY l.partner_id, m.name, l.date, l.date_maturity, l.name, + aj.type, 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(), @@ -166,14 +156,14 @@ 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 + ids, 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(): diff --git a/partner_statement/report/activity_statement_xlsx.py b/partner_statement/report/activity_statement_xlsx.py index 5f7cfa14..b142771b 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" @@ -26,17 +38,14 @@ class ActivityStatementXslx(models.AbstractModel): currency_data = partner_data.get("currencies", {}).get(currency.id) account_type = data.get("account_type", False) row_pos += 2 - statement_header = _( - "%sStatement between %s and %s in %s" - % ( - account_type == "payable" and _("Supplier ") or "", - partner_data.get("start"), - partner_data.get("end"), - currency.display_name, - ) + statement_header = _("%sStatement between %s and %s in %s") % ( + account_type == "payable" and _("Supplier ") or "", + partner_data.get("start"), + partner_data.get("end"), + currency.display_name, ) 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( @@ -51,13 +60,11 @@ class ActivityStatementXslx(models.AbstractModel): _("Description"), FORMATS["format_theader_yellow_center"], ) - sheet.write( - row_pos, 5, _("Open Amount"), FORMATS["format_theader_yellow_center"] - ) + sheet.write(row_pos, 5, _("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"] @@ -68,7 +75,16 @@ 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", "") @@ -83,21 +99,11 @@ 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", ""), 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"] - ) + 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, 4, name_to_show, format_distributed) + sheet.write(row_pos, 5, line.get("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"] @@ -180,7 +186,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 +209,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 +217,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 +292,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..91733b8a --- /dev/null +++ b/partner_statement/report/detailed_activity_statement.py @@ -0,0 +1,149 @@ +# 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 _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 + ): + res = dict(map(lambda x: (x, []), partner_ids)) + 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, name, 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), + ) + ) + for row in self.env.cr.dictfetchall(): + res[row.pop("partner_id")].append(row) + return res + + 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_reconciled_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._display_outstanding_lines_sql_q2 = ( + OutstandingStatement._display_outstanding_lines_sql_q2 +) +DetailedActivityStatement._display_outstanding_lines_sql_q3 = ( + OutstandingStatement._display_outstanding_lines_sql_q3 +) +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..c5ae0fb0 --- /dev/null +++ b/partner_statement/report/detailed_activity_statement_xlsx.py @@ -0,0 +1,585 @@ +# 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) + show_balance = data.get("show_balance", True) + row_pos += 2 + statement_header = _("Detailed %sStatement between %s and %s in %s") % ( + account_type == "payable" and _("Supplier ") or "", + partner_data.get("start"), + partner_data.get("end"), + currency.display_name, + ) + sheet.merge_range( + row_pos, + 0, + row_pos, + 7 if show_balance else 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"), 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"] + ) + if show_balance: + sheet.write( + row_pos, 7, _("Balance"), 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, + 6 if show_balance else 5, + _("Initial Balance"), + FORMATS["format_tcell_left"], + ) + sheet.write( + row_pos, + 7 if show_balance else 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", "") != "/": + 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, + ) + if show_balance: + sheet.write(row_pos, 7, 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, + 6 if show_balance else 5, + _("Ending Balance"), + FORMATS["format_tcell_left"], + ) + sheet.write( + row_pos, + 7 if show_balance else 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) + show_balance = data.get("show_balance", True) + row_pos += 2 + statement_header = _("%sStatement up to %s in %s") % ( + account_type == "payable" and _("Supplier ") or "", + partner_data.get("prior_day"), + currency.display_name, + ) + sheet.merge_range( + row_pos, + 0, + row_pos, + 7 if show_balance else 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.merge_range( + row_pos, + 3, + row_pos, + 4, + _("Description"), + FORMATS["format_theader_yellow_center"], + ) + sheet.write(row_pos, 5, _("Original"), FORMATS["format_theader_yellow_center"]) + sheet.write( + row_pos, 6, _("Open Amount"), FORMATS["format_theader_yellow_center"] + ) + if show_balance: + sheet.write( + row_pos, 7, _("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", "") != "/": + 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.merge_range(row_pos, 3, row_pos, 4, name_to_show, format_distributed) + sheet.write(row_pos, 5, line.get("amount", ""), current_money_format) + sheet.write(row_pos, 6, line.get("open_amount", ""), current_money_format) + if show_balance: + sheet.write(row_pos, 7, 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, + 6 if show_balance else 5, + _("Ending Balance"), + FORMATS["format_tcell_left"], + ) + sheet.write( + row_pos, + 7 if show_balance else 6, + currency_data.get("amount_due"), + 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) + show_balance = data.get("show_balance", True) + row_pos += 2 + statement_header = _("%sStatement up to %s in %s") % ( + account_type == "payable" and _("Supplier ") or "", + partner_data.get("end"), + currency.display_name, + ) + sheet.merge_range( + row_pos, + 0, + row_pos, + 7 if show_balance else 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.merge_range( + row_pos, + 3, + row_pos, + 4, + _("Description"), + FORMATS["format_theader_yellow_center"], + ) + sheet.write(row_pos, 5, _("Original"), FORMATS["format_theader_yellow_center"]) + sheet.write( + row_pos, 6, _("Open Amount"), FORMATS["format_theader_yellow_center"] + ) + if show_balance: + sheet.write( + row_pos, 7, _("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", "") != "/": + 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.merge_range(row_pos, 3, row_pos, 4, name_to_show, format_distributed) + sheet.write(row_pos, 5, line.get("amount", ""), current_money_format) + sheet.write(row_pos, 6, line.get("open_amount", ""), current_money_format) + if show_balance: + sheet.write(row_pos, 7, 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, + 6 if show_balance else 5, + _("Ending Balance"), + FORMATS["format_tcell_left"], + ) + sheet.write( + row_pos, + 7 if show_balance else 6, + currency_data.get("amount_due"), + FORMATS["current_money_format"], + ) + return row_pos + + def _size_columns(self, sheet, data): + show_balance = data.get("show_balance", True) + for i in range(8 if show_balance else 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)) + show_balance = data.get("show_balance", True) + 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, + 7 if show_balance else 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..d055b4dc 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 @@ -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, blocked, + COALESCE({sub}.currency_id, c.currency_id) AS currency_id, + {sub}.open_amount, {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,15 @@ 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, name, 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 9eea688c..e6cb41d6 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" @@ -26,16 +38,13 @@ class OutstandingStatementXslx(models.AbstractModel): currency_data = partner_data.get("currencies", {}).get(currency.id) account_type = data.get("account_type", False) row_pos += 2 - statement_header = _( - "%sStatement up to %s in %s" - % ( - account_type == "payable" and _("Supplier ") or "", - partner_data.get("end"), - currency.display_name, - ) + statement_header = _("%sStatement up to %s in %s") % ( + account_type == "payable" and _("Supplier ") or "", + partner_data.get("end"), + currency.display_name, ) 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( @@ -51,7 +60,16 @@ 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", "") @@ -66,28 +84,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"] @@ -170,7 +178,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) @@ -193,7 +201,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 @@ -201,10 +209,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"] @@ -276,6 +284,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..ed75483d 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,20 @@ 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, }, currencies, ) @@ -290,6 +309,15 @@ 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_reconciled_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 +353,9 @@ 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"] + show_balance = data.get("show_balance", True) + is_detailed = data.get("is_detailed") and data.get("is_activity") + # because detailed outstanding doesn't exist (yet) today = fields.Date.today() amount_field = data.get("amount_field", "amount") @@ -344,9 +375,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_detailed + else {} + ) balances_forward = self._get_account_initial_balance( company_id, partner_ids, date_start, account_type ) @@ -369,6 +422,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 +434,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,7 +468,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"]] if not line["blocked"]: @@ -399,9 +480,73 @@ class ReportStatementCommon(models.AbstractModel): line["date_maturity"] = format_date( line["date_maturity"], date_formats.get(partner_id, default_fmt) ) + line["reconciled_line"] = False + if is_detailed: + line["open_amount"] = 0.0 line_currency["lines"].extend( self._add_currency_line(line, currencies[line["currency_id"]]) ) + for line2 in reconciled_lines.get(partner_id, []): + 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_currency["lines"].extend( + self._add_currency_reconciled_line( + line2, currencies[line["currency_id"]] + ) + ) + + for line in ending_lines.get(partner_id, []): + line_currency = currency_dict[line["currency_id"]] + ending_balance = 0.0 + for line2 in prior_lines.get(partner_id, []): + if line["id"] == line2["id"]: + if not line["blocked"]: + ending_balance += line["open_amount"] + line["balance"] = 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_currency["ending_lines"].extend( + self._add_currency_ending_line( + line, currencies[line["currency_id"]] + ) + ) + for line in ending_lines.get(partner_id, []): + line_currency = currency_dict[line["currency_id"]] + for line2 in lines.get(partner_id, []): + if line2["reconciled_line"]: + continue + if line["id"] == line2["ids"][0]: + same_lines = [ + ln + for ln in ending_lines[partner_id] + if ln["id"] in set(line2["ids"]) + ] + line2["open_amount"] = sum( + ln["open_amount"] for ln in same_lines + ) + line_currency["ending_lines"].extend( + self._add_currency_ending_line( + line2, currencies[line["currency_id"]] + ) + ) + if is_detailed: + for line in lines[partner_id]: + if line["reconciled_line"]: + continue + line["applied_amount"] = line["open_amount"] - line["amount"] if data["show_aging_buckets"]: for line in buckets[partner_id]: @@ -410,7 +555,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 +584,8 @@ class ReportStatementCommon(models.AbstractModel): "company": self.env["res.company"].browse(company_id), "Currencies": currencies, "account_type": account_type, + "is_detailed": is_detailed, + "show_balance": show_balance, "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..6374fa4a 100644 --- a/partner_statement/views/activity_statement.xml +++ b/partner_statement/views/activity_statement.xml @@ -2,6 +2,131 @@ +