[ADD] partner_autocomplete: Added autocomplete on partners fields
For Name field or M2O, gives a list of companies Data comes from Odoo IAP Service
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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_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.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:ir.model,name:partner_autocomplete.model_res_partner_autocomplete_sync
|
||||
msgid "res.partner.autocomplete.sync"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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',
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
});
|
||||
@@ -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();
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
@@ -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,
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import res_partner
|
||||
@@ -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(),
|
||||
}
|
||||
@@ -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.")
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)]",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.")
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user