From 40d039833f8cdef90b6bb0d9a689cf4e5fd1d40a Mon Sep 17 00:00:00 2001 From: Dario Lodeiros Date: Fri, 5 Apr 2019 11:02:16 +0200 Subject: [PATCH] [ADD] WorkFlow Partner checkin and duplicated --- .../hotel_board_service_room_type_line.py | 3 - hotel/models/hotel_checkin_partner.py | 59 +- hotel/models/hotel_reservation.py | 23 +- hotel/models/inherited_res_partner.py | 286 +--------- hotel/views/hotel_checkin_partner_views.xml | 24 +- hotel/views/hotel_reservation_views.xml | 5 +- hotel/views/inherited_res_partner_views.xml | 8 +- hotel_l10n_es/__manifest__.py | 3 +- hotel_l10n_es/models/__init__.py | 1 + .../models/inherit_hotel_checkin_partner.py | 166 ++++-- .../models/inherit_hotel_reservation.py | 35 ++ hotel_l10n_es/models/inherit_res_partner.py | 515 ++++++++++++++---- .../inherit_hotel_checkin_partner_views.xml | 210 ++++--- hotel_l10n_es/views/inherit_res_partner.xml | 14 + .../inherited_hotel_reservation_views.xml | 17 + 15 files changed, 807 insertions(+), 562 deletions(-) create mode 100644 hotel_l10n_es/models/inherit_hotel_reservation.py create mode 100644 hotel_l10n_es/views/inherited_hotel_reservation_views.xml diff --git a/hotel/models/hotel_board_service_room_type_line.py b/hotel/models/hotel_board_service_room_type_line.py index 8896c0644..8fcbb6afd 100644 --- a/hotel/models/hotel_board_service_room_type_line.py +++ b/hotel/models/hotel_board_service_room_type_line.py @@ -16,6 +16,3 @@ class HotelBoardServiceRoomTypeLine(models.Model): product_id = fields.Many2one( 'product.product', 'Product', required=True, readonly=True) amount = fields.Float('Amount', digits=dp.get_precision('Product Price'), default=0.0) - - - diff --git a/hotel/models/hotel_checkin_partner.py b/hotel/models/hotel_checkin_partner.py index 5dbb2641a..16758c308 100644 --- a/hotel/models/hotel_checkin_partner.py +++ b/hotel/models/hotel_checkin_partner.py @@ -19,6 +19,25 @@ class HotelCheckinPartner(models.Model): return reservation return False + def _default_partner_id(self): + if 'reservation_id' in self.env.context: + reservation = self.env['hotel.reservation'].browse([ + self.env.context['reservation_id'] + ]) + partner_ids = [] + if reservation.folio_id: + for room in reservation.folio_id.room_lines: + partner_ids.append(room.mapped( + 'checkin_partner_ids.partner_id.id')) + if 'checkin_partner_ids' in self.env.context: + for checkin in self.env.context['checkin_partner_ids']: + if checkin[0] == 0: + partner_ids.append(checkin[2].get('partner_id')) + if self._context.get('include_customer') and reservation.partner_id.id \ + not in partner_ids and not reservation.partner_id.is_company: + return reservation.partner_id + return False + def _default_folio_id(self): if 'folio_id' in self.env.context: folio = self.env['hotel.folio'].browse([ @@ -48,16 +67,6 @@ class HotelCheckinPartner(models.Model): return reservation.checkout return False - def _default_partner_id(self): - if 'reservation_id' in self.env.context: - reservation = self.env['hotel.reservation'].browse([ - self.env.context['reservation_id'] - ]) - if reservation.partner_id.id not in reservation.mapped( - 'checkin_partner_ids.partner_id.id'): - return reservation.partner_id - return False - def _default_to_enter(self): tz_hotel = self.env['ir.default'].sudo().get( 'res.config.settings', 'tz_hotel') @@ -122,25 +131,13 @@ class HotelCheckinPartner(models.Model): @api.onchange('partner_id') def _check_partner_id(self): for record in self: - checkins = self.env['hotel.checkin.partner'].search([ - ('id', '!=', record.id), - ('reservation_id', '=', record.reservation_id.id) - ]) - if record.partner_id.id in checkins.mapped('partner_id.id'): - raise models.ValidationError( - _('This guest is already registered in the room')) - - @api.multi - @api.constrains('partner_id') - def _check_partner_id(self): - for record in self: - checkins = self.env['hotel.checkin.partner'].search([ - ('id', '!=', record.id), - ('reservation_id', '=', record.reservation_id.id) - ]) - if record.partner_id.id in checkins.mapped('partner_id.id'): - raise models.ValidationError( - _('This guest is already registered in the room')) + if record.partner_id: + indoor_partner_ids = record.reservation_id.checkin_partner_ids.\ + filtered(lambda r: r.id != record.id).mapped('partner_id.id') + if indoor_partner_ids.count(record.partner_id.id) > 1: + raise models.ValidationError( + _('This guest is already registered in the room')) + record.partner_id = None @api.multi def action_on_board(self): @@ -152,4 +149,6 @@ class HotelCheckinPartner(models.Model): @api.multi def action_done(self): for record in self: - record.state = 'done' + if record.state == 'booking': + record.state = 'done' + return True diff --git a/hotel/models/hotel_reservation.py b/hotel/models/hotel_reservation.py index 662d9f872..a2068d940 100644 --- a/hotel/models/hotel_reservation.py +++ b/hotel/models/hotel_reservation.py @@ -3,10 +3,8 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import time from datetime import timedelta -from lxml import etree from odoo.exceptions import UserError, ValidationError from odoo.tools import ( - misc, float_is_zero, float_compare, DEFAULT_SERVER_DATE_FORMAT, @@ -245,11 +243,17 @@ class HotelReservation(models.Model): # Non-stored field hotel.reservation.checkin_partner_count cannot be searched # searching on a computed field can also be enabled by setting the search parameter. # The value is a method name returning a Domains - checkin_partner_count = fields.Integer('Checkin counter', - compute='_compute_checkin_partner_count') - checkin_partner_pending_count = fields.Integer('Checkin Pending Num', - compute='_compute_checkin_partner_count', - search='_search_checkin_partner_pending') + checkin_partner_count = fields.Integer( + 'Checkin counter', + compute='_compute_checkin_partner_count') + checkin_partner_pending_count = fields.Integer( + 'Checkin Pending Num', + compute='_compute_checkin_partner_count', + search='_search_checkin_partner_pending') + customer_sleep_here = fields.Boolean(default=True, + string="Include customer", + help="Indicates if the customer \ + sleeps in this room") # check_rooms = fields.Boolean('Check Rooms') splitted = fields.Boolean('Splitted', default=False) parent_reservation = fields.Many2one('hotel.reservation', @@ -621,7 +625,6 @@ class HotelReservation(models.Model): """ ONCHANGES ---------------------------------------------------------- """ - @api.onchange('adults', 'room_id') def onchange_room_id(self): if self.room_id: @@ -633,7 +636,6 @@ class HotelReservation(models.Model): _('%s people do not fit in this room! ;)') % (self.adults)) if self.adults == 0: write_vals.update({'adults': self.room_id.capacity}) - #Si el registro no existe, modificar room_type aunque ya esté establecido if not self.room_type_id: write_vals.update({'room_type_id': self.room_id.room_type_id.id}) self.update(write_vals) @@ -1195,7 +1197,8 @@ class HotelReservation(models.Model): for record in self: record.state = 'done' if record.checkin_partner_ids: - record.checkin_partner_ids.action_done() + record.checkin_partner_ids.filtered( + lambda check: check.state == 'booking').action_done() return True @api.multi diff --git a/hotel/models/inherited_res_partner.py b/hotel/models/inherited_res_partner.py index eaebd2b1d..10e54e527 100644 --- a/hotel/models/inherited_res_partner.py +++ b/hotel/models/inherited_res_partner.py @@ -1,15 +1,9 @@ # Copyright 2017 Alexandre Díaz # Copyright 2017 Dario Lodeiros # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -import functools -import itertools -import logging -import psycopg2 - -from odoo import api, fields, models, _ +from odoo import api, fields, models from odoo.osv.expression import get_unaccent_wrapper -from odoo.exceptions import ValidationError, UserError -from odoo.tools import mute_logger +import logging _logger = logging.getLogger(__name__) @@ -34,37 +28,7 @@ class ResPartner(models.Model): compute='_compute_reservations_count') folios_count = fields.Integer('Folios', compute='_compute_folios_count') unconfirmed = fields.Boolean('Unconfirmed', default=True) - - def _get_fk_on(self, table): - """ return a list of many2one relation with the given table. - :param table : the name of the sql table to return relations - :returns a list of tuple 'table name', 'column name'. - """ - query = """ - SELECT cl1.relname as table, att1.attname as column - FROM pg_constraint as con, pg_class as cl1, pg_class as cl2, pg_attribute as att1, pg_attribute as att2 - WHERE con.conrelid = cl1.oid - AND con.confrelid = cl2.oid - AND array_lower(con.conkey, 1) = 1 - AND con.conkey[1] = att1.attnum - AND att1.attrelid = cl1.oid - AND cl2.relname = %s - AND att2.attname = 'id' - AND array_lower(con.confkey, 1) = 1 - AND con.confkey[1] = att2.attnum - AND att2.attrelid = cl2.oid - AND con.contype = 'f' - """ - self._cr.execute(query, (table,)) - return self._cr.fetchall() - - @api.multi - def write(self, vals): - res = super(ResPartner, self).write(vals) - for i, record in enumerate(self): - if record.unconfirmed is True: - res = self.env['res.partner']._check_duplicated_partner(record) - return res + main_partner_id = fields.Many2one('res.partner') @api.model def name_search(self, name, args=None, operator='ilike', limit=100): @@ -111,247 +75,3 @@ class ResPartner(models.Model): if partner_ids: result += self.browse(partner_ids).name_get() return result - - @api.model - def _check_duplicated_partner(self, partner): - duplicated_ids = self.env['res.partner']._get_duplicated_ids(partner) - if len(duplicated_ids) > 1: - partners = self.env['res.partner'].browse(duplicated_ids) - action = self.env.ref('crm.action_partner_deduplicate').read()[0] - if partners: - action['context'] = { - 'default_partner_ids': partners.ids, - 'default_dst_partner_id': partner.id, - } - else: - action = {'type': 'ir.actions.act_window_close'} - return action - # return partner._merge(partners._ids) - return partner - - def _merge_fields(self): - duplicated_fields = ['vat'] - return duplicated_fields - - @api.model - def _get_duplicated_ids(self, partner): - partner_ids = [] - for field in self.env['res.partner']._merge_fields(): - if partner[field]: - partner_ids += self.env['res.partner'].search([(field, '=', partner[field])]).ids - return partner_ids - - def _merge(self, partner_ids, dst_partner=None): - """ private implementation of merge partner - :param partner_ids : ids of partner to merge - :param dst_partner : record of destination res.partner - """ - partner = self.env['res.partner'] - partner_ids = partner.browse(partner_ids).exists() - if len(partner_ids) < 2: - return - - if len(partner_ids) > 3: - raise UserError(_("For safety reasons, you cannot merge more than 3 contacts together. You can re-open the wizard several times if needed.")) - - # check if the list of partners to merge contains child/parent relation - child_ids = self.env['res.partner'] - for partner_id in partner_ids: - child_ids |= partner.search([('id', 'child_of', [partner_id.id])]) - partner_id - if partner_ids & child_ids: - raise UserError(_("You cannot merge a contact with one of his parent.")) - - # remove dst_partner from partners to merge - if dst_partner and dst_partner in partner_ids: - src_partners = partner_ids - dst_partner - else: - ordered_partners = self._get_ordered_partner(partner_ids.ids) - dst_partner = ordered_partners[-1] - src_partners = ordered_partners[:-1] - _logger.info("dst_partner: %s", dst_partner.id) - - # call sub methods to do the merge - self._update_foreign_keys(src_partners, dst_partner) - self._update_reference_fields(src_partners, dst_partner) - self._update_values(src_partners, dst_partner) - - _logger.info('(uid = %s) merged the partners %r with %s', self._uid, src_partners.ids, dst_partner.id) - dst_partner.message_post(body='%s %s' % (_("Merged with the following partners:"), ", ".join('%s <%s> (ID %s)' % (p.name, p.email or 'n/a', p.id) for p in src_partners))) - - # delete source partner, since they are merged - #~ src_partners.update({'active':False}) - src_partners.unlink() - return dst_partner - - @api.model - def _update_foreign_keys(self, src_partners, dst_partner): - """ Update all foreign key from the src_partner to dst_partner. All many2one fields will be updated. - :param src_partners : merge source res.partner recordset (does not include destination one) - :param dst_partner : record of destination res.partner - """ - _logger.debug('_update_foreign_keys for dst_partner: %s for src_partners: %s', dst_partner.id, str(src_partners.ids)) - - # find the many2one relation to a partner - partner = self.env['res.partner'] - relations = self._get_fk_on('res_partner') - - for table, column in relations: - # get list of columns of current table (exept the current fk column) - query = "SELECT column_name FROM information_schema.columns WHERE table_name LIKE '%s'" % (table) - self._cr.execute(query, ()) - columns = [] - for data in self._cr.fetchall(): - if data[0] != column: - columns.append(data[0]) - - # do the update for the current table/column in SQL - query_dic = { - 'table': table, - 'column': column, - 'value': columns[0], - } - if len(columns) <= 1: - # unique key treated - query = """ - UPDATE "%(table)s" as ___tu - SET %(column)s = %%s - WHERE - %(column)s = %%s AND - NOT EXISTS ( - SELECT 1 - FROM "%(table)s" as ___tw - WHERE - %(column)s = %%s AND - ___tu.%(value)s = ___tw.%(value)s - )""" % query_dic - for partner in src_partners: - self._cr.execute(query, (dst_partner.id, partner.id, dst_partner.id)) - else: - try: - with mute_logger('odoo.sql_db'), self._cr.savepoint(): - query = 'UPDATE "%(table)s" SET %(column)s = %%s WHERE %(column)s IN %%s' % query_dic - self._cr.execute(query, (dst_partner.id, tuple(src_partners.ids),)) - - # handle the recursivity with parent relation - if column == partner._parent_name and table == 'res_partner': - query = """ - WITH RECURSIVE cycle(id, parent_id) AS ( - SELECT id, parent_id FROM res_partner - UNION - SELECT cycle.id, res_partner.parent_id - FROM res_partner, cycle - WHERE res_partner.id = cycle.parent_id AND - cycle.id != cycle.parent_id - ) - SELECT id FROM cycle WHERE id = parent_id AND id = %s - """ - self._cr.execute(query, (dst_partner.id,)) - # NOTE JEM : shouldn't we fetch the data ? - except psycopg2.Error: - # updating fails, most likely due to a violated unique constraint - # keeping record with nonexistent partner_id is useless, better delete it - query = 'DELETE FROM "%(table)s" WHERE "%(column)s" IN %%s' % query_dic - self._cr.execute(query, (tuple(src_partners.ids),)) - - @api.model - def _update_reference_fields(self, src_partners, dst_partner): - """ Update all reference fields from the src_partner to dst_partner. - :param src_partners : merge source res.partner recordset (does not include destination one) - :param dst_partner : record of destination res.partner - """ - _logger.debug('_update_reference_fields for dst_partner: %s for src_partners: %r', dst_partner.id, src_partners.ids) - - def update_records(model, src, field_model='model', field_id='res_id'): - Model = self.env[model] if model in self.env else None - if Model is None: - return - records = Model.sudo().search([(field_model, '=', 'res.partner'), (field_id, '=', src.id)]) - try: - with mute_logger('odoo.sql_db'), self._cr.savepoint(): - return records.sudo().write({field_id: dst_partner.id}) - except psycopg2.Error: - # updating fails, most likely due to a violated unique constraint - # keeping record with nonexistent partner_id is useless, better delete it - return records.sudo().unlink() - - update_records = functools.partial(update_records) - - for partner in src_partners: - update_records('calendar', src=partner, field_model='model_id.model') - update_records('ir.attachment', src=partner, field_model='res_model') - update_records('mail.followers', src=partner, field_model='res_model') - update_records('mail.message', src=partner) - update_records('ir.model.data', src=partner) - - records = self.env['ir.model.fields'].search([('ttype', '=', 'reference')]) - for record in records.sudo(): - try: - Model = self.env[record.model] - field = Model._fields[record.name] - except KeyError: - # unknown model or field => skip - continue - - if field.compute is not None: - continue - - for partner in src_partners: - records_ref = Model.sudo().search([(record.name, '=', 'res.partner,%d' % partner.id)]) - values = { - record.name: 'res.partner,%d' % dst_partner.id, - } - records_ref.sudo().write(values) - - @api.model - def _update_values(self, src_partners, dst_partner): - """ Update values of dst_partner with the ones from the src_partners. - :param src_partners : recordset of source res.partner - :param dst_partner : record of destination res.partner - """ - _logger.debug('_update_values for dst_partner: %s for src_partners: %r', dst_partner.id, src_partners.ids) - - model_fields = dst_partner.fields_get().keys() - - def write_serializer(item): - if isinstance(item, models.BaseModel): - return item.id - else: - return item - # get all fields that are not computed or x2many - values = dict() - for column in model_fields: - field = dst_partner._fields[column] - if field.type not in ('many2many', 'one2many') and field.compute is None: - for item in itertools.chain(src_partners, [dst_partner]): - if item[column]: - values[column] = write_serializer(item[column]) - # remove fields that can not be updated (id and parent_id) - values.pop('id', None) - parent_id = values.pop('parent_id', None) - dst_partner.write(values) - # try to update the parent_id - if parent_id and parent_id != dst_partner.id: - try: - dst_partner.write({'parent_id': parent_id}) - except ValidationError: - _logger.info('Skip recursive partner hierarchies for parent_id %s of partner: %s', parent_id, dst_partner.id) - - @api.model - def _get_ordered_partner(self, partner_ids): - """ Helper : returns a `res.partner` recordset ordered by create_date/active fields - :param partner_ids : list of partner ids to sort - """ - return self.env['res.partner'].browse(partner_ids).sorted( - key=lambda p: (p.active, (p.create_date or '')), - reverse=True, - ) - - @api.multi - def _compute_models(self): - """ Compute the different models needed by the system if you want to exclude some partners. """ - model_mapping = {} - if self.exclude_contact: - model_mapping['res.users'] = 'partner_id' - if 'account.move.line' in self.env and self.exclude_journal_item: - model_mapping['account.move.line'] = 'partner_id' - return model_mapping diff --git a/hotel/views/hotel_checkin_partner_views.xml b/hotel/views/hotel_checkin_partner_views.xml index 5bc64cbfd..e8d421e24 100644 --- a/hotel/views/hotel_checkin_partner_views.xml +++ b/hotel/views/hotel_checkin_partner_views.xml @@ -22,7 +22,8 @@ - + @@ -42,25 +43,25 @@ - - - - - - - + diff --git a/hotel_l10n_es/__manifest__.py b/hotel_l10n_es/__manifest__.py index 1889e9ea5..2b7bfe214 100755 --- a/hotel_l10n_es/__manifest__.py +++ b/hotel_l10n_es/__manifest__.py @@ -34,7 +34,7 @@ 'partner_contact_gender', 'partner_contact_birthdate', 'partner_firstname', - 'web_responsive' + 'partner_vat_unique', ], 'data': [ 'data/code.ine.csv', @@ -50,6 +50,7 @@ 'views/inherit_hotel_checkin_partner_views.xml', 'security/ir.model.access.csv', 'views/inherit_res_partner.xml', + 'views/inherited_hotel_reservation_views.xml', 'views/report_viajero_document.xml', 'views/report_viajero_head.xml', 'views/report_viajero_data.xml', diff --git a/hotel_l10n_es/models/__init__.py b/hotel_l10n_es/models/__init__.py index 9b8d5b74c..9a800ac78 100755 --- a/hotel_l10n_es/models/__init__.py +++ b/hotel_l10n_es/models/__init__.py @@ -25,3 +25,4 @@ from . import code_ine from . import inherit_res_company from . import inherit_res_partner from . import inherit_hotel_checkin_partner +from . import inherit_hotel_reservation diff --git a/hotel_l10n_es/models/inherit_hotel_checkin_partner.py b/hotel_l10n_es/models/inherit_hotel_checkin_partner.py index b14608d89..b7eb5e6c8 100755 --- a/hotel_l10n_es/models/inherit_hotel_checkin_partner.py +++ b/hotel_l10n_es/models/inherit_hotel_checkin_partner.py @@ -1,39 +1,127 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2017 Alda Hotels -# Jose Luis Algara -# -# 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 . -# -############################################################################## -from openerp import models, fields, api, _ -from odoo.osv.expression import get_unaccent_wrapper - - -class HotelCheckinPartner(models.Model): - _inherit = 'hotel.checkin.partner' - - document_type = fields.Selection(related='partner_id.document_type') - document_number = fields.Char(related='partner_id.document_number') - document_expedition_date = fields.Date(related='partner_id.document_expedition_date') - gender = fields.Selection('Gender', related='partner_id.gender') - birthdate_date = fields.Date('Birhdate', related='partner_id.birthdate_date') - code_ine_id = fields.Many2one(related="partner_id.code_ine_id") - - #TMP_FIX VAT Validation - @api.constrains("vat") - def check_vat(self): - return +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2017 Alda Hotels +# Jose Luis Algara +# +# 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 . +# +############################################################################## +from openerp import models, fields, api, _ +from odoo.exceptions import UserError +import logging +_logger = logging.getLogger(__name__) + + +class HotelCheckinPartner(models.Model): + _inherit = 'hotel.checkin.partner' + + document_type = fields.Selection(related='partner_id.document_type') + document_number = fields.Char(related='partner_id.document_number') + document_expedition_date = fields.Date( + related='partner_id.document_expedition_date') + gender = fields.Selection('Gender', related='partner_id.gender') + birthdate_date = fields.Date('Birhdate', + related='partner_id.birthdate_date') + code_ine_id = fields.Many2one(related="partner_id.code_ine_id") + name = fields.Char(related='partner_id.name') + lastname = fields.Char(related='partner_id.lastname') + firstname = fields.Char(related='partner_id.firstname') + + @api.model + def create(self, vals): + if not vals.get('partner_id'): + name = self.env['res.partner']._get_computed_name( + vals.get('lastname'), + vals.get('firstname') + ) + partner = self.env['res.partner'].create({ + 'name': name, + }) + vals.update({'partner_id': partner.id}) + vals.pop('firstname') + vals.pop('lastname') + return super(HotelCheckinPartner, self).create(vals) + + @api.multi + def write(self, vals): + for record in self: + if not vals.get('partner_id') and not record.partner_id: + name = self.env['res.partner']._get_computed_name( + vals.get('lastname'), + vals.get('firstname') + ) + partner = self.env['res.partner'].create({ + 'name': name, + }) + record.update({'partner_id': partner.id}) + vals.pop('firstname') + vals.pop('lastname') + return super(HotelCheckinPartner, self).write(vals) + + @api.multi + def action_on_board(self): + self.check_required_fields() + return super(HotelCheckinPartner, self).action_on_board() + + @api.model + def check_dni(self, dni): + digits = "TRWAGMYFPDXBNJZSQVHLCKE" + dig_ext = "XYZ" + reemp_dig_ext = {'X': '0', 'Y': '1', 'Z': '2'} + numbers = "1234567890" + dni = dni.upper() + if len(dni) == 9: + dig_control = dni[8] + dni = dni[:8] + if dni[0] in dig_ext: + dni = dni.replace(dni[0], reemp_dig_ext[dni[0]]) + return len(dni) == len([n for n in dni if n in numbers]) \ + and digits[int(dni) % 23] == dig_control + else: + return False + + @api.onchange('document_number', 'document_type') + def onchange_document_number(self): + for record in self: + if record.document_type == 'D' and record.document_number: + if not record.check_dni(record.document_number): + record.document_number = False + raise UserError(_('Incorrect DNI')) + if not record.partner_id and record.document_number and record.document_type: + partner = self.env['res.partner'].search([ + ('document_number', '=', record.document_number), + ('document_type', '=', record.document_type) + ], limit=1) + if partner: + record.update({'partner_id': partner}) + + + + @api.multi + def check_required_fields(self): + for record in self: + missing_fields = [] + required_fields = ['document_type', 'document_number', + 'document_expedition_date', 'gender', + 'birthdate_date', 'code_ine_id', + 'lastname', 'firstname'] + for field in required_fields: + if not record[field]: + missing_fields.append(record._fields[field].string) + if missing_fields: + raise UserError( + _('To perform the checkin the following data are missing:\ + %s') % (', '.join(missing_fields))) diff --git a/hotel_l10n_es/models/inherit_hotel_reservation.py b/hotel_l10n_es/models/inherit_hotel_reservation.py new file mode 100644 index 000000000..d50f48cbe --- /dev/null +++ b/hotel_l10n_es/models/inherit_hotel_reservation.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2017 Darío Lodeiros +# +# 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 . +# +############################################################################## +from openerp import models, api + + +class HotelReservation(models.Model): + _inherit = 'hotel.reservation' + + @api.multi + def print_all_checkins(self): + checkins = self.env['hotel.checkin.partner'] + for record in self: + checkins += record.checkin_partner_ids.filtered( + lambda s: s.state in ('booking', 'done')) + if checkins: + return self.env.ref('hotel_l10n_es.action_report_viajero').\ + report_action(checkins) diff --git a/hotel_l10n_es/models/inherit_res_partner.py b/hotel_l10n_es/models/inherit_res_partner.py index a8dd5b5ca..48395cb4b 100755 --- a/hotel_l10n_es/models/inherit_res_partner.py +++ b/hotel_l10n_es/models/inherit_res_partner.py @@ -1,100 +1,415 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2017 Alda Hotels -# Jose Luis Algara -# -# 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 . -# -############################################################################## -from openerp import models, fields, api, _ -from odoo.osv.expression import get_unaccent_wrapper - - -class ResPartner(models.Model): - _inherit = 'res.partner' - - document_type = fields.Selection([ - ('D', 'DNI'), - ('P', 'Pasaporte'), - ('C', 'Permiso de Conducir'), - ('I', 'Carta o Doc. de Identidad'), - ('N', 'Permiso Residencia Español'), - ('X', 'Permiso Residencia Europeo')], - help=_('Select a valid document type'), - default='D', - string='Doc. type', - ) - document_number = fields.Char('Document number') - document_expedition_date = fields.Date('Document expedition date') - - code_ine_id = fields.Many2one('code.ine', - help=_('Country or province of origin. Used for INE statistics.')) - - @api.model - def name_search(self, name, args=None, operator='ilike', limit=100): - result = super(ResPartner, self).name_search(name, args=None, - operator='ilike', - limit=100) - if args is None: - args = [] - if name and operator in ('=', 'ilike', '=ilike', 'like', '=like'): - self.check_access_rights('read') - where_query = self._where_calc(args) - self._apply_ir_rules(where_query, 'read') - from_clause, where_clause, where_clause_params = where_query.get_sql() - where_str = where_clause and (" WHERE %s AND " % where_clause) or ' WHERE ' - - # search on the name of the contacts and of its company - search_name = name - if operator in ('ilike', 'like'): - search_name = '%%%s%%' % name - if operator in ('=ilike', '=like'): - operator = operator[1:] - - unaccent = get_unaccent_wrapper(self.env.cr) - - query = """SELECT id - FROM res_partner - {where} ({document_number} {operator} {percent}) - ORDER BY {display_name} {operator} {percent} desc, - {display_name} - """.format(where=where_str, - operator=operator, - document_number=unaccent('document_number'), - display_name=unaccent('display_name'), - percent=unaccent('%s'),) - - where_clause_params += [search_name]*2 - if limit: - query += ' limit %s' - where_clause_params.append(limit) - self.env.cr.execute(query, where_clause_params) - partner_ids = [row[0] for row in self.env.cr.fetchall()] - if partner_ids: - result += self.browse(partner_ids).name_get() - return result - - #TMP_FIX VAT Validation - @api.constrains("vat") - def check_vat(self): - return - - #TODO: Review better VAT & DocumentNumber integration - @api.onchange('document_number') - def onchange_document_number(self): - for partner in self: - if partner.document_number and partner.document_type == 'D': - partner.vat = 'ES' + partner.document_number +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2017 Alda Hotels +# Jose Luis Algara +# +# 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 . +# +############################################################################## +import functools +import itertools +import logging +import psycopg2 + +from odoo import api, fields, models, _ +from odoo.osv.expression import get_unaccent_wrapper +from odoo.exceptions import ValidationError, UserError +from odoo.tools import mute_logger +_logger = logging.getLogger(__name__) + + +class ResPartner(models.Model): + _inherit = 'res.partner' + + document_type = fields.Selection([ + ('D', 'DNI'), + ('P', 'Pasaporte'), + ('C', 'Permiso de Conducir'), + ('I', 'Carta o Doc. de Identidad'), + ('N', 'Permiso Residencia Español'), + ('X', 'Permiso Residencia Europeo')], + help=_('Select a valid document type'), + default='D', + string='Doc. type', + ) + document_number = fields.Char('Document number', index=True) + document_expedition_date = fields.Date('Document expedition date') + code_ine_id = fields.Many2one('code.ine', + help=_('Country or province of origin. Used for INE statistics.')) + unconfirmed = fields.Boolean('Unconfirmed', default=True) + main_partner_id = fields.Many2one('res.partner') + + @api.model + def name_search(self, name, args=None, operator='ilike', limit=100): + result = super(ResPartner, self).name_search(name, args=None, + operator='ilike', + limit=100) + if args is None: + args = [] + if name and operator in ('=', 'ilike', '=ilike', 'like', '=like'): + self.check_access_rights('read') + where_query = self._where_calc(args) + self._apply_ir_rules(where_query, 'read') + from_clause, where_clause, where_clause_params = where_query.get_sql() + where_str = where_clause and (" WHERE %s AND " % where_clause) or ' WHERE ' + + # search on the name of the contacts and of its company + search_name = name + if operator in ('ilike', 'like'): + search_name = '%%%s%%' % name + if operator in ('=ilike', '=like'): + operator = operator[1:] + + unaccent = get_unaccent_wrapper(self.env.cr) + + query = """SELECT id + FROM res_partner + {where} ({document_number} {operator} {percent}) + ORDER BY {display_name} {operator} {percent} desc, + {display_name} + """.format(where=where_str, + operator=operator, + document_number=unaccent('document_number'), + display_name=unaccent('display_name'), + percent=unaccent('%s'),) + + where_clause_params += [search_name]*2 + if limit: + query += ' limit %s' + where_clause_params.append(limit) + self.env.cr.execute(query, where_clause_params) + partner_ids = [row[0] for row in self.env.cr.fetchall()] + if partner_ids: + result += self.browse(partner_ids).name_get() + return result + + @api.model + def _get_duplicated_ids(self, partner): + partner_ids = [] + if partner.vat: + partner_ids += self.env['res.partner'].search([ + ('vat', '=', partner.vat), + ('parent_id', '=', False) + ]).ids + if partner.document_number: + partner_ids += self.env['res.partner'].search([ + ('document_number', '=', partner.document_number), + ('child_ids', '=', False) + ]).ids + if partner_ids: + return partner_ids + + def _merge_fields(self): + duplicated_fields = ['vat', 'document_number'] + return duplicated_fields + + @api.constrains('vat') + def _check_vat_unique(self): + for record in self: + if record.unconfirmed: + if record.vat: + record.update({'unconfirmed': False}) + partner_ids = self.env['res.partner'].search([ + ('vat', '=', record.vat), + ('parent_id', '=', False) + ]).ids + if len(partner_ids) > 1: + partners = self.env['res.partner'].browse(partner_ids) + record._merge(partners._ids) + else: + return super(ResPartner, self)._check_vat_unique() + return True + + @api.constrains('document_number') + def _check_document_number_unique(self): + for record in self: + if not record.document_number: + continue + if record.unconfirmed: + if record.document_number: + record.update({'unconfirmed': False}) + partner_ids = self.env['res.partner'].search([ + ('document_number', '=', record.document_number), + ]).ids + if len(partner_ids) > 1: + partners = self.env['res.partner'].browse(partner_ids) + record._merge(partners._ids) + if not record.parent_id and record.document_type == 'D' \ + and not record.vat: + record.update({ + 'vat': record.document_number, + }) + else: + results = self.env['res.partner'].search_count([ + ('document_type', '=', record.document_type), + ('document_number', '=', record.document_number), + ('id', '!=', record.id) + ]) + if results: + raise ValidationError(_( + "The Document Number %s already exists in another " + "partner.") % record.document_number) + + @api.multi + def open_main_partner(self): + self.ensure_one() + action = self.env.ref('base.action_partner_form').read()[0] + if self.main_partner_id: + action['views'] = [(self.env.ref('base.view_partner_form').id, 'form')] + action['res_id'] = self.main_partner_id.id + else: + action = {'type': 'ir.actions.act_window_close'} + return action + + def _get_fk_on(self, table): + """ return a list of many2one relation with the given table. + :param table : the name of the sql table to return relations + :returns a list of tuple 'table name', 'column name'. + """ + query = """ + SELECT cl1.relname as table, att1.attname as column + FROM pg_constraint as con, pg_class as cl1, pg_class as cl2, pg_attribute as att1, pg_attribute as att2 + WHERE con.conrelid = cl1.oid + AND con.confrelid = cl2.oid + AND array_lower(con.conkey, 1) = 1 + AND con.conkey[1] = att1.attnum + AND att1.attrelid = cl1.oid + AND cl2.relname = %s + AND att2.attname = 'id' + AND array_lower(con.confkey, 1) = 1 + AND con.confkey[1] = att2.attnum + AND att2.attrelid = cl2.oid + AND con.contype = 'f' + """ + self._cr.execute(query, (table,)) + return self._cr.fetchall() + + def _merge(self, partner_ids, dst_partner=None): + """ private implementation of merge partner + :param partner_ids : ids of partner to merge + :param dst_partner : record of destination res.partner + """ + partner = self.env['res.partner'] + partner_ids = partner.browse(partner_ids).exists() + if len(partner_ids) < 2: + return + + if len(partner_ids) > 3: + raise UserError(_("For safety reasons, you cannot merge more than 3 contacts together. You can re-open the wizard several times if needed.")) + + # check if the list of partners to merge contains child/parent relation + child_ids = self.env['res.partner'] + for partner_id in partner_ids: + child_ids |= partner.search([('id', 'child_of', [partner_id.id])]) - partner_id + if partner_ids & child_ids: + raise UserError(_("You cannot merge a contact with one of his parent.")) + + # remove dst_partner from partners to merge + if dst_partner and dst_partner in partner_ids: + src_partners = partner_ids - dst_partner + else: + ordered_partners = self._get_ordered_partner(partner_ids.ids) + dst_partner = ordered_partners[-1] + src_partners = ordered_partners[:-1] + _logger.info("dst_partner: %s", dst_partner.id) + + # call sub methods to do the merge + self._update_foreign_keys(src_partners, dst_partner) + self._update_reference_fields(src_partners, dst_partner) + self._update_values(src_partners, dst_partner) + + _logger.info('(uid = %s) merged the partners %r with %s', self._uid, src_partners.ids, dst_partner.id) + dst_partner.message_post(body='%s %s' % (_("Merged with the following partners:"), ", ".join('%s <%s> (ID %s)' % (p.name, p.email or 'n/a', p.id) for p in src_partners))) + + return dst_partner + + @api.model + def _update_foreign_keys(self, src_partners, dst_partner): + """ Update all foreign key from the src_partner to dst_partner. All many2one fields will be updated. + :param src_partners : merge source res.partner recordset (does not include destination one) + :param dst_partner : record of destination res.partner + """ + _logger.debug('_update_foreign_keys for dst_partner: %s for src_partners: %s', dst_partner.id, str(src_partners.ids)) + + # find the many2one relation to a partner + partner = self.env['res.partner'] + relations = self._get_fk_on('res_partner') + + for table, column in relations: + # get list of columns of current table (exept the current fk column) + query = "SELECT column_name FROM information_schema.columns WHERE table_name LIKE '%s'" % (table) + self._cr.execute(query, ()) + columns = [] + for data in self._cr.fetchall(): + if data[0] != column: + columns.append(data[0]) + + # do the update for the current table/column in SQL + query_dic = { + 'table': table, + 'column': column, + 'value': columns[0], + } + if len(columns) <= 1: + # unique key treated + query = """ + UPDATE "%(table)s" as ___tu + SET %(column)s = %%s + WHERE + %(column)s = %%s AND + NOT EXISTS ( + SELECT 1 + FROM "%(table)s" as ___tw + WHERE + %(column)s = %%s AND + ___tu.%(value)s = ___tw.%(value)s + )""" % query_dic + for partner in src_partners: + self._cr.execute(query, (dst_partner.id, partner.id, dst_partner.id)) + else: + try: + with mute_logger('odoo.sql_db'), self._cr.savepoint(): + query = 'UPDATE "%(table)s" SET %(column)s = %%s WHERE %(column)s IN %%s' % query_dic + self._cr.execute(query, (dst_partner.id, tuple(src_partners.ids),)) + + # handle the recursivity with parent relation + if column == partner._parent_name and table == 'res_partner': + query = """ + WITH RECURSIVE cycle(id, parent_id) AS ( + SELECT id, parent_id FROM res_partner + UNION + SELECT cycle.id, res_partner.parent_id + FROM res_partner, cycle + WHERE res_partner.id = cycle.parent_id AND + cycle.id != cycle.parent_id + ) + SELECT id FROM cycle WHERE id = parent_id AND id = %s + """ + self._cr.execute(query, (dst_partner.id,)) + # NOTE JEM : shouldn't we fetch the data ? + except psycopg2.Error: + # updating fails, most likely due to a violated unique constraint + # keeping record with nonexistent partner_id is useless, better delete it + query = 'DELETE FROM "%(table)s" WHERE "%(column)s" IN %%s' % query_dic + self._cr.execute(query, (tuple(src_partners.ids),)) + + @api.model + def _update_reference_fields(self, src_partners, dst_partner): + """ Update all reference fields from the src_partner to dst_partner. + :param src_partners : merge source res.partner recordset (does not include destination one) + :param dst_partner : record of destination res.partner + """ + _logger.debug('_update_reference_fields for dst_partner: %s for src_partners: %r', dst_partner.id, src_partners.ids) + + def update_records(model, src, field_model='model', field_id='res_id'): + Model = self.env[model] if model in self.env else None + if Model is None: + return + records = Model.sudo().search([(field_model, '=', 'res.partner'), (field_id, '=', src.id)]) + try: + with mute_logger('odoo.sql_db'), self._cr.savepoint(): + return records.sudo().write({field_id: dst_partner.id}) + except psycopg2.Error: + # updating fails, most likely due to a violated unique constraint + # keeping record with nonexistent partner_id is useless, better delete it + return records.sudo().unlink() + + update_records = functools.partial(update_records) + + for partner in src_partners: + update_records('calendar', src=partner, field_model='model_id.model') + update_records('ir.attachment', src=partner, field_model='res_model') + update_records('mail.followers', src=partner, field_model='res_model') + update_records('mail.message', src=partner) + update_records('ir.model.data', src=partner) + + records = self.env['ir.model.fields'].search([('ttype', '=', 'reference')]) + for record in records.sudo(): + try: + Model = self.env[record.model] + field = Model._fields[record.name] + except KeyError: + # unknown model or field => skip + continue + + if field.compute is not None: + continue + + for partner in src_partners: + records_ref = Model.sudo().search([(record.name, '=', 'res.partner,%d' % partner.id)]) + values = { + record.name: 'res.partner,%d' % dst_partner.id, + } + records_ref.sudo().write(values) + + @api.model + def _update_values(self, src_partners, dst_partner): + """ Update values of dst_partner with the ones from the src_partners. + :param src_partners : recordset of source res.partner + :param dst_partner : record of destination res.partner + """ + _logger.debug('_update_values for dst_partner: %s for src_partners: %r', dst_partner.id, src_partners.ids) + + model_fields = dst_partner.fields_get().keys() + + def write_serializer(item): + if isinstance(item, models.BaseModel): + return item.id + else: + return item + # get all fields that are not computed or x2many + values = dict() + for column in model_fields: + field = dst_partner._fields[column] + if field.type not in ('many2many', 'one2many') and field.compute is None: + for item in itertools.chain(src_partners, [dst_partner]): + if item[column]: + values[column] = write_serializer(item[column]) + # remove fields that can not be updated (id and parent_id) + values.pop('id', None) + parent_id = values.pop('parent_id', None) + src_partners.update({ + 'active': False, + 'document_number': False, + 'vat': False, + 'main_partner_id': dst_partner.id}) + dst_partner.write(values) + # try to update the parent_id + if parent_id and parent_id != dst_partner.id: + try: + dst_partner.write({'parent_id': parent_id}) + except ValidationError: + _logger.info('Skip recursive partner hierarchies for parent_id %s of partner: %s', parent_id, dst_partner.id) + + @api.model + def _get_ordered_partner(self, partner_ids): + """ Helper : returns a `res.partner` recordset ordered by create_date/active fields + :param partner_ids : list of partner ids to sort + """ + return self.env['res.partner'].browse(partner_ids).sorted( + key=lambda p: (p.active, (p.create_date or '')), + reverse=True, + ) + + @api.multi + def _compute_models(self): + """ Compute the different models needed by the system if you want to exclude some partners. """ + model_mapping = {} + if self.exclude_contact: + model_mapping['res.users'] = 'partner_id' + if 'account.move.line' in self.env and self.exclude_journal_item: + model_mapping['account.move.line'] = 'partner_id' + return model_mapping diff --git a/hotel_l10n_es/views/inherit_hotel_checkin_partner_views.xml b/hotel_l10n_es/views/inherit_hotel_checkin_partner_views.xml index 973f50d62..ef515eb66 100755 --- a/hotel_l10n_es/views/inherit_hotel_checkin_partner_views.xml +++ b/hotel_l10n_es/views/inherit_hotel_checkin_partner_views.xml @@ -1,78 +1,132 @@ - - - - - hotel.checkin.partner.view.form - hotel.checkin.partner - - - - - - - - - - - -