diff --git a/account_invoice_subcontractor/models/invoice.py b/account_invoice_subcontractor/models/invoice.py index de9ebd6..cfaf62c 100644 --- a/account_invoice_subcontractor/models/invoice.py +++ b/account_invoice_subcontractor/models/invoice.py @@ -111,6 +111,11 @@ def _prepare_account_move_line(self, dest_invoice, dest_company, form=False): res["subcontractor_work_invoiced_id"] = self.subcontractor_work_invoiced_id.id return res + def _prepare_invoice_data(self, dest_company): + vals = super()._prepare_invoice_data(dest_company) + vals["customer_invoice_id"] = self.origin_customer_invoice_id.id + return vals + def edit_subcontractor(self): view = { "name": ("Details"), diff --git a/project_invoicing_subcontractor/__manifest__.py b/project_invoicing_subcontractor/__manifest__.py index 8870b27..3e21e5e 100644 --- a/project_invoicing_subcontractor/__manifest__.py +++ b/project_invoicing_subcontractor/__manifest__.py @@ -27,6 +27,7 @@ "views/project_invoice_typology.xml", "views/product_template.xml", "views/account_account.xml", + "views/res_partner.xml", "wizards/res_config_settings.xml", "data/ir_cron.xml", ], diff --git a/project_invoicing_subcontractor/data/ir_cron.xml b/project_invoicing_subcontractor/data/ir_cron.xml index a831a1e..4fcfd87 100644 --- a/project_invoicing_subcontractor/data/ir_cron.xml +++ b/project_invoicing_subcontractor/data/ir_cron.xml @@ -12,7 +12,7 @@ -1 code - model.compute_enought_analytic_amount_cron() + model.compute_enought_analytic_amount() diff --git a/project_invoicing_subcontractor/models/__init__.py b/project_invoicing_subcontractor/models/__init__.py index 7328a0f..65894dc 100644 --- a/project_invoicing_subcontractor/models/__init__.py +++ b/project_invoicing_subcontractor/models/__init__.py @@ -7,3 +7,4 @@ 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_line.py b/project_invoicing_subcontractor/models/account_analytic_line.py index 915a82a..9742790 100644 --- a/project_invoicing_subcontractor/models/account_analytic_line.py +++ b/project_invoicing_subcontractor/models/account_analytic_line.py @@ -46,7 +46,9 @@ def write(self, vals): "subcontractor_work_id", ] ): - already_invoiced = self.filtered(lambda aal: aal.subcontractor_work_id) + already_invoiced = self.filtered( + lambda aal: aal.subcontractor_work_id or aal.supplier_invoice_line_id + ) if already_invoiced: raise UserError( _( @@ -99,7 +101,9 @@ def _get_invoiceable_qty_with_unit(self, uom): raise NotImplementedError def unlink(self): - already_invoiced = self.filtered(lambda aal: aal.subcontractor_work_id) + already_invoiced = self.filtered( + lambda aal: aal.subcontractor_work_id or aal.supplier_invoice_line_id + ) if already_invoiced: raise UserError( _( diff --git a/project_invoicing_subcontractor/models/account_move.py b/project_invoicing_subcontractor/models/account_move.py index 447a829..28c110c 100644 --- a/project_invoicing_subcontractor/models/account_move.py +++ b/project_invoicing_subcontractor/models/account_move.py @@ -3,6 +3,7 @@ from collections import defaultdict from odoo import _, api, exceptions, fields, models +from odoo.tools import float_compare class AccountMoveLine(models.Model): @@ -28,6 +29,12 @@ class AccountMoveLine(models.Model): 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 + ) + + # TODO contrainte product.prepaid_revenue_account_id et compte analytic ? + # et account_id is prepaid account ? @api.depends( "account_id", @@ -77,6 +84,27 @@ def _compute_timesheet_qty(self): 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] @@ -97,6 +125,23 @@ def _get_computed_account(self): else: return super()._get_computed_account() + def _prepaid_account_amounts(self): + account_amounts = defaultdict(float) + for prepaid_line in self: + if ( + not prepaid_line.product_id.prepaid_revenue_account_id + or not prepaid_line.product_id.property_account_income_id + ): + raise # TODO what is it ?? contrainte ? + account_amounts[ + ( + prepaid_line.product_id.prepaid_revenue_account_id, + prepaid_line.product_id.property_account_income_id, + prepaid_line.analytic_account_id, + ) + ] += prepaid_line.contribution_price_subtotal + return account_amounts + class AccountMove(models.Model): _inherit = "account.move" @@ -113,6 +158,12 @@ class AccountMove(models.Model): customer_id = fields.Many2one( "res.partner", compute="_compute_customer_id", store=True ) + subcontractor_state_message = fields.Text( + compute="_compute_subcontractor_state", compute_sudo=True + ) + subcontractor_state_color = fields.Char( + compute="_compute_subcontractor_state", compute_sudo=True + ) @api.depends("invoice_line_ids.analytic_account_id") def _compute_customer_id(self): @@ -135,6 +186,126 @@ def _compute_is_supplier_prepaid(self): else: pass + def _compute_subcontractor_state(self): # noqa: C901 + precision = self.env["decimal.precision"].precision_get("Account") + for inv in self: + reason = "" + color = "" + if ( + inv.move_type != "in_invoice" + or inv.payment_state == "paid" + or inv.state == "cancel" + ): + inv.subcontractor_state_message = reason + inv.subcontractor_state_color = color + continue + 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] + ) + color = "success" + else: + reason = ( + """La facture est à payer, elle sera incluse dans le prochain """ + """ordre de paiement.""" + ) + color = "success" + elif inv.customer_invoice_id: + if ( + inv.state == "draft" + and inv.auto_invoice_id + and float_compare( + inv.amount_total, + inv.auto_invoice_id.amount_total, + precision_digits=precision, + ) + ): + reason = ( + """La facture est en brouillon car le montant de la facture ne """ + """correspond pas à celui de la facture inter société.""" + ) # TODO block action_post ? + 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.""" # TODO block to pay ? + ) + color = "danger" + if inv.customer_invoice_id.payment_state != "paid": + reason = ( + """La facture client Akretion %s n'est pas encore payée ou son """ + """paiement n'a pas encore été importé dans l'erp.""" + % inv.customer_invoice_id.name + ) + color = "info" + elif inv.is_supplier_prepaid: + prepaid_lines = inv.invoice_line_ids.filtered( + lambda line: line.product_id.prepaid_revenue_account_id + ) + account_amounts = prepaid_lines._prepaid_account_amounts() + account_reasons = [] + other_draft_invoices = self.env["account.move"] + for ( + _prepaid_revenue_account, + _revenue_account, + analytic_account, + ), amount in account_amounts.items(): + # read on project not very intuitive to discuss + project = analytic_account.project_ids[0] + total_amount = project.prepaid_total_amount + available_amount = project.prepaid_available_amount + if inv.state == "draft": + total_amount -= amount + available_amount -= amount + other_draft_invoices = self.env["account.move.line"].search( + [ + ("parent_state", "=", "draft"), + ("analytic_account_id", "=", analytic_account.id), + ("move_id", "!=", inv.id), + ("move_id.move_type", "=", ["in_invoice", "in_refund"]), + ] + ) + if float_compare(total_amount, 0, precision_digits=precision) == -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) + ) + color = "danger" + elif ( + float_compare(available_amount, 0, precision_digits=precision) + == -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) + ) + if color != "red": + color = "info" + else: + account_reasons.append( + """Le solde payé du compte analytique %s est suffisant. """ + """La facture sera payable une fois que la tâche planifiée """ + """aura tourné.""" % analytic_account.name + ) + 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, elle peuvent influer les montants """ + """disponibles.""" + ) + reason = "\n".join(account_reasons) + # TODO infra + inv.subcontractor_state_message = reason + inv.subcontractor_state_color = color + def action_view_subcontractor(self): self.ensure_one() action = ( @@ -159,11 +330,12 @@ def action_view_analytic_line(self): if self.move_type in ["out_invoice", "out_refund"]: action["domain"] = [("invoice_id", "=", self.id)] elif self.move_type in ["in_invoice", "in_refund"]: + works = self.invoice_line_ids.subcontractor_work_invoiced_id action["domain"] = [ ( "id", "=", - self.invoice_line_ids.subcontractor_work_invoiced_id.timesheet_line_ids.ids, + works.timesheet_line_ids.ids, ) ] return action @@ -217,48 +389,20 @@ def _manage_prepaid_lines(self): prepaid_move = self.create(vals) self.write({"prepaid_countdown_move_id": prepaid_move.id}) line_vals_list = [] - account_amounts = defaultdict(float) - for prepaid_line in prepaid_lines: - # line_vals = { - # "name": prepaid_line.name, - # "account_id": prepaid_line.account_id.id, - # "amount_currency": -prepaid_line.amount_currency, - # "move_id": prepaid_move.id, - # } - # line_vals = self.env["account.move.line"].play_onchanges( - # line_vals, ["account_id", "amount_currency"] - # ) - # line_vals_list.append(line_vals) - if ( - not prepaid_line.product_id.prepaid_revenue_account_id - or not prepaid_line.product_id.property_account_income_id - ): - raise - account_amounts[ - ( - prepaid_line.product_id.prepaid_revenue_account_id, - prepaid_line.product_id.property_account_income_id, - prepaid_line.analytic_account_id, - ) - ] += prepaid_line.amount_currency + account_amounts = prepaid_lines._prepaid_account_amounts() for ( - prepaid_revenue_accout, + prepaid_revenue_account, revenue_account, analytic_account, - ), amount_curr in account_amounts.items(): + ), amount in account_amounts.items(): # prepaid line - # Add AK contribution name = "prepaid transfer from invoice %s - %s" % ( self.name, self.customer_id.name, ) - contribution = self.company_id.with_context( - partner=analytic_account.partner_id - )._get_commission_rate() - amount = amount_curr / (1 - contribution) line_vals = { "name": name, - "account_id": prepaid_revenue_accout.id, + "account_id": prepaid_revenue_account.id, "amount_currency": amount, "move_id": prepaid_move.id, "partner_id": analytic_account.partner_id.id, diff --git a/project_invoicing_subcontractor/models/project.py b/project_invoicing_subcontractor/models/project.py index aa80ebe..addeb58 100644 --- a/project_invoicing_subcontractor/models/project.py +++ b/project_invoicing_subcontractor/models/project.py @@ -45,6 +45,8 @@ 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.Float(compute="_compute_prepaid_amount") + prepaid_total_amount = fields.Float(compute="_compute_prepaid_amount") # Not sure we really need this. # price_unit = fields.Float(compute="_compute_price_unit") # @@ -62,6 +64,44 @@ def _get_force_uom_id_domain(self): # project.price_unit = 0.0 # + def _prepaid_move_lines(self): + self.ensure_one() + move_lines = self.env["account.move.line"].search( + [ + ("analytic_account_id", "=", self.analytic_account_id.id), + ("account_id.is_prepaid_account", "=", True), + ], + ) + paid_lines = move_lines.filtered( + lambda m: m.prepaid_is_paid + or ( + m.move_id.supplier_invoice_ids + and all( + [ + x.to_pay and x.payment_state != "paid" + for x in m.move_id.supplier_invoice_ids + ] + ) + ) + ) + return move_lines, paid_lines + + @api.depends( + "invoicing_mode", + "analytic_account_id", + "analytic_account_id.account_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._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 + 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: @@ -105,6 +145,21 @@ def _check_analytic_account(self): ) ) + 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._prepaid_move_lines() + if self.env.context.get("prepaid_is_paid"): + move_lines = paid_lines + action["domain"] = [("id", "in", move_lines.ids)] + action["context"] = { + "search_default_group_by_account": 1, + "create": False, + "edit": False, + "delete": False, + } + return action + class ProjectTask(models.Model): _inherit = "project.task" @@ -131,7 +186,7 @@ def write(self, vals): if not vals["project_id"]: raise UserError( _( - "The project can not be remove, " + "The project can not be removed, " "please remove the timesheet first" ) ) diff --git a/project_invoicing_subcontractor/models/res_partner.py b/project_invoicing_subcontractor/models/res_partner.py new file mode 100644 index 0000000..d70b8bb --- /dev/null +++ b/project_invoicing_subcontractor/models/res_partner.py @@ -0,0 +1,48 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + prepaid_available_amount = fields.Float(compute="_compute_prepaid_amount") + prepaid_total_amount = fields.Float(compute="_compute_prepaid_amount") + + def _compute_prepaid_amount(self): + for partner in self: + total_amount = 0 + available_amount = 0 + projects = self.env["project.project"].search( + [ + ("partner_id", "=", partner.id), + ("invoicing_mode", "=", "customer_prepaid"), + ] + ) + for project in projects: + total_amount += project.prepaid_total_amount + available_amount += project.prepaid_available_amount + partner.prepaid_total_amount = total_amount + partner.prepaid_available_amount = available_amount + + def action_partner_prepaid_move_line(self): + self.ensure_one() + action = self.env.ref("account.action_account_moves_all_tree").sudo().read()[0] + projects = self.env["project.project"].search( + [("partner_id", "=", self.id), ("invoicing_mode", "=", "customer_prepaid")] + ) + move_lines = paid_lines = self.env["account.move.line"] + for project in projects: + project_move_lines, project_paid_lines = project._prepaid_move_lines() + move_lines |= project_move_lines + paid_lines |= project_paid_lines + if self.env.context.get("prepaid_is_paid"): + move_lines = paid_lines + action["domain"] = [("id", "in", move_lines.ids)] + action["context"] = { + "search_default_group_by_account": 1, + "create": False, + "edit": False, + "delete": False, + } + return action diff --git a/project_invoicing_subcontractor/views/account_invoice_view.xml b/project_invoicing_subcontractor/views/account_invoice_view.xml index e44a9ff..15d15b7 100644 --- a/project_invoicing_subcontractor/views/account_invoice_view.xml +++ b/project_invoicing_subcontractor/views/account_invoice_view.xml @@ -41,6 +41,36 @@ attrs="{'column_invisible':[('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}" /> + + + + + + @@ -88,5 +118,21 @@ + + account.move.line + + + + + + + + + + diff --git a/project_invoicing_subcontractor/views/project_view.xml b/project_invoicing_subcontractor/views/project_view.xml index 950bac4..c1b74c7 100644 --- a/project_invoicing_subcontractor/views/project_view.xml +++ b/project_invoicing_subcontractor/views/project_view.xml @@ -5,6 +5,51 @@ project.project +
+ + +
+ + + + partner.view.buttons + res.partner + + + +
+ + +
+
+
+ +