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