diff --git a/hotel/__manifest__.py b/hotel/__manifest__.py index b63ab57fa..b1bcb0e22 100644 --- a/hotel/__manifest__.py +++ b/hotel/__manifest__.py @@ -44,14 +44,16 @@ 'views/hotel_room_type.xml', 'views/hotel_room.xml', 'views/hotel_room_type_class.xml', + 'views/general.xml', # 'views/hotel_service.xml', - 'views/inherit_product_product.xml', + 'views/inherit_product_template.xml', 'views/hotel_room_amenities_type.xml', 'views/hotel_room_amenities.xml', 'views/hotel_room_type_restriction_views.xml', 'views/hotel_room_type_restriction_item_views.xml', 'views/hotel_reservation.xml', 'views/room_closure_reason.xml', + 'views/hotel_board_service.xml', # 'views/room_type_views.xml', 'views/cardex.xml', 'views/hotel_room_type_availability.xml', @@ -61,6 +63,7 @@ 'data/email_template_cancel.xml', 'data/email_template_reserv.xml', 'data/email_template_exit.xml', + 'wizard/wizard_reservation.xml', ], 'css': ['static/src/css/room_kanban.css'], 'auto_install': False, diff --git a/hotel/models/__init__.py b/hotel/models/__init__.py index abf50a070..5cab9a32f 100644 --- a/hotel/models/__init__.py +++ b/hotel/models/__init__.py @@ -15,7 +15,7 @@ from . import hotel_room_type from . import hotel_service from . import inherit_account_invoice # from . import inherit_product_category -from . import inherit_product_product +from . import inherit_product_template from . import inherit_res_company # from . import room_type from . import inherit_account_payment @@ -30,4 +30,6 @@ from . import inherit_res_partner from . import inherited_mail_compose_message from . import hotel_room_type_class from . import room_closure_reason +from . import hotel_service_line +from . import hotel_board_service #~ from . import hotel_dashboard diff --git a/hotel/models/hotel_board_service.py b/hotel/models/hotel_board_service.py new file mode 100644 index 000000000..94accdfcc --- /dev/null +++ b/hotel/models/hotel_board_service.py @@ -0,0 +1,15 @@ +# Copyright 2017 Dario Lodeiros +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from openerp import models, fields, api, _ + + +class HotelBoardService(models.Model): + _name = "hotel.board.service" + _description = "Board Services" + + name = fields.Char('Board Name', size=64, required=True, index=True) + service_ids = fields.Many2many(comodel_name='product.template', + relation='board_services_room', + column1='board_id', + column2='service_id') + sequence = fields.Integer('Sequence', size=64) diff --git a/hotel/models/hotel_folio.py b/hotel/models/hotel_folio.py index 6285b4363..a7ffd4ced 100644 --- a/hotel/models/hotel_folio.py +++ b/hotel/models/hotel_folio.py @@ -353,13 +353,12 @@ class HotelFolio(models.Model): #~ }) return addr = self.partner_id.address_get(['invoice']) - values = {'user_id': self.partner_id.user_id.id or self.env.uid, - 'pricelist_id':self.partner_id.property_product_pricelist and \ + pricelist = self.partner_id.property_product_pricelist and \ self.partner_id.property_product_pricelist.id or \ - self.env['ir.default'].sudo().get('res.config.settings', 'parity_pricelist_id'), - 'reservation_type': self.env['hotel.folio'].calcule_reservation_type( - self.partner_id.is_staff, - self.reservation_type)} + self.env['ir.default'].sudo().get('res.config.settings', 'parity_pricelist_id') + values = {'user_id': self.partner_id.user_id.id or self.env.uid, + 'pricelist_id': pricelist + } if self.env['ir.config_parameter'].sudo().get_param('sale.use_sale_note') and \ self.env.user.company_id.sale_note: values['note'] = self.with_context( @@ -369,6 +368,15 @@ class HotelFolio(models.Model): values['team_id'] = self.partner_id.team_id.id self.update(values) + @api.multi + @api.onchange('pricelist_id') + def onchange_pricelist_id(self): + values = {'reservation_type': self.env['hotel.folio'].calcule_reservation_type( + self.pricelist_id.is_staff, + self.reservation_type)} + self.update(values) + + @api.model def calcule_reservation_type(self, is_staff, current_type): if current_type == 'out': diff --git a/hotel/models/hotel_reservation.py b/hotel/models/hotel_reservation.py index 6fa883e69..f1974b24a 100644 --- a/hotel/models/hotel_reservation.py +++ b/hotel/models/hotel_reservation.py @@ -196,6 +196,7 @@ class HotelReservation(models.Model): parent_reservation = fields.Many2one('hotel.reservation', 'Parent Reservation') overbooking = fields.Boolean('Is Overbooking', default=False) + reselling = fields.Boolean('Is Reselling', default=False) nights = fields.Integer('Nights', compute='_computed_nights', store=True) channel_type = fields.Selection([ @@ -408,6 +409,7 @@ class HotelReservation(models.Model): 'parent_reservation': self.parent_reservation.id, 'state': self.state, 'overbooking': self.overbooking, + 'reselling': self.reselling, 'price_unit': self.price_unit, 'splitted': self.splitted, # 'room_type_id': self.room_type_id.id, @@ -444,16 +446,22 @@ class HotelReservation(models.Model): @api.onchange('partner_id') def onchange_partner_id(self): #TODO: Change parity pricelist by default pricelist - values = { - 'pricelist_id': self.partner_id.property_product_pricelist and \ + pricelist = self.partner_id.property_product_pricelist and \ self.partner_id.property_product_pricelist.id or \ - self.env['ir.default'].sudo().get('res.config.settings', 'parity_pricelist_id'), - 'reservation_type': self.env['hotel.folio'].calcule_reservation_type( - self.partner_id.is_staff, - self.reservation_type) + self.env['ir.default'].sudo().get('res.config.settings', 'parity_pricelist_id') + values = { + 'pricelist_id': pricelist, } self.update(values) + @api.multi + @api.onchange('pricelist_id') + def onchange_pricelist_id(self): + values = {'reservation_type': self.env['hotel.folio'].calcule_reservation_type( + self.pricelist_id.is_staff, + self.reservation_type)} + self.update(values) + @api.onchange('reservation_type') def assign_partner_company_on_out_service(self): if self.reservation_type == 'out': @@ -507,7 +515,7 @@ class HotelReservation(models.Model): def onchange_room_availabiltiy_domain(self): self.ensure_one() if self.checkin and self.checkout: - if self.overbooking: + if self.overbooking or self.reselling: return occupied = self.env['hotel.reservation'].get_reservations( self.checkin, @@ -842,7 +850,8 @@ class HotelReservation(models.Model): domain = [('reservation_line_ids.date', '>=', dfrom), ('reservation_line_ids.date', '<', dto), ('state', '!=', 'cancelled'), - ('overbooking', '=', False)] + ('overbooking', '=', False), + ('reselling', '=', False),] return domain @api.model @@ -872,7 +881,7 @@ class HotelReservation(models.Model): return reservations_dates # TODO: Use default values on checkin /checkout is empty - @api.constrains('checkin', 'checkout', 'state', 'room_id', 'overbooking') + @api.constrains('checkin', 'checkout', 'state', 'room_id', 'overbooking', 'reselling') def check_dates(self): """ 1.-When date_order is less then checkin date or diff --git a/hotel/models/hotel_service.py b/hotel/models/hotel_service.py index 7352702db..a58f523c2 100644 --- a/hotel/models/hotel_service.py +++ b/hotel/models/hotel_service.py @@ -10,18 +10,6 @@ _logger = logging.getLogger(__name__) class HotelService(models.Model): _name = 'hotel.service' _description = 'Hotel Services and its charges' - - @api.model - def _service_checkin(self): - if 'checkin' in self._context: - return self._context['checkin'] - return time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) - - @api.model - def _service_checkout(self): - if 'checkout' in self._context: - return self._context['checkout'] - return time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) @api.model def _default_ser_room_line(self): @@ -33,42 +21,116 @@ class HotelService(models.Model): return False name = fields.Char('Service description') - # services in the hotel are products - product_id = fields.Many2one('product.product', 'Service', required=True) - - folio_id = fields.Many2one('hotel.folio', 'Folio', ondelete='cascade') - + product_id = fields.Many2one('product.product', 'Service', + required=True) + folio_id = fields.Many2one('hotel.folio', 'Folio', + ondelete='cascade') ser_room_line = fields.Many2one('hotel.reservation', 'Room', default=_default_ser_room_line) - - list_price = fields.Float( - related='product_id.list_price') - + service_line_ids = fields.One2many('hotel.service.line', + 'service_id') + product_qty = fields.Integer('Quantity') + pricelist_id = fields.Many2one( + related='folio_id.pricelist_id') channel_type = fields.Selection([ ('door', 'Door'), ('mail', 'Mail'), ('phone', 'Phone'), ('call', 'Call Center'), ('web', 'Web')], 'Sales Channel') + currency_id = fields.Many2one('res.currency', + related='pricelist_id.currency_id', + string='Currency', readonly=True, required=True) + price_subtotal = fields.Monetary(string='Subtotal', + readonly=True, + store=True, + compute='_compute_amount_reservation') + price_total = fields.Monetary(string='Total', + readonly=True, + store=True, + compute='_compute_amount_reservation') + price_tax = fields.Float(string='Taxes', + readonly=True, + store=True, + compute='_compute_amount_reservation') - ser_checkin = fields.Datetime('From Date', required=True, - default=_service_checkin) - ser_checkout = fields.Datetime('To Date', required=True, - default=_service_checkout) + @api.onchange('product_id') + def onchange_product_calc_qty(self): + """ + Compute the default quantity according to the + configuration of the selected product + """ + for record in self: + product = record.product_id + reservation = record.ser_room_line + if product and reservation: + qty = 1 + if product.per_day: + qty = qty * reservation.nights + if product.per_person: + qty = qty * (reservation.adults + reservation.children) + record.product_qty = qty + @api.onchange('product_qty') + def onchange_product_qty_days_selection(self): + """ + Try to calculate the days on which the product + should be served as long as the product is per day + """ + for record in self: + reservation = record.ser_room_line + if record.product_id.per_day: + days_diff = ( + fields.Date.from_string(reservation.checkout) - fields.Date.from_string(reservation.checkin) + ).days + record.update(record.prepare_service_lines( + reservation.checkin, + days_diff)) + else: + record.update(rec.prepare_service_lines( + reservation.checkin, 1)) + - # TODO Hierarchical relationship for parent-child tree - # parent_id = fields.Many2one ... + @api.multi + def prepare_service_lines(self, dfrom, days, vals=False): + self.ensure_one() + old_qty = 0 + cmds = [(5, 0, 0)] + if not vals: + vals + product = vals.get('product_id') or self.product_id + old_lines_days = self.mapped('service_line_ids.date') + for day in service_line_ids: + old_qty = old_qty + day.day_qty + qty_day = (self.product_qty - old_qty) // (days - count(old_line_days)) + rest_day = (self.product_qty - old_qty) % (days - count(old_line_days)) + reservation = rec.ser_room_line + for i in range(0, days): + idate = (fields.Date.from_string(dfrom) + timedelta(days=i)).strftime( + DEFAULT_SERVER_DATE_FORMAT) + old_line = self.service_line_ids.filtered(lambda r: r.date == idate) + if idate not in old_lines_days: + cmds.append((0, False, { + 'date': idate, + 'day_qty': qty + })) + else: + cmds.append((4, old_line.id)) + return {'service_line_ids': cmds} - # service_id = fields.Many2one('product.product', 'Service_id', - # required=True, ondelete='cascade', - # delegate=True) - # service_type_id = fields.Many2one('hotel.service.type', - # 'Service Catagory') - # service_line_id = fields.Many2one('hotel.service.line', - # 'Service Line') - # @api.multi - # def unlink(self): - # # for record in self: - # # record.service_id.unlink() - # return super(HotelServices, self).unlink() + @api.depends('qty_product', 'tax_id') + def _compute_amount_service(self): + """ + Compute the amounts of the service line. + """ + for record in self: + product = rec.product_id + price = amount_room * (1 - (record.discount or 0.0) * 0.01) + taxes = record.tax_id.compute_all(price, record.currency_id, 1, product=product) + record.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'], + }) + + diff --git a/hotel/models/hotel_service_line.py b/hotel/models/hotel_service_line.py new file mode 100644 index 000000000..7391fdb43 --- /dev/null +++ b/hotel/models/hotel_service_line.py @@ -0,0 +1,32 @@ +# Copyright 2017-2018 Alexandre Díaz +# Copyright 2017 Dario Lodeiros +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import models, fields +from odoo.addons import decimal_precision as dp + +class HotelServiceLine(models.Model): + _name = "hotel.service.line" + _order = "date" + + service_id = fields.Many2one('hotel.service', string='Service', + ondelete='cascade', required=True, + copy=False) + date = fields.Date('Date') + day_qty = fields.Integer('Units') + product_id = fields.Many2one(related='service_id.product_id') + + @api.constrains('day_qty') + def no_free_resources(self): + for record in self: + limit = record.product_id.daily_limit + if limit > 0: + out_qty = sum(self.env['hotel.service.line'].search([( + 'product_id','=',record.product_id, + 'date','=',record.date)]).mapped('day_qty')) + if limit < out_qty + record.day_qty: + raise ValidationError( + _("Limit exceeded for %s")% record.date) + + + + diff --git a/hotel/models/inherit_product_pricelist.py b/hotel/models/inherit_product_pricelist.py index 9e9d47bef..4f9ce2695 100644 --- a/hotel/models/inherit_product_pricelist.py +++ b/hotel/models/inherit_product_pricelist.py @@ -1,6 +1,6 @@ # Copyright 2017 Alexandre Díaz # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from openerp import models, api +from openerp import models, fields, api, _ class ProductPricelist(models.Model): @@ -21,3 +21,6 @@ class ProductPricelist(models.Model): else: names.append((name[0], name[1])) return names + + + is_staff = fields.Boolean('Is Staff') diff --git a/hotel/models/inherit_product_product.py b/hotel/models/inherit_product_product.py deleted file mode 100644 index dca420ccb..000000000 --- a/hotel/models/inherit_product_product.py +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright 2017 Alexandre Díaz -# Copyright 2017 Dario Lodeiros -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from openerp import models, fields, api, _ - - -class ProductProduct(models.Model): - _inherit = "product.product" - - is_room_type = fields.Boolean('Is a Room Type', default=False) - # iscategid = fields.Boolean('Is categ id') - # isservice = fields.Boolean('Is Service id') diff --git a/hotel/models/inherit_product_template.py b/hotel/models/inherit_product_template.py new file mode 100644 index 000000000..4b5a9f180 --- /dev/null +++ b/hotel/models/inherit_product_template.py @@ -0,0 +1,13 @@ +# Copyright 2017 Alexandre Díaz +# Copyright 2017 Dario Lodeiros +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from openerp import models, fields, api, _ + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + is_hotel_service = fields.Boolean('Is a Hotel Service', default=False) + per_day = fields.Boolean('Unit increment per day') + per_person = fields.Boolean('Unit increment per person') + daily_limit = fields.Integer('Daily limit') diff --git a/hotel/models/inherit_res_partner.py b/hotel/models/inherit_res_partner.py index ea5bca3d0..88a1192f3 100644 --- a/hotel/models/inherit_res_partner.py +++ b/hotel/models/inherit_res_partner.py @@ -24,7 +24,6 @@ class ResPartner(models.Model): reservations_count = fields.Integer('Reservations', compute='_compute_reservations_count') folios_count = fields.Integer('Folios', compute='_compute_folios_count') - is_staff = fields.Boolean('Is Staff') """ TODO @api.onchange('is_staff') diff --git a/hotel/static/src/js/open_reservation_wizard_listview_button.js b/hotel/static/src/js/open_reservation_wizard_listview_button.js new file mode 100644 index 000000000..974594a59 --- /dev/null +++ b/hotel/static/src/js/open_reservation_wizard_listview_button.js @@ -0,0 +1,28 @@ +// Copyright 2018 Alexandre Díaz +// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +odoo.define('hotel_calendar.listview_button_open_reservation_wizard', function(require) { +'use strict'; + +var ListView = require('web.ListView'), + Core = require('web.core'), + + _t = Core._t; + + +ListView.include({ + render_buttons: function () { + var self = this; + this._super.apply(this, arguments); // Sets this.$buttons + + if (this.dataset.model == 'hotel.reservation') { + this.$buttons.append(""); + this.$buttons.find('.oe_open_reservation_wizard').on('click', function(){ + self.do_action('hotel_calendar.open_wizard_reservations'); + }); + } + } +}); + +return ListView; + +}); diff --git a/hotel/views/general.xml b/hotel/views/general.xml new file mode 100644 index 000000000..a02d866be --- /dev/null +++ b/hotel/views/general.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/hotel/views/hotel_board_service.xml b/hotel/views/hotel_board_service.xml new file mode 100644 index 000000000..ba37c0cee --- /dev/null +++ b/hotel/views/hotel_board_service.xml @@ -0,0 +1,48 @@ + + + + + + + hotel.board.service.form + hotel.board.service + +
+ + + + + + + + + +
+
+
+ + + + hotel.board.service.tree + hotel.board.service + + + + + + + + + + + Board Services + hotel.board.service + form + tree,form + + + + +
diff --git a/hotel/views/hotel_folio.xml b/hotel/views/hotel_folio.xml index 3dac1349b..31c67da7d 100644 --- a/hotel/views/hotel_folio.xml +++ b/hotel/views/hotel_folio.xml @@ -414,10 +414,10 @@ - + - + diff --git a/hotel/views/inherit_product_product.xml b/hotel/views/inherit_product_product.xml deleted file mode 100644 index 26a650cf4..000000000 --- a/hotel/views/inherit_product_product.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - view.product.product.form.inherited - product.product - - - - - - - - - diff --git a/hotel/views/inherit_product_template.xml b/hotel/views/inherit_product_template.xml new file mode 100644 index 000000000..f10b6dd02 --- /dev/null +++ b/hotel/views/inherit_product_template.xml @@ -0,0 +1,26 @@ + + + + + view.product.template.form.inherited + product.template + + + + + + + + + + + + + + + + + + + + diff --git a/hotel/wizard/__init__.py b/hotel/wizard/__init__.py index ae08f23cd..87e69afbd 100644 --- a/hotel/wizard/__init__.py +++ b/hotel/wizard/__init__.py @@ -28,3 +28,4 @@ from . import massive_changes from . import split_reservation from . import duplicate_reservation from . import massive_price_reservation_days +from . import wizard_reservation diff --git a/hotel/wizard/hotel_wizard.xml b/hotel/wizard/hotel_wizard.xml index 1d6ff68c0..5d089dcb7 100644 --- a/hotel/wizard/hotel_wizard.xml +++ b/hotel/wizard/hotel_wizard.xml @@ -21,7 +21,7 @@ - + Hotel Folio Report folio.report.wizard form @@ -29,7 +29,7 @@ new - diff --git a/hotel_calendar/wizard/wizard_reservation.py b/hotel/wizard/wizard_reservation.py similarity index 100% rename from hotel_calendar/wizard/wizard_reservation.py rename to hotel/wizard/wizard_reservation.py diff --git a/hotel_calendar/wizard/wizard_reservation.xml b/hotel/wizard/wizard_reservation.xml similarity index 100% rename from hotel_calendar/wizard/wizard_reservation.xml rename to hotel/wizard/wizard_reservation.xml diff --git a/hotel_calendar/__init__.py b/hotel_calendar/__init__.py index b3576e732..bc60815fd 100644 --- a/hotel_calendar/__init__.py +++ b/hotel_calendar/__init__.py @@ -2,4 +2,3 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from . import models from . import controllers -from . import wizard diff --git a/hotel_calendar/__manifest__.py b/hotel_calendar/__manifest__.py index 9c977338a..0e910b00b 100644 --- a/hotel_calendar/__manifest__.py +++ b/hotel_calendar/__manifest__.py @@ -33,7 +33,6 @@ 'data/menus.xml', 'data/records.xml', 'security/ir.model.access.csv', - 'wizard/wizard_reservation.xml' ], 'qweb': [ 'static/src/xml/*.xml', diff --git a/hotel_calendar/static/src/xml/hotel_calendar_view.xml b/hotel_calendar/static/src/xml/hotel_calendar_view.xml index ed12414fc..45bd912e2 100644 --- a/hotel_calendar/static/src/xml/hotel_calendar_view.xml +++ b/hotel_calendar/static/src/xml/hotel_calendar_view.xml @@ -17,7 +17,7 @@ BOOKS
-