1# -*- coding: utf-8 -*-
2# Part of Odoo. See LICENSE file for full copyright and licensing details.
3
4import base64
5import collections
6import datetime
7import hashlib
8import pytz
9import threading
10import re
11
12import requests
13from lxml import etree
14from random import randint
15from werkzeug import urls
16
17from odoo import api, fields, models, tools, SUPERUSER_ID, _
18from odoo.modules import get_module_resource
19from odoo.osv.expression import get_unaccent_wrapper
20from odoo.exceptions import UserError, ValidationError
21
22# Global variables used for the warning fields declared on the res.partner
23# in the following modules : sale, purchase, account, stock
24WARNING_MESSAGE = [
25                   ('no-message','No Message'),
26                   ('warning','Warning'),
27                   ('block','Blocking Message')
28                   ]
29WARNING_HELP = 'Selecting the "Warning" option will notify user with the message, Selecting "Blocking Message" will throw an exception with the message and block the flow. The Message has to be written in the next field.'
30
31
32ADDRESS_FIELDS = ('street', 'street2', 'zip', 'city', 'state_id', 'country_id')
33@api.model
34def _lang_get(self):
35    return self.env['res.lang'].get_installed()
36
37
38# put POSIX 'Etc/*' entries at the end to avoid confusing users - see bug 1086728
39_tzs = [(tz, tz) for tz in sorted(pytz.all_timezones, key=lambda tz: tz if not tz.startswith('Etc/') else '_')]
40def _tz_get(self):
41    return _tzs
42
43
44class FormatAddressMixin(models.AbstractModel):
45    _name = "format.address.mixin"
46    _description = 'Address Format'
47
48    def _fields_view_get_address(self, arch):
49        # consider the country of the user, not the country of the partner we want to display
50        address_view_id = self.env.company.country_id.address_view_id.sudo()
51        if address_view_id and not self._context.get('no_address_format') and (not address_view_id.model or address_view_id.model == self._name):
52            #render the partner address accordingly to address_view_id
53            doc = etree.fromstring(arch)
54            for address_node in doc.xpath("//div[hasclass('o_address_format')]"):
55                Partner = self.env['res.partner'].with_context(no_address_format=True)
56                sub_view = Partner.fields_view_get(
57                    view_id=address_view_id.id, view_type='form', toolbar=False, submenu=False)
58                sub_view_node = etree.fromstring(sub_view['arch'])
59                #if the model is different than res.partner, there are chances that the view won't work
60                #(e.g fields not present on the model). In that case we just return arch
61                if self._name != 'res.partner':
62                    try:
63                        self.env['ir.ui.view'].postprocess_and_fields(sub_view_node, model=self._name)
64                    except ValueError:
65                        return arch
66                address_node.getparent().replace(address_node, sub_view_node)
67            arch = etree.tostring(doc, encoding='unicode')
68        return arch
69
70class PartnerCategory(models.Model):
71    _description = 'Partner Tags'
72    _name = 'res.partner.category'
73    _order = 'name'
74    _parent_store = True
75
76    def _get_default_color(self):
77        return randint(1, 11)
78
79    name = fields.Char(string='Tag Name', required=True, translate=True)
80    color = fields.Integer(string='Color Index', default=_get_default_color)
81    parent_id = fields.Many2one('res.partner.category', string='Parent Category', index=True, ondelete='cascade')
82    child_ids = fields.One2many('res.partner.category', 'parent_id', string='Child Tags')
83    active = fields.Boolean(default=True, help="The active field allows you to hide the category without removing it.")
84    parent_path = fields.Char(index=True)
85    partner_ids = fields.Many2many('res.partner', column1='category_id', column2='partner_id', string='Partners')
86
87    @api.constrains('parent_id')
88    def _check_parent_id(self):
89        if not self._check_recursion():
90            raise ValidationError(_('You can not create recursive tags.'))
91
92    def name_get(self):
93        """ Return the categories' display name, including their direct
94            parent by default.
95
96            If ``context['partner_category_display']`` is ``'short'``, the short
97            version of the category name (without the direct parent) is used.
98            The default is the long version.
99        """
100        if self._context.get('partner_category_display') == 'short':
101            return super(PartnerCategory, self).name_get()
102
103        res = []
104        for category in self:
105            names = []
106            current = category
107            while current:
108                names.append(current.name)
109                current = current.parent_id
110            res.append((category.id, ' / '.join(reversed(names))))
111        return res
112
113    @api.model
114    def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None):
115        args = args or []
116        if name:
117            # Be sure name_search is symetric to name_get
118            name = name.split(' / ')[-1]
119            args = [('name', operator, name)] + args
120        return self._search(args, limit=limit, access_rights_uid=name_get_uid)
121
122
123class PartnerTitle(models.Model):
124    _name = 'res.partner.title'
125    _order = 'name'
126    _description = 'Partner Title'
127
128    name = fields.Char(string='Title', required=True, translate=True)
129    shortcut = fields.Char(string='Abbreviation', translate=True)
130
131
132class Partner(models.Model):
133    _description = 'Contact'
134    _inherit = ['format.address.mixin', 'image.mixin']
135    _name = "res.partner"
136    _order = "display_name"
137
138    def _default_category(self):
139        return self.env['res.partner.category'].browse(self._context.get('category_id'))
140
141    @api.model
142    def default_get(self, default_fields):
143        """Add the company of the parent as default if we are creating a child partner.
144        Also take the parent lang by default if any, otherwise, fallback to default DB lang."""
145        values = super().default_get(default_fields)
146        parent = self.env["res.partner"]
147        if 'parent_id' in default_fields and values.get('parent_id'):
148            parent = self.browse(values.get('parent_id'))
149            values['company_id'] = parent.company_id.id
150        if 'lang' in default_fields:
151            values['lang'] = values.get('lang') or parent.lang or self.env.lang
152        return values
153
154    name = fields.Char(index=True)
155    display_name = fields.Char(compute='_compute_display_name', store=True, index=True)
156    date = fields.Date(index=True)
157    title = fields.Many2one('res.partner.title')
158    parent_id = fields.Many2one('res.partner', string='Related Company', index=True)
159    parent_name = fields.Char(related='parent_id.name', readonly=True, string='Parent name')
160    child_ids = fields.One2many('res.partner', 'parent_id', string='Contact', domain=[('active', '=', True)])  # force "active_test" domain to bypass _search() override
161    ref = fields.Char(string='Reference', index=True)
162    lang = fields.Selection(_lang_get, string='Language',
163                            help="All the emails and documents sent to this contact will be translated in this language.")
164    active_lang_count = fields.Integer(compute='_compute_active_lang_count')
165    tz = fields.Selection(_tz_get, string='Timezone', default=lambda self: self._context.get('tz'),
166                          help="When printing documents and exporting/importing data, time values are computed according to this timezone.\n"
167                               "If the timezone is not set, UTC (Coordinated Universal Time) is used.\n"
168                               "Anywhere else, time values are computed according to the time offset of your web client.")
169
170    tz_offset = fields.Char(compute='_compute_tz_offset', string='Timezone offset', invisible=True)
171    user_id = fields.Many2one('res.users', string='Salesperson',
172      help='The internal user in charge of this contact.')
173    vat = fields.Char(string='Tax ID', index=True, help="The Tax Identification Number. Complete it if the contact is subjected to government taxes. Used in some legal statements.")
174    same_vat_partner_id = fields.Many2one('res.partner', string='Partner with same Tax ID', compute='_compute_same_vat_partner_id', store=False)
175    bank_ids = fields.One2many('res.partner.bank', 'partner_id', string='Banks')
176    website = fields.Char('Website Link')
177    comment = fields.Text(string='Notes')
178
179    category_id = fields.Many2many('res.partner.category', column1='partner_id',
180                                    column2='category_id', string='Tags', default=_default_category)
181    credit_limit = fields.Float(string='Credit Limit')
182    active = fields.Boolean(default=True)
183    employee = fields.Boolean(help="Check this box if this contact is an Employee.")
184    function = fields.Char(string='Job Position')
185    type = fields.Selection(
186        [('contact', 'Contact'),
187         ('invoice', 'Invoice Address'),
188         ('delivery', 'Delivery Address'),
189         ('other', 'Other Address'),
190         ("private", "Private Address"),
191        ], string='Address Type',
192        default='contact',
193        help="Invoice & Delivery addresses are used in sales orders. Private addresses are only visible by authorized users.")
194    # address fields
195    street = fields.Char()
196    street2 = fields.Char()
197    zip = fields.Char(change_default=True)
198    city = fields.Char()
199    state_id = fields.Many2one("res.country.state", string='State', ondelete='restrict', domain="[('country_id', '=?', country_id)]")
200    country_id = fields.Many2one('res.country', string='Country', ondelete='restrict')
201    partner_latitude = fields.Float(string='Geo Latitude', digits=(16, 5))
202    partner_longitude = fields.Float(string='Geo Longitude', digits=(16, 5))
203    email = fields.Char()
204    email_formatted = fields.Char(
205        'Formatted Email', compute='_compute_email_formatted',
206        help='Format email address "Name <email@domain>"')
207    phone = fields.Char()
208    mobile = fields.Char()
209    is_company = fields.Boolean(string='Is a Company', default=False,
210        help="Check if the contact is a company, otherwise it is a person")
211    industry_id = fields.Many2one('res.partner.industry', 'Industry')
212    # company_type is only an interface field, do not use it in business logic
213    company_type = fields.Selection(string='Company Type',
214        selection=[('person', 'Individual'), ('company', 'Company')],
215        compute='_compute_company_type', inverse='_write_company_type')
216    company_id = fields.Many2one('res.company', 'Company', index=True)
217    color = fields.Integer(string='Color Index', default=0)
218    user_ids = fields.One2many('res.users', 'partner_id', string='Users', auto_join=True)
219    partner_share = fields.Boolean(
220        'Share Partner', compute='_compute_partner_share', store=True,
221        help="Either customer (not a user), either shared user. Indicated the current partner is a customer without "
222             "access or with a limited access created for sharing data.")
223    contact_address = fields.Char(compute='_compute_contact_address', string='Complete Address')
224
225    # technical field used for managing commercial fields
226    commercial_partner_id = fields.Many2one('res.partner', compute='_compute_commercial_partner',
227                                             string='Commercial Entity', store=True, index=True)
228    commercial_company_name = fields.Char('Company Name Entity', compute='_compute_commercial_company_name',
229                                          store=True)
230    company_name = fields.Char('Company Name')
231    barcode = fields.Char(help="Use a barcode to identify this contact.", copy=False, company_dependent=True)
232
233    # hack to allow using plain browse record in qweb views, and used in ir.qweb.field.contact
234    self = fields.Many2one(comodel_name=_name, compute='_compute_get_ids')
235
236    _sql_constraints = [
237        ('check_name', "CHECK( (type='contact' AND name IS NOT NULL) or (type!='contact') )", 'Contacts require a name'),
238    ]
239
240    @api.depends('is_company', 'name', 'parent_id.display_name', 'type', 'company_name')
241    def _compute_display_name(self):
242        diff = dict(show_address=None, show_address_only=None, show_email=None, html_format=None, show_vat=None)
243        names = dict(self.with_context(**diff).name_get())
244        for partner in self:
245            partner.display_name = names.get(partner.id)
246
247    @api.depends('lang')
248    def _compute_active_lang_count(self):
249        lang_count = len(self.env['res.lang'].get_installed())
250        for partner in self:
251            partner.active_lang_count = lang_count
252
253    @api.depends('tz')
254    def _compute_tz_offset(self):
255        for partner in self:
256            partner.tz_offset = datetime.datetime.now(pytz.timezone(partner.tz or 'GMT')).strftime('%z')
257
258    @api.depends('user_ids.share', 'user_ids.active')
259    def _compute_partner_share(self):
260        super_partner = self.env['res.users'].browse(SUPERUSER_ID).partner_id
261        if super_partner in self:
262            super_partner.partner_share = False
263        for partner in self - super_partner:
264            partner.partner_share = not partner.user_ids or not any(not user.share for user in partner.user_ids)
265
266    @api.depends('vat', 'company_id')
267    def _compute_same_vat_partner_id(self):
268        for partner in self:
269            # use _origin to deal with onchange()
270            partner_id = partner._origin.id
271            #active_test = False because if a partner has been deactivated you still want to raise the error,
272            #so that you can reactivate it instead of creating a new one, which would loose its history.
273            Partner = self.with_context(active_test=False).sudo()
274            domain = [
275                ('vat', '=', partner.vat),
276                ('company_id', 'in', [False, partner.company_id.id]),
277            ]
278            if partner_id:
279                domain += [('id', '!=', partner_id), '!', ('id', 'child_of', partner_id)]
280            partner.same_vat_partner_id = bool(partner.vat) and not partner.parent_id and Partner.search(domain, limit=1)
281
282    @api.depends(lambda self: self._display_address_depends())
283    def _compute_contact_address(self):
284        for partner in self:
285            partner.contact_address = partner._display_address()
286
287    def _compute_get_ids(self):
288        for partner in self:
289            partner.self = partner.id
290
291    @api.depends('is_company', 'parent_id.commercial_partner_id')
292    def _compute_commercial_partner(self):
293        for partner in self:
294            if partner.is_company or not partner.parent_id:
295                partner.commercial_partner_id = partner
296            else:
297                partner.commercial_partner_id = partner.parent_id.commercial_partner_id
298
299    @api.depends('company_name', 'parent_id.is_company', 'commercial_partner_id.name')
300    def _compute_commercial_company_name(self):
301        for partner in self:
302            p = partner.commercial_partner_id
303            partner.commercial_company_name = p.is_company and p.name or partner.company_name
304
305    @api.model
306    def _fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False):
307        if (not view_id) and (view_type == 'form') and self._context.get('force_email'):
308            view_id = self.env.ref('base.view_partner_simple_form').id
309        res = super(Partner, self)._fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
310        if view_type == 'form':
311            res['arch'] = self._fields_view_get_address(res['arch'])
312        return res
313
314    @api.constrains('parent_id')
315    def _check_parent_id(self):
316        if not self._check_recursion():
317            raise ValidationError(_('You cannot create recursive Partner hierarchies.'))
318
319    def copy(self, default=None):
320        self.ensure_one()
321        chosen_name = default.get('name') if default else ''
322        new_name = chosen_name or _('%s (copy)', self.name)
323        default = dict(default or {}, name=new_name)
324        return super(Partner, self).copy(default)
325
326    @api.onchange('parent_id')
327    def onchange_parent_id(self):
328        # return values in result, as this method is used by _fields_sync()
329        if not self.parent_id:
330            return
331        result = {}
332        partner = self._origin
333        if partner.parent_id and partner.parent_id != self.parent_id:
334            result['warning'] = {
335                'title': _('Warning'),
336                'message': _('Changing the company of a contact should only be done if it '
337                             'was never correctly set. If an existing contact starts working for a new '
338                             'company then a new contact should be created under that new '
339                             'company. You can use the "Discard" button to abandon this change.')}
340        if partner.type == 'contact' or self.type == 'contact':
341            # for contacts: copy the parent address, if set (aka, at least one
342            # value is set in the address: otherwise, keep the one from the
343            # contact)
344            address_fields = self._address_fields()
345            if any(self.parent_id[key] for key in address_fields):
346                def convert(value):
347                    return value.id if isinstance(value, models.BaseModel) else value
348                result['value'] = {key: convert(self.parent_id[key]) for key in address_fields}
349        return result
350
351    @api.onchange('parent_id')
352    def _onchange_parent_id_for_lang(self):
353        # While creating / updating child contact, take the parent lang by default if any
354        # otherwise, fallback to default context / DB lang
355        if self.parent_id:
356            self.lang = self.parent_id.lang or self.env.context.get('default_lang') or self.env.lang
357
358    @api.onchange('country_id')
359    def _onchange_country_id(self):
360        if self.country_id and self.country_id != self.state_id.country_id:
361            self.state_id = False
362
363    @api.onchange('state_id')
364    def _onchange_state(self):
365        if self.state_id.country_id:
366            self.country_id = self.state_id.country_id
367
368    @api.onchange('email')
369    def onchange_email(self):
370        if not self.image_1920 and self._context.get('gravatar_image') and self.email:
371            self.image_1920 = self._get_gravatar_image(self.email)
372
373    @api.onchange('parent_id', 'company_id')
374    def _onchange_company_id(self):
375        if self.parent_id:
376            self.company_id = self.parent_id.company_id.id
377
378    @api.depends('name', 'email')
379    def _compute_email_formatted(self):
380        for partner in self:
381            if partner.email:
382                partner.email_formatted = tools.formataddr((partner.name or u"False", partner.email or u"False"))
383            else:
384                partner.email_formatted = ''
385
386    @api.depends('is_company')
387    def _compute_company_type(self):
388        for partner in self:
389            partner.company_type = 'company' if partner.is_company else 'person'
390
391    def _write_company_type(self):
392        for partner in self:
393            partner.is_company = partner.company_type == 'company'
394
395    @api.onchange('company_type')
396    def onchange_company_type(self):
397        self.is_company = (self.company_type == 'company')
398
399    @api.constrains('barcode')
400    def _check_barcode_unicity(self):
401        if self.env['res.partner'].search_count([('barcode', '=', self.barcode)]) > 1:
402            raise ValidationError('An other user already has this barcode')
403
404    def _update_fields_values(self, fields):
405        """ Returns dict of write() values for synchronizing ``fields`` """
406        values = {}
407        for fname in fields:
408            field = self._fields[fname]
409            if field.type == 'many2one':
410                values[fname] = self[fname].id
411            elif field.type == 'one2many':
412                raise AssertionError(_('One2Many fields cannot be synchronized as part of `commercial_fields` or `address fields`'))
413            elif field.type == 'many2many':
414                values[fname] = [(6, 0, self[fname].ids)]
415            else:
416                values[fname] = self[fname]
417        return values
418
419    @api.model
420    def _address_fields(self):
421        """Returns the list of address fields that are synced from the parent."""
422        return list(ADDRESS_FIELDS)
423
424    @api.model
425    def _formatting_address_fields(self):
426        """Returns the list of address fields usable to format addresses."""
427        return self._address_fields()
428
429    def update_address(self, vals):
430        addr_vals = {key: vals[key] for key in self._address_fields() if key in vals}
431        if addr_vals:
432            return super(Partner, self).write(addr_vals)
433
434    @api.model
435    def _commercial_fields(self):
436        """ Returns the list of fields that are managed by the commercial entity
437        to which a partner belongs. These fields are meant to be hidden on
438        partners that aren't `commercial entities` themselves, and will be
439        delegated to the parent `commercial entity`. The list is meant to be
440        extended by inheriting classes. """
441        return ['vat', 'credit_limit']
442
443    def _commercial_sync_from_company(self):
444        """ Handle sync of commercial fields when a new parent commercial entity is set,
445        as if they were related fields """
446        commercial_partner = self.commercial_partner_id
447        if commercial_partner != self:
448            sync_vals = commercial_partner._update_fields_values(self._commercial_fields())
449            self.write(sync_vals)
450
451    def _commercial_sync_to_children(self):
452        """ Handle sync of commercial fields to descendants """
453        commercial_partner = self.commercial_partner_id
454        sync_vals = commercial_partner._update_fields_values(self._commercial_fields())
455        sync_children = self.child_ids.filtered(lambda c: not c.is_company)
456        for child in sync_children:
457            child._commercial_sync_to_children()
458        res = sync_children.write(sync_vals)
459        sync_children._compute_commercial_partner()
460        return res
461
462    def _fields_sync(self, values):
463        """ Sync commercial fields and address fields from company and to children after create/update,
464        just as if those were all modeled as fields.related to the parent """
465        # 1. From UPSTREAM: sync from parent
466        if values.get('parent_id') or values.get('type') == 'contact':
467            # 1a. Commercial fields: sync if parent changed
468            if values.get('parent_id'):
469                self._commercial_sync_from_company()
470            # 1b. Address fields: sync if parent or use_parent changed *and* both are now set
471            if self.parent_id and self.type == 'contact':
472                onchange_vals = self.onchange_parent_id().get('value', {})
473                self.update_address(onchange_vals)
474
475        # 2. To DOWNSTREAM: sync children
476        self._children_sync(values)
477
478    def _children_sync(self, values):
479        if not self.child_ids:
480            return
481        # 2a. Commercial Fields: sync if commercial entity
482        if self.commercial_partner_id == self:
483            commercial_fields = self._commercial_fields()
484            if any(field in values for field in commercial_fields):
485                self._commercial_sync_to_children()
486        for child in self.child_ids.filtered(lambda c: not c.is_company):
487            if child.commercial_partner_id != self.commercial_partner_id:
488                self._commercial_sync_to_children()
489                break
490        # 2b. Address fields: sync if address changed
491        address_fields = self._address_fields()
492        if any(field in values for field in address_fields):
493            contacts = self.child_ids.filtered(lambda c: c.type == 'contact')
494            contacts.update_address(values)
495
496    def _handle_first_contact_creation(self):
497        """ On creation of first contact for a company (or root) that has no address, assume contact address
498        was meant to be company address """
499        parent = self.parent_id
500        address_fields = self._address_fields()
501        if (parent.is_company or not parent.parent_id) and len(parent.child_ids) == 1 and \
502            any(self[f] for f in address_fields) and not any(parent[f] for f in address_fields):
503            addr_vals = self._update_fields_values(address_fields)
504            parent.update_address(addr_vals)
505
506    def _clean_website(self, website):
507        url = urls.url_parse(website)
508        if not url.scheme:
509            if not url.netloc:
510                url = url.replace(netloc=url.path, path='')
511            website = url.replace(scheme='http').to_url()
512        return website
513
514    def write(self, vals):
515        if vals.get('active') is False:
516            # DLE: It should not be necessary to modify this to make work the ORM. The problem was just the recompute
517            # of partner.user_ids when you create a new user for this partner, see test test_70_archive_internal_partners
518            # You modified it in a previous commit, see original commit of this:
519            # https://github.com/odoo/odoo/commit/9d7226371730e73c296bcc68eb1f856f82b0b4ed
520            #
521            # RCO: when creating a user for partner, the user is automatically added in partner.user_ids.
522            # This is wrong if the user is not active, as partner.user_ids only returns active users.
523            # Hence this temporary hack until the ORM updates inverse fields correctly.
524            self.invalidate_cache(['user_ids'], self._ids)
525            for partner in self:
526                if partner.active and partner.user_ids:
527                    raise ValidationError(_('You cannot archive a contact linked to a portal or internal user.'))
528        # res.partner must only allow to set the company_id of a partner if it
529        # is the same as the company of all users that inherit from this partner
530        # (this is to allow the code from res_users to write to the partner!) or
531        # if setting the company_id to False (this is compatible with any user
532        # company)
533        if vals.get('website'):
534            vals['website'] = self._clean_website(vals['website'])
535        if vals.get('parent_id'):
536            vals['company_name'] = False
537        if 'company_id' in vals:
538            company_id = vals['company_id']
539            for partner in self:
540                if company_id and partner.user_ids:
541                    company = self.env['res.company'].browse(company_id)
542                    companies = set(user.company_id for user in partner.user_ids)
543                    if len(companies) > 1 or company not in companies:
544                        raise UserError(
545                            ("The selected company is not compatible with the companies of the related user(s)"))
546                if partner.child_ids:
547                    partner.child_ids.write({'company_id': company_id})
548        result = True
549        # To write in SUPERUSER on field is_company and avoid access rights problems.
550        if 'is_company' in vals and self.user_has_groups('base.group_partner_manager') and not self.env.su:
551            result = super(Partner, self.sudo()).write({'is_company': vals.get('is_company')})
552            del vals['is_company']
553        result = result and super(Partner, self).write(vals)
554        for partner in self:
555            if any(u.has_group('base.group_user') for u in partner.user_ids if u != self.env.user):
556                self.env['res.users'].check_access_rights('write')
557            partner._fields_sync(vals)
558        return result
559
560    @api.model_create_multi
561    def create(self, vals_list):
562        if self.env.context.get('import_file'):
563            self._check_import_consistency(vals_list)
564        for vals in vals_list:
565            if vals.get('website'):
566                vals['website'] = self._clean_website(vals['website'])
567            if vals.get('parent_id'):
568                vals['company_name'] = False
569        partners = super(Partner, self).create(vals_list)
570
571        if self.env.context.get('_partners_skip_fields_sync'):
572            return partners
573
574        for partner, vals in zip(partners, vals_list):
575            partner._fields_sync(vals)
576            # Lang: propagate from parent if no value was given
577            if 'lang' not in vals and partner.parent_id:
578                partner._onchange_parent_id_for_lang()
579            partner._handle_first_contact_creation()
580        return partners
581
582    def _load_records_create(self, vals_list):
583        partners = super(Partner, self.with_context(_partners_skip_fields_sync=True))._load_records_create(vals_list)
584
585        # batch up first part of _fields_sync
586        # group partners by commercial_partner_id (if not self) and parent_id (if type == contact)
587        groups = collections.defaultdict(list)
588        for partner, vals in zip(partners, vals_list):
589            cp_id = None
590            if vals.get('parent_id') and partner.commercial_partner_id != partner:
591                cp_id = partner.commercial_partner_id.id
592
593            add_id = None
594            if partner.parent_id and partner.type == 'contact':
595                add_id = partner.parent_id.id
596            groups[(cp_id, add_id)].append(partner.id)
597
598        for (cp_id, add_id), children in groups.items():
599            # values from parents (commercial, regular) written to their common children
600            to_write = {}
601            # commercial fields from commercial partner
602            if cp_id:
603                to_write = self.browse(cp_id)._update_fields_values(self._commercial_fields())
604            # address fields from parent
605            if add_id:
606                parent = self.browse(add_id)
607                for f in self._address_fields():
608                    v = parent[f]
609                    if v:
610                        to_write[f] = v.id if isinstance(v, models.BaseModel) else v
611            if to_write:
612                self.browse(children).write(to_write)
613
614        # do the second half of _fields_sync the "normal" way
615        for partner, vals in zip(partners, vals_list):
616            partner._children_sync(vals)
617            partner._handle_first_contact_creation()
618        return partners
619
620    def create_company(self):
621        self.ensure_one()
622        if self.company_name:
623            # Create parent company
624            values = dict(name=self.company_name, is_company=True, vat=self.vat)
625            values.update(self._update_fields_values(self._address_fields()))
626            new_company = self.create(values)
627            # Set new company as my parent
628            self.write({
629                'parent_id': new_company.id,
630                'child_ids': [(1, partner_id, dict(parent_id=new_company.id)) for partner_id in self.child_ids.ids]
631            })
632        return True
633
634    def open_commercial_entity(self):
635        """ Utility method used to add an "Open Company" button in partner views """
636        self.ensure_one()
637        return {'type': 'ir.actions.act_window',
638                'res_model': 'res.partner',
639                'view_mode': 'form',
640                'res_id': self.commercial_partner_id.id,
641                'target': 'current',
642                'flags': {'form': {'action_buttons': True}}}
643
644    def open_parent(self):
645        """ Utility method used to add an "Open Parent" button in partner views """
646        self.ensure_one()
647        address_form_id = self.env.ref('base.view_partner_address_form').id
648        return {'type': 'ir.actions.act_window',
649                'res_model': 'res.partner',
650                'view_mode': 'form',
651                'views': [(address_form_id, 'form')],
652                'res_id': self.parent_id.id,
653                'target': 'new',
654                'flags': {'form': {'action_buttons': True}}}
655
656    def _get_contact_name(self, partner, name):
657        return "%s, %s" % (partner.commercial_company_name or partner.sudo().parent_id.name, name)
658
659    def _get_name(self):
660        """ Utility method to allow name_get to be overrided without re-browse the partner """
661        partner = self
662        name = partner.name or ''
663
664        if partner.company_name or partner.parent_id:
665            if not name and partner.type in ['invoice', 'delivery', 'other']:
666                name = dict(self.fields_get(['type'])['type']['selection'])[partner.type]
667            if not partner.is_company:
668                name = self._get_contact_name(partner, name)
669        if self._context.get('show_address_only'):
670            name = partner._display_address(without_company=True)
671        if self._context.get('show_address'):
672            name = name + "\n" + partner._display_address(without_company=True)
673        name = name.replace('\n\n', '\n')
674        name = name.replace('\n\n', '\n')
675        if self._context.get('address_inline'):
676            splitted_names = name.split("\n")
677            name = ", ".join([n for n in splitted_names if n.strip()])
678        if self._context.get('show_email') and partner.email:
679            name = "%s <%s>" % (name, partner.email)
680        if self._context.get('html_format'):
681            name = name.replace('\n', '<br/>')
682        if self._context.get('show_vat') and partner.vat:
683            name = "%s ‒ %s" % (name, partner.vat)
684        return name
685
686    def name_get(self):
687        res = []
688        for partner in self:
689            name = partner._get_name()
690            res.append((partner.id, name))
691        return res
692
693    def _parse_partner_name(self, text):
694        """ Parse partner name (given by text) in order to find a name and an
695        email. Supported syntax:
696
697          * Raoul <raoul@grosbedon.fr>
698          * "Raoul le Grand" <raoul@grosbedon.fr>
699          * Raoul raoul@grosbedon.fr (strange fault tolerant support from df40926d2a57c101a3e2d221ecfd08fbb4fea30e)
700
701        Otherwise: default, everything is set as the name. Starting from 13.3
702        returned email will be normalized to have a coherent encoding.
703         """
704        name, email = '', ''
705        split_results = tools.email_split_tuples(text)
706        if split_results:
707            name, email = split_results[0]
708
709        if email and not name:
710            fallback_emails = tools.email_split(text.replace(' ', ','))
711            if fallback_emails:
712                email = fallback_emails[0]
713                name = text[:text.index(email)].replace('"', '').replace('<', '').strip()
714
715        if email:
716            email = tools.email_normalize(email)
717        else:
718            name, email = text, ''
719
720        return name, email
721
722    @api.model
723    def name_create(self, name):
724        """ Override of orm's name_create method for partners. The purpose is
725            to handle some basic formats to create partners using the
726            name_create.
727            If only an email address is received and that the regex cannot find
728            a name, the name will have the email value.
729            If 'force_email' key in context: must find the email address. """
730        default_type = self._context.get('default_type')
731        if default_type and default_type not in self._fields['type'].get_values(self.env):
732            context = dict(self._context)
733            context.pop('default_type')
734            self = self.with_context(context)
735        name, email = self._parse_partner_name(name)
736        if self._context.get('force_email') and not email:
737            raise UserError(_("Couldn't create contact without email address!"))
738
739        create_values = {self._rec_name: name or email}
740        if email:  # keep default_email in context
741            create_values['email'] = email
742        partner = self.create(create_values)
743        return partner.name_get()[0]
744
745    @api.model
746    def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None):
747        """ Override search() to always show inactive children when searching via ``child_of`` operator. The ORM will
748        always call search() with a simple domain of the form [('parent_id', 'in', [ids])]. """
749        # a special ``domain`` is set on the ``child_ids`` o2m to bypass this logic, as it uses similar domain expressions
750        if len(args) == 1 and len(args[0]) == 3 and args[0][:2] == ('parent_id','in') \
751                and args[0][2] != [False]:
752            self = self.with_context(active_test=False)
753        return super(Partner, self)._search(args, offset=offset, limit=limit, order=order,
754                                            count=count, access_rights_uid=access_rights_uid)
755
756    def _get_name_search_order_by_fields(self):
757        return ''
758
759    @api.model
760    def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None):
761        self = self.with_user(name_get_uid or self.env.uid)
762        # as the implementation is in SQL, we force the recompute of fields if necessary
763        self.recompute(['display_name'])
764        self.flush()
765        if args is None:
766            args = []
767        order_by_rank = self.env.context.get('res_partner_search_mode')
768        if (name or order_by_rank) and operator in ('=', 'ilike', '=ilike', 'like', '=like'):
769            self.check_access_rights('read')
770            where_query = self._where_calc(args)
771            self._apply_ir_rules(where_query, 'read')
772            from_clause, where_clause, where_clause_params = where_query.get_sql()
773            from_str = from_clause if from_clause else 'res_partner'
774            where_str = where_clause and (" WHERE %s AND " % where_clause) or ' WHERE '
775
776            # search on the name of the contacts and of its company
777            search_name = name
778            if operator in ('ilike', 'like'):
779                search_name = '%%%s%%' % name
780            if operator in ('=ilike', '=like'):
781                operator = operator[1:]
782
783            unaccent = get_unaccent_wrapper(self.env.cr)
784
785            fields = self._get_name_search_order_by_fields()
786
787            query = """SELECT res_partner.id
788                         FROM {from_str}
789                      {where} ({email} {operator} {percent}
790                           OR {display_name} {operator} {percent}
791                           OR {reference} {operator} {percent}
792                           OR {vat} {operator} {percent})
793                           -- don't panic, trust postgres bitmap
794                     ORDER BY {fields} {display_name} {operator} {percent} desc,
795                              {display_name}
796                    """.format(from_str=from_str,
797                               fields=fields,
798                               where=where_str,
799                               operator=operator,
800                               email=unaccent('res_partner.email'),
801                               display_name=unaccent('res_partner.display_name'),
802                               reference=unaccent('res_partner.ref'),
803                               percent=unaccent('%s'),
804                               vat=unaccent('res_partner.vat'),)
805
806            where_clause_params += [search_name]*3  # for email / display_name, reference
807            where_clause_params += [re.sub('[^a-zA-Z0-9\-\.]+', '', search_name) or None]  # for vat
808            where_clause_params += [search_name]  # for order by
809            if limit:
810                query += ' limit %s'
811                where_clause_params.append(limit)
812            self.env.cr.execute(query, where_clause_params)
813            return [row[0] for row in self.env.cr.fetchall()]
814
815        return super(Partner, self)._name_search(name, args, operator=operator, limit=limit, name_get_uid=name_get_uid)
816
817    @api.model
818    @api.returns('self', lambda value: value.id)
819    def find_or_create(self, email, assert_valid_email=False):
820        """ Find a partner with the given ``email`` or use :py:method:`~.name_create`
821        to create a new one.
822
823        :param str email: email-like string, which should contain at least one email,
824            e.g. ``"Raoul Grosbedon <r.g@grosbedon.fr>"``
825        :param boolean assert_valid_email: raise if no valid email is found
826        :return: newly created record
827        """
828        if not email:
829            raise ValueError(_('An email is required for find_or_create to work'))
830
831        parsed_name, parsed_email = self._parse_partner_name(email)
832        if not parsed_email and assert_valid_email:
833            raise ValueError(_('A valid email is required for find_or_create to work properly.'))
834
835        partners = self.search([('email', '=ilike', parsed_email)], limit=1)
836        if partners:
837            return partners
838
839        create_values = {self._rec_name: parsed_name or parsed_email}
840        if parsed_email:  # keep default_email in context
841            create_values['email'] = parsed_email
842        return self.create(create_values)
843
844    def _get_gravatar_image(self, email):
845        email_hash = hashlib.md5(email.lower().encode('utf-8')).hexdigest()
846        url = "https://www.gravatar.com/avatar/" + email_hash
847        try:
848            res = requests.get(url, params={'d': '404', 's': '128'}, timeout=5)
849            if res.status_code != requests.codes.ok:
850                return False
851        except requests.exceptions.ConnectionError as e:
852            return False
853        except requests.exceptions.Timeout as e:
854            return False
855        return base64.b64encode(res.content)
856
857    def _email_send(self, email_from, subject, body, on_error=None):
858        for partner in self.filtered('email'):
859            tools.email_send(email_from, [partner.email], subject, body, on_error)
860        return True
861
862    def address_get(self, adr_pref=None):
863        """ Find contacts/addresses of the right type(s) by doing a depth-first-search
864        through descendants within company boundaries (stop at entities flagged ``is_company``)
865        then continuing the search at the ancestors that are within the same company boundaries.
866        Defaults to partners of type ``'default'`` when the exact type is not found, or to the
867        provided partner itself if no type ``'default'`` is found either. """
868        adr_pref = set(adr_pref or [])
869        if 'contact' not in adr_pref:
870            adr_pref.add('contact')
871        result = {}
872        visited = set()
873        for partner in self:
874            current_partner = partner
875            while current_partner:
876                to_scan = [current_partner]
877                # Scan descendants, DFS
878                while to_scan:
879                    record = to_scan.pop(0)
880                    visited.add(record)
881                    if record.type in adr_pref and not result.get(record.type):
882                        result[record.type] = record.id
883                    if len(result) == len(adr_pref):
884                        return result
885                    to_scan = [c for c in record.child_ids
886                                 if c not in visited
887                                 if not c.is_company] + to_scan
888
889                # Continue scanning at ancestor if current_partner is not a commercial entity
890                if current_partner.is_company or not current_partner.parent_id:
891                    break
892                current_partner = current_partner.parent_id
893
894        # default to type 'contact' or the partner itself
895        default = result.get('contact', self.id or False)
896        for adr_type in adr_pref:
897            result[adr_type] = result.get(adr_type) or default
898        return result
899
900    @api.model
901    def view_header_get(self, view_id, view_type):
902        if self.env.context.get('category_id'):
903            return  _(
904                'Partners: %(category)s',
905                category=self.env['res.partner.category'].browse(self.env.context['category_id']).name,
906            )
907        return super().view_header_get(view_id, view_type)
908
909    @api.model
910    @api.returns('self')
911    def main_partner(self):
912        ''' Return the main partner '''
913        return self.env.ref('base.main_partner')
914
915    @api.model
916    def _get_default_address_format(self):
917        return "%(street)s\n%(street2)s\n%(city)s %(state_code)s %(zip)s\n%(country_name)s"
918
919    @api.model
920    def _get_address_format(self):
921        return self.country_id.address_format or self._get_default_address_format()
922
923    def _display_address(self, without_company=False):
924
925        '''
926        The purpose of this function is to build and return an address formatted accordingly to the
927        standards of the country where it belongs.
928
929        :param address: browse record of the res.partner to format
930        :returns: the address formatted in a display that fit its country habits (or the default ones
931            if not country is specified)
932        :rtype: string
933        '''
934        # get the information that will be injected into the display format
935        # get the address format
936        address_format = self._get_address_format()
937        args = {
938            'state_code': self.state_id.code or '',
939            'state_name': self.state_id.name or '',
940            'country_code': self.country_id.code or '',
941            'country_name': self._get_country_name(),
942            'company_name': self.commercial_company_name or '',
943        }
944        for field in self._formatting_address_fields():
945            args[field] = getattr(self, field) or ''
946        if without_company:
947            args['company_name'] = ''
948        elif self.commercial_company_name:
949            address_format = '%(company_name)s\n' + address_format
950        return address_format % args
951
952    def _display_address_depends(self):
953        # field dependencies of method _display_address()
954        return self._formatting_address_fields() + [
955            'country_id.address_format', 'country_id.code', 'country_id.name',
956            'company_name', 'state_id.code', 'state_id.name',
957        ]
958
959    @api.model
960    def get_import_templates(self):
961        return [{
962            'label': _('Import Template for Customers'),
963            'template': '/base/static/xls/res_partner.xls'
964        }]
965
966    @api.model
967    def _check_import_consistency(self, vals_list):
968        """
969        The values created by an import are generated by a name search, field by field.
970        As a result there is no check that the field values are consistent with each others.
971        We check that if the state is given a value, it does belong to the given country, or we remove it.
972        """
973        States = self.env['res.country.state']
974        states_ids = {vals['state_id'] for vals in vals_list if vals.get('state_id')}
975        state_to_country = States.search([('id', 'in', list(states_ids))]).read(['country_id'])
976        for vals in vals_list:
977            if vals.get('state_id'):
978                country_id = next(c['country_id'][0] for c in state_to_country if c['id'] == vals.get('state_id'))
979                state = States.browse(vals['state_id'])
980                if state.country_id.id != country_id:
981                    state_domain = [('code', '=', state.code),
982                                    ('country_id', '=', country_id)]
983                    state = States.search(state_domain, limit=1)
984                    vals['state_id'] = state.id  # replace state or remove it if not found
985
986    def _get_country_name(self):
987        return self.country_id.name or ''
988
989
990class ResPartnerIndustry(models.Model):
991    _description = 'Industry'
992    _name = "res.partner.industry"
993    _order = "name"
994
995    name = fields.Char('Name', translate=True)
996    full_name = fields.Char('Full Name', translate=True)
997    active = fields.Boolean('Active', default=True)
998