summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorAntony Lesuisse <al@openerp.com>2018-09-18 17:16:16 +0200
committerTWA <twa@odoo.com>2018-09-21 14:02:22 +0200
commita6e1eb9f0ad285fac7d0ca0b9f89f046d78ec9c7 (patch)
tree147f86739a95d6924ebda835c62716e66edcbdb5 /addons
parentce16d0d2ab20ed223ab3dc2932ac7a6f27cccf5f (diff)
[ADD] partner_autocomplete: Added autocomplete on partners fields
For Name field or M2O, gives a list of companies Data comes from Odoo IAP Service
Diffstat (limited to 'addons')
-rw-r--r--addons/account/models/account_invoice.py2
-rw-r--r--addons/account/views/account_invoice_view.xml6
-rw-r--r--addons/account/views/partner_view.xml1
-rw-r--r--addons/base_setup/models/res_config_settings.py1
-rw-r--r--addons/base_setup/views/res_config_settings_views.xml11
-rw-r--r--addons/crm/models/crm_lead.py2
-rw-r--r--addons/crm/models/res_config_settings.py1
-rw-r--r--addons/crm/views/crm_lead_views.xml16
-rw-r--r--addons/crm/views/res_config_settings_views.xml11
-rw-r--r--addons/crm/wizard/crm_lead_to_opportunity_views.xml4
-rw-r--r--addons/iap/models/iap.py2
-rw-r--r--addons/iap/static/src/js/crash_manager.js4
-rw-r--r--addons/partner_autocomplete/__init__.py3
-rw-r--r--addons/partner_autocomplete/__manifest__.py27
-rw-r--r--addons/partner_autocomplete/data/cron.xml12
-rw-r--r--addons/partner_autocomplete/i18n/partner_autocomplete.pot233
-rw-r--r--addons/partner_autocomplete/models/__init__.py7
-rw-r--r--addons/partner_autocomplete/models/res_company.py14
-rw-r--r--addons/partner_autocomplete/models/res_config_settings.py17
-rw-r--r--addons/partner_autocomplete/models/res_partner.py180
-rw-r--r--addons/partner_autocomplete/models/res_partner_autocomplete_sync.py37
-rw-r--r--addons/partner_autocomplete/security/ir.model.access.csv5
-rw-r--r--addons/partner_autocomplete/static/lib/jsvat.js1338
-rw-r--r--addons/partner_autocomplete/static/src/js/iap_credit_checker_widget.js94
-rw-r--r--addons/partner_autocomplete/static/src/js/partner_autocomplete_core.js331
-rw-r--r--addons/partner_autocomplete/static/src/js/partner_autocomplete_fieldchar.js339
-rw-r--r--addons/partner_autocomplete/static/src/js/partner_autocomplete_many2one.js138
-rw-r--r--addons/partner_autocomplete/static/src/scss/partner_autocomplete.scss28
-rw-r--r--addons/partner_autocomplete/static/src/xml/partner_autocomplete.xml33
-rw-r--r--addons/partner_autocomplete/static/tests/partner_autocomplete_tests.js335
-rw-r--r--addons/partner_autocomplete/views/additional_info_template.xml45
-rw-r--r--addons/partner_autocomplete/views/partner_autocomplete_assets.xml18
-rw-r--r--addons/partner_autocomplete/views/res_company_views.xml19
-rw-r--r--addons/partner_autocomplete/views/res_config_settings_views.xml28
-rw-r--r--addons/partner_autocomplete/views/res_partner_views.xml59
-rw-r--r--addons/partner_autocomplete_address_extended/__init__.py3
-rw-r--r--addons/partner_autocomplete_address_extended/__manifest__.py11
-rw-r--r--addons/partner_autocomplete_address_extended/models/__init__.py4
-rw-r--r--addons/partner_autocomplete_address_extended/models/res_partner.py44
-rw-r--r--addons/purchase/models/purchase.py2
-rw-r--r--addons/purchase/views/purchase_views.xml4
-rw-r--r--addons/repair/models/repair.py2
-rw-r--r--addons/repair/views/repair_views.xml2
-rw-r--r--addons/sale/models/sale.py2
-rw-r--r--addons/sale/views/sale_views.xml2
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js62
-rw-r--r--addons/web/static/src/js/fields/upgrade_fields.js1
47 files changed, 3498 insertions, 42 deletions
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py
index c42a1edddba..7847f238e08 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -279,7 +279,7 @@ class AccountInvoice(models.Model):
"means direct payment.")
partner_id = fields.Many2one('res.partner', string='Partner', change_default=True,
readonly=True, states={'draft': [('readonly', False)]},
- track_visibility='always')
+ track_visibility='always', help="You can find a contact by its Name, TIN, Email or Internal Reference.")
vendor_bill_id = fields.Many2one('account.invoice', string='Vendor Bill',
help="Auto-complete from a past bill.")
payment_term_id = fields.Many2one('account.payment.term', string='Payment Terms', oldname='payment_term',
diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml
index 0cd723f1ff9..1c7ea86e3ae 100644
--- a/addons/account/views/account_invoice_view.xml
+++ b/addons/account/views/account_invoice_view.xml
@@ -283,7 +283,8 @@
<group>
<group>
<field string="Vendor" name="partner_id"
- context="{'default_customer': 0, 'search_default_supplier': 1, 'default_supplier': 1, 'default_company_type': 'company'}"
+ widget="res_partner_many2one"
+ context="{'default_customer': 0, 'search_default_supplier': 1, 'default_supplier': 1, 'default_is_company': True, 'show_vat': True}"
domain="[('supplier', '=', True)]"/>
<field name="reference" string="Vendor Reference"/>
<field name="vendor_bill_id" attrs="{'invisible': [('state','not in',['draft'])]}"
@@ -436,7 +437,8 @@
<group>
<group>
<field string="Customer" name="partner_id"
- context="{'search_default_customer':1, 'show_address': 1, 'default_company_type': 'company'}"
+ widget="res_partner_many2one"
+ context="{'search_default_customer':1, 'show_address': 1, 'default_is_company': True, 'show_vat': True}"
options='{"always_reload": True, "no_quick_create": True}'
domain="[('customer', '=', True)]" required="1"/>
<field name="payment_term_id"/>
diff --git a/addons/account/views/partner_view.xml b/addons/account/views/partner_view.xml
index 84279d3878f..152356fcb65 100644
--- a/addons/account/views/partner_view.xml
+++ b/addons/account/views/partner_view.xml
@@ -200,6 +200,7 @@
<field name="sequence" widget="handle"/>
<field name="bank_id"/>
<field name="acc_number"/>
+ <field name="acc_holder_name" invisible="1"/>
</tree>
</field>
<button type="action" class="btn-link"
diff --git a/addons/base_setup/models/res_config_settings.py b/addons/base_setup/models/res_config_settings.py
index de5037a2245..d23737db901 100644
--- a/addons/base_setup/models/res_config_settings.py
+++ b/addons/base_setup/models/res_config_settings.py
@@ -31,6 +31,7 @@ class ResConfigSettings(models.TransientModel):
module_pad = fields.Boolean("Collaborative Pads")
module_voip = fields.Boolean("Asterisk (VoIP)")
module_web_unsplash = fields.Boolean("Unsplash Image Library")
+ module_partner_autocomplete = fields.Boolean("Auto-populate company data")
company_share_partner = fields.Boolean(string='Share partners to all companies',
help="Share your partners to all companies defined in your instance.\n"
" * Checked : Partners are visible for every companies, even if a company is defined on the partner.\n"
diff --git a/addons/base_setup/views/res_config_settings_views.xml b/addons/base_setup/views/res_config_settings_views.xml
index 95515c8ab97..de090d491d5 100644
--- a/addons/base_setup/views/res_config_settings_views.xml
+++ b/addons/base_setup/views/res_config_settings_views.xml
@@ -244,6 +244,17 @@
</div>
</div>
</div>
+ <div class="col-xs-12 col-md-6 o_setting_box" title="When populating your address book, Odoo provides a list of matching companies. When selecting one item, the company data and logo are auto-filled.">
+ <div class="o_setting_left_pane">
+ <field name="module_partner_autocomplete"/>
+ </div>
+ <div class="o_setting_right_pane" id="partner_autocomplete_settings">
+ <label for="module_partner_autocomplete"/>
+ <div class="text-muted">
+ Autocomplete company data (name, logo, address, etc.)
+ </div>
+ </div>
+ </div>
</div>
</div>
</xpath>
diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py
index 177d6aa8048..df5a0e0cfce 100644
--- a/addons/crm/models/crm_lead.py
+++ b/addons/crm/models/crm_lead.py
@@ -65,7 +65,7 @@ class Lead(models.Model):
name = fields.Char('Opportunity', required=True, index=True)
partner_id = fields.Many2one('res.partner', string='Customer', track_visibility='onchange', track_sequence=1, index=True,
- help="Linked partner (optional). Usually created when converting the lead.")
+ help="Linked partner (optional). Usually created when converting the lead. You can find a partner by its Name, TIN, Email or Internal Reference.")
active = fields.Boolean('Active', default=True, track_visibility=True)
date_action_last = fields.Datetime('Last Action', readonly=True)
email_from = fields.Char('Email', help="Email address of the contact", track_visibility='onchange', track_sequence=4, index=True)
diff --git a/addons/crm/models/res_config_settings.py b/addons/crm/models/res_config_settings.py
index c2ca5a60735..33386434f2a 100644
--- a/addons/crm/models/res_config_settings.py
+++ b/addons/crm/models/res_config_settings.py
@@ -11,7 +11,6 @@ class ResConfigSettings(models.TransientModel):
generate_lead_from_alias = fields.Boolean('Manual Assignation of Emails', config_parameter='crm.generate_lead_from_alias')
group_use_lead = fields.Boolean(string="Leads", implied_group='crm.group_use_lead')
module_crm_phone_validation = fields.Boolean("Phone Formatting")
- module_web_clearbit = fields.Boolean("Customer Autocomplete")
def _find_default_lead_alias_id(self):
alias = self.env.ref('crm.mail_alias_lead_info', False)
diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml
index f4a5ba6ca19..04b29c6153c 100644
--- a/addons/crm/views/crm_lead_views.xml
+++ b/addons/crm/views/crm_lead_views.xml
@@ -68,7 +68,9 @@
<group>
<!-- Preload all the partner's information -->
<field name="partner_id" string="Customer"
- context="{'default_name': contact_name, 'default_street': street, 'default_city': city, 'default_state_id': state_id, 'default_zip': zip, 'default_country_id': country_id, 'default_function': function, 'default_phone': phone, 'default_mobile': mobile, 'default_email': email_from, 'default_user_id': user_id, 'default_team_id': team_id, 'default_website': website}" groups="base.group_no_one"/>
+ widget="res_partner_many2one"
+ context="{'default_name': contact_name, 'default_street': street, 'default_city': city, 'default_state_id': state_id, 'default_zip': zip, 'default_country_id': country_id, 'default_function': function, 'default_phone': phone, 'default_mobile': mobile, 'default_email': email_from, 'default_user_id': user_id, 'default_team_id': team_id, 'default_website': website, 'show_vat': True}"
+ groups="base.group_no_one"/>
<field name="partner_name" string="Company Name"/>
<label for="street" string="Address"/>
<div class="o_address_format">
@@ -240,7 +242,7 @@
<group>
<group>
<field name="name" string="Opportunity Title" placeholder="e.g. Customer Deal"/>
- <field name="partner_id" domain="[('customer', '=', True)]" context="{'search_default_customer': 1}"/>
+ <field name="partner_id" widget="res_partner_many2one" domain="[('customer', '=', True)]" context="{'search_default_customer': 1, 'show_vat': True}"/>
<field name="partner_name" invisible="1"/>
<field name="street" invisible="1"/>
<field name="street2" invisible="1"/>
@@ -298,7 +300,7 @@
<form>
<group>
<field name="name"/>
- <field name="partner_id" domain="[('customer', '=', True)]" context="{'search_default_customer': 1}"/>
+ <field name="partner_id" widget="res_partner_many2one" domain="[('customer', '=', True)]" context="{'search_default_customer': 1, 'show_vat': True}"/>
<field name="planned_revenue" widget="monetary" options="{'currency_field': 'company_currency'}"/>
<field name="company_currency" invisible="1"/>
<field name="company_id" invisible="1"/>
@@ -370,7 +372,7 @@
</div>
</div>
</div>
- <div class="oe_clear"></div>
+ <div class="oe_clear"/>
</div>
</t>
</templates>
@@ -493,6 +495,7 @@
<group>
<group>
<field name="partner_id"
+ widget="res_partner_many2one"
string="Customer" domain="[('customer', '=', True)]"
context="{'search_default_customer': 1,
'default_name': partner_name, 'default_street': street,
@@ -501,7 +504,10 @@
'default_country_id': country_id, 'default_function': function,
'default_phone': phone, 'default_mobile': mobile,
'default_email': email_from,
- 'default_user_id': user_id, 'default_team_id': team_id, 'default_website': website}"/>
+ 'default_user_id': user_id, 'default_team_id': team_id, 'default_website': website,
+ 'show_vat': True,
+ }"
+ />
<field name="is_blacklisted" invisible="1"/>
<field name="partner_is_blacklisted" invisible="1"/>
<label for="email_from" class="oe_inline"/>
diff --git a/addons/crm/views/res_config_settings_views.xml b/addons/crm/views/res_config_settings_views.xml
index a32b08a63f2..e37b01336a4 100644
--- a/addons/crm/views/res_config_settings_views.xml
+++ b/addons/crm/views/res_config_settings_views.xml
@@ -66,17 +66,6 @@
</div>
</div>
</div>
- <div class="col-12 col-lg-6 o_setting_box" title="When populating your address book, Odoo relies on Clearbit’s API to provide you with a list of matching contacts or companies.When selecting one item, the partner name, logo and website get automatically set.">
- <div class="o_setting_left_pane">
- <field name="module_web_clearbit" widget="upgrade_boolean"/>
- </div>
- <div class="o_setting_right_pane">
- <label for="module_web_clearbit"/>
- <div class="text-muted">
- Look up company information (name, logo, etc.)
- </div>
- </div>
- </div>
</div>
</div>
</xpath>
diff --git a/addons/crm/wizard/crm_lead_to_opportunity_views.xml b/addons/crm/wizard/crm_lead_to_opportunity_views.xml
index c12587a5fb0..97a06862ab1 100644
--- a/addons/crm/wizard/crm_lead_to_opportunity_views.xml
+++ b/addons/crm/wizard/crm_lead_to_opportunity_views.xml
@@ -32,7 +32,7 @@
<group name="action" attrs="{'invisible': [('name', '!=', 'convert')]}" string="Customers" col="1">
<field name="action" nolabel="1" widget="radio"/>
<group col="2">
- <field name="partner_id" domain="[('customer', '=', True)]" context="{'search_default_customer': 1}" attrs="{'required': [('action', '=', 'exist')], 'invisible':[('action','!=','exist')]}"/>
+ <field name="partner_id" widget="res_partner_many2one" domain="[('customer', '=', True)]" context="{'search_default_customer': 1, 'show_vat': True}" attrs="{'required': [('action', '=', 'exist')], 'invisible':[('action','!=','exist')]}"/>
</group>
</group>
<footer>
@@ -79,7 +79,9 @@
<field name="action" class="oe_inline" widget="radio"/>
<group col="2">
<field name="partner_id"
+ widget="res_partner_many2one"
attrs="{'required': [('action', '=', 'exist')], 'invisible':[('action','!=','exist')]}"
+ context="{'show_vat': True}"
class="oe_inline"/>
</group>
</group>
diff --git a/addons/iap/models/iap.py b/addons/iap/models/iap.py
index ea97914da1b..8d6b85e6d73 100644
--- a/addons/iap/models/iap.py
+++ b/addons/iap/models/iap.py
@@ -145,7 +145,7 @@ class IapAccount(models.Model):
service_name = fields.Char()
account_token = fields.Char(default=lambda s: uuid.uuid4().hex)
company_id = fields.Many2one('res.company', default=lambda self: self.env.user.company_id)
- insufficient_credit= fields.Boolean('Insufficient credits', default=False)
+ insufficient_credit = fields.Boolean('Insufficient credits', default=False)
@api.model
def get(self, service_name):
diff --git a/addons/iap/static/src/js/crash_manager.js b/addons/iap/static/src/js/crash_manager.js
index 93ccf8bee95..59b07af1294 100644
--- a/addons/iap/static/src/js/crash_manager.js
+++ b/addons/iap/static/src/js/crash_manager.js
@@ -52,11 +52,11 @@ CrashManager.include({
$content: content,
buttons: [{
text: self._getButtonMessage(error_data.trial),
- classes: "btn-primary",
+ classes : "btn-primary",
click: function () {
window.open(url, '_blank');
},
- close: true,
+ close:true,
}, {
text: _t("Cancel"),
close: true,
diff --git a/addons/partner_autocomplete/__init__.py b/addons/partner_autocomplete/__init__.py
new file mode 100644
index 00000000000..cde864bae21
--- /dev/null
+++ b/addons/partner_autocomplete/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+from . import models
diff --git a/addons/partner_autocomplete/__manifest__.py b/addons/partner_autocomplete/__manifest__.py
new file mode 100644
index 00000000000..eeb767f2258
--- /dev/null
+++ b/addons/partner_autocomplete/__manifest__.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+{
+ 'name': "Partner Autocomplete",
+ 'summary': """
+ Auto-complete partner companies' data""",
+ 'description': """
+ Auto-complete partner companies' data
+ """,
+ 'author': "Odoo SA",
+ 'category': 'Tools',
+ 'version': '1.0',
+
+ 'depends': ['web', 'mail', 'iap'],
+ 'data': [
+ 'security/ir.model.access.csv',
+ 'views/partner_autocomplete_assets.xml',
+ 'views/additional_info_template.xml',
+ 'views/res_partner_views.xml',
+ 'views/res_company_views.xml',
+ 'views/res_config_settings_views.xml',
+ 'data/cron.xml',
+ ],
+ 'qweb': [
+ 'static/src/xml/partner_autocomplete.xml',
+ ],
+ 'auto_install': True,
+}
diff --git a/addons/partner_autocomplete/data/cron.xml b/addons/partner_autocomplete/data/cron.xml
new file mode 100644
index 00000000000..f382d10f7f2
--- /dev/null
+++ b/addons/partner_autocomplete/data/cron.xml
@@ -0,0 +1,12 @@
+<odoo>
+ <record id="ir_cron_partner_autocomplete" model="ir.cron">
+ <field name="name">Partner Autocomplete : Sync with remote DB</field>
+ <field name="model_id" ref="model_res_partner_autocomplete_sync"/>
+ <field name="state">code</field>
+ <field name="code">model.start_sync()</field>
+ <field name="interval_number">1</field>
+ <field name="interval_type">minutes</field>
+ <field name="numbercall">-1</field>
+ <field eval="False" name="doall"/>
+ </record>
+</odoo>
diff --git a/addons/partner_autocomplete/i18n/partner_autocomplete.pot b/addons/partner_autocomplete/i18n/partner_autocomplete.pot
index 1817e359bc7..2a7e7ec0964 100644
--- a/addons/partner_autocomplete/i18n/partner_autocomplete.pot
+++ b/addons/partner_autocomplete/i18n/partner_autocomplete.pot
@@ -6,8 +6,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Odoo Server saas~11.5\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-09-18 09:49+0000\n"
-"PO-Revision-Date: 2018-09-18 09:49+0000\n"
+"POT-Creation-Date: 2018-09-21 11:54+0000\n"
+"PO-Revision-Date: 2018-09-21 11:54+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
@@ -16,25 +16,240 @@ msgstr ""
"Plural-Forms: \n"
#. module: partner_autocomplete
+#: model_terms:ir.ui.view,arch_db:partner_autocomplete.additional_info_template
+msgid "&amp;nbsp;&amp;nbsp;&amp;nbsp;"
+msgstr ""
+
+#. module: partner_autocomplete
+#: model_terms:ir.ui.view,arch_db:partner_autocomplete.additional_info_template
+msgid "<b>Annual revenue:</b>"
+msgstr ""
+
+#. module: partner_autocomplete
+#: model_terms:ir.ui.view,arch_db:partner_autocomplete.additional_info_template
+msgid "<b>Description</b><br/>"
+msgstr ""
+
+#. module: partner_autocomplete
+#: model_terms:ir.ui.view,arch_db:partner_autocomplete.additional_info_template
+msgid "<b>Email addresses</b><br/>"
+msgstr ""
+
+#. module: partner_autocomplete
+#: model_terms:ir.ui.view,arch_db:partner_autocomplete.additional_info_template
+msgid "<b>Employees:</b>"
+msgstr ""
+
+#. module: partner_autocomplete
+#: model_terms:ir.ui.view,arch_db:partner_autocomplete.additional_info_template
+msgid "<b>Estimated annual revenue:</b>"
+msgstr ""
+
+#. module: partner_autocomplete
+#: model_terms:ir.ui.view,arch_db:partner_autocomplete.additional_info_template
+msgid "<b>Phone numbers</b><br/>"
+msgstr ""
+
+#. module: partner_autocomplete
+#: model_terms:ir.ui.view,arch_db:partner_autocomplete.additional_info_template
+msgid "<b>Sector:</b>"
+msgstr ""
+
+#. module: partner_autocomplete
+#: model_terms:ir.ui.view,arch_db:partner_autocomplete.additional_info_template
+msgid "<b>Social networks</b><br/>\n"
+" &amp;nbsp;&amp;nbsp;&amp;nbsp; <b>Facebook:</b>"
+msgstr ""
+
+#. module: partner_autocomplete
+#: model_terms:ir.ui.view,arch_db:partner_autocomplete.additional_info_template
+msgid "<b>Tech:</b>"
+msgstr ""
+
+#. module: partner_autocomplete
+#: model_terms:ir.ui.view,arch_db:partner_autocomplete.additional_info_template
+msgid "<br/>\n"
+" &amp;nbsp;&amp;nbsp;&amp;nbsp; <b>Crunchbase:</b>"
+msgstr ""
+
+#. module: partner_autocomplete
+#: model_terms:ir.ui.view,arch_db:partner_autocomplete.additional_info_template
+msgid "<br/>\n"
+" &amp;nbsp;&amp;nbsp;&amp;nbsp; <b>Linkedin:</b>"
+msgstr ""
+
+#. module: partner_autocomplete
+#: model_terms:ir.ui.view,arch_db:partner_autocomplete.additional_info_template
+msgid "<br/>\n"
+" &amp;nbsp;&amp;nbsp;&amp;nbsp; <b>Twitter:</b>"
+msgstr ""
+
+#. module: partner_autocomplete
+#: model_terms:ir.ui.view,arch_db:partner_autocomplete.res_config_settings_view_form
+msgid "<i class=\"fa fa-arrow-right\"/>\n"
+" Buy more credits"
+msgstr ""
+
+#. module: partner_autocomplete
+#: model_terms:ir.ui.view,arch_db:partner_autocomplete.res_config_settings_view_form
+msgid "<i class=\"fa fa-exclamation-triangle text-warning\"/> &amp;nbsp; You don't have credits to auto-complete companies' data anymore."
+msgstr ""
+
+#. module: partner_autocomplete
+#: model_terms:ir.ui.view,arch_db:partner_autocomplete.res_config_settings_view_form
+msgid "<span>&amp;times;</span>"
+msgstr ""
+
+#. module: partner_autocomplete
+#: model:ir.model.fields,field_description:partner_autocomplete.field_res_partner__additional_info
+#: model:ir.model.fields,field_description:partner_autocomplete.field_res_users__additional_info
+msgid "Additional info"
+msgstr ""
+
+#. module: partner_autocomplete
+#. openerp-web
+#: code:addons/partner_autocomplete/static/src/xml/partner_autocomplete.xml:30
+#, python-format
+msgid "Buy more credits"
+msgstr ""
+
+#. module: partner_autocomplete
+#. openerp-web
+#: code:addons/partner_autocomplete/static/src/xml/partner_autocomplete.xml:20
+#, python-format
+msgid "Checking remaining credit ..."
+msgstr ""
+
+#. module: partner_autocomplete
+#: model_terms:ir.ui.view,arch_db:partner_autocomplete.res_config_settings_view_form
+msgid "Close"
+msgstr ""
+
+#. module: partner_autocomplete
#: model:ir.model,name:partner_autocomplete.model_res_company
msgid "Companies"
msgstr ""
#. module: partner_autocomplete
+#: model:ir.model.fields,field_description:partner_autocomplete.field_res_company__partner_gid
+#: model:ir.model.fields,field_description:partner_autocomplete.field_res_partner__partner_gid
+#: model:ir.model.fields,field_description:partner_autocomplete.field_res_users__partner_gid
+msgid "Company database ID"
+msgstr ""
+
+#. module: partner_autocomplete
#: model:ir.model,name:partner_autocomplete.model_res_partner
msgid "Contact"
msgstr ""
#. module: partner_autocomplete
-#: model_terms:ir.ui.view,arch_db:partner_autocomplete.res_company_view_form
-#: model_terms:ir.ui.view,arch_db:partner_autocomplete.view_partner_form
-#: model_terms:ir.ui.view,arch_db:partner_autocomplete.view_partner_short_form
-msgid "VAT"
+#. openerp-web
+#: code:addons/partner_autocomplete/static/src/js/partner_autocomplete_many2one.js:108
+#, python-format
+msgid "Create and Edit from Autocomplete :"
+msgstr ""
+
+#. module: partner_autocomplete
+#: model:ir.model.fields,field_description:partner_autocomplete.field_res_partner_autocomplete_sync__create_uid
+msgid "Created by"
+msgstr ""
+
+#. module: partner_autocomplete
+#: model:ir.model.fields,field_description:partner_autocomplete.field_res_partner_autocomplete_sync__create_date
+msgid "Created on"
+msgstr ""
+
+#. module: partner_autocomplete
+#: model:ir.model.fields,field_description:partner_autocomplete.field_res_partner_autocomplete_sync__display_name
+msgid "Display Name"
+msgstr ""
+
+#. module: partner_autocomplete
+#: model:ir.model.fields,field_description:partner_autocomplete.field_res_partner_autocomplete_sync__id
+msgid "ID"
+msgstr ""
+
+#. module: partner_autocomplete
+#. openerp-web
+#: code:addons/partner_autocomplete/static/src/xml/partner_autocomplete.xml:23
+#: model:ir.model.fields,field_description:partner_autocomplete.field_res_config_settings__partner_autocomplete_insufficient_credit
+#, python-format
+msgid "Insufficient credit"
+msgstr ""
+
+#. module: partner_autocomplete
+#: model:ir.model.fields,field_description:partner_autocomplete.field_res_partner_autocomplete_sync__synched
+msgid "Is synched"
+msgstr ""
+
+#. module: partner_autocomplete
+#: model:ir.model.fields,field_description:partner_autocomplete.field_res_partner_autocomplete_sync____last_update
+msgid "Last Modified on"
+msgstr ""
+
+#. module: partner_autocomplete
+#: model:ir.model.fields,field_description:partner_autocomplete.field_res_partner_autocomplete_sync__write_uid
+msgid "Last Updated by"
+msgstr ""
+
+#. module: partner_autocomplete
+#: model:ir.model.fields,field_description:partner_autocomplete.field_res_partner_autocomplete_sync__write_date
+msgid "Last Updated on"
+msgstr ""
+
+#. module: partner_autocomplete
+#: model:ir.model.fields,field_description:partner_autocomplete.field_res_partner_autocomplete_sync__partner_id
+msgid "Partner"
+msgstr ""
+
+#. module: partner_autocomplete
+#: model:ir.actions.server,name:partner_autocomplete.ir_cron_partner_autocomplete_ir_actions_server
+#: model:ir.cron,cron_name:partner_autocomplete.ir_cron_partner_autocomplete
+#: model:ir.cron,name:partner_autocomplete.ir_cron_partner_autocomplete
+msgid "Partner Autocomplete : Sync with remote DB"
+msgstr ""
+
+#. module: partner_autocomplete
+#. openerp-web
+#: code:addons/partner_autocomplete/static/src/xml/partner_autocomplete.xml:8
+#, python-format
+msgid "Placeholder"
+msgstr ""
+
+#. module: partner_autocomplete
+#. openerp-web
+#: code:addons/partner_autocomplete/static/src/xml/partner_autocomplete.xml:26
+#, python-format
+msgid "Remaining :"
+msgstr ""
+
+#. module: partner_autocomplete
+#. openerp-web
+#: code:addons/partner_autocomplete/static/src/js/partner_autocomplete_many2one.js:22
+#, python-format
+msgid "Searching Autocomplete..."
+msgstr ""
+
+#. module: partner_autocomplete
+#: model_terms:ir.ui.view,arch_db:partner_autocomplete.view_res_partner_form_inherit_partner_autocomplete
+#: model_terms:ir.ui.view,arch_db:partner_autocomplete.view_res_partner_short_form_inherit_partner_autocomplete
+msgid "You can find a customer, a contact, etc. by its Name, TIN, Email or Internal Reference."
+msgstr ""
+
+#. module: partner_autocomplete
+#. openerp-web
+#: code:addons/partner_autocomplete/static/src/xml/partner_autocomplete.xml:26
+#, python-format
+msgid "credits"
+msgstr ""
+
+#. module: partner_autocomplete
+#: model:ir.model,name:partner_autocomplete.model_res_config_settings
+msgid "res.config.settings"
msgstr ""
#. module: partner_autocomplete
-#: model_terms:ir.ui.view,arch_db:partner_autocomplete.view_partner_form
-#: model_terms:ir.ui.view,arch_db:partner_autocomplete.view_partner_short_form
-msgid "e.g. BE0477472701"
+#: model:ir.model,name:partner_autocomplete.model_res_partner_autocomplete_sync
+msgid "res.partner.autocomplete.sync"
msgstr ""
diff --git a/addons/partner_autocomplete/models/__init__.py b/addons/partner_autocomplete/models/__init__.py
new file mode 100644
index 00000000000..5d9eecc13d7
--- /dev/null
+++ b/addons/partner_autocomplete/models/__init__.py
@@ -0,0 +1,7 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import res_partner
+from . import res_company
+from . import res_config_settings
+from . import res_partner_autocomplete_sync
diff --git a/addons/partner_autocomplete/models/res_company.py b/addons/partner_autocomplete/models/res_company.py
new file mode 100644
index 00000000000..e946567fcde
--- /dev/null
+++ b/addons/partner_autocomplete/models/res_company.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models
+
+class ResCompany(models.Model):
+ _name = 'res.company'
+ _inherit = 'res.company'
+
+ partner_gid = fields.Integer('Company database ID', related="partner_id.partner_gid", inverse="_inverse_partner_gid", store=True)
+
+ def _inverse_partner_gid(self):
+ for company in self:
+ company.partner_id.partner_gid = company.partner_gid
diff --git a/addons/partner_autocomplete/models/res_config_settings.py b/addons/partner_autocomplete/models/res_config_settings.py
new file mode 100644
index 00000000000..f28046d6189
--- /dev/null
+++ b/addons/partner_autocomplete/models/res_config_settings.py
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models, _
+
+class ResConfigSettings(models.TransientModel):
+ _inherit = 'res.config.settings'
+
+ partner_autocomplete_insufficient_credit = fields.Boolean('Insufficient credit', default=lambda self: self.env['iap.account'].get('partner_autocomplete').insufficient_credit)
+
+ @api.multi
+ def redirect_to_buy_autocmplete_credit(self):
+ return {
+ 'type': 'ir.actions.act_url',
+ 'url': self.env['iap.account'].get_credits_url('partner_autocomplete'),
+ 'target': '_new',
+ }
diff --git a/addons/partner_autocomplete/models/res_partner.py b/addons/partner_autocomplete/models/res_partner.py
new file mode 100644
index 00000000000..60d5b182222
--- /dev/null
+++ b/addons/partner_autocomplete/models/res_partner.py
@@ -0,0 +1,180 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import logging
+from odoo import api, fields, models, exceptions, _
+from odoo.addons.iap import jsonrpc
+from requests.exceptions import ConnectionError, HTTPError
+from odoo.tools import pycompat
+from odoo.addons.iap.models.iap import InsufficientCreditError
+
+_logger = logging.getLogger(__name__)
+
+PARTNER_REMOTE_URL = 'https://partner-autocomplete.odoo.com/iap/partner_autocomplete'
+
+class ResPartner(models.Model):
+ _name = 'res.partner'
+ _inherit = 'res.partner'
+
+ partner_gid = fields.Integer('Company database ID')
+ additional_info = fields.Char('Additional info')
+
+ @api.model
+ def _replace_location_code_by_id(self, record):
+ record['country_id'], record['state_id'] = self._find_country_data(
+ state_code=record.pop('state_code', False),
+ state_name=record.pop('state_name', False),
+ country_code=record.pop('country_code', False),
+ country_name=record.pop('country_name', False)
+ )
+ return record
+
+ @api.model
+ def _format_data_company(self, company):
+ self._replace_location_code_by_id(company)
+
+ if company.get('child_ids'):
+ child_ids = []
+ for child in company.get('child_ids'):
+ child_ids.append(self._replace_location_code_by_id(child))
+ company['child_ids'] = child_ids
+
+ if company.get('additional_info'):
+ company['additional_info'] = self.env.ref('partner_autocomplete.additional_info_template').render(company.get('additional_info'))
+
+ return company
+
+ @api.model
+ def _find_country_data(self, state_code, state_name, country_code, country_name):
+ country = self.env['res.country'].search([['code', '=ilike', country_code]])
+ if not country:
+ country = self.env['res.country'].search([['name', '=ilike', country_name]])
+
+ state_id = {}
+ country_id = {}
+ if country:
+ country_id = {
+ 'id': country.id,
+ 'display_name': country.display_name
+ }
+ if state_name or state_code:
+ state = self.env['res.country.state'].search([
+ ('country_id', '=', country_id.get('id')),
+ '|',
+ ('name', '=ilike', state_name),
+ ('code', '=ilike', state_code)
+ ], limit=1)
+
+ if state:
+ state_id = {
+ 'id': state.id,
+ 'display_name': state.display_name
+ }
+ else:
+ _logger.info('Country code not found: %s', country_code)
+
+ return country_id, state_id
+
+ @api.model
+ def _rpc_remote_api(self, action, params, timeout=15):
+ url = '%s/%s' % (PARTNER_REMOTE_URL, action)
+ account = self.env['iap.account'].get('partner_autocomplete')
+ params.update({
+ 'db_uuid': self.env['ir.config_parameter'].sudo().get_param('database.uuid'),
+ 'account_token': account.account_token,
+ 'country_code': self.env.user.company_id.country_id.code,
+ 'zip': self.env.user.company_id.zip,
+ })
+ try:
+ return jsonrpc(url=url, params=params, timeout=timeout), False
+ except (ConnectionError, HTTPError, exceptions.AccessError) as exception:
+ _logger.error('Autocomplete API error: %s' % str(exception))
+ return False, str(exception)
+ except InsufficientCreditError:
+ account = self.env['iap.account'].get('partner_autocomplete')
+ if account:
+ account.write({'insufficient_credit': True})
+ return False, 'Insufficient Credit'
+
+ @api.model
+ def autocomplete(self, query):
+ suggestions, error = self._rpc_remote_api('search', {
+ 'query': query,
+ })
+ if suggestions:
+ results = []
+ for suggestion in suggestions:
+ results.append(suggestion)
+ return results
+ else:
+ return []
+
+ @api.model
+ def enrich_company(self, company_domain, partner_gid, vat):
+ response, error = self._rpc_remote_api('enrich', {
+ 'domain': company_domain,
+ 'partner_gid': partner_gid,
+ 'vat': vat,
+ })
+ if response and response.get('company_data'):
+ return self._format_data_company(response.get('company_data'))
+ else:
+ return {}
+
+ @api.model
+ def read_by_vat(self, vat):
+ vies_vat_data, error = self._rpc_remote_api('search_vat', {
+ 'vat': vat,
+ })
+ if vies_vat_data:
+ return [self._format_data_company(vies_vat_data)]
+ else:
+ return []
+
+ @api.model
+ def _is_company_in_europe(self, country_code):
+ country = self.env['res.country'].search([('code', '=ilike', country_code)])
+ if country:
+ country_id = country.id
+ europe = self.env.ref('base.europe')
+ if not europe:
+ europe = self.env["res.country.group"].search([('name', '=', 'Europe')], limit=1)
+ if not europe or country_id not in europe.country_ids.ids:
+ return False
+ return True
+
+ def _is_vat_syncable(self, vat):
+ vat_country_code = vat[:2]
+ partner_country_code = self.country_id and self.country_id.code
+ return self._is_company_in_europe(vat_country_code) and (partner_country_code == vat_country_code or not partner_country_code)
+
+ def _is_synchable(self):
+ already_synched = self.env['res.partner.autocomplete.sync'].search([('partner_id', '=', self.id), ('synched', '=', True)])
+ return self.is_company and self.partner_gid and not already_synched
+
+ def _update_autocomplete_data(self, vat):
+ self.ensure_one()
+ if vat and self._is_synchable() and self._is_vat_syncable(vat):
+ self.env['res.partner.autocomplete.sync'].sudo().add_to_queue(self.id)
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ partners = super(ResPartner, self).create(vals_list)
+ if len(vals_list) == 1:
+ for partner, values in pycompat.izip(partners, vals_list):
+ partner._update_autocomplete_data(values.get('vat', False))
+
+ if partner.additional_info:
+ partner.message_post(body=partner.additional_info)
+ partner.write({'additional_info': False})
+
+ return partners
+
+ @api.multi
+ def write(self, values):
+ record = super(ResPartner, self).write(values)
+ if len(self) == 1:
+ for partner in self:
+ partner._update_autocomplete_data(values.get('vat', False))
+
+ return record
diff --git a/addons/partner_autocomplete/models/res_partner_autocomplete_sync.py b/addons/partner_autocomplete/models/res_partner_autocomplete_sync.py
new file mode 100644
index 00000000000..a0ae355662f
--- /dev/null
+++ b/addons/partner_autocomplete/models/res_partner_autocomplete_sync.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import logging
+from odoo import api, fields, models
+
+_logger = logging.getLogger(__name__)
+
+class ResPartnerAutocompleteSync(models.Model):
+ _name = 'res.partner.autocomplete.sync'
+
+ partner_id = fields.Many2one('res.partner', string="Partner", ondelete='cascade')
+ synched = fields.Boolean('Is synched', default=False)
+
+ @api.model
+ def start_sync(self):
+ to_sync_items = self.search([('synched', '=', False)])
+ for to_sync_item in to_sync_items:
+ partner = to_sync_item.partner_id
+
+ params = {
+ 'partner_gid': partner.partner_gid,
+ }
+
+ if partner.vat and partner._is_vat_syncable(partner.vat):
+ params['vat'] = partner.vat
+ result, error = partner._rpc_remote_api('update', params)
+ if error:
+ _logger.error('Send Partner to sync failed: %s' % str(error))
+
+ to_sync_item.write({'synched': True})
+
+ def add_to_queue(self, partner_id):
+ to_sync = self.search([('partner_id', '=', partner_id)])
+ if not to_sync:
+ to_sync = self.create({'partner_id': partner_id})
+ return to_sync
diff --git a/addons/partner_autocomplete/security/ir.model.access.csv b/addons/partner_autocomplete/security/ir.model.access.csv
new file mode 100644
index 00000000000..67da1da8d05
--- /dev/null
+++ b/addons/partner_autocomplete/security/ir.model.access.csv
@@ -0,0 +1,5 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_partner_autocomplete_sync_all_all,res.partner.autocomplete.sync.all,model_res_partner_autocomplete_sync,,1,0,0,0
+access_partner_autocomplete_sync_portal,res.partner.autocomplete.sync.portal,model_res_partner_autocomplete_sync,base.group_portal,1,0,1,0
+access_partner_autocomplete_sync_user,res.partner.autocomplete.sync.user,model_res_partner_autocomplete_sync,base.group_user,1,1,1,0
+access_partner_autocomplete_sync_system,res.partner.autocomplete.sync.system,model_res_partner_autocomplete_sync,base.group_system,1,1,1,1 \ No newline at end of file
diff --git a/addons/partner_autocomplete/static/lib/jsvat.js b/addons/partner_autocomplete/static/lib/jsvat.js
new file mode 100644
index 00000000000..07dae2d3af0
--- /dev/null
+++ b/addons/partner_autocomplete/static/lib/jsvat.js
@@ -0,0 +1,1338 @@
+/*==================================================================
+
+Application: Utility Function
+Author: John Gardner
+Website: http://www.braemoor.co.uk/software/vat.shtml
+
+Version: V1.0
+Date: 30th July 2005
+Description: Used to check the validity of an EU country VAT number
+
+Version: V1.1
+Date: 3rd August 2005
+Description: Lithuanian legal entities & Maltese check digit checks added.
+
+Version: V1.2
+Date: 20th October 2005
+Description: Italian checks refined (thanks Matteo Mike Peluso).
+
+Version: V1.3
+Date: 16th November 2005
+Description: Error in GB numbers ending in 00 fixed (thanks Guy Dawson).
+
+Version: V1.4
+Date: 28th September 2006
+Description: EU-type numbers added.
+
+Version: V1.5
+Date: 1st January 2007
+Description: Romanian and Bulgarian numbers added.
+
+Version: V1.6
+Date: 7th January 2007
+Description: Error with Slovenian numbers (thanks to Ales Hotko).
+
+Version: V1.7
+Date: 10th February 2007
+Description: Romanian check digits added.
+ Thanks to Dragu Costel for the test suite.
+
+Version: V1.8
+Date: 3rd August 2007
+Description: IE code modified to allow + and * in old format numbers.
+ Thanks to Antonin Moy of Sphere Solutions for pointing out the error.
+
+Version: V1.9
+Date: 6th August 2007
+Description: BE code modified to make a specific check that the leading character of 10 digit
+ numbers is 0 (belts and braces).
+
+Version: V1.10
+Date: 10th August 2007
+Description: Cypriot check digit support added.
+ Check digit validation support for non-standard UK numbers
+
+Version: V1.11
+Date: 25th September 2007
+Description: Spain check digit support for personal numbers.
+ Author: David Perez Carmona
+
+Version: V1.12
+Date: 23rd November 2009
+Description: GB code modified to take into account new style check digits.
+ Thanks to Guy Dawson of Crossflight Ltd for pointing out the necessity.
+
+Version: V1.13
+Date: 7th July 2012
+Description: EL, GB, SE and BE formats updated - thanks to Joost Van Biervliet of VAT Applications
+
+Version: V.14
+Date: 8th April 2013
+Description: BE Pattern match refined
+ BG Add check digit checks for all four types of VAT number
+ CY Pattern match improved
+ CZ Personal pattern match checking improved
+ CZ Personal check digits incorporated
+ EE improved pattern match
+ ES Physical person number checking refined
+ GB Check digit support provided for 12 digit VAT codes and range checks included
+ IT Bug removed to allow 999 and 888 issuing office codes
+ LT temporarily registered taxpayers check digit support added
+ LV Natural persons checks added
+ RO improved pattern match
+ SK improved pattern match and added check digit support
+
+ Thanks to Theo Vroom for his help in this latest release.
+
+Version: V1.15
+Date: 15th April 2013
+ Swedish algorithm re-implemented.
+
+Version: V1.16
+Date: 25th July 2013
+ Support for Croatian numbers added
+
+Version V1.17
+ 10th September 2013
+ Support for Norwegian MVA numbers added (yes, I know that Norway is not in the EU!)
+
+Version V1.18
+ 29th October 2013
+ Partial support for new style Irish numbers.
+ See http://www.revenue.ie/en/practitioner/ebrief/2013/no-032013.html
+ Thanks to Simon Leigh for drawing the author's attention to this.
+
+Version V1.19
+ 31st October 2013
+ Support for Serbian PBI numbers added (yes, I know that Serbia is not in the EU!)
+
+Version V1.20
+ 1st November 2013
+ Support for Swiss MWST numbers added (yes, I know that Switzerland is not in the EU!)
+
+Version V1.21
+ 16th December 2014
+ Non-critical code tidies to French and Danish regular expressions.
+ Thanks to Bill Seddon of Lyquidity Solutions
+
+Version V1.22
+ 14th January 2014
+ Non-critical code tidy to regular expression for new format Irish numbers.
+ Thanks to Olivier Reubens of UNIT4 C-Logic N.V.
+
+Version V1.23
+ 10th April 2014
+ Support for Russian INN numbers added (yes, I know that Russia is not in the EU!).
+ Thanks to Marco Cesaratto of Arki Tech, Italy
+
+Version V1.24
+ 4th June 2014
+ Check digit validation supported for Irish Type 3 numbers
+ Thanks to Olivier Reubens of UNIT4 C-Logic N.V.
+
+Version V1.25
+ 29th July 2014
+ Code improvements
+ Thanks to Sébastien Boelpaep and Nate Kerkhofs
+
+Version V1.26
+ 4th May 2015
+ Code improvements to regular expressions
+ Thanks to Robert Gust-Bardon of webcraft.ch
+
+Version V1.27
+ 3rd December 2015
+ Extend Swiss optional suffix to allow TVA and ITA
+ Thanks to Oskars Petermanis
+
+Version V1.28
+ 30th August 2016
+ Correct Swiss optional suffix to allow TVA and IVA
+ Thanks to Jan Verhaegen
+
+Version V1.29
+ 29th July 2017
+ Correct Czeck Republic checking of Individual type 2 - Special Cases
+ Thanks to Andreas Wuermser of Auer Packaging UK
+
+Parameters: toCheck - VAT number be checked.
+
+This function checks the value of the parameter for a valid European VAT number.
+
+If the number is found to be invalid format, the function returns a value of false. Otherwise it
+returns the VAT number re-formatted.
+
+Example call:
+
+ if (checkVATNumber (myVATNumber))
+ alert ("VAT number has a valid format")
+ else
+ alert ("VAT number has invalid format");
+
+---------------------------------------------------------------------------------------------------*/
+
+var checkVATNumber = (function (){
+ // Array holds the regular expressions for the valid VAT number
+ var vatexp = new Array();
+
+ // To change the default country (e.g. from the UK to Germany - DE):
+ // 1. Change the country code in the defCCode variable below to "DE".
+ // 2. Remove the question mark from the regular expressions associated with the UK VAT number:
+ // i.e. "(GB)?" -> "(GB)"
+ // 3. Add a question mark into the regular expression associated with Germany's number
+ // following the country code: i.e. "(DE)" -> "(DE)?"
+
+ var defCCode = "GB";
+
+ // Note - VAT codes without the "**" in the comment do not have check digit checking.
+
+ vatexp.push(/^(AT)U(\d{8})$/); //** Austria
+ vatexp.push(/^(BE)(0?\d{9})$/); //** Belgium
+ vatexp.push(/^(BG)(\d{9,10})$/); //** Bulgaria
+ vatexp.push(/^(CHE)(\d{9})(MWST|TVA|IVA)?$/); //** Switzerland
+ vatexp.push(/^(CY)([0-59]\d{7}[A-Z])$/); //** Cyprus
+ vatexp.push(/^(CZ)(\d{8,10})(\d{3})?$/); //** Czech Republic
+ vatexp.push(/^(DE)([1-9]\d{8})$/); //** Germany
+ vatexp.push(/^(DK)(\d{8})$/); //** Denmark
+ vatexp.push(/^(EE)(10\d{7})$/); //** Estonia
+ vatexp.push(/^(EL)(\d{9})$/); //** Greece
+ vatexp.push(/^(ES)([A-Z]\d{8})$/); //** Spain (National juridical entities)
+ vatexp.push(/^(ES)([A-HN-SW]\d{7}[A-J])$/); //** Spain (Other juridical entities)
+ vatexp.push(/^(ES)([0-9YZ]\d{7}[A-Z])$/); //** Spain (Personal entities type 1)
+ vatexp.push(/^(ES)([KLMX]\d{7}[A-Z])$/); //** Spain (Personal entities type 2)
+ vatexp.push(/^(EU)(\d{9})$/); //** EU-type
+ vatexp.push(/^(FI)(\d{8})$/); //** Finland
+ vatexp.push(/^(FR)(\d{11})$/); //** France (1)
+ vatexp.push(/^(FR)([A-HJ-NP-Z]\d{10})$/); // France (2)
+ vatexp.push(/^(FR)(\d[A-HJ-NP-Z]\d{9})$/); // France (3)
+ vatexp.push(/^(FR)([A-HJ-NP-Z]{2}\d{9})$/); // France (4)
+ vatexp.push(/^(GB)?(\d{9})$/); //** UK (Standard)
+ vatexp.push(/^(GB)?(\d{12})$/); //** UK (Branches)
+ vatexp.push(/^(GB)?(GD\d{3})$/); //** UK (Government)
+ vatexp.push(/^(GB)?(HA\d{3})$/); //** UK (Health authority)
+ vatexp.push(/^(HR)(\d{11})$/); //** Croatia
+ vatexp.push(/^(HU)(\d{8})$/); //** Hungary
+ vatexp.push(/^(IE)(\d{7}[A-W])$/); //** Ireland (1)
+ vatexp.push(/^(IE)([7-9][A-Z\*\+)]\d{5}[A-W])$/); //** Ireland (2)
+ vatexp.push(/^(IE)(\d{7}[A-W][AH])$/); //** Ireland (3)
+ vatexp.push(/^(IT)(\d{11})$/); //** Italy
+ vatexp.push(/^(LV)(\d{11})$/); //** Latvia
+ vatexp.push(/^(LT)(\d{9}|\d{12})$/); //** Lithunia
+ vatexp.push(/^(LU)(\d{8})$/); //** Luxembourg
+ vatexp.push(/^(MT)([1-9]\d{7})$/); //** Malta
+ vatexp.push(/^(NL)(\d{9})B\d{2}$/); //** Netherlands
+ vatexp.push(/^(NO)(\d{9})$/); //** Norway (not EU)
+ vatexp.push(/^(PL)(\d{10})$/); //** Poland
+ vatexp.push(/^(PT)(\d{9})$/); //** Portugal
+ vatexp.push(/^(RO)([1-9]\d{1,9})$/); //** Romania
+ vatexp.push(/^(RU)(\d{10}|\d{12})$/); //** Russia
+ vatexp.push(/^(RS)(\d{9})$/); //** Serbia
+ vatexp.push(/^(SI)([1-9]\d{7})$/); //** Slovenia
+ vatexp.push(/^(SK)([1-9]\d[2346-9]\d{7})$/); //** Slovakia Republic
+ vatexp.push(/^(SE)(\d{10}01)$/); //** Sweden
+
+ function checkVATNumber(toCheck) {
+ // Load up the string to check
+ var VATNumber = toCheck.toUpperCase();
+
+ // Remove spaces etc. from the VAT number to help validation
+ VATNumber = VATNumber.replace(/(\s|-|\.)+/g, '');
+
+ // Assume we're not going to find a valid VAT number
+ var valid = false;
+
+ // Check the string against the regular expressions for all types of VAT numbers
+ for (var i = 0; i < vatexp.length; i++) {
+
+ // Have we recognised the VAT number?
+ if (vatexp[i].test(VATNumber)) {
+
+ // Yes - we have
+ var cCode = RegExp.$1; // Isolate country code
+ var cNumber = RegExp.$2; // Isolate the number
+ if (cCode.length == 0) cCode = defCCode; // Set up default country code
+
+ // Call the appropriate country VAT validation routine depending on the country code
+ if (eval(cCode + "VATCheckDigit ('" + cNumber + "')")) valid = VATNumber;
+
+ // Having processed the number, we break from the loop
+ break;
+ }
+ }
+
+ // Return with either an error or the reformatted VAT number
+ return valid;
+ }
+
+ function ATVATCheckDigit(vatnumber) {
+
+ // Checks the check digits of an Austrian VAT number.
+
+ var total = 0;
+ var multipliers = [1, 2, 1, 2, 1, 2, 1];
+ var temp = 0;
+
+ // Extract the next digit and multiply by the appropriate multiplier.
+ for (var i = 0; i < 7; i++) {
+ temp = Number(vatnumber.charAt(i)) * multipliers[i];
+ if (temp > 9)
+ total += Math.floor(temp / 10) + temp % 10;
+ else
+ total += temp;
+ }
+
+ // Establish check digit.
+ total = 10 - (total + 4) % 10;
+ if (total == 10) total = 0;
+
+ // Compare it with the last character of the VAT number. If it's the same, then it's valid.
+ if (total == vatnumber.slice(7, 8))
+ return true;
+ else
+ return false;
+ }
+
+ function BEVATCheckDigit(vatnumber) {
+
+ // Checks the check digits of a Belgium VAT number.
+
+ // Nine digit numbers have a 0 inserted at the front.
+ if (vatnumber.length == 9) vatnumber = "0" + vatnumber;
+
+ if (vatnumber.slice(1, 2) == 0) return false;
+
+ // Modulus 97 check on last nine digits
+ if (97 - vatnumber.slice(0, 8) % 97 == vatnumber.slice(8, 10))
+ return true;
+ else
+ return false;
+ }
+
+ function BGVATCheckDigit(vatnumber) {
+ var temp, total, multipliers, i;
+
+ // Checks the check digits of a Bulgarian VAT number.
+
+ if (vatnumber.length == 9) {
+ // Check the check digit of 9 digit Bulgarian VAT numbers.
+ total = 0;
+
+ // First try to calculate the check digit using the first multipliers
+ temp = 0;
+ for (i = 0; i < 8; i++) temp += Number(vatnumber.charAt(i)) * (i + 1);
+
+ // See if we have a check digit yet
+ total = temp % 11;
+ if (total != 10) {
+ if (total == vatnumber.slice(8))
+ return true;
+ else
+ return false;
+ }
+
+ // We got a modulus of 10 before so we have to keep going. Calculate the new check digit using
+ // the different multipliers
+ temp = 0;
+ for (i = 0; i < 8; i++) temp += Number(vatnumber.charAt(i)) * (i + 3);
+
+ // See if we have a check digit yet. If we still have a modulus of 10, set it to 0.
+ total = temp % 11;
+ if (total == 10) total = 0;
+ if (total == vatnumber.slice(8))
+ return true;
+ else
+ return false;
+ }
+
+ // 10 digit VAT code - see if it relates to a standard physical person
+ if ((/^\d\d[0-5]\d[0-3]\d\d{4}$/).test(vatnumber)) {
+
+ // Check month
+ var month = Number(vatnumber.slice(2, 4));
+ if ((month > 0 && month < 13) || (month > 20 && month < 33) || (month > 40 && month < 53)) {
+
+ // Extract the next digit and multiply by the counter.
+ multipliers = [2, 4, 8, 5, 10, 9, 7, 3, 6];
+ total = 0;
+ for (i = 0; i < 9; i++) total += Number(vatnumber.charAt(i)) * multipliers[i];
+
+ // Establish check digit.
+ total = total % 11;
+ if (total == 10) total = 0;
+
+ // Check to see if the check digit given is correct, If not, try next type of person
+ if (total == vatnumber.substr(9, 1)) return true;
+ }
+ }
+
+ // It doesn't relate to a standard physical person - see if it relates to a foreigner.
+
+ // Extract the next digit and multiply by the counter.
+ multipliers = [21, 19, 17, 13, 11, 9, 7, 3, 1];
+ total = 0;
+ for (i = 0; i < 9; i++) total += Number(vatnumber.charAt(i)) * multipliers[i];
+
+ // Check to see if the check digit given is correct, If not, try next type of person
+ if (total % 10 == vatnumber.substr(9, 1)) return true;
+
+ // Finally, if not yet identified, see if it conforms to a miscellaneous VAT number
+
+ // Extract the next digit and multiply by the counter.
+ multipliers = [4, 3, 2, 7, 6, 5, 4, 3, 2];
+ total = 0;
+ for (i = 0; i < 9; i++) total += Number(vatnumber.charAt(i)) * multipliers[i];
+
+ // Establish check digit.
+ total = 11 - total % 11;
+ if (total == 10) return false;
+ if (total == 11) total = 0;
+
+ // Check to see if the check digit given is correct, If not, we have an error with the VAT number
+ if (total == vatnumber.substr(9, 1))
+ return true;
+ else
+ return false;
+ }
+
+ function CHEVATCheckDigit(vatnumber) {
+
+ // Checks the check digits of a Swiss VAT number.
+
+ // Extract the next digit and multiply by the counter.
+ var multipliers = [5, 4, 3, 2, 7, 6, 5, 4];
+ var total = 0;
+ for (var i = 0; i < 8; i++) total += Number(vatnumber.charAt(i)) * multipliers[i];
+
+ // Establish check digit.
+ total = 11 - total % 11;
+ if (total == 10) return false;
+ if (total == 11) total = 0;
+
+ // Check to see if the check digit given is correct, If not, we have an error with the VAT number
+ if (total == vatnumber.substr(8, 1))
+ return true;
+ else
+ return false;
+ }
+
+ function CYVATCheckDigit(vatnumber) {
+
+ // Checks the check digits of a Cypriot VAT number.
+
+ // Not allowed to start with '12'
+ if (Number(vatnumber.slice(0, 2) == 12)) return false;
+
+ // Extract the next digit and multiply by the counter.
+ var total = 0;
+ for (var i = 0; i < 8; i++) {
+ var temp = Number(vatnumber.charAt(i));
+ if (i % 2 == 0) {
+ switch (temp) {
+ case 0:
+ temp = 1;
+ break;
+ case 1:
+ temp = 0;
+ break;
+ case 2:
+ temp = 5;
+ break;
+ case 3:
+ temp = 7;
+ break;
+ case 4:
+ temp = 9;
+ break;
+ default:
+ temp = temp * 2 + 3;
+ }
+ }
+ total += temp;
+ }
+
+ // Establish check digit using modulus 26, and translate to char. equivalent.
+ total = total % 26;
+ total = String.fromCharCode(total + 65);
+
+ // Check to see if the check digit given is correct
+ if (total == vatnumber.substr(8, 1))
+ return true;
+ else
+ return false;
+ }
+
+ function CZVATCheckDigit(vatnumber) {
+
+ // Checks the check digits of a Czech Republic VAT number.
+
+ var total = 0;
+ var multipliers = [8, 7, 6, 5, 4, 3, 2];
+
+ var czexp = new Array();
+ czexp[0] = (/^\d{8}$/); // 8 digit legal entities
+ // Note - my specification says that that the following should have a range of 0-3 in the fourth
+ // digit, but the valid number CZ395601439 did not confrm, so a range of 0-9 has been allowed.
+ czexp[1] = (/^[0-5][0-9][0|1|5|6][0-9][0-3][0-9]\d{3}$/); // 9 digit individuals
+ czexp[2] = (/^6\d{8}$/); // 9 digit individuals (Special cases)
+ czexp[3] = (/^\d{2}[0-3|5-8][0-9][0-3][0-9]\d{4}$/); // 10 digit individuals
+ var i = 0;
+ var a;
+
+ // Legal entities
+ if (czexp[0].test(vatnumber)) {
+
+ // Extract the next digit and multiply by the counter.
+ for (i = 0; i < 7; i++) total += Number(vatnumber.charAt(i)) * multipliers[i];
+
+ // Establish check digit.
+ total = 11 - total % 11;
+ if (total == 10) total = 0;
+ if (total == 11) total = 1;
+
+ // Compare it with the last character of the VAT number. If it's the same, then it's valid.
+ if (total == vatnumber.slice(7, 8))
+ return true;
+ else
+ return false;
+ }
+
+ // Individuals type 1 (Standard) - 9 digits without check digit
+ else if (czexp[1].test(vatnumber)) {
+ if (Number(vatnumber.slice(0, 2)) > 62) return false;
+ return true;
+ }
+
+ // Individuals type 2 (Special Cases) - 9 digits including check digit
+ else if (czexp[2].test(vatnumber)) {
+
+ // Extract the next digit and multiply by the counter.
+ for (i = 0; i < 7; i++) total += Number(vatnumber.charAt(i + 1)) * multipliers[i];
+
+ // Establish check digit pointer into lookup table
+ if (total % 11 == 0)
+ a = total + 11;
+ else
+ a = Math.ceil(total / 11) * 11;
+ var pointer = a - total;
+
+ // Convert calculated check digit according to a lookup table;
+ var lookup = [8, 7, 6, 5, 4, 3, 2, 1, 0, 9, 8];
+ if (lookup[pointer - 1] == vatnumber.slice(8, 9))
+ return true;
+ else
+ return false;
+ }
+
+ // Individuals type 3 - 10 digits
+ else if (czexp[3].test(vatnumber)) {
+ var temp = Number(vatnumber.slice(0, 2)) + Number(vatnumber.slice(2, 4)) + Number(vatnumber.slice(4, 6)) + Number(vatnumber.slice(6, 8)) + Number(vatnumber.slice(8));
+ if (temp % 11 == 0 && Number(vatnumber) % 11 == 0)
+ return true;
+ else
+ return false;
+ }
+
+ // else error
+ return false;
+ }
+
+ function DEVATCheckDigit(vatnumber) {
+
+ // Checks the check digits of a German VAT number.
+
+ var product = 10;
+ var sum = 0;
+ var checkdigit = 0;
+ for (var i = 0; i < 8; i++) {
+
+ // Extract the next digit and implement peculiar algorithm!.
+ sum = (Number(vatnumber.charAt(i)) + product) % 10;
+ if (sum == 0) {
+ sum = 10;
+ }
+ product = (2 * sum) % 11;
+ }
+
+ // Establish check digit.
+ if (11 - product == 10) {
+ checkdigit = 0;
+ } else {
+ checkdigit = 11 - product;
+ }
+
+ // Compare it with the last two characters of the VAT number. If the same, then it is a valid
+ // check digit.
+ if (checkdigit == vatnumber.slice(8, 9))
+ return true;
+ else
+ return false;
+ }
+
+ function DKVATCheckDigit(vatnumber) {
+
+ // Checks the check digits of a Danish VAT number.
+
+ var total = 0;
+ var multipliers = [2, 7, 6, 5, 4, 3, 2, 1];
+
+ // Extract the next digit and multiply by the counter.
+ for (var i = 0; i < 8; i++) total += Number(vatnumber.charAt(i)) * multipliers[i];
+
+ // Establish check digit.
+ total = total % 11;
+
+ // The remainder should be 0 for it to be valid..
+ if (total == 0)
+ return true;
+ else
+ return false;
+ }
+
+ function EEVATCheckDigit(vatnumber) {
+
+ // Checks the check digits of an Estonian VAT number.
+
+ var total = 0;
+ var multipliers = [3, 7, 1, 3, 7, 1, 3, 7];
+
+ // Extract the next digit and multiply by the counter.
+ for (var i = 0; i < 8; i++) total += Number(vatnumber.charAt(i)) * multipliers[i];
+
+ // Establish check digits using modulus 10.
+ total = 10 - total % 10;
+ if (total == 10) total = 0;
+
+ // Compare it with the last character of the VAT number. If it's the same, then it's valid.
+ if (total == vatnumber.slice(8, 9))
+ return true;
+ else
+ return false;
+ }
+
+ function ELVATCheckDigit(vatnumber) {
+
+ // Checks the check digits of a Greek VAT number.
+
+ var total = 0;
+ var multipliers = [256, 128, 64, 32, 16, 8, 4, 2];
+
+ //eight character numbers should be prefixed with an 0.
+ if (vatnumber.length == 8) {
+ vatnumber = "0" + vatnumber;
+ }
+
+ // Extract the next digit and multiply by the counter.
+ for (var i = 0; i < 8; i++) total += Number(vatnumber.charAt(i)) * multipliers[i];
+
+ // Establish check digit.
+ total = total % 11;
+ if (total > 9) {
+ total = 0;
+ }
+
+ // Compare it with the last character of the VAT number. If it's the same, then it's valid.
+ if (total == vatnumber.slice(8, 9))
+ return true;
+ else
+ return false;
+ }
+
+ function ESVATCheckDigit(vatnumber) {
+
+ // Checks the check digits of a Spanish VAT number.
+
+ var total = 0;
+ var temp = 0;
+ var multipliers = [2, 1, 2, 1, 2, 1, 2];
+ var esexp = new Array();
+ esexp[0] = (/^[A-H|J|U|V]\d{8}$/);
+ esexp[1] = (/^[A-H|N-S|W]\d{7}[A-J]$/);
+ esexp[2] = (/^[0-9|Y|Z]\d{7}[A-Z]$/);
+ esexp[3] = (/^[K|L|M|X]\d{7}[A-Z]$/);
+ var i = 0;
+
+ // National juridical entities
+ if (esexp[0].test(vatnumber)) {
+
+ // Extract the next digit and multiply by the counter.
+ for (i = 0; i < 7; i++) {
+ temp = Number(vatnumber.charAt(i + 1)) * multipliers[i];
+ if (temp > 9)
+ total += Math.floor(temp / 10) + temp % 10;
+ else
+ total += temp;
+ }
+ // Now calculate the check digit itself.
+ total = 10 - total % 10;
+ if (total == 10) {
+ total = 0;
+ }
+
+ // Compare it with the last character of the VAT number. If it's the same, then it's valid.
+ if (total == vatnumber.slice(8, 9))
+ return true;
+ else
+ return false;
+ }
+
+ // Juridical entities other than national ones
+ else if (esexp[1].test(vatnumber)) {
+
+ // Extract the next digit and multiply by the counter.
+ for (i = 0; i < 7; i++) {
+ temp = Number(vatnumber.charAt(i + 1)) * multipliers[i];
+ if (temp > 9)
+ total += Math.floor(temp / 10) + temp % 10;
+ else
+ total += temp;
+ }
+
+ // Now calculate the check digit itself.
+ total = 10 - total % 10;
+ total = String.fromCharCode(total + 64);
+
+ // Compare it with the last character of the VAT number. If it's the same, then it's valid.
+ if (total == vatnumber.slice(8, 9))
+ return true;
+ else
+ return false;
+ }
+
+ // Personal number (NIF) (starting with numeric of Y or Z)
+ else if (esexp[2].test(vatnumber)) {
+ var tempnumber = vatnumber;
+ if (tempnumber.substring(0, 1) == 'Y') tempnumber = tempnumber.replace(/Y/, "1");
+ if (tempnumber.substring(0, 1) == 'Z') tempnumber = tempnumber.replace(/Z/, "2");
+ return tempnumber.charAt(8) == 'TRWAGMYFPDXBNJZSQVHLCKE'.charAt(Number(tempnumber.substring(0, 8)) % 23);
+ }
+
+ // Personal number (NIF) (starting with K, L, M, or X)
+ else if (esexp[3].test(vatnumber)) {
+ return vatnumber.charAt(8) == 'TRWAGMYFPDXBNJZSQVHLCKE'.charAt(Number(vatnumber.substring(1, 8)) % 23);
+ }
+
+ else return false;
+ }
+
+ function EUVATCheckDigit(vatnumber) {
+
+ // We know little about EU numbers apart from the fact that the first 3 digits represent the
+ // country, and that there are nine digits in total.
+ return true;
+ }
+
+ function FIVATCheckDigit(vatnumber) {
+
+ // Checks the check digits of a Finnish VAT number.
+
+ var total = 0;
+ var multipliers = [7, 9, 10, 5, 8, 4, 2];
+
+ // Extract the next digit and multiply by the counter.
+ for (var i = 0; i < 7; i++) total += Number(vatnumber.charAt(i)) * multipliers[i];
+
+ // Establish check digit.
+ total = 11 - total % 11;
+ if (total > 9) {
+ total = 0;
+ }
+
+ // Compare it with the last character of the VAT number. If it's the same, then it's valid.
+ if (total == vatnumber.slice(7, 8))
+ return true;
+ else
+ return false;
+ }
+
+ function FRVATCheckDigit(vatnumber) {
+
+ // Checks the check digits of a French VAT number.
+
+ if (!(/^\d{11}$/).test(vatnumber)) return true;
+
+ // Extract the last nine digits as an integer.
+ var total = vatnumber.substring(2);
+
+ // Establish check digit.
+ total = (total * 100 + 12) % 97;
+
+ // Compare it with the last character of the VAT number. If it's the same, then it's valid.
+ if (total == vatnumber.slice(0, 2))
+ return true;
+ else
+ return false;
+ }
+
+ function GBVATCheckDigit(vatnumber) {
+
+ // Checks the check digits of a UK VAT number.
+
+ var multipliers = [8, 7, 6, 5, 4, 3, 2];
+
+ // Government departments
+ if (vatnumber.substr(0, 2) == 'GD') {
+ if (vatnumber.substr(2, 3) < 500)
+ return true;
+ else
+ return false;
+ }
+
+ // Health authorities
+ if (vatnumber.substr(0, 2) == 'HA') {
+ if (vatnumber.substr(2, 3) > 499)
+ return true;
+ else
+ return false;
+ }
+
+ // Standard and commercial numbers
+ var total = 0;
+
+ // 0 VAT numbers disallowed!
+ if (Number(vatnumber.slice(0)) == 0) return false;
+
+ // Check range is OK for modulus 97 calculation
+ var no = Number(vatnumber.slice(0, 7));
+
+ // Extract the next digit and multiply by the counter.
+ for (var i = 0; i < 7; i++) total += Number(vatnumber.charAt(i)) * multipliers[i];
+
+ // Old numbers use a simple 97 modulus, but new numbers use an adaptation of that (less 55). Our
+ // VAT number could use either system, so we check it against both.
+
+ // Establish check digits by subtracting 97 from total until negative.
+ var cd = total;
+ while (cd > 0) {
+ cd = cd - 97;
+ }
+
+ // Get the absolute value and compare it with the last two characters of the VAT number. If the
+ // same, then it is a valid traditional check digit. However, even then the number must fit within
+ // certain specified ranges.
+ cd = Math.abs(cd);
+ if (cd == vatnumber.slice(7, 9) && no < 9990001 && (no < 100000 || no > 999999) && (no < 9490001 || no > 9700000)) return true;
+
+ // Now try the new method by subtracting 55 from the check digit if we can - else add 42
+ if (cd >= 55)
+ cd = cd - 55;
+ else
+ cd = cd + 42;
+ if (cd == vatnumber.slice(7, 9) && no > 1000000)
+ return true;
+ else
+ return false;
+ }
+
+ function HRVATCheckDigit(vatnumber) {
+
+ // Checks the check digits of a Croatian VAT number using ISO 7064, MOD 11-10 for check digit.
+
+ var product = 10;
+ var sum = 0;
+ var checkdigit = 0;
+
+ for (var i = 0; i < 10; i++) {
+
+ // Extract the next digit and implement the algorithm
+ sum = (Number(vatnumber.charAt(i)) + product) % 10;
+ if (sum == 0) {
+ sum = 10;
+ }
+ product = (2 * sum) % 11;
+ }
+
+ // Now check that we have the right check digit
+ if ((product + vatnumber.slice(10, 11) * 1) % 10 == 1)
+ return true;
+ else
+ return false;
+ }
+
+ function HUVATCheckDigit(vatnumber) {
+
+ // Checks the check digits of a Hungarian VAT number.
+
+ var total = 0;
+ var multipliers = [9, 7, 3, 1, 9, 7, 3];
+
+ // Extract the next digit and multiply by the counter.
+ for (var i = 0; i < 7; i++) total += Number(vatnumber.charAt(i)) * multipliers[i];
+
+ // Establish check digit.
+ total = 10 - total % 10;
+ if (total == 10) total = 0;
+
+ // Compare it with the last character of the VAT number. If it's the same, then it's valid.
+ if (total == vatnumber.slice(7, 8))
+ return true;
+ else
+ return false;
+ }
+
+ function IEVATCheckDigit(vatnumber) {
+
+ // Checks the check digits of an Irish VAT number.
+
+ var total = 0;
+ var multipliers = [8, 7, 6, 5, 4, 3, 2];
+
+ // If the code is type 1 format, we need to convert it to the new before performing the validation.
+ if (/^\d[A-Z\*\+]/.test(vatnumber)) vatnumber = "0" + vatnumber.substring(2, 7) + vatnumber.substring(0, 1) + vatnumber.substring(7, 8);
+
+ // Extract the next digit and multiply by the counter.
+ for (var i = 0; i < 7; i++) total += Number(vatnumber.charAt(i)) * multipliers[i];
+
+ // If the number is type 3 then we need to include the trailing A or H in the calculation
+ if (/^\d{7}[A-Z][AH]$/.test(vatnumber)) {
+
+ // Add in a multiplier for the character A (1*9=9) or H (8*9=72)
+ if (vatnumber.charAt(8) == 'H')
+ total += 72;
+ else
+ total += 9;
+ }
+
+ // Establish check digit using modulus 23, and translate to char. equivalent.
+ total = total % 23;
+ if (total == 0)
+ total = "W";
+ else
+ total = String.fromCharCode(total + 64);
+
+ // Compare it with the eighth character of the VAT number. If it's the same, then it's valid.
+ if (total == vatnumber.slice(7, 8))
+ return true;
+ else
+ return false;
+ }
+
+ function ITVATCheckDigit(vatnumber) {
+
+ // Checks the check digits of an Italian VAT number.
+
+ var total = 0;
+ var multipliers = [1, 2, 1, 2, 1, 2, 1, 2, 1, 2];
+ var temp;
+
+ // The last three digits are the issuing office, and cannot exceed more 201, unless 999 or 888
+ if (Number(vatnumber.slice(0, 7)) == 0) return false;
+ temp = Number(vatnumber.slice(7, 10));
+ if ((temp < 1) || (temp > 201) && temp != 999 && temp != 888) return false;
+
+ // Extract the next digit and multiply by the appropriate
+ for (var i = 0; i < 10; i++) {
+ temp = Number(vatnumber.charAt(i)) * multipliers[i];
+ if (temp > 9)
+ total += Math.floor(temp / 10) + temp % 10;
+ else
+ total += temp;
+ }
+
+ // Establish check digit.
+ total = 10 - total % 10;
+ if (total > 9) {
+ total = 0;
+ }
+
+ // Compare it with the last character of the VAT number. If it's the same, then it's valid.
+ if (total == vatnumber.slice(10, 11))
+ return true;
+ else
+ return false;
+ }
+
+ function LTVATCheckDigit(vatnumber) {
+
+ // Checks the check digits of a Lithuanian VAT number.
+ var total, multipliers, i;
+
+ // 9 character VAT numbers are for legal persons
+ if (vatnumber.length == 9) {
+
+ // 8th character must be one
+ if (!(/^\d{7}1/).test(vatnumber)) return false;
+
+ // Extract the next digit and multiply by the counter+1.
+ total = 0;
+ for (i = 0; i < 8; i++) total += Number(vatnumber.charAt(i)) * (i + 1);
+
+ // Can have a double check digit calculation!
+ if (total % 11 == 10) {
+ multipliers = [3, 4, 5, 6, 7, 8, 9, 1];
+ total = 0;
+ for (i = 0; i < 8; i++) total += Number(vatnumber.charAt(i)) * multipliers[i];
+ }
+
+ // Establish check digit.
+ total = total % 11;
+ if (total == 10) {
+ total = 0;
+ }
+
+ // Compare it with the last character of the VAT number. If it's the same, then it's valid.
+ if (total == vatnumber.slice(8, 9))
+ return true;
+ else
+ return false;
+ }
+
+ // 12 character VAT numbers are for temporarily registered taxpayers
+ else {
+
+ // 11th character must be one
+ if (!(/^\d{10}1/).test(vatnumber)) return false;
+
+ // Extract the next digit and multiply by the counter+1.
+ total = 0;
+ multipliers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2];
+ for (i = 0; i < 11; i++) total += Number(vatnumber.charAt(i)) * multipliers[i];
+
+ // Can have a double check digit calculation!
+ if (total % 11 == 10) {
+ multipliers = [3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4];
+ total = 0;
+ for (i = 0; i < 11; i++) total += Number(vatnumber.charAt(i)) * multipliers[i];
+ }
+
+ // Establish check digit.
+ total = total % 11;
+ if (total == 10) {
+ total = 0;
+ }
+
+ // Compare it with the last character of the VAT number. If it's the same, then it's valid.
+ if (total == vatnumber.slice(11, 12))
+ return true;
+ else
+ return false;
+ }
+ }
+
+ function LUVATCheckDigit(vatnumber) {
+
+ // Checks the check digits of a Luxembourg VAT number.
+
+ if (vatnumber.slice(0, 6) % 89 == vatnumber.slice(6, 8))
+ return true;
+ else
+ return false;
+ }
+
+ function LVVATCheckDigit(vatnumber) {
+
+ // Checks the check digits of a Latvian VAT number.
+
+ // Differentiate between legal entities and natural bodies. For the latter we simply check that
+ // the first six digits correspond to valid DDMMYY dates.
+ if ((/^[0-3]/).test(vatnumber)) {
+ if ((/^[0-3][0-9][0-1][0-9]/).test(vatnumber))
+ return true;
+ else
+ return false;
+ }
+
+ else {
+
+ var total = 0;
+ var multipliers = [9, 1, 4, 8, 3, 10, 2, 5, 7, 6];
+
+ // Extract the next digit and multiply by the counter.
+ for (var i = 0; i < 10; i++) total += Number(vatnumber.charAt(i)) * multipliers[i];
+
+ // Establish check digits by getting modulus 11.
+ if (total % 11 == 4 && vatnumber[0] == 9) total = total - 45;
+ if (total % 11 == 4)
+ total = 4 - total % 11;
+ else if (total % 11 > 4)
+ total = 14 - total % 11;
+ else if (total % 11 < 4)
+ total = 3 - total % 11;
+
+ // Compare it with the last character of the VAT number. If it's the same, then it's valid.
+ if (total == vatnumber.slice(10, 11))
+ return true;
+ else
+ return false;
+ }
+ }
+
+ function MTVATCheckDigit(vatnumber) {
+
+ // Checks the check digits of a Maltese VAT number.
+
+ var total = 0;
+ var multipliers = [3, 4, 6, 7, 8, 9];
+
+ // Extract the next digit and multiply by the counter.
+ for (var i = 0; i < 6; i++) total += Number(vatnumber.charAt(i)) * multipliers[i];
+
+ // Establish check digits by getting modulus 37.
+ total = 37 - total % 37;
+
+ // Compare it with the last character of the VAT number. If it's the same, then it's valid.
+ if (total == vatnumber.slice(6, 8) * 1)
+ return true;
+ else
+ return false;
+ }
+
+ function NLVATCheckDigit(vatnumber) {
+
+ // Checks the check digits of a Dutch VAT number.
+
+ var total = 0;
+ var multipliers = [9, 8, 7, 6, 5, 4, 3, 2];
+
+ // Extract the next digit and multiply by the counter.
+ for (var i = 0; i < 8; i++) total += Number(vatnumber.charAt(i)) * multipliers[i];
+
+ // Establish check digits by getting modulus 11.
+ total = total % 11;
+ if (total > 9) {
+ total = 0;
+ }
+
+ // Compare it with the last character of the VAT number. If it's the same, then it's valid.
+ if (total == vatnumber.slice(8, 9))
+ return true;
+ else
+ return false;
+ }
+
+ function NOVATCheckDigit(vatnumber) {
+
+ // Checks the check digits of a Norwegian VAT number.
+ // See http://www.brreg.no/english/coordination/number.html
+
+ var total = 0;
+ var multipliers = [3, 2, 7, 6, 5, 4, 3, 2];
+
+ // Extract the next digit and multiply by the counter.
+ for (var i = 0; i < 8; i++) total += Number(vatnumber.charAt(i)) * multipliers[i];
+
+ // Establish check digits by getting modulus 11. Check digits > 9 are invalid
+ total = 11 - total % 11;
+ if (total == 11) {
+ total = 0;
+ }
+ if (total < 10) {
+
+ // Compare it with the last character of the VAT number. If it's the same, then it's valid.
+ if (total == vatnumber.slice(8, 9))
+ return true;
+ else
+ return false;
+ }
+ }
+
+ function PLVATCheckDigit(vatnumber) {
+
+ // Checks the check digits of a Polish VAT number.
+
+ var total = 0;
+ var multipliers = [6, 5, 7, 2, 3, 4, 5, 6, 7];
+
+ // Extract the next digit and multiply by the counter.
+ for (var i = 0; i < 9; i++) total += Number(vatnumber.charAt(i)) * multipliers[i];
+
+ // Establish check digits subtracting modulus 11 from 11.
+ total = total % 11;
+ if (total > 9) {
+ total = 0;
+ }
+
+ // Compare it with the last character of the VAT number. If it's the same, then it's valid.
+ if (total == vatnumber.slice(9, 10))
+ return true;
+ else
+ return false;
+ }
+
+ function PTVATCheckDigit(vatnumber) {
+
+ // Checks the check digits of a Portugese VAT number.
+
+ var total = 0;
+ var multipliers = [9, 8, 7, 6, 5, 4, 3, 2];
+
+ // Extract the next digit and multiply by the counter.
+ for (var i = 0; i < 8; i++) total += Number(vatnumber.charAt(i)) * multipliers[i];
+
+ // Establish check digits subtracting modulus 11 from 11.
+ total = 11 - total % 11;
+ if (total > 9) {
+ total = 0;
+ }
+
+ // Compare it with the last character of the VAT number. If it's the same, then it's valid.
+ if (total == vatnumber.slice(8, 9))
+ return true;
+ else
+ return false;
+ }
+
+ function ROVATCheckDigit(vatnumber) {
+
+ // Checks the check digits of a Romanian VAT number.
+
+ var multipliers = [7, 5, 3, 2, 1, 7, 5, 3, 2];
+
+ // Extract the next digit and multiply by the counter.
+ var VATlen = vatnumber.length;
+ multipliers = multipliers.slice(10 - VATlen);
+ var total = 0;
+ for (var i = 0; i < vatnumber.length - 1; i++) {
+ total += Number(vatnumber.charAt(i)) * multipliers[i];
+ }
+
+ // Establish check digits by getting modulus 11.
+ total = (10 * total) % 11;
+ if (total == 10) total = 0;
+
+ // Compare it with the last character of the VAT number. If it's the same, then it's valid.
+ if (total == vatnumber.slice(vatnumber.length - 1, vatnumber.length))
+ return true;
+ else
+ return false;
+ }
+
+ function RSVATCheckDigit(vatnumber) {
+
+ // Checks the check digits of a Serbian VAT number using ISO 7064, MOD 11-10 for check digit.
+
+ var product = 10;
+ var sum = 0;
+ var checkdigit = 0;
+
+ for (var i = 0; i < 8; i++) {
+
+ // Extract the next digit and implement the algorithm
+ sum = (Number(vatnumber.charAt(i)) + product) % 10;
+ if (sum == 0) {
+ sum = 10;
+ }
+ product = (2 * sum) % 11;
+ }
+
+ // Now check that we have the right check digit
+ if ((product + vatnumber.slice(8, 9) * 1) % 10 == 1)
+ return true;
+ else
+ return false;
+ }
+
+ function RUVATCheckDigit(vatnumber) {
+
+ // Checks the check digits of a Russian INN number
+ // See http://russianpartner.biz/test_inn.html for algorithm
+
+ var i;
+
+ // 10 digit INN numbers
+ if (vatnumber.length == 10) {
+ var total = 0;
+ var multipliers = [2, 4, 10, 3, 5, 9, 4, 6, 8, 0];
+ for (i = 0; i < 10; i++) {
+ total += Number(vatnumber.charAt(i)) * multipliers[i];
+ }
+ total = total % 11;
+ if (total > 9) {
+ total = total % 10;
+ }
+
+ // Compare it with the last character of the VAT number. If it is the same, then it's valid
+ if (total == vatnumber.slice(9, 10))
+ return true;
+ else
+ return false;
+
+ // 12 digit INN numbers
+ } else if (vatnumber.length == 12) {
+ var total1 = 0;
+ var multipliers1 = [7, 2, 4, 10, 3, 5, 9, 4, 6, 8, 0];
+ var total2 = 0;
+ var multipliers2 = [3, 7, 2, 4, 10, 3, 5, 9, 4, 6, 8, 0];
+
+ for (i = 0; i < 11; i++) total1 += Number(vatnumber.charAt(i)) * multipliers1[i];
+ total1 = total1 % 11;
+ if (total1 > 9) {
+ total1 = total1 % 10;
+ }
+
+ for (i = 0; i < 11; i++) total2 += Number(vatnumber.charAt(i)) * multipliers2[i];
+ total2 = total2 % 11;
+ if (total2 > 9) {
+ total2 = total2 % 10;
+ }
+
+ // Compare the first check with the 11th character and the second check with the 12th and last
+ // character of the VAT number. If they're both the same, then it's valid
+ if ((total1 == vatnumber.slice(10, 11)) && (total2 == vatnumber.slice(11, 12)))
+ return true;
+ else
+ return false;
+ }
+ }
+
+ function SEVATCheckDigit(vatnumber) {
+ var i;
+
+ // Calculate R where R = R1 + R3 + R5 + R7 + R9, and Ri = INT(Ci/5) + (Ci*2) modulo 10
+ var R = 0;
+ var digit;
+ for (i = 0; i < 9; i = i + 2) {
+ digit = Number(vatnumber.charAt(i));
+ R += Math.floor(digit / 5) + ((digit * 2) % 10);
+ }
+
+ // Calculate S where S = C2 + C4 + C6 + C8
+ var S = 0;
+ for (i = 1; i < 9; i = i + 2) S += Number(vatnumber.charAt(i));
+
+ // Calculate the Check Digit
+ var cd = (10 - (R + S) % 10) % 10;
+
+ // Compare it with the last character of the VAT number. If it's the same, then it's valid.
+ if (cd == vatnumber.slice(9, 10))
+ return true;
+ else
+ return false;
+ }
+
+ function SIVATCheckDigit(vatnumber) {
+
+ // Checks the check digits of a Slovenian VAT number.
+
+ var total = 0;
+ var multipliers = [8, 7, 6, 5, 4, 3, 2];
+
+ // Extract the next digit and multiply by the counter.
+ for (var i = 0; i < 7; i++) total += Number(vatnumber.charAt(i)) * multipliers[i];
+
+ // Establish check digits using modulus 11
+ total = 11 - total % 11;
+ if (total == 10) {
+ total = 0;
+ }
+
+ // Compare the number with the last character of the VAT number. If it is the
+ // same, then it's a valid check digit.
+ if (total != 11 && total == vatnumber.slice(7, 8))
+ return true;
+ else
+ return false;
+ }
+
+ function SKVATCheckDigit(vatnumber) {
+
+ // Checks the check digits of a Slovakian VAT number.
+
+ // Check that the modulus of the whole VAT number is 0 - else error
+ if (Number(vatnumber % 11) == 0)
+ return true;
+ else
+ return false;
+ }
+
+ return checkVATNumber;
+})(); \ No newline at end of file
diff --git a/addons/partner_autocomplete/static/src/js/iap_credit_checker_widget.js b/addons/partner_autocomplete/static/src/js/iap_credit_checker_widget.js
new file mode 100644
index 00000000000..ab25dab0029
--- /dev/null
+++ b/addons/partner_autocomplete/static/src/js/iap_credit_checker_widget.js
@@ -0,0 +1,94 @@
+odoo.define('iap.credit.checker', function (require) {
+'use strict';
+
+var widgetRegistry = require('web.widget_registry');
+var Widget = require('web.Widget');
+
+var core = require('web.core');
+var rpc = require('web.rpc');
+
+var QWeb = core.qweb;
+
+var IAPCreditChecker = Widget.extend({
+ className: 'o_field_iap_credit_checker',
+
+ /**
+ * @constructor
+ * Prepares the basic rendering of edit mode by setting the root to be a
+ * div.dropdown.open.
+ * @see FieldChar.init
+ */
+ init: function (parent, data, options) {
+ this._super.apply(this, arguments);
+ this.service_name = options.attrs.service_name;
+ },
+
+ /**
+ * @override
+ */
+ start: function () {
+ this.$widget = $(QWeb.render('partner_autocomplete.iap_credit_checker'));
+ this.$loading = this.$widget.find('.loading');
+ this.$sufficient = this.$widget.find('.sufficient');
+ this.$insufficient = this.$widget.find('.insufficient');
+ this.$buyLink = this.$widget.find('.oe_link');
+
+ this.$widget.appendTo(this.$el);
+
+ this._getLink();
+ this._getCredits();
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+ _getCredits: function () {
+ var self = this;
+ this._showLoading();
+
+ return rpc.query({
+ model: 'iap.account',
+ method: 'get_credits',
+ args: [this.service_name],
+ }, {
+ shadow: true,
+ }).then(function (credit) {
+ if (credit) self._showSufficient(credit);
+ else self._showInsufficient();
+ });
+ },
+
+ _getLink: function () {
+ var self = this;
+ return rpc.query({
+ model: 'iap.account',
+ method: 'get_credits_url',
+ args: [this.service_name],
+ }, {
+ shadow: true,
+ }).then(function (url) {
+ self.$buyLink.attr('href', url);
+ });
+ },
+
+ _showLoading: function () {
+ this.$loading.show();
+ this.$sufficient.hide();
+ this.$insufficient.hide();
+ },
+ _showSufficient: function (credits) {
+ this.$loading.hide();
+ this.$sufficient.show().find('.remaining_credits').text(credits);
+ this.$insufficient.hide();
+ },
+ _showInsufficient: function () {
+ this.$loading.hide();
+ this.$sufficient.hide();
+ this.$insufficient.show();
+ },
+});
+
+widgetRegistry.add('iap_credit_checker', IAPCreditChecker);
+
+return IAPCreditChecker;
+});
diff --git a/addons/partner_autocomplete/static/src/js/partner_autocomplete_core.js b/addons/partner_autocomplete/static/src/js/partner_autocomplete_core.js
new file mode 100644
index 00000000000..05bd10c9114
--- /dev/null
+++ b/addons/partner_autocomplete/static/src/js/partner_autocomplete_core.js
@@ -0,0 +1,331 @@
+odoo.define('partner.autocomplete.core', function (require) {
+'use strict';
+
+var rpc = require('web.rpc');
+var concurrency = require('web.concurrency');
+
+return {
+ _dropPreviousOdoo: new concurrency.DropPrevious(),
+ _dropPreviousClearbit: new concurrency.DropPrevious(),
+ _timeout : 1000, // Timeout for Clearbit autocomplete in ms
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Get list of companies via Autocomplete API
+ *
+ * @param {string} value
+ * @returns {Deferred}
+ * @private
+ */
+ autocomplete: function (value) {
+ value = value.trim();
+ var def = $.Deferred(),
+ isVAT = this._isVAT(value),
+ odooSuggestions = [],
+ clearbitSuggestions = [];
+
+ var odooPromise = this._getOdooSuggestions(value, isVAT).then(function (suggestions){
+ odooSuggestions = suggestions;
+ });
+
+ // Only get Clearbit suggestions if not a VAT number
+ var clearbitPromise = isVAT ? false : this._getClearbitSuggestions(value).then(function (suggestions){
+ clearbitSuggestions = suggestions;
+ });
+
+ var concatResults = function () {
+ // Add Clearbit result with Odoo result (with unique domain)
+ if (clearbitSuggestions && clearbitSuggestions.length) {
+ var websites = odooSuggestions.map(function (suggestion) {
+ return suggestion.website;
+ });
+ clearbitSuggestions.forEach(function (suggestion) {
+ if (websites.indexOf(suggestion.website) < 0) {
+ odooSuggestions.push(suggestion);
+ }
+ });
+ }
+
+ return def.resolve(odooSuggestions);
+ };
+
+ this._whenAll([odooPromise, clearbitPromise]).then(concatResults, concatResults);
+
+ return def;
+ },
+
+ /**
+ * Get enrichment data
+ *
+ * @param {Object} company
+ * @returns {Deferred}
+ * @private
+ */
+ enrichCompany: function (company) {
+ return rpc.query({
+ model: 'res.partner',
+ method: 'enrich_company',
+ args: [company.website, company.partner_gid, company.vat],
+ });
+ },
+
+ /**
+ * Get the company logo as Base 64 image from url
+ *
+ * @param {string} url
+ * @returns {Deferred}
+ * @private
+ */
+ getCompanyLogo: function (url) {
+ return this._getBase64Image(url).then(function (base64Image) {
+ // base64Image equals "data:" if image not available on given url
+ return base64Image ? base64Image.replace(/^data:image[^;]*;base64,?/, '') : false;
+ });
+ },
+
+ /**
+ * Get enriched data + logo before populating partner form
+ *
+ * @param {Object} company
+ * @returns {Deferred}
+ */
+ getCreateData: function (company) {
+ var removeUselessFields = function (company) {
+ var fields = 'label,description,domain,logo,legal_name'.split(',');
+ fields.forEach(function (field) {
+ delete company[field];
+ });
+
+ var notEmptyFields = "country_id,state_id".split(',');
+ notEmptyFields.forEach(function (field) {
+ if (!company[field]) delete company[field];
+ });
+ };
+
+ var def = $.Deferred();
+
+ // Fetch additional company info via Autocomplete Enrichment API
+ var enrichPromise = this.enrichCompany(company);
+
+ // Get logo
+ var logoPromise = company.logo ? this.getCompanyLogo(company.logo) : false;
+
+ $.when(enrichPromise, logoPromise).done(function (company_data, logo_data) {
+ if (_.isEmpty(company_data)) company_data = company;
+
+ // Delete attribute to avoid "Field_changed" errors
+ removeUselessFields(company_data);
+
+ // Assign VAT coming from parent VIES VAT query
+ if (company.vat) company_data.vat = company.vat;
+ def.resolve({
+ company: company_data,
+ logo: logo_data
+ });
+ });
+
+ return def;
+ },
+
+ /**
+ * Check connectivity
+ *
+ * @returns {boolean}
+ */
+ isOnline: function () {
+ return navigator && navigator.onLine;
+ },
+
+ /**
+ * Validate: Not empty and length > 1
+ *
+ * @param {string} search_val
+ * @param {string} onlyVAT : Only valid VAT Number search
+ * @returns {boolean}
+ * @private
+ */
+ validateSearchTerm: function (search_val, onlyVAT) {
+ if (onlyVAT) return this._isVAT(search_val);
+ else return search_val && search_val.length > 2;
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Returns a deferred which will be resolved with the base64 data of the
+ * image fetched from the given url.
+ *
+ * @private
+ * @param {string} url : the url where to find the image to fetch
+ * @returns {Deferred}
+ */
+ _getBase64Image: function (url) {
+ var def = $.Deferred();
+ var xhr = new XMLHttpRequest();
+ xhr.onload = function () {
+ var reader = new FileReader();
+ reader.onloadend = function () {
+ def.resolve(reader.result);
+ };
+ reader.readAsDataURL(xhr.response);
+ };
+ xhr.open('GET', url);
+ xhr.responseType = 'blob';
+ xhr.onerror = def.reject.bind(def);
+ xhr.send();
+ return def;
+ },
+
+ /**
+ * Use Clearbit Autocomplete API to return suggestions
+ *
+ * @param {string} value
+ * @returns {Deferred}
+ * @private
+ */
+ _getClearbitSuggestions: function (value) {
+ var url = 'https://autocomplete.clearbit.com/v1/companies/suggest?query=' + value;
+ var def = $.ajax({
+ url: url,
+ dataType: 'json',
+ timeout: this._timeout,
+ success: function (suggestions) {
+ suggestions.map(function (suggestion) {
+ suggestion.label = suggestion.name;
+ suggestion.website = suggestion.domain;
+ suggestion.description = suggestion.website;
+ return suggestion;
+ });
+ return suggestions;
+ },
+ });
+
+ return this._dropPreviousClearbit.add(def);
+ },
+
+ /**
+ * Use Odoo Autocomplete API to return suggestions
+ *
+ * @param {string} value
+ * @param {boolean} isVAT
+ * @returns {Deferred}
+ * @private
+ */
+ _getOdooSuggestions: function (value, isVAT) {
+ var method = isVAT ? 'read_by_vat' : 'autocomplete';
+
+ var def = rpc.query({
+ model: 'res.partner',
+ method: method,
+ args: [value],
+ }, {
+ shadow: true,
+ }).then(function (suggestions) {
+ suggestions.map(function (suggestion) {
+ suggestion.logo = suggestion.logo || '';
+ suggestion.label = suggestion.legal_name || suggestion.name;
+ if (suggestion.vat) suggestion.description = suggestion.vat;
+ else if (suggestion.website) suggestion.description = suggestion.website;
+
+ if (suggestion.country_id && suggestion.country_id.display_name) {
+ if (suggestion.description) suggestion.description += _.str.sprintf(' (%s)', suggestion.country_id.display_name);
+ else suggestion.description += suggestion.country_id.display_name;
+ }
+
+ return suggestion;
+ });
+ return suggestions;
+ });
+
+ return this._dropPreviousOdoo.add(def);
+ },
+ /**
+ * Check if searched value is possibly a VAT : 2 first chars = alpha + min 5 numbers
+ *
+ * @param {string} search_val
+ * @returns {boolean}
+ * @private
+ */
+ _isVAT: function (search_val) {
+ var str = this._sanitizeVAT(search_val);
+ return checkVATNumber(str);
+ },
+
+ /**
+ * Sanitize search value by removing all not alphanumeric
+ *
+ * @param {string} search_value
+ * @returns {string}
+ * @private
+ */
+ _sanitizeVAT: function (search_value) {
+ return search_value ? search_value.replace(/[^A-Za-z0-9]/g, '') : '';
+ },
+
+ /**
+ * Utility to wait for multiple promises
+ * $.when will reject all promises whenever a promise is rejected
+ * This utility will continue
+ * Source : https://gist.github.com/fearphage/4341799
+ *
+ * @param array
+ * @returns {*}
+ * @private
+ */
+ _whenAll: function (array) {
+ var slice = [].slice;
+
+ var
+ resolveValues = arguments.length === 1 && $.isArray(array)
+ ? array
+ : slice.call(arguments)
+ , length = resolveValues.length
+ , remaining = length
+ , deferred = $.Deferred()
+ , i = 0
+ , failed = 0
+ , rejectContexts = Array(length)
+ , rejectValues = Array(length)
+ , resolveContexts = Array(length)
+ , value
+ ;
+
+ function updateFunc(index, contexts, values) {
+ return function () {
+ !(values === resolveValues) && failed++;
+ deferred.notifyWith(
+ contexts[index] = this
+ , values[index] = slice.call(arguments)
+ );
+ if (!(--remaining)) {
+ deferred[(!failed ? 'resolve' : 'reject') + 'With'](contexts, values);
+ }
+ };
+ }
+
+ for (; i < length; i++) {
+ if ((value = resolveValues[i]) && $.isFunction(value.promise)) {
+ value.promise()
+ .done(updateFunc(i, resolveContexts, resolveValues))
+ .fail(updateFunc(i, rejectContexts, rejectValues))
+ ;
+ }
+ else {
+ deferred.notifyWith(this, value);
+ --remaining;
+ }
+ }
+
+ if (!remaining) {
+ deferred.resolveWith(resolveContexts, resolveValues);
+ }
+
+ return deferred.promise();
+ },
+};
+});
diff --git a/addons/partner_autocomplete/static/src/js/partner_autocomplete_fieldchar.js b/addons/partner_autocomplete/static/src/js/partner_autocomplete_fieldchar.js
new file mode 100644
index 00000000000..b1843ec8de1
--- /dev/null
+++ b/addons/partner_autocomplete/static/src/js/partner_autocomplete_fieldchar.js
@@ -0,0 +1,339 @@
+odoo.define('partner.autocomplete.fieldchar', function (require) {
+'use strict';
+
+var basic_fields = require('web.basic_fields');
+var core = require('web.core');
+var field_registry = require('web.field_registry');
+var Autocomplete = require('partner.autocomplete.core');
+
+var QWeb = core.qweb;
+
+var FieldChar = basic_fields.FieldChar;
+
+/**
+ * FieldChar extension to suggest existing companies when changing the company
+ * name on a res.partner view (indeed, it is designed to change the "name",
+ * "website" and "image" fields of records of this model).
+ */
+var FieldAutocomplete = FieldChar.extend({
+ className: 'o_field_partner_autocomplete',
+ debounceSuggestions: 400,
+ resetOnAnyFieldChange: true,
+
+ jsLibs: [
+ '/partner_autocomplete/static/lib/jsvat.js'
+ ],
+
+ events: _.extend({}, FieldChar.prototype.events, {
+ 'keyup': '_onKeyup',
+ 'mousedown .o_partner_autocomplete_suggestion': '_onMousedown',
+ 'focusout': '_onFocusout',
+ 'mouseenter .o_partner_autocomplete_suggestion': '_onHoverDropdown',
+ 'click .o_partner_autocomplete_suggestion': '_onSuggestionClicked',
+ }),
+
+ /**
+ * @constructor
+ * Prepares the basic rendering of edit mode by setting the root to be a
+ * div.dropdown.open.
+ * @see FieldChar.init
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+
+ // If the autocomplete is applied to vat field, only search valid vat number
+ this.onlyVAT = this.name === 'vat';
+
+ if (this.mode === 'edit') {
+ this.tagName = 'div';
+ this.className += ' dropdown open';
+ }
+
+ if (this.debounceSuggestions > 0) {
+ this._suggestCompanies = _.debounce(this._suggestCompanies.bind(this), this.debounceSuggestions);
+ }
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Check if the autocomplete should be active
+ * Active :
+ * - only when creating new record
+ * - on model res.partner and is_company=true
+ * - on model res.company
+ *
+ * @returns {boolean}
+ * @private
+ */
+ _isActive: function () {
+ return this.model === 'res.company' ||
+ (
+ this.model === 'res.partner'
+ && this.record.data.is_company
+ && !(this.record.data && this.record.data.id)
+ );
+ },
+
+ /**
+ *
+ * @private
+ */
+ _removeDropdown: function () {
+ if (this.$dropdown) {
+ this.$dropdown.remove();
+ this.$dropdown = undefined;
+ }
+ },
+
+ /**
+ * Adds the <input/> element and prepares it. Note: the dropdown rendering
+ * is handled outside of the rendering routine (but instead by reacting to
+ * user input).
+ *
+ * @override
+ * @private
+ */
+ _renderEdit: function () {
+ this.$el.empty();
+ // Prepare and add the input
+ this._prepareInput().appendTo(this.$el);
+ },
+
+ /**
+ * Selects the given company suggestions by notifying changes to the view
+ * for the "name", "website" and "image" fields. This is of course intended
+ * to work only with the "res.partner" form view.
+ *
+ * @private
+ * @param {Object} company
+ */
+ _selectCompany: function (company) {
+ var self = this;
+ Autocomplete.getCreateData(company).then(function (data) {
+ if (data.logo) {
+ var logoField = self.model === 'res.partner' ? 'image' : 'logo';
+ data.company[logoField] = data.logo;
+ }
+
+ // Some fields are unnecessary in res.company
+ if (self.model === 'res.company') {
+ var fields = 'comment,child_ids,bank_ids,additional_info'.split(',');
+ fields.forEach(function (field) {
+ delete data.company[field];
+ });
+ }
+
+ self._setOne2ManyField('child_ids', data.company.child_ids);
+ delete data.company.child_ids;
+
+ self._setOne2ManyField('bank_ids', data.company.bank_ids);
+ delete data.company.bank_ids;
+
+ self.trigger_up('field_changed', {
+ dataPointID: self.dataPointID,
+ changes: data.company,
+ });
+ });
+
+ // update the input's value directly
+ if (this.onlyVAT)
+ this.$input.val(this._formatValue(company.vat));
+ else
+ this.$input.val(this._formatValue(company.name));
+ this._removeDropdown();
+ },
+
+ _setOne2ManyField: function (field, list) {
+ var self = this;
+ var viewType = this.record.viewType;
+ if (list && this.record.fieldsInfo[viewType] && this.record.fieldsInfo[viewType][field]) {
+ list.forEach(function (item) {
+ var changes = {};
+ changes[field] = {
+ operation: 'CREATE',
+ data: item,
+ };
+
+ self.trigger_up('field_changed', {
+ dataPointID: self.dataPointID,
+ changes: changes,
+ });
+ });
+ }
+ },
+
+ /**
+ * Shows the dropdown with the suggestions. If one is
+ * already opened, it removes the old one before rerendering the dropdown.
+ *
+ * @private
+ */
+ _showDropdown: function () {
+ this._removeDropdown();
+ if (this.suggestions.length > 0) {
+ this.$dropdown = $(QWeb.render('partner_autocomplete.dropdown', {
+ suggestions: this.suggestions,
+ }));
+ this.$dropdown.appendTo(this.$el);
+ }
+ },
+
+ /**
+ * Shows suggestions according to the given value.
+ * Note: this method is debounced (@see init).
+ *
+ * @private
+ * @param {string} value - searched term
+ */
+ _suggestCompanies: function (value) {
+ var self = this;
+ if (Autocomplete.validateSearchTerm(value, this.onlyVAT) && Autocomplete.isOnline()) {
+ return Autocomplete.autocomplete(value).then(function (suggestions) {
+ if (suggestions && suggestions.length) {
+ self.suggestions = suggestions;
+ self._showDropdown();
+ } else {
+ self._removeDropdown();
+ }
+ });
+ } else {
+ this._removeDropdown();
+ }
+ },
+
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Called on focusout -> removes the suggestions dropdown.
+ *
+ * @private
+ */
+ _onFocusout: function () {
+ this._removeDropdown();
+ },
+
+ /**
+ * Called when hovering a suggestion in the dropdown -> sets it as active.
+ *
+ * @private
+ * @param {Event} e
+ */
+ _onHoverDropdown: function (e) {
+ this.$dropdown.find('.active').removeClass('active');
+ $(e.currentTarget).parent().addClass('active');
+ },
+
+ /**
+ * @override of FieldChar (called when the user is typing text)
+ * Checks the <input/> value and shows suggestions according to
+ * this value.
+ *
+ * @private
+ */
+ _onInput: function () {
+ this._super.apply(this, arguments);
+ if (this._isActive()) {
+ this._suggestCompanies(this.$input.val());
+ }
+ },
+
+ /**
+ * @override of FieldChar
+ * Changes the "up" and "down" key behavior when the dropdown is opened (to
+ * navigate through dropdown suggestions).
+ * Triggered by keydown to execute the navigation multiple times when the
+ * user keeps the "down" or "up" pressed.
+ *
+ * @private
+ * @param {Event} e
+ */
+ _onKeydown: function (e) {
+ switch (e.which) {
+ case $.ui.keyCode.UP:
+ case $.ui.keyCode.DOWN:
+ if (!this.$dropdown) {
+ break;
+ }
+ e.preventDefault();
+ var $suggestions = this.$dropdown.children();
+ var $active = $suggestions.filter('.active');
+ var $to;
+ if ($active.length) {
+ $to = e.which === $.ui.keyCode.DOWN ?
+ $active.next() :
+ $active.prev();
+ } else {
+ $to = $suggestions.first();
+ }
+ if ($to.length) {
+ $active.removeClass('active');
+ $to.addClass('active');
+ }
+ return;
+ }
+ this._super.apply(this, arguments);
+ },
+
+ /**
+ * Called on keyup events to:
+ * -> remove the suggestions dropdown when hitting the "escape" key
+ * -> select the highlighted suggestion when hitting the "enter" key
+ *
+ * @private
+ * @param {Event} e
+ */
+ _onKeyup: function (e) {
+ switch (e.which) {
+ case $.ui.keyCode.ESCAPE:
+ e.preventDefault();
+ this._removeDropdown();
+ break;
+ case $.ui.keyCode.ENTER:
+ if (!this.$dropdown) {
+ break;
+ }
+ e.preventDefault();
+ var $active = this.$dropdown.find('.o_partner_autocomplete_suggestion.active');
+ if (!$active.length) {
+ return;
+ }
+ this._selectCompany(this.suggestions[$active.data('index')]);
+ break;
+ }
+ },
+
+ /**
+ * Called on mousedown event on a suggestion -> prevent default
+ * action so that the <input/> element does not lose the focus.
+ *
+ * @private
+ * @param {Event} e
+ */
+ _onMousedown: function (e) {
+ e.preventDefault(); // prevent losing focus on suggestion click
+ },
+
+ /**
+ * Called when a dropdown suggestion is clicked -> trigger_up changes for
+ * some fields in the view (not only this <input/> one) with the associated
+ * data (@see _selectCompany).
+ *
+ * @private
+ * @param {Event} e
+ */
+ _onSuggestionClicked: function (e) {
+ e.preventDefault();
+ this._selectCompany(this.suggestions[$(e.currentTarget).data('index')]);
+ },
+});
+
+field_registry.add('field_partner_autocomplete', FieldAutocomplete);
+
+return FieldAutocomplete;
+});
diff --git a/addons/partner_autocomplete/static/src/js/partner_autocomplete_many2one.js b/addons/partner_autocomplete/static/src/js/partner_autocomplete_many2one.js
new file mode 100644
index 00000000000..40780df75ce
--- /dev/null
+++ b/addons/partner_autocomplete/static/src/js/partner_autocomplete_many2one.js
@@ -0,0 +1,138 @@
+odoo.define('partner.autocomplete.many2one', function (require) {
+'use strict';
+
+var FieldMany2One = require('web.relational_fields').FieldMany2One;
+var core = require('web.core');
+var Autocomplete = require('partner.autocomplete.core');
+var field_registry = require('web.field_registry');
+
+var _t = core._t;
+
+var PartnerField = FieldMany2One.extend({
+ jsLibs: [
+ '/partner_autocomplete/static/lib/jsvat.js'
+ ],
+
+ /**
+ * @override
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+ this._addAutocompleteSource(this._searchSuggestions, {
+ placeholder: _t('Searching Autocomplete...'),
+ order: 20,
+ validation: Autocomplete.validateSearchTerm,
+ });
+
+ this.additionalContext['show_vat'] = true;
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Action : create popup form with pre-filled values from Autocomplete
+ *
+ * @param {Object} company
+ * @returns {Deferred}
+ * @private
+ */
+ _createPartner: function (company) {
+ var self = this;
+ self.$('input').val('');
+
+ return Autocomplete.getCreateData(company).then(function (data){
+ var context = {
+ 'default_is_company': true
+ };
+ _.each(data.company, function (val, key) {
+ context['default_' + key] = val && val.id ? val.id : val;
+ });
+
+ // if(data.company.street_name && !data.company.street_number) context.default_street_number = '';
+ if (data.logo) context.default_image = data.logo;
+
+ return self._searchCreatePopup("form", false, context);
+ });
+ },
+
+ /**
+ * Modify autocomplete results rendering
+ * Add logo in the autocomplete results if logo is provided
+ *
+ * @private
+ */
+ _modifyAutompleteRendering: function (){
+ var api = this.$input.data('ui-autocomplete');
+ var renderWithoutLogo = api._renderItem;
+ api._renderItem = function ( ul, item ) {
+ var $li = renderWithoutLogo.call(this, ul, item);
+ if (item.logo){
+ var $a = $li.find('>a').addClass('o_partner_autocomplete_dropdown_item');
+ var $img = $('<img/>').attr('src', item.logo);
+ $a.append($img);
+ }
+
+ return $li;
+ };
+ },
+
+ /**
+ * @override
+ * @private
+ */
+ _renderEdit: function (){
+ this._super.apply(this, arguments);
+ this._modifyAutompleteRendering();
+ },
+
+ /**
+ * Query Autocomplete and add results to the popup
+ *
+ * @override
+ * @param search_val {string}
+ * @returns {Deferred}
+ * @private
+ */
+ _searchSuggestions: function (search_val) {
+ var def = $.Deferred();
+
+ if (Autocomplete.isOnline()) {
+ var self = this;
+
+ Autocomplete.autocomplete(search_val).then(function (suggestions) {
+ var choices = [];
+ if (suggestions && suggestions.length) {
+ choices.push({
+ label: _t('Create and Edit from Autocomplete :'),
+ });
+ _.each(suggestions, function (suggestion) {
+ var label =_.str.sprintf('%s - %s', suggestion.label, suggestion.description);
+ label = label.replace(new RegExp(search_val, "gi"), "<b>$&</b>");
+
+ choices.push({
+ label: label,
+ action: function () {
+ self._createPartner(suggestion);
+ },
+ classname: 'o_m2o_dropdown_option',
+ logo: suggestion.logo,
+ });
+ });
+ }
+
+ def.resolve(choices);
+ });
+ } else {
+ def.resolve([]);
+ }
+
+ return def;
+ },
+});
+
+field_registry.add('res_partner_many2one', PartnerField);
+
+return PartnerField;
+});
diff --git a/addons/partner_autocomplete/static/src/scss/partner_autocomplete.scss b/addons/partner_autocomplete/static/src/scss/partner_autocomplete.scss
new file mode 100644
index 00000000000..a63e2434b6f
--- /dev/null
+++ b/addons/partner_autocomplete/static/src/scss/partner_autocomplete.scss
@@ -0,0 +1,28 @@
+.o_field_partner_autocomplete.dropdown {
+ display: block;
+
+ > .o_partner_autocomplete_dropdown .dropdown-item {
+ min-width: 300px;
+ padding: 4px 8px;
+ > img {
+ float: left;
+ width: 36px;
+ height: 36px;
+ }
+ > .o_partner_autocomplete_info {
+ margin-left: 50px;
+ > * {
+ @include o-text-overflow(block);
+ }
+ }
+ }
+}
+
+.ui-autocomplete .ui-menu-item > a.o_partner_autocomplete_dropdown_item {
+ > img {
+ float: right;
+ width: 20px;
+ height: 20px;
+ }
+
+} \ No newline at end of file
diff --git a/addons/partner_autocomplete/static/src/xml/partner_autocomplete.xml b/addons/partner_autocomplete/static/src/xml/partner_autocomplete.xml
new file mode 100644
index 00000000000..bd6e85c8779
--- /dev/null
+++ b/addons/partner_autocomplete/static/src/xml/partner_autocomplete.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<templates>
+ <div t-name="partner_autocomplete.dropdown" class="o_partner_autocomplete_dropdown dropdown-menu show" role="menu">
+ <t t-foreach="suggestions" t-as="info">
+ <a role="menuitem" href="#"
+ t-attf-class="dropdown-item o_partner_autocomplete_suggestion clearfix#{info_index == 0 and ' active' or ''}"
+ t-att-data-index="info_index">
+ <img t-att-src="info['logo']" onerror="this.src='/base/static/img/company_image.png'" alt="Placeholder"/>
+ <div class="o_partner_autocomplete_info">
+ <strong><t t-esc="info['label'] or '&#160;'"/></strong>
+ <div><t t-esc="info['description']"/></div>
+ </div>
+ </a>
+ </t>
+ </div>
+
+ <div t-name="partner_autocomplete.iap_credit_checker" class="mt-2 row">
+ <div class="col-sm">
+ <div class="loading text-muted">
+ <i class="fa fa-spinner fa-spin"/> Checking remaining credit ...
+ </div>
+ <div class="insufficient">
+ <i class="fa fa-exclamation-triangle text-warning"/> Insufficient credit<br/>
+ </div>
+ <div class="sufficient">
+ <i class="fa fa-check-circle text-success"/> Remaining : <b class="remaining_credits"/> credits
+ </div>
+ </div>
+ <div class="col-sm text-right">
+ <a class="oe_link" target="_blank"><i class="fa fa-arrow-right"/> Buy more credits</a>
+ </div>
+ </div>
+</templates>
diff --git a/addons/partner_autocomplete/static/tests/partner_autocomplete_tests.js b/addons/partner_autocomplete/static/tests/partner_autocomplete_tests.js
new file mode 100644
index 00000000000..5e7a3ae3c1e
--- /dev/null
+++ b/addons/partner_autocomplete/static/tests/partner_autocomplete_tests.js
@@ -0,0 +1,335 @@
+odoo.define('partner_autocomplete.tests', function (require) {
+ "use strict";
+
+ var FormView = require('web.FormView');
+ var testUtils = require("web.test_utils");
+ var AutocompleteCore = require('partner.autocomplete.core');
+ var AutocompleteField = require('partner.autocomplete.fieldchar');
+
+ var createView = testUtils.createAsyncView;
+
+ function _compareResultFields(assert, form, fields, createData) {
+ var type, formatted, $fieldInput;
+
+ _.each(createData, function (val, key) {
+ if (fields[key]) {
+ if (key === 'image') {
+ if (val) val = 'data:image/png;base64,' + val;
+ assert.strictEqual(form.$(".o_field_image img").attr("data-src"), val, 'image value should have been updated to "' + val + '"');
+ } else {
+ type = fields[key].type;
+ $fieldInput = form.$('input[name="' + key + '"]');
+ if ($fieldInput.length) {
+ formatted = $fieldInput.val();
+ formatted = type === 'integer' ? parseInt(formatted, 10) : formatted;
+ assert.strictEqual(
+ formatted,
+ val === false ? 0 : val,
+ key + ' value should have been updated to "' + val + '"'
+ );
+ }
+
+ }
+ }
+ });
+ }
+
+ QUnit.module('partner_autocomplete', {
+ before: function () {
+ var suggestions = [
+ {name: "Odoo", website: "odoo.com", logo: "odoo.com/logo.png", vat: "BE0477472701"}
+ ];
+
+ var enrich_data = {
+ country_id: 20,
+ state_id: false,
+ partner_gid: 1,
+ website: "odoo.com",
+ comment: "Comment on Odoo",
+ street: "40 Chaussée de Namur",
+ city: "Ramillies",
+ zip: "1367",
+ phone: "+1 650-691-3277",
+ email: "info@odoo.com",
+ vat: "BE0477472701",
+ };
+
+ testUtils.patch(AutocompleteCore, {
+ _getBase64Image: function (url) {
+ return $.when(url === "odoo.com/logo.png" ? "odoobase64" : "");
+ },
+ isOnline: function () {
+ return true;
+ },
+ getCreateData: function (company) {
+ var self = this;
+ var def = this._super.apply(this, arguments);
+ def.then(function (data) {
+ self._createData = data.company;
+ });
+ return def;
+ },
+ enrichCompany: function (company) {
+ return $.when(enrich_data);
+ },
+ _getOdooSuggestions(value, isVAT) {
+ var results = _.filter(suggestions, function (suggestion) {
+ value = value ? value.toLowerCase() : '';
+ if (isVAT) return (suggestion.vat.toLowerCase().indexOf(value) >= 0);
+ else return (suggestion.name.toLowerCase().indexOf(value) >= 0);
+ });
+ return $.when(results);
+ },
+ _getClearbitSuggestions(value) {
+ return this._getOdooSuggestions(value);
+ },
+ });
+
+ testUtils.patch(AutocompleteField, {
+ debounceSuggestions: 0,
+ });
+ },
+ beforeEach: function () {
+ this.data = {
+ 'res.partner': {
+ fields: {
+ company_type: {
+ string: "Company Type",
+ type: "selection",
+ selection: [["company", "Company"], ["individual", "Individual"]],
+ searchable: true
+ },
+ name: {string: "Name", type: "char", searchable: true},
+ website: {string: "Website", type: "char", searchable: true},
+ image: {string: "Image", type: "binary", searchable: true},
+ email: {string: "Email", type: "char", searchable: true},
+ phone: {string: "Phone", type: "char", searchable: true},
+ street: {string: "Street", type: "char", searchable: true},
+ city: {string: "City", type: "char", searchable: true},
+ zip: {string: "Zip", type: "char", searchable: true},
+ state_id: {string: "State", type: "integer", searchable: true},
+ country_id: {string: "Country", type: "integer", searchable: true},
+ comment: {string: "Comment", type: "char", searchable: true},
+ vat: {string: "Vat", type: "char", searchable: true},
+ is_company: {string: "Is comapny", type: "bool", searchable: true},
+ partner_gid: {string: "Company data ID", type: "integer", searchable: true},
+ },
+ records: [],
+ onchanges: {
+ company_type: function (obj) {
+ obj.is_company = obj.company_type === 'company';
+ },
+ },
+ },
+ };
+ },
+ after: function () {
+ testUtils.unpatch(AutocompleteField);
+ testUtils.unpatch(AutocompleteCore);
+ },
+ });
+
+ QUnit.test("Partner autocomplete : Company type = Individual", function (assert) {
+ assert.expect(2);
+ var done = assert.async();
+ createView({
+ View: FormView,
+ model: 'res.partner',
+ data: this.data,
+ arch:
+ '<form>' +
+ '<field name="company_type"/>' +
+ '<field name="name" widget="field_partner_autocomplete"/>' +
+ '<field name="website"/>' +
+ '<field name="image" widget="image"/>' +
+ '</form>',
+ }).then(function (form){
+ // Set company type to Individual
+ var $company_type = form.$("select[name='company_type']");
+ $company_type.val('"individual"').trigger('change');
+
+ // Check input exists
+ var $input = form.$(".o_field_partner_autocomplete > input:visible");
+ assert.strictEqual($input.length, 1, "there should be an <input/> for the Partner field");
+
+ // Change input val and assert nothing happens
+ $input.val("odoo").trigger("input");
+ var $dropdown = form.$(".o_field_partner_autocomplete .dropdown-menu:visible");
+ assert.strictEqual($dropdown.length, 0, "there should not be an opened dropdown");
+
+ form.destroy();
+
+ done();
+ });
+ });
+
+
+ QUnit.test("Partner autocomplete : Company type = Company / Name search", function (assert) {
+ assert.expect(21);
+ var done = assert.async();
+ var fields = this.data['res.partner'].fields;
+ createView({
+ View: FormView,
+ model: 'res.partner',
+ data: this.data,
+ arch:
+ '<form>' +
+ '<field name="company_type"/>' +
+ '<field name="name" widget="field_partner_autocomplete"/>' +
+ '<field name="website"/>' +
+ '<field name="image" widget="image"/>' +
+ '<field name="email"/>' +
+ '<field name="phone"/>' +
+ '<field name="street"/>' +
+ '<field name="city"/>' +
+ '<field name="state_id"/>' +
+ '<field name="zip"/>' +
+ '<field name="country_id"/>' +
+ '<field name="comment"/>' +
+ '<field name="vat"/>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method) {
+ assert.step(args.method);
+ }
+ if (route === "/web/static/src/img/placeholder.png"
+ || route === "odoo.com/logo.png"
+ || route === "data:image/png;base64,odoobase64") { // land here as it is not valid base64 content
+ return $.when();
+ }
+ return this._super.apply(this, arguments);
+ },
+ }).then(function (form){
+ // Set company type to Company
+ var $company_type = form.$("select[name='company_type']");
+ $company_type.val('"company"').trigger('change');
+
+ // Check input exists
+ var $input = form.$(".o_field_partner_autocomplete > input:visible");
+ assert.strictEqual($input.length, 1, "there should be an <input/> for the field");
+
+ // Change input val and assert changes
+ $input.val("odoo").trigger("input");
+
+ var $dropdown = form.$(".o_field_partner_autocomplete .dropdown-menu:visible");
+ assert.strictEqual($dropdown.length, 1, "there should be an opened dropdown");
+ assert.strictEqual($dropdown.children().length, 1, "there should be only one proposition");
+
+ $dropdown.find("a").first().click();
+ $input = form.$(".o_field_partner_autocomplete > input");
+ assert.strictEqual($input.val(), "Odoo", "Input value should have been updated to \"Odoo\"");
+ assert.strictEqual(form.$("input.o_field_widget").val(), "odoo.com", "website value should have been updated to \"odoo.com\"");
+
+ _compareResultFields(assert, form, fields, AutocompleteCore._createData);
+
+ // Try suggestion with bullshit query
+ $input.val("ZZZZZZZZZZZZZZZZZZZZZZ").trigger("input");
+ $dropdown = form.$(".o_field_partner_autocomplete .dropdown-menu:visible");
+ assert.strictEqual($dropdown.length, 0, "there should be no opened dropdown when no result");
+
+ // Try autocomplete again
+ $input.val("odoo").trigger("input");
+ $dropdown = form.$(".o_field_partner_autocomplete .dropdown-menu:visible");
+ assert.strictEqual($dropdown.length, 1, "there should be an opened dropdown when typing odoo letters again");
+
+ // Test if dropdown closes on focusout
+ $input.trigger("focusout");
+ $dropdown = form.$(".o_field_partner_autocomplete .dropdown-menu:visible");
+ assert.strictEqual($dropdown.length, 0, "unfocusing the input should close the dropdown");
+
+ form.destroy();
+
+ done();
+ });
+ });
+
+ QUnit.test("Partner autocomplete : Company type = Company / VAT search", function (assert) {
+ assert.expect(32);
+ var done = assert.async();
+ var fields = this.data['res.partner'].fields;
+ createView({
+ View: FormView,
+ model: 'res.partner',
+ data: this.data,
+ arch:
+ '<form>' +
+ '<field name="company_type"/>' +
+ '<field name="name" widget="field_partner_autocomplete"/>' +
+ '<field name="website"/>' +
+ '<field name="image" widget="image"/>' +
+ '<field name="email"/>' +
+ '<field name="phone"/>' +
+ '<field name="street"/>' +
+ '<field name="city"/>' +
+ '<field name="state_id"/>' +
+ '<field name="zip"/>' +
+ '<field name="country_id"/>' +
+ '<field name="comment"/>' +
+ '<field name="vat"/>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method) {
+ assert.step(args.method);
+ }
+ if (route === "/web/static/src/img/placeholder.png"
+ || route === "odoo.com/logo.png"
+ || route === "data:image/png;base64,odoobase64") { // land here as it is not valid base64 content
+ return $.when();
+ }
+ return this._super.apply(this, arguments);
+ },
+ }).then(function (form){
+ // Set company type to Company
+ var $company_type = form.$("select[name='company_type']");
+ $company_type.val('"company"').trigger('change');
+
+
+ // Check input exists
+ var $input = form.$(".o_field_partner_autocomplete > input:visible");
+ assert.strictEqual($input.length, 1, "there should be an <input/> for the field");
+
+ // Set incomplete VAT and assert changes
+ $input.val("BE047747270").trigger("input");
+
+ var $dropdown = form.$(".o_field_partner_autocomplete .dropdown-menu:visible");
+ assert.strictEqual($dropdown.length, 0, "there should be no opened dropdown no results with incomplete VAT number");
+
+ // Set complete VAT and assert changes
+ // First suggestion (only vat result)
+ $input.val("BE0477472701").trigger("input");
+ $dropdown = form.$(".o_field_partner_autocomplete .dropdown-menu:visible");
+ assert.strictEqual($dropdown.length, 1, "there should be an opened dropdown");
+ assert.strictEqual($dropdown.children().length, 1, "there should be one proposition for complete VAT number");
+
+ $dropdown.find("a").first().click();
+
+ $input = form.$(".o_field_partner_autocomplete > input");
+ assert.strictEqual($input.val(), "Odoo", "Input value should have been updated to \"Odoo\"");
+
+ _compareResultFields(assert, form, fields, AutocompleteCore._createData);
+
+ // Set complete VAT and assert changes
+ // Second suggestion (only vat + clearbit result)
+ $input.val("BE0477472701").trigger("input");
+ $dropdown = form.$(".o_field_partner_autocomplete .dropdown-menu:visible");
+ assert.strictEqual($dropdown.length, 1, "there should be an opened dropdown");
+ assert.strictEqual($dropdown.children().length, 1, "there should be one proposition for complete VAT number");
+
+ $dropdown.find("a").first().click();
+
+ $input = form.$(".o_field_partner_autocomplete > input");
+ assert.strictEqual($input.val(), "Odoo", "Input value should have been updated to \"Odoo\"");
+
+ _compareResultFields(assert, form, fields, AutocompleteCore._createData);
+
+ // Test if dropdown closes on focusout
+ $input.trigger("focusout");
+ $dropdown = form.$(".o_field_partner_autocomplete .dropdown-menu:visible");
+ assert.strictEqual($dropdown.length, 0, "unfocusing the input should close the dropdown");
+
+ form.destroy();
+
+ done();
+ });
+ });
+}); \ No newline at end of file
diff --git a/addons/partner_autocomplete/views/additional_info_template.xml b/addons/partner_autocomplete/views/additional_info_template.xml
new file mode 100644
index 00000000000..70290f59af0
--- /dev/null
+++ b/addons/partner_autocomplete/views/additional_info_template.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <template id="additional_info_template">
+ <p t-if="description">
+ <b>Description</b><br/>
+ <t t-esc="description"/><br/>
+ </p>
+ <p>
+ <t t-if="employees">
+ <b>Employees:</b> <t t-esc="employees"/><br/>
+ </t>
+ <t t-if="annual_revenue">
+ <b>Annual revenue:</b> <t t-esc="annual_revenue"/><br/>
+ </t>
+ <t t-if="estimated_annual_revenue">
+ <b>Estimated annual revenue:</b> <t t-esc="estimated_annual_revenue"/><br/>
+ </t>
+ <t t-if="sector">
+ <b>Sector:</b> <t t-esc="sector"/><br/>
+ </t>
+ <t t-if="tech">
+ <b>Tech:</b> <t t-esc="tech"/><br/>
+ </t>
+ </p>
+ <p>
+ <b>Social networks</b><br/>
+ &amp;nbsp;&amp;nbsp;&amp;nbsp; <b>Facebook:</b> <a t-att-href="facebook" target="_blank"><t t-esc="facebook"/></a><br/>
+ &amp;nbsp;&amp;nbsp;&amp;nbsp; <b>Linkedin:</b> <a t-att-href="linkedin" target="_blank"><t t-esc="linkedin"/></a><br/>
+ &amp;nbsp;&amp;nbsp;&amp;nbsp; <b>Crunchbase:</b> <a t-att-href="crunchbase" target="_blank"><t t-esc="crunchbase"/></a><br/>
+ &amp;nbsp;&amp;nbsp;&amp;nbsp; <b>Twitter:</b> <a t-att-href="twitter" target="_blank"><t t-esc="twitter"/></a><br/>
+ </p>
+ <p t-if="emails">
+ <b>Email addresses</b><br/>
+ <t t-foreach="emails" t-as="email">
+ &amp;nbsp;&amp;nbsp;&amp;nbsp; <a t-att-href="'mailto:%s' % email"><t t-esc="email"/></a><br/>
+ </t>
+ </p>
+ <p t-if="phones">
+ <b>Phone numbers</b><br/>
+ <t t-foreach="phones" t-as="phone">
+ &amp;nbsp;&amp;nbsp;&amp;nbsp; <t t-esc="phone"/><br/>
+ </t>
+ </p>
+ </template>
+</odoo>
diff --git a/addons/partner_autocomplete/views/partner_autocomplete_assets.xml b/addons/partner_autocomplete/views/partner_autocomplete_assets.xml
new file mode 100644
index 00000000000..8e9f1ecb300
--- /dev/null
+++ b/addons/partner_autocomplete/views/partner_autocomplete_assets.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <template id="assets_backend" name="Partner Autocomplete Assets" inherit_id="web.assets_backend">
+ <xpath expr="." position="inside">
+ <link rel="stylesheet" type="text/scss" href="/partner_autocomplete/static/src/scss/partner_autocomplete.scss"/>
+ <script type="text/javascript" src="/partner_autocomplete/static/src/js/partner_autocomplete_core.js" />
+ <script type="text/javascript" src="/partner_autocomplete/static/src/js/partner_autocomplete_fieldchar.js" />
+ <script type="text/javascript" src="/partner_autocomplete/static/src/js/partner_autocomplete_many2one.js" />
+ <script type="text/javascript" src="/partner_autocomplete/static/src/js/iap_credit_checker_widget.js" />
+ </xpath>
+ </template>
+
+ <template id="qunit_suite" inherit_id="web.qunit_suite">
+ <xpath expr="//script[last()]" position="after">
+ <script type="text/javascript" src="/partner_autocomplete/static/tests/partner_autocomplete_tests.js"/>
+ </xpath>
+ </template>
+</odoo>
diff --git a/addons/partner_autocomplete/views/res_company_views.xml b/addons/partner_autocomplete/views/res_company_views.xml
new file mode 100644
index 00000000000..64fdfe85809
--- /dev/null
+++ b/addons/partner_autocomplete/views/res_company_views.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="view_company_form_inherit_partner_autocomplete" model="ir.ui.view">
+ <field name="name">res.company.form.inherit.web.partner.autocomplete</field>
+ <field name="model">res.company</field>
+ <field name="inherit_id" ref="base.view_company_form"/>
+ <field name="arch" type="xml">
+ <xpath expr="//div[hasclass('oe_title')]/h1/field[@name='name']" position="attributes">
+ <attribute name="widget">field_partner_autocomplete</attribute>
+ </xpath>
+ <xpath expr="//field[@name='vat']" position="attributes">
+ <attribute name="widget">field_partner_autocomplete</attribute>
+ </xpath>
+ <xpath expr="//field[last()]" position="after">
+ <field name="partner_gid" invisible="True"/>
+ </xpath>
+ </field>
+ </record>
+</odoo>
diff --git a/addons/partner_autocomplete/views/res_config_settings_views.xml b/addons/partner_autocomplete/views/res_config_settings_views.xml
new file mode 100644
index 00000000000..8c97b9e4aeb
--- /dev/null
+++ b/addons/partner_autocomplete/views/res_config_settings_views.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="res_config_settings_view_form" model="ir.ui.view">
+ <field name="name">res.config.settings.view.form.inherit.partner.autcomplete</field>
+ <field name="model">res.config.settings</field>
+ <field name="inherit_id" ref="base_setup.res_config_settings_view_form"/>
+ <field name="arch" type="xml">
+ <div id="partner_autocomplete_settings" position="inside">
+ <widget name="iap_credit_checker" service_name="partner_autocomplete"/>
+ </div>
+
+ <xpath expr="//div[hasclass('app_settings_block')]" position="before">
+ <field name="partner_autocomplete_insufficient_credit" invisible="1"/>
+ <div class="alert alert-info alert-dismissible fade show pt-2 pb-2" role="alert" attrs="{'invisible': [('partner_autocomplete_insufficient_credit', '=', False)]}">
+ <i class="fa fa-exclamation-triangle text-warning"></i> &amp;nbsp; You don't have credits to auto-complete companies' data anymore.
+ <button name="redirect_to_buy_autocmplete_credit" type="object" class="btn-link">
+ <i class="fa fa-arrow-right"/>
+ Buy more credits
+ </button>
+ <button type="button" class="close" data-dismiss="alert" aria-label="Close">
+ <span>&amp;times;</span>
+ </button>
+ </div>
+ </xpath>
+ </field>
+ </record>
+</odoo>
+
diff --git a/addons/partner_autocomplete/views/res_partner_views.xml b/addons/partner_autocomplete/views/res_partner_views.xml
new file mode 100644
index 00000000000..d7234bb3774
--- /dev/null
+++ b/addons/partner_autocomplete/views/res_partner_views.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="view_res_partner_form_inherit_partner_autocomplete" model="ir.ui.view">
+ <field name="name">res.partner.form.inherit.partner.autocomplete</field>
+ <field name="model">res.partner</field>
+ <field name="inherit_id" ref="base.view_partner_form"/>
+ <field name="arch" type="xml">
+ <xpath expr="//div[hasclass('oe_title')]/h1/field[@name='name']" position="attributes">
+ <attribute name="widget">field_partner_autocomplete</attribute>
+ </xpath>
+ <xpath expr="//field[@name='vat']" position="attributes">
+ <attribute name="widget">field_partner_autocomplete</attribute>
+ </xpath>
+ <xpath expr="//field[last()]" position="after">
+ <field name="partner_gid" invisible="True"/>
+ <field name="additional_info" invisible="True"/>
+ </xpath>
+ <xpath expr="//div[hasclass('oe_title')]/div[hasclass('o_row')]/field[@name='parent_id']" position="attributes">
+ <attribute name="help">You can find a customer, a contact, etc. by its Name, TIN, Email or Internal Reference.</attribute>
+ </xpath>
+ </field>
+ </record>
+
+ <record id="view_res_partner_short_form_inherit_partner_autocomplete" model="ir.ui.view">
+ <field name="name">res.partner.short.form.inherit.partner.autocomplete</field>
+ <field name="model">res.partner</field>
+ <field name="inherit_id" ref="base.view_partner_short_form"/>
+ <field name="arch" type="xml">
+ <xpath expr="//div[hasclass('oe_title')]/h1/field[@name='name']" position="attributes">
+ <attribute name="widget">field_partner_autocomplete</attribute>
+ </xpath>
+ <xpath expr="//field[@name='vat']" position="attributes">
+ <attribute name="widget">field_partner_autocomplete</attribute>
+ </xpath>
+ <xpath expr="//field[last()]" position="after">
+ <field name="partner_gid" invisible="True"/>
+ <field name="additional_info" invisible="True"/>
+ </xpath>
+ <xpath expr="//div[hasclass('oe_title')]/div[hasclass('o_row')]/field[@name='parent_id']" position="attributes">
+ <attribute name="help">You can find a customer, a contact, etc. by its Name, TIN, Email or Internal Reference.</attribute>
+ </xpath>
+ </field>
+ </record>
+
+ <record id="view_partner_simple_form_inherit_partner_autocomplete" model="ir.ui.view">
+ <field name="name">res.partner.simplified.form.inherit.partner.autocomplete</field>
+ <field name="model">res.partner</field>
+ <field name="inherit_id" ref="base.view_partner_simple_form"/>
+ <field name="arch" type="xml">
+ <xpath expr="//div[hasclass('oe_title')]/h1/field[@name='name']" position="attributes">
+ <attribute name="widget">field_partner_autocomplete</attribute>
+ </xpath>
+ <xpath expr="//field[last()]" position="after">
+ <field name="partner_gid" invisible="True"/>
+ <field name="additional_info" invisible="True"/>
+ </xpath>
+ </field>
+ </record>
+</odoo>
diff --git a/addons/partner_autocomplete_address_extended/__init__.py b/addons/partner_autocomplete_address_extended/__init__.py
new file mode 100644
index 00000000000..cde864bae21
--- /dev/null
+++ b/addons/partner_autocomplete_address_extended/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+from . import models
diff --git a/addons/partner_autocomplete_address_extended/__manifest__.py b/addons/partner_autocomplete_address_extended/__manifest__.py
new file mode 100644
index 00000000000..85a86f231e1
--- /dev/null
+++ b/addons/partner_autocomplete_address_extended/__manifest__.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+{
+ 'name': "Partner Autocomplete extends Address autocomplete",
+ 'summary': """
+ Correct address formating when both modules are installed""",
+ 'author': "Odoo SA",
+ 'category': 'Tools',
+ 'version': '1.0',
+ 'depends': ['partner_autocomplete', 'base_address_extended'],
+ 'auto_install': True,
+}
diff --git a/addons/partner_autocomplete_address_extended/models/__init__.py b/addons/partner_autocomplete_address_extended/models/__init__.py
new file mode 100644
index 00000000000..8b86ffb924b
--- /dev/null
+++ b/addons/partner_autocomplete_address_extended/models/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import res_partner \ No newline at end of file
diff --git a/addons/partner_autocomplete_address_extended/models/res_partner.py b/addons/partner_autocomplete_address_extended/models/res_partner.py
new file mode 100644
index 00000000000..79c604eed7a
--- /dev/null
+++ b/addons/partner_autocomplete_address_extended/models/res_partner.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import re
+from odoo import api, fields, models
+
+
+class ResPartner(models.Model):
+ _name = 'res.partner'
+ _inherit = 'res.partner'
+
+ @api.model
+ def _split_street_with_params(self, street_raw, street_format=False):
+ regex = '((\d+\w* ?(-|\/) ?\d*\w*)|(\d+\w*))'
+
+ street_name = street_raw
+ street_number = ''
+ street_number2 = ''
+
+ # Try to find number at beginning
+ start_regex = re.compile('^' + regex)
+ matches = re.search(start_regex, street_raw)
+ if matches and matches.group(0):
+ street_number = matches.group(0)
+ street_name = re.sub(start_regex, '', street_raw, 1)
+ else:
+ # Try to find number at end
+ end_regex = re.compile(regex + '$')
+ matches = re.search(end_regex, street_raw)
+ if matches and matches.group(0):
+ street_number = matches.group(0)
+ street_name = re.sub(end_regex, '', street_raw, 1)
+
+ if street_number:
+ street_number_split = street_number.split('/')
+ if len(street_number_split) > 1:
+ street_number2 = street_number_split.pop(-1)
+ street_number = '/'.join(street_number_split)
+
+ return {
+ 'street_name': street_name.strip(),
+ 'street_number': street_number.strip(),
+ 'street_number2': street_number2.strip(),
+ }
diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py
index 8e8ae8c77b6..90ac6e930f1 100644
--- a/addons/purchase/models/purchase.py
+++ b/addons/purchase/models/purchase.py
@@ -85,7 +85,7 @@ class PurchaseOrder(models.Model):
date_order = fields.Datetime('Order Date', required=True, states=READONLY_STATES, index=True, copy=False, default=fields.Datetime.now,\
help="Depicts the date where the Quotation should be validated and converted into a purchase order.")
date_approve = fields.Date('Approval Date', readonly=1, index=True, copy=False)
- partner_id = fields.Many2one('res.partner', string='Vendor', required=True, states=READONLY_STATES, change_default=True, track_visibility='always')
+ partner_id = fields.Many2one('res.partner', string='Vendor', required=True, states=READONLY_STATES, change_default=True, track_visibility='always', help="You can find a vendor by its Name, TIN, Email or Internal Reference.")
dest_address_id = fields.Many2one('res.partner', string='Drop Ship Address', states=READONLY_STATES,
help="Put an address if you want to deliver directly from the vendor to the customer. "
"Otherwise, keep empty to deliver to your own company.")
diff --git a/addons/purchase/views/purchase_views.xml b/addons/purchase/views/purchase_views.xml
index aa4cf40bb15..437d86a0c75 100644
--- a/addons/purchase/views/purchase_views.xml
+++ b/addons/purchase/views/purchase_views.xml
@@ -165,7 +165,9 @@
</div>
<group>
<group>
- <field name="partner_id" context="{'search_default_supplier':1, 'default_supplier':1, 'default_customer':0}" domain="[('supplier','=',True)]"/>
+ <field name="partner_id" widget="res_partner_many2one" context="{'search_default_supplier':1, 'default_supplier':1, 'default_customer':0, 'show_vat': True}" domain="[('supplier','=',True)]"
+ placeholder="Name, TIN, Email, or Reference"
+ />
<field name="partner_ref"/>
<field name="currency_id" groups="base.group_multi_currency"/>
</group>
diff --git a/addons/repair/models/repair.py b/addons/repair/models/repair.py
index e38c76e20ce..1302cb93a01 100644
--- a/addons/repair/models/repair.py
+++ b/addons/repair/models/repair.py
@@ -46,7 +46,7 @@ class Repair(models.Model):
partner_id = fields.Many2one(
'res.partner', 'Customer',
index=True, states={'confirmed': [('readonly', True)]},
- help='Choose partner for whom the order will be invoiced and delivered.')
+ help='Choose partner for whom the order will be invoiced and delivered. You can find a partner by its Name, TIN, Email or Internal Reference.')
address_id = fields.Many2one(
'res.partner', 'Delivery Address',
domain="[('parent_id','=',partner_id)]",
diff --git a/addons/repair/views/repair_views.xml b/addons/repair/views/repair_views.xml
index 005dca2936c..ca2f0fd5aec 100644
--- a/addons/repair/views/repair_views.xml
+++ b/addons/repair/views/repair_views.xml
@@ -59,7 +59,7 @@
<field name="product_uom" groups="uom.group_uom"/>
</div>
<field name="lot_id" domain="[('product_id', '=', product_id)]" context="{'default_product_id': product_id}" groups="stock.group_production_lot" attrs="{'required': [('tracking', '!=', 'none')], 'readonly': [('state', '=', 'done')]}"/>
- <field name="partner_id" attrs="{'required':[('invoice_method','!=','none')]}"/>
+ <field name="partner_id" widget="res_partner_many2one" attrs="{'required':[('invoice_method','!=','none')]}" context="{'show_vat': True}"/>
<field name="address_id" groups="sale.group_delivery_invoice_address"/>
</group>
<group>
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index 8547a0cf5db..aa317ebff59 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -154,7 +154,7 @@ class SaleOrder(models.Model):
create_date = fields.Datetime(string='Creation Date', readonly=True, index=True, help="Date on which sales order is created.")
confirmation_date = fields.Datetime(string='Confirmation Date', readonly=True, index=True, help="Date on which the sales order is confirmed.", oldname="date_confirm", copy=False)
user_id = fields.Many2one('res.users', string='Salesperson', index=True, track_visibility='onchange', track_sequence=2, default=lambda self: self.env.user)
- partner_id = fields.Many2one('res.partner', string='Customer', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, required=True, change_default=True, index=True, track_visibility='always', track_sequence=1)
+ partner_id = fields.Many2one('res.partner', string='Customer', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, required=True, change_default=True, index=True, track_visibility='always', track_sequence=1, help="You can find a customer by its Name, TIN, Email or Internal Reference.")
partner_invoice_id = fields.Many2one('res.partner', string='Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)], 'sale': [('readonly', False)]}, help="Invoice address for current sales order.")
partner_shipping_id = fields.Many2one('res.partner', string='Delivery Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)], 'sale': [('readonly', False)]}, help="Delivery address for current sales order.")
diff --git a/addons/sale/views/sale_views.xml b/addons/sale/views/sale_views.xml
index 41259952307..21876534f7e 100644
--- a/addons/sale/views/sale_views.xml
+++ b/addons/sale/views/sale_views.xml
@@ -264,7 +264,7 @@
</div>
<group>
<group>
- <field name="partner_id" domain="[('customer','=',True)]" context="{'search_default_customer':1, 'show_address': 1}" options='{"always_reload": True}'/>
+ <field name="partner_id" widget="res_partner_many2one" domain="[('customer','=',True)]" context="{'search_default_customer':1, 'show_address': 1, 'show_vat': True}" options='{"always_reload": True}'/>
<field name="partner_invoice_id" groups="sale.group_delivery_invoice_address" context="{'default_type':'invoice'}" options='{"always_reload": True}'/>
<field name="partner_shipping_id" groups="sale.group_delivery_invoice_address" context="{'default_type':'delivery'}" options='{"always_reload": True}'/>
</group>
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index a1efa85498e..c3d25f37d09 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -138,6 +138,11 @@ var FieldMany2One = AbstractField.extend({
this.isDirty = false;
this.lastChangeEvent = undefined;
+ // List of autocomplete sources
+ this._autocompleteSources = [];
+ // Add default search method for M20 (name_search)
+ this._addAutocompleteSource(this._search, {placeholder: _t('Loading...'), order: 1});
+
// use a DropPrevious to properly handle related record quick creations,
// and store a createDef to be able to notify the environment that there
// is pending quick create operation
@@ -213,14 +218,46 @@ var FieldMany2One = AbstractField.extend({
//--------------------------------------------------------------------------
/**
+ * Add a source to the autocomplete results
+ *
+ * @param {function} method : A function that returns a list of results. If async source, the function should return a promise
+ * @param {Object} params : Parameters containing placeholder/validation/order
+ * @private
+ */
+ _addAutocompleteSource: function (method, params) {
+ this._autocompleteSources.push({
+ method: method,
+ placeholder: (params.placeholder ? _t(params.placeholder) : _t('Loading...')) + '<i class="fa fa-spinner fa-spin pull-right"></i>' ,
+ validation: params.validation,
+ loading: false,
+ order: params.order || 999
+ });
+
+ this._autocompleteSources = _.sortBy(this._autocompleteSources, 'order');
+ },
+ /**
* @private
*/
_bindAutoComplete: function () {
var self = this;
this.$input.autocomplete({
source: function (req, resp) {
- self._search(req.term).then(function (result) {
- resp(result);
+ _.each(self._autocompleteSources, function (source) {
+ // Resets the results for this source
+ source.results = [];
+
+ // Check if this source should be used for the searched term
+ if (!source.validation || source.validation.call(self, req.term)) {
+ source.loading = true;
+
+ // Wrap the returned value of the source.method with $.when.
+ // So event if the returned value is not async, it will work
+ $.when(source.method.call(self, req.term)).then(function (results) {
+ source.results = results;
+ source.loading = false;
+ resp(self._concatenateAutocompleteResults());
+ });
+ }
});
},
select: function (event, ui) {
@@ -257,6 +294,25 @@ var FieldMany2One = AbstractField.extend({
this.autocomplete_bound = true;
},
/**
+ * Concatenate async results for autocomplete.
+ *
+ * @returns {Array}
+ * @private
+ */
+ _concatenateAutocompleteResults: function () {
+ var results = [];
+ _.each(this._autocompleteSources, function (source) {
+ if (source.results && source.results.length) {
+ results = results.concat(source.results);
+ } else if (source.loading) {
+ results.push({
+ label: source.placeholder
+ });
+ }
+ });
+ return results;
+ },
+ /**
* @private
* @param {string} [name]
* @returns {Object}
@@ -389,6 +445,8 @@ var FieldMany2One = AbstractField.extend({
this.m2o_value = this._formatValue(this.value);
},
/**
+ * Executes a name_search and process its result.
+ *
* @private
* @param {string} search_val
* @returns {Deferred}
diff --git a/addons/web/static/src/js/fields/upgrade_fields.js b/addons/web/static/src/js/fields/upgrade_fields.js
index dd91ca2efc2..445cc18cf81 100644
--- a/addons/web/static/src/js/fields/upgrade_fields.js
+++ b/addons/web/static/src/js/fields/upgrade_fields.js
@@ -146,6 +146,7 @@ var UpgradeBoolean = FieldBoolean.extend(AbstractFieldUpgrade, {
* @private
*/
_insertEnterpriseLabel: function ($enterpriseLabel) {
+ if(this.name==='module_partner_autocomplete') debugger;
var $el = this.$label || this.$el;
$el.append('&nbsp;').append($enterpriseLabel);
},