From 408c9e1b4595fc11a943a71a51a69c8fa05f1351 Mon Sep 17 00:00:00 2001 From: Florian da Costa Date: Mon, 14 Oct 2024 15:34:12 +0200 Subject: [PATCH] [REF] project_invoicing_subcontractor : Replace analytic_account_id on accounting entries by project_id --- account_move_line_project/README.rst | 60 +++ account_move_line_project/__init__.py | 1 + account_move_line_project/__manifest__.py | 21 + account_move_line_project/models/__init__.py | 2 + .../models/account_account.py | 35 ++ .../models/account_move_line.py | 57 +++ .../readme/CONTRIBUTORS.rst | 1 + .../readme/DESCRIPTION.rst | 1 + .../security/security.xml | 9 + .../static/description/index.html | 415 ++++++++++++++++++ .../views/account_move.xml | 31 ++ .../views/account_move_line.xml | 31 ++ .../__manifest__.py | 4 +- .../data/ir_cron.xml | 6 +- .../migrations/16.0.2.0.0/post-migrate.py | 8 + .../migrations/16.0.2.0.0/pre-migrate.py | 2 + .../models/__init__.py | 1 - .../models/account_analytic_account.py | 64 --- .../models/account_move.py | 117 +++-- .../models/account_move_line.py | 10 +- .../models/project_project.py | 123 +++--- .../models/res_partner.py | 2 +- .../tests/test_invoicing.py | 24 +- .../subcontractor_timesheet_invoice.py | 2 +- .../odoo/addons/account_move_line_project | 1 + setup/account_move_line_project/setup.py | 6 + 26 files changed, 814 insertions(+), 220 deletions(-) create mode 100644 account_move_line_project/README.rst create mode 100644 account_move_line_project/__init__.py create mode 100644 account_move_line_project/__manifest__.py create mode 100644 account_move_line_project/models/__init__.py create mode 100644 account_move_line_project/models/account_account.py create mode 100644 account_move_line_project/models/account_move_line.py create mode 100644 account_move_line_project/readme/CONTRIBUTORS.rst create mode 100644 account_move_line_project/readme/DESCRIPTION.rst create mode 100644 account_move_line_project/security/security.xml create mode 100644 account_move_line_project/static/description/index.html create mode 100644 account_move_line_project/views/account_move.xml create mode 100644 account_move_line_project/views/account_move_line.xml create mode 100644 project_invoicing_subcontractor/migrations/16.0.2.0.0/post-migrate.py create mode 100644 project_invoicing_subcontractor/migrations/16.0.2.0.0/pre-migrate.py delete mode 100644 project_invoicing_subcontractor/models/account_analytic_account.py create mode 120000 setup/account_move_line_project/odoo/addons/account_move_line_project create mode 100644 setup/account_move_line_project/setup.py diff --git a/account_move_line_project/README.rst b/account_move_line_project/README.rst new file mode 100644 index 0000000..a72c99d --- /dev/null +++ b/account_move_line_project/README.rst @@ -0,0 +1,60 @@ +========================= +Account Move Line Project +========================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:13f5cce3bbfe28711777d4847ed1a885171306fabc7c34730d113497b7c65b71 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-akretion%2Fsubcontractor-lightgray.png?logo=github + :target: https://github.com/akretion/subcontractor/tree/16.0/account_move_line_project + :alt: akretion/subcontractor + +|badge1| |badge2| |badge3| + +Add project on accounting entries to allow some analysis based on project + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Akretion + +Contributors +~~~~~~~~~~~~ + +* Florian da Costa + +Maintainers +~~~~~~~~~~~ + +This module is part of the `akretion/subcontractor `_ project on GitHub. + +You are welcome to contribute. diff --git a/account_move_line_project/__init__.py b/account_move_line_project/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/account_move_line_project/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/account_move_line_project/__manifest__.py b/account_move_line_project/__manifest__.py new file mode 100644 index 0000000..93d2134 --- /dev/null +++ b/account_move_line_project/__manifest__.py @@ -0,0 +1,21 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + "name": "Account Move Line Project", + "version": "16.0.1.0.0", + "category": "Accounting", + "summary": "Add project on account move line", + "author": "Akretion,Odoo Community Association (OCA)", + "website": "https://github.com/akretion/subcontractor", + "license": "AGPL-3", + "depends": [ + "account", + "project", + ], + "data": [ + "security/security.xml", + "views/account_move.xml", + "views/account_move_line.xml", + ], + "installable": True, +} diff --git a/account_move_line_project/models/__init__.py b/account_move_line_project/models/__init__.py new file mode 100644 index 0000000..e15138f --- /dev/null +++ b/account_move_line_project/models/__init__.py @@ -0,0 +1,2 @@ +from . import account_move_line +from . import account_account diff --git a/account_move_line_project/models/account_account.py b/account_move_line_project/models/account_account.py new file mode 100644 index 0000000..3f67b66 --- /dev/null +++ b/account_move_line_project/models/account_account.py @@ -0,0 +1,35 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class AccountAccount(models.Model): + _inherit = "account.account" + + project_policy = fields.Selection( + selection=[ + ("optional", "Optional"), + ("always", "Always"), + ("posted", "Posted moves"), + ("never", "Never"), + ], + string="Policy for project on accounting entries", + default="optional", + help=( + "Sets the policy for analytic accounts.\n" + "If you select:\n" + "- Optional: The accountant is free to put an analytic account " + "on an account move line with this type of account.\n" + "- Always: The accountant will get an error message if " + "there is no analytic account.\n" + "- Posted moves: The accountant will get an error message if no " + "analytic account is defined when the move is posted.\n" + "- Never: The accountant will get an error message if an analytic " + "account is present.\n\n" + ), + ) + + def _get_project_policy(self): + """Extension point to obtain simple analytic policy for an account""" + self.ensure_one() + return self.project_policy diff --git a/account_move_line_project/models/account_move_line.py b/account_move_line_project/models/account_move_line.py new file mode 100644 index 0000000..37f3f5e --- /dev/null +++ b/account_move_line_project/models/account_move_line.py @@ -0,0 +1,57 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import _, api, exceptions, fields, models + + +class AccountMoveLine(models.Model): + _inherit = "account.move.line" + + project_id = fields.Many2one("project.project", string="Project", index="btree") + + def _check_project_required_msg(self): + self.ensure_one() + company_cur = self.company_currency_id + if company_cur.is_zero(self.debit) and company_cur.is_zero(self.credit): + return None + project_policy = self.account_id._get_project_policy() + if project_policy == "always" and not self.project_id: + return _( + "Project policy is set to 'Always' with account " + "'%(account)s' but the project is missing in " + "the account move line with label '%(move)s'." + ) % { + "account": self.account_id.display_name, + "move": self.name or "", + } + elif project_policy == "never" and (self.project_id): + project = self.project_id + return _( + "Project policy is set to 'Never' with account " + "'%(account)s' but the account move line with label '%(move)s' " + "has an project '%(project_account)s'." + ) % { + "account": self.account_id.display_name, + "move": self.name or "", + "project_account": project.name, + } + elif ( + project_policy == "posted" + and not self.project_id + and self.move_id.state == "posted" + ): + return _( + "Project policy is set to 'Posted moves' with " + "account '%(account)s' but the project is missing " + "in the account move line with label '%(move)s'." + ) % { + "account": self.account_id.display_name, + "move": self.name or "", + } + return None + + @api.constrains("project_id", "account_id", "debit", "credit") + def _check_project_required(self): + for rec in self: + message = rec._check_project_required_msg() + if message: + raise exceptions.ValidationError(message) diff --git a/account_move_line_project/readme/CONTRIBUTORS.rst b/account_move_line_project/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000..0bddb05 --- /dev/null +++ b/account_move_line_project/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Florian da Costa diff --git a/account_move_line_project/readme/DESCRIPTION.rst b/account_move_line_project/readme/DESCRIPTION.rst new file mode 100644 index 0000000..bfa1ce3 --- /dev/null +++ b/account_move_line_project/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +Add project on accounting entries to allow some analysis based on project diff --git a/account_move_line_project/security/security.xml b/account_move_line_project/security/security.xml new file mode 100644 index 0000000..09b797a --- /dev/null +++ b/account_move_line_project/security/security.xml @@ -0,0 +1,9 @@ + + + + + Project on account move line + + + + diff --git a/account_move_line_project/static/description/index.html b/account_move_line_project/static/description/index.html new file mode 100644 index 0000000..75c2bb0 --- /dev/null +++ b/account_move_line_project/static/description/index.html @@ -0,0 +1,415 @@ + + + + + +Account Move Line Project + + + +
+

Account Move Line Project

+ + +

Beta License: AGPL-3 akretion/subcontractor

+

Add project on accounting entries to allow some analysis based on project

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Akretion
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is part of the akretion/subcontractor project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/account_move_line_project/views/account_move.xml b/account_move_line_project/views/account_move.xml new file mode 100644 index 0000000..f30a11f --- /dev/null +++ b/account_move_line_project/views/account_move.xml @@ -0,0 +1,31 @@ + + + + + account.move + + + + + + + + + + + + diff --git a/account_move_line_project/views/account_move_line.xml b/account_move_line_project/views/account_move_line.xml new file mode 100644 index 0000000..e989b88 --- /dev/null +++ b/account_move_line_project/views/account_move_line.xml @@ -0,0 +1,31 @@ + + + + + account.move.line + + + + + + + + + + account.move.line + + + + + + + + + + + diff --git a/project_invoicing_subcontractor/__manifest__.py b/project_invoicing_subcontractor/__manifest__.py index ccb3a09..9187452 100644 --- a/project_invoicing_subcontractor/__manifest__.py +++ b/project_invoicing_subcontractor/__manifest__.py @@ -2,7 +2,7 @@ { "name": "project_invoicing_subcontractor", - "version": "16.0.1.0.0", + "version": "16.0.2.0.0", "author": "Akretion", "website": "https://github.com/akretion/subcontractor", "license": "AGPL-3", @@ -14,7 +14,7 @@ "hr_timesheet_sheet", "account_invoice_subcontractor", "project_time_in_day", - "account_analytic_simple", + "account_move_line_project", ], "data": [ "security/ir.model.access.csv", diff --git a/project_invoicing_subcontractor/data/ir_cron.xml b/project_invoicing_subcontractor/data/ir_cron.xml index 4fcfd87..90a3e9c 100644 --- a/project_invoicing_subcontractor/data/ir_cron.xml +++ b/project_invoicing_subcontractor/data/ir_cron.xml @@ -1,10 +1,10 @@ - + Compute subonctractor supplier invoices to pay depending on analytic accounts + >Compute subonctractor supplier invoices to pay depending on projects 1 @@ -12,7 +12,7 @@ -1 code - model.compute_enought_analytic_amount() + model.compute_enought_project_amount() diff --git a/project_invoicing_subcontractor/migrations/16.0.2.0.0/post-migrate.py b/project_invoicing_subcontractor/migrations/16.0.2.0.0/post-migrate.py new file mode 100644 index 0000000..e8931a8 --- /dev/null +++ b/project_invoicing_subcontractor/migrations/16.0.2.0.0/post-migrate.py @@ -0,0 +1,8 @@ +def migrate(cr, version): + cr.execute( + """ + UPDATE account_move + SET enough_project_amount = enough_analytic_amount + WHERE enough_analytic_amount = true + """ + ) diff --git a/project_invoicing_subcontractor/migrations/16.0.2.0.0/pre-migrate.py b/project_invoicing_subcontractor/migrations/16.0.2.0.0/pre-migrate.py new file mode 100644 index 0000000..c7de5b4 --- /dev/null +++ b/project_invoicing_subcontractor/migrations/16.0.2.0.0/pre-migrate.py @@ -0,0 +1,2 @@ +def migrate(cr, version): + cr.execute("ALTER TABLE account_move ADD COLUMN enough_project_amount bool") diff --git a/project_invoicing_subcontractor/models/__init__.py b/project_invoicing_subcontractor/models/__init__.py index 7594205..b6ae071 100644 --- a/project_invoicing_subcontractor/models/__init__.py +++ b/project_invoicing_subcontractor/models/__init__.py @@ -7,6 +7,5 @@ from . import project_invoice_typology from . import product_template from . import res_company -from . import account_analytic_account from . import account_account from . import res_partner diff --git a/project_invoicing_subcontractor/models/account_analytic_account.py b/project_invoicing_subcontractor/models/account_analytic_account.py deleted file mode 100644 index 8c2ab11..0000000 --- a/project_invoicing_subcontractor/models/account_analytic_account.py +++ /dev/null @@ -1,64 +0,0 @@ -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) - -from odoo import api, fields, models - - -class AccountAnalyticAccount(models.Model): - _inherit = "account.analytic.account" - - available_amount = fields.Monetary(compute="_compute_prepaid_amount") - prepaid_total_amount = fields.Monetary(compute="_compute_prepaid_amount") - prepaid_available_amount = fields.Monetary(compute="_compute_prepaid_amount") - prepaid_move_line_ids = fields.One2many( - "account.move.line", - "analytic_account_id", - domain=[("is_prepaid_line", "=", True)], - ) - - @api.depends("prepaid_move_line_ids.prepaid_is_paid") - def _compute_prepaid_amount(self): - for account in self: - move_lines, paid_lines = account._prepaid_move_lines() - total_amount = -sum(move_lines.mapped("amount_currency")) or 0.0 - available_amount = -sum(paid_lines.mapped("amount_currency")) or 0.0 - not_paid_lines = move_lines - paid_lines - supplier_not_paid = not_paid_lines.filtered( - lambda line: line.amount_currency > 0.0 - ) - available_amount -= sum(supplier_not_paid.mapped("amount_currency")) - account.prepaid_total_amount = total_amount - # this one is used for display/info, so we show what is really available - # as if all supplier invoices were paid. - account.prepaid_available_amount = available_amount - # Keep available_amount without to_pay supplier invoices neither ongoing - # supplier invoices because it is used to make them to pay. - account.available_amount = ( - -sum( - move_lines.filtered(lambda m: m.prepaid_is_paid).mapped( - "amount_currency" - ) - ) - or 0.0 - ) - - def _prepaid_move_lines(self): - self.ensure_one() - move_lines = self.env["account.move.line"].search( - [ - ("analytic_account_id", "=", self.id), - ("account_id.is_prepaid_account", "=", True), - ], - ) - paid_lines = move_lines.filtered( - lambda line: line.prepaid_is_paid - or ( - line.move_id.supplier_invoice_ids - and all( - [ - x.to_pay and x.payment_state != "paid" - for x in line.move_id.supplier_invoice_ids - ] - ) - ) - ) - return move_lines, paid_lines diff --git a/project_invoicing_subcontractor/models/account_move.py b/project_invoicing_subcontractor/models/account_move.py index e7a511a..fccec09 100644 --- a/project_invoicing_subcontractor/models/account_move.py +++ b/project_invoicing_subcontractor/models/account_move.py @@ -15,9 +15,9 @@ class AccountMove(models.Model): is_supplier_prepaid = fields.Boolean( compute="_compute_is_supplier_prepaid", store=True ) - enough_analytic_amount = fields.Boolean( + enough_project_amount = fields.Boolean( help="This field indicates that the invoice can be paid because there is " - "enough money in the linked analytic account." + "enough money in the linked project." ) customer_id = fields.Many2one( "res.partner", compute="_compute_customer_id", store=True @@ -30,14 +30,12 @@ class AccountMove(models.Model): ) invoicing_mode = fields.Char(compute="_compute_invoicing_mode", store=True) - @api.depends("invoice_line_ids.analytic_account_id") + @api.depends("invoice_line_ids.project_id") def _compute_invoicing_mode(self): for move in self: if move.move_type not in ("in_invoice", "in_refund"): continue - modes = move.invoice_line_ids.analytic_account_id.project_ids.mapped( - "invoicing_mode" - ) + modes = move.invoice_line_ids.project_id.mapped("invoicing_mode") move.invoicing_mode = ( modes and all(x == modes[0] for x in modes) and modes[0] or False ) @@ -60,19 +58,17 @@ def _prepaid_account_amounts(self): ( prepaid_account, revenue_account, - prepaid_line.analytic_account_id, + prepaid_line.project_id, ) ] += prepaid_line.contribution_price_subtotal return account_amounts - @api.depends("invoice_line_ids.analytic_account_id") + @api.depends("invoice_line_ids.project_id") def _compute_customer_id(self): for move in self: if move.move_type not in ("in_invoice", "in_refund"): continue - partner = first( - move.invoice_line_ids.analytic_account_id.project_ids - ).partner_id + partner = first(move.invoice_line_ids.project_id).partner_id move.customer_id = len(partner) == 1 and partner.id or False @api.depends("move_type", "invoice_line_ids.product_id") @@ -151,25 +147,25 @@ def _compute_subcontractor_state(self): # noqa: C901 for ( _prepaid_revenue_account, _revenue_account, - analytic_account, + project, ), amount in account_amounts.items(): # read on project not very intuitive to discuss - if not analytic_account: + if not project: reason = ( - "Le compte analytique est obligatoire sur les lignes de " + "Le Project est obligatoire sur les lignes de " "cette facture." ) break - total_amount = analytic_account.prepaid_total_amount - available_amount = analytic_account.prepaid_available_amount + total_amount = project.prepaid_total_amount + available_amount = project.prepaid_available_amount if inv.state == "draft": other_draft_invoices = self.env["account.move.line"].search( [ ("parent_state", "=", "draft"), ( - "analytic_account_id", + "project_id", "=", - analytic_account.id, + project.id, ), ("move_id", "!=", inv.id), ("move_id.move_type", "=", ["in_invoice", "in_refund"]), @@ -183,7 +179,7 @@ def _compute_subcontractor_state(self): # noqa: C901 == -1 ): account_reasons.append( - f"Le solde du compte analytique {analytic_account.name} " + f"Le solde du projet {project.name} " f"n'est pas suffisant : {total_amount}. " f"Il est necessaire de facturer le client." ) @@ -195,8 +191,8 @@ def _compute_subcontractor_state(self): # noqa: C901 == -1 ): account_reasons.append( - f"Le solde payé du compte analytique " - f"{analytic_account.name}" + f"Le solde payé du projet " + f"{project.name}" f" est insuffisant {available_amount}. " f"La facture sera payable une fois que le client aura reglé" f"ses factures." @@ -209,7 +205,7 @@ def _compute_subcontractor_state(self): # noqa: C901 == -1 ): account_reasons.append( - f"Le solde du compte analytique {analytic_account.name} " + f"Le solde du projet {project.name} " f"est négatif {total_amount}. " f"Il est necessaire de facturer le client." ) @@ -219,8 +215,8 @@ def _compute_subcontractor_state(self): # noqa: C901 == -1 ): account_reasons.append( - f"Le solde payé du compte analytique " - f"{analytic_account.name} est insuffisant " + f"Le solde payé du compte projet " + f"{project.name} est insuffisant " f"{available_amount}. " f"La facture sera payable une fois que le client aura reglé" f" ses factures." @@ -229,8 +225,8 @@ def _compute_subcontractor_state(self): # noqa: C901 color = "info" else: account_reasons.append( - f"Le solde payé du compte analytique " - f"{analytic_account.name} est suffisant. " + f"Le solde payé du projet " + f"{project.name} est suffisant. " f"La facture sera payable une fois qu'elle sera validée et " f"que la tâche planifiée aura tourné." ) @@ -239,7 +235,7 @@ def _compute_subcontractor_state(self): # noqa: C901 if other_draft_invoices: account_reasons.append( "Attention, il existe des factures à l'état 'brouillon' pour " - "ce/ces comptes analytiques, si elles sont validées, elles " + "ce/ces projets, si elles sont validées, elles " "peuvent influer les montants disponibles." ) reason = "\n".join(account_reasons) @@ -317,7 +313,7 @@ def _manage_prepaid_lines(self): for ( prepaid_revenue_account, revenue_account, - analytic_account, + project, ), amount in account_amounts.items(): customer_name = self.customer_id.name # prepaid line @@ -328,7 +324,7 @@ def _manage_prepaid_lines(self): "amount_currency": amount, "move_id": prepaid_move.id, "partner_id": self.customer_id.id, - "analytic_account_id": analytic_account.id, + "project_id": project.id, } line_vals_list.append(line_vals) # revenue line @@ -337,7 +333,7 @@ def _manage_prepaid_lines(self): "account_id": revenue_account.id, "amount_currency": -amount, "move_id": prepaid_move.id, - "analytic_account_id": analytic_account.id, + "project_id": project.id, } line_vals_list.append(line_vals) prepaid_move.write({"line_ids": [(0, 0, vals) for vals in line_vals_list]}) @@ -347,35 +343,27 @@ def _manage_prepaid_lines(self): def _check_invoice_mode_validity(self): self.ensure_one() for line in self.invoice_line_ids: - if ( - line.product_id.prepaid_revenue_account_id - and not line.analytic_account_id - ): + if line.product_id.prepaid_revenue_account_id and not line.project_id: raise exceptions.ValidationError( - _( - "Line %s is not valid, the analytic_account is mandatory." - % line.name - ) + _("Line %s is not valid, the project is mandatory." % line.name) ) if ( line.product_id.prepaid_revenue_account_id and line.move_id.move_type in ("out_invoice", "out_refund") ): - project_typology = first( - line.analytic_account_id.project_ids - ).invoicing_typology_id + project_typology = line.project_id.invoicing_typology_id if project_typology.product_id != line.product_id: raise exceptions.ValidationError( _( - "Line %s is not valid, the analytic_account is not " + "Line %s is not valid, the project is not " "consistent with the chosen product" % line.name ) ) - project_partner = first(line.analytic_account_id.project_ids).partner_id + project_partner = line.project_id.partner_id if project_partner != line.move_id.partner_id.commercial_partner_id: raise exceptions.ValidationError( _( - "Line %s is not valid, the analytic_account is not " + "Line %s is not valid, the project is not " "consistent with the chosen customer" % line.name ) ) @@ -396,14 +384,12 @@ def _check_invoice_mode_validity(self): raise exceptions.ValidationError( _( "You can't have a supplier invoice related to multiple end-customer" - "Check that all the analytic accounts of the line belong to the " + "Check that all the projects of the lines belong to the " "same partner" ) ) if self.move_type in ("in_invoice", "in_refund"): - modes = self.invoice_line_ids.analytic_account_id.project_ids.mapped( - "invoicing_mode" - ) + modes = self.invoice_line_ids.project_id.mapped("invoicing_mode") if modes and not all(x == modes[0] for x in modes): raise exceptions.ValidationError( _("All invoice lines should have the same invoicing mode.") @@ -418,7 +404,7 @@ def _post(self, soft=True): for move in self: if move.is_supplier_prepaid: move._manage_prepaid_lines() - move.compute_enought_analytic_amount(partner_id=move.customer_id.id) + move.compute_enought_project_amount(partner_id=move.customer_id.id) return res def _check_reset_allowed(self): @@ -451,7 +437,7 @@ def button_cancel(self): # also called by cron @api.model - def compute_enought_analytic_amount(self, partner_id=False): + def compute_enought_project_amount(self, partner_id=False): # it concerns only subcontractor partner domain = [ ("move_type", "=", "in_invoice"), @@ -462,9 +448,9 @@ def compute_enought_analytic_amount(self, partner_id=False): if partner_id: domain.append(("customer_id", "=", partner_id)) invoices_to_check = self.search(domain, order="invoice_date") - # We only need invoices with analytic account and all lines should have - # analytic accounts we should avoid invoices generated from subcontract work. - available_analytic_amount = {} + # We only need invoices with project and all lines should have + # projects we should avoid invoices generated from subcontract work. + available_project_amount = {} to_pay_invoices = self.env["account.move"] for invoice in invoices_to_check: to_pay = True @@ -472,32 +458,29 @@ def compute_enought_analytic_amount(self, partner_id=False): if not prepaid_move: continue for line in prepaid_move.line_ids: - if ( - not line.account_id.is_prepaid_account - or not line.analytic_account_id - ): + if not line.account_id.is_prepaid_account or not line.project_id: continue - account = line.analytic_account_id - if account not in available_analytic_amount: - available_analytic_amount[account] = account.available_amount - if abs(line.amount_currency) > available_analytic_amount[account]: - available_analytic_amount[account] = 0.0 + project = line.project_id + if project not in available_project_amount: + available_project_amount[project] = project.available_amount + if abs(line.amount_currency) > available_project_amount[project]: + available_project_amount[project] = 0.0 to_pay = False break else: - available_analytic_amount[account] -= abs(line.amount_currency) + available_project_amount[project] -= abs(line.amount_currency) if to_pay: to_pay_invoices |= invoice - to_pay_invoices.write({"enough_analytic_amount": True}) - (invoices_to_check - to_pay_invoices).write({"enough_analytic_amount": False}) + to_pay_invoices.write({"enough_project_amount": True}) + (invoices_to_check - to_pay_invoices).write({"enough_project_amount": False}) @api.depends( - "enough_analytic_amount", + "enough_project_amount", ) def _compute_to_pay(self): res = super()._compute_to_pay() for invoice in self: - if invoice.enough_analytic_amount and invoice.payment_state not in ( + if invoice.enough_project_amount and invoice.payment_state not in ( "reversed", "paid", ): diff --git a/project_invoicing_subcontractor/models/account_move_line.py b/project_invoicing_subcontractor/models/account_move_line.py index 9ba7ba5..bd69bdb 100644 --- a/project_invoicing_subcontractor/models/account_move_line.py +++ b/project_invoicing_subcontractor/models/account_move_line.py @@ -31,7 +31,9 @@ class AccountMoveLine(models.Model): ) prepaid_is_paid = fields.Boolean(compute="_compute_prepaid_is_paid", store=True) contribution_price_subtotal = fields.Float( - compute="_compute_contribution_subtotal", store=True + compute="_compute_contribution_subtotal", + store=True, + help="Amount with contribution included", ) @api.depends( @@ -84,7 +86,7 @@ def _compute_timesheet_qty(self): @api.depends( "move_id", - "analytic_account_id.project_ids.partner_id", + "project_id.partner_id", "move_id.move_type", "product_id.prepaid_revenue_account_id", "amount_currency", @@ -95,10 +97,10 @@ def _compute_contribution_subtotal(self): if ( line.move_id.move_type in ["in_invoice", "in_refund"] and line.product_id.prepaid_revenue_account_id - and line.analytic_account_id + and line.project_id ): contribution = line.company_id.with_context( - partner=line.analytic_account_id.partner_id + partner=line.project_id.partner_id )._get_commission_rate() contribution_price = line.amount_currency / (1 - contribution) line.contribution_price_subtotal = contribution_price diff --git a/project_invoicing_subcontractor/models/project_project.py b/project_invoicing_subcontractor/models/project_project.py index f2379af..a07cf17 100644 --- a/project_invoicing_subcontractor/models/project_project.py +++ b/project_invoicing_subcontractor/models/project_project.py @@ -1,12 +1,33 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import _, api, fields, models -from odoo.exceptions import UserError +from odoo import api, fields, models class ProjectProject(models.Model): _inherit = "project.project" + def _prepaid_move_lines(self): + self.ensure_one() + move_lines = self.env["account.move.line"].search( + [ + ("project_id", "=", self.id), + ("account_id.is_prepaid_account", "=", True), + ], + ) + paid_lines = move_lines.filtered( + lambda line: line.prepaid_is_paid + or ( + line.move_id.supplier_invoice_ids + and all( + [ + x.to_pay and x.payment_state != "paid" + for x in line.move_id.supplier_invoice_ids + ] + ) + ) + ) + return move_lines, paid_lines + def _get_allowed_uom_ids(self): return [ self.env.ref("uom.product_uom_hour").id, @@ -45,9 +66,42 @@ def _get_force_uom_id_domain(self): "If this is an akretion project, the price is mandatory, and is also " "net of the akretion contribution", ) - prepaid_available_amount = fields.Monetary(compute="_compute_prepaid_amount") - prepaid_total_amount = fields.Monetary(compute="_compute_prepaid_amount") price_unit = fields.Float(compute="_compute_price_unit") + prepaid_move_line_ids = fields.One2many( + "account.move.line", + "project_id", + domain=[("is_prepaid_line", "=", True)], + ) + + available_amount = fields.Monetary(compute="_compute_prepaid_amount") + prepaid_total_amount = fields.Monetary(compute="_compute_prepaid_amount") + prepaid_available_amount = fields.Monetary(compute="_compute_prepaid_amount") + + @api.depends("prepaid_move_line_ids.prepaid_is_paid") + def _compute_prepaid_amount(self): + for project in self: + move_lines, paid_lines = project._prepaid_move_lines() + total_amount = -sum(move_lines.mapped("amount_currency")) or 0.0 + available_amount = -sum(paid_lines.mapped("amount_currency")) or 0.0 + not_paid_lines = move_lines - paid_lines + supplier_not_paid = not_paid_lines.filtered( + lambda line: line.amount_currency > 0.0 + ) + available_amount -= sum(supplier_not_paid.mapped("amount_currency")) + project.prepaid_total_amount = total_amount + # this one is used for display/info, so we show what is really available + # as if all supplier invoices were paid. + project.prepaid_available_amount = available_amount + # Keep available_amount without to_pay supplier invoices neither ongoing + # supplier invoices because it is used to make them to pay. + project.available_amount = ( + -sum( + move_lines.filtered(lambda m: m.prepaid_is_paid).mapped( + "amount_currency" + ) + ) + or 0.0 + ) @api.depends( "partner_id", "invoicing_typology_id", "uom_id", "supplier_invoice_price_unit" @@ -65,25 +119,6 @@ def _compute_price_unit(self): else: project.price_unit = 0.0 - @api.depends( - "invoicing_mode", - "analytic_account_id", - "analytic_account_id.prepaid_move_line_ids.prepaid_is_paid", - ) - def _compute_prepaid_amount(self): - for project in self: - total_amount = 0 - available_amount = 0 - if project.invoicing_mode == "customer_prepaid": - ( - move_lines, - paid_lines, - ) = project.analytic_account_id._prepaid_move_lines() - total_amount = project.analytic_account_id.prepaid_total_amount - available_amount = project.analytic_account_id.prepaid_available_amount - project.prepaid_total_amount = total_amount - project.prepaid_available_amount = available_amount - @api.depends("force_uom_id", "invoicing_typology_id") def _compute_uom_id(self): for project in self: @@ -117,50 +152,10 @@ def _get_sale_price_unit(self): price = product.list_price return price - @api.constrains("invoicing_mode", "analytic_account_id") - def _check_analytic_account(self): - for project in self: - if ( - project.invoicing_mode == "customer_prepaid" - and not project.analytic_account_id - ): - raise UserError( - _( - f"The analytic account is mandatory on project [{project.id}] " - f"{project.name} configured with prepaid invoicing" - ) - ) - - @api.constrains("analytic_account_id", "partner_id", "invoicing_mode") - def _check_analytic_account_consistency(self): - for project in self: - if project.analytic_account_id and not all( - x == project.analytic_account_id.project_ids.partner_id[0] - for x in project.analytic_account_id.project_ids.partner_id - ): - raise UserError( - _( - "All projects linked to a same analytic account has to have the" - " same customer." - ) - ) - if project.analytic_account_id and not all( - x == project.analytic_account_id.project_ids.mapped("invoicing_mode")[0] - for x in project.analytic_account_id.project_ids.mapped( - "invoicing_mode" - ) - ): - raise UserError( - _( - "All projects linked to a same analytic account has to have the" - " same invoicing mode." - ) - ) - def action_project_prepaid_move_line(self): self.ensure_one() action = self.env.ref("account.action_account_moves_all_tree").sudo().read()[0] - move_lines, paid_lines = self.analytic_account_id._prepaid_move_lines() + move_lines, paid_lines = self._prepaid_move_lines() if self.env.context.get("prepaid_is_paid"): move_lines = paid_lines action["domain"] = [("id", "in", move_lines.ids)] diff --git a/project_invoicing_subcontractor/models/res_partner.py b/project_invoicing_subcontractor/models/res_partner.py index 2ec8d0c..be85d79 100644 --- a/project_invoicing_subcontractor/models/res_partner.py +++ b/project_invoicing_subcontractor/models/res_partner.py @@ -36,7 +36,7 @@ def action_partner_prepaid_move_line(self): ( project_move_lines, project_paid_lines, - ) = project.analytic_account_id._prepaid_move_lines() + ) = project._prepaid_move_lines() move_lines |= project_move_lines paid_lines |= project_paid_lines if self.env.context.get("prepaid_is_paid"): diff --git a/project_invoicing_subcontractor/tests/test_invoicing.py b/project_invoicing_subcontractor/tests/test_invoicing.py index 3af80a8..ea0d89b 100644 --- a/project_invoicing_subcontractor/tests/test_invoicing.py +++ b/project_invoicing_subcontractor/tests/test_invoicing.py @@ -158,7 +158,7 @@ def test_invoicing_update_multiple_employee(self): self.assertIn(line_1.task_id.name, line_1.name) self.assertIn(line2.task_id.name, line2.name) - def _create_prepaid_customer_invoice(self, quantity, analytic_account): + def _create_prepaid_customer_invoice(self, quantity, project): invoice = ( self.env["account.move"] .with_context(default_move_type="out_invoice") @@ -174,7 +174,7 @@ def _create_prepaid_customer_invoice(self, quantity, analytic_account): "product_uom_id": self.env.ref( "uom.product_uom_hour" ).id, - "analytic_account_id": analytic_account.id, + "project_id": project.id, "name": self.maintenance_product.name, } ) @@ -185,9 +185,7 @@ def _create_prepaid_customer_invoice(self, quantity, analytic_account): return invoice def test_prepaid_invoicing_process_same_project(self): - invoice = self._create_prepaid_customer_invoice( - 10, self.line_5_2.project_id.analytic_account_id - ) + invoice = self._create_prepaid_customer_invoice(10, self.line_5_2.project_id) self.assertTrue(invoice.invoice_line_ids.tax_ids) invoice.action_post() self.assertEqual(invoice.invoice_line_ids.account_id.code, "418101") @@ -216,40 +214,40 @@ def test_prepaid_invoicing_process_same_project(self): # demo invoice of 10H is validated, admin not, so it won't be taken into account # by the to pay cron - self.env["account.move"].compute_enought_analytic_amount() + self.env["account.move"].compute_enought_project_amount() self.assertFalse(demo_invoice.to_pay) self.env["account.payment.register"].with_context( active_ids=invoice.ids, active_model="account.move" ).create({})._create_payments() - self.env["account.move"].compute_enought_analytic_amount() + self.env["account.move"].compute_enought_project_amount() self.assertTrue(demo_invoice.to_pay) # set admin invoice one day later to be sure demo invoice has priority # Add customer invoice so there is enough amount to validate the admin invoice # but it is not paid customer_invoice2 = self._create_prepaid_customer_invoice( - 1.99, self.line_5_2.project_id.analytic_account_id + 1.99, self.line_5_2.project_id ) customer_invoice2.action_post() customer_invoice3 = self._create_prepaid_customer_invoice( - 0.01, self.line_5_2.project_id.analytic_account_id + 0.01, self.line_5_2.project_id ) customer_invoice3.action_post() admin_invoice.write({"invoice_date": date.today() + timedelta(days=1)}) admin_invoice.action_post() - self.env["account.move"].compute_enought_analytic_amount() + self.env["account.move"].compute_enought_project_amount() self.assertFalse(admin_invoice.to_pay) self.assertTrue(demo_invoice.to_pay) self.env["account.payment.register"].with_context( active_ids=customer_invoice2.ids, active_model="account.move" ).create({})._create_payments() - self.env["account.move"].compute_enought_analytic_amount() + self.env["account.move"].compute_enought_project_amount() self.assertFalse(admin_invoice.to_pay) self.env["account.payment.register"].with_context( active_ids=customer_invoice3.ids, active_model="account.move" ).create({})._create_payments() - self.env["account.move"].compute_enought_analytic_amount() + self.env["account.move"].compute_enought_project_amount() self.assertTrue(admin_invoice.to_pay) self.assertTrue(demo_invoice.to_pay) @@ -276,7 +274,7 @@ def test_prepaid_invoicing_multiple_project_multiple_employee(self): # check the to pay is done at invoice validation if amount available is # enough (without the need of the cron) customer_invoice = self._create_prepaid_customer_invoice( - 15, self.line_5_2.project_id.analytic_account_id + 15, self.line_5_2.project_id ) customer_invoice.action_post() self.env["account.payment.register"].with_context( diff --git a/project_invoicing_subcontractor/wizards/subcontractor_timesheet_invoice.py b/project_invoicing_subcontractor/wizards/subcontractor_timesheet_invoice.py index d07855a..267f800 100644 --- a/project_invoicing_subcontractor/wizards/subcontractor_timesheet_invoice.py +++ b/project_invoicing_subcontractor/wizards/subcontractor_timesheet_invoice.py @@ -288,6 +288,7 @@ def _prepare_invoice_line(self, invoice, task, timesheet_lines): "name": f"[{task.id}] {task.name}", "product_uom_id": task.project_id.uom_id.id, "quantity": quantity, + "project_id": project.id, } if hasattr(self.env["account.move.line"], "start_date") and hasattr( self.env["account.move.line"], "end_date" @@ -301,7 +302,6 @@ def _prepare_invoice_line(self, invoice, task, timesheet_lines): } ) if project.invoicing_typology_id.invoicing_mode != "customer_postpaid": - vals["analytic_account_id"] = project.analytic_account_id.id vals["price_unit"] = project.price_unit if project.invoicing_typology_id.invoicing_mode == "customer_prepaid": contribution = invoice.company_id._get_commission_rate() diff --git a/setup/account_move_line_project/odoo/addons/account_move_line_project b/setup/account_move_line_project/odoo/addons/account_move_line_project new file mode 120000 index 0000000..0455244 --- /dev/null +++ b/setup/account_move_line_project/odoo/addons/account_move_line_project @@ -0,0 +1 @@ +../../../../account_move_line_project \ No newline at end of file diff --git a/setup/account_move_line_project/setup.py b/setup/account_move_line_project/setup.py new file mode 100644 index 0000000..28c57bb --- /dev/null +++ b/setup/account_move_line_project/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)