diff --git a/project_invoicing_subcontractor/__manifest__.py b/project_invoicing_subcontractor/__manifest__.py index 76cfa80..35db104 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", 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/account_analytic_account.py b/project_invoicing_subcontractor/models/account_analytic_account.py index d068fb1..632352e 100644 --- a/project_invoicing_subcontractor/models/account_analytic_account.py +++ b/project_invoicing_subcontractor/models/account_analytic_account.py @@ -9,7 +9,11 @@ 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") + account_move_line_ids = fields.One2many( + "account.move.line", + "prepaid_analytic_account_id", + domain=[("is_prepaid_line", "=", True)], + ) @api.depends("account_move_line_ids.prepaid_is_paid") def _compute_prepaid_amount(self): @@ -41,18 +45,18 @@ def _prepaid_move_lines(self): self.ensure_one() move_lines = self.env["account.move.line"].search( [ - ("analytic_account_id", "=", self.id), + ("prepaid_analytic_account_id", "=", self.id), ("account_id.is_prepaid_account", "=", True), ], ) 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 0ddb6d4..73a4d9a 100644 --- a/project_invoicing_subcontractor/models/account_move.py +++ b/project_invoicing_subcontractor/models/account_move.py @@ -30,13 +30,15 @@ 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.prepaid_analytic_account_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.prepaid_analytic_account_id.project_ids.mapped( + "invoicing_mode" + ) ) move.invoicing_mode = ( modes and all(x == modes[0] for x in modes) and modes[0] or False @@ -60,18 +62,18 @@ def _prepaid_account_amounts(self): ( prepaid_account, revenue_account, - prepaid_line.analytic_account_id, + prepaid_line.prepaid_analytic_account_id, ) ] += prepaid_line.contribution_price_subtotal return account_amounts - @api.depends("invoice_line_ids.analytic_account_id") + @api.depends("invoice_line_ids.prepaid_analytic_account_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 + move.invoice_line_ids.prepaid_analytic_account_id.project_ids ).partner_id move.customer_id = len(partner) == 1 and partner.id or False @@ -166,7 +168,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), + ( + "prepaid_analytic_account_id", + "=", + analytic_account.id, + ), ("move_id", "!=", inv.id), ("move_id.move_type", "=", ["in_invoice", "in_refund"]), ] @@ -279,24 +285,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, @@ -340,11 +330,8 @@ 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, + "prepaid_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 = { @@ -352,11 +339,8 @@ def _manage_prepaid_lines(self): "account_id": revenue_account.id, "amount_currency": -amount, "move_id": prepaid_move.id, - "analytic_account_id": analytic_account.id, + "prepaid_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() @@ -367,7 +351,7 @@ def _check_invoice_mode_validity(self): for line in self.invoice_line_ids: if ( line.product_id.prepaid_revenue_account_id - and not line.analytic_account_id + and not line.prepaid_analytic_account_id ): raise exceptions.ValidationError( _( @@ -380,7 +364,7 @@ def _check_invoice_mode_validity(self): and line.move_id.move_type in ("out_invoice", "out_refund") ): project_typology = first( - line.analytic_account_id.project_ids + line.prepaid_analytic_account_id.project_ids ).invoicing_typology_id if project_typology.product_id != line.product_id: raise exceptions.ValidationError( @@ -389,7 +373,9 @@ def _check_invoice_mode_validity(self): "consistent with the chosen product" % line.name ) ) - project_partner = first(line.analytic_account_id.project_ids).partner_id + project_partner = first( + line.prepaid_analytic_account_id.project_ids + ).partner_id if project_partner != line.move_id.partner_id.commercial_partner_id: raise exceptions.ValidationError( _( @@ -418,7 +404,7 @@ def _check_invoice_mode_validity(self): "same partner" ) ) - modes = self.invoice_line_ids.analytic_account_id.project_ids.mapped( + modes = self.invoice_line_ids.prepaid_analytic_account_id.project_ids.mapped( "invoicing_mode" ) if modes and not all(x == modes[0] for x in modes): @@ -491,10 +477,10 @@ def compute_enought_analytic_amount(self, partner_id=False): for line in prepaid_move.line_ids: if ( not line.account_id.is_prepaid_account - or not line.analytic_account_id + or not line.prepaid_analytic_account_id ): continue - account = line.analytic_account_id + account = line.prepaid_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]: diff --git a/project_invoicing_subcontractor/models/account_move_line.py b/project_invoicing_subcontractor/models/account_move_line.py index 9c5beb4..d16f549 100644 --- a/project_invoicing_subcontractor/models/account_move_line.py +++ b/project_invoicing_subcontractor/models/account_move_line.py @@ -26,10 +26,16 @@ class AccountMoveLine(models.Model): 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 ) + prepaid_analytic_account_id = fields.Many2one( + "account.analytic.account", index=True + ) @api.depends( "account_id", @@ -81,7 +87,7 @@ def _compute_timesheet_qty(self): @api.depends( "move_id", - "analytic_account_id.partner_id", + "prepaid_analytic_account_id.project_ids.partner_id", "move_id.move_type", "product_id.prepaid_revenue_account_id", "amount_currency", @@ -92,10 +98,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.prepaid_analytic_account_id ): contribution = line.company_id.with_context( - partner=line.analytic_account_id.partner_id + partner=line.prepaid_analytic_account_id.partner_id )._get_commission_rate() contribution_price = line.amount_currency / (1 - contribution) line.contribution_price_subtotal = contribution_price @@ -111,13 +117,24 @@ def open_task(self): ) 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() + 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 diff --git a/project_invoicing_subcontractor/models/project_project.py b/project_invoicing_subcontractor/models/project_project.py index 0fb7524..480e976 100644 --- a/project_invoicing_subcontractor/models/project_project.py +++ b/project_invoicing_subcontractor/models/project_project.py @@ -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") diff --git a/project_invoicing_subcontractor/tests/test_invoicing.py b/project_invoicing_subcontractor/tests/test_invoicing.py index cdba2cb..d40bf8a 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, + "prepaid_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..b9dce16 100644 --- a/project_invoicing_subcontractor/views/account_invoice_view.xml +++ b/project_invoicing_subcontractor/views/account_invoice_view.xml @@ -41,6 +41,12 @@ attrs="{'column_invisible':[('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}" /> + + +
+ + account.move.line + + + + + + + + account.move.line - + 1 - + partner.view.buttons res.partner -