diff --git a/partner_statement/report/activity_statement.py b/partner_statement/report/activity_statement.py index be8fe29d..6bb391b3 100644 --- a/partner_statement/report/activity_statement.py +++ b/partner_statement/report/activity_statement.py @@ -52,6 +52,9 @@ class ActivityStatement(models.AbstractModel): return title def _initial_balance_sql_q1(self, partners, date_start, account_type): + excluded_accounts_ids = tuple( + self.env.context.get("excluded_accounts_ids", []) + ) or (-1,) return str( self._cr.mogrify( """ @@ -84,6 +87,7 @@ class ActivityStatement(models.AbstractModel): WHERE l2.date < %(date_start)s ) as pc ON pc.credit_move_id = l.id WHERE l.partner_id IN %(partners)s + AND aa.id not in %(excluded_accounts_ids)s AND l.date < %(date_start)s AND not l.blocked AND m.state IN ('posted') AND aa.account_type = %(account_type)s @@ -156,6 +160,9 @@ class ActivityStatement(models.AbstractModel): def _display_activity_lines_sql_q1( self, partners, date_start, date_end, account_type ): + excluded_accounts_ids = tuple( + self.env.context.get("excluded_accounts_ids", []) + ) or (-1,) payment_ref = _("Payment") return str( self._cr.mogrify( @@ -191,6 +198,7 @@ class ActivityStatement(models.AbstractModel): JOIN account_move m ON (l.move_id = m.id) JOIN account_journal aj ON (l.journal_id = aj.id) WHERE l.partner_id IN %(partners)s + AND aa.id not in %(excluded_accounts_ids)s AND %(date_start)s <= l.date AND l.date <= %(date_end)s AND m.state IN ('posted') diff --git a/partner_statement/report/outstanding_statement.py b/partner_statement/report/outstanding_statement.py index 987734ed..67b60f88 100644 --- a/partner_statement/report/outstanding_statement.py +++ b/partner_statement/report/outstanding_statement.py @@ -26,6 +26,9 @@ class OutstandingStatement(models.AbstractModel): def _display_outstanding_lines_sql_q1(self, partners, date_end, account_type): partners = tuple(partners) + excluded_accounts_ids = tuple( + self.env.context.get("excluded_accounts_ids", []) + ) or (-1,) return str( self._cr.mogrify( """ @@ -71,6 +74,7 @@ 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 aa.id not in %(excluded_accounts_ids)s AND ( (pd.id IS NOT NULL AND pd.max_date <= %(date_end)s) OR diff --git a/partner_statement/report/report_statement_common.py b/partner_statement/report/report_statement_common.py index 6ac95bb0..f70ad9d9 100644 --- a/partner_statement/report/report_statement_common.py +++ b/partner_statement/report/report_statement_common.py @@ -59,6 +59,9 @@ class ReportStatementCommon(models.AbstractModel): return {} def _show_buckets_sql_q1(self, partners, date_end, account_type): + excluded_accounts_ids = tuple( + self.env.context.get("excluded_accounts_ids", []) + ) or (-1,) return str( self._cr.mogrify( """ @@ -91,18 +94,18 @@ class ReportStatementCommon(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 ( - (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 not l.blocked - AND m.state IN ('posted') - AND aa.account_type = %(account_type)s + AND aa.id not in %(excluded_accounts_ids)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 not l.blocked + AND m.state IN ('posted') + AND aa.account_type = %(account_type)s GROUP BY l.partner_id, l.currency_id, l.date, l.date_maturity, - l.amount_currency, l.balance, l.move_id, - l.company_id, l.id + l.amount_currency, l.balance, l.move_id, l.company_id, l.id """, locals(), ), @@ -358,6 +361,11 @@ class ReportStatementCommon(models.AbstractModel): if isinstance(date_end, str): date_end = datetime.strptime(date_end, DEFAULT_SERVER_DATE_FORMAT).date() account_type = data["account_type"] + excluded_accounts_ids = data["excluded_accounts_ids"] + if excluded_accounts_ids: + self = self.with_context( + excluded_accounts_ids=excluded_accounts_ids, + ) aging_type = data["aging_type"] is_activity = data.get("is_activity") is_detailed = data.get("is_detailed") @@ -583,6 +591,7 @@ class ReportStatementCommon(models.AbstractModel): "company": self.env["res.company"].browse(company_id), "Currencies": currencies, "account_type": account_type, + "excluded_accounts_ids": excluded_accounts_ids, "is_detailed": is_detailed, "bucket_labels": bucket_labels, "get_inv_addr": self._get_invoice_address, diff --git a/partner_statement/tests/test_outstanding_statement.py b/partner_statement/tests/test_outstanding_statement.py index 582586ba..f68533e6 100644 --- a/partner_statement/tests/test_outstanding_statement.py +++ b/partner_statement/tests/test_outstanding_statement.py @@ -92,3 +92,59 @@ class TestOutstandingStatement(TransactionCase): self.assertIn( "bucket_labels", report, "There was an error while compiling the report." ) + + def test_exclude_accounts(self): + """Accounts can be excluded with a code selector.""" + # Arrange + partners = self.partner1 | self.partner2 + wizard = self.wiz.with_context( + active_ids=partners.ids, + ).create({}) + + # Edit one invoice + # including a new account + # that will be the only one not excluded + partner_invoice = self.env["account.move"].search( + [ + ("partner_id", "in", partners.ids), + ("state", "=", "posted"), + ], + limit=1, + ) + account = partner_invoice.line_ids.account_id.filtered( + lambda a: a.account_type == wizard.account_type + ) + copy_account = account.copy() + partner_invoice.line_ids.filtered( + lambda line: line.account_id == account + ).account_id = copy_account + partner_invoice.line_ids.flush_recordset() + wizard_accounts = self.env["account.account"].search( + [ + ("id", "!=", copy_account.id), + ("account_type", "=", wizard.account_type), + ], + ) + wizard.excluded_accounts_selector = ", ".join( + [account.code for account in wizard_accounts] + ) + # pre-condition + self.assertTrue(wizard.excluded_accounts_selector) + + # Act + data = wizard._prepare_statement() + report = self.statement_model._get_report_values(partners.ids, data) + + # Assert + # Only the new invoice is shown + invoice_partner = partner_invoice.partner_id + invoice_partner_data = report["data"][invoice_partner.id]["currencies"] + invoice_partner_move_lines = invoice_partner_data[ + partner_invoice.currency_id.id + ]["lines"] + self.assertEqual(len(invoice_partner_move_lines), 1) + self.assertEqual(invoice_partner_move_lines[0]["name"], partner_invoice.name) + + other_partner = partners - invoice_partner + other_partner_data = report["data"].get(other_partner.id) + self.assertFalse(other_partner_data) diff --git a/partner_statement/wizard/statement_common.py b/partner_statement/wizard/statement_common.py index 37d23909..77ff6325 100644 --- a/partner_statement/wizard/statement_common.py +++ b/partner_statement/wizard/statement_common.py @@ -4,6 +4,7 @@ from dateutil.relativedelta import relativedelta from odoo import api, fields, models +from odoo.osv import expression class StatementCommon(models.AbstractModel): @@ -38,6 +39,44 @@ class StatementCommon(models.AbstractModel): [("asset_receivable", "Receivable"), ("liability_payable", "Payable")], default="asset_receivable", ) + excluded_accounts_selector = fields.Char( + string="Accounts to exclude", + help="Select account codes to be excluded " + "with a comma-separated list of expressions like 70%.", + ) + + @api.model + def _get_excluded_accounts_domain(self, selector): + """Convert an account codes selector to a domain to search accounts. + + The selector is a comma-separated list of expressions like 70%. + The algorithm is the same as + AccountingExpressionProcessor._account_codes_to_domain + of `mis_builder` module. + """ + if not selector: + selector = "" + domains = [] + for account_code in selector.split(","): + account_code = account_code.strip() + if "%" in account_code: + domains.append( + [ + ("code", "=like", account_code), + ] + ) + else: + domains.append( + [ + ("code", "=", account_code), + ] + ) + return expression.OR(domains) + + def _get_excluded_accounts(self): + self.ensure_one() + domain = self._get_excluded_accounts_domain(self.excluded_accounts_selector) + return self.env["account.account"].search(domain) @api.onchange("aging_type") def onchange_aging_type(self): @@ -59,6 +98,7 @@ class StatementCommon(models.AbstractModel): "account_type": self.account_type, "aging_type": self.aging_type, "filter_negative_balances": self.filter_negative_balances, + "excluded_accounts_ids": self._get_excluded_accounts().ids, } def button_export_html(self): diff --git a/partner_statement/wizard/statement_wizard.xml b/partner_statement/wizard/statement_wizard.xml index c740e3d9..b97be724 100644 --- a/partner_statement/wizard/statement_wizard.xml +++ b/partner_statement/wizard/statement_wizard.xml @@ -63,6 +63,7 @@ />