diff --git a/hotel/models/inherited_res_partner.py b/hotel/models/inherited_res_partner.py
index 37fd8ca57..767c578f0 100644
--- a/hotel/models/inherited_res_partner.py
+++ b/hotel/models/inherited_res_partner.py
@@ -1,8 +1,19 @@
# Copyright 2017 Alexandre Díaz
# Copyright 2017 Dario Lodeiros
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-from openerp import models, fields, api
+from ast import literal_eval
+import functools
+import itertools
+import logging
+import psycopg2
+
+from odoo import api, fields, models
from odoo.osv.expression import get_unaccent_wrapper
+from odoo import SUPERUSER_ID, _
+from odoo.exceptions import ValidationError, UserError
+from odoo.tools import mute_logger
+_logger = logging.getLogger(__name__)
+
class ResPartner(models.Model):
_inherit = 'res.partner'
@@ -23,6 +34,30 @@ class ResPartner(models.Model):
reservations_count = fields.Integer('Reservations', 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.model
def name_search(self, name, args=None, operator='ilike', limit=100):
@@ -69,3 +104,237 @@ class ResPartner(models.Model):
if partner_ids:
result += self.browse(partner_ids).name_get()
return result
+
+ @api.constrains('vat')
+ def _constrain_duplicated_partner(self):
+ for record in self:
+ domain = record._get_fields_domain_partner()
+ if record.unconfirmed == True and len(domain) > 0:
+ partners = self.env['res.partner'].search(domain)
+ record.update({'unconfirmed': False})
+ if len(partners) > 0:
+ with self.env.norecompute():
+ import wdb; wdb.set_trace()
+ record._merge(partners._ids)
+
+
+ @api.model
+ def _get_fields_domain_partner(self):
+ self.ensure_one()
+ domain = []
+ if self.vat:
+ domain.append(('vat', '=', self.vat))
+ return domain
+
+ 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})
+
+ @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/inherited_res_partner_views.xml b/hotel/views/inherited_res_partner_views.xml
index c2a68add2..ad33c7097 100644
--- a/hotel/views/inherited_res_partner_views.xml
+++ b/hotel/views/inherited_res_partner_views.xml
@@ -32,6 +32,9 @@
+
+
+
diff --git a/hotel_channel_connector/__manifest__.py b/hotel_channel_connector/__manifest__.py
index 8bd78dcb6..51ac74bb6 100644
--- a/hotel_channel_connector/__manifest__.py
+++ b/hotel_channel_connector/__manifest__.py
@@ -27,7 +27,6 @@
'views/inherited_product_pricelist_item_views.xml',
'views/inherited_hotel_room_type_restriction_views.xml',
'views/inherited_hotel_room_type_restriction_item_views.xml',
- 'views/inherited_res_partner_views.xml',
'views/channel_hotel_reservation_views.xml',
'views/channel_hotel_room_type_views.xml',
'views/channel_hotel_room_type_availability_views.xml',
diff --git a/hotel_channel_connector/models/__init__.py b/hotel_channel_connector/models/__init__.py
index 735f854d4..612e9674c 100644
--- a/hotel_channel_connector/models/__init__.py
+++ b/hotel_channel_connector/models/__init__.py
@@ -11,7 +11,6 @@ from . import hotel_room_type_restriction_item
from . import hotel_room_type_availability
from . import hotel_reservation
from . import inherited_hotel_folio
-from . import inherited_res_partner
from . import channel_ota_info
from . import hotel_channel_connector_issue
from . import inherited_hotel_board_service_room_type
diff --git a/hotel_channel_connector/models/inherited_res_partner.py b/hotel_channel_connector/models/inherited_res_partner.py
deleted file mode 100644
index f71988ca9..000000000
--- a/hotel_channel_connector/models/inherited_res_partner.py
+++ /dev/null
@@ -1,51 +0,0 @@
-# Copyright 2018 Alexandre Díaz
-# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-
-from odoo import models, fields, api
-
-
-class ResPartner(models.Model):
-
- _inherit = 'res.partner'
-
- unconfirmed = fields.Boolean('Unconfirmed', default=False)
-
- @api.multi
- def write(self, vals):
- res = False
- new_vat = vals.get('vat')
- if new_vat:
- org_partner_id = self.env['res.partner'].search([
- ('vat', '=', new_vat),
- ('unconfirmed', '=', False)
- ], limit=1)
- if org_partner_id:
- res = super(ResPartner, self).write(vals)
- for record in self:
- # replace all folios partners with the
- # first 'confirmed' partner with the same vat
- if record.unconfirmed:
- folio_ids = self.env['hotel.folio'].search([
- ('partner_id', '=', record.id)
- ])
- if folio_ids:
- folio_ids.write({
- 'partner_id': org_partner_id.id,
- })
- folio_ids = self.env['hotel.folio'].search([
- ('partner_invoice_id', '=', record.id)
- ])
- if folio_ids:
- folio_ids.write({
- 'partner_invoice_id': org_partner_id.id,
- })
- record.write({'active': False})
- else:
- # If not found, this is the 'confirmed'
- vals.update({'unconfirmed': False})
- res = super(ResPartner, self).write(vals)
- else:
- # If not have new vat... do nothing
- res = super(ResPartner, self).write(vals)
-
- return res
diff --git a/hotel_channel_connector/views/inherited_res_partner_views.xml b/hotel_channel_connector/views/inherited_res_partner_views.xml
deleted file mode 100644
index 27212c2bf..000000000
--- a/hotel_channel_connector/views/inherited_res_partner_views.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
- res.partner
-
-
-
-
-
-
-
-
-
-
-