mirror of
https://github.com/OCA/pms.git
synced 2025-01-29 00:17:45 +02:00
[ADD] Pricelist cancelled rules
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
30
hotel/models/hotel_cancelation_rule.py
Normal file
30
hotel/models/hotel_cancelation_rule.py
Normal 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")
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
63
hotel/views/hotel_cancelation_rule_views.xml
Normal file
63
hotel/views/hotel_cancelation_rule_views.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
17
hotel/views/inherited_product_pricelist_views.xml
Normal file
17
hotel/views/inherited_product_pricelist_views.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user