From e1a64337ff69ca0bda48570f53a960b7b3ebf689 Mon Sep 17 00:00:00 2001 From: Marc Durepos Date: Wed, 10 Sep 2025 10:02:10 -0400 Subject: [PATCH] [MIG] aged_partner_balance_na to 18.0 --- aged_partner_balance_na/__init__.py | 33 +++ aged_partner_balance_na/__manifest__.py | 32 +++ aged_partner_balance_na/models/__init__.py | 1 + .../models/aged_partner_balance.py | 251 ++++++++++++++++++ 4 files changed, 317 insertions(+) create mode 100644 aged_partner_balance_na/__init__.py create mode 100644 aged_partner_balance_na/__manifest__.py create mode 100644 aged_partner_balance_na/models/__init__.py create mode 100644 aged_partner_balance_na/models/aged_partner_balance.py diff --git a/aged_partner_balance_na/__init__.py b/aged_partner_balance_na/__init__.py new file mode 100644 index 0000000..f9f04df --- /dev/null +++ b/aged_partner_balance_na/__init__.py @@ -0,0 +1,33 @@ +from . import models + + +def post_init(env): + env['account.report'].search([('name', 'ilike', 'Aged % - North America')]).unlink() + new_receivable_report = env.ref('account_reports.aged_receivable_report').copy() + new_payable_report = env.ref('account_reports.aged_payable_report').copy() + new_receivable_report.line_ids.mapped('expression_ids').write({ + 'formula': '_report_custom_engine_aged_receivable_na' + }) + new_payable_report.line_ids.mapped('expression_ids').write({'formula': '_report_custom_engine_aged_payable_na'}) + new_receivable_report.write({ + 'name': 'Aged Receivable - North America', + 'root_report_id': env.ref('account_reports.aged_receivable_report').id, + 'availability_condition': 'always', + }) + new_payable_report.write({ + 'name': 'Aged Payable - North America', + 'root_report_id': env.ref('account_reports.aged_payable_report').id, + 'availability_condition': 'always', + }) + col_name_mapping = [ + ('At Date', 'Overdue'), + ('1-30', '0-29'), + ('31-60', '30-59'), + ('61-90', '60-89'), + ('91-120', '90-119'), + ('Older', 'Later'), + ] + for old_name, new_name in col_name_mapping: + ((new_payable_report | new_receivable_report) + .mapped('column_ids') + .filtered(lambda col: col.name == old_name)).write({'name': new_name}) diff --git a/aged_partner_balance_na/__manifest__.py b/aged_partner_balance_na/__manifest__.py new file mode 100644 index 0000000..7e6bcdb --- /dev/null +++ b/aged_partner_balance_na/__manifest__.py @@ -0,0 +1,32 @@ +# +# Bemade Inc. +# +# Copyright (C) 2023-June Bemade Inc. (). +# Author: Marc Durepos (Contact : marc@bemade.org) +# +# This program is under the terms of the Odoo Proprietary License v1.0 (OPL-1) +# It is forbidden to publish, distribute, sublicense, or sell copies of the Software +# or modified copies of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# +{ + "name": "Aged Partner Balance (North American Style)", + "version": "18.0.1.0.0", + "summary": "Present aged partner balance as predictive rather than past due.", + "category": "Accounting", + "author": "Bemade Inc.", + "website": "http://www.bemade.org", + "license": "LGPL-3", + "depends": ["account_reports"], + "assets": {}, + "installable": True, + "auto_install": False, + "post_init_hook": "post_init", +} diff --git a/aged_partner_balance_na/models/__init__.py b/aged_partner_balance_na/models/__init__.py new file mode 100644 index 0000000..06ce624 --- /dev/null +++ b/aged_partner_balance_na/models/__init__.py @@ -0,0 +1 @@ +from . import aged_partner_balance \ No newline at end of file diff --git a/aged_partner_balance_na/models/aged_partner_balance.py b/aged_partner_balance_na/models/aged_partner_balance.py new file mode 100644 index 0000000..571328c --- /dev/null +++ b/aged_partner_balance_na/models/aged_partner_balance.py @@ -0,0 +1,251 @@ +from odoo import models, fields +from odoo.tools import SQL +from itertools import chain +from dateutil.relativedelta import relativedelta + + +class AgedPartnerBalanceCustomHandler(models.AbstractModel): + _inherit = "account.aged.partner.balance.report.handler" + + def _report_custom_engine_aged_receivable_na( + self, + expressions, + options, + date_scope, + current_groupby, + next_groupby, + offset=0, + limit=None, + warnings=None + ): + return self._aged_partner_report_custom_engine_common_na( + options, + 'asset_receivable', + current_groupby, + next_groupby, + offset=offset, + limit=limit + ) + + def _report_custom_engine_aged_payable_na( + self, + expressions, + options, + date_scope, + current_groupby, + next_groupby, + offset=0, + limit=None, + warnings=None + ): + return self._aged_partner_report_custom_engine_common_na( + options, + 'liability_payable', + current_groupby, + next_groupby, + offset=offset, + limit=limit + ) + + def _aged_partner_report_custom_engine_common_na(self, options, internal_type, current_groupby, next_groupby, offset=0, limit=None): + report = self.env['account.report'].browse(options['report_id']) + report._check_groupby_fields((next_groupby.split(',') if next_groupby else []) + ([current_groupby] if current_groupby else [])) + + def plus_days(date_obj, days): + return fields.Date.to_string(date_obj + relativedelta(days=days)) + + def minus_days(date_obj, days): + return fields.Date.to_string(date_obj - relativedelta(days=days)) + + date_to = fields.Date.from_string(options['date']['date_to']) + # North American style: show future due dates instead of past due + periods = [ + (False, minus_days(date_to, 1)), # Overdue + (date_to, plus_days(date_to, 29)), # 0-29 days + (plus_days(date_to, 30), plus_days(date_to, 59)), # 30-59 days + (plus_days(date_to, 60), plus_days(date_to, 89)), # 60-89 days + (plus_days(date_to, 90), plus_days(date_to, 119)), # 90-119 days + (plus_days(date_to, 120), False), # 120+ days + ] + + def build_result_dict(report, query_res_lines): + rslt = {f'period{i}': 0 for i in range(len(periods))} + + for query_res in query_res_lines: + for i in range(len(periods)): + period_key = f'period{i}' + rslt[period_key] += query_res[period_key] + + if current_groupby == 'id': + query_res = query_res_lines[0] # We're grouping by id, so there is only 1 element in query_res_lines anyway + currency = self.env['res.currency'].browse(query_res['currency_id'][0]) if len(query_res['currency_id']) == 1 else None + rslt.update({ + 'invoice_date': query_res['invoice_date'][0] if len(query_res['invoice_date']) == 1 else None, + 'due_date': query_res['due_date'][0] if len(query_res['due_date']) == 1 else None, + 'amount_currency': query_res['amount_currency'], + 'currency_id': query_res['currency_id'][0] if len(query_res['currency_id']) == 1 else None, + 'currency': currency.display_name if currency else None, + 'account_name': query_res['account_name'][0] if len(query_res['account_name']) == 1 else None, + 'total': None, + 'has_sublines': query_res['aml_count'] > 0, + + # Needed by the custom_unfold_all_batch_data_generator, to speed-up unfold_all + 'partner_id': query_res['partner_id'][0] if query_res['partner_id'] else None, + }) + else: + rslt.update({ + 'invoice_date': None, + 'due_date': None, + 'amount_currency': None, + 'currency_id': None, + 'currency': None, + 'account_name': None, + 'total': sum(rslt[f'period{i}'] for i in range(len(periods))), + 'has_sublines': False, + }) + + return rslt + + # Build period table using SQL class + period_table_format = ('(VALUES %s)' % ','.join("(%s, %s, %s)" for period in periods)) + params = list(chain.from_iterable( + (period[0] or None, period[1] or None, i) + for i, period in enumerate(periods) + )) + period_table = SQL(period_table_format, *params) + + # Build query using new Odoo 18.0 methods + query = report._get_report_query(options, 'strict_range', domain=[('account_id.account_type', '=', internal_type)]) + account_alias = query.left_join(lhs_alias='account_move_line', lhs_column='account_id', rhs_table='account_account', rhs_column='id', link='account_id') + account_code = self.env['account.account']._field_to_sql(account_alias, 'code', query) + + always_present_groupby = SQL("period_table.period_index") + if current_groupby: + groupby_field_sql = self.env['account.move.line']._field_to_sql("account_move_line", current_groupby, query) + select_from_groupby = SQL("%s AS grouping_key,", groupby_field_sql) + groupby_clause = SQL("%s, %s", groupby_field_sql, always_present_groupby) + else: + select_from_groupby = SQL() + groupby_clause = always_present_groupby + + multiplicator = -1 if internal_type == 'liability_payable' else 1 + select_period_query = SQL(',').join( + SQL(""" + CASE WHEN period_table.period_index = %(period_index)s + THEN %(multiplicator)s * SUM(%(balance_select)s) + ELSE 0 END AS %(column_name)s + """, + period_index=i, + multiplicator=multiplicator, + column_name=SQL.identifier(f"period{i}"), + balance_select=report._currency_table_apply_rate(SQL( + "account_move_line.balance - COALESCE(part_debit.amount, 0) + COALESCE(part_credit.amount, 0)" + )), + ) + for i in range(len(periods)) + ) + + tail_query = report._get_engine_query_tail(offset, limit) + query = SQL( + """ + WITH period_table(date_start, date_stop, period_index) AS (%(period_table)s) + + SELECT + %(select_from_groupby)s + %(multiplicator)s * ( + SUM(account_move_line.amount_currency) + - COALESCE(SUM(part_debit.debit_amount_currency), 0) + + COALESCE(SUM(part_credit.credit_amount_currency), 0) + ) AS amount_currency, + ARRAY_AGG(DISTINCT account_move_line.partner_id) AS partner_id, + ARRAY_AGG(account_move_line.payment_id) AS payment_id, + ARRAY_AGG(DISTINCT account_move_line.invoice_date) AS invoice_date, + ARRAY_AGG(DISTINCT COALESCE(account_move_line.date_maturity, account_move_line.date)) AS report_date, + ARRAY_AGG(DISTINCT %(account_code)s) AS account_name, + ARRAY_AGG(DISTINCT COALESCE(account_move_line.date_maturity, account_move_line.date)) AS due_date, + ARRAY_AGG(DISTINCT account_move_line.currency_id) AS currency_id, + COUNT(account_move_line.id) AS aml_count, + ARRAY_AGG(%(account_code)s) AS account_code, + %(select_period_query)s + + FROM %(table_references)s + + JOIN account_journal journal ON journal.id = account_move_line.journal_id + %(currency_table_join)s + + LEFT JOIN LATERAL ( + SELECT + SUM(part.amount) AS amount, + SUM(part.debit_amount_currency) AS debit_amount_currency, + part.debit_move_id + FROM account_partial_reconcile part + WHERE part.max_date <= %(date_to)s AND part.debit_move_id = account_move_line.id + GROUP BY part.debit_move_id + ) part_debit ON TRUE + + LEFT JOIN LATERAL ( + SELECT + SUM(part.amount) AS amount, + SUM(part.credit_amount_currency) AS credit_amount_currency, + part.credit_move_id + FROM account_partial_reconcile part + WHERE part.max_date <= %(date_to)s AND part.credit_move_id = account_move_line.id + GROUP BY part.credit_move_id + ) part_credit ON TRUE + + JOIN period_table ON + ( + period_table.date_start IS NULL + OR COALESCE(account_move_line.date_maturity, account_move_line.date) >= DATE(period_table.date_start) + ) + AND + ( + period_table.date_stop IS NULL + OR COALESCE(account_move_line.date_maturity, account_move_line.date) <= DATE(period_table.date_stop) + ) + + WHERE %(search_condition)s + + GROUP BY %(groupby_clause)s + + HAVING + ROUND(SUM(%(having_debit)s), %(currency_precision)s) != 0 + OR ROUND(SUM(%(having_credit)s), %(currency_precision)s) != 0 + + ORDER BY %(groupby_clause)s + + %(tail_query)s + """, + account_code=account_code, + period_table=period_table, + select_from_groupby=select_from_groupby, + select_period_query=select_period_query, + multiplicator=multiplicator, + table_references=query.from_clause, + currency_table_join=report._currency_table_aml_join(options), + date_to=date_to, + search_condition=query.where_clause, + groupby_clause=groupby_clause, + having_debit=report._currency_table_apply_rate(SQL("CASE WHEN account_move_line.balance > 0 THEN account_move_line.balance else 0 END - COALESCE(part_debit.amount, 0)")), + having_credit=report._currency_table_apply_rate(SQL("CASE WHEN account_move_line.balance < 0 THEN -account_move_line.balance else 0 END - COALESCE(part_credit.amount, 0)")), + currency_precision=self.env.company.currency_id.decimal_places, + tail_query=tail_query, + ) + + self._cr.execute(query) + query_res_lines = self._cr.dictfetchall() + + if not current_groupby: + return build_result_dict(report, query_res_lines) + else: + rslt = [] + + all_res_per_grouping_key = {} + for query_res in query_res_lines: + grouping_key = query_res['grouping_key'] + all_res_per_grouping_key.setdefault(grouping_key, []).append(query_res) + + for grouping_key, query_res_lines in all_res_per_grouping_key.items(): + rslt.append((grouping_key, build_result_dict(report, query_res_lines))) + + return rslt