diff --git a/.docker_files/main/__manifest__.py b/.docker_files/main/__manifest__.py index 84e10da3..9d1eb650 100644 --- a/.docker_files/main/__manifest__.py +++ b/.docker_files/main/__manifest__.py @@ -40,6 +40,7 @@ "account_type_archive", "account_type_sane", "account_unaffected_earnings_disabled", + "aged_payables_receivables_foreign_currency", "bank_statement_import_csv", "bank_statement_extra_columns", "bank_statement_no_reverse", @@ -61,7 +62,7 @@ "old_accounts", "payment_list_not_sent", "payment_stripe_not_silenced", - "account_sale_invoice_date_required" + "account_sale_invoice_date_required", ], "installable": True, } diff --git a/Dockerfile b/Dockerfile index 83d3c81c..019023b7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -47,6 +47,7 @@ COPY account_show_full_features /mnt/extra-addons/account_show_full_features COPY account_type_archive /mnt/extra-addons/account_type_archive COPY account_type_sane /mnt/extra-addons/account_type_sane COPY account_unaffected_earnings_disabled /mnt/extra-addons/account_unaffected_earnings_disabled +COPY aged_payables_receivables_foreign_currency /mnt/extra-addons/aged_payables_receivables_foreign_currency COPY bank_statement_extra_columns /mnt/extra-addons/bank_statement_extra_columns COPY bank_statement_import_csv /mnt/extra-addons/bank_statement_import_csv COPY bank_statement_no_reverse /mnt/extra-addons/bank_statement_no_reverse diff --git a/aged_payables_receivables_foreign_currency/README.rst b/aged_payables_receivables_foreign_currency/README.rst new file mode 100644 index 00000000..17cb6997 --- /dev/null +++ b/aged_payables_receivables_foreign_currency/README.rst @@ -0,0 +1,14 @@ +========================================== +AGED PAYABLES RECEIVABLES FOREIGN CURRENCY +========================================== +This module helps to : +- View and print the aged balance of third parties with the amounts +in foreign currency (according to the currency of the receivable and payable account configured on the partner) +and in the functional currency of the company. +- View partners in alphabetical order + +Available in html, pdf and xlsx format. + +Contributors +------------ +* Numigi (tm) and all its contributors (https://bit.ly/numigiens) diff --git a/aged_payables_receivables_foreign_currency/__init__.py b/aged_payables_receivables_foreign_currency/__init__.py new file mode 100644 index 00000000..65bcd92f --- /dev/null +++ b/aged_payables_receivables_foreign_currency/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2024 Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import report diff --git a/aged_payables_receivables_foreign_currency/__manifest__.py b/aged_payables_receivables_foreign_currency/__manifest__.py new file mode 100644 index 00000000..351d2414 --- /dev/null +++ b/aged_payables_receivables_foreign_currency/__manifest__.py @@ -0,0 +1,18 @@ +# Copyright 2024 Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Aged Payables and Receivables in Foreign Currency", + "version": "1.0.0", + "author": "Numigi", + "maintainer": "Numigi", + "website": "https://bit.ly/numigi-com", + "license": "AGPL-3", + "category": "project", + "depends": ["account_financial_report"], + "summary": "Shows the original currency on receivable/payable account.", + "data": [ + "report/templates/aged_partner_balance.xml", + ], + "installable": True, +} diff --git a/aged_payables_receivables_foreign_currency/i18n/fr.po b/aged_payables_receivables_foreign_currency/i18n/fr.po new file mode 100644 index 00000000..ae364093 --- /dev/null +++ b/aged_payables_receivables_foreign_currency/i18n/fr.po @@ -0,0 +1,85 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * aged_payables_receivables_foreign_currency +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-11-21 03:09+0000\n" +"PO-Revision-Date: 2024-11-21 03:09+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: aged_payables_receivables_foreign_currency +#: model:ir.model,name:aged_payables_receivables_foreign_currency.model_report_account_financial_report_aged_partner_balance +msgid "Aged Partner Balance Report" +msgstr "Balance âgée des tiers" + +#. module: aged_payables_receivables_foreign_currency +#: model:ir.model,name:aged_payables_receivables_foreign_currency.model_report_a_f_r_report_aged_partner_balance_xlsx +msgid "Aged Partner Balance XLSL Report" +msgstr "Balance âgée des tiers XLSX" + +#. module: aged_payables_receivables_foreign_currency +#: code:addons/aged_payables_receivables_foreign_currency/report/aged_partner_balance_xlsx.py:0 +#: code:addons/aged_payables_receivables_foreign_currency/report/aged_partner_balance_xlsx.py:0 +#: model_terms:ir.ui.view,arch_db:aged_payables_receivables_foreign_currency.report_aged_partner_balance_lines_header +#: model_terms:ir.ui.view,arch_db:aged_payables_receivables_foreign_currency.report_aged_partner_balance_move_lines +#, python-format +msgid "Amount Currency" +msgstr "Montant en devise" + +#. module: aged_payables_receivables_foreign_currency +#: code:addons/aged_payables_receivables_foreign_currency/report/aged_partner_balance_xlsx.py:0 +#: code:addons/aged_payables_receivables_foreign_currency/report/aged_partner_balance_xlsx.py:0 +#: model_terms:ir.ui.view,arch_db:aged_payables_receivables_foreign_currency.report_aged_partner_balance_lines_header +#: model_terms:ir.ui.view,arch_db:aged_payables_receivables_foreign_currency.report_aged_partner_balance_move_lines +#, python-format +msgid "Currency" +msgstr "Devise" + +#. module: aged_payables_receivables_foreign_currency +#: model:ir.model.fields,field_description:aged_payables_receivables_foreign_currency.field_report_a_f_r_report_aged_partner_balance_xlsx__display_name +#: model:ir.model.fields,field_description:aged_payables_receivables_foreign_currency.field_report_account_financial_report_aged_partner_balance__display_name +msgid "Display Name" +msgstr "Nom affiché" + +#. module: aged_payables_receivables_foreign_currency +#: model_terms:ir.ui.view,arch_db:aged_payables_receivables_foreign_currency.report_aged_partner_balance_move_lines +msgid "" +"Due\n" +" date" +msgstr "" +"Échéance\n" +" date" + +#. module: aged_payables_receivables_foreign_currency +#: model:ir.model.fields,field_description:aged_payables_receivables_foreign_currency.field_report_a_f_r_report_aged_partner_balance_xlsx__id +#: model:ir.model.fields,field_description:aged_payables_receivables_foreign_currency.field_report_account_financial_report_aged_partner_balance__id +msgid "ID" +msgstr "" + +#. module: aged_payables_receivables_foreign_currency +#: model:ir.model.fields,field_description:aged_payables_receivables_foreign_currency.field_report_a_f_r_report_aged_partner_balance_xlsx____last_update +#: model:ir.model.fields,field_description:aged_payables_receivables_foreign_currency.field_report_account_financial_report_aged_partner_balance____last_update +msgid "Last Modified on" +msgstr "Dernière modification le" + +#. module: aged_payables_receivables_foreign_currency +#: model_terms:ir.ui.view,arch_db:aged_payables_receivables_foreign_currency.report_aged_partner_balance_lines_header +msgid "Partner" +msgstr "Partenaire" + +#. module: aged_payables_receivables_foreign_currency +#: model_terms:ir.ui.view,arch_db:aged_payables_receivables_foreign_currency.report_aged_partner_balance_move_lines +msgid "" +"Ref -\n" +" Label" +msgstr "" +"Ref -\n" +" Libellé" \ No newline at end of file diff --git a/aged_payables_receivables_foreign_currency/report/__init__.py b/aged_payables_receivables_foreign_currency/report/__init__.py new file mode 100644 index 00000000..879e1df8 --- /dev/null +++ b/aged_payables_receivables_foreign_currency/report/__init__.py @@ -0,0 +1,6 @@ +# Copyright 2024 Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import abstract_report_xlsx +from . import aged_partner_balance +from . import aged_partner_balance_xlsx \ No newline at end of file diff --git a/aged_payables_receivables_foreign_currency/report/abstract_report_xlsx.py b/aged_payables_receivables_foreign_currency/report/abstract_report_xlsx.py new file mode 100644 index 00000000..20867f04 --- /dev/null +++ b/aged_payables_receivables_foreign_currency/report/abstract_report_xlsx.py @@ -0,0 +1,71 @@ +# Copyright 2024 Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.addons.account_financial_report.report.abstract_report_xlsx import ( + AbstractReportXslx as AbstractReportXslxOriginal, +) + + +class AbstractReportXslx(AbstractReportXslxOriginal): + def write_line_from_dict(self, line_dict, report_data): + """Write a line on current line""" + for col_pos, column in report_data["columns"].items(): + value = line_dict.get(column["field"], False) + cell_type = column.get("type", "string") + + # If the column is a currency column, we need to get the currency name + if column["field"] == "currency_id": + if isinstance(value, tuple): + value = value[0] + currency = self.env["res.currency"].browse(value).name + line_dict["currency_id"] = value = currency + + if cell_type == "string": + if line_dict.get("type", "") == "group_type": + report_data["sheet"].write_string( + report_data["row_pos"], + col_pos, + value or "", + report_data["formats"]["format_bold"], + ) + else: + if ( + not isinstance(value, str) + and not isinstance(value, bool) + and not isinstance(value, int) + ): + value = value and value.strftime("%d/%m/%Y") + report_data["sheet"].write_string( + report_data["row_pos"], col_pos, value or "" + ) + elif cell_type == "amount": + if ( + line_dict.get("account_group_id", False) + and line_dict["account_group_id"] + ): + cell_format = report_data["formats"]["format_amount_bold"] + else: + cell_format = report_data["formats"]["format_amount"] + report_data["sheet"].write_number( + report_data["row_pos"], col_pos, float(value), cell_format + ) + elif cell_type == "amount_currency": + if line_dict.get("currency_name", False): + format_amt = self._get_currency_amt_format_dict( + line_dict, report_data + ) + report_data["sheet"].write_number( + report_data["row_pos"], col_pos, float(value), format_amt + ) + elif cell_type == "currency_name": + report_data["sheet"].write_string( + report_data["row_pos"], + col_pos, + value or "", + report_data["formats"]["format_right"], + ) + else: + self.write_non_standard_column(cell_type, col_pos, value) + report_data["row_pos"] += 1 + + AbstractReportXslxOriginal.write_line_from_dict = write_line_from_dict diff --git a/aged_payables_receivables_foreign_currency/report/aged_partner_balance.py b/aged_payables_receivables_foreign_currency/report/aged_partner_balance.py new file mode 100644 index 00000000..b4abe282 --- /dev/null +++ b/aged_payables_receivables_foreign_currency/report/aged_partner_balance.py @@ -0,0 +1,277 @@ +# Copyright 2024 Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import operator +from datetime import date +from collections import OrderedDict + +from odoo import api, models +from odoo.tools import float_is_zero +from odoo.addons.account_financial_report.report.aged_partner_balance import ( + AgedPartnerBalanceReport as AgedPartnerBalanceReportOriginal, +) + + +class AgedPartnerBalanceReport(AgedPartnerBalanceReportOriginal): + + def _get_move_lines_data( + self, + company_id, + account_ids, + partner_ids, + date_at_object, + date_from, + only_posted_moves, + show_move_line_details, + ): + domain = self._get_move_lines_domain_not_reconciled( + company_id, account_ids, partner_ids, only_posted_moves, date_from + ) + ml_fields = self._get_ml_fields() + line_model = self.env["account.move.line"] + move_lines = line_model.search_read(domain=domain, fields=ml_fields) + journals_ids = set() + partners_ids = set() + partners_data = {} + ag_pb_data = {} + if date_at_object < date.today(): + ( + acc_partial_rec, + debit_amount, + credit_amount, + debit_amount_currency, + credit_amount_currency, + ) = self._get_account_partial_reconciled(company_id, date_at_object) + if acc_partial_rec: + ml_ids = list(map(operator.itemgetter("id"), move_lines)) + debit_ids = list( + map(operator.itemgetter("debit_move_id"), acc_partial_rec) + ) + credit_ids = list( + map(operator.itemgetter("credit_move_id"), acc_partial_rec) + ) + move_lines = self._recalculate_move_lines( + move_lines, + debit_ids, + credit_ids, + debit_amount, + credit_amount, + ml_ids, + account_ids, + company_id, + partner_ids, + only_posted_moves, + debit_amount_currency, + credit_amount_currency, + ) + move_lines = [ + move_line + for move_line in move_lines + if move_line["date"] <= date_at_object + and not float_is_zero(move_line["amount_residual"], precision_digits=2) + ] + for move_line in move_lines: + journals_ids.add(move_line["journal_id"][0]) + acc_id = move_line["account_id"][0] + if move_line["partner_id"]: + prt_id = move_line["partner_id"][0] + prt_name = move_line["partner_id"][1] + else: + prt_id = 0 + prt_name = "" + if prt_id not in partners_ids: + partners_data.update({prt_id: {"id": prt_id, "name": prt_name}}) + partners_ids.add(prt_id) + if acc_id not in ag_pb_data.keys(): + ag_pb_data = self._initialize_account(ag_pb_data, acc_id) + if prt_id not in ag_pb_data[acc_id]: + ag_pb_data = self._initialize_partner(ag_pb_data, acc_id, prt_id) + move_line_data = {} + if show_move_line_details: + if move_line["ref"] == move_line["name"]: + if move_line["ref"]: + ref_label = move_line["ref"] + else: + ref_label = "" + elif not move_line["ref"]: + ref_label = move_line["name"] + elif not move_line["name"]: + ref_label = move_line["ref"] + else: + ref_label = move_line["ref"] + str(" - ") + move_line["name"] + move_line_data.update( + { + "line_rec": line_model.browse(move_line["id"]), + "date": move_line["date"], + "entry": move_line["move_id"][1], + "jnl_id": move_line["journal_id"][0], + "acc_id": acc_id, + "partner": prt_name, + "ref_label": ref_label, + "due_date": move_line["date_maturity"], + "residual": move_line["amount_residual"], + # Add amount_currency and currency_id data + "amount_currency": move_line["amount_currency"], + "currency_id": move_line["currency_id"], + } + ) + ag_pb_data[acc_id][prt_id]["move_lines"].append(move_line_data) + ag_pb_data = self._calculate_amounts( + ag_pb_data, + acc_id, + prt_id, + move_line["amount_residual"], + move_line["date_maturity"], + date_at_object, + # Add amount_currency and currency_id data + move_line["amount_currency"], + move_line["currency_id"][0], # it is a tuple of id and name + ) + journals_data = self._get_journals_data(list(journals_ids)) + accounts_data = self._get_accounts_data(ag_pb_data.keys()) + + # Sort partners data by name, apply the same order to ag_pb_data + partners_data = self._sort_partners_data(partners_data) + partner_names = self._extract_partner_names(partners_data) + ag_pb_data = self._sort_partners(ag_pb_data, partner_names) + + return ag_pb_data, accounts_data, partners_data, journals_data + + def _create_account_list( + self, + ag_pb_data, + accounts_data, + partners_data, + journals_data, + show_move_line_details, + date_at_oject, + ): + aged_partner_data = [] + for account in accounts_data.values(): + acc_id = account["id"] + account.update( + { + "residual": ag_pb_data[acc_id]["residual"], + "current": ag_pb_data[acc_id]["current"], + "30_days": ag_pb_data[acc_id]["30_days"], + "60_days": ag_pb_data[acc_id]["60_days"], + "90_days": ag_pb_data[acc_id]["90_days"], + "120_days": ag_pb_data[acc_id]["120_days"], + "older": ag_pb_data[acc_id]["older"], + "partners": [], + # Add amount_currency and currency_id data + "amount_currency": ag_pb_data[acc_id]["amount_currency"], + "currency_id": ag_pb_data[acc_id]["currency_id"], + } + ) + for prt_id in ag_pb_data[acc_id]: + if isinstance(prt_id, int): + partner = { + "name": partners_data[prt_id]["name"], + "residual": ag_pb_data[acc_id][prt_id]["residual"], + "current": ag_pb_data[acc_id][prt_id]["current"], + "30_days": ag_pb_data[acc_id][prt_id]["30_days"], + "60_days": ag_pb_data[acc_id][prt_id]["60_days"], + "90_days": ag_pb_data[acc_id][prt_id]["90_days"], + "120_days": ag_pb_data[acc_id][prt_id]["120_days"], + "older": ag_pb_data[acc_id][prt_id]["older"], + # Add amount_currency and currency_id data + "amount_currency": ag_pb_data[acc_id][prt_id][ + "amount_currency" + ], + "currency_id": ag_pb_data[acc_id][prt_id]["currency_id"], + } + if show_move_line_details: + move_lines = [] + for ml in ag_pb_data[acc_id][prt_id]["move_lines"]: + ml.update( + { + "journal": journals_data[ml["jnl_id"]]["code"], + "account": accounts_data[ml["acc_id"]]["code"], + } + ) + self._compute_maturity_date(ml, date_at_oject) + move_lines.append(ml) + move_lines = sorted(move_lines, key=lambda k: (k["date"])) + partner.update({"move_lines": move_lines}) + account["partners"].append(partner) + aged_partner_data.append(account) + return aged_partner_data + + AgedPartnerBalanceReportOriginal._get_move_lines_data = _get_move_lines_data + AgedPartnerBalanceReportOriginal._create_account_list = _create_account_list + + +class AgedPartnerBalanceReportInherit(models.AbstractModel): + _inherit = "report.account_financial_report.aged_partner_balance" + + def _sort_partners_data(self, partners_data): + return OrderedDict( + sorted(partners_data.items(), key=lambda item: item[1]["name"]) + ) + + def _extract_partner_names(self, partners_data): + return {prt_id: prt_data["name"] for prt_id, prt_data in partners_data.items()} + + def _sort_partners(self, ag_pb_data, partner_names): + sorted_ag_pb_data = {} + for acc_id, acc_data in ag_pb_data.items(): + # Extract partner data + partners = { + prt_id: prt_data + for prt_id, prt_data in acc_data.items() + if isinstance(prt_id, int) + } + # Sort partners by name + sorted_partners = OrderedDict( + sorted(partners.items(), key=lambda item: partner_names[item[0]]) + ) + # Update account data with sorted partners + sorted_ag_pb_data[acc_id] = { + k: v for k, v in acc_data.items() if not isinstance(k, int) + } + sorted_ag_pb_data[acc_id].update(sorted_partners) + return sorted_ag_pb_data + + @api.model + def _initialize_account(self, ag_pb_data, acc_id): + ag_pb_data = super()._initialize_account(ag_pb_data, acc_id) + ag_pb_data[acc_id]["amount_currency"] = 0.0 + ag_pb_data[acc_id]["currency_id"] = None + return ag_pb_data + + @api.model + def _initialize_partner(self, ag_pb_data, acc_id, prt_id): + ag_pb_data = super()._initialize_partner(ag_pb_data, acc_id, prt_id) + ag_pb_data[acc_id][prt_id]["amount_currency"] = 0.0 + ag_pb_data[acc_id][prt_id]["currency_id"] = None + return ag_pb_data + + def _get_ml_fields(self): + res = super()._get_ml_fields() + res.append("amount_currency") + res.append("currency_id") + return res + + @api.model + def _calculate_amounts( + self, + ag_pb_data, + acc_id, + prt_id, + residual, + due_date, + date_at_object, + amount_currency=None, + currency_id=None, + ): + ag_pb_data = super()._calculate_amounts( + ag_pb_data, acc_id, prt_id, residual, due_date, date_at_object + ) + if amount_currency: + ag_pb_data[acc_id]["amount_currency"] += amount_currency + ag_pb_data[acc_id][prt_id]["amount_currency"] += amount_currency + if currency_id: + ag_pb_data[acc_id]["currency_id"] = currency_id + ag_pb_data[acc_id][prt_id]["currency_id"] = currency_id + return ag_pb_data diff --git a/aged_payables_receivables_foreign_currency/report/aged_partner_balance_xlsx.py b/aged_payables_receivables_foreign_currency/report/aged_partner_balance_xlsx.py new file mode 100644 index 00000000..9ae946f1 --- /dev/null +++ b/aged_payables_receivables_foreign_currency/report/aged_partner_balance_xlsx.py @@ -0,0 +1,68 @@ +# Copyright 2024 Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, models + + +class AgedPartnerBalanceXslx(models.AbstractModel): + _inherit = "report.a_f_r.report_aged_partner_balance_xlsx" + + def _get_col_pos_footer_label(self, report): + return 2 if not report.show_move_line_details else 8 + + def _get_col_pos_final_balance_label(self): + return 8 + + def _get_report_columns(self, report): + def insert_and_update_indexes(original_dict, new_entries, insert_position): + # Create a new dictionary to hold the updated entries + updated_dict = {} + + # Insert the new entries at the specified position + for i in range(insert_position): + updated_dict[i] = original_dict[i] + + for i, entry in enumerate(new_entries, start=insert_position): + updated_dict[i] = entry + + for i in range(insert_position, len(original_dict)): + updated_dict[i + len(new_entries)] = original_dict[i] + + return updated_dict + + if not report.show_move_line_details: + res = super()._get_report_columns(report) + # New entries to insert + new_entries = [ + {"header": _("Currency"), "field": "currency_id", "width": 14}, + { + "header": _("Amount Currency"), + "field": "amount_currency", + "type": "amount", + "width": 17, + }, + ] + + # Insert new entries at position 1 + insert_position = 1 + updated_dict = insert_and_update_indexes(res, new_entries, insert_position) + return updated_dict + else: + + res = super()._get_report_columns(report) + + # New entries to insert + new_entries = [ + {"header": _("Currency"), "field": "currency_id", "width": 14}, + { + "header": _("Amount Currency"), + "field": "amount_currency", + "type": "amount", + "width": 17, + }, + ] + # Insert new entries before the "Residual" entry (index 7) + insert_position = 7 + updated_dict = insert_and_update_indexes(res, new_entries, insert_position) + return updated_dict + return diff --git a/aged_payables_receivables_foreign_currency/report/templates/aged_partner_balance.xml b/aged_payables_receivables_foreign_currency/report/templates/aged_partner_balance.xml new file mode 100644 index 00000000..6b13bab4 --- /dev/null +++ b/aged_payables_receivables_foreign_currency/report/templates/aged_partner_balance.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + diff --git a/aged_payables_receivables_foreign_currency/static/description/icon.png b/aged_payables_receivables_foreign_currency/static/description/icon.png new file mode 100644 index 00000000..92a86b10 Binary files /dev/null and b/aged_payables_receivables_foreign_currency/static/description/icon.png differ diff --git a/gitoo.yml b/gitoo.yml index 5b05143c..a1ecc07f 100644 --- a/gitoo.yml +++ b/gitoo.yml @@ -48,6 +48,11 @@ - account_statement_import_txt_xlsx - account_statement_import_paypal +- url: https://github.com/OCA/account-financial-reporting + branch: "14.0" + includes: + - account_financial_report + - url: https://github.com/OCA/account-financial-tools branch: "14.0" includes: