mirror of
https://github.com/OCA/pms.git
synced 2025-01-29 00:17:45 +02:00
Merge branch '11.0' into pr_roommatik
This commit is contained in:
6
call_center_report/README.rst
Normal file
6
call_center_report/README.rst
Normal file
@@ -0,0 +1,6 @@
|
||||
CALL CENTER REPORT
|
||||
=============
|
||||
|
||||
Export call center report in xls format
|
||||
|
||||
|
||||
21
call_center_report/__init__.py
Normal file
21
call_center_report/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2018 Alexandre Díaz <dev@redneboa.es>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
from . import wizard
|
||||
48
call_center_report/__manifest__.py
Normal file
48
call_center_report/__manifest__.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2018 Alexandre Díaz <dev@redneboa.es>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
{
|
||||
'name': 'Call Center Report',
|
||||
'version': '1.0',
|
||||
'author': "Dario Lodeiros",
|
||||
'website': 'https://www.eiqui.com',
|
||||
'category': 'reports',
|
||||
'summary': "Export services and reservation report in xls format",
|
||||
'description': "Call Center Report",
|
||||
'depends': [
|
||||
'hotel',
|
||||
],
|
||||
'external_dependencies': {
|
||||
'python': ['xlsxwriter']
|
||||
},
|
||||
'data': [
|
||||
'wizard/call_center_report.xml',
|
||||
'data/menus.xml',
|
||||
],
|
||||
'qweb': [],
|
||||
'test': [
|
||||
],
|
||||
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
'application': False,
|
||||
'license': 'AGPL-3',
|
||||
}
|
||||
11
call_center_report/data/menus.xml
Normal file
11
call_center_report/data/menus.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<menuitem id="call_center_report_wizard" name="Call Center Report"
|
||||
parent="hotel.hotel_management_menu"
|
||||
groups="hotel.group_hotel_call,hotel.group_hotel_manager"
|
||||
action="action_open_call_center_report_wizard" sequence="1000" />
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
171
call_center_report/i18n/es.po
Normal file
171
call_center_report/i18n/es.po
Normal file
@@ -0,0 +1,171 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * cash_daily_report
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 10.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-05-20 07:33+0000\n"
|
||||
"PO-Revision-Date: 2018-05-20 09:34+0200\n"
|
||||
"Last-Translator: <>\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: \n"
|
||||
"Language: es\n"
|
||||
"X-Generator: Poedit 1.8.7.1\n"
|
||||
|
||||
#. module: cash_daily_report
|
||||
#: code:addons/cash_daily_report/wizard/cash_daily_report.py:85
|
||||
#, python-format
|
||||
msgid "Amount"
|
||||
msgstr "Importe"
|
||||
|
||||
#. module: cash_daily_report
|
||||
#: code:addons/cash_daily_report/wizard/cash_daily_report.py:78
|
||||
#: model:ir.ui.view,arch_db:cash_daily_report.view_cash_daily_report_wizard
|
||||
#, python-format
|
||||
msgid "Cash Daily Report"
|
||||
msgstr "Informe de caja"
|
||||
|
||||
#. module: cash_daily_report
|
||||
#: model:ir.actions.act_window,name:cash_daily_report.action_open_cash_daily_report_wizard
|
||||
#: model:ir.ui.menu,name:cash_daily_report.cash_daily_report_wizard
|
||||
msgid "Cash Daily Report Wizard"
|
||||
msgstr "Informe de caja diaria"
|
||||
|
||||
#. module: cash_daily_report
|
||||
#: code:addons/cash_daily_report/wizard/cash_daily_report.py:82
|
||||
#, python-format
|
||||
msgid "Client"
|
||||
msgstr "Cliente"
|
||||
|
||||
#. module: cash_daily_report
|
||||
#: model:ir.ui.view,arch_db:cash_daily_report.view_cash_daily_report_wizard
|
||||
msgid "Close"
|
||||
msgstr "Cerrar"
|
||||
|
||||
#. module: cash_daily_report
|
||||
#: model:ir.model.fields,field_description:cash_daily_report.field_cash_daily_report_wizard_create_uid
|
||||
msgid "Created by"
|
||||
msgstr "Creado por"
|
||||
|
||||
#. module: cash_daily_report
|
||||
#: model:ir.model.fields,field_description:cash_daily_report.field_cash_daily_report_wizard_create_date
|
||||
msgid "Created on"
|
||||
msgstr "Creado en"
|
||||
|
||||
#. module: cash_daily_report
|
||||
#: code:addons/cash_daily_report/wizard/cash_daily_report.py:83
|
||||
#, python-format
|
||||
msgid "Date"
|
||||
msgstr "Fecha"
|
||||
|
||||
#. module: cash_daily_report
|
||||
#: model:ir.model.fields,field_description:cash_daily_report.field_cash_daily_report_wizard_display_name
|
||||
msgid "Display Name"
|
||||
msgstr "Mostrar Nombre"
|
||||
|
||||
#. module: cash_daily_report
|
||||
#: model:ir.model.fields,field_description:cash_daily_report.field_cash_daily_report_wizard_date_end
|
||||
msgid "End Date"
|
||||
msgstr "Fecha finalización"
|
||||
|
||||
#. module: cash_daily_report
|
||||
#: model:ir.ui.view,arch_db:cash_daily_report.view_cash_daily_report_wizard
|
||||
msgid "Generate XLS"
|
||||
msgstr "Generar XLS"
|
||||
|
||||
#. module: cash_daily_report
|
||||
#: model:ir.model.fields,field_description:cash_daily_report.field_cash_daily_report_wizard_id
|
||||
msgid "ID"
|
||||
msgstr "ID"
|
||||
|
||||
#. module: cash_daily_report
|
||||
#: code:addons/cash_daily_report/wizard/cash_daily_report.py:84
|
||||
#, python-format
|
||||
msgid "Journal"
|
||||
msgstr "Diario"
|
||||
|
||||
#. module: cash_daily_report
|
||||
#: model:ir.model.fields,field_description:cash_daily_report.field_cash_daily_report_wizard___last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr "Última modificación en"
|
||||
|
||||
#. module: cash_daily_report
|
||||
#: model:ir.model.fields,field_description:cash_daily_report.field_cash_daily_report_wizard_write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr "Última actualización por"
|
||||
|
||||
#. module: cash_daily_report
|
||||
#: model:ir.model.fields,field_description:cash_daily_report.field_cash_daily_report_wizard_write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr "Última actualización en"
|
||||
|
||||
#. module: cash_daily_report
|
||||
#: code:addons/cash_daily_report/wizard/cash_daily_report.py:80
|
||||
#, python-format
|
||||
msgid "Name"
|
||||
msgstr "Nombre"
|
||||
|
||||
#. module: cash_daily_report
|
||||
#: code:addons/cash_daily_report/wizard/cash_daily_report.py:128
|
||||
#, python-format
|
||||
msgid "Not Any Payments"
|
||||
msgstr "No hay movimientos"
|
||||
|
||||
#. module: cash_daily_report
|
||||
#: code:addons/cash_daily_report/wizard/cash_daily_report.py:81
|
||||
#, python-format
|
||||
msgid "Reference"
|
||||
msgstr "Referencia"
|
||||
|
||||
#. module: cash_daily_report
|
||||
#: model:ir.model.fields,field_description:cash_daily_report.field_cash_daily_report_wizard_date_start
|
||||
msgid "Start Date"
|
||||
msgstr "Fecha de inicio"
|
||||
|
||||
#. module: cash_daily_report
|
||||
#: code:addons/cash_daily_report/wizard/cash_daily_report.py:143
|
||||
#, python-format
|
||||
msgid "TOTAL"
|
||||
msgstr "TOTAL"
|
||||
|
||||
#. module: cash_daily_report
|
||||
#: code:addons/cash_daily_report/wizard/cash_daily_report.py:139
|
||||
#, python-format
|
||||
msgid "TOTAL PAYMENT RETURNS"
|
||||
msgstr "TOTAL DEVOLUCIONES"
|
||||
|
||||
#. module: cash_daily_report
|
||||
#: code:addons/cash_daily_report/wizard/cash_daily_report.py:134
|
||||
#, python-format
|
||||
msgid "TOTAL PAYMENTS"
|
||||
msgstr "TOTAL PAGOS"
|
||||
|
||||
#. module: cash_daily_report
|
||||
#: model:ir.ui.menu,name:cash_daily_report.menu_account_finance_xls_reports
|
||||
msgid "XLS Reports"
|
||||
msgstr "XLS Reports"
|
||||
|
||||
#. module: cash_daily_report
|
||||
#: model:ir.model.fields,field_description:cash_daily_report.field_cash_daily_report_wizard_xls_binary
|
||||
msgid "Xls binary"
|
||||
msgstr "Xls archivo"
|
||||
|
||||
#. module: cash_daily_report
|
||||
#: model:ir.model.fields,field_description:cash_daily_report.field_cash_daily_report_wizard_xls_filename
|
||||
msgid "Xls filename"
|
||||
msgstr "Xls nombre de archivo"
|
||||
|
||||
#. module: cash_daily_report
|
||||
#: model:ir.model,name:cash_daily_report.model_cash_daily_report_wizard
|
||||
msgid "cash.daily.report.wizard"
|
||||
msgstr "cash.daily.report.wizard"
|
||||
|
||||
#. module: cash_daily_report
|
||||
#: model:ir.ui.view,arch_db:cash_daily_report.view_cash_daily_report_wizard
|
||||
msgid "or"
|
||||
msgstr "o"
|
||||
21
call_center_report/wizard/__init__.py
Normal file
21
call_center_report/wizard/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2018 Alexandre Díaz <dev@redneboa.es>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
from . import call_center_report
|
||||
346
call_center_report/wizard/call_center_report.py
Normal file
346
call_center_report/wizard/call_center_report.py
Normal file
@@ -0,0 +1,346 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2018 Alexandre Díaz <dev@redneboa.es>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
from io import BytesIO
|
||||
from datetime import datetime, date
|
||||
import xlsxwriter
|
||||
import base64
|
||||
from odoo import api, fields, models, _
|
||||
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
|
||||
|
||||
|
||||
class CallCenterReportWizard(models.TransientModel):
|
||||
_name = 'call.center.report.wizard'
|
||||
|
||||
@api.model
|
||||
def _get_default_date_start(self):
|
||||
return datetime.now().strftime(DEFAULT_SERVER_DATE_FORMAT)
|
||||
|
||||
@api.model
|
||||
def _get_default_date_end(self):
|
||||
return datetime.now().strftime(DEFAULT_SERVER_DATE_FORMAT)
|
||||
|
||||
date_start = fields.Date("Start Date", default=_get_default_date_start)
|
||||
date_end = fields.Date("End Date", default=_get_default_date_end)
|
||||
xls_filename = fields.Char()
|
||||
xls_binary = fields.Binary()
|
||||
|
||||
@api.model
|
||||
def _export(self):
|
||||
date_format = "%d-%m-%Y"
|
||||
time_format = "%H:%M"
|
||||
file_data = BytesIO()
|
||||
workbook = xlsxwriter.Workbook(file_data, {
|
||||
'strings_to_numbers': True,
|
||||
'default_date_format': 'dd/mm/yyyy'
|
||||
})
|
||||
|
||||
company_id = self.env.user.company_id
|
||||
workbook.set_properties({
|
||||
'title': 'Exported data from ' + company_id.name,
|
||||
'subject': 'Payments Data from Odoo of ' + company_id.name,
|
||||
'author': 'Odoo',
|
||||
'manager': u'Call Center',
|
||||
'company': company_id.name,
|
||||
'category': 'Hoja de Calculo',
|
||||
'keywords': 'payments, odoo, data, ' + company_id.name,
|
||||
'comments': 'Created with Python in Odoo and XlsxWriter'})
|
||||
workbook.use_zip64()
|
||||
|
||||
xls_cell_format_date = workbook.add_format({
|
||||
'num_format': 'dd/mm/yyyy'
|
||||
})
|
||||
xls_cell_format_money = workbook.add_format({
|
||||
'num_format': '#,##0.00'
|
||||
})
|
||||
xls_cell_format_header = workbook.add_format({
|
||||
'bg_color': '#CCCCCC'
|
||||
})
|
||||
|
||||
worksheet = workbook.add_worksheet(_('Call Center Report - Production'))
|
||||
|
||||
worksheet.write('A1', _('Ficha'), xls_cell_format_header)
|
||||
worksheet.write('B1', _('Fecha de Pedido'), xls_cell_format_header)
|
||||
worksheet.write('C1', _('Cliente'), xls_cell_format_header)
|
||||
worksheet.write('D1', _('Producto'), xls_cell_format_header)
|
||||
worksheet.write('E1', _('Noches/Uds'), xls_cell_format_header)
|
||||
worksheet.write('F1', _('Adultos'), xls_cell_format_header)
|
||||
worksheet.write('G1', _('Checkin'), xls_cell_format_header)
|
||||
worksheet.write('H1', _('In-Hora'), xls_cell_format_header)
|
||||
worksheet.write('I1', _('Checkout'), xls_cell_format_header)
|
||||
worksheet.write('J1', _('Creado por'), xls_cell_format_header)
|
||||
worksheet.write('K1', _('Total'), xls_cell_format_header)
|
||||
|
||||
worksheet.set_column('B:B', 20)
|
||||
worksheet.set_column('C:C', 20)
|
||||
worksheet.set_column('D:D', 20)
|
||||
worksheet.set_column('E:E', 20)
|
||||
worksheet.set_column('F:F', 13)
|
||||
|
||||
reservations_obj = self.env['hotel.reservation']
|
||||
reservations = reservations_obj.search([
|
||||
('checkout', '>=', self.date_start),
|
||||
('checkout', '<=', self.date_end),
|
||||
('state', '=', 'done'),
|
||||
('channel_type', '=', 'call'),
|
||||
('folio_id.pending_amount', '<', 1),
|
||||
])
|
||||
offset = 1
|
||||
total_reservation_amount = 0.0
|
||||
for k_res, v_res in enumerate(reservations):
|
||||
checkin_date = datetime.strptime(v_res.checkin, DEFAULT_SERVER_DATE_FORMAT)
|
||||
checkout_date = datetime.strptime(v_res.checkout, DEFAULT_SERVER_DATE_FORMAT)
|
||||
worksheet.write(k_res+offset, 0, v_res.folio_id.name)
|
||||
worksheet.write(k_res+offset, 1, v_res.folio_id.date_order,
|
||||
xls_cell_format_date)
|
||||
worksheet.write(k_res+offset, 2, v_res.partner_id.name)
|
||||
worksheet.write(k_res+offset, 3, v_res.room_type_id.name)
|
||||
worksheet.write(k_res+offset, 4, v_res.nights)
|
||||
worksheet.write(k_res+offset, 5, v_res.adults)
|
||||
worksheet.write(k_res+offset, 6, checkin_date.strftime(date_format),
|
||||
xls_cell_format_date)
|
||||
worksheet.write(k_res+offset, 7, v_res.arrival_hour)
|
||||
worksheet.write(k_res+offset, 8, checkout_date.strftime(date_format),
|
||||
xls_cell_format_date)
|
||||
worksheet.write(k_res+offset, 9, v_res.create_uid.name)
|
||||
worksheet.write(k_res+offset, 10, v_res.price_total,
|
||||
xls_cell_format_money)
|
||||
total_reservation_amount += v_res.price_total
|
||||
|
||||
folio_ids = reservations.mapped('folio_id.id')
|
||||
folios = self.env['hotel.folio'].browse(folio_ids)
|
||||
services = self.env['hotel.service'].browse()
|
||||
for folio in folios:
|
||||
services += folio.service_ids.filtered(lambda r:
|
||||
r.channel_type == 'call' and r.folio_id.pending_amount < 1)
|
||||
offset += len(reservations)
|
||||
total_service_amount = k_line = 0.0
|
||||
for k_service, v_service in enumerate(services):
|
||||
worksheet.write(k_service+offset, 0, v_service.folio_id.name)
|
||||
worksheet.write(k_service+offset, 1, v_service.folio_id.date_order,
|
||||
xls_cell_format_date)
|
||||
worksheet.write(k_service+offset, 2, v_service.folio_id.partner_id.name)
|
||||
worksheet.write(k_service+offset, 3, v_service.product_id.name)
|
||||
worksheet.write(k_service+offset, 4, v_service.product_qty)
|
||||
worksheet.write(k_service+offset, 5, '')
|
||||
worksheet.write(k_service+offset, 6, '')
|
||||
worksheet.write(k_service+offset, 7, '')
|
||||
worksheet.write(k_service+offset, 8, '')
|
||||
worksheet.write(k_service+offset, 9, v_service .create_uid.name)
|
||||
worksheet.write(k_service+offset, 10, v_service.price_total,
|
||||
xls_cell_format_money)
|
||||
total_service_amount += v_service.price_total
|
||||
offset += len(services)
|
||||
#~ if total_reservation_amount == 0 and total_service_amount == 0:
|
||||
#~ raise UserError(_('No Hay reservas de Call Center'))
|
||||
line = offset
|
||||
if k_line:
|
||||
line = k_line + offset
|
||||
if total_reservation_amount > 0:
|
||||
line += 1
|
||||
worksheet.write(line, 9, _('TOTAL RESERVAS'))
|
||||
worksheet.write(line, 10, total_reservation_amount,
|
||||
xls_cell_format_money)
|
||||
if total_service_amount > 0:
|
||||
line += 1
|
||||
worksheet.write(line, 9, _('TOTAL SERVICIOS'))
|
||||
worksheet.write(line, 10, total_service_amount,
|
||||
xls_cell_format_money)
|
||||
line += 1
|
||||
worksheet.write(line, 9, _('TOTAL'))
|
||||
worksheet.write(line, 10 , total_reservation_amount + total_service_amount,
|
||||
xls_cell_format_money)
|
||||
|
||||
worksheet = workbook.add_worksheet(_('Call Center Report - Sales'))
|
||||
|
||||
worksheet.write('A1', _('Estado'), xls_cell_format_header)
|
||||
worksheet.write('B1', _('Ficha'), xls_cell_format_header)
|
||||
worksheet.write('C1', _('Fecha de Pedido'), xls_cell_format_header)
|
||||
worksheet.write('D1', _('Cliente'), xls_cell_format_header)
|
||||
worksheet.write('E1', _('Producto'), xls_cell_format_header)
|
||||
worksheet.write('F1', _('Noches/Uds'), xls_cell_format_header)
|
||||
worksheet.write('G1', _('Adultos'), xls_cell_format_header)
|
||||
worksheet.write('H1', _('Checkin'), xls_cell_format_header)
|
||||
worksheet.write('I1', _('In-Hora'), xls_cell_format_header)
|
||||
worksheet.write('J1', _('Checkout'), xls_cell_format_header)
|
||||
worksheet.write('K1', _('Creado por'), xls_cell_format_header)
|
||||
worksheet.write('L1', _('Total'), xls_cell_format_header)
|
||||
|
||||
worksheet.set_column('B:B', 20)
|
||||
worksheet.set_column('C:C', 20)
|
||||
worksheet.set_column('D:D', 20)
|
||||
worksheet.set_column('E:E', 20)
|
||||
worksheet.set_column('F:F', 13)
|
||||
|
||||
reservations_obj = self.env['hotel.reservation']
|
||||
reservations = reservations_obj.search([
|
||||
('folio_id.date_order', '>=', self.date_start),
|
||||
('folio_id.date_order', '<=', self.date_end),
|
||||
('channel_type','=','call'),
|
||||
])
|
||||
offset = 1
|
||||
total_reservation_amount = 0.0
|
||||
for k_res, v_res in enumerate(reservations):
|
||||
checkin_date = datetime.strptime(v_res.checkin, DEFAULT_SERVER_DATE_FORMAT)
|
||||
checkout_date = datetime.strptime(v_res.checkout, DEFAULT_SERVER_DATE_FORMAT)
|
||||
worksheet.write(k_res+offset, 0, v_res.state)
|
||||
worksheet.write(k_res+offset, 1, v_res.folio_id.name)
|
||||
worksheet.write(k_res+offset, 2, v_res.folio_id.date_order,
|
||||
xls_cell_format_date)
|
||||
worksheet.write(k_res+offset, 3, v_res.partner_id.name)
|
||||
worksheet.write(k_res+offset, 4, v_res.room_type_id.name)
|
||||
worksheet.write(k_res+offset, 5, v_res.nights)
|
||||
worksheet.write(k_res+offset, 6, v_res.adults)
|
||||
worksheet.write(k_res+offset, 7, checkin_date.strftime(date_format),
|
||||
xls_cell_format_date)
|
||||
worksheet.write(k_res+offset, 8, v_res.arrival_hour)
|
||||
worksheet.write(k_res+offset, 9, checkout_date.strftime(date_format),
|
||||
xls_cell_format_date)
|
||||
worksheet.write(k_res+offset, 10, v_res.create_uid.name)
|
||||
worksheet.write(k_res+offset, 11, v_res.price_total,
|
||||
xls_cell_format_money)
|
||||
total_reservation_amount += v_res.price_total
|
||||
|
||||
folio_ids = reservations.mapped('folio_id.id')
|
||||
folios = self.env['hotel.folio'].browse(folio_ids)
|
||||
services = self.env['hotel.service'].browse()
|
||||
for folio in folios:
|
||||
services += folio.service_ids.filtered(lambda r:
|
||||
r.channel_type == 'call' and r.folio_id.pending_amount < 1)
|
||||
offset += len(reservations)
|
||||
total_service_amount = k_line = 0.0
|
||||
for k_service, v_service in enumerate(services):
|
||||
worksheet.write(k_service+offset, 1, v_service.folio_id.state)
|
||||
worksheet.write(k_service+offset, 1, v_service.folio_id.name)
|
||||
worksheet.write(k_service+offset, 2, v_service.folio_id.date_order,
|
||||
xls_cell_format_date)
|
||||
worksheet.write(k_service+offset, 3, v_service.folio_id.partner_id.name)
|
||||
worksheet.write(k_service+offset, 4, v_service.product_id.name)
|
||||
worksheet.write(k_service+offset, 5, v_service.product_qty)
|
||||
worksheet.write(k_service+offset, 6, '')
|
||||
worksheet.write(k_service+offset, 7, '')
|
||||
worksheet.write(k_service+offset, 8, '')
|
||||
worksheet.write(k_service+offset, 9, '')
|
||||
worksheet.write(k_service+offset, 10, v_service .create_uid.name)
|
||||
worksheet.write(k_service+offset, 11, v_service.price_total,
|
||||
xls_cell_format_money)
|
||||
total_service_amount += v_service.price_total
|
||||
offset += len(services)
|
||||
#~ if total_reservation_amount == 0 and total_service_amount == 0:
|
||||
#~ raise UserError(_('No Hay reservas de Call Center'))
|
||||
line = offset
|
||||
if k_line:
|
||||
line = k_line + offset
|
||||
if total_reservation_amount > 0:
|
||||
line += 1
|
||||
worksheet.write(line, 10, _('TOTAL RESERVAS'))
|
||||
worksheet.write(line, 11, total_reservation_amount,
|
||||
xls_cell_format_money)
|
||||
if total_service_amount > 0:
|
||||
line += 1
|
||||
worksheet.write(line, 10, _('TOTAL SERVICIOS'))
|
||||
worksheet.write(line, 11, total_service_amount,
|
||||
xls_cell_format_money)
|
||||
line += 1
|
||||
worksheet.write(line, 10, _('TOTAL'))
|
||||
worksheet.write(line, 11 , total_reservation_amount + total_service_amount,
|
||||
xls_cell_format_money)
|
||||
|
||||
worksheet = workbook.add_worksheet(_('Call Center Report - Cancelations'))
|
||||
|
||||
worksheet.write('A1', _('Estado'), xls_cell_format_header)
|
||||
worksheet.write('B1', _('Ficha'), xls_cell_format_header)
|
||||
worksheet.write('C1', _('Fecha de Pedido'), xls_cell_format_header)
|
||||
worksheet.write('D1', _('Cliente'), xls_cell_format_header)
|
||||
worksheet.write('E1', _('Producto'), xls_cell_format_header)
|
||||
worksheet.write('F1', _('Noches/Uds'), xls_cell_format_header)
|
||||
worksheet.write('G1', _('Adultos'), xls_cell_format_header)
|
||||
worksheet.write('H1', _('Checkin'), xls_cell_format_header)
|
||||
worksheet.write('I1', _('In-Hora'), xls_cell_format_header)
|
||||
worksheet.write('J1', _('Checkout'), xls_cell_format_header)
|
||||
worksheet.write('K1', _('Creado por'), xls_cell_format_header)
|
||||
worksheet.write('L1', _('Precio Final'), xls_cell_format_header)
|
||||
worksheet.write('M1', _('Precio Original'), xls_cell_format_header)
|
||||
|
||||
worksheet.set_column('B:B', 20)
|
||||
worksheet.set_column('C:C', 20)
|
||||
worksheet.set_column('D:D', 20)
|
||||
worksheet.set_column('E:E', 20)
|
||||
worksheet.set_column('F:F', 13)
|
||||
|
||||
reservations_obj = self.env['hotel.reservation']
|
||||
reservations = reservations_obj.search([
|
||||
('last_updated_res', '>=', self.date_start),
|
||||
('last_updated_res', '<=', self.date_end),
|
||||
('channel_type','=','call'),
|
||||
('state','=','cancelled'),
|
||||
])
|
||||
offset = 1
|
||||
total_reservation_amount = 0.0
|
||||
for k_res, v_res in enumerate(reservations):
|
||||
checkin_date = datetime.strptime(v_res.checkin, DEFAULT_SERVER_DATE_FORMAT)
|
||||
checkout_date = datetime.strptime(v_res.checkout, DEFAULT_SERVER_DATE_FORMAT)
|
||||
worksheet.write(k_res+offset, 0, v_res.state)
|
||||
worksheet.write(k_res+offset, 1, v_res.folio_id.name)
|
||||
worksheet.write(k_res+offset, 2, v_res.folio_id.date_order,
|
||||
xls_cell_format_date)
|
||||
worksheet.write(k_res+offset, 3, v_res.partner_id.name)
|
||||
worksheet.write(k_res+offset, 4, v_res.room_type_id.name)
|
||||
worksheet.write(k_res+offset, 5, v_res.nights)
|
||||
worksheet.write(k_res+offset, 6, v_res.adults)
|
||||
worksheet.write(k_res+offset, 7, checkin_date.strftime(date_format),
|
||||
xls_cell_format_date)
|
||||
worksheet.write(k_res+offset, 8, v_res.arrival_hour)
|
||||
worksheet.write(k_res+offset, 9, checkout_date.strftime(date_format),
|
||||
xls_cell_format_date)
|
||||
worksheet.write(k_res+offset, 10, v_res.create_uid.name)
|
||||
worksheet.write(k_res+offset, 11, v_res.price_total,
|
||||
xls_cell_format_money)
|
||||
worksheet.write(k_res+offset, 12, v_res.price_total - v_res.discount,
|
||||
xls_cell_format_money)
|
||||
total_reservation_amount += v_res.price_total
|
||||
|
||||
offset += len(reservations)
|
||||
|
||||
#~ if total_reservation_amount == 0 and total_service_amount == 0:
|
||||
#~ raise UserError(_('No Hay reservas de Call Center'))
|
||||
line = offset
|
||||
if k_line:
|
||||
line = k_line + offset
|
||||
if total_reservation_amount > 0:
|
||||
line += 1
|
||||
worksheet.write(line, 11, _('TOTAL RESERVAS'))
|
||||
worksheet.write(line, 12, total_reservation_amount,
|
||||
xls_cell_format_money)
|
||||
|
||||
workbook.close()
|
||||
file_data.seek(0)
|
||||
tnow = fields.Datetime.now().replace(' ', '_')
|
||||
return {
|
||||
'xls_filename': 'call_%s.xlsx' %self.env.user.company_id.property_name,
|
||||
'xls_binary': base64.encodestring(file_data.read()),
|
||||
}
|
||||
|
||||
@api.multi
|
||||
def export(self):
|
||||
self.write(self._export())
|
||||
return {
|
||||
"type": "ir.actions.do_nothing",
|
||||
}
|
||||
42
call_center_report/wizard/call_center_report.xml
Normal file
42
call_center_report/wizard/call_center_report.xml
Normal file
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" ?>
|
||||
<odoo>
|
||||
|
||||
<record model="ir.ui.view" id="view_call_center_report_wizard">
|
||||
<field name="name">call.center.report.wizard</field>
|
||||
<field name="model">call.center.report.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Call Center Report" >
|
||||
<group>
|
||||
<group>
|
||||
<field name="date_start" required="1" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="date_end" required="1" />
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<group>
|
||||
<field name="xls_filename" invisible="1"/>
|
||||
<field name="xls_binary" filename="xls_filename" readonly="1" />
|
||||
</group>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="export" string="Generate XLS" type="object" class="oe_highlight" />
|
||||
or
|
||||
<button string="Close" class="oe_link" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_open_call_center_report_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Call Center Report Wizard</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">call.center.report.wizard</field>
|
||||
<field name="view_id" ref="view_call_center_report_wizard"/>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -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',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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")
|
||||
@@ -276,16 +276,17 @@ class HotelFolio(models.Model):
|
||||
'amount_total': amount_untaxed + amount_tax,
|
||||
})
|
||||
|
||||
@api.depends('amount_total', 'payment_ids', 'return_ids', 'reservation_type')
|
||||
@api.depends('amount_total', 'payment_ids', 'return_ids',
|
||||
'reservation_type', 'state')
|
||||
@api.multi
|
||||
def compute_amount(self):
|
||||
acc_pay_obj = self.env['account.payment']
|
||||
for record in self:
|
||||
if record.reservation_type in ('staff', 'out'):
|
||||
vals = {
|
||||
'pending_amount': 0,
|
||||
'invoices_paid': 0,
|
||||
'refund_amount': 0,
|
||||
'pending_amount': 0,
|
||||
'invoices_paid': 0,
|
||||
'refund_amount': 0,
|
||||
}
|
||||
record.update(vals)
|
||||
else:
|
||||
@@ -294,10 +295,17 @@ class HotelFolio(models.Model):
|
||||
('folio_id', '=', record.id)
|
||||
])
|
||||
total_paid = sum(pay.amount for pay in payments)
|
||||
return_lines = self.env['payment.return.line'].search([('move_line_ids','in',payments.mapped('move_line_ids.id')),('return_id.state','=', 'done')])
|
||||
return_lines = self.env['payment.return.line'].search([
|
||||
('move_line_ids', 'in', payments.mapped('move_line_ids.id')),
|
||||
('return_id.state', '=', 'done')
|
||||
])
|
||||
total_inv_refund = sum(pay_return.amount for pay_return in return_lines)
|
||||
total = record.amount_total
|
||||
# REVIEW: Must We ignored services in cancelled folios pending amount?
|
||||
if record.state == 'cancelled':
|
||||
total = total - sum(record.service_ids.mapped('price_total'))
|
||||
vals = {
|
||||
'pending_amount': record.amount_total - total_paid + total_inv_refund,
|
||||
'pending_amount': total - total_paid + total_inv_refund,
|
||||
'invoices_paid': total_paid,
|
||||
'refund_amount': total_inv_refund,
|
||||
}
|
||||
@@ -408,7 +416,6 @@ class HotelFolio(models.Model):
|
||||
else:
|
||||
vals['name'] = self.env['ir.sequence'].next_by_code('hotel.folio') or _('New')
|
||||
|
||||
|
||||
# Makes sure partner_invoice_id' and 'pricelist_id' are defined
|
||||
lfields = ('partner_invoice_id', 'partner_shipping_id', 'pricelist_id')
|
||||
if any(f not in vals for f in lfields):
|
||||
@@ -471,7 +478,7 @@ class HotelFolio(models.Model):
|
||||
|
||||
@api.onchange('partner_diff_invoicing')
|
||||
def onchange_partner_diff_invoicing(self):
|
||||
if self.partner_diff_invoicing == False:
|
||||
if self.partner_diff_invoicing is False:
|
||||
self.update({'partner_invoice_id': self.partner_id.id})
|
||||
elif self.partner_id == self.partner_invoice_id:
|
||||
self.update({'partner_invoice_id': self.partner_id.address_get(['invoice'])['invoice'] or None})
|
||||
@@ -515,17 +522,14 @@ class HotelFolio(models.Model):
|
||||
|
||||
@api.multi
|
||||
def action_cancel(self):
|
||||
'''
|
||||
@param self: object pointer
|
||||
'''
|
||||
pass
|
||||
# for sale in self:
|
||||
# if not sale.order_id:
|
||||
# raise ValidationError(_('Order id is not available'))
|
||||
# for invoice in sale.invoice_ids:
|
||||
# invoice.state = 'cancel'
|
||||
# sale.room_lines.action_cancel()
|
||||
# sale.order_id.action_cancel()
|
||||
for folio in self:
|
||||
for reservation in folio.room_lines.filtered(lambda res:
|
||||
res.state != 'cancelled'):
|
||||
reservation.action_cancel()
|
||||
self.write({
|
||||
'state': 'cancel',
|
||||
})
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
def print_quotation(self):
|
||||
@@ -544,13 +548,14 @@ class HotelFolio(models.Model):
|
||||
'state': 'confirm',
|
||||
'confirmation_date': fields.Datetime.now()
|
||||
})
|
||||
#~ if self.env.context.get('send_email'):
|
||||
#~ self.force_quotation_send()
|
||||
|
||||
# if self.env.context.get('send_email'):
|
||||
# self.force_quotation_send()
|
||||
|
||||
# create an analytic account if at least an expense product
|
||||
#~ if any([expense_policy != 'no' for expense_policy in self.order_line.mapped('product_id.expense_policy')]):
|
||||
#~ if not self.analytic_account_id:
|
||||
#~ self._create_analytic_account()
|
||||
# if any([expense_policy != 'no' for expense_policy in self.order_line.mapped('product_id.expense_policy')]):
|
||||
# if not self.analytic_account_id:
|
||||
# self._create_analytic_account()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -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')
|
||||
@@ -488,7 +491,7 @@ class HotelReservation(models.Model):
|
||||
def _prepare_add_missing_fields(self, values):
|
||||
""" Deduce missing required fields from the onchange """
|
||||
res = {}
|
||||
onchange_fields = ['room_id', 'reservation_type',
|
||||
onchange_fields = ['room_id', 'reservation_type', 'tax_ids',
|
||||
'currency_id', 'name', 'service_ids']
|
||||
if values.get('room_type_id'):
|
||||
line = self.new(values)
|
||||
@@ -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)
|
||||
|
||||
@@ -117,6 +117,7 @@ class HotelService(models.Model):
|
||||
folio_id = fields.Many2one('hotel.folio', 'Folio',
|
||||
ondelete='cascade',
|
||||
default=_default_folio_id)
|
||||
state = fields.Selection(related='folio_id.state')
|
||||
ser_room_line = fields.Many2one('hotel.reservation', 'Room',
|
||||
default=_default_ser_room_line)
|
||||
per_day = fields.Boolean(related='product_id.per_day', related_sudo=True)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class HotelServiceLine(models.Model):
|
||||
_name = "hotel.service.line"
|
||||
_order = "date"
|
||||
@@ -11,6 +12,7 @@ class HotelServiceLine(models.Model):
|
||||
service_id = fields.Many2one('hotel.service', string='Service Room',
|
||||
ondelete='cascade', required=True,
|
||||
copy=False)
|
||||
active = fields.Boolean('Active', compute="_compute_active")
|
||||
date = fields.Date('Date')
|
||||
day_qty = fields.Integer('Units')
|
||||
product_id = fields.Many2one(related='service_id.product_id', store=True)
|
||||
|
||||
@@ -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>
|
||||
@@ -21,20 +21,18 @@
|
||||
<!-- <button name="send_exit_mail" type="object" string="Send Exit Email"
|
||||
attrs="{'invisible': [('has_checkout_to_send', '=', False)]}" class="oe_highlight"/> -->
|
||||
<button name="%(hotel.action_view_folio_advance_payment_inv)d"
|
||||
string="Create Invoice" type="action" class="btn-primary" states="sale"
|
||||
string="Create Invoice" type="action" class="btn-primary"
|
||||
attrs="{'invisible': [('state', '!=', 'confirm')]}"/>
|
||||
<!-- <button name="action_cancel_draft" states="cancel,sale" string="Set to Draft"
|
||||
type="object" icon="fa-undo" class="oe_highlight" /> -->
|
||||
<button name="action_cancel" string="Cancel Folio" states="sale"
|
||||
type="object" icon="fa-minus-square" class="oe_highlight" />
|
||||
<button name="action_cancel" string="Cancel Folio" states="draft"
|
||||
icon="fa-minus-square" type="object" class="oe_highlight" />
|
||||
<button name="action_cancel" string="Cancel Folio"
|
||||
attrs="{'invisible': [('state', 'not in', ('confirm','draft'))]}"
|
||||
type="object" />
|
||||
<button name="action_done" type="object" string="Set to Done"
|
||||
help="If a Hotel Folio is done, you cannot modify it manually anymore. However, you will still be able to invoice or deliver. This is used to freeze the Hotel Folio." />
|
||||
<!-- states="sale" attrs="{'invisible': [('invoice_status', '!=', 'invoiced')]}" -->
|
||||
help="If a Hotel Folio is done, you cannot modify it manually anymore. However, you will still be able to invoice. This is used to freeze the Hotel Folio." />
|
||||
<!-- <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>
|
||||
@@ -66,7 +64,6 @@
|
||||
</button>
|
||||
|
||||
</div>
|
||||
<!-- <field name="image" widget="image" class="oe_avatar" options="{"preview_image": "image_medium", "size": [90, 90]}"/> -->
|
||||
<h2><field name="name"/></h2>
|
||||
<h1>
|
||||
<field name="partner_id" default_focus="1" placeholder="Guest" attrs="{'invisible':[('reservation_type','in',('out'))]}"/>
|
||||
|
||||
@@ -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>
|
||||
@@ -1,8 +1,8 @@
|
||||
# Copyright 2017 Alexandre Díaz
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
from datetime import timedelta
|
||||
from openerp import models, fields, api
|
||||
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
|
||||
from odoo import models, fields, api
|
||||
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
|
||||
|
||||
|
||||
class MassiveChangesWizard(models.TransientModel):
|
||||
@@ -26,8 +26,7 @@ class MassiveChangesWizard(models.TransientModel):
|
||||
('0', 'Global'),
|
||||
('1', 'Room Type'),
|
||||
], string='Applied On', default='0')
|
||||
# room_type_ids = fields.Many2many('hotel.virtual.room',
|
||||
# string="Virtual Rooms")
|
||||
|
||||
room_type_ids = fields.Many2many('hotel.room.type', string="Room Types")
|
||||
|
||||
# Restriction fields
|
||||
@@ -157,11 +156,10 @@ class MassiveChangesWizard(models.TransientModel):
|
||||
def _save_restrictions(self, ndate, room_types, record):
|
||||
hotel_room_type_re_it_obj = self.env['hotel.room.type.restriction.item']
|
||||
domain = [
|
||||
('date_start', '>=', ndate.strftime(DEFAULT_SERVER_DATE_FORMAT)),
|
||||
('date_end', '<=', ndate.strftime(DEFAULT_SERVER_DATE_FORMAT)),
|
||||
('date', '>=', ndate.strftime(DEFAULT_SERVER_DATE_FORMAT)),
|
||||
('date', '<=', ndate.strftime(DEFAULT_SERVER_DATE_FORMAT)),
|
||||
('restriction_id', '=', record.restriction_id.id),
|
||||
]
|
||||
|
||||
for room_type in room_types:
|
||||
vals = self._get_restrictions_values(record)
|
||||
if not any(vals):
|
||||
@@ -173,8 +171,7 @@ class MassiveChangesWizard(models.TransientModel):
|
||||
rrest_item_ids.write(vals)
|
||||
else:
|
||||
vals.update({
|
||||
'date_start': ndate.strftime(DEFAULT_SERVER_DATE_FORMAT),
|
||||
'date_end': ndate.strftime(DEFAULT_SERVER_DATE_FORMAT),
|
||||
'date': ndate.strftime(DEFAULT_SERVER_DATE_FORMAT),
|
||||
'restriction_id': record.restriction_id.id,
|
||||
'room_type_id': room_type.id,
|
||||
})
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
<group colspan="8" col="8">
|
||||
<field name="date_start" required="1" colspan="4"/>
|
||||
<field name="date_end" required="1" colspan="4" />
|
||||
<field name="dmo" colspan="1" />
|
||||
<field name="dtu" colspan="1" />
|
||||
<field name="dwe" colspan="1" />
|
||||
<field name="dth" colspan="1" />
|
||||
<field name="dfr" colspan="1" />
|
||||
<field name="dsa" colspan="1" />
|
||||
<field name="dsu" colspan="1" />
|
||||
<field name="dmo" />
|
||||
<field name="dtu" />
|
||||
<field name="dwe" />
|
||||
<field name="dth" />
|
||||
<field name="dfr" />
|
||||
<field name="dsa" />
|
||||
<field name="dsu" />
|
||||
</group>
|
||||
<!-- Restricion Fields -->
|
||||
<group col="4" colspan="4" attrs="{'invisible':[('section', '!=', 'restrictions')]}">
|
||||
|
||||
@@ -18,18 +18,22 @@ class HotelRoomTypeAvailability(models.Model):
|
||||
|
||||
@api.model
|
||||
def _default_max_avail(self):
|
||||
room_type_id = self._context.get('room_type_id')
|
||||
if room_type_id:
|
||||
room_type_id = self.env['hotel.room_type'].browse(room_type_id)
|
||||
return room_type_id.default_max_avail if room_type_id else -1
|
||||
room_type_id = self.room_type_id.id or self._context.get('room_type_id')
|
||||
channel_room_type = self.env['channel.hotel.room.type'].search([
|
||||
('odoo_id', '=', room_type_id)
|
||||
]) or None
|
||||
if channel_room_type:
|
||||
return channel_room_type.default_max_avail
|
||||
return -1
|
||||
|
||||
@api.model
|
||||
def _default_quota(self):
|
||||
room_type_id = self._context.get('room_type_id')
|
||||
if room_type_id:
|
||||
room_type_id = self.env['hotel.room_type'].browse(room_type_id)
|
||||
return room_type_id.default_quota if room_type_id else -1
|
||||
room_type_id = self.room_type_id.id or self._context.get('room_type_id')
|
||||
channel_room_type = self.env['channel.hotel.room.type'].search([
|
||||
('odoo_id', '=', room_type_id)
|
||||
]) or None
|
||||
if channel_room_type:
|
||||
return channel_room_type.default_quota
|
||||
return -1
|
||||
|
||||
room_type_id = fields.Many2one('hotel.room.type', 'Room Type',
|
||||
@@ -44,6 +48,7 @@ class HotelRoomTypeAvailability(models.Model):
|
||||
|
||||
quota = fields.Integer("Quota", default=_default_quota,
|
||||
help="Quota assigned to the channel.")
|
||||
# TODO: WHY max_avail IS READONLY ¿?
|
||||
max_avail = fields.Integer("Max. Availability", default=-1, readonly=True,
|
||||
help="Maximum simultaneous availability.")
|
||||
|
||||
@@ -72,8 +77,34 @@ class HotelRoomTypeAvailability(models.Model):
|
||||
|
||||
@api.onchange('room_type_id')
|
||||
def onchange_room_type_id(self):
|
||||
if self.room_type_id:
|
||||
self.quota = self.room_type_id.default_quota
|
||||
channel_room_type = self.env['channel.hotel.room.type'].search([
|
||||
('odoo_id', '=', self.room_type_id.id)
|
||||
]) or None
|
||||
if channel_room_type:
|
||||
self.quota = channel_room_type.default_quota
|
||||
self.max_avail = channel_room_type.default_max_avail
|
||||
self.no_ota = 0
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
vals.update(self._prepare_add_missing_fields(vals))
|
||||
return super().create(vals)
|
||||
|
||||
@api.model
|
||||
def _prepare_add_missing_fields(self, values):
|
||||
""" Deduce missing required fields from the onchange """
|
||||
res = {}
|
||||
onchange_fields = ['quota', 'max_avail']
|
||||
if values.get('room_type_id'):
|
||||
record = self.new(values)
|
||||
if 'quota' not in values:
|
||||
record.quota = record._default_quota()
|
||||
if 'max_avail' not in values:
|
||||
record.max_avail = record._default_max_avail()
|
||||
for field in onchange_fields:
|
||||
if field not in values:
|
||||
res[field] = record._fields[field].convert_to_write(record[field], record)
|
||||
return res
|
||||
|
||||
|
||||
class ChannelHotelRoomTypeAvailability(models.Model):
|
||||
@@ -152,6 +183,8 @@ class ChannelHotelRoomTypeAvailability(models.Model):
|
||||
_logger.info(vals_avail)
|
||||
if room_type_avail_id.channel_avail != avail:
|
||||
vals_avail.update({'channel_avail': avail})
|
||||
if self._context.get('update_no_ota', False):
|
||||
vals_avail.update({'channel_pushed': False})
|
||||
if vals_avail:
|
||||
room_type_avail_id.write(vals_avail)
|
||||
|
||||
@@ -209,11 +242,13 @@ class BindingHotelRoomTypeAvailabilityListener(Component):
|
||||
fields_to_check = ('quota', 'max_avail', 'no_ota')
|
||||
fields_checked = [elm for elm in fields_to_check if elm in fields]
|
||||
|
||||
_logger.info("==[on_record_write] :: hotel.room.type.availability==")
|
||||
_logger.info(fields)
|
||||
|
||||
if any(fields_checked) and any(record.channel_bind_ids):
|
||||
|
||||
_logger.info("==[on_record_write] :: hotel.room.type.availability==")
|
||||
_logger.info(fields)
|
||||
|
||||
if 'no_ota' in fields_checked:
|
||||
self.env.context = dict(self.env.context)
|
||||
self.env.context.update({'update_no_ota': True})
|
||||
for binding in record.channel_bind_ids:
|
||||
binding.refresh_availability(
|
||||
record.date,
|
||||
@@ -261,7 +296,7 @@ class ChannelBindingHotelRoomTypeAvailabilityListener(Component):
|
||||
|
||||
@skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
|
||||
def on_record_write(self, record, fields=None):
|
||||
fields_to_check = ('date', 'channel_avail') # no_ota ¿?
|
||||
fields_to_check = ('date', 'channel_avail')
|
||||
fields_checked = [elm for elm in fields_to_check if elm in fields]
|
||||
|
||||
_logger.info("==[on_record_write] :: channel.hotel.room.type.availability==")
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -13,9 +13,9 @@ class MassiveChangesWizard(models.TransientModel):
|
||||
|
||||
# Availability fields
|
||||
change_quota = fields.Boolean(default=False)
|
||||
quota = fields.Integer('Quota', default=-1)
|
||||
quota = fields.Integer('Quota', default=0)
|
||||
change_max_avail = fields.Boolean(default=False)
|
||||
max_avail = fields.Integer('Max. Avail.', default=-1)
|
||||
max_avail = fields.Integer('Max. Avail.', default=0)
|
||||
change_no_ota = fields.Boolean(default=False)
|
||||
no_ota = fields.Boolean('No OTA', default=False)
|
||||
|
||||
|
||||
@@ -7,27 +7,21 @@
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//form/group[last()]" position="after">
|
||||
<!-- Availability Fields -->
|
||||
<group col="3" colspan="3" attrs="{'invisible':[('section', '!=', 'avail')]}">
|
||||
<table class="oe_form_group">
|
||||
<thead>
|
||||
<th width="12%"></th>
|
||||
<th></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="oe_form_group_row">
|
||||
<td class="oe_form_group_cell oe_form_group_cell_label"><field name="change_max_avail" /> <strong> Max. Avail.</strong></td>
|
||||
<td class="oe_form_group_cell" colspan="3"><field name="max_avail" attrs="{'readonly':[('change_max_avail', '=', False)]}" /></td>
|
||||
</tr>
|
||||
<tr class="oe_form_group_row">
|
||||
<td class="oe_form_group_cell oe_form_group_cell_label"><field name="change_quota" /> <strong> Quota</strong></td>
|
||||
<td class="oe_form_group_cell" colspan="3"><field name="quota" attrs="{'readonly':[('change_quota', '=', False)]}" /></td>
|
||||
</tr>
|
||||
<tr class="oe_form_group_row">
|
||||
<td class="oe_form_group_cell oe_form_group_cell_label"><field name="change_no_ota" /> <strong> No OTA</strong></td>
|
||||
<td class="oe_form_group_cell"><field name="no_ota" attrs="{'readonly':[('change_no_ota', '=', False)]}" /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<group col="8" colspan="8" attrs="{'invisible':[('section', '!=', 'avail')]}">
|
||||
<field name="change_max_avail"/>
|
||||
<field name="max_avail"
|
||||
attrs="{'readonly':[('change_max_avail', '=', False)],
|
||||
'invisible':[('change_max_avail', '=', False)]}"/>
|
||||
|
||||
<field name="change_quota"/>
|
||||
<field name="quota"
|
||||
attrs="{'readonly':[('change_quota', '=', False)],
|
||||
'invisible':[('change_quota', '=', False)]}"/>
|
||||
|
||||
<field name="change_no_ota"/>
|
||||
<field name="no_ota"
|
||||
attrs="{'readonly':[('change_no_ota', '=', False)],
|
||||
'invisible':[('change_no_ota', '=', False)]}"/>
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
|
||||
13
hotel_door_codes/README.rst
Normal file
13
hotel_door_codes/README.rst
Normal file
@@ -0,0 +1,13 @@
|
||||
DOOR CODES
|
||||
==========
|
||||
|
||||
Generate HOTEL DOOR CODES
|
||||
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Creator
|
||||
------------
|
||||
|
||||
* Jose Luis Algara Toledo <osotranquilo@gmail.com>
|
||||
22
hotel_door_codes/__init__.py
Normal file
22
hotel_door_codes/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2018-2019 Jose Luis Algara Toledo <osotranquilo@gmail.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
from . import models
|
||||
from . import wizard
|
||||
47
hotel_door_codes/__manifest__.py
Normal file
47
hotel_door_codes/__manifest__.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# Odoo, Open Source Management Solution
|
||||
# Copyright (C) 2018-2019 Jose Luis Algara Toledo <osotranquilo@gmail.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
{
|
||||
'name': 'Hotel Door Codes',
|
||||
'version': '2.1',
|
||||
'author': "Jose Luis Algara Toledo <osotranquilo@gmail.com>",
|
||||
'website': 'https://www.aldahotels.com',
|
||||
'category': 'hotel code',
|
||||
'summary': "Generate Hotel door codes, in Pseudo random system",
|
||||
'description': "Hotel Door Codes",
|
||||
'depends': [
|
||||
'hotel', 'hotel_l10n_es'
|
||||
],
|
||||
'data': [
|
||||
'wizard/door_code.xml',
|
||||
'data/menus.xml',
|
||||
'views/inherit_res_company.xml',
|
||||
'views/inherit_hotel_reservation.xml',
|
||||
'views/inherit_report_viajero.xml',
|
||||
],
|
||||
'qweb': [],
|
||||
'test': [
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
'application': False,
|
||||
'license': 'AGPL-3',
|
||||
}
|
||||
13
hotel_door_codes/data/menus.xml
Normal file
13
hotel_door_codes/data/menus.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<menuitem
|
||||
id="menu_hotel_door_code"
|
||||
name="Codigos para la Puerta"
|
||||
parent="hotel.hotel_reports_menu"
|
||||
action="door_code_act"
|
||||
groups="hotel.group_hotel_user,hotel.group_hotel_manager"
|
||||
sequence="45"
|
||||
/>
|
||||
</data>
|
||||
</odoo>
|
||||
23
hotel_door_codes/models/__init__.py
Normal file
23
hotel_door_codes/models/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2018-2019 Alda Hotels <informatica@aldahotels.com>
|
||||
# Jose Luis Algara <osotranquilo@gmail.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
from . import inherit_res_company
|
||||
from . import inherit_hotel_reservation
|
||||
71
hotel_door_codes/models/inherit_hotel_reservation.py
Normal file
71
hotel_door_codes/models/inherit_hotel_reservation.py
Normal file
@@ -0,0 +1,71 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2018-2019 Alda Hotels <informatica@aldahotels.com>
|
||||
# Jose Luis Algara <osotranquilo@gmail.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
from openerp import models, fields, api
|
||||
from datetime import datetime, date, time, timedelta
|
||||
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
|
||||
|
||||
|
||||
class Inherit_hotel_reservation(models.Model):
|
||||
_inherit = 'hotel.reservation'
|
||||
|
||||
@api.multi
|
||||
def doorcode4(self, fecha):
|
||||
# Calculate de Door Code... need a date in String format "%Y-%m-%d"
|
||||
compan = self.env.user.company_id
|
||||
d = datetime.strptime(fecha, DEFAULT_SERVER_DATE_FORMAT)
|
||||
dia_semana = datetime.weekday(d) # Dias a restar y ponerlo en lunes
|
||||
d = d - timedelta(days=dia_semana)
|
||||
dtxt = d.strftime('%s.%%06d') % d.microsecond
|
||||
dtxt = compan.precode + dtxt[4:8] + compan.postcode
|
||||
return dtxt
|
||||
|
||||
@api.multi
|
||||
def _compute_door_codes(self):
|
||||
for res in self:
|
||||
entrada = datetime.strptime(
|
||||
res.checkin[:10], DEFAULT_SERVER_DATE_FORMAT)
|
||||
if datetime.weekday(entrada) == 0:
|
||||
entrada = entrada + timedelta(days=1)
|
||||
salida = datetime.strptime(
|
||||
res.checkout[:10], DEFAULT_SERVER_DATE_FORMAT)
|
||||
if datetime.weekday(salida) == 0:
|
||||
salida = salida - timedelta(days=1)
|
||||
codes = (u'Código de entrada: ' +
|
||||
'<strong><span style="font-size: 1.4em;">' +
|
||||
res.doorcode4(datetime.strftime(entrada, "%Y-%m-%d")) +
|
||||
'</span></strong>')
|
||||
while entrada <= salida:
|
||||
if datetime.weekday(entrada) == 0:
|
||||
codes += ("<br>" +
|
||||
u'Cambiará el Lunes ' +
|
||||
datetime.strftime(entrada, "%d-%m-%Y") +
|
||||
' a: <strong><span style="font-size: 1.4em;">' +
|
||||
res.doorcode4(datetime.strftime(
|
||||
entrada, "%Y-%m-%d")) +
|
||||
'</span></strong>')
|
||||
entrada = entrada + timedelta(days=1)
|
||||
res.door_codes = codes
|
||||
|
||||
door_codes = fields.Html(u'Códigos de entrada',
|
||||
compute='_compute_door_codes')
|
||||
box_number = fields.Integer ('Numero de Caja')
|
||||
box_code = fields.Char ('Cod. Caja')
|
||||
29
hotel_door_codes/models/inherit_res_company.py
Normal file
29
hotel_door_codes/models/inherit_res_company.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2018-2019 Alda Hotels <informatica@aldahotels.com>
|
||||
# Jose Luis Algara <osotranquilo@gmail.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
from openerp import models, fields, api
|
||||
|
||||
|
||||
class Inherit_res_company(models.Model):
|
||||
_inherit = 'res.company'
|
||||
|
||||
precode = fields.Char('Characters before the door code', default='')
|
||||
postcode = fields.Char('Characters after the code', default='')
|
||||
BIN
hotel_door_codes/static/description/icon.png
Normal file
BIN
hotel_door_codes/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
23
hotel_door_codes/views/inherit_hotel_reservation.xml
Normal file
23
hotel_door_codes/views/inherit_hotel_reservation.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Hotel Settings -->
|
||||
<data>
|
||||
<!-- Inherit Company view to add 'door_code' in Reservation Form -->
|
||||
<record id="door_code_reservation_form" model="ir.ui.view">
|
||||
<field name="name">door_code.reservation_form</field>
|
||||
<field name="model">hotel.reservation</field>
|
||||
<field name="inherit_id" ref="hotel.hotel_reservation_view_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//page[@name='others']" position="after">
|
||||
<page name="doorcode" string="Códigos entrada">
|
||||
<group>
|
||||
<field name="door_codes" />
|
||||
<field name="box_number" />
|
||||
<field name="box_code" />
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
14
hotel_door_codes/views/inherit_report_viajero.xml
Normal file
14
hotel_door_codes/views/inherit_report_viajero.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Hotel Settings -->
|
||||
<data>
|
||||
<!-- Inherit viajero repdort to add 'door_code' -->
|
||||
<template id="door_code_view_report" inherit_id="hotel_l10n_es.report_viajero_document">
|
||||
<xpath expr="//div[@class='final']" position="replace">
|
||||
<div class ="doorcodes" style="font-size:1.8em;">
|
||||
<span t-field="o.reservation_id.door_codes"/>
|
||||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
||||
18
hotel_door_codes/views/inherit_res_company.xml
Normal file
18
hotel_door_codes/views/inherit_res_company.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Hotel Settings -->
|
||||
<data>
|
||||
<!-- Inherit Company view to add 'door_code' -->
|
||||
<record id="door_code_view_company_form" model="ir.ui.view">
|
||||
<field name="name">door_code.config.view_company_form</field>
|
||||
<field name="model">res.company</field>
|
||||
<field name="inherit_id" ref="base.view_company_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='cardex_warning']" position="after">
|
||||
<field name="precode" />
|
||||
<field name="postcode" />
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
21
hotel_door_codes/wizard/__init__.py
Normal file
21
hotel_door_codes/wizard/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2018-2019 Jose Luis Algara Toledo <osotranquilo@gmail.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
from . import door_code
|
||||
78
hotel_door_codes/wizard/door_code.py
Normal file
78
hotel_door_codes/wizard/door_code.py
Normal file
@@ -0,0 +1,78 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# Odoo, Open Source Management Solution
|
||||
# Copyright (C) 2018-2019 Jose Luis Algara Toledo <osotranquilo@gmail.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
import datetime
|
||||
from datetime import datetime, date, time, timedelta
|
||||
from odoo import api, fields, models, _
|
||||
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
|
||||
|
||||
|
||||
class DoorCodeWizard(models.TransientModel):
|
||||
_name = 'door_code'
|
||||
|
||||
@api.model
|
||||
def _get_default_date_start(self):
|
||||
return datetime.now().strftime(DEFAULT_SERVER_DATE_FORMAT)
|
||||
|
||||
date_start = fields.Date("Inicio periodo",
|
||||
default=_get_default_date_start)
|
||||
date_end = fields.Date("Fin del periodo",
|
||||
default=_get_default_date_start)
|
||||
door_code = fields.Html(u'Código para la puerta')
|
||||
|
||||
@api.multi
|
||||
def doorcode4(self, fecha):
|
||||
# Calculate de Door Code... need a date in String format "%Y-%m-%d"
|
||||
compan = self.env.user.company_id
|
||||
d = datetime.strptime(fecha, DEFAULT_SERVER_DATE_FORMAT)
|
||||
dia_semana = datetime.weekday(d) # Dias a restar y ponerlo en lunes
|
||||
d = d - timedelta(days=dia_semana)
|
||||
dtxt = d.strftime('%s.%%06d') % d.microsecond
|
||||
dtxt = compan.precode + dtxt[4:8] + compan.postcode
|
||||
return dtxt
|
||||
|
||||
@api.multi
|
||||
def check_code(self):
|
||||
entrada = datetime.strptime(
|
||||
self.date_start, DEFAULT_SERVER_DATE_FORMAT)
|
||||
if datetime.weekday(entrada) == 0:
|
||||
entrada = entrada + timedelta(days=1)
|
||||
salida = datetime.strptime(
|
||||
self.date_end, DEFAULT_SERVER_DATE_FORMAT)
|
||||
if datetime.weekday(salida) == 0:
|
||||
salida = salida - timedelta(days=1)
|
||||
codes = (u'Código de entrada: ' +
|
||||
'<strong><span style="font-size: 2em;">' +
|
||||
self.doorcode4(self.date_start) +
|
||||
'</span></strong>')
|
||||
while entrada <= salida:
|
||||
if datetime.weekday(entrada) == 0:
|
||||
codes += ("<br>" +
|
||||
u'Cambiará el Lunes ' +
|
||||
datetime.strftime(entrada, "%d-%m-%Y") +
|
||||
' a: <strong><span style="font-size: 2em;">' +
|
||||
self.doorcode4(datetime.strftime(
|
||||
entrada, "%Y-%m-%d")) +
|
||||
'</span></strong>')
|
||||
entrada = entrada + timedelta(days=1)
|
||||
|
||||
return self.write({
|
||||
'door_code': codes
|
||||
})
|
||||
33
hotel_door_codes/wizard/door_code.xml
Normal file
33
hotel_door_codes/wizard/door_code.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="hotel_door_code_view">
|
||||
<field name="name">door_code.view</field>
|
||||
<field name="model">door_code</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Door Code Generator" >
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="date_start" required="1" />
|
||||
<field name="date_end" required="1" />
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="door_code" readonly="1"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="check_code" string="Generate Code" type="object" class="oe_highlight" />
|
||||
</footer>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<act_window
|
||||
id="door_code_act"
|
||||
name="Door Code Generator"
|
||||
res_model="door_code"
|
||||
view_mode="form"
|
||||
/>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -56,6 +56,8 @@
|
||||
Asimismo hemos solicitado que confirme esta autorización para ofrecerle nuestros servicios y poder fidelizarle como cliente.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="final">
|
||||
</div>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -37,6 +37,7 @@ class PoliceWizard(models.TransientModel):
|
||||
txt_binary = fields.Binary()
|
||||
txt_message = fields.Char()
|
||||
log_police = fields.Char()
|
||||
error_partner = fields.Many2one('res.partner')
|
||||
|
||||
@api.one
|
||||
def generate_file(self):
|
||||
@@ -56,6 +57,7 @@ class PoliceWizard(models.TransientModel):
|
||||
if ((line.partner_id.document_type is not False)
|
||||
and (line.partner_id.document_number is not False)
|
||||
and (line.partner_id.firstname is not False)
|
||||
and (line.partner_id.gender is not False)
|
||||
and (line.partner_id.lastname is not False)):
|
||||
|
||||
log_police += 1
|
||||
@@ -101,11 +103,13 @@ class PoliceWizard(models.TransientModel):
|
||||
content += """
|
||||
"""
|
||||
else:
|
||||
self.error_partner = line.partner_id
|
||||
|
||||
return self.write({
|
||||
'error_partner': line.partner_id.id,
|
||||
'txt_message': _('Problem generating the file. \
|
||||
Checkin without data, \
|
||||
or incorrect data: - ' +
|
||||
line.partner_id.name)})
|
||||
or incorrect data: ')})
|
||||
log_police = str(log_police) + _(' records added from ')
|
||||
log_police += str(len(lines)) + _(' records processed.')
|
||||
return self.write({
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<group colspan="1">
|
||||
<div>
|
||||
<p><field name="log_police" readonly="1"/></p>
|
||||
<p><field name="txt_message" readonly="1"/></p>
|
||||
<p><field name="txt_message" readonly="1"/><strong><field name="error_partner" readonly="1"/></strong></p>
|
||||
</div>
|
||||
</group>
|
||||
<group>
|
||||
|
||||
Reference in New Issue
Block a user