[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:
Antony Lesuisse
2018-09-21 14:02:22 +02:00
committed by TWA
parent ce16d0d2ab
commit a6e1eb9f0a
51 changed files with 3525 additions and 48 deletions
+5
View File
@@ -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
+1 -1
View File
@@ -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"/>
+1
View File
@@ -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>
+1 -1
View File
@@ -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)
-1
View File
@@ -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)
+11 -5
View File
@@ -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>
+1 -1
View File
@@ -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):
+2 -2
View File
@@ -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,
+3
View File
@@ -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,
}
+12
View File
@@ -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 "&amp;nbsp;&amp;nbsp;&amp;nbsp;"
msgstr ""
#. module: partner_autocomplete
#: model_terms:ir.ui.view,arch_db:partner_autocomplete.additional_info_template
msgid "<b>Annual revenue:</b>"
msgstr ""
#. module: partner_autocomplete
#: model_terms:ir.ui.view,arch_db:partner_autocomplete.additional_info_template
msgid "<b>Description</b><br/>"
msgstr ""
#. module: partner_autocomplete
#: model_terms:ir.ui.view,arch_db:partner_autocomplete.additional_info_template
msgid "<b>Email addresses</b><br/>"
msgstr ""
#. module: partner_autocomplete
#: model_terms:ir.ui.view,arch_db:partner_autocomplete.additional_info_template
msgid "<b>Employees:</b>"
msgstr ""
#. module: partner_autocomplete
#: model_terms:ir.ui.view,arch_db:partner_autocomplete.additional_info_template
msgid "<b>Estimated annual revenue:</b>"
msgstr ""
#. module: partner_autocomplete
#: model_terms:ir.ui.view,arch_db:partner_autocomplete.additional_info_template
msgid "<b>Phone numbers</b><br/>"
msgstr ""
#. module: partner_autocomplete
#: model_terms:ir.ui.view,arch_db:partner_autocomplete.additional_info_template
msgid "<b>Sector:</b>"
msgstr ""
#. module: partner_autocomplete
#: model_terms:ir.ui.view,arch_db:partner_autocomplete.additional_info_template
msgid "<b>Social networks</b><br/>\n"
" &amp;nbsp;&amp;nbsp;&amp;nbsp; <b>Facebook:</b>"
msgstr ""
#. module: partner_autocomplete
#: model_terms:ir.ui.view,arch_db:partner_autocomplete.additional_info_template
msgid "<b>Tech:</b>"
msgstr ""
#. module: partner_autocomplete
#: model_terms:ir.ui.view,arch_db:partner_autocomplete.additional_info_template
msgid "<br/>\n"
" &amp;nbsp;&amp;nbsp;&amp;nbsp; <b>Crunchbase:</b>"
msgstr ""
#. module: partner_autocomplete
#: model_terms:ir.ui.view,arch_db:partner_autocomplete.additional_info_template
msgid "<br/>\n"
" &amp;nbsp;&amp;nbsp;&amp;nbsp; <b>Linkedin:</b>"
msgstr ""
#. module: partner_autocomplete
#: model_terms:ir.ui.view,arch_db:partner_autocomplete.additional_info_template
msgid "<br/>\n"
" &amp;nbsp;&amp;nbsp;&amp;nbsp; <b>Twitter:</b>"
msgstr ""
#. module: partner_autocomplete
#: model_terms:ir.ui.view,arch_db:partner_autocomplete.res_config_settings_view_form
msgid "<i class=\"fa fa-arrow-right\"/>\n"
" Buy more credits"
msgstr ""
#. module: partner_autocomplete
#: model_terms:ir.ui.view,arch_db:partner_autocomplete.res_config_settings_view_form
msgid "<i class=\"fa fa-exclamation-triangle text-warning\"/> &amp;nbsp; You don't have credits to auto-complete companies' data anymore."
msgstr ""
#. module: partner_autocomplete
#: model_terms:ir.ui.view,arch_db:partner_autocomplete.res_config_settings_view_form
msgid "<span>&amp;times;</span>"
msgstr ""
#. module: partner_autocomplete
#: model:ir.model.fields,field_description:partner_autocomplete.field_res_partner__additional_info
#: model:ir.model.fields,field_description:partner_autocomplete.field_res_users__additional_info
msgid "Additional info"
msgstr ""
#. module: partner_autocomplete
#. openerp-web
#: code:addons/partner_autocomplete/static/src/xml/partner_autocomplete.xml:30
#, python-format
msgid "Buy more credits"
msgstr ""
#. module: partner_autocomplete
#. openerp-web
#: code:addons/partner_autocomplete/static/src/xml/partner_autocomplete.xml:20
#, python-format
msgid "Checking remaining credit ..."
msgstr ""
#. module: partner_autocomplete
#: model_terms:ir.ui.view,arch_db:partner_autocomplete.res_config_settings_view_form
msgid "Close"
msgstr ""
#. module: partner_autocomplete
#: model:ir.model,name:partner_autocomplete.model_res_company
msgid "Companies"
msgstr ""
#. module: partner_autocomplete
#: model:ir.model.fields,field_description:partner_autocomplete.field_res_company__partner_gid
#: model:ir.model.fields,field_description:partner_autocomplete.field_res_partner__partner_gid
#: model:ir.model.fields,field_description:partner_autocomplete.field_res_users__partner_gid
msgid "Company database ID"
msgstr ""
#. module: partner_autocomplete
#: model:ir.model,name:partner_autocomplete.model_res_partner
msgid "Contact"
msgstr ""
#. module: partner_autocomplete
#: model_terms:ir.ui.view,arch_db:partner_autocomplete.res_company_view_form
#: model_terms:ir.ui.view,arch_db:partner_autocomplete.view_partner_form
#: model_terms:ir.ui.view,arch_db:partner_autocomplete.view_partner_short_form
msgid "VAT"
#. openerp-web
#: code:addons/partner_autocomplete/static/src/js/partner_autocomplete_many2one.js:108
#, python-format
msgid "Create and Edit from Autocomplete :"
msgstr ""
#. module: partner_autocomplete
#: model_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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_partner_autocomplete_sync_all_all res.partner.autocomplete.sync.all model_res_partner_autocomplete_sync 1 0 0 0
3 access_partner_autocomplete_sync_portal res.partner.autocomplete.sync.portal model_res_partner_autocomplete_sync base.group_portal 1 0 1 0
4 access_partner_autocomplete_sync_user res.partner.autocomplete.sync.user model_res_partner_autocomplete_sync base.group_user 1 1 1 0
5 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 '&#160;'"/></strong>
<div><t t-esc="info['description']"/></div>
</div>
</a>
</t>
</div>
<div t-name="partner_autocomplete.iap_credit_checker" class="mt-2 row">
<div class="col-sm">
<div class="loading text-muted">
<i class="fa fa-spinner fa-spin"/> Checking remaining credit ...
</div>
<div class="insufficient">
<i class="fa fa-exclamation-triangle text-warning"/> Insufficient credit<br/>
</div>
<div class="sufficient">
<i class="fa fa-check-circle text-success"/> Remaining : <b class="remaining_credits"/> credits
</div>
</div>
<div class="col-sm text-right">
<a class="oe_link" target="_blank"><i class="fa fa-arrow-right"/> Buy more credits</a>
</div>
</div>
</templates>
@@ -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/>
&amp;nbsp;&amp;nbsp;&amp;nbsp; <b>Facebook:</b> <a t-att-href="facebook" target="_blank"><t t-esc="facebook"/></a><br/>
&amp;nbsp;&amp;nbsp;&amp;nbsp; <b>Linkedin:</b> <a t-att-href="linkedin" target="_blank"><t t-esc="linkedin"/></a><br/>
&amp;nbsp;&amp;nbsp;&amp;nbsp; <b>Crunchbase:</b> <a t-att-href="crunchbase" target="_blank"><t t-esc="crunchbase"/></a><br/>
&amp;nbsp;&amp;nbsp;&amp;nbsp; <b>Twitter:</b> <a t-att-href="twitter" target="_blank"><t t-esc="twitter"/></a><br/>
</p>
<p t-if="emails">
<b>Email addresses</b><br/>
<t t-foreach="emails" t-as="email">
&amp;nbsp;&amp;nbsp;&amp;nbsp; <a t-att-href="'mailto:%s' % email"><t t-esc="email"/></a><br/>
</t>
</p>
<p t-if="phones">
<b>Phone numbers</b><br/>
<t t-foreach="phones" t-as="phone">
&amp;nbsp;&amp;nbsp;&amp;nbsp; <t t-esc="phone"/><br/>
</t>
</p>
</template>
</odoo>
@@ -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> &amp;nbsp; You don't have credits to auto-complete companies' data anymore.
<button name="redirect_to_buy_autocmplete_credit" type="object" class="btn-link">
<i class="fa fa-arrow-right"/>
Buy more credits
</button>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span>&amp;times;</span>
</button>
</div>
</xpath>
</field>
</record>
</odoo>
@@ -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(),
}
+1 -1
View File
@@ -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.")
+3 -1
View File
@@ -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>
+1 -1
View File
@@ -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)]",
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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.")
+1 -1
View File
@@ -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('&nbsp;').append($enterpriseLabel);
},
+12 -1
View File
@@ -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>
+9 -5
View File
@@ -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': ['|', '&amp;', ('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': ['|', '&amp;', ('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">