trytonpsk-hotel/operation.py

1570 lines
55 KiB
Python

# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from decimal import Decimal
from datetime import date, timedelta, datetime
from trytond.model import ModelView, Workflow, ModelSQL, fields
from trytond.pyson import Eval, Bool
from trytond.pool import Pool
from trytond.report import Report
from trytond.wizard import Wizard, StateView, StateAction, Button, StateTransition
from trytond.transaction import Transaction
from trytond.model.exceptions import AccessError
from trytond.i18n import gettext
STATES_OP = {
'readonly': Eval('state').in_(['check_out', 'done', 'cancelled'])
}
STATES_MNT = {
'readonly': Eval('state') != 'draft',
}
OPERATION_STATES = [
('draft', 'Draft'),
('open', 'Open'),
('closed', 'Closed'),
('invoiced', 'Invoiced'),
('transfered', 'Transfered'),
('cancelled', 'Cancel'),
]
COLOR_BOOKING = {
'draft': '#9b9f9f',
'check_in': '#e6bd0f',
'check_out': '#09a4cd',
'done': '#315274',
}
COLOR_MNT = {
'draft': '#e87a7a',
'confirmed': '#d45757',
'done': '#d45757',
}
INVOICE_STATES = [
('', ''),
('pending', 'Pending'),
('in_process', 'In Process'),
('invoiced', 'Invoiced'),
('paid', 'Paid')
]
COMPLEMENTARY = [
('', ''),
('in_house', 'In House'),
('courtesy', 'courtesy')
]
_ZERO = Decimal('0.00')
class Operation(Workflow, ModelSQL, ModelView):
'Operation'
__name__ = 'hotel.operation'
_rec_name = 'reference'
reference = fields.Char('Reference', select=True, readonly=True)
kind = fields.Selection([
('occupancy', 'Occupancy'),
('maintenance', 'Maintenance'),
('statement', 'Statement'),
], 'Kind')
party = fields.Many2One('party.party', 'Party', select=True,
states=STATES_OP, required=True)
main_guest = fields.Many2One('party.party', 'Main Guest', select=True,
states=STATES_OP, required=True)
room = fields.Many2One('hotel.room', 'Room', select=True, required=False,
states=STATES_OP)
accommodation = fields.Many2One('product.product', 'Accommodation Product',
domain=[('kind', '=', 'accommodation')], states=STATES_OP)
start_date = fields.Date('Arrival Date', states=STATES_OP)
end_date = fields.Date('Departure Date', states=STATES_OP)
description = fields.Char('Description', states=STATES_OP)
state = fields.Selection(OPERATION_STATES, 'State', readonly=True)
state_string = state.translated('state')
invoice_state = fields.Selection(INVOICE_STATES, 'Invoice State',
readonly=True)
lines = fields.One2Many('hotel.operation.line', 'operation', 'Lines',
states=STATES_OP)
origin = fields.Reference('Origin', selection='get_origin', select=True,
states={'readonly': True})
guests = fields.One2Many('hotel.operation.guest', 'operation',
'Guests', add_remove=[], states=STATES_OP)
price_list = fields.Many2One('product.price_list', 'Price List',
states=STATES_OP, depends=['state', 'lines'])
currency = fields.Many2One('currency.currency', 'Currency',
required=True, depends=['state'])
nights_quantity = fields.Function(fields.Integer('Nights'),
'get_nights_quantity')
unit_price = fields.Numeric('Unit Price', digits=(16, 4), states=STATES_OP)
unit_price_w_tax = fields.Function(fields.Numeric('Unit Price With Tax'),
'get_unit_price_w_tax')
taxes_exception = fields.Boolean('Taxes Exception', states=STATES_OP)
add_default_charges = fields.Boolean('Add Default Charges', states=STATES_OP)
# sale = fields.Function(fields.Many2One('sale.sale', 'Sale'), 'get_sale')
sale_line = fields.Many2One('sale.line', 'Sale Line', select=True)
company = fields.Many2One('company.company', 'Company', states=STATES_OP)
total_amount = fields.Function(fields.Numeric('Total Amount',
digits=(16, 2)), 'get_total_amount')
total_amount_today = fields.Function(fields.Numeric('Total Amount Today',
digits=(16, 2)), 'get_total_amount')
room_amount = fields.Function(fields.Numeric('Room Amount',
digits=(16, 2)), 'get_room_amount')
room_amount_today = fields.Function(fields.Numeric('Room Amount Today',
digits=(16, 2)), 'get_room_amount')
pending_payment = fields.Function(fields.Numeric('Pending Payment',
digits=(16, 2)), 'get_pending_payment')
notes = fields.Text('Notes', select=True, states=STATES_OP)
complementary = fields.Boolean('complementary', states=STATES_OP)
type_complementary = fields.Selection(COMPLEMENTARY, 'Type Complementary', states={
'invisible': ~Bool(Eval('complementary')),
'required': Bool(Eval('complementary')),
})
operation_target = fields.Many2One('hotel.operation', 'Operation Target')
transfered_operations = fields.One2Many('hotel.operation',
'operation_target', 'Transfered Operations', states=STATES_OP)
vouchers = fields.Many2Many('hotel.operation-account.voucher', 'operation',
'voucher', 'Vouchers', states=STATES_OP, domain=[], depends=['party'])
agent = fields.Many2One('commission.agent', 'Agent')
# stock_moves = fields.Many2Many('hotel.operation-stock.move', 'operation',
# 'move', 'Stock Moves', states={'readonly': True})
@classmethod
def __setup__(cls):
super(Operation, cls).__setup__()
cls._transitions |= set((
('draft', 'open'),
('draft', 'cancelled'),
('open', 'draft'),
('open', 'closed'),
('closed', 'open'),
))
cls._buttons.update({
'draft': {
'invisible': True,
},
'open': {
'invisible': Eval('state') != 'draft',
},
'close': {
'invisible': Eval('state') == 'closed',
},
'cancel': {
'invisible': Eval('state') != 'draft',
},
'bill': {
'invisible': Eval('invoice_state') == 'invoiced',
},
'pay_advance': {
'invisible': Eval('state') == 'closed',
},
})
@classmethod
def validate(cls, records):
super(Operation, cls).validate(records)
for r in records:
operations = cls.search([
('room', '=', r.room.id),
('id', '!=', r.id),
('state', '!=', 'cancelled'),
('kind', '=', 'occupancy'),
['AND', ['OR',
[
('start_date', '>=', r.start_date),
('end_date', '<', r.start_date),
], [
('start_date', '>=', r.end_date),
('end_date', '<=', r.end_date),
]
]]])
if operations:
raise AccessError(gettext('hotel.msg_occupied_room', s=r.room.name))
@classmethod
def occupancy_rate(cls, start, end):
pool = Pool()
today_ = pool.get('ir.date').today()
after_days = today_ + timedelta(days=3)
before_days = today_ - timedelta(days=3)
operations = cls.search([
('state', 'not in', ['closed', 'cancelled']),
('start_date', '>=', before_days),
('end_date', '<=', after_days),
])
Room = pool.get('hotel.room')
Housekeeping = pool.get('hotel.housekeeping')
housekeepings = Housekeeping.search([
('state', '=', 'maintenance')
])
room_ids = []
for hk in housekeepings:
room_ids.append(hk.room.id)
rooms = Room.search([
('id', 'not in', room_ids)
])
qty_booking = len(operations)
qty_rooms = len(rooms)
return qty_booking / qty_rooms
def get_unit_price_w_tax(self, name=None):
Tax = Pool().get('account.tax')
res = self.unit_price
if self.unit_price and self.accommodation and not self.taxes_exception:
values = Tax.compute(
self.accommodation.template.customer_taxes_used,
self.unit_price or _ZERO, 1
)
if len(values) > 0:
value = values[0]
res = value['base'] + value['amount']
return round(res, Operation.total_amount.digits[1])
def get_pending_payment(self, name=None):
res = _ZERO
if self.total_amount:
amount_paid = sum([v.amount_to_pay for v in self.vouchers])
res = self.total_amount - amount_paid
res = round(res, Operation.total_amount.digits[1])
return res
def get_total_amount(self, name=None):
res = []
if name == 'total_amount':
if self.unit_price_w_tax and self.nights_quantity:
res.append(self.room_amount)
else:
if self.unit_price_w_tax and self.nights_quantity:
res.append(self.room_amount_today)
res.extend([l.amount for l in self.lines])
if res:
return round(sum(res), Operation.total_amount.digits[1])
return _ZERO
def get_room_amount(self, name=None):
res = 0
Date = Pool().get('ir.date')
if self.unit_price_w_tax and self.nights_quantity:
if name == 'room_amount':
res = self.unit_price_w_tax * self.nights_quantity
else:
delta = (Date.today() - self.start_date).days
res = self.unit_price_w_tax * delta
return round(res, Operation.total_amount.digits[1])
@staticmethod
def default_invoice_state():
return 'pending'
@staticmethod
def default_company():
return Transaction().context.get('company')
@staticmethod
def default_currency():
Company = Pool().get('company.company')
company = Company(Transaction().context.get('company'))
return company.currency.id
@staticmethod
def default_state():
return 'draft'
@staticmethod
def default_kind():
return 'occupancy'
@classmethod
def search_rec_name(cls, name, clause):
_, operator, value = clause
if operator.startswith('!') or operator.startswith('not '):
bool_op = 'AND'
else:
bool_op = 'OR'
domain = [
bool_op,
('main_guest', operator, value),
('reference', operator, value),
('room.name', operator, value),
('party.name', operator, value),
]
return domain
@classmethod
@ModelView.button
@Workflow.transition('open')
def open(cls, records):
for rec in records:
rec.update_housekeeping('check_in')
@classmethod
@ModelView.button
@Workflow.transition('closed')
def close(cls, records):
pass
# for rec in records:
# rec.update_housekeeping('check_out')
@classmethod
@ModelView.button
def bill(cls, records):
for rec in records:
if rec.complementary:
return
cls.create_sales([rec])
@classmethod
@ModelView.button
@Workflow.transition('draft')
def draft(cls, records):
pass
@classmethod
@ModelView.button
@Workflow.transition('cancelled')
def cancel(cls, records):
pass
@classmethod
@ModelView.button_action('hotel.wizard_operation_check_out')
def check_out_wizard(cls, records):
pass
@classmethod
@ModelView.button_action('hotel.wizard_operation_advance_voucher')
def pay_advance(cls, records):
pass
@classmethod
def validate_check_out_date(cls, records):
Date = Pool().get('ir.date')
today = Date.today()
for rec in records:
if rec.end_date != today:
return False
def update_housekeeping(self, state):
pool = Pool()
Housekeeping = pool.get('hotel.housekeeping')
Configuration = pool.get('hotel.configuration')
config = Configuration.get_configuration()
if not config.cleaning_check_in or not config.cleaning_check_out \
or not config.cleaning_occupied:
raise AccessError(gettext('hotel.msg_missing_cleaning_configuration'))
if state == 'check_in':
values = {
'availability': 'occupied',
'cleaning_type': config.cleaning_occupied.id
}
elif state == 'check_out':
values = {
'availability': 'available',
'state': 'dirty',
'cleaning_type': config.cleaning_check_out.id
}
housekeepings = Housekeeping.search([
('room', '=', self.room.id)
])
if not housekeepings:
raise AccessError(gettext('hotel.msg_missing_configuration_housekeeping_rooms'))
Housekeeping.write(housekeepings, values)
# def get_sale(self, name=None):
# if self.sale_line:
# return self.sale_line.sale.id
@classmethod
def _get_origin(cls):
return ['hotel.operation.maintenance', 'hotel.booking.line']
@classmethod
def copy(cls, operations, default=None):
if default is None:
default = {}
else:
default = default.copy()
default.setdefault('state', 'draft')
default.setdefault('invoice_state', '')
return super(Operation, cls).copy(operations, default=default)
@classmethod
def get_origin(cls):
Model = Pool().get('ir.model')
models = cls._get_origin()
models = Model.search([
('model', 'in', models),
])
return [(None, '')] + [(m.model, m.name) for m in models]
@classmethod
def get_calendar(cls, args={}):
Room = Pool().get('hotel.room')
rooms = Room.search_read([], fields_names=['name', 'code'],
order=[('code', 'ASC')]
)
resources = [{'id': str(r['id']), 'name': r['name']} for r in rooms]
_date = args.get('date')
if _date:
_date = datetime.strptime(_date, 'YYYY-MM-DD')
else:
_date = date.today()
start_date = _date - timedelta(days=15)
end_date = _date + timedelta(days=45)
operations = cls.search_read([
('start_date', '>=', start_date),
('start_date', '<=', end_date),
('kind', '=', 'occupancy'),
('state', 'in', ['draft', 'check_in', 'check_out', 'finished']),
], fields_names=['reference', 'kind', 'party.name', 'start_date',
'end_date', 'room', 'room.name', 'accommodation.rec_name',
'state', 'origin', 'main_guest.name']
)
events = []
for o in operations:
_state = o['state']
if o['kind'] == 'occupancy':
bg_color = COLOR_BOOKING[_state]
else:
bg_color = COLOR_MNT[_state]
events.append({
'id': o['id'],
'resourceId': str(o['room']),
'title': o['party.name'] or ' ',
'reference': o['reference'],
'room': o['room.name'],
'accommodation': o['accommodation.rec_name'],
'start': str(o['start_date']),
'end': str(o['end_date']),
'resizable': False,
'bgColor': bg_color
})
return [resources, events]
# @classmethod
# def validate(cls, operations):
# for op in operations:
# op.check_dates(
# op.start_date,
# op.end_date,
# op.room,
# op,
# )
# super(Operation, cls).validate(operations)
@classmethod
def get_available_rooms(cls, start_date, end_date, rooms_ids=[], oper=None):
"""
Look for available rooms.
given the date interval, return a list of room ids.
a room is available if it has no operation that overlaps
with the given date interval.
the optional 'rooms' list is a list of room instance, is an
additional filter, specifying the ids of the desirable rooms.
the optional 'oper' is an operation object that has to be
filtered out of the test. it is useful for validating an already
existing operation.
"""
if start_date >= end_date:
raise AccessError(gettext('hotel.msg_invalid_date_range'))
# define the domain of the operations that find a
# room to be available
dom = ['AND', ['OR',
[
('start_date', '>=', start_date),
('start_date', '<', end_date),
('kind', '=', 'occupancy'),
], [
('end_date', '<=', end_date),
('end_date', '>', start_date),
('kind', '=', 'occupancy'),
], [
('start_date', '<=', start_date),
('end_date', '>=', end_date),
('kind', '=', 'occupancy'),
],
]]
## If oper was specified, do not compare the operations with it
if oper is not None:
dom.append(('id', '!=', oper.id))
if rooms_ids:
dom.append(('room', 'in', rooms_ids))
operations = cls.search(dom)
rooms_not_available_ids = [operation.room.id for operation in operations]
rooms_available_ids = set(rooms_ids) - set(rooms_not_available_ids)
return list(rooms_available_ids)
@classmethod
def check_dates(cls, start_date, end_date, room, operation=None):
rooms_ids = [room.id]
available_rooms = cls.get_available_rooms(
start_date, end_date, rooms_ids, operation
)
if room.id not in available_rooms:
raise AccessError(gettext('hotel.overlap_operation_line', s=room.name))
def get_nights_quantity(self, name=None):
"""
Compute nights between start and end
return a integer the mean days of occupancy.
"""
nights = 0
if self.start_date and self.end_date:
nights = (self.end_date - self.start_date).days
return nights
@classmethod
def get_room_info(cls, op):
description = ' \n'.join([
op.accommodation.rec_name,
'Huesped Principal: ' + op.main_guest.name,
'Habitacion: ' + op.room.name,
'Llegada: ' + str(op.start_date),
'Salida: ' + str(op.end_date),
])
return description
@classmethod
def get_grouped_sales(cls, operations):
res = {}
for op in operations:
for top in op.transfered_operations:
top.party = op.party
operations.append(top)
filtered_ops = [op for op in operations if op.invoice_state == 'pending']
for op in filtered_ops:
if op.party.id not in res.keys():
# Add room product to sale
res[op.party.id] = {
'party': op.party.id,
'currency': op.currency.id,
'payment_term': None,
'guests_qty': len(op.guests) + 1,
'reference': op.reference,
'agent': op.agent or None,
'rooms': op.room.name,
'company': op.company.id,
'add_default_charges': op.add_default_charges,
'vouchers': op.vouchers,
'lines': [{
'operations': [op],
'description': cls.get_room_info(op),
'quantity': op.nights_quantity,
'product': op.accommodation,
'unit_price': op.unit_price,
'taxes_exception': op.taxes_exception,
}]
}
else:
res[op.party.id]['rooms'] += ' ' + op.room.name
res[op.party.id]['lines'].append({
'operations': [op],
'description': cls.get_room_info(op),
'quantity': op.nights_quantity,
'product': op.accommodation,
'unit_price': op.unit_price,
'taxes_exception': op.taxes_exception,
})
for line in op.lines:
if line.sale_line:
continue
invoice_party_id = line.invoice_to.id
unit_price = op.currency.round(line.unit_price)
if invoice_party_id != op.party.id:
if invoice_party_id not in res.keys():
res[invoice_party_id] = {
'party': line.invoice_to.id,
'currency': op.currency.id,
'payment_term': None,
'lines': [],
}
res[invoice_party_id]['lines'].append({
'description': ' | '.join([
str(line.date_service),
line.order or '',
line.description or ''
]),
'quantity': line.quantity,
'product': line.product,
'operation_line': line,
'unit_price': unit_price,
'operation_line': line,
'taxes_exception': op.taxes_exception,
})
return res
@classmethod
def create_sales(cls, records):
pool = Pool()
Date = pool.get('ir.date')
SaleLine = pool.get('sale.line')
OperationLine = pool.get('hotel.operation.line')
Configuration = pool.get('hotel.configuration')
SaleVoucher = pool.get('sale.sale-account.voucher')
configuration = Configuration.get_configuration()
date_ = Date.today()
ctx = {}
sales_to_create = cls.get_grouped_sales(records)
for rec in sales_to_create.values():
if rec.get('price_list'):
ctx['price_list'] = rec.get('price_list')
ctx['sale_date'] = date_
ctx['currency'] = rec['currency']
if rec.party:
ctx['customer'] = rec.party.id
sale = cls._get_new_sale(rec)
sale.save()
if rec.get('vouchers'):
for v in rec['vouchers']:
SaleVoucher.create([{
'voucher': v.id,
'sale': sale.id
}])
# Add and create default charges lines if exists
if rec.get('guests_qty') and rec.get('add_default_charges'):
for product in configuration.default_charges:
taxes_ids = cls.get_taxes(sale, product)
new_line = {
'sale': sale.id,
'type': 'line',
'unit': product.template.default_uom.id,
'quantity': rec['guests_qty'],
'unit_price': product.template.list_price,
'product': product.id,
'description': product.rec_name,
}
if taxes_ids:
new_line.update({'taxes': [('add', taxes_ids)]})
if new_line:
SaleLine.create([new_line])
for _line in rec['lines']:
line, = SaleLine.create([cls._get_sale_line(sale, _line)])
if _line.get('operations'):
cls.write(_line.get('operations'), {
'sale_line': line.id,
'invoice_state': 'in_process'
})
else:
OperationLine.write([_line.get('operation_line')], {
'sale_line': line.id,
'state': 'invoiced',
})
@classmethod
def _get_new_sale(cls, data):
pool = Pool()
Sale = pool.get('sale.sale')
Party = pool.get('party.party')
Agent = pool.get('commission.agent')
Date = pool.get('ir.date')
date_ = Date.today()
price_list_id = None
if data.get('price_list'):
price_list_id = data['price_list']
company_id = Transaction().context.get('company')
party = Party(data['party'])
description = data.get('rooms')
reference = data.get('reference')
agent = None
if data.get('agent'):
agent = Agent(data['agent'])
return Sale(
company=company_id,
payment_term=data['payment_term'],
party=party.id,
price_list=price_list_id,
sale_date=date_,
description=description,
state='draft',
reference=reference,
agent=agent,
invoice_address=Party.address_get(party, type='invoice'),
shipment_address=Party.address_get(party, type='delivery'),
)
@classmethod
def get_context_price(cls, sale, product):
context = {}
context['currency'] = sale.currency.id
context['customer'] = sale.party.id
context['price_list'] = sale.price_list.id if sale.price_list else None
context['uom'] = product.template.default_uom.id
# Set taxes before unit_price to have taxes in context of sale price
taxes = []
pattern = {}
for tax in product.customer_taxes_used:
if sale.party and sale.party.customer_tax_rule:
tax_ids = sale.party.customer_tax_rule.apply(tax, pattern)
if tax_ids:
taxes.extend(tax_ids)
continue
taxes.append(tax.id)
if sale.party and sale.party.customer_tax_rule:
tax_ids = sale.party.customer_tax_rule.apply(None, pattern)
if tax_ids:
taxes.extend(tax_ids)
context['taxes'] = taxes
return context
@classmethod
def get_taxes(cls, sale, product):
ctx = cls.get_context_price(sale, product)
return ctx['taxes']
@classmethod
def _get_sale_line(cls, sale, line):
new_line = {
'sale': sale.id,
'type': 'line',
'unit': line['product'].template.default_uom.id,
'quantity': line['quantity'],
'unit_price': line['unit_price'],
'product': line['product'].id,
'description': line['description'],
}
if not line['taxes_exception']:
taxes_ids = cls.get_taxes(sale, line['product'])
if taxes_ids:
new_line.update({'taxes': [('add', taxes_ids)]})
return new_line
class OperationGuest(ModelSQL, ModelView):
'Operation Guest'
__name__ = 'hotel.operation.guest'
_rec_name = 'party'
operation = fields.Many2One('hotel.operation', 'Operation',
required=True, ondelete='CASCADE')
party = fields.Many2One('party.party', 'Party', required=True)
def get_rec_name(self, name):
if self.party:
return self.party.name
class OperationMaintenance(Workflow, ModelSQL, ModelView):
'Operation Maintenance'
__name__ = 'hotel.operation.maintenance'
operation = fields.Many2One('hotel.operation', 'Operation', states=STATES_MNT)
room = fields.Many2One('hotel.room', 'Room', states=STATES_MNT, select=True,
required=True)
kind = fields.Selection([
('maintenance', 'Maintenance'),
], 'Kind', readonly=True, select=True)
start_date = fields.Date('Start Date', states=STATES_MNT, required=True,
select=True)
end_date = fields.Date('End Date', states=STATES_MNT, required=True,
select=True)
description = fields.Text('Description', states=STATES_MNT, select=True)
state = fields.Selection([
('draft', 'Draft'),
('confirmed', 'Confirmed'),
('finished', 'Finished'),
('cancelled', 'Canceled'),
], 'State', readonly=True, select=True)
@classmethod
def __setup__(cls):
super(OperationMaintenance, cls).__setup__()
cls._transitions |= set((
('draft', 'confirmed'),
('draft', 'cancelled'),
('confirmed', 'finished'),
('confirmed', 'cancelled'),
))
cls._buttons.update({
'cancel': {
'invisible': ~Eval('state').in_(['draft', 'confirmed']),
},
'draft': {
'invisible': Eval('state') != 'confirmed',
},
'confirm': {
'invisible': Eval('state') != 'draft',
},
'finish': {
'invisible': Eval('state') != 'confirmed',
},
})
@classmethod
def trigger_create(cls, records):
for mant in records:
mant.create_operation()
@staticmethod
def default_kind():
return 'maintenance'
@staticmethod
def default_state():
return 'draft'
@classmethod
@ModelView.button
@Workflow.transition('draft')
def draft(cls, records):
pass
@classmethod
@ModelView.button
@Workflow.transition('cancelled')
def cancel(cls, records):
for mant in records:
mant.delete_operation()
@classmethod
@ModelView.button
@Workflow.transition('confirmed')
def confirm(cls, records):
for mant in records:
mant.update_operation({'state': 'confirmed'})
@classmethod
@ModelView.button
@Workflow.transition('finished')
def finish(cls, records):
for mant in records:
mant.update_operation({'state': 'finished'})
def delete_operation(self):
Operation = Pool().get('hotel.operation')
Operation.delete([self.operation])
def create_operation(self):
Operation = Pool().get('hotel.operation')
values = {
'kind': 'maintenance',
'room': self.room,
'start_date': self.start_date,
'end_date': self.end_date,
'state': self.state,
'origin': str(self)
}
operation, = Operation.create([values])
self.write([self], {'operation': operation.id})
def update_operation(self, values):
Operation = Pool().get('hotel.operation')
Operation.write([self.operation], values)
@classmethod
def write(cls, records, values):
Room = Pool().get('hotel.room')
for rec in records:
if values.get('start_date') or values.get('end_date') or values.get('room'):
start_date = values.get('start_date') or rec.start_date
end_date = values.get('end_date') or rec.end_date
room = Room(values.get('room') or rec.room)
Operation().check_dates(start_date, end_date, room, rec.operation)
rec.update_operation({
'start_date': start_date,
'end_date': end_date,
'room': room.id,
})
super(OperationMaintenance, cls).write(records, values)
def check_method(self):
"""
Check the methods.
"""
Operation = Pool().get('hotel.operation')
Operation().check_dates(self.start_date, self.end_date, self.room,
self.operation)
class OperationLine(Workflow, ModelSQL, ModelView):
'Operation Line'
__name__ = 'hotel.operation.line'
operation = fields.Many2One('hotel.operation', 'Operation', required=True)
date_service = fields.Date('Date Service', select=True, required=True)
product = fields.Many2One('product.product', 'Product',
domain=[
('salable', '=', True),
], required=True)
quantity = fields.Integer('Quantity', required=True)
invoice_to = fields.Many2One('party.party', 'Invoice To', required=True)
unit_price = fields.Numeric('Unit Price', required=True)
unit_price_w_tax = fields.Function(fields.Numeric('Unit Price'),
'get_unit_price_w_tax')
order = fields.Char('Order', select=True)
description = fields.Char('Description', select=True)
state = fields.Selection(INVOICE_STATES, 'State', readonly=True)
state_string = state.translated('state')
sale_line = fields.Many2One('sale.line', 'Sale Line', readonly=True)
# sale = fields.Function(fields.Many2One('sale.sale', 'Sale'), 'get_sale')
amount = fields.Function(fields.Numeric('Amount',
digits=(16, 2)), 'get_amount')
taxed_amount = fields.Function(fields.Numeric('Amount with Tax',
digits=(16, 2)), 'get_taxed_amount')
@classmethod
def __setup__(cls):
super(OperationLine, cls).__setup__()
cls._buttons.update({
'transfer': {
'invisible': True,
},
'bill': {
'invisible': Eval('invoice_state') is not None,
},
})
@staticmethod
def default_quantity():
return 1
@staticmethod
def default_date_service():
today = Pool().get('ir.date').today()
return today
def get_amount(self, name=None):
if self.quantity and self.unit_price:
return self.quantity * self.unit_price_w_tax
def get_unit_price_w_tax(self, name=None):
Tax = Pool().get('account.tax')
res = self.unit_price or 0
if self.unit_price:
values = Tax.compute(self.product.template.customer_taxes_used,
self.unit_price, 1)
if values:
value = values[0]
res = value['base'] + value['amount']
return res
def get_taxed_amount(self, name=None):
if self.quantity and self.unit_price:
return self.quantity * self.unit_price
# def get_sale(self, name=None):
# if self.sale_line:
# return self.sale_line.sale.id
# def compute_amount_with_tax(line):
# tax_amount = _ZERO
# amount = _ZERO
# if line.taxes:
# tax_list = Tax.compute(line.taxes, line.unit_price or _ZERO,
# line.quantity or 0.0)
#
# tax_amount = sum([t['amount'] for t in tax_list], _ZERO)
#
# if line.unit_price:
# amount = line.unit_price * Decimal(line.quantity)
# return amount + tax_amount
@classmethod
@ModelView.button
def bill(cls, records):
cls.create_sales(records)
@classmethod
@ModelView.button_action('hotel.wizard_operation_line_transfer')
def transfer(cls, records):
pass
@fields.depends('unit_price', 'product')
def on_change_product(self):
if self.product:
self.unit_price = self.product.template.list_price
self.description = self.product.description
class OpenMigrationStart(ModelView):
'Open Migration Start'
__name__ = 'hotel.open_migration.start'
start_date = fields.Date('Start Date', required=True)
end_date = fields.Date('End Date', required=True)
company = fields.Many2One('company.company', 'Company', required=True)
@staticmethod
def default_company():
return Transaction().context.get('company')
class OpenMigration(Wizard):
'Open Migration'
__name__ = 'hotel.open_migration'
start = StateView('hotel.open_migration.start',
'hotel.open_migration_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Open', 'print_', 'tryton-print', default=True),
])
print_ = StateAction('hotel.report_migration')
def do_print_(self, action):
data = {
'start_date': self.start.start_date,
'end_date': self.start.end_date,
'company': self.start.company.id,
}
return action, data
def transition_print_(self):
return 'end'
class Migration(Report):
'Hotel Migration'
__name__ = 'hotel.migration'
@classmethod
def get_context(cls, records, header, data):
report_context = super().get_context(records, header, data)
Line = Pool().get('hotel.booking.line')
start = data['start_date']
end = data['end_date']
report_context['records'] = Line.search([
('arrival_date', '>=', start),
('arrival_date', '<=', end),
])
return report_context
class OperationReport(Report):
__name__ = 'hotel.operation'
@classmethod
def get_context(cls, records, header, data):
report_context = super().get_context(records, header, data)
user = Pool().get('res.user')(Transaction().user)
report_context['company'] = user.company
return report_context
class CheckOutOperationFailed(ModelView):
'Check Out Operation Failed'
__name__ = 'hotel.operation.check_out.failed'
class CheckOutOperation(Wizard):
'Check Out Operation'
__name__ = 'hotel.operation.check_out'
start = StateTransition()
failed = StateView('hotel.operation.check_out.failed',
'hotel.operation_check_out_failed_view_form', [
Button('Force Check Out', 'force', 'tryton-forward'),
Button('Cancel', 'end', 'tryton-cancel', True),
])
force = StateTransition()
def transition_start(self):
Operation = Pool().get('hotel.operation')
active_id = Transaction().context['active_id']
operations = Operation.browse([active_id])
if Operation.validate_check_out_date(operations):
return 'end'
else:
return 'failed'
def transition_force(self):
Operation = Pool().get('hotel.operation')
active_id = Transaction().context['active_id']
operations = Operation.browse([active_id])
Operation.check_out(operations)
return 'end'
class ChangeRoomStart(ModelView):
'Change Room'
__name__ = 'hotel.operation.change_room.ask'
from_date = fields.Date('From Date', required=True)
room = fields.Many2One('hotel.room', 'Room', required=True, domain=[
# ('id', 'in', Eval('targets')),
])
accommodation = fields.Many2One('product.product',
'Accommodation', domain=[
('template.kind', '=', 'accommodation'),
], required=True)
targets = fields.Function(fields.Many2Many('hotel.room', None, None,
'Targets'), 'on_change_with_targets')
tranfer_charges = fields.Boolean('Transfer Charges')
@staticmethod
def default_from_date():
today = Pool().get('ir.date').today()
return today
@fields.depends('from_date', 'accommodation')
def on_change_with_targets(self, name=None):
pool = Pool()
Operation = pool.get('hotel.operation')
RoomTemplate = pool.get('hotel.room-product.template')
operation = Operation(Transaction().context.get('active_id'))
res = []
if not self.accommodation or not self.from_date:
return res
room_templates = RoomTemplate.search([
('template.accommodation_capacity', '>=', self.accommodation.accommodation_capacity)
])
rooms_ids = [t.room.id for t in room_templates]
rooms_available_ids = Operation.get_available_rooms(
self.from_date,
operation.end_date,
rooms_ids=rooms_ids
)
return rooms_available_ids
class ChangeRoom(Wizard):
'Change Room'
__name__ = 'hotel.operation.change_room'
"""
this is the wizard that allows the front desk employee to transfer
original room, and create a new operation occupany.
"""
start = StateView('hotel.operation.change_room.ask',
'hotel.operation_change_room_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Change', 'change', 'tryton-ok'),
]
)
change = StateTransition()
def transition_change(self):
pool = Pool()
Operation = pool.get('hotel.operation')
OperationLine = pool.get('hotel.operation.line')
operation = Operation(Transaction().context.get('active_id'))
new_operation = {
'reference': operation.reference,
'room': self.start.room.id,
'start_date': self.start.from_date,
'end_date': operation.end_date,
'party': operation.party.id,
'currency': operation.currency.id,
'company': operation.company.id,
'kind': operation.kind,
'main_guest': operation.main_guest.id,
'accommodation': self.start.accommodation.id,
'origin': str(operation.origin),
'state': 'open',
'unit_price': operation.unit_price,
'lines': []
}
lines_to_transfer = []
_operation, = Operation.create([new_operation])
if self.start.tranfer_charges:
for line in operation.lines:
if line.state is None:
lines_to_transfer.append(line)
if lines_to_transfer:
OperationLine.write([lines_to_transfer], {'operation': _operation.id})
operation.end_date = self.start.from_date
operation.state = 'closed'
operation.target = _operation.id
operation.save()
return 'end'
class TransferOperationStart(ModelView):
'Transfer Operation'
__name__ = 'hotel.operation.transfer_operation.ask'
operation = fields.Many2One('hotel.operation', 'Operation',
required=True, domain=[
('state', 'in', ['draft', 'open']),
])
tranfer_charges = fields.Boolean('Transfer Charges')
@staticmethod
def default_tranfer_charges():
return True
class TransferOperation(Wizard):
'Transfer Operation'
__name__ = 'hotel.operation.transfer_operation'
"""
this is the wizard that allows the front desk employee to transfer
original room, and create a new operation occupany.
"""
start = StateView('hotel.operation.transfer_operation.ask',
'hotel.operation_transfer_operation_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Transfer', 'transfer', 'tryton-ok'),
]
)
transfer = StateTransition()
@classmethod
def __setup__(cls):
super(TransferOperation, cls).__setup__()
def transition_transfer(self):
pool = Pool()
Operation = pool.get('hotel.operation')
OperationLine = pool.get('hotel.operation.line')
current_op = Operation(Transaction().context.get('active_id'))
target_op = self.start.operation
if target_op.id == current_op.id:
raise AccessError(gettext('hotel.msg_operation_current'))
lines_to_transfer = []
if self.start.tranfer_charges:
for line in current_op.lines:
if line.state is None:
lines_to_transfer.append(line)
if lines_to_transfer:
OperationLine.write(lines_to_transfer, {
'operation': target_op.id
})
current_op.state = 'transfered'
current_op.operation_target = target_op.id
current_op.save()
target_op.save()
return 'end'
class TransferChargeStart(ModelView):
'Transfer Charge'
__name__ = 'hotel.operation.transfer_charge.ask'
operation = fields.Many2One('hotel.operation', 'Operation',
required=True, domain=[('state', '=', 'check_in')])
class TransferCharge(Wizard):
'Transfer Operation'
__name__ = 'hotel.operation.transfer_charge'
start = StateView('hotel.operation.transfer_charge.ask',
'hotel.operation_transfer_charge_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Transfer', 'transfer', 'tryton-ok'),
]
)
transfer = StateTransition()
def transition_transfer(self):
pool = Pool()
OperationLine = pool.get('hotel.operation.line')
current_line = OperationLine(Transaction().context.get('active_id'))
target_op = self.start.operation
current_operation = current_line.operation
current_line.operation = target_op.id
current_line.save()
current_operation.save()
return 'end'
class OperationByConsumerReport(Report):
__name__ = 'hotel.operation.line'
@classmethod
def get_context(cls, records, header, data):
report_context = super().get_context(records, header, data)
pool = Pool()
Operation = pool.get('hotel.operation')
user = pool.get('res.user')(Transaction().user)
consumer_lines = []
total_amount = 0
if records:
line = records[0]
operation = Operation(line.operation.id)
total_amount = operation.room_amount
for l in operation.lines:
if l.invoice_to.id == line.invoice_to.id:
consumer_lines.append(l)
total_amount += l.amount
setattr(operation, 'lines', consumer_lines)
setattr(operation, 'total_amount', total_amount)
report_context['records'] = [operation]
report_context['company'] = user.company
return report_context
class OperationBill(Wizard):
'Operation Bill'
__name__ = 'hotel.operation.bill'
start_state = 'create_bill'
create_bill = StateTransition()
def transition_create_bill(self):
Operation = Pool().get('hotel.operation')
ids = Transaction().context['active_ids']
operations = Operation.browse(ids)
Operation.bill(operations)
return 'end'
class OperationVoucher(ModelSQL):
'Operation - Voucher'
__name__ = 'hotel.operation-account.voucher'
_table = 'operation_vouchers_rel'
operation = fields.Many2One('hotel.operation', 'Operation',
ondelete='CASCADE', select=True, required=True)
voucher = fields.Many2One('account.voucher', 'Voucher', select=True,
domain=[('voucher_type', '=', 'receipt')], ondelete='RESTRICT',
required=True)
@classmethod
def set_voucher_origin(cls, voucher_id, operation_id):
cls.create([{
'voucher': voucher_id,
'operation': operation_id,
}])
class StatisticsByMonthStart(ModelView):
'Statistics By Month Start'
__name__ = 'hotel.statistics_by_month.start'
period = fields.Many2One('account.period', 'Period', required=True)
company = fields.Many2One('company.company', 'Company', required=True)
@staticmethod
def default_company():
return Transaction().context.get('company')
class StatisticsByMonth(Wizard):
'Statistics By Month'
__name__ = 'hotel.statistics_by_month'
start = StateView('hotel.statistics_by_month.start',
'hotel.print_hotel_statistics_by_month_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Open', 'print_', 'tryton-print', default=True),
])
print_ = StateAction('hotel.statistics_by_month_report')
def do_print_(self, action):
data = {
'period': self.start.period.id,
'company': self.start.company.id,
}
return action, data
def transition_print_(self):
return 'end'
class StatisticsByMonthReport(Report):
'Hotel Statistics By Month'
__name__ = 'hotel.statistics_by_month.report'
@classmethod
def get_context(cls, records, header, data):
report_context = super().get_context(records, header, data)
Operation = Pool().get('hotel.operation')
Period = Pool().get('account.period')
Company = Pool().get('company.company')
Rooms = Pool().get('hotel.room')
period = Period(data['period'])
company = Company(data['company'])
month_days = (period.end_date - period.start_date).days + 1
rooms_available = []
rooms_sold = []
beds_available = []
beds_sold = []
local_guests = []
foreign_guests = []
room1_sold = []
room2_sold = []
room_suite_sold = []
room_other_sold = []
guests_one_ng = []
guests_two_ng = []
guests_three_ng = []
guests_four_ng = []
guests_five_ng = []
guests_six_ng = []
guests_seven_ng = []
guests_eight_ng = []
guests_permanent_ng = []
reason_bussiness = []
reason_bussiness_foreign = []
reason_leisure = []
reason_leisure_foreign = []
reason_convention = []
reason_convention_foreign = []
reason_health = []
reason_health_foreign = []
reason_transport = []
reason_transport_foreign = []
reason_other = []
reason_other_foreign = []
total_guests = []
operations = Operation.search(['OR',
[
('start_date', '>=', period.start_date),
('start_date', '<=', period.end_date),
('kind', '=', 'occupancy'),
], [
('end_date', '>=', period.start_date),
('end_date', '<=', period.end_date),
('kind', '=', 'occupancy'),
]
])
rooms = Rooms.search([])
rooms_available = [len(rooms) * month_days]
for r in rooms:
beds_available.append(
r.main_accommodation.accommodation_capacity * month_days
)
def _get_ctx_dates(op):
start_date = op.start_date
end_date = op.end_date
if op.start_date < period.start_date:
start_date = period.start_date
if op.end_date > period.end_date:
end_date = period.end_date
sub_dates = [
str(start_date + timedelta(n))
for n in range((end_date - start_date).days)
]
return sub_dates
for op in operations:
room_capacity = op.room.main_accommodation.accommodation_capacity
ctx_dates = _get_ctx_dates(op)
len_ctx_dates = len(ctx_dates)
rooms_sold.append(len_ctx_dates)
beds_sold.append(len_ctx_dates * room_capacity)
qty_guests = len(op.guests)
if not qty_guests:
qty_guests = 1
total_guests.append(qty_guests)
is_local = True
if op.main_guest.type_document == '41':
is_local = False
foreign_guests.append(qty_guests)
else:
local_guests.append(qty_guests)
if room_capacity == 1:
room1_sold.append(qty_guests)
elif room_capacity == 2:
room2_sold.append(qty_guests)
elif room_capacity > 2:
room_suite_sold.append(qty_guests)
if len_ctx_dates == 1:
guests_one_ng.append(qty_guests)
elif len_ctx_dates == 2:
guests_two_ng.append(qty_guests)
elif len_ctx_dates == 3:
guests_three_ng.append(qty_guests)
elif len_ctx_dates == 4:
guests_four_ng.append(qty_guests)
elif len_ctx_dates == 5:
guests_five_ng.append(qty_guests)
elif len_ctx_dates == 6:
guests_six_ng.append(qty_guests)
elif len_ctx_dates == 7:
guests_seven_ng.append(qty_guests)
elif len_ctx_dates >= 8:
guests_eight_ng.append(qty_guests)
segment = 'bussiness'
if op.origin:
if hasattr(op.origin, 'booking'):
segment = op.origin.booking.segment
if segment == 'bussiness':
if is_local:
reason_bussiness.append(qty_guests)
else:
reason_bussiness_foreign.append(qty_guests)
elif segment == 'convention':
if is_local:
reason_convention.append(qty_guests)
else:
reason_convention_foreign.append(qty_guests)
elif segment == 'health':
if is_local:
reason_health.append(qty_guests)
else:
reason_health_foreign.append(qty_guests)
else:
if is_local:
reason_leisure.append(qty_guests)
else:
reason_leisure_foreign.append(qty_guests)
def _get_rate(val):
res = 0
if sum_total_guests > 0:
res = round(float(sum(val)) / sum_total_guests, 4)
return res
sum_total_guests = sum(total_guests)
rate_guests_one_ng = _get_rate(guests_one_ng)
rate_guests_two_ng = _get_rate(guests_two_ng)
rate_guests_three_ng = _get_rate(guests_three_ng)
rate_guests_four_ng = _get_rate(guests_four_ng)
rate_guests_five_ng = _get_rate(guests_five_ng)
rate_guests_six_ng = _get_rate(guests_six_ng)
rate_guests_seven_ng = _get_rate(guests_seven_ng)
rate_guests_eight_ng = _get_rate(guests_eight_ng)
report_context['period'] = period.name
report_context['company'] = company.party.name
report_context['rooms_available'] = sum(rooms_available)
report_context['rooms_sold'] = sum(rooms_sold)
report_context['beds_available'] = sum(beds_available)
report_context['beds_sold'] = sum(beds_sold)
report_context['local_guests'] = sum(local_guests)
report_context['foreign_guests'] = sum(foreign_guests)
report_context['room1_sold'] = sum(room1_sold)
report_context['room2_sold'] = sum(room2_sold)
report_context['room_suite_sold'] = sum(room_suite_sold)
report_context['room_other_sold'] = sum(room_other_sold)
report_context['guests_one_ng'] = rate_guests_one_ng
report_context['guests_two_ng'] = rate_guests_two_ng
report_context['guests_three_ng'] = rate_guests_three_ng
report_context['guests_four_ng'] = rate_guests_four_ng
report_context['guests_five_ng'] = rate_guests_five_ng
report_context['guests_six_ng'] = rate_guests_six_ng
report_context['guests_seven_ng'] = rate_guests_seven_ng
report_context['guests_eight_ng'] = rate_guests_eight_ng
report_context['guests_permanent_ng'] = sum(guests_permanent_ng)
report_context['reason_bussiness'] = sum(reason_bussiness)
report_context['reason_bussiness_foreign'] = sum(reason_bussiness_foreign)
report_context['reason_leisure'] = sum(reason_leisure)
report_context['reason_leisure_foreign'] = sum(reason_leisure_foreign)
report_context['reason_convention'] = sum(reason_convention)
report_context['reason_convention_foreign'] = sum(reason_convention_foreign)
report_context['reason_health'] = sum(reason_health)
report_context['reason_health_foreign'] = sum(reason_health_foreign)
report_context['reason_transport'] = sum(reason_transport)
report_context['reason_transport_foreign'] = sum(reason_transport_foreign)
report_context['reason_other_foreign'] = sum(reason_other_foreign)
report_context['reason_other'] = sum(reason_other)
return report_context