oca-purchase/odoo-bringout-oca-purchase-workflow-purchase_invoice_plan/purchase_invoice_plan/models/purchase.py
Ernad Husremovic 7378b233e9 Add oca-purchase submodule with 96 purchase modules moved from oca-workflow-process
🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-30 18:00:40 +02:00

327 lines
11 KiB
Python

# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
from dateutil.relativedelta import relativedelta
from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.tools.float_utils import float_compare, float_round
class PurchaseOrder(models.Model):
_inherit = "purchase.order"
invoice_plan_ids = fields.One2many(
comodel_name="purchase.invoice.plan",
inverse_name="purchase_id",
string="Invoice Plan",
copy=False,
)
use_invoice_plan = fields.Boolean(
default=False,
copy=False,
)
ip_invoice_plan = fields.Boolean(
string="Invoice Plan In Process",
compute="_compute_ip_invoice_plan",
help="At least one invoice plan line pending to create invoice",
)
ip_total_percent = fields.Float(
compute="_compute_ip_total",
string="Percent",
)
ip_total_amount = fields.Monetary(
compute="_compute_ip_total",
string="Total Amount",
)
@api.depends("invoice_plan_ids")
def _compute_ip_total(self):
for rec in self:
installments = rec.invoice_plan_ids.filtered("installment")
rec.ip_total_percent = sum(installments.mapped("percent"))
rec.ip_total_amount = sum(installments.mapped("amount"))
def _compute_ip_invoice_plan(self):
for rec in self:
rec.ip_invoice_plan = (
rec.use_invoice_plan
and rec.invoice_plan_ids
and len(rec.invoice_plan_ids.filtered(lambda l: not l.invoiced))
)
@api.constrains("invoice_plan_ids")
def _check_ip_total_percent(self):
for rec in self:
installments = rec.invoice_plan_ids.filtered("installment")
ip_total_percent = sum(installments.mapped("percent"))
if float_round(ip_total_percent, 0) > 100:
raise UserError(_("Invoice plan total percentage must not exceed 100%"))
@api.constrains("state")
def _check_invoice_plan(self):
for rec in self:
if rec.state != "draft":
if rec.invoice_plan_ids.filtered(lambda l: not l.percent):
raise ValidationError(
_("Please fill percentage for all invoice plan lines")
)
def button_confirm(self):
if self.filtered(lambda r: r.use_invoice_plan and not r.invoice_plan_ids):
raise UserError(_("Use Invoice Plan selected, but no plan created"))
return super().button_confirm()
def create_invoice_plan(
self, num_installment, installment_date, interval, interval_type
):
self.ensure_one()
self.invoice_plan_ids.unlink()
invoice_plans = []
Decimal = self.env["decimal.precision"]
prec = Decimal.precision_get("Purchase Invoice Plan Percent")
percent = float_round(1.0 / num_installment * 100, prec)
percent_last = 100 - (percent * (num_installment - 1))
for i in range(num_installment):
this_installment = i + 1
if num_installment == this_installment:
percent = percent_last
vals = {
"installment": this_installment,
"plan_date": installment_date,
"invoice_type": "installment",
"percent": percent,
}
invoice_plans.append((0, 0, vals))
installment_date = self._next_date(
installment_date, interval, interval_type
)
self.write({"invoice_plan_ids": invoice_plans})
return True
def remove_invoice_plan(self):
self.ensure_one()
self.invoice_plan_ids.unlink()
return True
@api.model
def _next_date(self, installment_date, interval, interval_type):
installment_date = fields.Date.from_string(installment_date)
if interval_type == "month":
next_date = installment_date + relativedelta(months=+interval)
elif interval_type == "year":
next_date = installment_date + relativedelta(years=+interval)
else:
next_date = installment_date + relativedelta(days=+interval)
next_date = fields.Date.to_string(next_date)
return next_date
def action_view_invoice(self, invoices=False):
invoice_plan_id = self._context.get("invoice_plan_id")
if invoice_plan_id and invoices:
plan = self.env["purchase.invoice.plan"].browse(invoice_plan_id)
for invoice in invoices:
plan._compute_new_invoice_quantity(invoice)
invoice.write(
{
"date": plan.plan_date,
"invoice_date": plan.plan_date,
}
)
plan.invoice_ids += invoice
return super().action_view_invoice(invoices=invoices)
class PurchaseInvoicePlan(models.Model):
_name = "purchase.invoice.plan"
_description = "Invoice Planning Detail"
_order = "installment"
purchase_id = fields.Many2one(
comodel_name="purchase.order",
string="Purchases Order",
index=True,
readonly=True,
ondelete="cascade",
)
partner_id = fields.Many2one(
comodel_name="res.partner",
string="Supplier",
related="purchase_id.partner_id",
store=True,
index=True,
)
state = fields.Selection(
string="Status",
related="purchase_id.state",
store=True,
index=True,
)
installment = fields.Integer()
plan_date = fields.Date(
required=True,
)
invoice_type = fields.Selection(
selection=[("installment", "Installment")],
string="Type",
required=True,
default="installment",
)
last = fields.Boolean(
string="Last Installment",
compute="_compute_last",
help="Last installment will create invoice use remaining amount",
)
percent = fields.Float(
digits="Purchase Invoice Plan Percent",
help="This percent will be used to calculate new quantity",
)
amount = fields.Float(
digits="Product Price",
compute="_compute_amount",
inverse="_inverse_amount",
help="This amount will be used to calculate the percent",
)
invoice_ids = fields.Many2many(
comodel_name="account.move",
relation="purchase_invoice_plan_invoice_rel",
column1="plan_id",
column2="move_id",
string="Invoices",
readonly=True,
)
amount_invoiced = fields.Float(
compute="_compute_invoiced",
store=True,
readonly=False,
)
to_invoice = fields.Boolean(
string="Next Invoice",
compute="_compute_to_invoice",
help="If this line is ready to create new invoice",
store=True,
)
invoiced = fields.Boolean(
string="Invoice Created",
compute="_compute_invoiced",
help="If this line already invoiced",
store=True,
)
no_edit = fields.Boolean(
compute="_compute_no_edit",
)
@api.depends("percent")
def _compute_amount(self):
for rec in self:
amount_untaxed = rec.purchase_id._origin.amount_untaxed
# With invoice already created, no recompute
if rec.invoiced:
rec.amount = rec.amount_invoiced
rec.percent = rec.amount / amount_untaxed * 100
continue
# For last line, amount is the left over
if rec.last:
installments = rec.purchase_id.invoice_plan_ids.filtered(
lambda l: l.invoice_type == "installment"
)
prev_amount = sum((installments - rec).mapped("amount"))
rec.amount = amount_untaxed - prev_amount
continue
rec.amount = rec.percent * amount_untaxed / 100
@api.onchange("amount", "percent")
def _inverse_amount(self):
for rec in self:
if rec.purchase_id.amount_untaxed != 0:
if rec.last:
installments = rec.purchase_id.invoice_plan_ids.filtered(
lambda l: l.invoice_type == "installment"
)
prev_percent = sum((installments - rec).mapped("percent"))
rec.percent = 100 - prev_percent
continue
rec.percent = rec.amount / rec.purchase_id.amount_untaxed * 100
continue
rec.percent = 0
@api.depends("purchase_id.state", "purchase_id.invoice_plan_ids.invoiced")
def _compute_to_invoice(self):
"""If any invoice is in draft/open/paid do not allow to create inv
Only if previous to_invoice is False, it is eligible to_invoice
"""
for rec in self:
rec.to_invoice = False
for rec in self.sorted("installment"):
if rec.purchase_id.state != "purchase":
continue
if not rec.invoiced:
rec.to_invoice = True
break
def _get_amount_invoice(self, invoices):
"""Hook function"""
return sum(invoices.mapped("amount_untaxed"))
@api.depends("invoice_ids.state")
def _compute_invoiced(self):
for rec in self:
invoiced = rec.invoice_ids.filtered(
lambda l: l.state in ("draft", "posted")
)
rec.invoiced = invoiced and True or False
rec.amount_invoiced = rec._get_amount_invoice(invoiced[:1])
def _compute_last(self):
for rec in self:
last = max(rec.purchase_id.invoice_plan_ids.mapped("installment"))
rec.last = rec.installment == last
def _no_edit(self):
self.ensure_one()
return self.invoiced
def _compute_no_edit(self):
for rec in self:
rec.no_edit = rec._no_edit()
def _compute_new_invoice_quantity(self, invoice_move):
self.ensure_one()
if self.last: # For last install, let the system do the calc.
return
percent = self.percent
move = invoice_move.with_context(**{"check_move_validity": False})
for line in move.invoice_line_ids:
self._update_new_quantity(line, percent)
def _update_new_quantity(self, line, percent):
"""Hook function"""
plan_qty = self._get_plan_qty(line.purchase_line_id, percent)
prec = line.purchase_line_id.product_uom.rounding
if float_compare(abs(plan_qty), abs(line.quantity), prec) == 1:
raise ValidationError(
_(
"Plan quantity: %(plan)s, exceed invoiceable quantity: %(qty)s"
"\nProduct should be delivered before invoice"
)
% {"plan": plan_qty, "qty": line.quantity}
)
line.write({"quantity": plan_qty})
@api.model
def _get_plan_qty(self, order_line, percent):
plan_qty = order_line.product_qty * (percent / 100)
return plan_qty
@api.ondelete(at_uninstall=False)
def _unlink_except_no_edit(self):
lines = self.filtered("no_edit")
if lines:
installments = [str(x) for x in lines.mapped("installment")]
raise UserError(
_(
"Installment %s: already used and not allowed to delete.\n"
"Please discard changes."
)
% ", ".join(installments)
)