diff --git a/project_invoicing_subcontractor/__manifest__.py b/project_invoicing_subcontractor/__manifest__.py index 76cfa80..ccb3a09 100644 --- a/project_invoicing_subcontractor/__manifest__.py +++ b/project_invoicing_subcontractor/__manifest__.py @@ -2,7 +2,7 @@ { "name": "project_invoicing_subcontractor", - "version": "14.0.1.0.2", + "version": "16.0.1.0.0", "author": "Akretion", "website": "https://github.com/akretion/subcontractor", "license": "AGPL-3", @@ -14,6 +14,7 @@ "hr_timesheet_sheet", "account_invoice_subcontractor", "project_time_in_day", + "account_analytic_simple", ], "data": [ "security/ir.model.access.csv", diff --git a/project_invoicing_subcontractor/demo/account_account.xml b/project_invoicing_subcontractor/demo/account_account.xml index b30ef8f..bcbb266 100644 --- a/project_invoicing_subcontractor/demo/account_account.xml +++ b/project_invoicing_subcontractor/demo/account_account.xml @@ -4,31 +4,31 @@ Service Consulting 706140 - + income Service Maintenance 706150 - + income Consulting subcontracting 611140 - + expense Maintenance Subcontracting 611150 - + expense Internal task cost 611160 - + expense @@ -36,7 +36,7 @@ 418101 - + liability_current diff --git a/project_invoicing_subcontractor/demo/project_demo.xml b/project_invoicing_subcontractor/demo/project_demo.xml index a5bec42..a6fa605 100644 --- a/project_invoicing_subcontractor/demo/project_demo.xml +++ b/project_invoicing_subcontractor/demo/project_demo.xml @@ -17,7 +17,7 @@ - + 0 Task 1 @@ -26,7 +26,7 @@ - + 0 Task 2 @@ -35,7 +35,7 @@ - + 0 Task 3 @@ -44,7 +44,7 @@ - + 0 Task 4 @@ -152,7 +152,7 @@ - + 0 Task 1 @@ -161,7 +161,7 @@ - + 0 Task 2 @@ -170,7 +170,7 @@ - + 0 Task 3 @@ -179,7 +179,7 @@ - + 0 Task 4 @@ -287,7 +287,7 @@ - + 0 Task 1 @@ -324,7 +324,7 @@ - + 0 Task 1 @@ -333,7 +333,7 @@ - + 0 Task 2 diff --git a/project_invoicing_subcontractor/migrations/14.0.1.0.0/post-migration.py b/project_invoicing_subcontractor/migrations/14.0.1.0.0/post-migration.py deleted file mode 100644 index 2ef8135..0000000 --- a/project_invoicing_subcontractor/migrations/14.0.1.0.0/post-migration.py +++ /dev/null @@ -1,42 +0,0 @@ -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from openupgradelib import openupgrade - - -@openupgrade.migrate() -def migrate(env, version): - openupgrade.logged_query( - env.cr, - """ - UPDATE account_analytic_line - SET - invoice_line_id = aml.id - FROM account_invoice_line ail - JOIN account_move_line aml ON aml.old_invoice_line_id = ail.id - WHERE account_analytic_line.old_invoice_line_id = ail.id - AND account_analytic_line.old_invoice_line_id IS NOT NULL - """, - ) - openupgrade.logged_query( - env.cr, - """ - UPDATE account_analytic_line - SET - invoice_id = am.id - FROM account_invoice ai - JOIN account_move am ON am.old_invoice_id = ai.id - WHERE account_analytic_line.old_invoice_id = ai.id - AND account_analytic_line.old_invoice_id IS NOT NULL - """, - ) - openupgrade.logged_query( - env.cr, - """ - UPDATE account_analytic_line - SET - supplier_invoice_line_id = aml.id - FROM account_invoice_line ail - JOIN account_move_line aml ON aml.old_invoice_line_id = ail.id - WHERE account_analytic_line.old_supplier_invoice_line_id = ail.id - AND account_analytic_line.old_supplier_invoice_line_id IS NOT NULL - """, - ) diff --git a/project_invoicing_subcontractor/migrations/14.0.1.0.0/pre-migration.py b/project_invoicing_subcontractor/migrations/14.0.1.0.0/pre-migration.py deleted file mode 100644 index 381c038..0000000 --- a/project_invoicing_subcontractor/migrations/14.0.1.0.0/pre-migration.py +++ /dev/null @@ -1,43 +0,0 @@ -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from openupgradelib import openupgrade - - -@openupgrade.migrate() -def migrate(env, version): - # Rename column so we keep the data but leave the update of the module create - # a new column with a foreign key towards account.move - # in post migration we'll manage the migraiton of the data from old field to new one - openupgrade.logged_query( - env.cr, - """ - ALTER TABLE account_analytic_line - RENAME COLUMN invoice_line_id TO old_invoice_line_id; - """, - ) - openupgrade.logged_query( - env.cr, - """ - ALTER TABLE account_analytic_line - RENAME COLUMN invoice_id TO old_invoice_id; - """, - ) - openupgrade.logged_query( - env.cr, - """ - ALTER TABLE account_analytic_line - RENAME COLUMN supplier_invoice_line_id TO old_supplier_invoice_line_id; - """, - ) - - openupgrade.logged_query( - env.cr, - """ - DELETE FROM subcontractor_timesheet_invoice; - """, - ) - openupgrade.logged_query( - env.cr, - """ - DELETE FROM supplier_timesheet_invoice; - """, - ) diff --git a/project_invoicing_subcontractor/models/__init__.py b/project_invoicing_subcontractor/models/__init__.py index 65894dc..7594205 100644 --- a/project_invoicing_subcontractor/models/__init__.py +++ b/project_invoicing_subcontractor/models/__init__.py @@ -1,5 +1,7 @@ -from . import project +from . import project_project +from . import project_task from . import account_move +from . import account_move_line from . import subcontractor_work from . import account_analytic_line from . import project_invoice_typology diff --git a/project_invoicing_subcontractor/models/account_analytic_account.py b/project_invoicing_subcontractor/models/account_analytic_account.py index d068fb1..8c2ab11 100644 --- a/project_invoicing_subcontractor/models/account_analytic_account.py +++ b/project_invoicing_subcontractor/models/account_analytic_account.py @@ -9,9 +9,13 @@ class AccountAnalyticAccount(models.Model): 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") - account_move_line_ids = fields.One2many("account.move.line", "analytic_account_id") + prepaid_move_line_ids = fields.One2many( + "account.move.line", + "analytic_account_id", + domain=[("is_prepaid_line", "=", True)], + ) - @api.depends("account_move_line_ids.prepaid_is_paid") + @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() @@ -46,13 +50,13 @@ def _prepaid_move_lines(self): ], ) paid_lines = move_lines.filtered( - lambda m: m.prepaid_is_paid + lambda line: line.prepaid_is_paid or ( - m.move_id.supplier_invoice_ids + line.move_id.supplier_invoice_ids and all( [ x.to_pay and x.payment_state != "paid" - for x in m.move_id.supplier_invoice_ids + for x in line.move_id.supplier_invoice_ids ] ) ) diff --git a/project_invoicing_subcontractor/models/account_move.py b/project_invoicing_subcontractor/models/account_move.py index 0148733..a1eb828 100644 --- a/project_invoicing_subcontractor/models/account_move.py +++ b/project_invoicing_subcontractor/models/account_move.py @@ -7,125 +7,6 @@ from odoo.tools import float_compare -class AccountMoveLine(models.Model): - _inherit = "account.move.line" - - task_id = fields.Many2one("project.task") - task_stage_id = fields.Many2one( - "project.task.type", related="task_id.stage_id", store=True - ) - timesheet_line_ids = fields.One2many( - "account.analytic.line", "invoice_line_id", "Timesheet Line" - ) - timesheet_error = fields.Char(compute="_compute_timesheet_qty", store=True) - timesheet_qty = fields.Float( - digits="Product Unit of Measure", - compute="_compute_timesheet_qty", - store=True, - ) - task_invoiceable_days = fields.Float( - related="task_id.invoiceable_days", - digits="Product Unit of Measure", - string="Task Days", - help="Total days of the task, helper to check if you miss some timesheet", - ) - prepaid_is_paid = fields.Boolean(compute="_compute_prepaid_is_paid", store=True) - contribution_price_subtotal = fields.Float( - compute="_compute_contribution_subtotal", store=True - ) - - @api.depends( - "account_id", - "move_id.payment_state", - "move_id.supplier_invoice_ids.payment_state", - "move_id.move_type", - "move_id.state", - ) - def _compute_prepaid_is_paid(self): - for line in self: - if not line.account_id.is_prepaid_account: - continue - move = line.move_id - if move.state in ("draft", "cancel"): - line.prepaid_is_paid = False - elif move.move_type == "out_refund": - line.prepaid_is_paid = True - elif move.move_type == "out_invoice": - if move.payment_state in ("paid", "reversed"): - line.prepaid_is_paid = True - else: - line.prepaid_is_paid = False - elif move.supplier_invoice_ids: - if all( - [ - x.payment_state in ("paid", "reversed") - for x in move.supplier_invoice_ids - ] - ): - line.prepaid_is_paid = True - else: - line.prepaid_is_paid = False - # OD to manage migration toward this system? - else: - line.prepaid_is_paid = True - - @api.depends( - "timesheet_line_ids.discount", "timesheet_line_ids.unit_amount", "quantity" - ) - def _compute_timesheet_qty(self): - for record in self: - record.timesheet_qty = ( - record.timesheet_line_ids._get_invoiceable_qty_with_unit( - record.product_uom_id - ) - ) - if abs(record.timesheet_qty - record.quantity) > 0.001: - record.timesheet_error = "⏰ %s" % record.timesheet_qty - - @api.depends( - "move_id", - "analytic_account_id.partner_id", - "move_id.move_type", - "product_id.prepaid_revenue_account_id", - "amount_currency", - ) - def _compute_contribution_subtotal(self): - for line in self: - contribution_price = 0 - if ( - line.move_id.move_type in ["in_invoice", "in_refund"] - and line.product_id.prepaid_revenue_account_id - and line.analytic_account_id - ): - contribution = line.company_id.with_context( - partner=line.analytic_account_id.partner_id - )._get_commission_rate() - contribution_price = line.amount_currency / (1 - contribution) - line.contribution_price_subtotal = contribution_price - - def open_task(self): - self.ensure_one() - action = self.env.ref("project.action_view_task").sudo().read()[0] - action.update( - { - "res_id": self.task_id.id, - "views": [x for x in action["views"] if x[1] == "form"], - } - ) - return action - - def _get_computed_account(self): - if ( - self.move_id.move_type in ("out_refund", "out_invoice") - and self.product_id.prepaid_revenue_account_id - ): - return self.product_id.product_tmpl_id.get_product_accounts( - self.move_id.fiscal_position_id - ).get("prepaid") - else: - return super()._get_computed_account() - - class AccountMove(models.Model): _inherit = "account.move" @@ -223,15 +104,16 @@ def _compute_subcontractor_state(self): # noqa: C901 if inv.to_pay: if inv.line_ids.payment_line_ids: reason = ( - """La facture a été ajoutée au prochain ordre de paiement qui """ - """est à l'état '%s'.\nElle devrait être payée dans les prochains """ - """jours""" % inv.line_ids.payment_line_ids.mapped("state")[0] + "La facture a été ajoutée au prochain ordre de paiement qui " + "est à l'état '%s'.\nElle devrait être payée dans les prochains" + " jours" + "" % inv.line_ids.payment_line_ids.mapped("state")[0] ) color = "success" else: reason = ( - """La facture est à payer, elle sera incluse dans le prochain """ - """ordre de paiement.""" + "La facture est à payer, elle sera incluse dans le prochain " + "ordre de paiement." ) color = "success" elif inv.customer_invoice_ids: @@ -245,20 +127,20 @@ def _compute_subcontractor_state(self): # noqa: C901 ) ): reason = ( - """La facture est en brouillon car le montant de la facture ne """ - """correspond pas à celui de la facture inter société.""" + "La facture est en brouillon car le montant de la facture ne " + "correspond pas à celui de la facture inter société." ) color = "danger" if inv.invalid_work_amount: reason = ( - """Le montant des lignes de factures n'est pas cohérent avec le """ - """montant des lignes de sous-traitance.""" + "Le montant des lignes de factures n'est pas cohérent avec le " + "montant des lignes de sous-traitance." ) color = "danger" if any([x.payment_state != "paid" for x in inv.customer_invoice_ids]): reason = ( - """Les factures clients Akretion %s ne sont pas encore payées ou """ - """leur paiement n'a pas encore été importé dans l'erp.""" + "Les factures clients Akretion %s ne sont pas encore payées ou" + " leur paiement n'a pas encore été importé dans l'erp." % ", ".join(inv.customer_invoice_ids.mapped("name")) ) color = "info" @@ -274,8 +156,8 @@ def _compute_subcontractor_state(self): # noqa: C901 # read on project not very intuitive to discuss if not analytic_account: reason = ( - """Le compte analytique est obligatoire sur les lignes de """ - """cette facture.""" + "Le compte analytique est obligatoire sur les lignes de " + "cette facture." ) break total_amount = analytic_account.prepaid_total_amount @@ -284,7 +166,11 @@ def _compute_subcontractor_state(self): # noqa: C901 other_draft_invoices = self.env["account.move.line"].search( [ ("parent_state", "=", "draft"), - ("analytic_account_id", "=", analytic_account.id), + ( + "analytic_account_id", + "=", + analytic_account.id, + ), ("move_id", "!=", inv.id), ("move_id.move_type", "=", ["in_invoice", "in_refund"]), ] @@ -297,9 +183,9 @@ def _compute_subcontractor_state(self): # noqa: C901 == -1 ): account_reasons.append( - """Le solde du compte analytique %s n'est pas suffisant : %s. """ - """Il est necessaire de facturer le client.""" - % (analytic_account.name, total_amount) + f"Le solde du compte analytique {analytic_account.name} " + f"n'est pas suffisant : {total_amount}. " + f"Il est necessaire de facturer le client." ) color = "danger" elif inv.state == "draft" and ( @@ -309,10 +195,11 @@ def _compute_subcontractor_state(self): # noqa: C901 == -1 ): account_reasons.append( - """Le solde payé du compte analytique %s est insuffisant %s. """ - """La facture sera payable une fois que le client aura reglé """ - """ses factures.""" - % (analytic_account.name, available_amount) + f"Le solde payé du compte analytique " + f"{analytic_account.name}" + f" est insuffisant {available_amount}. " + f"La facture sera payable une fois que le client aura reglé" + f"ses factures." ) if color != "red": color = "info" @@ -322,9 +209,9 @@ def _compute_subcontractor_state(self): # noqa: C901 == -1 ): account_reasons.append( - """Le solde du compte analytique %s est négatif %s. """ - """Il est necessaire de facturer le client.""" - % (analytic_account.name, total_amount) + f"Le solde du compte analytique {analytic_account.name} " + f"est négatif {total_amount}. " + f"Il est necessaire de facturer le client." ) color = "danger" elif inv.state != "draft" and ( @@ -332,33 +219,34 @@ def _compute_subcontractor_state(self): # noqa: C901 == -1 ): account_reasons.append( - """Le solde payé du compte analytique %s est insuffisant %s. """ - """La facture sera payable une fois que le client aura reglé """ - """ses factures.""" - % (analytic_account.name, available_amount) + f"Le solde payé du compte analytique " + f"{analytic_account.name} est insuffisant " + f"{available_amount}. " + f"La facture sera payable une fois que le client aura reglé" + f" ses factures." ) if color != "red": color = "info" else: account_reasons.append( - """Le solde payé du compte analytique %s est suffisant. """ - """La facture sera payable une fois qu'elle sera validée et """ - """que la tâche planifiée aura tourné.""" - % analytic_account.name + f"Le solde payé du compte analytique " + f"{analytic_account.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é." ) if not color: color = "success" 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 """ - """peuvent influer les montants disponibles.""" + "Attention, il existe des factures à l'état 'brouillon' pour " + "ce/ces comptes analytiques, si elles sont validées, elles " + "peuvent influer les montants disponibles." ) reason = "\n".join(account_reasons) elif inv.invoicing_mode == "supplier": reason = ( - """La validation et le paiement de cette facture se font manuellement """ - """selon la gestion des budgets.""" + "La validation et le paiement de cette facture se font manuellement" + " selon la gestion des budgets." ) inv.subcontractor_state_message = reason inv.subcontractor_state_color = color @@ -395,24 +283,8 @@ def action_view_analytic_line(self): action["domain"] = [("id", "=", lines.ids)] return action - def _move_autocomplete_invoice_lines_values(self): - # Following code is in this method : - # if line.product_id and not line._cache.get('name'): - # line.name = line._get_computed_name() - # it reset invoice_line name to defaut in case it is not in cache. - # The reason to do this would be - # "Furthermore, the product's label was missing on all invoice lines." - # https://github.com/OCA/OCB/commit/7965c890c4e6f6562d265e1605fef3384b00316e - # So to avoid issues I read the name before the supper to ensure it is in cache - # That is really depressing... - # TODO a PR to fix this should be done I guess, but I have not the motivation - # right now... - self.invoice_line_ids.mapped("name") - return super()._move_autocomplete_invoice_lines_values() - def _create_prepare_prepaid_move_vals(self): self.ensure_one() - # TODO configure dedicated journal on company? vals = { "ref": _("prepaid countdown for %s") % self.name, "date": self.date, @@ -447,11 +319,9 @@ def _manage_prepaid_lines(self): revenue_account, analytic_account, ), amount in account_amounts.items(): + customer_name = self.customer_id.name # prepaid line - name = "prepaid transfer from invoice %s - %s" % ( - self.name, - self.customer_id.name, - ) + name = f"prepaid transfer from invoice {self.name} - {customer_name}" line_vals = { "name": name, "account_id": prepaid_revenue_account.id, @@ -460,9 +330,6 @@ def _manage_prepaid_lines(self): "partner_id": self.customer_id.id, "analytic_account_id": analytic_account.id, } - line_vals = self.env["account.move.line"].play_onchanges( - line_vals, ["account_id", "amount_currency"] - ) line_vals_list.append(line_vals) # revenue line line_vals = { @@ -472,9 +339,6 @@ def _manage_prepaid_lines(self): "move_id": prepaid_move.id, "analytic_account_id": analytic_account.id, } - line_vals = self.env["account.move.line"].play_onchanges( - line_vals, ["account_id", "amount_currency"] - ) line_vals_list.append(line_vals) prepaid_move.write({"line_ids": [(0, 0, vals) for vals in line_vals_list]}) prepaid_move.action_post() @@ -630,13 +494,14 @@ def compute_enought_analytic_amount(self, partner_id=False): "enough_analytic_amount", ) def _compute_to_pay(self): - super()._compute_to_pay() + res = super()._compute_to_pay() for invoice in self: if invoice.enough_analytic_amount and invoice.payment_state not in ( "reversed", "paid", ): invoice.to_pay = True + return res def _is_invoiced_with_parent_task_option(self): """ diff --git a/project_invoicing_subcontractor/models/account_move_line.py b/project_invoicing_subcontractor/models/account_move_line.py new file mode 100644 index 0000000..9ba7ba5 --- /dev/null +++ b/project_invoicing_subcontractor/models/account_move_line.py @@ -0,0 +1,148 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + + +from odoo import _, api, exceptions, fields, models + + +class AccountMoveLine(models.Model): + _inherit = "account.move.line" + + task_id = fields.Many2one("project.task") + task_stage_id = fields.Many2one( + "project.task.type", related="task_id.stage_id", store=True + ) + timesheet_line_ids = fields.One2many( + "account.analytic.line", "invoice_line_id", "Timesheet Line" + ) + timesheet_error = fields.Char(compute="_compute_timesheet_qty", store=True) + timesheet_qty = fields.Float( + digits="Product Unit of Measure", + compute="_compute_timesheet_qty", + store=True, + ) + task_invoiceable_days = fields.Float( + related="task_id.invoiceable_days", + digits="Product Unit of Measure", + string="Task Days", + help="Total days of the task, helper to check if you miss some timesheet", + ) + is_prepaid_line = fields.Boolean( + related="account_id.is_prepaid_account", store=True + ) + prepaid_is_paid = fields.Boolean(compute="_compute_prepaid_is_paid", store=True) + contribution_price_subtotal = fields.Float( + compute="_compute_contribution_subtotal", store=True + ) + + @api.depends( + "account_id", + "move_id.payment_state", + "move_id.supplier_invoice_ids.payment_state", + "move_id.move_type", + "move_id.state", + ) + def _compute_prepaid_is_paid(self): + for line in self: + if not line.account_id.is_prepaid_account: + continue + move = line.move_id + if move.state in ("draft", "cancel"): + line.prepaid_is_paid = False + elif move.move_type == "out_refund": + line.prepaid_is_paid = True + elif move.move_type == "out_invoice": + if move.payment_state in ("paid", "reversed"): + line.prepaid_is_paid = True + else: + line.prepaid_is_paid = False + elif move.supplier_invoice_ids: + if all( + [ + x.payment_state in ("paid", "reversed") + for x in move.supplier_invoice_ids + ] + ): + line.prepaid_is_paid = True + else: + line.prepaid_is_paid = False + # OD to manage migration toward this system? + else: + line.prepaid_is_paid = True + + @api.depends( + "timesheet_line_ids.discount", "timesheet_line_ids.unit_amount", "quantity" + ) + def _compute_timesheet_qty(self): + for record in self: + record.timesheet_qty = ( + record.timesheet_line_ids._get_invoiceable_qty_with_unit( + record.product_uom_id + ) + ) + if abs(record.timesheet_qty - record.quantity) > 0.001: + record.timesheet_error = "⏰ %s" % record.timesheet_qty + + @api.depends( + "move_id", + "analytic_account_id.project_ids.partner_id", + "move_id.move_type", + "product_id.prepaid_revenue_account_id", + "amount_currency", + ) + def _compute_contribution_subtotal(self): + for line in self: + contribution_price = 0 + if ( + line.move_id.move_type in ["in_invoice", "in_refund"] + and line.product_id.prepaid_revenue_account_id + and line.analytic_account_id + ): + contribution = line.company_id.with_context( + partner=line.analytic_account_id.partner_id + )._get_commission_rate() + contribution_price = line.amount_currency / (1 - contribution) + line.contribution_price_subtotal = contribution_price + + def open_task(self): + self.ensure_one() + action = self.env.ref("project.action_view_task").sudo().read()[0] + action.update( + { + "res_id": self.task_id.id, + "views": [x for x in action["views"] if x[1] == "form"], + } + ) + return action + + def _compute_account_id(self): + res = super()._compute_account_id() + product_lines = self.filtered( + lambda line: line.display_type == "product" + and line.move_id.is_invoice(True) + ) + for line in product_lines: + if ( + line.move_id.is_sale_document() + and line.with_company( + line.company_id + ).product_id.prepaid_revenue_account_id + ): + fiscal_position = line.move_id.fiscal_position_id + accounts = line.with_company( + line.company_id + ).product_id.product_tmpl_id.get_product_accounts( + fiscal_pos=fiscal_position + ) + line.account_id = accounts["prepaid"] or line.account_id + return res + + @api.constrains("account_id", "subcontractor_work_ids") + def check_no_subcontractor_prepaid(self): + for rec in self: + if rec.account_id.is_prepaid_account and rec.subcontractor_work_ids: + raise exceptions.UserError( + _( + "You can't have subcontractor on an invoice line with a prepaid" + " account." + ) + ) diff --git a/project_invoicing_subcontractor/models/project.py b/project_invoicing_subcontractor/models/project_project.py similarity index 75% rename from project_invoicing_subcontractor/models/project.py rename to project_invoicing_subcontractor/models/project_project.py index b41feb6..a092019 100644 --- a/project_invoicing_subcontractor/models/project.py +++ b/project_invoicing_subcontractor/models/project_project.py @@ -68,7 +68,7 @@ def _compute_price_unit(self): @api.depends( "invoicing_mode", "analytic_account_id", - "analytic_account_id.account_move_line_ids.prepaid_is_paid", + "analytic_account_id.prepaid_move_line_ids.prepaid_is_paid", ) def _compute_prepaid_amount(self): for project in self: @@ -109,11 +109,11 @@ def _get_sale_price_unit(self): self.ensure_one() product = self._get_project_invoicing_product() partner = self.partner_id - price = product.with_context( - pricelist=partner.property_product_pricelist.id, - partner=partner.id, - uom=self.uom_id.id, - ).price + pricelist = partner.property_product_pricelist + if pricelist: + price = pricelist._get_product_price(product, 1, uom=self.uom_id) + else: + price = product.list_price return price @api.constrains("invoicing_mode", "analytic_account_id") @@ -125,8 +125,8 @@ def _check_analytic_account(self): ): raise UserError( _( - "The analytic account is mandatory on project [%s] %s configured with " - "prepaid invoicing" % (project.id, project.name) + f"The analytic account is mandatory on project [{project.id}] " + f"{project.name} configured with prepaid invoicing" ) ) @@ -139,8 +139,8 @@ def _check_analytic_account_consistency(self): ): raise UserError( _( - "All projects linked to a same analytic account has to have the " - "same customer." + "All projects linked to a same analytic account has to have the" + " same customer." ) ) if project.analytic_account_id and not all( @@ -151,8 +151,8 @@ def _check_analytic_account_consistency(self): ): raise UserError( _( - "All projects linked to a same analytic account has to have the " - "same invoicing mode." + "All projects linked to a same analytic account has to have the" + " same invoicing mode." ) ) @@ -170,42 +170,3 @@ def action_project_prepaid_move_line(self): "delete": False, } return action - - -class ProjectTask(models.Model): - _inherit = "project.task" - - invoiceable_hours = fields.Float(compute="_compute_invoiceable", store=True) - invoiceable_days = fields.Float(compute="_compute_invoiceable", store=True) - invoice_line_ids = fields.One2many("account.move.line", "task_id", "Invoice Line") - - @api.depends("timesheet_ids.discount", "timesheet_ids.unit_amount") - def _compute_invoiceable(self): - for record in self: - total = 0 - for line in record.timesheet_ids: - total += line.unit_amount * (1 - line.discount / 100.0) - record.invoiceable_hours = total - record.invoiceable_days = record.project_id.convert_hours_to_days(total) - - # TODO we should move this in a generic module - # changing the project on the task should be propagated - # on the analytic line to avoid issue during invoicing - def write(self, vals): - res = super().write(vals) - if "project_id" in vals: - if not vals["project_id"]: - raise UserError( - _( - "The project can not be removed, " - "please remove the timesheet first" - ) - ) - else: - project = self.env["project.project"].browse(vals["project_id"]) - vals = { - "project_id": project.id, - "account_id": project.analytic_account_id.id, - } - self.mapped("timesheet_ids").write(vals) - return res diff --git a/project_invoicing_subcontractor/models/project_task.py b/project_invoicing_subcontractor/models/project_task.py new file mode 100644 index 0000000..eba7ff9 --- /dev/null +++ b/project_invoicing_subcontractor/models/project_task.py @@ -0,0 +1,43 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class ProjectTask(models.Model): + _inherit = "project.task" + + invoiceable_hours = fields.Float(compute="_compute_invoiceable", store=True) + invoiceable_days = fields.Float(compute="_compute_invoiceable", store=True) + invoice_line_ids = fields.One2many("account.move.line", "task_id", "Invoice Line") + + @api.depends("timesheet_ids.discount", "timesheet_ids.unit_amount") + def _compute_invoiceable(self): + for record in self: + total = 0 + for line in record.timesheet_ids: + total += line.unit_amount * (1 - line.discount / 100.0) + record.invoiceable_hours = total + record.invoiceable_days = record.project_id.convert_hours_to_days(total) + + # TODO we should move this in a generic module + # changing the project on the task should be propagated + # on the analytic line to avoid issue during invoicing + def write(self, vals): + res = super().write(vals) + if "project_id" in vals: + if not vals["project_id"]: + raise UserError( + _( + "The project can not be removed, " + "please remove the timesheet first" + ) + ) + else: + project = self.env["project.project"].browse(vals["project_id"]) + vals = { + "project_id": project.id, + "account_id": project.analytic_account_id.id, + } + self.mapped("timesheet_ids").write(vals) + return res diff --git a/project_invoicing_subcontractor/tests/test_invoicing.py b/project_invoicing_subcontractor/tests/test_invoicing.py index cdba2cb..3af80a8 100644 --- a/project_invoicing_subcontractor/tests/test_invoicing.py +++ b/project_invoicing_subcontractor/tests/test_invoicing.py @@ -6,6 +6,7 @@ from datetime import date, timedelta from odoo.exceptions import UserError +from odoo.fields import Command from odoo.tests.common import Form, tagged from odoo.addons.account.tests.common import AccountTestInvoicingCommon @@ -158,21 +159,29 @@ def test_invoicing_update_multiple_employee(self): self.assertIn(line2.task_id.name, line2.name) def _create_prepaid_customer_invoice(self, quantity, analytic_account): - invoice = Form( - self.env["account.move"].with_context( - default_move_type="out_invoice", + invoice = ( + self.env["account.move"] + .with_context(default_move_type="out_invoice") + .create( + { + "partner_id": self.partner.id, + "invoice_date": date.today(), + "invoice_line_ids": [ + Command.create( + { + "product_id": self.maintenance_product.id, + "quantity": quantity, + "product_uom_id": self.env.ref( + "uom.product_uom_hour" + ).id, + "analytic_account_id": analytic_account.id, + "name": self.maintenance_product.name, + } + ) + ], + } ) ) - invoice.partner_id = self.partner - invoice.invoice_date = date.today() - - with invoice.invoice_line_ids.new() as line_form: - line_form.product_id = self.maintenance_product - line_form.quantity = quantity - line_form.product_uom_id = self.env.ref("uom.product_uom_hour") - line_form.analytic_account_id = analytic_account - line_form.name = self.maintenance_product.name - invoice = invoice.save() return invoice def test_prepaid_invoicing_process_same_project(self): diff --git a/project_invoicing_subcontractor/views/account_invoice_view.xml b/project_invoicing_subcontractor/views/account_invoice_view.xml index 15d15b7..0171966 100644 --- a/project_invoicing_subcontractor/views/account_invoice_view.xml +++ b/project_invoicing_subcontractor/views/account_invoice_view.xml @@ -122,7 +122,7 @@ account.move.line - + 1 - + partner.view.buttons res.partner -