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
+
+
+
+
+
+
+
+