mirror of
https://github.com/OCA/pms.git
synced 2025-01-29 00:17:45 +02:00
1013 lines
38 KiB
Python
1013 lines
38 KiB
Python
# Copyright 2020 Dario Lodeiros
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
|
|
from math import ceil
|
|
|
|
from odoo import _, api, fields, models
|
|
from odoo.osv import expression
|
|
from odoo.tools import float_compare, float_is_zero
|
|
|
|
|
|
class FolioSaleLine(models.Model):
|
|
_name = "folio.sale.line"
|
|
_description = "Folio Sale Line"
|
|
_order = "folio_id, sequence, reservation_order desc, service_order, date_order"
|
|
|
|
_check_company_auto = True
|
|
|
|
folio_id = fields.Many2one(
|
|
string="Folio Reference",
|
|
help="Folio to which folio sale line belongs",
|
|
required=True,
|
|
index=True,
|
|
copy=False,
|
|
comodel_name="pms.folio",
|
|
ondelete="cascade",
|
|
)
|
|
reservation_id = fields.Many2one(
|
|
string="Reservation Reference",
|
|
help="Reservation to which folio sale line belongs",
|
|
index=True,
|
|
copy=False,
|
|
comodel_name="pms.reservation",
|
|
ondelete="cascade",
|
|
)
|
|
service_id = fields.Many2one(
|
|
string="Service Reference",
|
|
help="Sevice included in folio sale line",
|
|
index=True,
|
|
copy=False,
|
|
comodel_name="pms.service",
|
|
ondelete="cascade",
|
|
)
|
|
is_board_service = fields.Boolean(
|
|
string="Board Service",
|
|
help="Indicates if the service included in "
|
|
"folio sale line is part of a board service",
|
|
store=True,
|
|
related="service_id.is_board_service",
|
|
)
|
|
|
|
name = fields.Text(
|
|
string="Description",
|
|
help="Description of folio sale line",
|
|
readonly=False,
|
|
store=True,
|
|
compute="_compute_name",
|
|
)
|
|
reservation_line_ids = fields.Many2many(
|
|
string="Nights",
|
|
help="Reservation lines associated with folio sale line,"
|
|
" they corresponds with nights",
|
|
comodel_name="pms.reservation.line",
|
|
)
|
|
service_line_ids = fields.Many2many(
|
|
string="Service Lines",
|
|
help="Subservices included in folio sale line service",
|
|
comodel_name="pms.service.line",
|
|
)
|
|
sequence = fields.Integer(string="Sequence", help="", default=10)
|
|
|
|
invoice_lines = fields.Many2many(
|
|
string="Invoice Lines",
|
|
copy=False,
|
|
help="Folio sale line invoice lines",
|
|
comodel_name="account.move.line",
|
|
relation="folio_sale_line_invoice_rel",
|
|
column1="sale_line_id",
|
|
column2="invoice_line_id",
|
|
)
|
|
invoice_status = fields.Selection(
|
|
string="Invoice Status",
|
|
help="Invoice Status; it can be: upselling, invoiced, to invoice, no",
|
|
readonly=True,
|
|
store=True,
|
|
selection=[
|
|
("upselling", "Upselling Opportunity"),
|
|
("invoiced", "Fully Invoiced"),
|
|
("to_invoice", "To Invoice"),
|
|
("no", "Nothing to Invoice"),
|
|
],
|
|
compute="_compute_invoice_status",
|
|
)
|
|
price_unit = fields.Float(
|
|
string="Unit Price",
|
|
help="Unit Price of folio sale line",
|
|
digits="Product Price",
|
|
)
|
|
|
|
price_subtotal = fields.Monetary(
|
|
string="Subtotal",
|
|
help="Subtotal price without taxes",
|
|
readonly=True,
|
|
store=True,
|
|
compute="_compute_amount",
|
|
)
|
|
price_tax = fields.Float(
|
|
string="Total Tax",
|
|
help="Total of taxes in a reservation",
|
|
readonly=True,
|
|
store=True,
|
|
compute="_compute_amount",
|
|
)
|
|
price_total = fields.Monetary(
|
|
string="Total",
|
|
help="Total price with taxes",
|
|
readonly=True,
|
|
store=True,
|
|
compute="_compute_amount",
|
|
)
|
|
price_reduce = fields.Float(
|
|
string="Price Reduce",
|
|
help="Reduced price amount, that is, total price with discounts applied",
|
|
readonly=True,
|
|
store=True,
|
|
digits="Product Price",
|
|
compute="_compute_get_price_reduce",
|
|
)
|
|
tax_ids = fields.Many2many(
|
|
string="Taxes",
|
|
help="Taxes applied in the folio sale line",
|
|
store=True,
|
|
comodel_name="account.tax",
|
|
compute="_compute_tax_ids",
|
|
domain=["|", ("active", "=", False), ("active", "=", True)],
|
|
)
|
|
price_reduce_taxinc = fields.Monetary(
|
|
string="Price Reduce Tax inc",
|
|
help="Price with discounts applied and taxes included",
|
|
readonly=True,
|
|
store=True,
|
|
compute="_compute_get_price_reduce_tax",
|
|
)
|
|
price_reduce_taxexcl = fields.Monetary(
|
|
string="Price Reduce Tax excl",
|
|
help="Price with discounts applied without taxes",
|
|
readonly=True,
|
|
store=True,
|
|
compute="_compute_get_price_reduce_notax",
|
|
)
|
|
|
|
discount = fields.Float(
|
|
string="Discount (%)",
|
|
help="Discount of total price in folio sale line",
|
|
readonly=False,
|
|
store=True,
|
|
digits="Discount",
|
|
compute="_compute_discount",
|
|
)
|
|
|
|
product_id = fields.Many2one(
|
|
string="Product",
|
|
help="Product associated with folio sale line, "
|
|
"can be product associated with service "
|
|
"or product associated with"
|
|
"reservation's room type, in other case it's false",
|
|
store=True,
|
|
comodel_name="product.product",
|
|
domain="[('sale_ok', '=', True),\
|
|
'|', ('company_id', '=', False), \
|
|
('company_id', '=', company_id)]",
|
|
ondelete="restrict",
|
|
compute="_compute_product_id",
|
|
check_company=True,
|
|
change_default=True,
|
|
)
|
|
product_uom_qty = fields.Float(
|
|
string="Quantity",
|
|
help="",
|
|
readonly=False,
|
|
store=True,
|
|
digits="Product Unit of Measure",
|
|
compute="_compute_product_uom_qty",
|
|
)
|
|
product_uom = fields.Many2one(
|
|
string="Unit of Measure",
|
|
help="",
|
|
comodel_name="uom.uom",
|
|
domain="[('category_id', '=', product_uom_category_id)]",
|
|
)
|
|
product_uom_category_id = fields.Many2one(
|
|
string="Unit of Measure Category",
|
|
help="",
|
|
readonly=True,
|
|
related="product_id.uom_id.category_id",
|
|
)
|
|
product_uom_readonly = fields.Boolean(
|
|
string="", help="", compute="_compute_product_uom_readonly"
|
|
)
|
|
|
|
product_custom_attribute_value_ids = fields.One2many(
|
|
string="Custom Values",
|
|
copy=True,
|
|
comodel_name="product.attribute.custom.value",
|
|
inverse_name="sale_order_line_id",
|
|
)
|
|
|
|
qty_to_invoice = fields.Float(
|
|
string="To Invoice Quantity",
|
|
help="The quantity to invoice. If the invoice policy is order, "
|
|
"the quantity to invoice is calculated from the ordered quantity. "
|
|
"Otherwise, the quantity delivered is used.",
|
|
readonly=True,
|
|
store=True,
|
|
digits="Product Unit of Measure",
|
|
compute="_compute_get_to_invoice_qty",
|
|
)
|
|
qty_invoiced = fields.Float(
|
|
string="Invoiced Quantity",
|
|
help="It is the amount invoiced when an invoice is issued",
|
|
readonly=True,
|
|
store=True,
|
|
digits="Product Unit of Measure",
|
|
compute="_compute_get_invoice_qty",
|
|
compute_sudo=True,
|
|
)
|
|
|
|
untaxed_amount_invoiced = fields.Monetary(
|
|
string="Untaxed Invoiced Amount",
|
|
help="The amount to invoice without taxes in the line of folio",
|
|
store=True,
|
|
compute="_compute_untaxed_amount_invoiced",
|
|
compute_sudo=True,
|
|
)
|
|
untaxed_amount_to_invoice = fields.Monetary(
|
|
string="Untaxed Amount To Invoice",
|
|
help="The invoiced amount without taxes in the line of the folio",
|
|
store=True,
|
|
compute="_compute_untaxed_amount_to_invoice",
|
|
compute_sudo=True,
|
|
)
|
|
|
|
currency_id = fields.Many2one(
|
|
string="Currency",
|
|
help="The currency for the folio",
|
|
readonly=True,
|
|
store=True,
|
|
depends=["folio_id.currency_id"],
|
|
related="folio_id.currency_id",
|
|
)
|
|
company_id = fields.Many2one(
|
|
string="Company",
|
|
help="The company in the folio sale line",
|
|
readonly=True,
|
|
store=True,
|
|
index=True,
|
|
related="folio_id.company_id",
|
|
check_pms_properties=True,
|
|
)
|
|
folio_partner_id = fields.Many2one(
|
|
string="Customer",
|
|
help="Related customer with Folio Sale Line",
|
|
readonly=False,
|
|
store=True,
|
|
related="folio_id.partner_id",
|
|
)
|
|
analytic_tag_ids = fields.Many2many(
|
|
string="Analytic Tags",
|
|
comodel_name="account.analytic.tag",
|
|
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",
|
|
)
|
|
analytic_line_ids = fields.One2many(
|
|
string="Analytic lines",
|
|
comodel_name="account.analytic.line",
|
|
inverse_name="so_line",
|
|
)
|
|
is_downpayment = fields.Boolean(
|
|
string="Is a down payment",
|
|
help="Down payments are made when creating invoices from a folio."
|
|
" They are not copied when duplicating a folio.",
|
|
)
|
|
|
|
state = fields.Selection(
|
|
string="Folio Status",
|
|
help="The status of the folio related with folio sale line",
|
|
readonly=True,
|
|
copy=False,
|
|
store=True,
|
|
related="folio_id.state",
|
|
)
|
|
|
|
display_type = fields.Selection(
|
|
string="Display Type",
|
|
help="Technical field for UX purpose.",
|
|
selection=[("line_section", "Section"), ("line_note", "Note")],
|
|
default=False,
|
|
)
|
|
|
|
service_order = fields.Integer(
|
|
string="Service Id",
|
|
help="Field to order by service id",
|
|
readonly=True,
|
|
store=True,
|
|
compute="_compute_service_order",
|
|
)
|
|
|
|
reservation_order = fields.Integer(
|
|
string="Reservation Id",
|
|
help="Field to order by reservation id",
|
|
readonly=True,
|
|
store=True,
|
|
compute="_compute_reservation_order",
|
|
)
|
|
|
|
date_order = fields.Date(
|
|
string="Date",
|
|
help="Field to order by service",
|
|
readonly=True,
|
|
store=True,
|
|
compute="_compute_date_order",
|
|
)
|
|
|
|
@api.depends("qty_to_invoice")
|
|
def _compute_service_order(self):
|
|
for record in self:
|
|
record.service_order = (
|
|
record.service_id
|
|
if record.service_id
|
|
else -1
|
|
if record.display_type
|
|
else 0
|
|
)
|
|
|
|
@api.depends("service_order")
|
|
def _compute_date_order(self):
|
|
for record in self:
|
|
if record.display_type:
|
|
record.date_order = 0
|
|
elif record.reservation_id and not record.service_id:
|
|
record.date_order = (
|
|
min(record.reservation_line_ids.mapped("date"))
|
|
if record.reservation_line_ids
|
|
else 0
|
|
)
|
|
elif record.reservation_id and record.service_id:
|
|
record.date_order = (
|
|
min(record.service_line_ids.mapped("date"))
|
|
if record.service_line_ids
|
|
else 0
|
|
)
|
|
else:
|
|
record.date_order = 0
|
|
|
|
@api.depends("date_order")
|
|
def _compute_reservation_order(self):
|
|
for record in self:
|
|
record.reservation_order = (
|
|
record.reservation_id if record.reservation_id else 0
|
|
)
|
|
|
|
@api.depends("reservation_line_ids", "service_line_ids", "service_line_ids.day_qty")
|
|
def _compute_product_uom_qty(self):
|
|
for line in self:
|
|
if line.reservation_line_ids:
|
|
line.product_uom_qty = len(line.reservation_line_ids)
|
|
elif line.service_line_ids:
|
|
line.product_uom_qty = sum(line.service_line_ids.mapped("day_qty"))
|
|
elif not line.product_uom_qty:
|
|
line.product_uom_qty = False
|
|
|
|
@api.depends("state")
|
|
def _compute_product_uom_readonly(self):
|
|
for line in self:
|
|
line.product_uom_readonly = line.state in ["sale", "done", "cancel"]
|
|
|
|
@api.depends(
|
|
"invoice_lines",
|
|
"invoice_lines.price_total",
|
|
"invoice_lines.move_id.state",
|
|
"invoice_lines.move_id.move_type",
|
|
)
|
|
def _compute_untaxed_amount_invoiced(self):
|
|
"""Compute the untaxed amount already invoiced from
|
|
the sale order line, taking the refund attached
|
|
the so line into account. This amount is computed as
|
|
SUM(inv_line.price_subtotal) - SUM(ref_line.price_subtotal)
|
|
where
|
|
`inv_line` is a customer invoice line linked to the SO line
|
|
`ref_line` is a customer credit note (refund) line linked to the SO line
|
|
"""
|
|
for line in self:
|
|
amount_invoiced = 0.0
|
|
for invoice_line in line.invoice_lines:
|
|
if invoice_line.move_id.state == "posted":
|
|
invoice_date = (
|
|
invoice_line.move_id.invoice_date or fields.Date.today()
|
|
)
|
|
if invoice_line.move_id.move_type == "out_invoice":
|
|
amount_invoiced += invoice_line.currency_id._convert(
|
|
invoice_line.price_subtotal,
|
|
line.currency_id,
|
|
line.company_id,
|
|
invoice_date,
|
|
)
|
|
elif invoice_line.move_id.move_type == "out_refund":
|
|
amount_invoiced -= invoice_line.currency_id._convert(
|
|
invoice_line.price_subtotal,
|
|
line.currency_id,
|
|
line.company_id,
|
|
invoice_date,
|
|
)
|
|
line.untaxed_amount_invoiced = amount_invoiced
|
|
|
|
@api.depends(
|
|
"state",
|
|
"price_reduce",
|
|
"product_id",
|
|
"untaxed_amount_invoiced",
|
|
"product_uom_qty",
|
|
)
|
|
def _compute_untaxed_amount_to_invoice(self):
|
|
"""Total of remaining amount to invoice on the sale order line (taxes excl.) as
|
|
total_sol - amount already invoiced
|
|
where Total_sol depends on the invoice policy of the product.
|
|
|
|
Note: Draft invoice are ignored on purpose, the 'to invoice' amount should
|
|
come only from the SO lines.
|
|
"""
|
|
for line in self:
|
|
amount_to_invoice = 0.0
|
|
if line.state != "draft":
|
|
# Note: do not use price_subtotal field as it returns
|
|
# zero when the ordered quantity is zero.
|
|
# It causes problem for expense line (e.i.: ordered qty = 0,
|
|
# deli qty = 4, price_unit = 20 ; subtotal is zero),
|
|
# but when you can invoice the line,
|
|
# you see an amount and not zero.
|
|
# Since we compute untaxed amount, we can use directly the price
|
|
# reduce (to include discount) without using `compute_all()`
|
|
# method on taxes.
|
|
price_subtotal = 0.0
|
|
price_subtotal = line.price_reduce * line.product_uom_qty
|
|
if len(line.tax_ids.filtered(lambda tax: tax.price_include)) > 0:
|
|
# As included taxes are not excluded from the computed subtotal,
|
|
# `compute_all()` method has to be called to retrieve
|
|
# the subtotal without them.
|
|
# `price_reduce_taxexcl` cannot be used as it is computed from
|
|
# `price_subtotal` field. (see upper Note)
|
|
price_subtotal = line.tax_ids.compute_all(
|
|
price_subtotal,
|
|
currency=line.folio_id.currency_id,
|
|
quantity=line.product_uom_qty,
|
|
product=line.product_id,
|
|
partner=line.folio_id.partner_shipping_id,
|
|
)["total_excluded"]
|
|
|
|
if any(
|
|
line.invoice_lines.mapped(lambda l: l.discount != line.discount)
|
|
):
|
|
# In case of re-invoicing with different
|
|
# discount we try to calculate manually the
|
|
# remaining amount to invoice
|
|
amount = 0
|
|
for inv_line in line.invoice_lines:
|
|
if (
|
|
len(
|
|
inv_line.tax_ids.filtered(lambda tax: tax.price_include)
|
|
)
|
|
> 0
|
|
):
|
|
amount += inv_line.tax_ids.compute_all(
|
|
inv_line.currency_id._convert(
|
|
inv_line.price_unit,
|
|
line.currency_id,
|
|
line.company_id,
|
|
inv_line.date or fields.Date.today(),
|
|
round=False,
|
|
)
|
|
* inv_line.quantity
|
|
)["total_excluded"]
|
|
else:
|
|
amount += (
|
|
inv_line.currency_id._convert(
|
|
inv_line.price_unit,
|
|
line.currency_id,
|
|
line.company_id,
|
|
inv_line.date or fields.Date.today(),
|
|
round=False,
|
|
)
|
|
* inv_line.quantity
|
|
)
|
|
|
|
amount_to_invoice = max(price_subtotal - amount, 0)
|
|
else:
|
|
amount_to_invoice = price_subtotal - line.untaxed_amount_invoiced
|
|
|
|
line.untaxed_amount_to_invoice = amount_to_invoice
|
|
|
|
@api.depends("state", "product_uom_qty", "qty_to_invoice", "qty_invoiced")
|
|
def _compute_invoice_status(self):
|
|
"""
|
|
Compute the invoice status of a SO line:
|
|
Its if compute based on reservations/services associated status
|
|
"""
|
|
precision = self.env["decimal.precision"].precision_get(
|
|
"Product Unit of Measure"
|
|
)
|
|
for line in self:
|
|
if line.state == "draft":
|
|
line.invoice_status = "no"
|
|
# REVIEW: if qty_to_invoice < 0 (invoice qty > sale qty),
|
|
# why status to_invoice?? this behavior is copied from sale order
|
|
# https://github.com/OCA/OCB/blob/14.0/addons/sale/models/sale.py#L1160
|
|
elif not float_is_zero(line.qty_to_invoice, precision_digits=precision):
|
|
line.invoice_status = "to_invoice"
|
|
elif (
|
|
float_compare(
|
|
line.qty_invoiced,
|
|
line.product_uom_qty,
|
|
precision_digits=precision,
|
|
)
|
|
>= 0
|
|
):
|
|
line.invoice_status = "invoiced"
|
|
else:
|
|
line.invoice_status = "no"
|
|
|
|
@api.depends("reservation_line_ids", "service_line_ids", "service_id")
|
|
def _compute_name(self):
|
|
for record in self:
|
|
record.name = self.generate_folio_sale_name(
|
|
record.reservation_id,
|
|
record.product_id,
|
|
record.service_id,
|
|
record.reservation_line_ids,
|
|
record.service_line_ids,
|
|
)
|
|
|
|
@api.depends("product_uom_qty", "discount", "price_unit", "tax_ids")
|
|
def _compute_amount(self):
|
|
"""
|
|
Compute the amounts of the Sale line.
|
|
"""
|
|
for line in self:
|
|
price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
|
|
taxes = line.tax_ids.compute_all(
|
|
price,
|
|
line.folio_id.currency_id,
|
|
line.product_uom_qty,
|
|
product=line.product_id,
|
|
)
|
|
line.update(
|
|
{
|
|
"price_tax": sum(
|
|
t.get("amount", 0.0) for t in taxes.get("taxes", [])
|
|
),
|
|
"price_total": taxes["total_included"],
|
|
"price_subtotal": taxes["total_excluded"],
|
|
}
|
|
)
|
|
if self.env.context.get(
|
|
"import_file", False
|
|
) and not self.env.user.user_has_groups("account.group_account_manager"):
|
|
line.tax_ids.invalidate_cache(
|
|
["invoice_repartition_line_ids"], [line.tax_ids.id]
|
|
)
|
|
|
|
@api.depends("reservation_id.tax_ids", "service_id.tax_ids")
|
|
def _compute_tax_ids(self):
|
|
for record in self:
|
|
record.tax_ids = (
|
|
record.service_id.tax_ids
|
|
if record.service_id
|
|
else record.reservation_id.tax_ids
|
|
)
|
|
|
|
@api.depends(
|
|
"service_id",
|
|
"service_id.service_line_ids",
|
|
"service_id.service_line_ids.discount",
|
|
)
|
|
def _compute_discount(self):
|
|
"""
|
|
Only in services without room we compute discount,
|
|
and this services only have one service line
|
|
"""
|
|
for record in self:
|
|
if record.service_id and not record.service_id.reservation_id:
|
|
record.discount = record.service_id.service_line_ids.mapped("discount")[
|
|
0
|
|
]
|
|
elif not record.discount:
|
|
record.discount = 0
|
|
|
|
@api.depends("reservation_id.room_type_id", "service_id.product_id")
|
|
def _compute_product_id(self):
|
|
for record in self:
|
|
if record.reservation_id and not record.service_id:
|
|
record.product_id = record.reservation_id.room_type_id.product_id
|
|
elif record.service_id:
|
|
record.product_id = record.service_id.product_id
|
|
else:
|
|
record.product_id = False
|
|
|
|
# @api.depends('product_id', 'folio_id.state', 'qty_invoiced', 'qty_delivered')
|
|
# def _compute_product_updatable(self):
|
|
# for line in self:
|
|
# if line.state in ['done', 'cancel'] or (
|
|
# line.state == 'sale' and (
|
|
# line.qty_invoiced > 0 or line.qty_delivered > 0)):
|
|
# line.product_updatable = False
|
|
# else:
|
|
# line.product_updatable = True
|
|
|
|
# no trigger product_id.invoice_policy to avoid retroactively changing SO
|
|
@api.depends("qty_invoiced", "product_uom_qty", "folio_id.state")
|
|
def _compute_get_to_invoice_qty(self):
|
|
"""
|
|
Compute the quantity to invoice.
|
|
If the invoice policy is order, the quantity to invoice is
|
|
calculated from the ordered quantity.
|
|
Otherwise, the quantity delivered is used.
|
|
"""
|
|
for line in self:
|
|
if line.folio_id.state not in ["draft"]:
|
|
line.qty_to_invoice = line.product_uom_qty - line.qty_invoiced
|
|
else:
|
|
line.qty_to_invoice = 0
|
|
|
|
@api.depends(
|
|
"invoice_lines.move_id.state",
|
|
"invoice_lines.quantity",
|
|
"untaxed_amount_to_invoice",
|
|
)
|
|
def _compute_get_invoice_qty(self):
|
|
"""
|
|
Compute the quantity invoiced. If case of a refund,
|
|
the quantity invoiced is decreased. Note
|
|
that this is the case only if the refund is
|
|
generated from the Folio and that is intentional: if
|
|
a refund made would automatically decrease the invoiced quantity,
|
|
then there is a risk of reinvoicing
|
|
it automatically, which may not be wanted at all.
|
|
That's why the refund has to be created from the Folio
|
|
"""
|
|
for line in self:
|
|
qty_invoiced = 0.0
|
|
for invoice_line in line.invoice_lines:
|
|
if invoice_line.move_id.state != "cancel":
|
|
if invoice_line.move_id.move_type == "out_invoice":
|
|
qty_invoiced += invoice_line.product_uom_id._compute_quantity(
|
|
invoice_line.quantity, line.product_uom
|
|
)
|
|
elif invoice_line.move_id.move_type == "out_refund":
|
|
if (
|
|
not line.is_downpayment
|
|
or line.untaxed_amount_to_invoice == 0
|
|
):
|
|
qty_invoiced -= (
|
|
invoice_line.product_uom_id._compute_quantity(
|
|
invoice_line.quantity, line.product_uom
|
|
)
|
|
)
|
|
line.qty_invoiced = qty_invoiced
|
|
|
|
@api.depends("price_unit", "discount")
|
|
def _compute_get_price_reduce(self):
|
|
for line in self:
|
|
line.price_reduce = line.price_unit * (1.0 - line.discount / 100.0)
|
|
|
|
@api.depends("price_total", "product_uom_qty")
|
|
def _compute_get_price_reduce_tax(self):
|
|
for line in self:
|
|
line.price_reduce_taxinc = (
|
|
line.price_total / line.product_uom_qty if line.product_uom_qty else 0.0
|
|
)
|
|
|
|
@api.depends("price_subtotal", "product_uom_qty")
|
|
def _compute_get_price_reduce_notax(self):
|
|
for line in self:
|
|
line.price_reduce_taxexcl = (
|
|
line.price_subtotal / line.product_uom_qty
|
|
if line.product_uom_qty
|
|
else 0.0
|
|
)
|
|
|
|
# @api.model
|
|
# def _prepare_add_missing_fields(self, values):
|
|
# """ Deduce missing required fields from the onchange """
|
|
# res = {}
|
|
# onchange_fields = ['name', 'price_unit', 'product_uom', 'tax_ids']
|
|
# if values.get('folio_id') and values.get('product_id') and any(
|
|
# f not in values for f in onchange_fields
|
|
# ):
|
|
# line = self.new(values)
|
|
# line.product_id_change()
|
|
# for field in onchange_fields:
|
|
# if field not in values:
|
|
# res[field] = line._fields[field].convert_to_write(
|
|
# line[field], line
|
|
# )
|
|
# return res
|
|
|
|
# @api.model_create_multi
|
|
# def create(self, vals_list):
|
|
# for values in vals_list:
|
|
# if values.get('display_type', self.default_get(
|
|
# ['display_type'])['display_type']
|
|
# ):
|
|
# values.update(product_id=False, price_unit=0,
|
|
# product_uom_qty=0, product_uom=False,
|
|
# customer_lead=0)
|
|
|
|
# values.update(self._prepare_add_missing_fields(values))
|
|
|
|
# lines = super().create(vals_list)
|
|
# for line in lines:
|
|
# if line.product_id and line.folio_id.state == 'sale':
|
|
# msg = _("Extra line with %s ") % (line.product_id.display_name,)
|
|
# line.folio_id.message_post(body=msg)
|
|
# # create an analytic account if at least an expense product
|
|
# if line.product_id.expense_policy not in [False, 'no'] and \
|
|
# not line.folio_id.analytic_account_id:
|
|
# line.folio_id._create_analytic_account()
|
|
# return lines
|
|
|
|
# _sql_constraints = [
|
|
# ('accountable_required_fields',
|
|
# "CHECK(display_type IS NOT NULL OR \
|
|
# (product_id IS NOT NULL AND product_uom IS NOT NULL))",
|
|
# "Missing required fields on accountable sale order line."),
|
|
# ('non_accountable_null_fields',
|
|
# "CHECK(display_type IS NULL OR (product_id IS NULL AND \
|
|
# price_unit = 0 AND product_uom_qty = 0 AND \
|
|
# product_uom IS NULL AND customer_lead = 0))",
|
|
# "Forbidden values on non-accountable sale order line"),
|
|
# ]
|
|
@api.model
|
|
def _name_search(
|
|
self, name, args=None, operator="ilike", limit=100, name_get_uid=None
|
|
):
|
|
if operator in ("ilike", "like", "=", "=like", "=ilike"):
|
|
args = expression.AND(
|
|
[
|
|
args or [],
|
|
["|", ("folio_id.name", operator, name), ("name", operator, name)],
|
|
]
|
|
)
|
|
return super(FolioSaleLine, self)._name_search(
|
|
name, args=args, operator=operator, limit=limit, name_get_uid=name_get_uid
|
|
)
|
|
|
|
def name_get(self):
|
|
result = []
|
|
for so_line in self.sudo():
|
|
name = "{} - {}".format(
|
|
so_line.folio_id.name,
|
|
so_line.name and so_line.name.split("\n")[0] or so_line.product_id.name,
|
|
)
|
|
result.append((so_line.id, name))
|
|
return result
|
|
|
|
@api.model
|
|
def generate_folio_sale_name(
|
|
self,
|
|
reservation_id,
|
|
product_id,
|
|
service_id,
|
|
reservation_line_ids,
|
|
service_line_ids,
|
|
qty=False,
|
|
):
|
|
if reservation_line_ids:
|
|
month = False
|
|
name = False
|
|
lines = reservation_line_ids.sorted(key="date")
|
|
for index, date in enumerate(lines.mapped("date")):
|
|
if qty and index > (qty - 1):
|
|
break
|
|
if date.month != month:
|
|
name = name + "\n" if name else ""
|
|
name += date.strftime("%B-%Y") + ": "
|
|
name += date.strftime("%d")
|
|
month = date.month
|
|
else:
|
|
name += ", " + date.strftime("%d")
|
|
|
|
return "{} ({}).".format(product_id.name, name)
|
|
elif service_line_ids:
|
|
month = False
|
|
name = False
|
|
lines = service_line_ids.filtered(
|
|
lambda x: x.service_id == service_id
|
|
).sorted(key="date")
|
|
|
|
for index, date in enumerate(lines.mapped("date")):
|
|
if qty and index > (ceil(qty / reservation_id.adults) - 1):
|
|
break
|
|
if date.month != month:
|
|
name = name + "\n" if name else ""
|
|
name += date.strftime("%B-%Y") + ": "
|
|
name += date.strftime("%d")
|
|
month = date.month
|
|
else:
|
|
name += ", " + date.strftime("%d")
|
|
return "{} ({}).".format(service_id.name, name)
|
|
else:
|
|
return service_id.name
|
|
|
|
def _get_invoice_line_sequence(self, new=0, old=0):
|
|
"""
|
|
Method intended to be overridden in third-party
|
|
module if we want to prevent the resequencing of invoice lines.
|
|
|
|
:param int new: the new line sequence
|
|
:param int old: the old line sequence
|
|
|
|
:return: the sequence of the SO line, by default the new one.
|
|
"""
|
|
return new or old
|
|
|
|
def _update_line_quantity(self, values):
|
|
folios = self.mapped("folio_id")
|
|
for order in folios:
|
|
order_lines = self.filtered(lambda x: x.folio_id == order)
|
|
msg = "<b>" + _("The ordered quantity has been updated.") + "</b><ul>"
|
|
for line in order_lines:
|
|
msg += "<li> %s: <br/>" % line.product_id.display_name
|
|
msg += (
|
|
_(
|
|
"Ordered Quantity: %(old_qty)s -> %(new_qty)s",
|
|
old_qty=line.product_uom_qty,
|
|
new_qty=values["product_uom_qty"],
|
|
)
|
|
+ "<br/>"
|
|
)
|
|
# if line.product_id.type in ('consu', 'product'):
|
|
# msg += _("Delivered Quantity: %s", line.qty_delivered) + "<br/>"
|
|
msg += _("Invoiced Quantity: %s", line.qty_invoiced) + "<br/>"
|
|
msg += "</ul>"
|
|
order.message_post(body=msg)
|
|
|
|
# def write(self, values):
|
|
# if 'display_type' in values and self.filtered(
|
|
# lambda line: line.display_type != values.get('display_type')):
|
|
# raise UserError(_("You cannot change the type of a sale order line.\
|
|
# Instead you should delete the current line and create \
|
|
# a new line of the proper type."))
|
|
|
|
# if 'product_uom_qty' in values:
|
|
# precision = self.env['decimal.precision'].precision_get(
|
|
# 'Product Unit of Measure'
|
|
# )
|
|
# self.filtered(
|
|
# lambda r: r.state == 'sale' and \
|
|
# float_compare(
|
|
# r.product_uom_qty,
|
|
# values['product_uom_qty'],
|
|
# precision_digits=precision) != 0)._update_line_quantity(
|
|
# values
|
|
# )
|
|
|
|
# # Prevent writing on a locked SO.
|
|
# protected_fields = self._get_protected_fields()
|
|
# if 'done' in self.mapped('folio_id.state') and any(
|
|
# f in values.keys() for f in protected_fields
|
|
# ):
|
|
# protected_fields_modified = list(set(protected_fields) & set(
|
|
# values.keys()
|
|
# ))
|
|
# fields = self.env['ir.model.fields'].search([
|
|
# ('name', 'in', protected_fields_modified),
|
|
# ('model', '=', self._name)
|
|
# ])
|
|
# raise UserError(
|
|
# _('It is forbidden to modify the following \
|
|
# fields in a locked order:\n%s')
|
|
# % '\n'.join(fields.mapped('field_description'))
|
|
# )
|
|
|
|
# result = super(SaleOrderLine, self).write(values)
|
|
# return resul def _prepare_invoice_line(self, qty=False, **optional_values):
|
|
def _prepare_invoice_line(self, qty=False, **optional_values):
|
|
"""
|
|
Prepare the dict of values to create the new invoice line for a folio sale line.
|
|
|
|
:param qty: float quantity to invoice
|
|
:param optional_values: any parameter that
|
|
should be added to the returned invoice line
|
|
"""
|
|
self.ensure_one()
|
|
if (qty > self.qty_to_invoice or qty < 1) and not self.display_type:
|
|
raise ValueError(
|
|
_(
|
|
"The qty (%s) is wrong." % qty
|
|
+ " The quantity pending to invoice is %s" % self.qty_to_invoice
|
|
)
|
|
)
|
|
res = {
|
|
"display_type": self.display_type,
|
|
"sequence": self.sequence,
|
|
"name": self.name,
|
|
"product_id": self.product_id.id,
|
|
"product_uom_id": self.product_uom.id,
|
|
"quantity": qty if qty else self.qty_to_invoice,
|
|
"discount": self.discount,
|
|
"price_unit": self.price_unit,
|
|
"tax_ids": [(6, 0, self.tax_ids.ids)],
|
|
"analytic_account_id": self.folio_id.analytic_account_id.id,
|
|
"analytic_tag_ids": [(6, 0, self.analytic_tag_ids.ids)],
|
|
"folio_line_ids": [(6, 0, [self.id])],
|
|
}
|
|
if optional_values:
|
|
res.update(optional_values)
|
|
if self.display_type:
|
|
res["account_id"] = False
|
|
return res
|
|
|
|
def _check_line_unlink(self):
|
|
"""
|
|
Check wether a line can be deleted or not.
|
|
|
|
Lines cannot be deleted if the folio is confirmed; downpayment
|
|
lines who have not yet been invoiced bypass that exception.
|
|
:rtype: recordset folio.sale.line
|
|
:returns: set of lines that cannot be deleted
|
|
"""
|
|
return self.filtered(
|
|
lambda line: line.state not in ("draft")
|
|
and (line.invoice_lines or not line.is_downpayment)
|
|
)
|
|
|
|
# def unlink(self):
|
|
# if self._check_line_unlink():
|
|
# raise UserError(
|
|
# _("""You can not remove an sale line once the sales
|
|
# folio is confirmed.\n
|
|
# You should rather set the quantity to 0.""")
|
|
# )
|
|
# return super(FolioSaleLine, self).unlink()
|
|
|
|
def _get_real_price_currency(self, product, rule_id, qty, uom, pricelist_id):
|
|
"""Retrieve the price before applying the pricelist
|
|
:param obj product: object of current product record
|
|
:parem float qty: total quentity of product
|
|
:param tuple price_and_rule: tuple(price, suitable_rule)
|
|
coming from pricelist computation
|
|
:param obj uom: unit of measure of current folio line
|
|
:param integer pricelist_id: pricelist id of folio"""
|
|
PricelistItem = self.env["product.pricelist.item"]
|
|
field_name = "lst_price"
|
|
currency_id = None
|
|
product_currency = product.currency_id
|
|
if rule_id:
|
|
pricelist_item = PricelistItem.browse(rule_id)
|
|
if pricelist_item.pricelist_id.discount_policy == "without_discount":
|
|
while (
|
|
pricelist_item.base == "pricelist"
|
|
and pricelist_item.base_pricelist_id
|
|
and pricelist_item.base_pricelist_id.discount_policy
|
|
== "without_discount"
|
|
):
|
|
price, rule_id = pricelist_item.base_pricelist_id.with_context(
|
|
uom=uom.id
|
|
).get_product_price_rule(product, qty, self.folio_id.partner_id)
|
|
pricelist_item = PricelistItem.browse(rule_id)
|
|
|
|
if pricelist_item.base == "standard_price":
|
|
field_name = "standard_price"
|
|
product_currency = product.cost_currency_id
|
|
elif (
|
|
pricelist_item.base == "pricelist" and pricelist_item.base_pricelist_id
|
|
):
|
|
field_name = "price"
|
|
product = product.with_context(
|
|
pricelist=pricelist_item.base_pricelist_id.id
|
|
)
|
|
product_currency = pricelist_item.base_pricelist_id.currency_id
|
|
currency_id = pricelist_item.pricelist_id.currency_id
|
|
|
|
if not currency_id:
|
|
currency_id = product_currency
|
|
cur_factor = 1.0
|
|
else:
|
|
if currency_id.id == product_currency.id:
|
|
cur_factor = 1.0
|
|
else:
|
|
cur_factor = currency_id._get_conversion_rate(
|
|
product_currency,
|
|
currency_id,
|
|
self.company_id or self.env.company,
|
|
self.folio_id.date_order or fields.Date.today(),
|
|
)
|
|
|
|
product_uom = self.env.context.get("uom") or product.uom_id.id
|
|
if uom and uom.id != product_uom:
|
|
# the unit price is in a different uom
|
|
uom_factor = uom._compute_price(1.0, product.uom_id)
|
|
else:
|
|
uom_factor = 1.0
|
|
|
|
return product[field_name] * uom_factor * cur_factor, currency_id
|
|
|
|
def _get_protected_fields(self):
|
|
return [
|
|
"product_id",
|
|
"name",
|
|
"price_unit",
|
|
"product_uom",
|
|
"product_uom_qty",
|
|
"tax_ids",
|
|
"analytic_tag_ids",
|
|
]
|