[ADD] Pricelist cancelled rules

This commit is contained in:
Dario Lodeiros
2019-03-23 15:51:24 +01:00
parent 8af44c43cf
commit e73be563d2
11 changed files with 230 additions and 29 deletions

View File

@@ -41,6 +41,7 @@
'views/hotel_room_type_class_views.xml',
'views/general.xml',
'views/inherited_product_template_views.xml',
'views/inherited_product_pricelist_views.xml',
'views/hotel_room_amenities_type_views.xml',
'views/hotel_room_amenities_views.xml',
'views/hotel_room_type_restriction_views.xml',
@@ -52,6 +53,7 @@
'views/hotel_board_service_views.xml',
'views/hotel_checkin_partner_views.xml',
'views/hotel_board_service_room_type_views.xml',
'views/hotel_cancelation_rule_views.xml',
'data/cron_jobs.xml',
'data/records.xml',
'data/email_template_cancel.xml',

View File

@@ -31,3 +31,4 @@ from . import hotel_board_service
from . import hotel_board_service_room_type_line
from . import hotel_board_service_line
from . import inherited_account_invoice_line
from . import hotel_cancelation_rule

View File

@@ -0,0 +1,30 @@
# Copyright 2017 Alexandre Díaz
# Copyright 2017 Dario Lodeiros
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, fields
class HotelCancelationRule(models.Model):
_name = 'hotel.cancelation.rule'
_description = 'Cancelation Rules'
name = fields.Char('Amenity Name', translate=True, required=True)
active = fields.Boolean('Active', default=True)
pricelist_ids = fields.One2many('product.pricelist',
'cancelation_rule_id',
'Pricelist that use this rule')
days_intime = fields.Integer(
'Days Late',
help='Maximum number of days for free cancellation before Checkin')
penalty_late = fields.Integer('% Penalty Late', defaul="100")
apply_on_late = fields.Selection([
('first', 'First Day'),
('all', 'All Days'),
('days', 'Specify days')], 'Late apply on', default='first')
days_late = fields.Integer('Late first days', default="2")
penalty_noshow = fields.Integer('% Penalty No Show', default="100")
apply_on_noshow = fields.Selection([
('first', 'First Day'),
('all', 'All Days'),
('days', 'Specify days')], 'No Show apply on', default='all')
days_noshow = fields.Integer('NoShow first days', default="2")

View File

@@ -170,12 +170,15 @@ class HotelReservation(models.Model):
track_visibility='onchange',
help='Number of children there in guest list.')
to_assign = fields.Boolean('To Assign', track_visibility='onchange')
state = fields.Selection([('draft', 'Pre-reservation'), ('confirm', 'Pending Entry'),
('booking', 'On Board'), ('done', 'Out'),
('cancelled', 'Cancelled')],
'State', readonly=True,
default=lambda *a: 'draft',
track_visibility='onchange')
state = fields.Selection([
('draft', 'Pre-reservation'),
('confirm', 'Pending Entry'),
('booking', 'On Board'),
('done', 'Out'),
('cancelled', 'Cancelled')
], string='State', readonly=True,
default=lambda *a: 'draft', copy=False,
track_visibility='onchange')
reservation_type = fields.Selection(related='folio_id.reservation_type',
default=lambda *a: 'normal')
invoice_count = fields.Integer(related='folio_id.invoice_count')
@@ -635,6 +638,11 @@ class HotelReservation(models.Model):
write_vals.update({'room_type_id': self.room_id.room_type_id.id})
self.update(write_vals)
@api.onchange('cancelled_reason')
def onchange_cancelled_reason(self):
for record in self:
record._compute_cancelled_discount()
@api.onchange('partner_id')
def onchange_partner_id(self):
addr = self.partner_id.address_get(['invoice'])
@@ -815,6 +823,9 @@ class HotelReservation(models.Model):
else:
vals.update({'state': 'confirm'})
record.write(vals)
record.reservation_line_ids.update({
'cancel_discount': 0
})
if record.splitted:
master_reservation = record.parent_reservation or record
@@ -846,7 +857,9 @@ class HotelReservation(models.Model):
for record in self:
record.write({
'state': 'cancelled',
'cancelled_reason': record.compute_cancelation_reason()
})
record._compute_cancelled_discount()
if record.splitted:
master_reservation = record.parent_reservation or record
splitted_reservs = self.env['hotel.reservation'].search([
@@ -861,6 +874,25 @@ class HotelReservation(models.Model):
splitted_reservs.action_cancel()
record.folio_id.compute_amount()
@api.multi
def compute_cancelation_reason(self):
self.ensure_one()
pricelist = self.pricelist_id
if pricelist and pricelist.cancelation_rule_id:
tz_hotel = self.env['ir.default'].sudo().get(
'res.config.settings', 'tz_hotel')
today = fields.Date.context_today(self.with_context(
tz=tz_hotel))
days_diff = (fields.Date.from_string(self.real_checkin) -
fields.Date.from_string(today)).days
if days_diff < 0:
return 'noshow'
elif days_diff < pricelist.cancelation_rule_id.days_intime:
return 'late'
else:
return 'intime'
return False
@api.multi
def draft(self):
for record in self:
@@ -904,11 +936,17 @@ class HotelReservation(models.Model):
return True
return False
@api.depends('reservation_line_ids.discount')
@api.depends('reservation_line_ids.discount',
'reservation_line_ids.cancel_discount')
def _compute_discount(self):
for record in self:
record.discount = sum(line.price * ((line.discount or 0.0) * 0.01) \
for line in record.reservation_line_ids)
discount = 0
for line in record.reservation_line_ids:
first_discount = line.price * ((line.discount or 0.0) * 0.01)
price = line.price - first_discount
cancel_discount = price * ((line.cancel_discount or 0.0) * 0.01)
discount += first_discount + cancel_discount
record.discount = discount
@api.depends('reservation_line_ids.price', 'discount', 'tax_ids')
def _compute_amount_reservation(self):
@@ -927,6 +965,51 @@ class HotelReservation(models.Model):
'price_subtotal': taxes['total_excluded'],
})
@api.multi
def _compute_cancelled_discount(self):
self.ensure_one()
pricelist = self.pricelist_id
if self.state == 'cancelled':
if self.cancelled_reason and pricelist and pricelist.cancelation_rule_id:
date_start_dt = fields.Date.from_string(self.real_checkin or self.checkin)
date_end_dt = fields.Date.from_string(self.real_checkout or self.checkout)
days = abs((date_end_dt - date_start_dt).days)
rule = pricelist.cancelation_rule_id
if self.cancelled_reason == 'late':
discount = 100 - rule.penalty_late
if rule.apply_on_late == 'first':
days = 1
elif rule.apply_on_late == 'days':
days = rule.days_late
elif self.cancelled_reason == 'noshow':
discount = 100 - rule.penalty_noshow
if rule.apply_on_noshow == 'first':
days = 1
elif rule.apply_on_noshow == 'days':
days = rule.days_late - 1
elif self.cancelled_reason == 'intime':
discount = 100
checkin = self.real_checkin or self.checkin
dates = []
for i in range(0, days):
dates.append((fields.Date.from_string(checkin) + timedelta(days=i)).strftime(
DEFAULT_SERVER_DATE_FORMAT))
self.reservation_line_ids.filtered(lambda r: r.date in dates).update({
'cancel_discount': discount
})
self.reservation_line_ids.filtered(lambda r: r.date not in dates).update({
'cancel_discount': 100
})
else:
self.reservation_line_ids.update({
'cancel_discount': 0
})
else:
self.reservation_line_ids.update({
'cancel_discount': 0
})
@api.model
def prepare_reservation_lines(self, dfrom, days, pricelist_id, vals=False, update_old_prices=False):
total_price = 0.0
@@ -1164,7 +1247,6 @@ class HotelReservation(models.Model):
'price_total': tprice[1],
'parent_reservation': parent_res.id,
'room_type_id': parent_res.room_type_id.id,
'discount': parent_res.discount,
'state': parent_res.state,
'reservation_line_ids': reservation_lines[1],
'preconfirm': False,

View File

@@ -22,9 +22,13 @@ class HotelReservationLine(models.Model):
ondelete='cascade', required=True,
copy=False)
date = fields.Date('Date')
state = fields.Selection(related='reservation_id.state')
price = fields.Float(
string='Price',
digits=dp.get_precision('Product Price'))
cancel_discount = fields.Float(
string='Cancel Discount (%)',
digits=dp.get_precision('Discount'), default=0.0)
discount = fields.Float(
string='Discount (%)',
digits=dp.get_precision('Discount'), default=0.0)

View File

@@ -11,6 +11,9 @@ class ProductPricelist(models.Model):
pricelist_type = fields.Selection([
('daily', 'Daily Plan'),
], string='Pricelist Type', default='daily')
cancelation_rule_id = fields.Many2one(
'hotel.cancelation.rule',
string="Cancelation Policy")
@api.multi
@api.depends('name')

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!--==================================================== Cancelation Rules ==================================================== -->
<!-- Form view of cancelation rules -->
<record model="ir.ui.view" id="hotel_cancelation_rule_form">
<field name="name">hotel.cancelation.rule.form</field>
<field name="model">hotel.cancelation.rule</field>
<field name="arch" type="xml">
<form string="Cancelation Rules">
<sheet>
<h3>
<field name="name" />
<separator />
<label for="name" string="Max. days InTime before Checkin" />
<field name="days_intime" />
</h3>
<group>
<group string="Late">
<field name="penalty_late" />
<field name="apply_on_late" />
<field name="days_late" attrs="{'invisible': [('apply_on_late','not in',('days'))]}" />
</group>
<group string="No Show">
<field name="penalty_noshow" />
<field name="apply_on_noshow" />
<field name="days_noshow" attrs="{'invisible': [('apply_on_noshow','not in',('days'))]}" />
</group>
</group>
</sheet>
</form>
</field>
</record>
<!-- Tree view of cancelation rules -->
<record model="ir.ui.view" id="hotel_cancelation_rule_view_tree">
<field name="name">hotel.cancelation.rule.tree</field>
<field name="model">hotel.cancelation.rule</field>
<field name="arch" type="xml">
<tree string="Cancelation Rules">
<field name="name" />
<field name="days_intime" />
<field name="penalty_late" />
<field name="apply_on_late" />
<field name="penalty_noshow" />
<field name="apply_on_noshow" />
</tree>
</field>
</record>
<!-- Action of hotel cancelation rules -->
<record model="ir.actions.act_window" id="action_hotel_cancelation_rule">
<field name="name">Cancelation Rules</field>
<field name="res_model">hotel.cancelation.rule</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem name="Cancelation Rules" id="menu_hotel_cancelation_rule"
action="action_hotel_cancelation_rule" sequence="25"
parent="hotel.configuration_others" />
</odoo>

View File

@@ -34,7 +34,7 @@
<!-- states="sale" attrs="{'invisible': [('invoice_status', '!=', 'invoiced')]}" -->
<!-- <button name="print_quotation" string="Print" type="object" states="sent,sale"/> -->
<field name="state" select="2" widget="statusbar"
statusbar_visible="draft,sent,sale,done" invisible="1"/>
statusbar_visible="draft,sent,sale,done" />
</header>
<sheet>

View File

@@ -16,7 +16,7 @@
<field name="model">hotel.reservation</field>
<field name="priority">20</field>
<field name="arch" type="xml">
<form string="Reservation" >
<form string="Reservation">
<header>
<field name="splitted" invisible="True" />
<field name="tax_ids" invisible="1"/>
@@ -61,21 +61,21 @@
attrs="{'invisible':[('splitted', '=', False)]}"
/>
<button name="open_master" string="Open Master" type="object" class="oe_highlight" icon="fa-file" attrs="{'invisible':['|',['parent_reservation', '=', False]]}" />
<field name="state" widget="statusbar"/>
<field name="state" select="2" widget="statusbar" statusbar_visible="draft,confirm,booking,done" />
</header>
<div class="alert alert-info" role="alert" style="margin-bottom:0px;"
attrs="{'invisible': ['|',('shared_folio','=',False),('splitted', '=', True)]}">
This reservation has other reservantions and/or services in the folio, you can check it in the
<bold><button class="alert-link" type="object" name="open_folio" string="Folio Form"/></bold>
</div>
<div class="alert alert-warning" role="alert" style="margin-bottom:0px;"
attrs="{'invisible': [('splitted','=',False)]}">
This reservation is part of splitted reservation!, you can check it in the
<bold><button class="alert-link" type="object" name="open_folio" string="Folio Form"/></bold>
</div>
<field name="shared_folio" invisible="1"/>
<sheet>
<div class="alert alert-info" role="alert" style="margin-bottom:0px;"
attrs="{'invisible': ['|',('shared_folio','=',False),('splitted', '=', True)]}">
This reservation has other reservantions and/or services in the folio, you can check it in the
<bold><button class="alert-link" type="object" name="open_folio" string="Folio Form"/></bold>
</div>
<div class="alert alert-warning" role="alert" style="margin-bottom:0px;"
attrs="{'invisible': [('splitted','=',False)]}">
This reservation is part of splitted reservation!, you can check it in the
<bold><button class="alert-link" type="object" name="open_folio" string="Folio Form"/></bold>
</div>
<field name="shared_folio" invisible="1"/>
<div class="oe_button_box" attrs="{'invisible': [('folio_id','=',False)]}">
<button type="object" class="oe_stat_button"
icon="fa-file"
@@ -196,6 +196,7 @@
<field name="departure_hour"/>
</group>
<group colspan="4" string="Reservation Details" name="reservation_details">
<field name="cancelled_reason" attrs="{'invisible': [('state', 'not in', ('cancelled'))]}"/>
<field name="nights"/>
<!-- TODO: How to filter to avoid show False (generic) pricelist board when exist a specific pricelist board¿? -->
<field name="board_service_room_id" domain="[
@@ -287,6 +288,8 @@
<field name="date" />
<field name="price" />
<field name="discount" />
<field name="cancel_discount"
attrs="{'column_invisible': [('parent.state','!=','cancelled')]}" />
</tree>
</field>
</page>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0"?>
<odoo>
<record id="product_pricelist_view_form" model="ir.ui.view">
<field name="model">product.pricelist</field>
<field name="inherit_id" ref="product.product_pricelist_view" />
<field name="arch" type="xml">
<xpath expr="//field[@name='country_group_ids']" position="before">
<field name="pricelist_type" />
<field name="cancelation_rule_id" />
</xpath>
</field>
</record>
</odoo>

View File

@@ -21,10 +21,6 @@
confirm="Disconnecting will automatically delete the pricelist in the Channel. Do you want to proceed?"
/>
</xpath>
<xpath expr="//field[@name='country_group_ids']" position="before">
<field name="pricelist_type" />
</xpath>
</field>
</record>