Files
tryton/modules/party/party.py
T

1177 lines
40 KiB
Python
Raw Normal View History

2014-12-02 12:55:24 +01:00
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
2023-09-30 16:14:33 +02:00
import logging
2015-07-28 07:56:09 +02:00
import stdnum.exceptions
2023-01-19 11:25:25 +01:00
from sql import Column, Literal
2021-03-24 21:37:48 +01:00
from sql.aggregate import Min
from sql.functions import CharLength
2021-12-11 17:58:42 +01:00
from stdnum import get_cc_module
2023-09-30 16:14:33 +02:00
from stdnum.eu.vat import MEMBER_STATES as EU_MEMBER_STATES
2021-12-11 17:58:42 +01:00
from trytond.i18n import gettext
from trytond.model import (
DeactivableMixin, Index, ModelSQL, ModelView, MultiValueMixin, Unique,
2023-03-23 00:33:22 +01:00
ValueMixin, convert_from, fields, sequence_ordered)
2021-12-11 17:58:42 +01:00
from trytond.model.exceptions import AccessError
from trytond.pool import Pool
from trytond.pyson import Bool, Eval
from trytond.tools import is_full_text, lstrip_wildcard
from trytond.transaction import Transaction, inactive_records
2021-12-11 17:58:42 +01:00
from trytond.wizard import Button, StateTransition, StateView, Wizard
2008-09-21 19:07:19 +02:00
2022-10-01 14:52:06 +02:00
from .contact_mechanism import _PHONE_TYPES, _ContactMechanismMixin
2021-12-11 17:58:42 +01:00
from .exceptions import (
EraseError, InvalidIdentifierCode, SimilarityWarning, VIESUnavailable)
2023-09-30 16:14:33 +02:00
logger = logging.getLogger(__name__)
2008-02-11 22:05:04 +01:00
2022-10-01 14:52:06 +02:00
class Party(
DeactivableMixin, _ContactMechanismMixin, ModelSQL, ModelView,
MultiValueMixin):
2008-05-21 18:12:20 +02:00
"Party"
2012-09-24 22:10:20 +02:00
__name__ = 'party.party'
2008-03-04 15:43:44 +01:00
_contact_mechanism_states = {
'readonly': Eval('id', -1) >= 0,
}
name = fields.Char(
"Name", strip=False,
2017-03-16 18:32:07 +01:00
help="The main identifier of the party.")
code = fields.Char(
"Code", required=True,
2011-11-29 11:33:59 +01:00
states={
'readonly': Eval('code_readonly', True),
},
2017-03-16 18:32:07 +01:00
help="The unique identifier of the party.")
2011-11-29 11:33:59 +01:00
code_readonly = fields.Function(fields.Boolean('Code Readonly'),
'get_code_readonly')
2017-04-01 00:31:21 +02:00
lang = fields.MultiValue(
fields.Many2One('ir.lang', "Language",
2017-03-16 18:32:07 +01:00
help="Used to translate communications with the party."))
2017-04-01 00:31:21 +02:00
langs = fields.One2Many(
'party.party.lang', 'party', "Languages")
identifiers = fields.One2Many(
'party.identifier', 'party', "Identifiers",
2017-03-16 18:32:07 +01:00
help="Add other identifiers of the party.")
2016-10-31 17:30:23 +01:00
tax_identifier = fields.Function(fields.Many2One(
2017-03-16 18:32:07 +01:00
'party.identifier', 'Tax Identifier',
help="The identifier used for tax report."),
2016-10-31 17:30:23 +01:00
'get_tax_identifier', searcher='search_tax_identifier')
addresses = fields.One2Many('party.address', 'party', "Addresses")
contact_mechanisms = fields.One2Many(
'party.contact_mechanism', 'party', "Contact Mechanisms")
categories = fields.Many2Many(
'party.party-party.category', 'party', 'category', "Categories",
2017-03-16 18:32:07 +01:00
help="The categories the party belongs to.")
2016-10-02 15:18:38 +02:00
replaced_by = fields.Many2One('party.party', "Replaced By", readonly=True,
states={
'invisible': ~Eval('replaced_by'),
2017-03-16 18:32:07 +01:00
},
help="The party replacing this one.")
2010-03-11 16:48:37 +01:00
full_name = fields.Function(fields.Char('Full Name'), 'get_full_name')
phone = fields.Function(
fields.Char("Phone", states=_contact_mechanism_states),
'get_contact_mechanism', setter='set_contact_mechanism')
mobile = fields.Function(
fields.Char("Mobile", states=_contact_mechanism_states),
'get_contact_mechanism', setter='set_contact_mechanism')
fax = fields.Function(
fields.Char("Fax", states=_contact_mechanism_states),
'get_contact_mechanism', setter='set_contact_mechanism')
email = fields.Function(
fields.Char("E-Mail", states=_contact_mechanism_states),
'get_contact_mechanism', setter='set_contact_mechanism')
website = fields.Function(
fields.Char("Website", states=_contact_mechanism_states),
'get_contact_mechanism', setter='set_contact_mechanism')
2021-03-24 21:37:48 +01:00
distance = fields.Function(fields.Integer('Distance'), 'get_distance')
2008-03-04 11:50:22 +01:00
del _contact_mechanism_states
2012-09-24 22:10:20 +02:00
@classmethod
def __setup__(cls):
cls.code.search_unaccented = False
2012-09-24 22:10:20 +02:00
super(Party, cls).__setup__()
t = cls.__table__()
2012-09-24 22:10:20 +02:00
cls._sql_constraints = [
2018-12-29 14:20:29 +01:00
('code_uniq', Unique(t, t.code), 'party.msg_party_code_unique')
]
cls._sql_indexes.update({
Index(t, (t.code, Index.Equality())),
Index(t, (t.code, Index.Similarity())),
})
2021-03-24 21:37:48 +01:00
cls._order.insert(0, ('distance', 'ASC NULLS LAST'))
cls._order.insert(1, ('name', 'ASC'))
2018-03-26 14:10:40 +02:00
cls.active.states.update({
'readonly': Bool(Eval('replaced_by')),
})
2013-09-06 22:34:20 +02:00
@staticmethod
def order_code(tables):
table, _ = tables[None]
return [CharLength(table.code), table.code]
2013-09-06 22:34:20 +02:00
2012-09-24 22:10:20 +02:00
@staticmethod
def default_categories():
2010-08-10 11:59:01 -04:00
return Transaction().context.get('categories', [])
2012-09-24 22:10:20 +02:00
@staticmethod
def default_addresses():
if Transaction().user == 0:
return []
2016-10-05 22:46:20 +02:00
return [{}]
2010-06-20 22:33:35 +02:00
2017-04-01 00:31:21 +02:00
@classmethod
def default_lang(cls, **pattern):
2012-09-24 22:10:20 +02:00
Configuration = Pool().get('party.configuration')
config = Configuration(1)
2017-04-01 00:31:21 +02:00
lang = config.get_multivalue('party_lang', **pattern)
return lang.id if lang else None
@classmethod
def default_code_readonly(cls, **pattern):
Configuration = Pool().get('party.configuration')
config = Configuration(1)
return bool(config.get_multivalue('party_sequence', **pattern))
2011-11-29 11:33:59 +01:00
2012-09-24 22:10:20 +02:00
def get_code_readonly(self, name):
return True
2011-11-29 11:33:59 +01:00
2015-07-28 07:56:09 +02:00
@classmethod
2019-03-02 22:56:01 +01:00
def tax_identifier_types(cls):
return TAX_IDENTIFIER_TYPES
2008-02-26 16:23:30 +01:00
2016-10-31 17:30:23 +01:00
def get_tax_identifier(self, name):
2019-03-02 22:56:01 +01:00
types = self.tax_identifier_types()
2015-07-28 07:56:09 +02:00
for identifier in self.identifiers:
if identifier.type in types:
2016-10-31 17:30:23 +01:00
return identifier.id
2015-01-12 22:53:00 +01:00
2012-09-24 22:10:20 +02:00
@classmethod
2016-10-31 17:30:23 +01:00
def search_tax_identifier(cls, name, clause):
2016-07-11 18:26:02 +02:00
_, operator, value = clause
nested = clause[0][len(name) + 1:]
2019-03-02 22:56:01 +01:00
types = cls.tax_identifier_types()
2016-07-11 18:26:02 +02:00
domain = [
('identifiers', 'where', [
(nested or 'rec_name', operator, value),
2016-10-31 17:30:23 +01:00
('type', 'in', types),
2016-07-11 18:26:02 +02:00
]),
2015-07-28 07:56:09 +02:00
]
2016-10-31 17:30:23 +01:00
# Add party without tax identifier
2016-07-11 18:26:02 +02:00
if ((operator == '=' and value is None)
or (operator == 'in' and None in value)):
domain = ['OR',
domain, [
('identifiers', 'not where', [
2016-10-31 17:30:23 +01:00
('type', 'in', types),
2016-07-11 18:26:02 +02:00
]),
],
]
return domain
2009-01-04 15:33:00 +01:00
2012-09-24 22:10:20 +02:00
def get_full_name(self, name):
return self.name
2008-10-22 20:07:32 +02:00
def get_contact_mechanism(self, name):
2012-09-24 22:10:20 +02:00
for mechanism in self.contact_mechanisms:
if mechanism.type == name:
return mechanism.value
return ''
2008-10-23 13:41:06 +02:00
@classmethod
def set_contact_mechanism(cls, parties, name, value):
pool = Pool()
ContactMechanism = pool.get('party.contact_mechanism')
contact_mechanisms = []
for party in parties:
if getattr(party, name):
type_string = cls.fields_get([name])[name]['string']
raise AccessError(gettext(
'party.msg_party_set_contact_mechanism',
party=party.rec_name,
field=type_string))
if value:
contact_mechanisms.append(ContactMechanism(
party=party,
type=name,
value=value))
ContactMechanism.save(contact_mechanisms)
2021-03-24 21:37:48 +01:00
@classmethod
def _distance_query(cls, usages=None, party=None, depth=None):
context = Transaction().context
if party is None:
party = context.get('related_party')
if not party:
return
table = cls.__table__()
return table.select(
table.id.as_('to'),
Literal(0).as_('distance'),
where=(table.id == party))
@classmethod
def get_distance(cls, parties, name):
distances = {p.id: None for p in parties}
query = cls._distance_query()
if query:
cursor = Transaction().connection.cursor()
cursor.execute(*query.select(
query.to.as_('to'),
Min(query.distance).as_('distance'),
group_by=[query.to]))
distances.update(cursor)
return distances
@classmethod
def order_distance(cls, tables):
party, _ = tables[None]
key = 'distance'
if key not in tables:
query = cls._distance_query()
if not query:
return []
query = query.select(
query.to.as_('to'),
Min(query.distance).as_('distance'),
group_by=[query.to])
join = party.join(query, type_='LEFT',
condition=query.to == party.id)
tables[key] = {
None: (join.right, join.condition),
}
else:
query, _ = tables[key][None]
return [query.distance]
@classmethod
def index_set_field(cls, name):
index = super().index_set_field(name)
if name in _PHONE_TYPES:
# Phone validation may need the address country
index = cls.index_set_field('addresses') + 1
return index
2012-09-24 22:10:20 +02:00
@classmethod
2017-04-01 00:31:21 +02:00
def _new_code(cls, **pattern):
pool = Pool()
Configuration = pool.get('party.configuration')
config = Configuration(1)
sequence = config.get_multivalue('party_sequence', **pattern)
if sequence:
2021-03-11 23:00:10 +01:00
return sequence.get()
2017-04-01 00:31:21 +02:00
@classmethod
def create(cls, vlist):
2013-01-06 22:18:13 +01:00
vlist = [x.copy() for x in vlist]
for values in vlist:
if not values.get('code'):
2017-04-01 00:31:21 +02:00
values['code'] = cls._new_code()
2013-10-27 19:33:45 +01:00
values.setdefault('addresses', None)
2013-01-06 22:18:13 +01:00
return super(Party, cls).create(vlist)
2012-09-24 22:10:20 +02:00
@classmethod
def copy(cls, parties, default=None):
if default is None:
default = {}
2018-09-01 01:02:03 +02:00
else:
default = default.copy()
default.setdefault('code', None)
2013-04-16 15:19:25 +02:00
return super(Party, cls).copy(parties, default=default)
2008-05-07 18:59:25 +02:00
2014-02-20 23:28:21 +01:00
@classmethod
def search_global(cls, text):
2015-02-04 13:38:14 +01:00
for record, rec_name, icon in super(Party, cls).search_global(text):
2014-02-20 23:28:21 +01:00
icon = icon or 'tryton-party'
2015-02-04 13:38:14 +01:00
yield record, rec_name, icon
2014-02-20 23:28:21 +01:00
2015-11-17 14:41:16 -03:00
def get_rec_name(self, name):
if not self.name:
return '[' + self.code + ']'
return self.name
2012-09-24 22:10:20 +02:00
@classmethod
def search_rec_name(cls, name, clause):
_, operator, operand, *extra = clause
if operator.startswith('!') or operator.startswith('not '):
2015-04-05 23:32:49 +02:00
bool_op = 'AND'
else:
bool_op = 'OR'
code_value = operand
if operator.endswith('like') and is_full_text(operand):
code_value = lstrip_wildcard(operand)
2015-04-05 23:32:49 +02:00
return [bool_op,
('code', operator, code_value, *extra),
('identifiers.code', operator, code_value, *extra),
('name', operator, operand, *extra),
('contact_mechanisms.rec_name', operator, operand, *extra),
]
2008-07-28 18:29:20 +02:00
2012-09-24 22:10:20 +02:00
def address_get(self, type=None):
"""
2012-09-24 22:10:20 +02:00
Try to find an address for the given type, if no type matches
the first address is returned.
"""
2018-02-11 14:53:15 +01:00
default_address = None
if self.addresses:
default_address = self.addresses[0]
if type:
for address in self.addresses:
if getattr(address, type):
return address
2008-03-06 01:07:08 +01:00
return default_address
2008-03-04 15:43:44 +01:00
2023-09-30 16:14:33 +02:00
@classmethod
def autocomplete(cls, text, domain=None, limit=None, order=None):
pool = Pool()
Configuration = pool.get('party.configuration')
configuration = Configuration(1)
result = super().autocomplete(
text, domain=domain, limit=limit, order=order)
identifier_types = dict(configuration.get_identifier_types())
def add(type, code):
result.append({
'id': None,
'name': code,
'defaults': {
'identifiers': [{
'type': type,
'code': code,
}],
},
})
eu_vat = get_cc_module('eu', 'vat')
if 'eu_vat' in identifier_types and eu_vat.is_valid(text):
code = eu_vat.compact(text)
add('eu_vat', code)
else:
for country in eu_vat.guess_country(text):
if 'eu_vat' in identifier_types:
code = eu_vat.compact(country + text)
add('eu_vat', code)
elif f'{country}_vat' in identifier_types:
vat = get_cc_module(country, 'vat')
code = vat.compact(text)
add(f'{country}_vat', code)
return result
@classmethod
def default_get(cls, fields_names, with_rec_name=True):
pool = Pool()
Country = pool.get('country.country')
values = super().default_get(fields_names, with_rec_name=with_rec_name)
eu_vat = get_cc_module('eu', 'vat')
eu_types = {'eu_vat'} | {f'{m}_vat' for m in EU_MEMBER_STATES}
for identifier in values.get('identifiers') or []:
if identifier.get('type') in eu_types and identifier.get('code'):
code = identifier['code']
if identifier['type'] != 'eu_vat':
code = identifier['type'][2:] + code
try:
result = eu_vat.check_vies(code)
except Exception:
logger.debug(
f"Fail to check {identifier['code']}", exc_info=True)
continue
if result['valid']:
values['name'] = result['name']
for address in values.get('addresses') or []:
if not address:
break
else:
address = {}
values.setdefault('addresses', []).insert(0, address)
address['street'] = result['address']
country_code = code[:2]
countries = Country.search([
('code', 'ilike', country_code),
], limit=1)
if countries:
country, = countries
address['country'] = country.id
if with_rec_name:
address.setdefault('country.', {})['rec_name'] = (
country.rec_name)
break
return values
2009-03-05 14:52:31 +01:00
2017-04-01 00:31:21 +02:00
class PartyLang(ModelSQL, ValueMixin):
"Party Lang"
__name__ = 'party.party.lang'
party = fields.Many2One(
'party.party', "Party", ondelete='CASCADE')
2017-04-01 00:31:21 +02:00
lang = fields.Many2One('ir.lang', "Language")
2009-03-05 14:52:31 +01:00
class PartyCategory(ModelSQL):
'Party - Category'
2012-09-24 22:10:20 +02:00
__name__ = 'party.party-party.category'
2009-03-05 14:52:31 +01:00
_table = 'party_category_rel'
party = fields.Many2One(
'party.party', "Party", ondelete='CASCADE', required=True)
category = fields.Many2One(
'party.category', "Category", ondelete='CASCADE', required=True)
2009-03-05 14:52:31 +01:00
2023-09-26 23:46:13 +02:00
IDENTIFIER_VAT = [
'ad_nrt',
'al_nipt',
'ar_cuit',
'at_uid',
'au_abn',
'br_cnpj',
'by_unp',
'ca_bn',
'cl_rut',
'cn_uscc',
'co_nit',
'cr_cpj',
'cz_dic',
'dk_cvr',
'do_rnc',
'dz_nif',
'ec_ruc',
'ee_kmkr',
'eg_tn',
'es_nif',
'fi_alv',
'fo_vn',
'fr_tva',
'gh_tin',
'gn_nifp',
'gt_nit',
'hr_oib',
'hu_anum',
'id_npwp',
'il_hp',
'in_gstin',
'is_vsk',
'it_iva',
'jp_cn',
'ke_pin',
'kr_brn',
'lt_pvm',
'lu_tva',
'lv_pvn',
'ma_ice',
'mc_tva',
'me_pib',
'mk_edb',
'mx_rfc',
'nl_btw',
'no_mva',
'nz_ird',
'pe_ruc',
'pl_nip',
'pt_nif',
'py_ruc',
'ro_cf',
'rs_pib',
'ru_inn',
'sg_uen',
'si_ddv',
'sk_dph',
'sm_coe',
'sv_nit',
'tn_mf',
'tr_vkn',
'tw_ubn',
'uy_rut',
've_rif',
'vn_mst',
]
def replace_vat(code):
if code in IDENTIFIER_VAT:
code = code.split('_', 1)[0] + '_vat'
return code
2021-03-18 22:36:31 +01:00
IDENTIFIER_TYPES = [
('ad_nrt', "Andorra Tax Number"),
('al_nipt', "Albanian VAT Number"),
('ar_cuit', "Argentinian Tax Number"),
('ar_dni', "Argentinian National Identity Number"),
('at_businessid', "Austrian Company Register"),
('at_tin', "Austrian Tax Identification"),
2021-08-13 11:21:20 +02:00
('at_uid', "Austrian Umsatzsteuer-Identifikationsnummer"),
2021-03-18 22:36:31 +01:00
('at_vnr', "Austrian Social Security Number"),
('au_abn', "Australian Business Number"),
('au_acn', "Australian Company Number"),
('au_tfn', "Australian Tax File Number"),
2023-09-10 10:11:16 +00:00
('be_bis', "Belgian BIS Number"),
('be_nn', "Belgian National Number"),
2021-03-18 22:36:31 +01:00
('be_vat', "Belgian Enterprise Number"),
('bg_egn', "Bulgarian Personal Identity Codes"),
('bg_pnf', "Bulgarian Number of a Foreigner"),
('bg_vat', "Bulgarian VAT Number"),
('br_cnpj', "Brazillian Company Identifier"),
('br_cpf', "Brazillian National Identifier"),
('by_unp', "Belarus VAT Number"),
('ca_bn', "Canadian Business Number"),
('ca_sin', "Canadian Social Insurance Number"),
('ch_ssn', "Swiss Social Security Number"),
('ch_uid', "Swiss Business Identifier"),
('ch_vat', "Swiss VAT Number"),
('cl_rut', "Chilean National Tax Number"),
('cn_ric', "Chinese Resident Identity Card Number"),
('cn_uscc', "Chinese Unified Social Credit Code"),
('co_nit', "Colombian Identity Code"),
('co_rut', "Colombian Business Tax Number"),
('cr_cpf', "Costa Rica Physical Person ID Number"),
('cr_cpj', "Costa Rica Tax Number"),
('cr_cr', "Costa Rica Foreigners ID Number"),
('cu_ni', "Cuban Identity Card Number"),
('cy_vat', "Cypriot VAT Number"),
('cz_dic', "Czech VAT Number"),
('cz_rc', "Czech National Identifier"),
('de_handelsregisternummer', "German Company Register Number"),
('de_idnr', "German Personal Tax Number"),
('de_stnr', "German Tax Number"),
('de_vat', "German VAT Number"),
('dk_cpr', "Danish Citizen Number"),
('dk_cvr', "Danish VAT Number"),
('do_cedula', "Dominican Republic National Identification Number"),
('do_rnc', "Dominican Republic Tax"),
('dz_nif', "Algerian Tax Number"),
2021-03-18 22:36:31 +01:00
('ec_ci', "Ecuadorian Personal Identity Code"),
('ec_ruc', "Ecuadorian Tax Identification"),
('ee_ik', "Estonian Personal ID Number"),
('ee_kmkr', "Estonian VAT Number"),
('ee_registrikood', "Estonian Organisation Registration Code"),
2023-09-10 10:11:16 +00:00
('eg_tn', "Egyptian Tax Registration Number"),
2021-03-18 22:36:31 +01:00
('es_cif', "Spanish Company Tax"),
('es_dni', "Spanish Personal Identity Codes"),
('es_nie', "Spanish Foreigner Number"),
('es_nif', "Spanish VAT Number"),
('eu_at_02', "SEPA Identifier of the Creditor (AT-02)"),
2023-09-10 10:11:16 +00:00
('eu_oss', "European VAT on e-Commerce - One Stop Shop"),
2021-03-18 22:36:31 +01:00
('eu_vat', "European VAT Number"),
('fi_alv', "Finnish VAT Number"),
('fi_associationid', "Finnish Association Identifier"),
('fi_hetu', "Finnish Personal Identity Code"),
('fi_veronumero', "Finnish Individual Tax Number"),
('fi_ytunnus', "Finnish Business Identifier"),
('fo_vn', "Faroese Tax Number"),
2021-03-18 22:36:31 +01:00
('fr_nif', "French Tax Identification Number"),
('fr_nir', "French Personal Identification Number"),
('fr_siren', "French Company Identification Number"),
('fr_siret', "French Company Establishment Identification Number"),
2021-03-18 22:36:31 +01:00
('fr_tva', "French VAT Number"),
('gb_nhs',
"United Kingdom National Health Service Patient Identifier"),
('gb_upn', "English Unique Pupil Number"),
('gb_vat', "United Kingdom (and Isle of Man) VAT Number"),
('gh_tin', "Ghanaian Taxpayer Identification Number"),
2023-09-10 10:11:16 +00:00
('gn_nifp', "Guinean Tax Number"),
2021-03-18 22:36:31 +01:00
('gr_amka', "Greek Social Security Number"),
('gr_vat', "Greek VAT Number"),
('gt_nit', "Guatemala Tax Number"),
('hr_oib', "Croatian Identification Number"),
('hu_anum', "Hungarian VAT Number"),
('id_npwp', "Indonesian VAT Number"),
('ie_pps', "Irish Personal Number"),
('ie_vat', "Irish VAT Number"),
('il_hp', "Israeli Company Number"),
('il_idnr', "Israeli Identity Number"),
('in_aadhaar', "Indian Digital Resident Personal Identity Number"),
('in_epic', "Indian Voter ID"),
('in_gstin', "Indian VAT number"),
2021-03-18 22:36:31 +01:00
('in_pan', "Indian Income Tax Identifier"),
('is_kennitala',
"Icelandic Personal and Organisation Identity Code"),
('is_vsk', "Icelandic VAT Number"),
('it_codicefiscale', "Italian Tax Code for Individuals"),
('it_iva', "Italian VAT Number"),
('jp_cn', "Japanese Corporate Number"),
('kr_brn', "South Korea Business Registration Number"),
('kr_krn', "South Korean Resident Registration Number"),
('lt_asmens', "Lithuanian Personal Number"),
('lt_pvm', "Lithuanian VAT Number"),
('lu_tva', "Luxembourgian VAT Number"),
('lv_pvn', "Latvian VAT Number"),
('ma_ice', "Moroccan Tax Number"),
2021-03-18 22:36:31 +01:00
('mc_tva', "Monacan VAT Number"),
('md_idno', "Moldavian Company Identification Number"),
('mk_edb', "Macedonian Tax Number"),
2021-03-18 22:36:31 +01:00
('mt_vat', "Maltese VAT Number"),
('mu_nid', "Mauritian National Identifier"),
('mx_rfc', "Mexican Tax Number"),
('my_nric',
"Malaysian National Registration Identity Card Number"),
('nl_brin', "Dutch School Identification Number"),
('nl_bsn', "Dutch Citizen Identification Number"),
('nl_btw', "Dutch VAT Number"),
('nl_onderwijsnummer', "Dutch Student Identification Number"),
('no_fodselsnummer',
"Norwegian Birth Number, the National Identity Number"),
('no_mva', "Norwegian VAT Number"),
('no_orgnr', "Norwegian Organisation Number"),
('nz_ird', "New Zealand Inland Revenue Department Number"),
('pe_cui', "Peruvian Identity Number"),
('pe_ruc', "Peruvian Company Tax Number"),
('pk_cnic', "Pakistani Computerised National Identity Card Number"),
2021-03-18 22:36:31 +01:00
('pl_nip', "Polish VAT Number"),
('pl_pesel', "Polish National Identification Number"),
('pl_regon', "Polish Register of Economic Units"),
('pt_cc', "Portuguese Identity Number"),
2021-03-18 22:36:31 +01:00
('pt_nif', "Portuguese VAT Number"),
('py_ruc', "Paraguay Tax Number"),
('ro_cf', "Romanian VAT Number"),
('ro_cnp', "Romanian Numerical Personal Code"),
('ro_onrc', "Romanian ONRC Number"),
('rs_pib', "Serbian Tax Identification"),
('ru_inn', "Russian Tax identifier"),
('se_orgnr', "Swedish Company Number"),
('se_personnummer', "Swedish Personal Number"),
('se_vat', "Swedish VAT Number"),
2023-09-10 10:11:16 +00:00
('si_businessid', "Slovenian Corporate Registration Number"),
2021-03-18 22:36:31 +01:00
('si_ddv', "Slovenian VAT Number"),
('si_emso', "Slovenian Unique Master Citizen Number"),
2021-03-18 22:36:31 +01:00
('sk_dph', "Slovak VAT Number"),
('sk_rc', "Slovak Birth Number"),
('sm_coe', "San Marino National Tax Number"),
('th_moa', "Thai Memorandum of Association Number"),
('th_pin', "Thai Personal Identification Number"),
('th_tin', "Thai Taxpayer Identification Number"),
('tn_mf', "Tunisian Tax Number"),
2021-03-18 22:36:31 +01:00
('tr_tckimlik', "Turkish Personal Identification Number"),
2021-08-13 11:21:20 +02:00
('ua_edrpou', "Ukrainian Identifier for Enterprises and Organizations"),
('ua_rntrc', "Ukrainian Individual Taxpayer Registration Number"),
2021-03-18 22:36:31 +01:00
('us_atin', "U.S. Adoption Taxpayer Identification Number"),
('us_ein', "U.S. Employer Identification Number"),
('us_itin', "U.S. Individual Taxpayer Identification Number"),
('us_ptin', "U.S. Preparer Tax Identification Number"),
('us_ssn', "U.S. Social Security Number"),
('us_tin', "U.S. Taxpayer Identification Number"),
('uy_ruc', "Uruguay Tax Number"),
('ve_rif', "Venezuelan VAT Number"),
2021-08-13 11:21:20 +02:00
('vn_mst', "Vietnam Tax Number"),
2021-03-18 22:36:31 +01:00
('za_idnr', "South African Identity Document Number"),
('za_tin', "South African Tax Identification Number"),
]
2023-09-26 23:46:13 +02:00
IDENTIFIER_TYPES = list(map(
lambda x: (replace_vat(x[0]), x[1]),
IDENTIFIER_TYPES))
TAX_IDENTIFIER_TYPES = [
'ad_nrt',
'al_nipt',
'ar_cuit',
'at_uid',
2021-08-17 10:06:56 +02:00
'au_abn',
'au_acn',
'be_vat',
'bg_vat',
'by_unp',
'ch_vat',
'cl_rut',
'cn_uscc',
'co_rut',
'cr_cpj',
'cz_dic',
'de_vat',
'dk_cvr',
'do_rnc',
'dz_nif',
'ec_ruc',
'ee_kmkr',
2023-09-10 10:11:16 +00:00
'eg_tn',
'es_nif',
'eu_vat',
'fi_alv',
'fo_vn',
'fr_tva',
'gb_vat',
'gh_tin',
2023-09-10 10:11:16 +00:00
'gn_nifp',
'gr_vat',
'gt_nit',
'hu_anum',
'id_npwp',
'ie_vat',
'il_hp',
'in_gstin',
'is_vsk',
'it_iva',
'jp_cn',
'kr_brn',
'lt_pvm',
'lu_tva',
'lv_pvn',
'ma_ice',
'mc_tva',
'md_idno',
'mk_edb',
'mt_vat',
'mx_rfc',
'nl_btw',
'no_mva',
'nz_ird',
'pe_ruc',
'pl_nip',
'pt_nif',
'py_ruc',
'ro_cf',
'rs_pib',
'ru_inn',
'se_vat',
'si_ddv',
'sk_dph',
'sm_coe',
'th_tin',
'tn_mf',
'ua_edrpou',
'ua_rntrc',
'us_atin',
'us_ein',
'us_itin',
'us_ptin',
'us_ssn',
'us_tin',
'uy_ruc',
've_rif',
'vn_mst',
'za_tin',
]
2023-09-26 23:46:13 +02:00
TAX_IDENTIFIER_TYPES = list(map(replace_vat, TAX_IDENTIFIER_TYPES))
2021-03-18 22:36:31 +01:00
class Identifier(sequence_ordered(), DeactivableMixin, ModelSQL, ModelView):
2015-07-28 07:56:09 +02:00
'Party Identifier'
__name__ = 'party.identifier'
_rec_name = 'code'
party = fields.Many2One(
'party.party', "Party", ondelete='CASCADE', required=True,
2017-03-16 18:32:07 +01:00
help="The party identified by this record.")
address = fields.Many2One(
'party.address', "Address", ondelete='CASCADE',
states={
'required': Eval('type_address', False),
'invisible': ~Eval('type_address', True),
},
domain=[
('party', '=', Eval('party', -1)),
],
help="The address identified by this record.")
2021-03-18 22:36:31 +01:00
type = fields.Selection('get_types', 'Type')
2016-10-31 17:28:25 +01:00
type_string = type.translated('type')
type_address = fields.Function(
fields.Boolean("Type of Address"), 'on_change_with_type_address')
2015-07-28 07:56:09 +02:00
code = fields.Char('Code', required=True)
@classmethod
def __register__(cls, module_name):
cursor = Transaction().connection.cursor()
table = cls.__table__()
2015-07-28 07:56:09 +02:00
2020-03-01 12:33:52 +01:00
super().__register__(module_name)
2015-07-28 07:56:09 +02:00
# Migration from 5.8: Rename cn_rit into cn_ric
cursor.execute(*table.update([table.type], ['cn_ric'],
where=(table.type == 'cn_rit')))
2023-09-26 23:46:13 +02:00
# Migration from 6.8: Use vat alias
for old in IDENTIFIER_VAT:
new = replace_vat(old)
cursor.execute(*table.update(
[table.type], [new],
where=table.type == old))
2021-03-18 22:36:31 +01:00
@classmethod
def get_types(cls):
pool = Pool()
Configuration = pool.get('party.configuration')
configuration = Configuration(1)
return [(None, '')] + configuration.get_identifier_types()
@classmethod
def _type_addresses(cls):
return {'fr_siret'}
@fields.depends('address', '_parent_address.party')
def on_change_address(self):
if self.address:
self.party = self.address.party
@fields.depends('type')
def on_change_with_type_address(self, name=None):
return self.type in self._type_addresses()
2015-07-28 07:56:09 +02:00
@fields.depends('type', 'code')
def on_change_with_code(self):
2019-03-02 22:56:01 +01:00
if self.type and '_' in self.type:
module = get_cc_module(*self.type.split('_', 1))
if module:
try:
return module.compact(self.code)
except stdnum.exceptions.ValidationError:
pass
2015-07-28 07:56:09 +02:00
return self.code
2016-03-05 17:27:36 +01:00
def pre_validate(self):
2020-03-01 12:33:52 +01:00
super().pre_validate()
2016-03-05 17:27:36 +01:00
self.check_code()
2015-07-28 07:56:09 +02:00
@fields.depends('type', 'party', 'code')
2015-07-28 07:56:09 +02:00
def check_code(self):
2019-03-02 22:56:01 +01:00
if self.type and '_' in self.type:
module = get_cc_module(*self.type.split('_', 1))
if module:
if not module.is_valid(self.code):
if self.party and self.party.id > 0:
party = self.party.rec_name
else:
party = ''
raise InvalidIdentifierCode(
gettext('party.msg_invalid_code',
type=self.type_string,
code=self.code,
party=party))
2023-01-30 00:11:21 +01:00
@fields.depends(methods=['_notify_duplicate'])
def on_change_notify(self):
notifications = super().on_change_notify()
notifications.extend(self._notify_duplicate())
return notifications
@fields.depends('code', methods=['on_change_with_code'])
def _notify_duplicate(self):
cls = self.__class__
if self.code:
code = self.on_change_with_code()
others = cls.search([
('id', '!=', self.id),
('type', '!=', None),
('code', '=', code),
], limit=1)
if others:
other, = others
yield ('warning', gettext(
'party.msg_party_identifier_duplicate',
party=other.party.rec_name,
type=other.type_string,
code=other.code))
2012-01-02 18:01:35 +01:00
class CheckVIESResult(ModelView):
'Check VIES'
2012-09-24 22:10:20 +02:00
__name__ = 'party.check_vies.result'
parties_succeed = fields.Many2Many('party.party', None, None,
2011-08-21 11:20:44 +02:00
'Parties Succeed', readonly=True, states={
'invisible': ~Eval('parties_succeed'),
})
parties_failed = fields.Many2Many('party.party', None, None,
2011-08-21 11:20:44 +02:00
'Parties Failed', readonly=True, states={
'invisible': ~Eval('parties_failed'),
})
class CheckVIES(Wizard):
'Check VIES'
2012-09-24 22:10:20 +02:00
__name__ = 'party.check_vies'
2012-01-02 18:01:35 +01:00
start_state = 'check'
check = StateTransition()
result = StateView('party.check_vies.result',
'party.check_vies_result', [
2015-03-10 18:41:24 +01:00
Button('OK', 'end', 'tryton-ok', True),
2012-01-02 18:01:35 +01:00
])
2012-09-24 22:10:20 +02:00
def transition_check(self):
2012-01-02 18:01:35 +01:00
parties_succeed = []
parties_failed = []
for party in self.records:
2015-07-28 07:56:09 +02:00
for identifier in party.identifiers:
if identifier.type != 'eu_vat':
continue
2019-03-02 22:56:01 +01:00
eu_vat = get_cc_module('eu', 'vat')
2015-07-28 07:56:09 +02:00
try:
2019-03-02 22:56:01 +01:00
if not eu_vat.check_vies(identifier.code)['valid']:
2012-01-02 18:01:35 +01:00
parties_failed.append(party.id)
2015-07-28 07:56:09 +02:00
else:
parties_succeed.append(party.id)
2018-06-26 19:11:48 +02:00
except Exception as e:
2015-07-28 07:56:09 +02:00
if hasattr(e, 'faultstring') \
and hasattr(e.faultstring, 'find'):
if e.faultstring.find('INVALID_INPUT'):
parties_failed.append(party.id)
continue
if e.faultstring.find('SERVICE_UNAVAILABLE') \
or e.faultstring.find('MS_UNAVAILABLE') \
or e.faultstring.find('TIMEOUT') \
or e.faultstring.find('SERVER_BUSY'):
2018-12-29 14:20:29 +01:00
raise VIESUnavailable(
gettext('party.msg_vies_unavailable')) from e
2015-07-28 07:56:09 +02:00
raise
2012-09-24 22:10:20 +02:00
self.result.parties_succeed = parties_succeed
self.result.parties_failed = parties_failed
2012-01-02 18:01:35 +01:00
return 'result'
2012-09-24 22:10:20 +02:00
def default_result(self, fields):
2012-01-02 18:01:35 +01:00
return {
2012-09-24 22:10:20 +02:00
'parties_succeed': [p.id for p in self.result.parties_succeed],
'parties_failed': [p.id for p in self.result.parties_failed],
2012-01-02 18:01:35 +01:00
}
2016-10-02 15:18:38 +02:00
2020-03-01 12:33:52 +01:00
class Replace(Wizard):
2016-10-02 15:18:38 +02:00
"Replace Party"
__name__ = 'party.replace'
start_state = 'ask'
ask = StateView('party.replace.ask', 'party.replace_ask_view_form', [
Button("Cancel", 'end', 'tryton-cancel'),
Button("Replace", 'replace', 'tryton-launch', default=True),
2016-10-02 15:18:38 +02:00
])
replace = StateTransition()
def check_similarity(self):
2018-12-29 14:20:29 +01:00
pool = Pool()
Warning = pool.get('res.user.warning')
2016-10-02 15:18:38 +02:00
source = self.ask.source
destination = self.ask.destination
if source.name != destination.name:
key = 'party.replace name %s %s' % (source.id, destination.id)
2018-12-29 14:20:29 +01:00
if Warning.check(key):
raise SimilarityWarning(
key,
gettext('party.msg_different_name',
source_name=source.name,
destination_name=destination.name))
2016-10-31 17:30:23 +01:00
source_code = (source.tax_identifier.code
if source.tax_identifier else '')
destination_code = (destination.tax_identifier.code
if destination.tax_identifier else '')
if source_code != destination_code:
key = 'party.replace tax_identifier %s %s' % (
source.id, destination.id)
2018-12-29 14:20:29 +01:00
if Warning.check(key):
raise SimilarityWarning(
key,
gettext('party.msg_different_tax_identifier',
source_code=source_code,
destination_code=destination_code))
2016-10-02 15:18:38 +02:00
def transition_replace(self):
pool = Pool()
Address = pool.get('party.address')
ContactMechanism = pool.get('party.contact_mechanism')
Identifier = pool.get('party.identifier')
2016-10-02 15:18:38 +02:00
transaction = Transaction()
self.check_similarity()
source = self.ask.source
destination = self.ask.destination
Address.write(list(source.addresses), {
'active': False,
})
ContactMechanism.write(list(source.contact_mechanisms), {
'active': False,
})
Identifier.write(list(source.identifiers), {
'active': False,
})
2016-10-02 15:18:38 +02:00
source.replaced_by = destination
source.active = False
source.save()
cursor = transaction.connection.cursor()
for model_name, field_name in self.fields_to_replace():
Model = pool.get(model_name)
2021-10-12 10:07:14 +02:00
field = getattr(Model, field_name)
2016-10-02 15:18:38 +02:00
table = Model.__table__()
column = Column(table, field_name)
2021-10-12 10:07:14 +02:00
if field._type == 'reference':
source_value = str(source)
destination_value = str(destination)
else:
source_value = source.id
destination_value = destination.id
where = column == source_value
2016-10-02 15:18:38 +02:00
if transaction.database.has_returning():
returning = [table.id]
else:
cursor.execute(*table.select(table.id, where=where))
ids = [x[0] for x in cursor]
returning = None
cursor.execute(*table.update(
[column],
2021-10-12 10:07:14 +02:00
[destination_value],
2016-10-02 15:18:38 +02:00
where=where,
returning=returning))
if transaction.database.has_returning():
ids = [x[0] for x in cursor]
Model._insert_history(ids)
return 'end'
@classmethod
def fields_to_replace(cls):
return [
('party.address', 'party'),
('party.contact_mechanism', 'party'),
('party.identifier', 'party'),
2016-10-02 15:18:38 +02:00
]
2020-03-01 12:33:52 +01:00
class ReplaceAsk(ModelView):
2016-10-02 15:18:38 +02:00
"Replace Party"
__name__ = 'party.replace.ask'
2017-03-16 18:32:07 +01:00
source = fields.Many2One('party.party', "Source", required=True,
help="The party to be replaced.")
2016-10-02 15:18:38 +02:00
destination = fields.Many2One('party.party', "Destination", required=True,
domain=[
('id', '!=', Eval('source', -1)),
],
2017-03-16 18:32:07 +01:00
help="The party that replaces.")
2016-10-02 15:18:38 +02:00
@classmethod
def default_source(cls):
context = Transaction().context
if context.get('active_model') == 'party.party':
return context.get('active_id')
@fields.depends('source')
def on_change_source(self):
if self.source and self.source.replaced_by:
self.destination = self.source.replaced_by
2018-03-24 00:02:03 +01:00
2020-03-01 12:33:52 +01:00
class Erase(Wizard):
2018-03-24 00:02:03 +01:00
"Erase Party"
__name__ = 'party.erase'
start_state = 'ask'
ask = StateView('party.erase.ask', 'party.erase_ask_view_form', [
Button("Cancel", 'end', 'tryton-cancel'),
Button("Erase", 'erase', 'tryton-clear', default=True),
])
erase = StateTransition()
def transition_erase(self):
pool = Pool()
Party = pool.get('party.party')
transaction = Transaction()
cursor = transaction.connection.cursor()
2018-03-24 00:02:03 +01:00
resources = self.get_resources()
parties = replacing = [self.ask.party]
with inactive_records():
2018-03-24 00:02:03 +01:00
while replacing:
replacing = Party.search([
2018-06-26 19:11:48 +02:00
('replaced_by', 'in', list(map(int, replacing))),
2018-03-24 00:02:03 +01:00
])
parties += replacing
for party in parties:
self.check_erase(party)
to_erase = self.to_erase(party.id)
for Model, domain, resource, columns, values in to_erase:
assert issubclass(Model, ModelSQL)
assert len(columns) == len(values)
if 'active' in Model._fields:
records = Model.search(domain)
Model.write(records, {'active': False})
tables, where = Model.search_domain(domain, active_test=False)
2023-03-23 00:33:22 +01:00
from_ = convert_from(None, tables, type_='INNER')
2018-03-24 00:02:03 +01:00
table, _ = tables[None]
query = from_.select(table.id, where=where)
if columns:
model_tables = [Model.__table__()]
if Model._history:
model_tables.append(Model.__table_history__())
2018-03-24 00:02:03 +01:00
for table in model_tables:
sql_columns, sql_values = [], []
for column, value in zip(columns, values):
column = Column(table, column)
sql_columns.append(column)
sql_values.append(
value(column) if callable(value) else value)
cursor.execute(*table.update(
sql_columns, sql_values,
where=table.id.in_(query)))
if resource:
for Resource in resources:
model_tables = [
(Resource.__table__(), Resource.resource)]
if Resource._history:
model_tables.append(
(Resource.__table_history__(),
Resource.resource))
for (table, resource_field) in model_tables:
cursor.execute(*table.delete(
where=table.resource.like(
Model.__name__ + ',%')
& resource_field.sql_id(
table.resource, Model).in_(query)))
transaction.counter += 1
2018-03-24 00:02:03 +01:00
return 'end'
def check_erase(self, party):
if party.active:
2018-12-29 14:20:29 +01:00
raise EraseError(gettext('party.msg_erase_active_party',
party=party.rec_name))
2018-03-24 00:02:03 +01:00
def to_erase(self, party_id):
pool = Pool()
Party = pool.get('party.party')
Identifier = pool.get('party.identifier')
Address = pool.get('party.address')
ContactMechanism = pool.get('party.contact_mechanism')
return [
(Party, [('id', '=', party_id)], True,
['name'],
[None]),
(Identifier, [('party', '=', party_id)], True,
['type', 'code'],
[None, '****']),
(Address, [('party', '=', party_id)], True,
2021-03-31 23:59:33 +02:00
['name', 'street', 'postal_code', 'city',
'country', 'subdivision'],
2018-03-24 00:02:03 +01:00
[None, None, None, None, None, None]),
(ContactMechanism, [('party', '=', party_id)], True,
['value', 'name', 'comment'],
[None, None, None]),
]
@classmethod
def get_resources(cls):
pool = Pool()
Attachment = pool.get('ir.attachment')
Note = pool.get('ir.note')
2021-03-28 19:20:36 +02:00
Avatar = pool.get('ir.avatar')
return [Attachment, Note, Avatar]
2018-03-24 00:02:03 +01:00
2020-03-01 12:33:52 +01:00
class EraseAsk(ModelView):
2018-03-24 00:02:03 +01:00
"Erase Party"
__name__ = 'party.erase.ask'
party = fields.Many2One('party.party', "Party", required=True,
help="The party to be erased.")
@classmethod
def default_party(cls):
context = Transaction().context
if context.get('active_model') == 'party.party':
return context.get('active_id')