From a6e1eb9f0ad285fac7d0ca0b9f89f046d78ec9c7 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse <al@openerp.com> Date: Tue, 18 Sep 2018 17:16:16 +0200 Subject: [PATCH] [ADD] partner_autocomplete: Added autocomplete on partners fields For Name field or M2O, gives a list of companies Data comes from Odoo IAP Service --- .tx/config | 5 + addons/account/models/account_invoice.py | 2 +- addons/account/views/account_invoice_view.xml | 6 +- addons/account/views/partner_view.xml | 1 + .../base_setup/models/res_config_settings.py | 1 + .../views/res_config_settings_views.xml | 11 + addons/crm/models/crm_lead.py | 2 +- addons/crm/models/res_config_settings.py | 1 - addons/crm/views/crm_lead_views.xml | 16 +- .../crm/views/res_config_settings_views.xml | 11 - .../wizard/crm_lead_to_opportunity_views.xml | 4 +- addons/iap/models/iap.py | 2 +- addons/iap/static/src/js/crash_manager.js | 4 +- addons/partner_autocomplete/__init__.py | 3 + addons/partner_autocomplete/__manifest__.py | 27 + addons/partner_autocomplete/data/cron.xml | 12 + .../i18n/partner_autocomplete.pot | 233 ++- .../partner_autocomplete/models/__init__.py | 7 + .../models/res_company.py | 14 + .../models/res_config_settings.py | 17 + .../models/res_partner.py | 180 +++ .../models/res_partner_autocomplete_sync.py | 37 + .../security/ir.model.access.csv | 5 + .../partner_autocomplete/static/lib/jsvat.js | 1338 +++++++++++++++++ .../src/js/iap_credit_checker_widget.js | 94 ++ .../src/js/partner_autocomplete_core.js | 331 ++++ .../src/js/partner_autocomplete_fieldchar.js | 339 +++++ .../src/js/partner_autocomplete_many2one.js | 138 ++ .../static/src/scss/partner_autocomplete.scss | 28 + .../static/src/xml/partner_autocomplete.xml | 33 + .../tests/partner_autocomplete_tests.js | 335 +++++ .../views/additional_info_template.xml | 45 + .../views/partner_autocomplete_assets.xml | 18 + .../views/res_company_views.xml | 19 + .../views/res_config_settings_views.xml | 28 + .../views/res_partner_views.xml | 59 + .../__init__.py | 3 + .../__manifest__.py | 11 + .../models/__init__.py | 4 + .../models/res_partner.py | 44 + addons/purchase/models/purchase.py | 2 +- addons/purchase/views/purchase_views.xml | 4 +- addons/repair/models/repair.py | 2 +- addons/repair/views/repair_views.xml | 2 +- addons/sale/models/sale.py | 2 +- addons/sale/views/sale_views.xml | 2 +- .../static/src/js/fields/relational_fields.js | 62 +- .../static/src/js/fields/upgrade_fields.js | 1 + odoo/addons/base/models/res_partner.py | 13 +- odoo/addons/base/views/res_bank_views.xml | 1 + odoo/addons/base/views/res_partner_views.xml | 14 +- 51 files changed, 3525 insertions(+), 48 deletions(-) create mode 100644 addons/partner_autocomplete/__init__.py create mode 100644 addons/partner_autocomplete/__manifest__.py create mode 100644 addons/partner_autocomplete/data/cron.xml create mode 100644 addons/partner_autocomplete/models/__init__.py create mode 100644 addons/partner_autocomplete/models/res_company.py create mode 100644 addons/partner_autocomplete/models/res_config_settings.py create mode 100644 addons/partner_autocomplete/models/res_partner.py create mode 100644 addons/partner_autocomplete/models/res_partner_autocomplete_sync.py create mode 100644 addons/partner_autocomplete/security/ir.model.access.csv create mode 100644 addons/partner_autocomplete/static/lib/jsvat.js create mode 100644 addons/partner_autocomplete/static/src/js/iap_credit_checker_widget.js create mode 100644 addons/partner_autocomplete/static/src/js/partner_autocomplete_core.js create mode 100644 addons/partner_autocomplete/static/src/js/partner_autocomplete_fieldchar.js create mode 100644 addons/partner_autocomplete/static/src/js/partner_autocomplete_many2one.js create mode 100644 addons/partner_autocomplete/static/src/scss/partner_autocomplete.scss create mode 100644 addons/partner_autocomplete/static/src/xml/partner_autocomplete.xml create mode 100644 addons/partner_autocomplete/static/tests/partner_autocomplete_tests.js create mode 100644 addons/partner_autocomplete/views/additional_info_template.xml create mode 100644 addons/partner_autocomplete/views/partner_autocomplete_assets.xml create mode 100644 addons/partner_autocomplete/views/res_company_views.xml create mode 100644 addons/partner_autocomplete/views/res_config_settings_views.xml create mode 100644 addons/partner_autocomplete/views/res_partner_views.xml create mode 100644 addons/partner_autocomplete_address_extended/__init__.py create mode 100644 addons/partner_autocomplete_address_extended/__manifest__.py create mode 100644 addons/partner_autocomplete_address_extended/models/__init__.py create mode 100644 addons/partner_autocomplete_address_extended/models/res_partner.py diff --git a/.tx/config b/.tx/config index 871ba9e412fd..840fb1e8ed05 100644 --- a/.tx/config +++ b/.tx/config @@ -462,6 +462,11 @@ file_filter = addons/pad_project/i18n/<lang>.po source_file = addons/pad_project/i18n/pad_project.pot source_lang = en +[odoo-12.partner_autocomplete] +file_filter = partner_autocomplete/i18n/<lang>.po +source_file = partner_autocomplete/i18n/partner_autocomplete.pot +source_lang = en + [odoo-12.payment] file_filter = addons/payment/i18n/<lang>.po source_file = addons/payment/i18n/payment.pot diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index c42a1edddbab..7847f238e087 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 0cd723f1ff90..1c7ea86e3aeb 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 84279d3878ff..152356fcb659 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 de5037a2245f..d23737db9013 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 95515c8ab972..de090d491d5e 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 177d6aa8048b..df5a0e0cfce5 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 c2ca5a607352..33386434f2a8 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 f4a5ba6ca19a..04b29c6153c3 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 a32b08a63f2d..e37b01336a4a 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 c12587a5fb01..97a06862ab14 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 ea97914da1bd..8d6b85e6d731 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 93ccf8bee953..59b07af1294a 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 000000000000..cde864bae21a --- /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 000000000000..eeb767f2258e --- /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 000000000000..f382d10f7f2e --- /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 1817e359bc71..2a7e7ec09648 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" @@ -15,26 +15,241 @@ msgstr "" "Content-Transfer-Encoding: \n" "Plural-Forms: \n" +#. module: partner_autocomplete +#: model_terms:ir.ui.view,arch_db:partner_autocomplete.additional_info_template +msgid "&nbsp;&nbsp;&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" +" &nbsp;&nbsp;&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" +" &nbsp;&nbsp;&nbsp; <b>Crunchbase:</b>" +msgstr "" + +#. module: partner_autocomplete +#: model_terms:ir.ui.view,arch_db:partner_autocomplete.additional_info_template +msgid "<br/>\n" +" &nbsp;&nbsp;&nbsp; <b>Linkedin:</b>" +msgstr "" + +#. module: partner_autocomplete +#: model_terms:ir.ui.view,arch_db:partner_autocomplete.additional_info_template +msgid "<br/>\n" +" &nbsp;&nbsp;&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\"/> &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>&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 000000000000..5d9eecc13d77 --- /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 000000000000..e946567fcdea --- /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 000000000000..f28046d6189d --- /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 000000000000..60d5b182222d --- /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 000000000000..a0ae355662f1 --- /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 000000000000..67da1da8d050 --- /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 000000000000..07dae2d3af0c --- /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 000000000000..ab25dab00290 --- /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 000000000000..05bd10c91144 --- /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 000000000000..b1843ec8de1a --- /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 000000000000..40780df75ced --- /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 000000000000..a63e2434b6f3 --- /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 000000000000..bd6e85c8779d --- /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 ' '"/></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 000000000000..5e7a3ae3c1e8 --- /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 000000000000..70290f59af0c --- /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/> + &nbsp;&nbsp;&nbsp; <b>Facebook:</b> <a t-att-href="facebook" target="_blank"><t t-esc="facebook"/></a><br/> + &nbsp;&nbsp;&nbsp; <b>Linkedin:</b> <a t-att-href="linkedin" target="_blank"><t t-esc="linkedin"/></a><br/> + &nbsp;&nbsp;&nbsp; <b>Crunchbase:</b> <a t-att-href="crunchbase" target="_blank"><t t-esc="crunchbase"/></a><br/> + &nbsp;&nbsp;&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"> + &nbsp;&nbsp;&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"> + &nbsp;&nbsp;&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 000000000000..8e9f1ecb300c --- /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 000000000000..64fdfe85809a --- /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 000000000000..8c97b9e4aeb3 --- /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> &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>&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 000000000000..d7234bb3774f --- /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 000000000000..cde864bae21a --- /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 000000000000..85a86f231e12 --- /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 000000000000..8b86ffb924b3 --- /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 000000000000..79c604eed7a9 --- /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 8e8ae8c77b67..90ac6e930f12 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 aa4cf40bb15c..437d86a0c75a 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 e38c76e20ce4..1302cb93a01c 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 005dca2936ce..ca2f0fd5aec9 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 8547a0cf5db8..aa317ebff59d 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 412599523071..21876534f7ed 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 a1efa85498e5..c3d25f37d092 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 @@ -212,6 +217,24 @@ var FieldMany2One = AbstractField.extend({ // Private //-------------------------------------------------------------------------- + /** + * 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 */ @@ -219,8 +242,22 @@ var FieldMany2One = AbstractField.extend({ 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) { @@ -256,6 +293,25 @@ var FieldMany2One = AbstractField.extend({ this.$input.autocomplete("option", "position", { my : "left top", at: "left bottom" }); 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] @@ -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 dd91ca2efc2a..445cc18cf819 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(' ').append($enterpriseLabel); }, diff --git a/odoo/addons/base/models/res_partner.py b/odoo/addons/base/models/res_partner.py index c74e10f28cb3..916bf15d7315 100644 --- a/odoo/addons/base/models/res_partner.py +++ b/odoo/addons/base/models/res_partner.py @@ -6,6 +6,7 @@ import datetime import hashlib import pytz import threading +import re from email.utils import formataddr @@ -239,6 +240,12 @@ class Partner(models.Model): ('check_name', "CHECK( (type='contact' AND name IS NOT NULL) or (type!='contact') )", 'Contacts require a name.'), ] + @api.model_cr + def init(self): + self._cr.execute("""SELECT indexname FROM pg_indexes WHERE indexname = 'res_partner_vat_index'""") + if not self._cr.fetchone(): + self._cr.execute("""CREATE INDEX res_partner_vat_index ON res_partner (regexp_replace(upper(vat), '[^A-Z0-9]+', '', 'g'))""") + @api.depends('is_company', 'name', 'parent_id.name', 'type', 'company_name') def _compute_display_name(self): diff = dict(show_address=None, show_address_only=None, show_email=None) @@ -610,6 +617,8 @@ class Partner(models.Model): name = "%s <%s>" % (name, partner.email) if self._context.get('html_format'): name = name.replace('\n', '<br/>') + if self._context.get('show_vat') and partner.vat: + name = "%s - %s" % (name, partner.vat) return name @api.multi @@ -697,7 +706,9 @@ class Partner(models.Model): percent=unaccent('%s'), vat=unaccent('vat'),) - where_clause_params += [search_name]*5 + where_clause_params += [search_name]*3 # for email / display_name, reference + where_clause_params += [re.sub('[^a-zA-Z0-9]+', '', search_name)] # for vat + where_clause_params += [search_name] # for order by if limit: query += ' limit %s' where_clause_params.append(limit) diff --git a/odoo/addons/base/views/res_bank_views.xml b/odoo/addons/base/views/res_bank_views.xml index 1df3b1cfa58e..c053a1e7b26f 100644 --- a/odoo/addons/base/views/res_bank_views.xml +++ b/odoo/addons/base/views/res_bank_views.xml @@ -88,6 +88,7 @@ <field name="bank_name"/> <field name="company_id" groups="base.group_multi_company"/> <field name="partner_id"/> + <field name="acc_holder_name" invisible="1"/> </tree> </field> </record> diff --git a/odoo/addons/base/views/res_partner_views.xml b/odoo/addons/base/views/res_partner_views.xml index 4f9905858b3b..a6ba287dcff6 100644 --- a/odoo/addons/base/views/res_partner_views.xml +++ b/odoo/addons/base/views/res_partner_views.xml @@ -70,8 +70,9 @@ <field name="name" default_focus="1" placeholder="Name" attrs="{'required' : [('type', '=', 'contact')]}"/> </h1> <field name="parent_id" + widget="res_partner_many2one" placeholder="Company" - domain="[('is_company', '=', True)]" context="{'default_is_company': True}" + domain="[('is_company', '=', True)]" context="{'default_is_company': True, 'show_vat': True}" attrs="{'invisible': [('is_company','=', True)]}"/> </div> <group> @@ -142,8 +143,9 @@ </h1> <div class="o_row"> <field name="parent_id" + widget="res_partner_many2one" placeholder="Company" - domain="[('is_company', '=', True)]" context="{'default_is_company': True, 'default_supplier': supplier, 'default_customer': customer}" + domain="[('is_company', '=', True)]" context="{'default_is_company': True, 'default_supplier': supplier, 'default_customer': customer, 'show_vat': True}" attrs="{'invisible': ['|', '&', ('is_company','=', True),('parent_id', '=', False),('company_name', '!=', False),('company_name', '!=', '')]}"/> <field name="company_name" attrs="{'invisible': ['|', '|', ('company_name', '=', False), ('company_name', '=', ''), ('is_company', '=', True)]}"/> <button name="create_company" type="object" class="oe_edit_only btn-link" @@ -215,8 +217,9 @@ </h1> <div class="o_row"> <field name="parent_id" + widget="res_partner_many2one" placeholder="Company" - domain="[('is_company', '=', True)]" context="{'default_is_company': True, 'default_supplier': supplier, 'default_customer': customer}" + domain="[('is_company', '=', True)]" context="{'default_is_company': True, 'default_supplier': supplier, 'default_customer': customer, 'show_vat': True}" attrs="{'invisible': ['|', '&', ('is_company','=', True),('parent_id', '=', False),('company_name', '!=', False),('company_name', '!=', '')]}"/> <field name="company_name" attrs="{'invisible': ['|', '|', ('company_name', '=', False), ('company_name', '=', ''), ('is_company', '=', True)]}"/> <button name="create_company" type="object" class="oe_edit_only btn-link" @@ -429,6 +432,7 @@ <field name="bank_ids"> <tree editable="bottom"> <field name="acc_number"/> + <field name="acc_holder_name" invisible="1"/> </tree> </field> </group> @@ -585,7 +589,7 @@ <field name="view_type">form</field> <field name="view_mode">kanban,tree,form</field> <field name="domain">[]</field> - <field name="context">{'default_customer':1, 'search_default_customer':1, 'default_company_type': 'company','default_is_company': 1}</field> + <field name="context">{'default_customer':1, 'search_default_customer':1, 'default_is_company': True}</field> <field name="filter" eval="True"/> <field name="help" type="html"> <p class="o_view_nocontent_smiling_face"> @@ -621,7 +625,7 @@ <field name="view_type">form</field> <field name="domain">[]</field> <field name="view_mode">kanban,tree,form</field> - <field name="context">{'search_default_supplier': 1,'default_customer': 0,'default_supplier': 1, 'default_company_type': 'company'}</field> + <field name="context">{'search_default_supplier': 1,'default_customer': 0,'default_supplier': 1, 'default_is_company': True}</field> <field name="filter" eval="True"/> <field name="help" type="html"> <p class="o_view_nocontent_smiling_face"> -- GitLab