[DONE] 14.0 improve compute folio sale lines and name invoice lines on change qty (#78)

* [IMP] pms: improve compute sale lines & generate new names when qty is modified @ invoice lines

* [ADD] pms: add tests for compute folio sale lines

* [FIX] pms: fix old name for av. plan & common base test class

* [FIX] pms: pre-commit fix

* [FIX] pms: fix view according to pr code review
This commit is contained in:
Miguel Padin
2021-04-19 09:19:13 +02:00
committed by GitHub
parent f99183805d
commit 3e9a6b00c5
9 changed files with 1707 additions and 119 deletions

View File

@@ -1,13 +1,14 @@
# Copyright 2017 Alexandre Díaz # Copyright 2017 Alexandre Díaz
# Copyright 2017 Dario Lodeiros # Copyright 2017 Dario Lodeiros
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
from odoo import fields, models from odoo import api, fields, models
class AccountMoveLine(models.Model): class AccountMoveLine(models.Model):
_inherit = "account.move.line" _inherit = "account.move.line"
# Fields declaration # Fields declaration
# TODO: REVIEW why not a Many2one?
folio_line_ids = fields.Many2many( folio_line_ids = fields.Many2many(
"folio.sale.line", "folio.sale.line",
"folio_sale_line_invoice_rel", "folio_sale_line_invoice_rel",
@@ -23,6 +24,50 @@ class AccountMoveLine(models.Model):
"folio_id", "folio_id",
string="Folios", string="Folios",
) )
name_changed_by_user = fields.Boolean(
default=False,
readonly=False,
store=True,
string="Custom label",
compute="_compute_name_changed_by_user",
)
@api.depends("name")
def _compute_name_changed_by_user(self):
for record in self:
# if not record._context.get("auto_name"):
if not self._context.get("auto_name"):
record.name_changed_by_user = True
else:
record.name_changed_by_user = False
name = fields.Char(
compute="_compute_name",
store=True,
readonly=False,
)
@api.depends("quantity")
def _compute_name(self):
for record in self:
record.name = self.env["folio.sale.line"].generate_folio_sale_name(
record.folio_line_ids.reservation_id,
record.product_id,
record.folio_line_ids.service_id,
record.folio_line_ids.reservation_line_ids,
record.folio_line_ids.service_line_ids,
qty=record.quantity,
)
# TODO: check why this code doesn't work
# if not record.name_changed_by_user:
# record.with_context(auto_name=True).name = self
# .env["folio.sale.line"].generate_folio_sale_name(
# record.folio_line_ids.service_id,
# record.folio_line_ids.reservation_line_ids,
# record.product_id,
# qty=record.quantity)
# record.with_context(auto_name=True)
# ._compute_name_changed_by_user()
def _copy_data_extend_business_fields(self, values): def _copy_data_extend_business_fields(self, values):
super(AccountMoveLine, self)._copy_data_extend_business_fields(values) super(AccountMoveLine, self)._copy_data_extend_business_fields(values)

View File

@@ -1,6 +1,8 @@
# Copyright 2020 Dario Lodeiros # Copyright 2020 Dario Lodeiros
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from math import ceil
from odoo import _, api, fields, models from odoo import _, api, fields, models
from odoo.osv import expression from odoo.osv import expression
from odoo.tools import float_compare, float_is_zero from odoo.tools import float_compare, float_is_zero
@@ -9,7 +11,8 @@ from odoo.tools import float_compare, float_is_zero
class FolioSaleLine(models.Model): class FolioSaleLine(models.Model):
_name = "folio.sale.line" _name = "folio.sale.line"
_description = "Folio Sale Line" _description = "Folio Sale Line"
_order = "folio_id, sequence, id" _order = "folio_id, sequence, reservation_order desc, service_order, date_order"
_check_company_auto = True _check_company_auto = True
@api.depends("state", "product_uom_qty", "qty_to_invoice", "qty_invoiced") @api.depends("state", "product_uom_qty", "qty_to_invoice", "qty_invoiced")
@@ -41,26 +44,34 @@ class FolioSaleLine(models.Model):
else: else:
line.invoice_status = "no" line.invoice_status = "no"
@api.depends("reservation_line_ids", "service_id") @api.depends("reservation_line_ids", "service_line_ids", "service_id")
def _compute_name(self): def _compute_name(self):
for record in self: for record in self:
if not record.name_updated: record.name = self.generate_folio_sale_name(
record.name = record._get_compute_name() record.reservation_id,
record.product_id,
record.service_id,
record.reservation_line_ids,
record.service_line_ids,
)
@api.depends("name") @api.model
def _compute_name_updated(self): def generate_folio_sale_name(
self.name_updated = False self,
for record in self.filtered("name"): reservation_id,
if record.name != record._get_compute_name(): product_id,
record.name_updated = True service_id,
reservation_line_ids,
def _get_compute_name(self): service_line_ids,
self.ensure_one() qty=False,
if self.reservation_line_ids: ):
if reservation_line_ids:
month = False month = False
name = False name = False
lines = self.reservation_line_ids.sorted("date") lines = reservation_line_ids.sorted(key="date")
for date in lines.mapped("date"): for index, date in enumerate(lines.mapped("date")):
if qty and index > (qty - 1):
break
if date.month != month: if date.month != month:
name = name + "\n" if name else "" name = name + "\n" if name else ""
name += date.strftime("%B-%Y") + ": " name += date.strftime("%B-%Y") + ": "
@@ -68,11 +79,28 @@ class FolioSaleLine(models.Model):
month = date.month month = date.month
else: else:
name += ", " + date.strftime("%d") name += ", " + date.strftime("%d")
return name
elif self.service_id: return "{} ({}).".format(product_id.name, name)
return self.service_id.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: else:
return False return service_id.name
@api.depends("product_uom_qty", "discount", "price_unit", "tax_ids") @api.depends("product_uom_qty", "discount", "price_unit", "tax_ids")
def _compute_amount(self): def _compute_amount(self):
@@ -133,7 +161,7 @@ class FolioSaleLine(models.Model):
@api.depends("reservation_id.room_type_id", "service_id.product_id") @api.depends("reservation_id.room_type_id", "service_id.product_id")
def _compute_product_id(self): def _compute_product_id(self):
for record in self: for record in self:
if record.reservation_id: if record.reservation_id and not record.service_id:
record.product_id = record.reservation_id.room_type_id.product_id record.product_id = record.reservation_id.room_type_id.product_id
elif record.service_id: elif record.service_id:
record.product_id = record.service_id.product_id record.product_id = record.service_id.product_id
@@ -367,7 +395,6 @@ class FolioSaleLine(models.Model):
name = fields.Text( name = fields.Text(
string="Description", compute="_compute_name", store=True, readonly=False string="Description", compute="_compute_name", store=True, readonly=False
) )
name_updated = fields.Boolean(compute="_compute_name_updated", store=True)
reservation_line_ids = fields.Many2many( reservation_line_ids = fields.Many2many(
"pms.reservation.line", "pms.reservation.line",
string="Nights", string="Nights",
@@ -562,6 +589,68 @@ class FolioSaleLine(models.Model):
help="Technical field for UX purpose.", help="Technical field for UX purpose.",
) )
service_order = fields.Integer(
string="Service id",
compute="_compute_service_order",
help="Field to order by service id",
store=True,
readonly=True,
)
reservation_order = fields.Integer(
string="Reservation id",
compute="_compute_reservation_order",
help="Field to order by reservation id",
store=True,
readonly=True,
)
date_order = fields.Date(
string="Date",
compute="_compute_date_order",
help="Field to order by service date",
store=True,
readonly=True,
)
@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") @api.depends("reservation_line_ids", "service_line_ids", "service_line_ids.day_qty")
def _compute_product_uom_qty(self): def _compute_product_uom_qty(self):
for line in self: for line in self:

View File

@@ -123,8 +123,6 @@ class PmsFolio(models.Model):
readonly=True, readonly=True,
required=True, required=True,
ondelete="restrict", ondelete="restrict",
# comodel_name="res.currency",
# compute="_compute_currency_id"
) )
pricelist_id = fields.Many2one( pricelist_id = fields.Many2one(
"product.pricelist", "product.pricelist",
@@ -259,7 +257,6 @@ class PmsFolio(models.Model):
client_order_ref = fields.Char(string="Customer Reference", copy=False) client_order_ref = fields.Char(string="Customer Reference", copy=False)
reservation_type = fields.Selection( reservation_type = fields.Selection(
[("normal", "Normal"), ("staff", "Staff"), ("out", "Out of Service")], [("normal", "Normal"), ("staff", "Staff"), ("out", "Out of Service")],
required=True,
string="Type", string="Type",
default=lambda *a: "normal", default=lambda *a: "normal",
) )
@@ -397,87 +394,139 @@ class PmsFolio(models.Model):
"service_ids.service_line_ids.price_day_total", "service_ids.service_line_ids.price_day_total",
"service_ids.service_line_ids.discount", "service_ids.service_line_ids.discount",
"service_ids.service_line_ids.cancel_discount", "service_ids.service_line_ids.cancel_discount",
"service_ids.service_line_ids.day_qty",
"service_ids.service_line_ids.tax_ids",
"reservation_ids.reservation_line_ids", "reservation_ids.reservation_line_ids",
"reservation_ids.reservation_line_ids.price", "reservation_ids.reservation_line_ids.price",
"reservation_ids.reservation_line_ids.discount", "reservation_ids.reservation_line_ids.discount",
"reservation_ids.reservation_line_ids.cancel_discount", "reservation_ids.reservation_line_ids.cancel_discount",
"reservation_ids.tax_ids",
) )
def _compute_sale_line_ids(self): def _compute_sale_line_ids(self):
for folio in self: for folio in self:
sale_lines = [(5, 0, 0)] for reservation in folio.reservation_ids:
reservations = folio.reservation_ids # RESERVATION LINES
services_without_room = folio.service_ids.filtered( # res = self.env['pms.reservation'].browse(reservation.id)
lambda s: not s.reservation_id self.generate_reservation_lines_sale_lines(folio, reservation)
)
# TODO: Not delete old sale line ids # RESERVATION SERVICES
for reservation in reservations: self.generate_reservation_services_sale_lines(folio, reservation)
sale_lines.append(
( # FOLIO SERVICES
0, self.generate_folio_services_sale_lines(folio)
False,
{ @api.model
"display_type": "line_section", def generate_reservation_lines_sale_lines(self, folio, reservation):
"name": reservation.name, if not reservation.sale_line_ids.filtered(lambda x: x.name == reservation.name):
}, reservation.sale_line_ids = [
) (
0,
0,
{
"name": reservation.name,
"display_type": "line_section",
"folio_id": folio.id,
},
) )
group_reservation_lines = {} ]
for line in reservation.reservation_line_ids: expected_reservation_lines = self.env["pms.reservation.line"].read_group(
# On resevations the price, and discounts fields are used [
# by group, we need pass this in the create line ("reservation_id", "=", reservation.id),
group_key = ( ("cancel_discount", "<", 100),
reservation.id, ],
line.price, ["price", "discount", "cancel_discount"],
line.discount, ["price", "discount", "cancel_discount"],
line.cancel_discount, lazy=False,
) )
if line.cancel_discount == 100: current_sale_line_ids = reservation.sale_line_ids.filtered(
continue lambda x: x.reservation_id.id == reservation.id
discount_factor = 1.0 and not x.display_type
for discount in [line.discount, line.cancel_discount]: and not x.service_id
discount_factor = discount_factor * ((100.0 - discount) / 100.0) )
final_discount = 100.0 - (discount_factor * 100.0)
if group_key not in group_reservation_lines: for index, item in enumerate(expected_reservation_lines):
group_reservation_lines[group_key] = { lines_to = self.env["pms.reservation.line"].search(item["__domain"])
"reservation_id": reservation.id, final_discount = self.concat_discounts(
"discount": final_discount, item["discount"], item["cancel_discount"]
"price_unit": line.price, )
"reservation_line_ids": [(4, line.id)],
} if current_sale_line_ids and index <= (len(current_sale_line_ids) - 1):
else: current_sale_line_ids[index].price_unit = item["price"]
group_reservation_lines[group_key][ current_sale_line_ids[index].discount = final_discount
("reservation_line_ids") current_sale_line_ids[index].reservation_line_ids = lines_to.ids
].append((4, line.id)) else:
for item in group_reservation_lines.items(): new = {
sale_lines.append((0, False, item[1])) "reservation_id": reservation.id,
for service in reservation.service_ids: "price_unit": item["price"],
# Service days with different prices, "discount": final_discount,
# go to differente sale lines "folio_id": folio.id,
group_service_lines = {} "reservation_line_ids": [(6, 0, lines_to.ids)],
for service_line in service.service_line_ids: }
service_group_key = ( reservation.sale_line_ids = [(0, 0, new)]
service_line.price_unit, if len(expected_reservation_lines) < len(current_sale_line_ids):
service_line.discount, folio_sale_lines_to_remove = [
service_line.cancel_discount, value.id
) for index, value in enumerate(current_sale_line_ids)
if service_group_key not in group_service_lines: if index > (len(expected_reservation_lines) - 1)
# On service the price, and discounts fields are ]
# compute in the sale.order.line for fsl in folio_sale_lines_to_remove:
group_service_lines[service_group_key] = { self.env["folio.sale.line"].browse(fsl).unlink()
"name": service.name,
"service_id": service.id, @api.model
"discount": service_line.discount, def generate_reservation_services_sale_lines(self, folio, reservation):
"price_unit": service_line.price_unit, for service in reservation.service_ids:
"service_line_ids": [(4, service_line.id)], expected_reservation_services = self.env["pms.service.line"].read_group(
} [
else: ("reservation_id", "=", reservation.id),
group_service_lines[service_group_key][ ("service_id", "=", service.id),
("service_line_ids") ("cancel_discount", "<", 100),
].append((4, service_line.id)) ],
for item in group_service_lines.items(): ["price_unit", "discount", "cancel_discount"],
sale_lines.append((0, False, item[1])) ["price_unit", "discount", "cancel_discount"],
if services_without_room: lazy=False,
sale_lines.append( )
current_sale_service_ids = reservation.sale_line_ids.filtered(
lambda x: x.reservation_id.id == reservation.id
and not x.display_type
and x.service_id.id == service.id
)
for index, item in enumerate(expected_reservation_services):
lines_to = self.env["pms.service.line"].search(item["__domain"])
final_discount = self.concat_discounts(
item["discount"], item["cancel_discount"]
)
if current_sale_service_ids and index <= (
len(current_sale_service_ids) - 1
):
current_sale_service_ids[index].price_unit = item["price_unit"]
current_sale_service_ids[index].discount = final_discount
current_sale_service_ids[index].service_line_ids = lines_to.ids
else:
new = {
"service_id": service.id,
"price_unit": item["price_unit"],
"discount": final_discount,
"folio_id": folio.id,
"service_line_ids": [(6, 0, lines_to.ids)],
}
reservation.sale_line_ids = [(0, 0, new)]
if len(expected_reservation_services) < len(current_sale_service_ids):
folio_sale_lines_to_remove = [
value.id
for index, value in enumerate(current_sale_service_ids)
if index > (len(expected_reservation_services) - 1)
]
for fsl in folio_sale_lines_to_remove:
self.env["folio.sale.line"].browse(fsl).unlink()
@api.model
def generate_folio_services_sale_lines(self, folio):
folio_services = folio.service_ids.filtered(lambda x: not x.reservation_id)
if folio_services:
if not folio.sale_line_ids.filtered(lambda x: x.name == _("Others")):
folio.sale_line_ids = [
( (
0, 0,
False, False,
@@ -486,19 +535,65 @@ class PmsFolio(models.Model):
"name": _("Others"), "name": _("Others"),
}, },
) )
]
for folio_service in folio_services:
expected_folio_services = self.env["pms.service.line"].read_group(
[
("service_id.folio_id", "=", folio.id),
("service_id", "=", folio_service.id),
("reservation_id", "=", False),
("cancel_discount", "<", 100),
],
["price_unit", "discount", "cancel_discount"],
["price_unit", "discount", "cancel_discount"],
lazy=False,
) )
for service in services_without_room: current_folio_service_ids = folio.sale_line_ids.filtered(
sale_lines.append( lambda x: x.service_id.folio_id.id == folio.id
( and not x.display_type
0, and not x.reservation_id
False, and x.service_id.id == folio_service.id
{ )
"name": service.name,
"service_id": service.id, for index, item in enumerate(expected_folio_services):
}, lines_to = self.env["pms.service.line"].search(item["__domain"])
) final_discount = self.concat_discounts(
item["discount"], item["cancel_discount"]
) )
folio.sale_line_ids = sale_lines if current_folio_service_ids and index <= (
len(current_folio_service_ids) - 1
):
current_folio_service_ids[index].price_unit = item["price_unit"]
current_folio_service_ids[index].discount = final_discount
current_folio_service_ids[index].service_line_ids = lines_to.ids
else:
new = {
"service_id": folio_service.id,
"price_unit": item["price_unit"],
"discount": final_discount,
"folio_id": folio.id,
"service_line_ids": [(6, 0, lines_to.ids)],
}
folio.sale_line_ids = [(0, 0, new)]
if len(expected_folio_services) < len(current_folio_service_ids):
folio_sale_lines_to_remove = [
value.id
for index, value in enumerate(current_folio_service_ids)
if index > (len(expected_folio_services) - 1)
]
for fsl in folio_sale_lines_to_remove:
self.env["folio.sale.line"].browse(fsl).unlink()
else:
to_unlink = folio.sale_line_ids.filtered(lambda x: x.name == _("Others"))
to_unlink.unlink()
@api.model
def concat_discounts(self, discount, cancel_discount):
discount_factor = 1.0
for discount in [discount, cancel_discount]:
discount_factor = discount_factor * ((100.0 - discount) / 100.0)
final_discount = 100.0 - (discount_factor * 100.0)
return final_discount
@api.depends("partner_id", "agency_id") @api.depends("partner_id", "agency_id")
def _compute_pricelist_id(self): def _compute_pricelist_id(self):
@@ -1168,7 +1263,7 @@ class PmsFolio(models.Model):
moves = ( moves = (
self.env["account.move"] self.env["account.move"]
.sudo() .sudo()
.with_context(default_move_type="out_invoice") .with_context(default_move_type="out_invoice", auto_name=True)
.create(invoice_vals_list) .create(invoice_vals_list)
) )

View File

@@ -87,8 +87,8 @@ class PmsServiceLine(models.Model):
store=True, store=True,
related="service_id.currency_id", related="service_id.currency_id",
) )
room_id = fields.Many2one( reservation_id = fields.Many2one(
string="Room", string="Reservation",
help="Room to which the services will be applied", help="Room to which the services will be applied",
readonly=True, readonly=True,
store=True, store=True,
@@ -104,7 +104,11 @@ class PmsServiceLine(models.Model):
compute="_compute_discount", compute="_compute_discount",
) )
cancel_discount = fields.Float( cancel_discount = fields.Float(
string="Cancelation Discount", help="", compute="_compute_cancel_discount" string="Cancelation Discount",
help="",
compute="_compute_cancel_discount",
readonly=True,
store=True,
) )
@api.depends("day_qty", "discount", "price_unit", "tax_ids") @api.depends("day_qty", "discount", "price_unit", "tax_ids")

View File

@@ -38,3 +38,4 @@ from . import test_pms_board_service_line
from . import test_pms_board_service_room_type from . import test_pms_board_service_room_type
from . import test_pms_board_service_room_type_line from . import test_pms_board_service_room_type_line
from . import test_pms_folio_invoice from . import test_pms_folio_invoice
from . import test_pms_folio_sale_line

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,9 @@
<field name="folio_ids" widget="many2many_tags" /> <field name="folio_ids" widget="many2many_tags" />
<field name="pms_property_id" invisible="1" /> <field name="pms_property_id" invisible="1" />
</xpath> </xpath>
<xpath expr="//field[@name='quantity']" position="before">
<field name="name_changed_by_user" invisible="1" />
</xpath>
</field> </field>
</record> </record>

View File

@@ -265,6 +265,7 @@
<field <field
name="sale_line_ids" name="sale_line_ids"
widget="section_and_note_one2many" widget="section_and_note_one2many"
default_order="folio_id, sequence, reservation_id asc, service_order, date_order, display_type"
> >
<tree string="Sales Lines" editable="bottom"> <tree string="Sales Lines" editable="bottom">
<control> <control>
@@ -283,7 +284,10 @@
context="{'default_display_type': 'line_note'}" context="{'default_display_type': 'line_note'}"
/> />
</control> </control>
<field name="service_order" invisible="1" />
<field name="date_order" invisible="1" />
<field name="reservation_id" invisible="1" />
<field name="service_id" invisible="1" />
<field name="sequence" widget="handle" /> <field name="sequence" widget="handle" />
<!-- We do not display the type because we don't want the user to be bothered with that information if he has no section or note. --> <!-- We do not display the type because we don't want the user to be bothered with that information if he has no section or note. -->
<field name="display_type" invisible="1" /> <field name="display_type" invisible="1" />
@@ -291,11 +295,22 @@
<!-- <field name="product_updatable" invisible="1"/> --> <!-- <field name="product_updatable" invisible="1"/> -->
<button <button
title="Board Service" title="Room"
class="oe_stat_button" class="oe_stat_button"
icon="fa-1x fa-bed" icon="fa-1x fa-bed"
name="open_service_ids" attrs="{'invisible':[('service_id','!=', False)]}"
/>
<button
title="Board Service"
class="oe_stat_button"
icon="fa-1x fa-cutlery"
attrs="{'invisible':[('is_board_service','=', False)]}" attrs="{'invisible':[('is_board_service','=', False)]}"
/>
<button
title="Extra Service"
class="oe_stat_button"
icon="fa-1x fa-tags"
attrs="{'invisible':['|',('is_board_service','=',True),('service_id','=',False)]}"
/> />
<field <field
name="product_id" name="product_id"

View File

@@ -61,7 +61,7 @@
attrs="{'invisible':[('is_board_service','=', True)]}" attrs="{'invisible':[('is_board_service','=', True)]}"
/> />
<field name="service_id" optional="show" /> <field name="service_id" optional="show" />
<field name="room_id" /> <field name="reservation_id" />
<field name="tax_ids" optional="show" /> <field name="tax_ids" optional="show" />
<field <field
name="discount" name="discount"
@@ -162,7 +162,7 @@
> >
<field name="service_id" /> <field name="service_id" />
<field name="product_id" filters="1" /> <field name="product_id" filters="1" />
<field name="room_id" filters="1" /> <field name="reservation_id" filters="1" />
</calendar> </calendar>
</field> </field>
</record> </record>