Add forecast operation
This commit is contained in:
parent
e95848ff84
commit
293dafdc07
|
@ -61,6 +61,8 @@ def register():
|
|||
booking.BookingVoucher,
|
||||
booking.RoomsOccupancyStart,
|
||||
booking.RevenueForecastStart,
|
||||
booking.OperationForecastStart,
|
||||
booking.RevenueSegmentationStart,
|
||||
booking.UpdateHolderStart,
|
||||
booking.BookingChannelCommision,
|
||||
booking.BillBookingStart,
|
||||
|
@ -103,6 +105,8 @@ def register():
|
|||
Pool.register(
|
||||
booking.BookingReport,
|
||||
booking.RevenueForecastReport,
|
||||
booking.OperationForecastReport,
|
||||
booking.RevenueSegmentationReport,
|
||||
booking.RoomsOccupancyReport,
|
||||
booking.BookingStatusReport,
|
||||
booking.ManagerReport,
|
||||
|
@ -119,6 +123,8 @@ def register():
|
|||
Pool.register(
|
||||
booking.SelectRooms,
|
||||
booking.RevenueForecast,
|
||||
booking.OperationForecast,
|
||||
booking.RevenueSegmentation,
|
||||
booking.RoomsOccupancy,
|
||||
booking.BookingStatus,
|
||||
booking.UpdateHolder,
|
||||
|
|
340
booking.py
340
booking.py
|
@ -2,6 +2,7 @@
|
|||
# this repository contains the full copyright notices and license terms.
|
||||
from datetime import datetime, timedelta, date
|
||||
from decimal import Decimal
|
||||
import copy
|
||||
|
||||
from trytond.model import Workflow, ModelView, ModelSQL, fields
|
||||
from trytond.wizard import (
|
||||
|
@ -59,6 +60,7 @@ class Booking(Workflow, ModelSQL, ModelView):
|
|||
children_num = fields.Function(fields.Integer('Children Number'),
|
||||
'get_person_num')
|
||||
group = fields.Boolean('Group', states=STATES_BLOCKED)
|
||||
corporative = fields.Boolean('Corporative', states=STATES_BLOCKED)
|
||||
complementary = fields.Boolean('Complementary', states=STATES_BLOCKED)
|
||||
type_complementary = fields.Selection(COMPLEMENTARY, 'Type Complementary',
|
||||
states=STATES_BLOCKED)
|
||||
|
@ -183,6 +185,8 @@ class Booking(Workflow, ModelSQL, ModelView):
|
|||
('post_checkin', 'Post Checkin'),
|
||||
], 'Collection Mode', required=False,
|
||||
help="Commission collection mode")
|
||||
link_web_checkin = fields.Function(fields.Char('Link Web Check-in'),
|
||||
'get_link_web_checkin')
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
|
@ -939,6 +943,14 @@ class Booking(Workflow, ModelSQL, ModelView):
|
|||
invoice.save()
|
||||
invoice.update_taxes([invoice])
|
||||
|
||||
def get_link_web_checkin(self, name=None):
|
||||
transaction = Transaction().context
|
||||
pool = Pool()
|
||||
host, _ = transaction['_request']['http_host'].split(":")
|
||||
db = pool.database_name
|
||||
link = f'http://{host}:3100/app/{db}/web_checkin/{self.id}'
|
||||
return link
|
||||
|
||||
@classmethod
|
||||
def _get_new_invoice(cls, data):
|
||||
pool = Pool()
|
||||
|
@ -1384,9 +1396,9 @@ class RevenueForecastStart(ModelView):
|
|||
fiscalyear = fields.Many2One('account.fiscalyear', 'Fiscalyear',
|
||||
required=True)
|
||||
period = fields.Many2One('account.period', 'Period', required=True,
|
||||
domain=[
|
||||
('fiscalyear', '=', Eval('fiscalyear')),
|
||||
])
|
||||
domain=[
|
||||
('fiscalyear', '=', Eval('fiscalyear')),
|
||||
])
|
||||
|
||||
@staticmethod
|
||||
def default_fiscalyear():
|
||||
|
@ -2450,3 +2462,325 @@ class BillBooking(Wizard):
|
|||
Booking.concile_charges([bk])
|
||||
Booking.check_finished([bk])
|
||||
return 'end'
|
||||
|
||||
|
||||
class OperationForecastStart(ModelView):
|
||||
'Operation Forecast Start'
|
||||
__name__ = 'hotel.print_operation_forecast.start'
|
||||
company = fields.Many2One('company.company', 'Company', required=True)
|
||||
fiscalyear = fields.Many2One('account.fiscalyear', 'Fiscalyear',
|
||||
required=True)
|
||||
period = fields.Many2One('account.period', 'Period', required=True,
|
||||
domain=[
|
||||
('fiscalyear', '=', Eval('fiscalyear')),
|
||||
])
|
||||
|
||||
@staticmethod
|
||||
def default_fiscalyear():
|
||||
Fiscalyear = Pool().get('account.fiscalyear')
|
||||
fiscalyear, = Fiscalyear.search([], limit=1, order=[
|
||||
('start_date', 'DESC')])
|
||||
return fiscalyear.id
|
||||
|
||||
@staticmethod
|
||||
def default_company():
|
||||
return Transaction().context.get('company')
|
||||
|
||||
|
||||
class OperationForecast(Wizard):
|
||||
'Operation Forecast'
|
||||
__name__ = 'hotel.print_operation_forecast'
|
||||
start = StateView(
|
||||
'hotel.print_operation_forecast.start',
|
||||
'hotel.print_operation_forecast_start_view_form', [
|
||||
Button('Cancel', 'end', 'tryton-cancel'),
|
||||
Button('Print', 'print_', 'tryton-print', default=True),
|
||||
])
|
||||
print_ = StateReport('hotel.operation_forecast.report')
|
||||
|
||||
def do_print_(self, action):
|
||||
company = self.start.company
|
||||
data = {
|
||||
'period': self.start.period.id,
|
||||
'company': company.id,
|
||||
}
|
||||
return action, data
|
||||
|
||||
def transition_print_(self):
|
||||
return 'end'
|
||||
|
||||
|
||||
class OperationForecastReport(Report):
|
||||
"Operation Forecast Report"
|
||||
__name__ = 'hotel.operation_forecast.report'
|
||||
|
||||
@classmethod
|
||||
def get_context(cls, records, header, data):
|
||||
report_context = super().get_context(records, header, data)
|
||||
pool = Pool()
|
||||
Company = pool.get('company.company')
|
||||
Period = pool.get('account.period')
|
||||
Room = pool.get('hotel.room')
|
||||
Occupancy = pool.get('hotel.folio.occupancy')
|
||||
|
||||
period = Period(data['period'])
|
||||
start_date = period.start_date
|
||||
end_date = period.end_date
|
||||
range_day = (end_date - start_date).days + 1
|
||||
default_data = {
|
||||
'available_rooms': 0,
|
||||
'occupied_rooms': 0,
|
||||
'pax': 0,
|
||||
'rooms_complementary': 0,
|
||||
'info_complementary': '',
|
||||
'rooms_maintenance': 0,
|
||||
'occupancy_rate': 0,
|
||||
'revenue': 0,
|
||||
'balance': 0,
|
||||
'average_price': 0,
|
||||
}
|
||||
|
||||
records = []
|
||||
|
||||
rooms = Room.search([])
|
||||
available_rooms = len(rooms)
|
||||
pax = []
|
||||
occupied_rooms = []
|
||||
revenue = []
|
||||
balance = 0
|
||||
|
||||
totals = copy.deepcopy(default_data)
|
||||
today = date.today()
|
||||
for key, value in default_data.items():
|
||||
totals[key + '_effective'] = []
|
||||
totals[key + '_forecast'] = []
|
||||
|
||||
for nday in range(range_day):
|
||||
rooms_complementary = 0
|
||||
rooms_maintenance = 0
|
||||
tdate = start_date + timedelta(nday)
|
||||
data_day = copy.deepcopy(default_data)
|
||||
data_day['date'] = tdate
|
||||
is_sunday = 'no'
|
||||
if tdate.weekday() == 6:
|
||||
is_sunday = 'yes'
|
||||
data_day['sunday'] = is_sunday
|
||||
occupancies = Occupancy.search([
|
||||
('occupancy_date', '=', str(tdate))
|
||||
])
|
||||
occ_rooms = len(occupancies)
|
||||
data_day['available_rooms'] = available_rooms
|
||||
data_day['occupied_rooms'] = occ_rooms
|
||||
occupied_rooms.append(occ_rooms)
|
||||
|
||||
sum_pax = sum([len(occ.folio.guests) for occ in occupancies])
|
||||
data_day['pax'] = sum_pax
|
||||
pax.append(sum_pax)
|
||||
amounts = []
|
||||
for occ in occupancies:
|
||||
if occ.charge:
|
||||
amount = occ.charge.unit_price
|
||||
else:
|
||||
amount = occ.unit_price
|
||||
amounts.append(amount)
|
||||
|
||||
total_amounts = sum(amounts)
|
||||
data_day['revenue'] = total_amounts
|
||||
revenue.append(total_amounts)
|
||||
|
||||
balance += total_amounts
|
||||
data_day['balance'] = balance
|
||||
data_day['occupancy_rate'] = round(
|
||||
occ_rooms / available_rooms, 2)
|
||||
|
||||
average_price = None
|
||||
if occ_rooms:
|
||||
average_price = round(total_amounts / occ_rooms, 2)
|
||||
data_day['average_price'] = average_price
|
||||
records.append(data_day)
|
||||
|
||||
if tdate < today:
|
||||
row = '_effective'
|
||||
else:
|
||||
row = '_forecast'
|
||||
|
||||
totals['available_rooms' + row].append(available_rooms)
|
||||
totals['occupied_rooms' + row].append(occ_rooms)
|
||||
totals['pax' + row].append(sum_pax)
|
||||
totals['rooms_complementary' + row].append(rooms_complementary)
|
||||
totals['rooms_maintenance' + row].append(rooms_maintenance)
|
||||
totals['revenue' + row].append(total_amounts)
|
||||
|
||||
for field in (
|
||||
'available_rooms', 'occupied_rooms', 'pax',
|
||||
'rooms_complementary', 'revenue', 'rooms_maintenance'):
|
||||
_effective = field + '_effective'
|
||||
_forecast = field + '_forecast'
|
||||
totals[_effective] = sum(totals[_effective])
|
||||
totals[_forecast] = sum(totals[_forecast])
|
||||
|
||||
average_price_effective = ''
|
||||
average_price_forecast = ''
|
||||
occupancy_rate_effective = ''
|
||||
occupancy_rate_forecast = ''
|
||||
if totals['occupied_rooms_effective'] > 0:
|
||||
average_price_effective = totals['revenue_effective'] / totals['occupied_rooms_effective']
|
||||
average_price_forecast = totals['revenue_forecast'] / totals['occupied_rooms_forecast']
|
||||
if totals['available_rooms_effective'] > 0:
|
||||
occupancy_rate_effective = totals['occupied_rooms_effective'] / totals['available_rooms_effective']
|
||||
occupancy_rate_forecast = totals['occupied_rooms_forecast'] / totals['available_rooms_forecast']
|
||||
|
||||
totals['occupancy_rate_effective'] = occupancy_rate_effective
|
||||
totals['occupancy_rate_forecast'] = occupancy_rate_forecast
|
||||
totals['average_price_effective'] = average_price_effective
|
||||
totals['average_price_forecast'] = average_price_forecast
|
||||
data.update(totals)
|
||||
total_available_rooms = len(rooms) * range_day
|
||||
total_occupied_rooms = sum(occupied_rooms)
|
||||
data['available_rooms'] = total_available_rooms
|
||||
data['occupied_rooms'] = total_occupied_rooms
|
||||
data['pax'] = sum(pax)
|
||||
data['revenue'] = sum(revenue)
|
||||
data['balance'] = balance
|
||||
_average_price = None
|
||||
_occupancy_rate = None
|
||||
if total_occupied_rooms > 0:
|
||||
_average_price = sum(revenue) / sum(occupied_rooms)
|
||||
if total_available_rooms > 0:
|
||||
_occupancy_rate = round(
|
||||
total_occupied_rooms / total_available_rooms, 2)
|
||||
data['average_price'] = _average_price
|
||||
data['occupancy_rate'] = _occupancy_rate
|
||||
|
||||
report_context['records'] = records
|
||||
report_context['period'] = period.name
|
||||
report_context['company'] = Company(data['company'])
|
||||
return report_context
|
||||
|
||||
|
||||
class RevenueSegmentationStart(ModelView):
|
||||
'Revenue Segmentation Start'
|
||||
__name__ = 'hotel.revenue_segmentation.start'
|
||||
company = fields.Many2One('company.company', 'Company', required=True)
|
||||
fiscalyear = fields.Many2One('account.fiscalyear', 'Fiscalyear',
|
||||
required=True)
|
||||
period = fields.Many2One('account.period', 'Period', required=True,
|
||||
domain=[
|
||||
('fiscalyear', '=', Eval('fiscalyear')),
|
||||
])
|
||||
|
||||
@staticmethod
|
||||
def default_fiscalyear():
|
||||
Fiscalyear = Pool().get('account.fiscalyear')
|
||||
fiscalyear, = Fiscalyear.search([], limit=1, order=[
|
||||
('start_date', 'DESC')])
|
||||
return fiscalyear.id
|
||||
|
||||
@staticmethod
|
||||
def default_company():
|
||||
return Transaction().context.get('company')
|
||||
|
||||
|
||||
class RevenueSegmentation(Wizard):
|
||||
'Revenue Segmentation'
|
||||
__name__ = 'hotel.revenue_segmentation'
|
||||
start = StateView(
|
||||
'hotel.revenue_segmentation.start',
|
||||
'hotel.revenue_segmentation_start_view_form', [
|
||||
Button('Cancel', 'end', 'tryton-cancel'),
|
||||
Button('Print', 'print_', 'tryton-print', default=True),
|
||||
])
|
||||
print_ = StateReport('hotel.revenue_segmentation.report')
|
||||
|
||||
def do_print_(self, action):
|
||||
company = self.start.company
|
||||
data = {
|
||||
'period': self.start.period.id,
|
||||
'company': company.id,
|
||||
}
|
||||
return action, data
|
||||
|
||||
def transition_print_(self):
|
||||
return 'end'
|
||||
|
||||
|
||||
class RevenueSegmentationReport(Report):
|
||||
"Revenue Segmentation Report"
|
||||
__name__ = 'hotel.revenue_segmentation.report'
|
||||
|
||||
@classmethod
|
||||
def get_context(cls, records, header, data):
|
||||
report_context = super().get_context(records, header, data)
|
||||
pool = Pool()
|
||||
Company = pool.get('company.company')
|
||||
Period = pool.get('account.period')
|
||||
Occupancy = pool.get('hotel.folio.occupancy')
|
||||
|
||||
period = Period(data['period'])
|
||||
start_date = period.start_date
|
||||
end_date = period.end_date
|
||||
range_day = (end_date - start_date).days + 1
|
||||
|
||||
default_data = {
|
||||
'complementary': [],
|
||||
'room_complementary': [],
|
||||
'booking': [],
|
||||
'room_booking': [],
|
||||
'expedia': [],
|
||||
'room_expedia': [],
|
||||
'other': [],
|
||||
'room_other': [],
|
||||
'web': [],
|
||||
'room_web': [],
|
||||
'direct': [],
|
||||
'room_direct': [],
|
||||
'corporative': [],
|
||||
'room_corporative': [],
|
||||
'group': [],
|
||||
'room_group': [],
|
||||
}
|
||||
_data = copy.deepcopy(default_data)
|
||||
|
||||
records = []
|
||||
for nday in range(range_day):
|
||||
tdate = start_date + timedelta(nday)
|
||||
|
||||
data_day = copy.deepcopy(default_data)
|
||||
data_day['date'] = tdate
|
||||
occupancies = Occupancy.search([
|
||||
('occupancy_date', '=', str(tdate))
|
||||
])
|
||||
|
||||
for occ in occupancies:
|
||||
booking = occ.folio.booking
|
||||
field = 'direct'
|
||||
if booking.channel and booking.channel.code in ('booking', 'expedia'):
|
||||
field = booking.channel.code
|
||||
else:
|
||||
if booking.media == 'web':
|
||||
field = 'web'
|
||||
|
||||
data_day[field].append(occ.unit_price)
|
||||
data_day['room_' + field].append(1)
|
||||
|
||||
_data[field].append(occ.unit_price)
|
||||
_data['room_' + field].append(1)
|
||||
|
||||
records.append(data_day)
|
||||
|
||||
for rec in records:
|
||||
for key, values in rec.items():
|
||||
if key == 'date':
|
||||
continue
|
||||
rec[key] = sum(values)
|
||||
|
||||
for key, value in _data.items():
|
||||
if key == 'date':
|
||||
continue
|
||||
_data[key] = sum(_data[key])
|
||||
|
||||
data.update(_data)
|
||||
report_context['records'] = records
|
||||
report_context['period'] = period.name
|
||||
report_context['company'] = Company(data['company'])
|
||||
return report_context
|
||||
|
|
39
booking.xml
39
booking.xml
|
@ -342,5 +342,44 @@ this repository contains the full copyright notices and license terms. -->
|
|||
<field name="type">form</field>
|
||||
<field name="name">bill_booking_start_form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.action.report" id="report_operation_forecast">
|
||||
<field name="name">Operation Forecast Report</field>
|
||||
<field name="model"></field>
|
||||
<field name="report_name">hotel.operation_forecast.report</field>
|
||||
<field name="report">hotel/operation_forecast.fods</field>
|
||||
<field name="template_extension">ods</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="print_operation_forecast_start_view_form">
|
||||
<field name="model">hotel.print_operation_forecast.start</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">operation_forecast_start_form</field>
|
||||
</record>
|
||||
<record model="ir.action.wizard" id="wizard_print_operation_forecast">
|
||||
<field name="name">Operation Forecast</field>
|
||||
<field name="wiz_name">hotel.print_operation_forecast</field>
|
||||
</record>
|
||||
<menuitem parent="hotel.menu_reporting" id="menu_operation_forecast"
|
||||
action="wizard_print_operation_forecast"/>
|
||||
|
||||
<record model="ir.action.report" id="report_revenue_segmentation">
|
||||
<field name="name">Revenue Segmentation Report</field>
|
||||
<field name="model"></field>
|
||||
<field name="report_name">hotel.revenue_segmentation.report</field>
|
||||
<field name="report">hotel/revenue_segmentation.fods</field>
|
||||
<field name="template_extension">ods</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="revenue_segmentation_start_view_form">
|
||||
<field name="model">hotel.revenue_segmentation.start</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">revenue_segmentation_start_form</field>
|
||||
</record>
|
||||
<record model="ir.action.wizard" id="wizard_revenue_segmentation">
|
||||
<field name="name">Revenue Segmentation</field>
|
||||
<field name="wiz_name">hotel.revenue_segmentation</field>
|
||||
</record>
|
||||
<menuitem parent="hotel.menu_reporting" id="menu_revenue_segmentation"
|
||||
action="wizard_revenue_segmentation"/>
|
||||
|
||||
</data>
|
||||
</tryton>
|
||||
|
|
|
@ -55,11 +55,11 @@ MEDIA = [
|
|||
('fax', 'Fax'),
|
||||
('mail', 'Mail'),
|
||||
('chat', 'Chat'),
|
||||
('direct', 'Direct'),
|
||||
('walking', 'Walking'),
|
||||
('web', 'Web'),
|
||||
('channel_manager', 'Channel Manager'),
|
||||
('other', 'Other'),
|
||||
('ota', 'OTA'),
|
||||
('other', 'Other'),
|
||||
]
|
||||
|
||||
PLAN = [
|
||||
|
|
2
folio.py
2
folio.py
|
@ -1169,7 +1169,7 @@ class FolioGuest(ModelSQL, ModelView):
|
|||
def default_country():
|
||||
Config = Pool().get('hotel.configuration')
|
||||
config = Config.get_configuration()
|
||||
if config.country:
|
||||
if config and config.country:
|
||||
return config.country.id
|
||||
|
||||
@staticmethod
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,5 +1,5 @@
|
|||
[tryton]
|
||||
version=6.0.67
|
||||
version=6.0.68
|
||||
depends:
|
||||
party
|
||||
company
|
||||
|
|
|
@ -38,10 +38,10 @@ this repository contains the full copyright notices and license terms. -->
|
|||
<group col="6" id="add_info_group_taxes" colspan="2">
|
||||
<label name="group"/>
|
||||
<field name="group"/>
|
||||
<label name="vip"/>
|
||||
<field name="vip"/>
|
||||
<label name="taxes_exception"/>
|
||||
<field name="taxes_exception"/>
|
||||
<label name="corporative"/>
|
||||
<field name="corporative"/>
|
||||
<label name="link_web_checkin"/>
|
||||
<field name="link_web_checkin" widget="url"/>
|
||||
</group>
|
||||
<notebook colspan="6">
|
||||
<page string="Lines" id="lines">
|
||||
|
@ -56,9 +56,6 @@ this repository contains the full copyright notices and license terms. -->
|
|||
<page string="Stock" id="stock_moves">
|
||||
<field name="stock_moves" colspan="4"/>
|
||||
</page>
|
||||
<page string="Account Moves" id="account_moves">
|
||||
<field name="income_moves" colspan="4"/>
|
||||
</page>
|
||||
<page string="Additional Info" id="additional_info">
|
||||
<label name="company"/>
|
||||
<field name="company"/>
|
||||
|
@ -70,6 +67,8 @@ this repository contains the full copyright notices and license terms. -->
|
|||
<field name="guarantee"/>
|
||||
<label name="reason"/>
|
||||
<field name="reason"/>
|
||||
<label name="vip"/>
|
||||
<field name="vip"/>
|
||||
<group col="6" id="people_num" colspan="4">
|
||||
<label name="guests_num"/>
|
||||
<field name="guests_num"/>
|
||||
|
@ -108,6 +107,9 @@ this repository contains the full copyright notices and license terms. -->
|
|||
<page string="Notifications" id="notifications">
|
||||
<field name="emails" colspan="4"/>
|
||||
</page>
|
||||
<page string="Account Moves" id="account_moves">
|
||||
<field name="income_moves" colspan="4"/>
|
||||
</page>
|
||||
</notebook>
|
||||
<group col="4" colspan="3" id="buttons">
|
||||
<button name="cancel" string="Cancel" icon="tryton-cancel"/>
|
||||
|
|
|
@ -7,10 +7,11 @@ this repository contains the full copyright notices and license terms. -->
|
|||
<field name="party" expand="1"/>
|
||||
<field name="contact" expand="1"/>
|
||||
<field name="booking_date" widget="date"/>
|
||||
<!-- <field name="channel" expand="1"/> -->
|
||||
<field name="ota_booking_code"/>
|
||||
<field name="media"/>
|
||||
<field name="total_amount"/>
|
||||
<field name="total_advances"/>
|
||||
<field name="state" expand="1"/>
|
||||
<field name="group" expand="1"/>
|
||||
<field name="corporative" expand="1"/>
|
||||
</tree>
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<form>
|
||||
<label name="fiscalyear"/>
|
||||
<field name="fiscalyear" widget="selection"/>
|
||||
<label name="company"/>
|
||||
<field name="company" widget="selection"/>
|
||||
<label name="period"/>
|
||||
<field name="period" widget="selection"/>
|
||||
</form>
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<form>
|
||||
<label name="fiscalyear"/>
|
||||
<field name="fiscalyear" widget="selection"/>
|
||||
<label name="company"/>
|
||||
<field name="company" widget="selection"/>
|
||||
<label name="period"/>
|
||||
<field name="period" widget="selection"/>
|
||||
</form>
|
Loading…
Reference in New Issue