From a050ce6221a48a33cf6904b2d6101fd86dcb3dcf Mon Sep 17 00:00:00 2001 From: Marc Durepos Date: Tue, 28 May 2024 14:29:09 -0400 Subject: [PATCH] add a new module for aged partner balance reports, NA style --- aged_partner_balance_na/__init__.py | 10 + aged_partner_balance_na/__manifest__.py | 32 +++ aged_partner_balance_na/models/__init__.py | 1 + .../models/aged_partner_balance.py | 242 ++++++++++++++++++ .../wizard/choose_delivery_package_views.xml | 8 +- multi_account_statement_import/__init__.py | 0 .../__manifest__.py | 32 +++ 7 files changed, 323 insertions(+), 2 deletions(-) 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 create mode 100644 multi_account_statement_import/__init__.py create mode 100644 multi_account_statement_import/__manifest__.py diff --git a/aged_partner_balance_na/__init__.py b/aged_partner_balance_na/__init__.py new file mode 100644 index 0000000..e63df36 --- /dev/null +++ b/aged_partner_balance_na/__init__.py @@ -0,0 +1,10 @@ +from . import models + + +def post_init(env): + 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'}) diff --git a/aged_partner_balance_na/__manifest__.py b/aged_partner_balance_na/__manifest__.py new file mode 100644 index 0000000..02c7e15 --- /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': '17.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..5ad1911 --- /dev/null +++ b/aged_partner_balance_na/models/aged_partner_balance.py @@ -0,0 +1,242 @@ +from odoo import models, fields +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)) + + date_to = fields.Date.from_string(options['date']['date_to']) + periods = [ + (False, fields.Date.to_string(date_to)), + (date_to, plus_days(date_to, 29)), + (plus_days(date_to, 30), plus_days(date_to, 59)), + (plus_days(date_to, 60), plus_days(date_to, 89)), + (plus_days(date_to, 90), plus_days(date_to, 119)), + (plus_days(date_to, 120), False), + ] + + 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 + expected_date = len(query_res['expected_date']) == 1 and query_res['expected_date'][0] or len(query_res['due_date']) == 1 and query_res['due_date'][0] + 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, + 'expected_date': expected_date or 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, + 'expected_date': None, + 'total': sum(rslt[f'period{i}'] for i in range(len(periods))), + 'has_sublines': False, + }) + + return rslt + + # Build period table + 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 = self.env.cr.mogrify(period_table_format, params).decode(self.env.cr.connection.encoding) + + # Build query + tables, where_clause, where_params = report._query_get(options, 'strict_range', domain=[('account_id.account_type', '=', internal_type)]) + + currency_table = report._get_query_currency_table(options) + always_present_groupby = "period_table.period_index, currency_table.rate, currency_table.precision" + if current_groupby: + select_from_groupby = f"account_move_line.{current_groupby} AS grouping_key," + groupby_clause = f"account_move_line.{current_groupby}, {always_present_groupby}" + else: + select_from_groupby = '' + groupby_clause = always_present_groupby + select_period_query = ','.join( + f""" + CASE WHEN period_table.period_index = {i} + THEN %s * ( + SUM(ROUND(account_move_line.balance * currency_table.rate, currency_table.precision)) + - COALESCE(SUM(ROUND(part_debit.amount * currency_table.rate, currency_table.precision)), 0) + + COALESCE(SUM(ROUND(part_credit.amount * currency_table.rate, currency_table.precision)), 0) + ) + ELSE 5 END AS period{i} + """ + for i in range(len(periods)) + ) + + tail_query, tail_params = report._get_engine_query_tail(offset, limit) + query = f""" + WITH period_table(date_start, date_stop, period_index) AS ({period_table}) + + SELECT + {select_from_groupby} + %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 move.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_move_line.expected_pay_date) AS expected_date, + ARRAY_AGG(DISTINCT account.code) 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) AS account_code, + {select_period_query} + + FROM {tables} + + JOIN account_journal journal ON journal.id = account_move_line.journal_id + JOIN account_account account ON account.id = account_move_line.account_id + JOIN account_move move ON move.id = account_move_line.move_id + JOIN {currency_table} ON currency_table.company_id = account_move_line.company_id + + 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 <= %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 <= %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 {where_clause} + + GROUP BY {groupby_clause} + + HAVING + ( + SUM(ROUND(account_move_line.debit * currency_table.rate, currency_table.precision)) + - COALESCE(SUM(ROUND(part_debit.amount * currency_table.rate, currency_table.precision)), 0) + ) != 0 + OR + ( + SUM(ROUND(account_move_line.credit * currency_table.rate, currency_table.precision)) + - COALESCE(SUM(ROUND(part_credit.amount * currency_table.rate, currency_table.precision)), 0) + ) != 0 + {tail_query} + """ + + multiplicator = -1 if internal_type == 'liability_payable' else 1 + params = [ + multiplicator, + *([multiplicator] * len(periods)), + date_to, + date_to, + *where_params, + *tail_params, + ] + self._cr.execute(query, params) + 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 diff --git a/bemade_packing_wizard/wizard/choose_delivery_package_views.xml b/bemade_packing_wizard/wizard/choose_delivery_package_views.xml index 1b85781..97cd641 100644 --- a/bemade_packing_wizard/wizard/choose_delivery_package_views.xml +++ b/bemade_packing_wizard/wizard/choose_delivery_package_views.xml @@ -7,10 +7,14 @@ - + - + + + diff --git a/multi_account_statement_import/__init__.py b/multi_account_statement_import/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/multi_account_statement_import/__manifest__.py b/multi_account_statement_import/__manifest__.py new file mode 100644 index 0000000..1a516b2 --- /dev/null +++ b/multi_account_statement_import/__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': 'Multi Account Statement Import', + 'version': '17.0.0.1.0', + 'summary': 'Import Bank Statements for Multiple Accounts', + 'category': 'Accounting/Accounting', + 'author': 'Bemade Inc.', + 'website': 'http://www.bemade.org', + 'license': 'LGPL-3', + 'depends': ['account_bank_statement_import'], + 'data': [], + 'assets': {}, + 'installable': True, + 'auto_install': False, +}