summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2018-03-09 16:25:58 +0100
committerChristophe Simonis <chs@odoo.com>2018-03-09 16:25:58 +0100
commit0f57bdba72b592ca097d4cd3da99a75883d8c623 (patch)
tree94eef0c9aaf885c7f717bab5e468ca72b59a219d
parent35f0797002e44ce5673e9fab63710d2764329694 (diff)
parent42659dc70c2b9b124e7e4fdc0449f660c3de5c76 (diff)
[MERGE] forward port branch saas-11.2 up to 42659dc70c2
-rw-r--r--MANIFEST.in5
-rw-r--r--addons/account/__manifest__.py1
-rw-r--r--addons/account/controllers/mail.py1
-rw-r--r--addons/account/data/ir_cron_data.xml14
-rw-r--r--addons/account/data/payment_receipt_data.xml2
-rw-r--r--addons/account/models/account.py6
-rw-r--r--addons/account/models/account_invoice.py65
-rw-r--r--addons/account/models/account_journal_dashboard.py3
-rw-r--r--addons/account/models/account_move.py6
-rw-r--r--addons/account/models/account_payment.py4
-rw-r--r--addons/account/models/company.py8
-rw-r--r--addons/account/models/res_config_settings.py2
-rw-r--r--addons/account/security/account_security.xml5
-rw-r--r--addons/account/static/src/js/reconciliation/reconciliation_renderer.js35
-rw-r--r--addons/account/static/tests/reconciliation_tests.js103
-rw-r--r--addons/account/tests/__init__.py1
-rw-r--r--addons/account/tests/test_account_supplier_invoice_recurrent.py104
-rw-r--r--addons/account/tests/test_payment.py54
-rw-r--r--addons/account/tests/test_reconciliation.py3
-rw-r--r--addons/account/views/account_invoice_view.xml52
-rw-r--r--addons/account/views/account_journal_dashboard_view.xml12
-rw-r--r--addons/account/views/account_report.xml1
-rw-r--r--addons/account/views/account_view.xml2
-rw-r--r--addons/account/views/res_config_settings_views.xml19
-rw-r--r--addons/account_analytic_default/models/account_analytic_default.py4
-rw-r--r--addons/account_analytic_default/models/sale_order_line.py1
-rw-r--r--addons/account_invoicing/__init__.py9
-rw-r--r--addons/account_invoicing/__manifest__.py1
-rw-r--r--addons/auth_signup/models/__init__.py1
-rw-r--r--addons/auth_signup/models/ir_model_fields.py19
-rw-r--r--addons/barcodes/static/src/js/barcode_form_view.js8
-rw-r--r--addons/base_gengo/wizard/base_gengo_translations.py2
-rw-r--r--addons/base_iban/models/res_partner_bank.py1
-rw-r--r--addons/base_import/models/base_import.py4
-rw-r--r--addons/base_import_module/models/ir_module.py11
-rw-r--r--addons/calendar_sms/__manifest__.py2
-rw-r--r--addons/crm/models/crm_lead.py26
-rw-r--r--addons/crm/models/res_config_settings.py4
-rw-r--r--addons/crm/views/res_config_settings_views.xml2
-rw-r--r--addons/delivery/models/__init__.py1
-rw-r--r--addons/delivery/models/delivery_carrier.py5
-rw-r--r--addons/delivery/models/res_config_settings.py13
-rw-r--r--addons/delivery/security/delivery_carrier_security.xml1
-rw-r--r--addons/event/data/email_template_data.xml22
-rw-r--r--addons/fleet/models/fleet_vehicle.py4
-rw-r--r--addons/google_calendar/README.md2
-rw-r--r--addons/hr_expense/models/hr_expense.py12
-rw-r--r--addons/hr_expense/views/hr_expense_views.xml51
-rw-r--r--addons/hr_holidays/models/hr_leave.py2
-rw-r--r--addons/hr_payroll/models/hr_payslip.py3
-rw-r--r--addons/hr_payroll/wizard/hr_payroll_payslips_by_employees.py1
-rw-r--r--addons/hr_timesheet/models/hr_timesheet.py2
-rw-r--r--addons/hr_timesheet/models/project.py15
-rw-r--r--addons/hr_timesheet/views/project_views.xml9
-rw-r--r--addons/im_livechat/controllers/main.py3
-rw-r--r--addons/im_livechat/models/im_livechat_channel.py2
-rw-r--r--addons/im_livechat/static/src/js/im_livechat.js4
-rw-r--r--addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py6
-rw-r--r--addons/l10n_be_hr_payroll_fleet/views/res_config_settings_views.xml4
-rw-r--r--addons/l10n_fr/data/account_chart_template_data.xml60
-rw-r--r--addons/l10n_in/views/report_invoice.xml10
-rw-r--r--addons/l10n_mx/data/account_tax_data.xml4
-rw-r--r--addons/mail/data/mail_shortcode_data.xml17
-rw-r--r--addons/mail/models/mail_activity.py9
-rw-r--r--addons/mail/models/mail_channel.py2
-rw-r--r--addons/mail/static/src/img/smiley/smile_open_mouth.pngbin0 -> 6262 bytes
-rw-r--r--addons/mail/static/src/js/activity.js147
-rw-r--r--addons/mail/static/src/js/composer.js2
-rw-r--r--addons/mail/static/src/js/discuss_mobile.js4
-rw-r--r--addons/mail/static/src/js/document_viewer.js35
-rw-r--r--addons/mail/static/src/less/composer.less4
-rw-r--r--addons/mail/static/src/less/discuss.less8
-rw-r--r--addons/mail/static/src/less/thread.less7
-rw-r--r--addons/mail/static/src/xml/thread.xml27
-rw-r--r--addons/mail/static/tests/chatter_tests.js128
-rw-r--r--addons/mail/static/tests/discuss_mobile_tests.js23
-rw-r--r--addons/mail/static/tests/discuss_tests.js5
-rw-r--r--addons/mail/static/tests/helpers/test_utils.js9
-rw-r--r--addons/mrp/wizard/mrp_product_produce.py2
-rw-r--r--addons/note_pad/models/note.py2
-rw-r--r--addons/pad/models/pad.py31
-rw-r--r--addons/pad_project/models/project.py9
-rw-r--r--addons/payment/controllers/portal.py6
-rw-r--r--addons/payment/models/payment_acquirer.py13
-rw-r--r--addons/payment/security/ir.model.access.csv1
-rw-r--r--addons/payment/views/payment_portal_templates.xml4
-rw-r--r--addons/payment/views/payment_views.xml2
-rw-r--r--addons/payment_ogone/controllers/main.py3
-rw-r--r--addons/payment_ogone/models/payment.py27
-rw-r--r--addons/payment_stripe/static/src/js/stripe.js4
-rw-r--r--addons/point_of_sale/README.md2
-rw-r--r--addons/point_of_sale/data/point_of_sale_demo.xml1
-rw-r--r--addons/point_of_sale/models/pos_config.py3
-rw-r--r--addons/point_of_sale/models/pos_order.py9
-rw-r--r--addons/point_of_sale/static/src/js/models.js16
-rw-r--r--addons/point_of_sale/static/src/js/screens.js15
-rw-r--r--addons/point_of_sale/static/src/js/tests.js17
-rw-r--r--addons/point_of_sale/views/account_journal_view.xml9
-rw-r--r--addons/portal/controllers/mail.py4
-rw-r--r--addons/portal/static/src/js/portal_chatter.js3
-rw-r--r--addons/pos_discount/models/pos_config.py13
-rw-r--r--addons/pos_discount/static/src/js/discount.js7
-rw-r--r--addons/pos_mercury/static/src/js/pos_mercury.js165
-rw-r--r--addons/product/models/product_template.py2
-rw-r--r--addons/product/tests/test_variants.py20
-rw-r--r--addons/product_expiry/models/product_product.py2
-rw-r--r--addons/project/models/project.py12
-rw-r--r--addons/project/static/src/less/project_dashboard.less4
-rw-r--r--addons/project/views/project_portal_templates.xml10
-rw-r--r--addons/project/views/project_views.xml6
-rw-r--r--addons/purchase/data/mail_template_data.xml2
-rw-r--r--addons/purchase/models/purchase.py10
-rw-r--r--addons/purchase/models/stock.py20
-rw-r--r--addons/purchase/tests/test_create_picking.py89
-rw-r--r--addons/purchase/tests/test_fifo_price.py3
-rw-r--r--addons/purchase/views/account_invoice_views.xml2
-rw-r--r--addons/purchase/views/stock_views.xml9
-rw-r--r--addons/purchase_requisition/models/purchase_requisition.py7
-rw-r--r--addons/purchase_requisition/views/purchase_requisition_views.xml2
-rw-r--r--addons/resource/models/resource.py2
-rw-r--r--addons/resource/models/resource_mixin.py6
-rw-r--r--addons/sale/__init__.py3
-rw-r--r--addons/sale/controllers/mail.py1
-rw-r--r--addons/sale/models/__init__.py1
-rw-r--r--addons/sale/models/account_invoice.py10
-rw-r--r--addons/sale/models/ir_model_fields.py19
-rw-r--r--addons/sale/models/product_product.py2
-rw-r--r--addons/sale/models/sale.py2
-rw-r--r--addons/sale/views/sale_views.xml9
-rw-r--r--addons/sale/wizard/sale_make_invoice_advance.py5
-rw-r--r--addons/sale_crm/models/crm_lead.py3
-rw-r--r--addons/sale_management/__init__.py9
-rw-r--r--addons/sale_management/__manifest__.py1
-rw-r--r--addons/sale_stock/models/account_invoice.py4
-rw-r--r--addons/sale_stock/views/stock_views.xml3
-rw-r--r--addons/sale_timesheet/models/account.py2
-rw-r--r--addons/sale_timesheet/models/sale_order.py4
-rw-r--r--addons/sale_timesheet/views/hr_timesheet_templates.xml16
-rw-r--r--addons/stock/__init__.py14
-rw-r--r--addons/stock/__manifest__.py1
-rw-r--r--addons/stock/data/stock_demo2.xml7
-rw-r--r--addons/stock/models/procurement.py15
-rw-r--r--addons/stock/models/stock_move.py8
-rw-r--r--addons/stock/models/stock_move_line.py4
-rw-r--r--addons/stock/models/stock_picking.py13
-rw-r--r--addons/stock/models/stock_quant.py10
-rw-r--r--addons/stock/models/stock_scrap.py8
-rw-r--r--addons/stock/report/report_deliveryslip.xml2
-rw-r--r--addons/stock/tests/test_move2.py45
-rw-r--r--addons/stock/views/stock_picking_views.xml10
-rw-r--r--addons/stock/wizard/stock_immediate_transfer_views.xml2
-rw-r--r--addons/stock_account/__init__.py7
-rw-r--r--addons/stock_account/models/res_config_settings.py2
-rw-r--r--addons/stock_account/models/stock.py34
-rw-r--r--addons/stock_account/tests/test_stockvaluation.py64
-rw-r--r--addons/stock_account/views/res_config_settings_views.xml2
-rw-r--r--addons/stock_account/views/stock_account_views.xml2
-rw-r--r--addons/stock_dropshipping/tests/__init__.py2
-rw-r--r--addons/stock_dropshipping/tests/test_lifo_price.py3
-rw-r--r--addons/stock_dropshipping/tests/test_stockvaluation.py269
-rw-r--r--addons/stock_landed_costs/views/stock_landed_cost_views.xml2
-rw-r--r--addons/survey/models/survey.py1
-rw-r--r--addons/test_mail/tests/test_performance.py6
-rw-r--r--addons/web/static/lib/qweb/qweb2.js7
-rw-r--r--addons/web/static/src/js/chrome/abstract_web_client.js13
-rw-r--r--addons/web/static/src/js/chrome/action_manager.js16
-rw-r--r--addons/web/static/src/js/chrome/action_manager_act_window.js53
-rw-r--r--addons/web/static/src/js/chrome/control_panel.js4
-rw-r--r--addons/web/static/src/js/chrome/search_menus.js31
-rw-r--r--addons/web/static/src/js/core/utils.js2
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js5
-rw-r--r--addons/web/static/src/js/services/crash_manager.js6
-rw-r--r--addons/web/static/src/js/views/basic/basic_controller.js7
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js156
-rw-r--r--addons/web/static/src/js/views/basic/basic_view.js9
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_controller.js2
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_model.js8
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_renderer.js5
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_controller.js1
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_model.js2
-rw-r--r--addons/web/static/src/js/views/view_dialogs.js19
-rw-r--r--addons/web/static/src/js/widgets/data_export.js2
-rw-r--r--addons/web/static/src/js/widgets/domain_selector_dialog.js6
-rw-r--r--addons/web/static/src/js/widgets/notification.js6
-rw-r--r--addons/web/static/src/less/form_view.less4
-rw-r--r--addons/web/static/src/less/views.less4
-rw-r--r--addons/web/static/src/less/web_calendar.less5
-rw-r--r--addons/web/static/src/xml/base.xml1
-rw-r--r--addons/web/static/tests/chrome/action_manager_mobile_tests.js43
-rw-r--r--addons/web/static/tests/chrome/action_manager_tests.js105
-rw-r--r--addons/web/static/tests/core/util_tests.js11
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js180
-rw-r--r--addons/web/static/tests/helpers/mock_server.js6
-rw-r--r--addons/web/static/tests/views/basic_model_tests.js36
-rw-r--r--addons/web/static/tests/views/view_dialogs_tests.js95
-rw-r--r--addons/web_editor/static/src/js/editor/summernote.js2
-rw-r--r--addons/web_settings_dashboard/static/src/js/dashboard.js23
-rw-r--r--addons/website/controllers/main.py2
-rw-r--r--addons/website/models/ir_model_fields.py11
-rw-r--r--addons/website/models/website.py6
-rw-r--r--addons/website/static/src/js/menu/content.js2
-rw-r--r--addons/website_crm_partner_assign/models/crm_lead.py2
-rw-r--r--addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml2
-rw-r--r--addons/website_crm_partner_assign/wizard/crm_forward_to_partner.py2
-rw-r--r--addons/website_customer/controllers/main.py2
-rw-r--r--addons/website_form/controllers/main.py16
-rw-r--r--addons/website_form/static/src/js/website_form.js4
-rw-r--r--addons/website_mail_channel/views/website_mail_channel_templates.xml2
-rw-r--r--addons/website_quote/controllers/main.py4
-rw-r--r--addons/website_quote/models/sale_order.py17
-rw-r--r--addons/website_sale/models/website.py2
-rw-r--r--addons/website_sale/views/templates.xml2
-rw-r--r--addons/website_sale_delivery/models/res_country.py2
-rw-r--r--addons/website_sale_delivery/views/website_sale_delivery_templates.xml6
-rw-r--r--addons/website_slides/models/slides.py2
-rw-r--r--addons/website_slides/static/src/js/slides.js7
-rw-r--r--addons/website_slides/static/src/js/slides_embed.js4
-rw-r--r--doc/_extensions/odoo_ext/layout.html4
-rw-r--r--doc/cla/corporate/camptocamp.md1
-rw-r--r--doc/cla/corporate/d2c.md13
-rw-r--r--doc/cla/corporate/merchise.md1
-rw-r--r--doc/cla/corporate/odoogap.md18
-rw-r--r--doc/cla/individual/apesquero.md11
-rw-r--r--doc/cla/individual/aymanadam5.md11
-rw-r--r--doc/cla/individual/hilarak.md11
-rw-r--r--doc/cla/individual/lbelorgey.md2
-rw-r--r--doc/cla/individual/novawish.md11
-rw-r--r--doc/cla/individual/true-datura.md11
-rw-r--r--doc/howtos/profile/flamegraph.svg1774
-rw-r--r--doc/howtos/profilecode.rst88
-rw-r--r--doc/howtos/themes.rst176
-rw-r--r--doc/reference/cmdline.rst49
-rw-r--r--doc/reference/views.rst3
-rw-r--r--doc/setup/deploy.rst16
-rw-r--r--doc/tutorials.rst1
-rw-r--r--doc/webservices.rst2
-rw-r--r--odoo/addons/base/data/ir_mail_server_data.xml4
-rw-r--r--odoo/addons/base/models/ir_actions_report.py2
-rw-r--r--odoo/addons/base/models/ir_autovacuum.py6
-rw-r--r--odoo/addons/base/models/ir_cron.py1
-rw-r--r--odoo/addons/base/models/ir_ui_view.py66
-rw-r--r--odoo/addons/base/models/res_company.py13
-rw-r--r--odoo/addons/base/models/res_partner.py2
-rw-r--r--odoo/addons/base/models/res_users.py6
-rw-r--r--odoo/addons/base/views/ir_qweb_widget_templates.xml4
-rw-r--r--odoo/addons/test_new_api/models.py4
-rw-r--r--odoo/addons/test_new_api/tests/test_new_fields.py36
-rw-r--r--odoo/addons/test_new_api/tests/test_onchange.py29
-rw-r--r--odoo/addons/test_pylint/tests/test_pylint.py2
-rw-r--r--odoo/fields.py2
-rw-r--r--odoo/models.py19
251 files changed, 4695 insertions, 1145 deletions
diff --git a/MANIFEST.in b/MANIFEST.in
index 46e071c97c8..d4e3af91152 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -4,6 +4,7 @@ include README.md
recursive-include odoo *.css
recursive-include odoo *.csv
recursive-include odoo *.doc
+recursive-include odoo *.docx
recursive-include odoo *.eml
recursive-include odoo *.eot
recursive-include odoo *.gif
@@ -16,6 +17,8 @@ recursive-include odoo *.less
recursive-include odoo *.md
recursive-include odoo *.mp3
recursive-include odoo *.ogg
+recursive-include odoo *.ods
+recursive-include odoo *.odt
recursive-include odoo *.otf
recursive-include odoo *.pdf
recursive-include odoo *.png
@@ -35,8 +38,10 @@ recursive-include odoo *.wsdl
recursive-include odoo *.xls
recursive-include odoo *.xsd
recursive-include odoo *.xsl
+recursive-include odoo *.xlsx
recursive-include odoo *.xml
recursive-include odoo *.yml
+recursive-include odoo *.zip
recursive-include odoo/addons/l10n_mx_edi *.xslt *.key *.cer *.txt
recursive-include odoo/addons/l10n_mx_reports *.xslt
recursive-exclude * *.py[co]
diff --git a/addons/account/__manifest__.py b/addons/account/__manifest__.py
index a1a7754a076..8ead6af8961 100644
--- a/addons/account/__manifest__.py
+++ b/addons/account/__manifest__.py
@@ -17,7 +17,6 @@ Core mechanisms for the accounting modules. To display the menuitems, install th
'security/ir.model.access.csv',
'data/data_account_type.xml',
'data/account_data.xml',
- 'data/ir_cron_data.xml',
'views/account_menuitem.xml',
'views/account_payment_view.xml',
'wizard/account_reconcile_view.xml',
diff --git a/addons/account/controllers/mail.py b/addons/account/controllers/mail.py
index f30c69e21ba..564856b909c 100644
--- a/addons/account/controllers/mail.py
+++ b/addons/account/controllers/mail.py
@@ -11,6 +11,7 @@ from odoo.tools.misc import consteq
class MailController(MailController):
+ @classmethod
def _redirect_to_record(cls, model, res_id, access_token=None):
# If the current user doesn't have access to the invoice, but provided
# a valid access token, redirect him to the front-end view.
diff --git a/addons/account/data/ir_cron_data.xml b/addons/account/data/ir_cron_data.xml
deleted file mode 100644
index 6e3ceb702aa..00000000000
--- a/addons/account/data/ir_cron_data.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo noupdate="1">
- <!-- Cron job for creating recurring vendor bills -->
- <record id="recurrent_vendor_bills_cron" model="ir.cron">
- <field name="name">Account: generate recurring vendor bills</field>
- <field name="model_id" ref="account.model_account_invoice"/>
- <field name="state">code</field>
- <field name="active">False</field>
- <field name="code">model._cron_generate_recurring_vendor_bill()</field>
- <field name="interval_number">1</field>
- <field name="interval_type">days</field>
- <field name="numbercall">-1</field>
- </record>
-</odoo>
diff --git a/addons/account/data/payment_receipt_data.xml b/addons/account/data/payment_receipt_data.xml
index 3190edfb151..d3c02413732 100644
--- a/addons/account/data/payment_receipt_data.xml
+++ b/addons/account/data/payment_receipt_data.xml
@@ -14,7 +14,7 @@
<field name="lang">${object.partner_id.lang}</field>
<field name="body_html" type="xml">
<p>Dear ${object.partner_id.name},</p>
-<p>Thank you for your payment.<br />Here is your payment receipt <strong>${(object.name or '').replace('/','-')}</strong> amounting to <strong>${object.amount} ${object.currency_id.name}</strong> from ${object.company_id.name}.</p>
+<p>Thank you for your payment.<br />Here is your payment receipt <strong>${(object.name or '').replace('/','-')}</strong> amounting to <strong>${format_amount(object.amount, object.currency_id)}</strong> from ${object.company_id.name}.</p>
<p>If you have any questions, please do not hesitate to contact us.</p>
<p>Best regards,
% if user and user.signature:
diff --git a/addons/account/models/account.py b/addons/account/models/account.py
index 2f5c1421a60..b7d8d52f096 100644
--- a/addons/account/models/account.py
+++ b/addons/account/models/account.py
@@ -441,7 +441,7 @@ class AccountJournal(models.Model):
for journal in self:
if journal.sequence_id and journal.sequence_number_next:
sequence = journal.sequence_id._get_current_sequence()
- sequence.number_next = journal.sequence_number_next
+ sequence.sudo().number_next = journal.sequence_number_next
@api.multi
# do not depend on 'refund_sequence_id.date_range_ids', because
@@ -523,6 +523,8 @@ class AccountJournal(models.Model):
if ('company_id' in vals and journal.company_id.id != vals['company_id']):
if self.env['account.move'].search([('journal_id', 'in', self.ids)], limit=1):
raise UserError(_('This journal already contains items, therefore you cannot modify its company.'))
+ if self.bank_account_id:
+ self.bank_account_id.company_id = vals['company_id']
if ('code' in vals and journal.code != vals['code']):
if self.env['account.move'].search([('journal_id', 'in', self.ids)], limit=1):
raise UserError(_('This journal already contains items, therefore you cannot modify its short name.'))
@@ -536,6 +538,8 @@ class AccountJournal(models.Model):
self.default_debit_account_id.currency_id = vals['currency_id']
if not 'default_credit_account_id' in vals and self.default_credit_account_id:
self.default_credit_account_id.currency_id = vals['currency_id']
+ if self.bank_account_id:
+ self.bank_account_id.currency_id = vals['currency_id']
if 'bank_account_id' in vals and not vals.get('bank_account_id'):
raise UserError(_('You cannot remove the bank account from the journal once set.'))
result = super(AccountJournal, self).write(vals)
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py
index e1e3dfa0499..47d7bcaf446 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -229,12 +229,6 @@ class AccountInvoice(models.Model):
payment_lines.update(line.mapped('matched_debit_ids.debit_move_id.id'))
self.payment_move_line_ids = self.env['account.move.line'].browse(list(payment_lines))
- @api.depends('date_invoice', 'is_recurrency_enabled', 'recurrency_interval', 'recurrency_type')
- def _compute_next_recurrency_date(self):
- #initially set the recurrency_date
- for invoice in self.filtered(lambda inv: inv.is_recurrency_enabled and inv.recurrency_interval and inv.recurrency_type and inv.date_invoice and not inv.next_recurrency_date):
- invoice.next_recurrency_date = invoice._get_next_recurrency_date(invoice.date_invoice)
-
name = fields.Char(string='Reference/Description', index=True,
readonly=True, states={'draft': [('readonly', False)]}, copy=False, help='The name that will be used on account move lines')
@@ -373,18 +367,6 @@ class AccountInvoice(models.Model):
#fields use to set the sequence, on the first invoice of the journal
sequence_number_next = fields.Char(string='Next Number', compute="_get_sequence_number_next", inverse="_set_sequence_next")
sequence_number_next_prefix = fields.Char(string='Next Number Prefix', compute="_get_sequence_prefix")
- #recurrent vendor bills fields
- is_recurrency_enabled = fields.Boolean(string="Generate Recurring Document(s)", copy=False, help="Tick this box to generate recurring bills. The recurring bills will only be generated if this bill is validated.")
- recurrency_interval = fields.Integer(string='Every', default=1, copy=False, help="Define the recurrency of the vendor bill.")
- recurrency_type = fields.Selection([
- ('days', 'Day(s)'),
- ('weeks', 'Week(s)'),
- ('months', 'Month(s)'),
- ('years', 'Year(s)'),
- ], string="Recurrency Type", default='months', copy=False)
- # Technical field depicting whether the document is generated by cron or by user
- is_recurring_document = fields.Boolean(string="Recurring Document", copy=False)
- next_recurrency_date = fields.Date(compute=_compute_next_recurrency_date, _string='Next Recurrency Date', copy=False, store=True)
_sql_constraints = [
('number_uniq', 'unique(number, company_id, journal_id, type)', 'Invoice Number must be unique per Company!'),
@@ -468,52 +450,6 @@ class AccountInvoice(models.Model):
self.type == 'in_refund' and self.state == 'draft' and _('Vendor Credit Note') or \
self.type == 'in_refund' and _('Vendor Credit Note - %s') % (self.number)
- def _get_next_recurrency_date(self, current_date):
- """
- This method is used to return next date based on recurrency configuration of the record.
-
- :param current_date: Origin date from which next date is to be generated.
- :returns: future date on which a duplicated document will be generated by crons.
- """
- assert self.recurrency_type, "Invalid `Recurrency` interval! Please set it to either of these: 'Day(s)', 'Week(s)', 'Month(s)' or 'Year(s)'."
- self.ensure_one()
- recurring_next_date = fields.Date.from_string(current_date) + relativedelta(**{self.recurrency_type: self.recurrency_interval})
- # In case of monthly recurrent invoice, future invoices should always be created on same day as of it's main invoice but
- # if the reference invoice was made on last day of the month, then the recurring invoice should be created on last day of next month.
- # example, main invoice is set to 31st Jan, the next invoices should be created on 28/29th Feb, 31st Mar, 30 Apr and so on
- if self.recurrency_type == 'months':
- reference_invoice_day = fields.Date.from_string(self.date_invoice).day
- recurring_next_date += relativedelta(day=reference_invoice_day)
- if reference_invoice_day == fields.Date.from_string(self.date_invoice) + relativedelta(day=31):
- recurring_next_date += relativedelta(day=31)
- return recurring_next_date
-
- @api.model
- def _cron_generate_recurring_vendor_bill(self):
- today = fields.Date.today()
- domain = [
- ('is_recurrency_enabled', '=', True),
- ('state', 'not in', ['draft', 'cancel']),
- ('next_recurrency_date', '<=', today)
- ]
- for invoice in self.search(domain):
- while invoice.next_recurrency_date <= today:
- invoice._create_recurrent_invoice()
- invoice.next_recurrency_date = invoice._get_next_recurrency_date(invoice.next_recurrency_date)
-
- def _create_recurrent_invoice(self):
- self.ensure_one()
- invoice = self.copy({
- 'origin': '',
- 'date_invoice': self.next_recurrency_date,
- 'is_recurring_document': True,
- })
- # set the due date
- #invoice._onchange_payment_term_date_invoice()
- link = '<a href=# data-oe-model=account.invoice data-oe-id=%d>%s</a>' % (self.id, self.number)
- invoice.message_post(body=_('This bill has been generated by %s') % link)
- return invoice
-
@api.model
def create(self, vals):
onchanges = {
@@ -1862,6 +1798,7 @@ class AccountPaymentTermLine(models.Model):
if self.value == 'percent' and (self.value_amount < 0.0 or self.value_amount > 100.0):
raise ValidationError(_('Percentages for Payment Terms Line must be between 0 and 100.'))
+ @api.one
@api.constrains('days')
def _check_days(self):
if self.option in ('day_following_month', 'day_current_month') and self.days <= 0:
diff --git a/addons/account/models/account_journal_dashboard.py b/addons/account/models/account_journal_dashboard.py
index e66a6ab068e..a769a3f8d17 100644
--- a/addons/account/models/account_journal_dashboard.py
+++ b/addons/account/models/account_journal_dashboard.py
@@ -341,12 +341,13 @@ class account_journal(models.Model):
ctx.update({
'journal_type': self.type,
'default_journal_id': self.id,
- 'search_default_journal_id': self.id,
'default_type': invoice_type,
'type': invoice_type
})
[action] = self.env.ref('account.%s' % action_name).read()
+ if not self.env.context.get('use_domain'):
+ ctx['search_default_journal_id'] = self.id
action['context'] = ctx
action['domain'] = self._context.get('use_domain', [])
account_invoice_filter = self.env.ref('account.view_account_invoice_filter', False)
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index 78e9cc0e3e9..f5196eb02d6 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -106,7 +106,7 @@ class AccountMove(models.Model):
default=lambda self: self.env.user.company_id)
matched_percentage = fields.Float('Percentage Matched', compute='_compute_matched_percentage', digits=0, store=True, readonly=True, help="Technical field used in cash basis method")
# Dummy Account field to search on account.move by account_id
- dummy_account_id = fields.Many2one('account.account', related='line_ids.account_id', string='Account', store=False)
+ dummy_account_id = fields.Many2one('account.account', related='line_ids.account_id', string='Account', store=False, readonly=True)
tax_cash_basis_rec_id = fields.Many2one(
'account.partial.reconcile',
string='Tax Cash Basis Entry of',
@@ -318,7 +318,7 @@ class AccountMoveLine(models.Model):
if not cr.fetchone():
cr.execute('CREATE INDEX account_move_line_partner_id_ref_idx ON account_move_line (partner_id, ref)')
- @api.depends('debit', 'credit', 'amount_currency', 'currency_id', 'matched_debit_ids', 'matched_credit_ids', 'matched_debit_ids.amount', 'matched_credit_ids.amount', 'account_id.currency_id', 'move_id.state')
+ @api.depends('debit', 'credit', 'amount_currency', 'currency_id', 'matched_debit_ids', 'matched_credit_ids', 'matched_debit_ids.amount', 'matched_credit_ids.amount', 'move_id.state')
def _amount_residual(self):
""" Computes the residual amount of a move line from a reconcilable account in the company currency and the line's currency.
This amount will be 0 for fully reconciled lines or lines from a non-reconcilable account, the original line amount
@@ -983,7 +983,7 @@ class AccountMoveLine(models.Model):
record.payment_id.state = 'reconciled'
result = super(AccountMoveLine, self).write(vals)
- if self._context.get('check_move_validity', True):
+ if self._context.get('check_move_validity', True) and any(key in vals for key in ('account_id', 'journal_id', 'date', 'move_id', 'debit', 'credit')):
move_ids = set()
for line in self:
if line.move_id.id not in move_ids:
diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py
index 2090d03bc30..ca1e13511d4 100644
--- a/addons/account/models/account_payment.py
+++ b/addons/account/models/account_payment.py
@@ -588,9 +588,9 @@ class account_payment(models.Model):
writeoff_line['amount_currency'] = amount_currency_wo
writeoff_line['currency_id'] = currency_id
writeoff_line = aml_obj.create(writeoff_line)
- if counterpart_aml['debit'] or writeoff_line['credit']:
+ if counterpart_aml['debit'] or (writeoff_line['credit'] and not counterpart_aml['credit']):
counterpart_aml['debit'] += credit_wo - debit_wo
- if counterpart_aml['credit'] or writeoff_line['debit']:
+ if counterpart_aml['credit'] or (writeoff_line['debit'] and not counterpart_aml['debit']):
counterpart_aml['credit'] += debit_wo - credit_wo
counterpart_aml['amount_currency'] -= amount_currency_wo
diff --git a/addons/account/models/company.py b/addons/account/models/company.py
index d73dbf82c64..8ae4230a7b2 100644
--- a/addons/account/models/company.py
+++ b/addons/account/models/company.py
@@ -4,7 +4,7 @@ from datetime import timedelta, datetime
import calendar
from odoo import fields, models, api, _
-from odoo.exceptions import ValidationError
+from odoo.exceptions import ValidationError, UserError
from odoo.exceptions import UserError
from odoo.tools.float_utils import float_round, float_is_zero
@@ -130,6 +130,12 @@ Best Regards,'''))
if values.get('cash_account_code_prefix'):
new_cash_code = values.get('cash_account_code_prefix') or company.cash_account_code_prefix
company.reflect_code_prefix_change(company.cash_account_code_prefix, new_cash_code)
+
+ #forbid the change of currency_id if there are already some accounting entries existing
+ if 'currency_id' in values and values['currency_id'] != company.currency_id.id:
+ if self.env['account.move.line'].search([('company_id', '=', company.id)]):
+ raise UserError(_('You cannot change the currency of the company since some journal items already exist'))
+
return super(ResCompany, self).write(values)
@api.model
diff --git a/addons/account/models/res_config_settings.py b/addons/account/models/res_config_settings.py
index 78a743ce9d7..68d8ef3e83b 100644
--- a/addons/account/models/res_config_settings.py
+++ b/addons/account/models/res_config_settings.py
@@ -57,12 +57,10 @@ class ResConfigSettings(models.TransientModel):
tax_exigibility = fields.Boolean(string='Cash Basis', related='company_id.tax_exigibility')
tax_cash_basis_journal_id = fields.Many2one('account.journal', related='company_id.tax_cash_basis_journal_id', string="Tax Cash Basis Journal")
account_hide_setup_bar = fields.Boolean(string='Hide Setup Bar', related='company_id.account_setup_bar_closed',help="Tick if you wish to hide the setup bar on the dashboard")
- group_allow_recurring = fields.Boolean(string="Recurring Vendor Bills", implied_group="account.group_allow_recurring")
@api.multi
def set_values(self):
super(ResConfigSettings, self).set_values()
- self.env.ref('account.recurrent_vendor_bills_cron').write({'active': self.group_allow_recurring})
if self.group_multi_currency:
self.env.ref('base.group_user').write({'implied_ids': [(4, self.env.ref('product.group_sale_pricelist').id)]})
""" install a chart of accounts for the given company (if required) """
diff --git a/addons/account/security/account_security.xml b/addons/account/security/account_security.xml
index 1bcf45e5121..c527729765c 100644
--- a/addons/account/security/account_security.xml
+++ b/addons/account/security/account_security.xml
@@ -32,11 +32,6 @@
<field name="category_id" ref="base.module_category_hidden"/>
</record>
- <record id="group_allow_recurring" model="res.groups">
- <field name="name">Allow to create recurring vendor bills</field>
- <field name="category_id" ref="base.module_category_hidden"/>
- </record>
-
<record id="group_cash_rounding" model="res.groups">
<field name="name">Allow the cash rounding management</field>
<field name="category_id" ref="base.module_category_hidden"/>
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
index 8108b0efa09..9003e9e7f4d 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
@@ -426,20 +426,24 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
this._renderCreate(state);
}
var data = this.model.get(this.handleCreateRecord).data;
-
- this.model.notifyChanges(this.handleCreateRecord, state.createForm);
- this.model.notifyChanges(this.handleCreateRecord, {analytic_tag_ids: {operation: 'REPLACE_WITH', ids: []}});
- _.each(state.createForm.analytic_tag_ids, function (tag) {
- self.model.notifyChanges(self.handleCreateRecord, {analytic_tag_ids: {operation: 'ADD_M2M', ids: tag}});
- });
-
- var record = this.model.get(this.handleCreateRecord);
- _.each(this.fields, function (field, fieldName) {
- if (self._avoidFieldUpdate[fieldName]) return;
- if (fieldName === "partner_id") return;
- if ((data[fieldName] || state.createForm[fieldName]) && !_.isEqual(state.createForm[fieldName], data[fieldName])) {
- field.reset(record);
- }
+ this.model.notifyChanges(this.handleCreateRecord, state.createForm).then(function () {
+ // FIXME can't it directly written REPLACE_WITH ids=state.createForm.analytic_tag_ids
+ self.model.notifyChanges(self.handleCreateRecord, {analytic_tag_ids: {operation: 'REPLACE_WITH', ids: []}}).then(function (){
+ var defs = [];
+ _.each(state.createForm.analytic_tag_ids, function (tag) {
+ defs.push(self.model.notifyChanges(self.handleCreateRecord, {analytic_tag_ids: {operation: 'ADD_M2M', ids: tag}}));
+ });
+ $.when.apply($, defs).then(function () {
+ var record = self.model.get(self.handleCreateRecord);
+ _.each(self.fields, function (field, fieldName) {
+ if (self._avoidFieldUpdate[fieldName]) return;
+ if (fieldName === "partner_id") return;
+ if ((data[fieldName] || state.createForm[fieldName]) && !_.isEqual(state.createForm[fieldName], data[fieldName])) {
+ field.reset(record);
+ }
+ });
+ });
+ });
});
}
this.$('.create .add_line').toggle(!!state.balance.amount_currency);
@@ -655,7 +659,8 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
return;
}
if(event.keyCode === 13) {
- if (_.findWhere(this.model.lines, {mode: 'create'}).balance.amount) {
+ var created_lines = _.findWhere(this.model.lines, {mode: 'create'});
+ if (created_lines && created_lines.balance.amount) {
this._onCreateProposition();
}
return;
diff --git a/addons/account/static/tests/reconciliation_tests.js b/addons/account/static/tests/reconciliation_tests.js
index 6a26ad45f2a..13427bb6870 100644
--- a/addons/account/static/tests/reconciliation_tests.js
+++ b/addons/account/static/tests/reconciliation_tests.js
@@ -43,18 +43,23 @@ var db = {
fields: {
id: {string: "ID", type: 'integer'},
code: {string: "code", type: 'integer'},
- display_name: {string: "Displayed name", type: 'char'},
+ name: {string: "Displayed name", type: 'char'},
company_id: {string: "Company", type: 'many2one', relation: 'res.company'},
},
records: [
- {id: 282, code: 100000, display_name: "100000 Fixed Asset Account", company_id: 1},
- {id: 283, code: 101000, display_name: "101000 Current Assets", company_id: 1},
- {id: 284, code: 101110, display_name: "101110 Stock Valuation Account", company_id: 1},
- {id: 285, code: 101120, display_name: "101120 Stock Interim Account (Received)", company_id: 1},
- {id: 286, code: 101130, display_name: "101130 Stock Interim Account (Delivered)", company_id: 1},
- {id: 287, code: 101200, display_name: "101200 Account Receivable", company_id: 1},
- {id: 288, code: 101300, display_name: "101300 Tax Paid", company_id: 1},
- {id: 308, code: 101401, display_name: "101401 Bank", company_id: 1},
+ {id: 282, code: 100000, name: "100000 Fixed Asset Account", company_id: 1},
+ {id: 283, code: 101000, name: "101000 Current Assets", company_id: 1},
+ {id: 284, code: 101110, name: "101110 Stock Valuation Account", company_id: 1},
+ {id: 285, code: 101120, name: "101120 Stock Interim Account (Received)", company_id: 1},
+ {id: 286, code: 101130, name: "101130 Stock Interim Account (Delivered)", company_id: 1},
+ {id: 287, code: 101200, name: "101200 Account Receivable", company_id: 1},
+ {id: 288, code: 101300, name: "101300 Tax Paid", company_id: 1},
+ {id: 308, code: 101401, name: "101401 Bank", company_id: 1},
+ {id: 500, code: 500, name: "500 Account", company_id: 1},
+ {id: 501, code: 501, name: "501 Account", company_id: 1},
+ {id: 502, code: 502, name: "502 Account", company_id: 1},
+ {id: 503, code: 503, name: "503 Account", company_id: 1},
+ {id: 504, code: 504, name: "504 Account", company_id: 1},
],
mark_as_reconciled: function () {
return $.when();
@@ -1050,13 +1055,77 @@ QUnit.module('account', {
clientAction.destroy();
});
+ QUnit.test('Reconciliation create line (many2one test)', function (assert) {
+ assert.expect(5);
+
+ var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options);
+ var def = $.Deferred();
+
+ testUtils.addMockEnvironment(clientAction, {
+ data: this.params.data,
+ session: {
+ currencies: {
+ 3: {
+ digits: [69, 2],
+ position: "before",
+ symbol: "$"
+ }
+ }
+ },
+ archs: {
+ "account.account,false,list": '<tree string="Account"><field name="code"/><field name="name"/></tree>',
+ "account.account,false,search": '<search string="Account"><field name="code"/></search>',
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'name_get') {
+ return def.then(this._super.bind(this, route, args));
+ }
+ return this._super(route, args);
+ },
+ });
+
+ clientAction.prependTo($('#qunit-fixture'));
+
+ var widget = clientAction.widgets[0];
+
+ // open the first line in write-off mode
+ widget.$('.accounting_view tfoot td:first').trigger('click');
+
+ // select an account with the many2one (drop down)
+ widget.$('.create .create_account_id input').trigger('click');
+ $('.ui-autocomplete .ui-menu-item a:contains(101200)').trigger('mouseenter').trigger('click');
+ assert.strictEqual(widget.$('.create .create_account_id input').val(), "101200 Account Receivable", "Display the selected account");
+ assert.strictEqual(widget.$('tbody:first .cell_account_code').text(), "101200", "Display the code of the selected account");
+
+ // use the many2one select dialog to change the account
+ widget.$('.create .create_account_id input').trigger('click');
+ $('.ui-autocomplete .ui-menu-item a:contains(Search)').trigger('mouseenter').trigger('click');
+ // select the account who does not appear in the drop drown
+ $('.modal tr.o_data_row:contains(502)').click();
+ assert.strictEqual(widget.$('.create .create_account_id input').val(), "101200 Account Receivable", "Selected account does not change");
+ // wait the name_get to render the changes
+ def.resolve();
+ assert.strictEqual(widget.$('.create .create_account_id input').val(), "502 Account", "Display the selected account");
+ assert.strictEqual(widget.$('tbody:first .cell_account_code').text(), "502", "Display the code of the selected account");
+ clientAction.destroy();
+ });
+
QUnit.test('Reconciliation create line with taxes', function (assert) {
assert.expect(13);
var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options);
testUtils.addMockEnvironment(clientAction, {
- 'data': this.params.data,
+ data: this.params.data,
+ session: {
+ currencies: {
+ 3: {
+ digits: [69, 2],
+ position: "before",
+ symbol: "$"
+ }
+ }
+ },
});
clientAction.appendTo($('#qunit-fixture'));
@@ -1068,26 +1137,26 @@ QUnit.module('account', {
widget.$('.create .create_label input').val('test1').trigger('input');
widget.$('.create .create_amount input').val('1100').trigger('input');
- assert.strictEqual(widget.$('.accounting_view tbody .cell_right:last').text(), "1100.00", "should display the value 1100.00 in left column");
+ assert.strictEqual(widget.$('.accounting_view tbody .cell_right:last').text(), "$\u00a01100.00", "should display the value 1100.00 in left column");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open Balance'");
- assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "75.00", "should display 'Open Balance' with 75.00 in right column");
+ assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "$\u00a075.00", "should display 'Open Balance' with 75.00 in right column");
assert.strictEqual(widget.$('.accounting_view tbody tr').length, 1, "should have 1 created reconcile lines");
widget.$('.create .create_tax_id input').trigger('click');
$('.ui-autocomplete .ui-menu-item a:contains(10.00%)').trigger('mouseenter').trigger('click');
- assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().replace('$_', ''), "1000.00100.00", "should have 2 created reconcile lines with right column values");
+ assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().replace('$_', ''), "$\u00a01000.00$\u00a0100.00", "should have 2 created reconcile lines with right column values");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open Balance'");
- assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "75.00", "should display 'Open Balance' with 75.00 in right column");
+ assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "$\u00a075.00", "should display 'Open Balance' with 75.00 in right column");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "", "should display 'Open Balance' without any value in left column");
assert.strictEqual(widget.$('.accounting_view tbody tr').length, 2, "should have 2 created reconcile lines");
widget.$('.create .create_tax_id input').trigger('click');
$('.ui-autocomplete .ui-menu-item a:contains(20.00%)').trigger('mouseenter').trigger('click');
- assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().replace('$_', ''), "1100.00220.00", "should have 2 created reconcile lines with right column values");
+ assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().replace('$_', ''), "$\u00a01100.00$\u00a0220.00", "should have 2 created reconcile lines with right column values");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Create Write-off", "should display 'Create Write-off'");
- assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "145.00", "should display 'Create Write-off' with 145.00 in right column");
+ assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "$\u00a0145.00", "should display 'Create Write-off' with 145.00 in right column");
assert.strictEqual(widget.$('.accounting_view tbody tr').length, 2, "should have 2 created reconcile lines");
clientAction.destroy();
@@ -1099,7 +1168,7 @@ QUnit.module('account', {
var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options);
testUtils.addMockEnvironment(clientAction, {
- 'data': this.params.data,
+ data: this.params.data,
});
clientAction.appendTo($('#qunit-fixture'));
diff --git a/addons/account/tests/__init__.py b/addons/account/tests/__init__.py
index 813896bdec0..d50e277028c 100644
--- a/addons/account/tests/__init__.py
+++ b/addons/account/tests/__init__.py
@@ -6,7 +6,6 @@ from . import test_account_move_closed_period
from . import test_account_supplier_invoice
from . import test_account_validate_account_move
from . import test_account_invoice_rounding
-from . import test_account_supplier_invoice_recurrent
from . import test_bank_statement_reconciliation
from . import test_fiscal_position
from . import test_reconciliation_widget
diff --git a/addons/account/tests/test_account_supplier_invoice_recurrent.py b/addons/account/tests/test_account_supplier_invoice_recurrent.py
deleted file mode 100644
index 47f681dd3cb..00000000000
--- a/addons/account/tests/test_account_supplier_invoice_recurrent.py
+++ /dev/null
@@ -1,104 +0,0 @@
-# -*- coding: utf-8 -*-
-from datetime import datetime
-from dateutil.relativedelta import relativedelta
-
-from odoo.addons.account.tests.account_test_classes import AccountingTestCase
-from odoo.tests import tagged
-
-
-@tagged('post_install', '-at_install')
-class TestAccountRecurrent(AccountingTestCase):
-
- def apply_vendor_cron(self):
- self.env.ref('account.recurrent_vendor_bills_cron').method_direct_trigger()
-
- def test_create_recurring_vendor_bills(self):
- AccountInvoice = self.env['account.invoice']
- invoice_account_id = self.env['account.account'].search([('user_type_id', '=', self.ref('account.data_account_type_receivable'))], limit=1).id
- invoice_line_account_id = self.env['account.account'].search([('user_type_id', '=', self.ref('account.data_account_type_expenses'))], limit=1).id
- purchase_journal = self.env['account.journal'].search([('type', '=', 'purchase')], limit=1)
-
- invoice_line_data = [
- (0, 0,
- {
- 'product_id': self.ref('product.product_product_4'),
- 'name': 'product 4 that cost 100',
- 'quantity': 1.0,
- 'price_unit': 100.0,
- 'account_id': invoice_line_account_id,
- }
- ),
- (0, 0,
- {
- 'product_id': self.ref('product.product_product_5'),
- 'name': 'product 5 that cost 200',
- 'quantity': 2.0,
- 'price_unit': 200.0,
- 'account_id': invoice_line_account_id,
- }
- )
- ]
-
- # Create an invoice dated 2 months before today
- invoice = AccountInvoice.create({
- 'partner_id': self.ref('base.res_partner_2'),
- 'account_id': invoice_account_id,
- 'name': 'invoice test recurrent',
- 'type': 'in_invoice',
- 'reference_type': 'none',
- 'date_invoice': datetime.today() + relativedelta(months=-2),
- 'journal_id': purchase_journal.id,
- 'is_recurrency_enabled': True,
- 'recurrency_interval': 1,
- 'recurrency_type': 'months',
- 'invoice_line_ids': invoice_line_data
- })
- recurring_domain = [('type', '=', 'in_invoice'), ('state', '=', 'draft'), ('is_recurring_document', '=', True)]
-
- # After executing cron, verify that no bill is auto generated if the recurrent bill is in `draft` state
- self.apply_vendor_cron()
- recurring_invoice_count = AccountInvoice.search_count(recurring_domain)
- self.assertEquals(recurring_invoice_count, 0, 'Recurring bills should not be generated when reference bill is still `draft`.')
-
- # After validating bill, run cron and check that 2 recurring bill have been generated
- invoice.action_invoice_open() # Validate invoice
- self.apply_vendor_cron()
- recurring_invoice_count = previous_recurring_invoice_count = AccountInvoice.search_count(recurring_domain)
- self.assertEquals(recurring_invoice_count, 2, '2 recurring bills should be generated.')
-
- # verify that bills aren't generated if reference bill is in `cancel` state
- purchase_journal.write({'update_posted': True}) # Allow to cancel the invoice
- inv2_data = {
- 'name': 'invoice test recurrent 2',
- 'date_invoice': datetime.today() + relativedelta(months=-2),
- 'is_recurrency_enabled': True,
- 'recurrency_interval': 1,
- 'recurrency_type': 'months',
- }
- invoice2 = invoice.copy(default=inv2_data)
- invoice2.action_invoice_cancel() # Cancel the invoice
- self.apply_vendor_cron()
- recurring_invoice_count = AccountInvoice.search_count(recurring_domain)
- self.assertEquals(previous_recurring_invoice_count, recurring_invoice_count, 'Recurring bills should not be generated when reference bill is in `cancel` state.')
-
- # verify that vendor bills are generated even when invoice is in `paid` state
- inv3_data = {
- 'name': 'invoice test recurrent 3',
- 'date_invoice': datetime.today() + relativedelta(months=-2),
- 'is_recurrency_enabled': True,
- 'recurrency_interval': 1,
- 'recurrency_type': 'months',
- }
-
- invoice3 = invoice.copy(default=inv3_data)
- invoice3.action_invoice_open() # Validate invoice
- Payment = self.env['account.payment']
- payment_vals = Payment.with_context(default_invoice_ids=[(4, invoice3.id, None)]).default_get(Payment._fields.keys())
- payment_vals.update({
- 'payment_method_id': self.ref("account.account_payment_method_manual_out"),
- 'journal_id': self.env['account.journal'].search([('type', '=', 'bank')], limit=1).id
- })
- Payment.create(payment_vals).action_validate_invoice_payment() # Register the payment
- self.apply_vendor_cron()
- recurring_invoice_count = AccountInvoice.search_count(recurring_domain)
- self.assertEquals(recurring_invoice_count, 4, '4 recurring bills should be generated.')
diff --git a/addons/account/tests/test_payment.py b/addons/account/tests/test_payment.py
index 3c22a127180..3d490eedc49 100644
--- a/addons/account/tests/test_payment.py
+++ b/addons/account/tests/test_payment.py
@@ -20,7 +20,9 @@ class TestPayment(AccountingTestCase):
self.currency_chf_id = self.env.ref("base.CHF").id
self.currency_usd_id = self.env.ref("base.USD").id
self.currency_eur_id = self.env.ref("base.EUR").id
- self.env.ref('base.main_company').write({'currency_id': self.currency_eur_id})
+
+ company = self.env.ref('base.main_company')
+ self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", [self.currency_eur_id, company.id])
self.product = self.env.ref("product.product_product_4")
self.payment_method_manual_in = self.env.ref("account.account_payment_method_manual_in")
self.payment_method_manual_out = self.env.ref("account.account_payment_method_manual_out")
@@ -361,3 +363,53 @@ class TestPayment(AccountingTestCase):
{'account_id': self.account_eur.id, 'debit': 300.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': False},
{'account_id': inv_3.account_id.id, 'debit': 0.0, 'credit': 300.0, 'amount_currency': 0.0, 'currency_id': False},
])
+
+ def test_payment_and_writeoff_in_other_currency(self):
+ # Use case:
+ # Company is in EUR, create a customer invoice for 25 EUR and register payment of 25 USD.
+ # Mark invoice as fully paid with a write_off
+ # Check that all the aml are correctly created.
+ invoice = self.create_invoice(amount=25, type='out_invoice', currency_id=self.currency_eur_id, partner=self.partner_agrolait.id)
+ # register payment on invoice
+ payment = self.payment_model.create({'payment_type': 'inbound',
+ 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
+ 'partner_type': 'customer',
+ 'partner_id': self.partner_agrolait.id,
+ 'amount': 25,
+ 'currency_id': self.currency_usd_id,
+ 'payment_date': time.strftime('%Y') + '-07-15',
+ 'payment_difference_handling': 'reconcile',
+ 'writeoff_account_id': self.account_payable.id,
+ 'journal_id': self.bank_journal_euro.id,
+ 'invoice_ids': [(4, invoice.id, None)]
+ })
+ payment.post()
+ self.check_journal_items(payment.move_line_ids, [
+ {'account_id': self.account_eur.id, 'debit': 16.35, 'credit': 0.0, 'amount_currency': 25.0, 'currency_id': self.currency_usd_id},
+ {'account_id': self.account_payable.id, 'debit': 8.65, 'credit': 0.0, 'amount_currency': 13.22, 'currency_id': self.currency_usd_id},
+ {'account_id': self.account_receivable.id, 'debit': 0.0, 'credit': 25.0, 'amount_currency': -38.22, 'currency_id': self.currency_usd_id},
+ ])
+ # Use case:
+ # Company is in EUR, create a vendor bill for 25 EUR and register payment of 25 USD.
+ # Mark invoice as fully paid with a write_off
+ # Check that all the aml are correctly created.
+ invoice = self.create_invoice(amount=25, type='in_invoice', currency_id=self.currency_eur_id, partner=self.partner_agrolait.id)
+ # register payment on invoice
+ payment = self.payment_model.create({'payment_type': 'inbound',
+ 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
+ 'partner_type': 'supplier',
+ 'partner_id': self.partner_agrolait.id,
+ 'amount': 25,
+ 'currency_id': self.currency_usd_id,
+ 'payment_date': time.strftime('%Y') + '-07-15',
+ 'payment_difference_handling': 'reconcile',
+ 'writeoff_account_id': self.account_payable.id,
+ 'journal_id': self.bank_journal_euro.id,
+ 'invoice_ids': [(4, invoice.id, None)]
+ })
+ payment.post()
+ self.check_journal_items(payment.move_line_ids, [
+ {'account_id': self.account_eur.id, 'debit': 16.35, 'credit': 0.0, 'amount_currency': 25.0, 'currency_id': self.currency_usd_id},
+ {'account_id': self.account_payable.id, 'debit': 0.0, 'credit': 8.65, 'amount_currency': -13.22, 'currency_id': self.currency_usd_id},
+ {'account_id': self.account_receivable.id, 'debit': 0.0, 'credit': 7.7, 'amount_currency': -11.78, 'currency_id': self.currency_usd_id},
+ ])
diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py
index aa4fc950279..1180df172c9 100644
--- a/addons/account/tests/test_reconciliation.py
+++ b/addons/account/tests/test_reconciliation.py
@@ -27,7 +27,8 @@ class TestReconciliation(AccountingTestCase):
self.currency_swiss_id = self.env.ref("base.CHF").id
self.currency_usd_id = self.env.ref("base.USD").id
self.currency_euro_id = self.env.ref("base.EUR").id
- self.env.ref('base.main_company').write({'currency_id': self.currency_euro_id})
+ company = self.env.ref('base.main_company')
+ self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", [self.currency_euro_id, company.id])
self.account_rcv = partner_agrolait.property_account_receivable_id or self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_receivable').id)], limit=1)
self.account_rsa = partner_agrolait.property_account_payable_id or self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_payable').id)], limit=1)
self.product = self.env.ref("product.product_product_4")
diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml
index b75bd604ee2..30b9382e607 100644
--- a/addons/account/views/account_invoice_view.xml
+++ b/addons/account/views/account_invoice_view.xml
@@ -339,12 +339,6 @@
<field name="move_id" groups="account.group_account_user" attrs="{'invisible': [('move_id', '=', False)]}"/>
<field name="date" domain="[('state', '=', 'draft'), ('company_id', '=', company_id)]" groups="account.group_account_user"/>
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
- <field name="is_recurrency_enabled" string="Recurring Bills" help="Tick this box to generate recurring bills. The recurring bills will only be generated if this bill is validated." groups="account.group_allow_recurring" attrs="{'invisible': [('type', '!=', 'in_invoice')]}"/>
- <label for="recurrency_interval" attrs="{'invisible': [('is_recurrency_enabled', '=', False)]}" groups="account.group_allow_recurring"/>
- <div class="o_row" attrs="{'invisible': [('is_recurrency_enabled', '=', False)]}" groups="account.group_allow_recurring">
- <field name="recurrency_interval" help="Define the recurrency of the vendor bill." attrs="{'required': [('is_recurrency_enabled', '=', True)]}"/>
- <field name="recurrency_type" attrs="{'required': [('is_recurrency_enabled', '=', True)]}"/>
- </div>
</group>
</group>
</page>
@@ -541,10 +535,8 @@
<field name="number" string="Invoice" filter_domain="['|','|','|', ('number','ilike',self), ('origin','ilike',self), ('reference', 'ilike', self), ('partner_id', 'child_of', self)]"/>
<field name="journal_id" />
<filter name="draft" string="Draft" domain="[('state','=','draft')]"/>
- <filter name="invoices" string="Invoices" domain="['&amp;', ('state','in',['draft','open','paid']),('type','in',('out_invoice','in_invoice'))]"/>
- <filter name="refunds" string="Credit Notes" domain="['&amp;', ('state','in',['draft','open','paid']),('type','in',('out_refund','in_refund'))]"/>
- <separator/>
- <filter name="unpaid" string="Not Paid" domain="[('state','=','open')]"/>
+ <filter name="unpaid" string="Open" domain="[('state', '=', 'open')]"/>
+ <filter name="paid" string="Paid" domain="[('state', '=', 'paid')]"/>
<filter name="late" string="Overdue" domain="['&amp;', ('date_due', '&lt;', time.strftime('%%Y-%%m-%%d')), ('state', '=', 'open')]" help="Overdue invoices, maturity date passed"/>
<separator/>
<field name="partner_id" operator="child_of"/>
@@ -552,8 +544,6 @@
<field name="date" string="Period"/>
<separator/>
<filter domain="[('user_id','=',uid)]" name="myinvoices" help="My Invoices"/>
- <filter domain="[('is_recurrency_enabled','=',True)]" name="is_recurrency_enabled" help="Bills Generating Recurring Bills" groups="account.group_allow_recurring" invisible="context.get('type') != 'in_invoice'"/>
- <filter domain="[('is_recurring_document','=',True)]" name="is_recurring_document" help="Bills Generated Automatically" groups="account.group_allow_recurring" invisible="context.get('type') != 'in_invoice'"/>
<separator/>
<filter string="My Activities" name="activities_my"
domain="[('activity_ids.user_id', '=', uid)]"/>
@@ -578,36 +568,6 @@
</field>
</record>
- <record id="view_account_invoice_filter_inherit_invoices" model="ir.ui.view">
- <field name="name">account.invoice.select.invoices</field>
- <field name="model">account.invoice</field>
- <field name="mode">primary</field>
- <field name="inherit_id" ref="view_account_invoice_filter"/>
- <field name="arch" type="xml">
- <data>
- <xpath expr="filter[@name='invoices']" position="attributes">
- <attribute name="string">Not Draft</attribute>
- </xpath>
- <filter name="refunds" position="replace"/>
- </data>
- </field>
- </record>
-
- <record id="view_account_invoice_filter_inherit_credit_notes" model="ir.ui.view">
- <field name="name">account.invoice.select.credit.notes</field>
- <field name="model">account.invoice</field>
- <field name="mode">primary</field>
- <field name="inherit_id" ref="view_account_invoice_filter"/>
- <field name="arch" type="xml">
- <data>
- <xpath expr="filter[@name='refunds']" position="attributes">
- <attribute name="string">Not Draft</attribute>
- </xpath>
- <filter name="invoices" position="replace"/>
- </data>
- </field>
- </record>
-
<record id="action_invoice_tree" model="ir.actions.act_window">
<field name="name">Invoices</field>
<field name="res_model">account.invoice</field>
@@ -675,7 +635,7 @@
<field eval="False" name="view_id"/>
<field name="domain">[('type','=','out_invoice')]</field>
<field name="context">{'type':'out_invoice', 'journal_type': 'sale'}</field>
- <field name="search_view_id" ref="view_account_invoice_filter_inherit_invoices"/>
+ <field name="search_view_id" ref="view_account_invoice_filter"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a customer invoice
@@ -710,7 +670,7 @@
<field eval="False" name="view_id"/>
<field name="domain">[('type','=','out_refund')]</field>
<field name="context">{'default_type': 'out_refund', 'type': 'out_refund', 'journal_type': 'sale'}</field>
- <field name="search_view_id" ref="view_account_invoice_filter_inherit_credit_notes"/>
+ <field name="search_view_id" ref="view_account_invoice_filter"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a credit note
@@ -748,7 +708,7 @@
<field eval="False" name="view_id"/>
<field name="domain">[('type','=','in_invoice')]</field>
<field name="context">{'default_type': 'in_invoice', 'type': 'in_invoice', 'journal_type': 'purchase'}</field>
- <field name="search_view_id" ref="view_account_invoice_filter_inherit_invoices"/>
+ <field name="search_view_id" ref="view_account_invoice_filter"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Record a new vendor bill
@@ -784,7 +744,7 @@
<field eval="False" name="view_id"/>
<field name="domain">[('type','=','in_refund')]</field>
<field name="context">{'default_type': 'in_refund', 'type': 'in_refund', 'journal_type': 'purchase'}</field>
- <field name="search_view_id" ref="view_account_invoice_filter_inherit_credit_notes"/>
+ <field name="search_view_id" ref="view_account_invoice_filter"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a vendor credit note
diff --git a/addons/account/views/account_journal_dashboard_view.xml b/addons/account/views/account_journal_dashboard_view.xml
index dfa08fbadce..9c7f1aca551 100644
--- a/addons/account/views/account_journal_dashboard_view.xml
+++ b/addons/account/views/account_journal_dashboard_view.xml
@@ -117,16 +117,12 @@
<span>View</span>
</div>
<div>
- <a type="object" name="open_action" context="{'use_domain': [('type','in',('out_invoice','in_invoice'))]}">
- <span t-if="journal_type == 'sale'">Invoices</span>
- <span t-if="journal_type == 'purchase'">Bills</span>
- </a>
+ <a t-if="journal_type == 'sale'" type="object" name="open_action" context="{'use_domain': [('type', '=', 'out_invoice')]}">Invoices</a>
+ <a t-if="journal_type == 'purchase'" type="object" name="open_action" context="{'use_domain': [('type', '=', 'in_invoice')]}">Bills</a>
</div>
<div>
- <a type="object" name="open_action" context="{'use_domain': [('type','in',('out_refund','in_refund'))], 'invoice_type': 'refund'}">
- <span t-if="journal_type == 'sale'">Credit Notes</span>
- <span t-if="journal_type == 'purchase'">Credit Notes</span>
- </a>
+ <a t-if="journal_type == 'sale'" type="object" name="open_action" context="{'use_domain': [('type', '=', 'out_refund')], 'invoice_type': 'refund'}">Credit Notes</a>
+ <a t-if="journal_type == 'purchase'" type="object" name="open_action" context="{'use_domain': [('type', '=', 'in_refund')], 'invoice_type': 'refund'}">Credit Notes</a>
</div>
<div>
<a type="object" name="action_open_reconcile">Payments Matching</a>
diff --git a/addons/account/views/account_report.xml b/addons/account/views/account_report.xml
index 06e077e7d8c..2ba116e884a 100644
--- a/addons/account/views/account_report.xml
+++ b/addons/account/views/account_report.xml
@@ -56,6 +56,7 @@
id="action_report_financial"
model="account.financial.report"
string="Financial report"
+ menu="False"
report_type="qweb-pdf"
name="account.report_financial"
file="account.report_financial"
diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml
index 7b57066bce8..6d8ffdd1ac0 100644
--- a/addons/account/views/account_view.xml
+++ b/addons/account/views/account_view.xml
@@ -16,7 +16,7 @@
<field name="code" placeholder="code"/>
<field name="name"/>
<field name="user_type_id" widget="selection"/>
- <field name="tax_ids" widget="many2many_tags"/>
+ <field name="tax_ids" widget="many2many_tags" domain="[('company_id','=',company_id)]"/>
<field name="tag_ids" widget="many2many_tags" domain="[('applicability', '!=', 'taxes')]" context="{'default_applicability': 'accounts'}" options="{'no_create_edit': True}"/>
<field name="group_id"/>
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
diff --git a/addons/account/views/res_config_settings_views.xml b/addons/account/views/res_config_settings_views.xml
index 5ba661a110f..179699fa726 100644
--- a/addons/account/views/res_config_settings_views.xml
+++ b/addons/account/views/res_config_settings_views.xml
@@ -446,20 +446,9 @@
</div>
</div>
</div>
- <h2>Automated Entries</h2>
- <div class="row mt16 o_settings_container" id="recommended_apps">
- <div class="col-xs-12 col-md-6 o_setting_box">
- <div class="o_setting_left_pane">
- <field name="group_allow_recurring"/>
- </div>
- <div class="o_setting_right_pane">
- <label for="group_allow_recurring"/>
- <div class="text-muted">
- Create recurring vendor bills
- </div>
- </div>
- </div>
- <div groups="account.group_account_user" class="col-xs-12 col-md-6 o_setting_box" id="asset_types" title="This allows you to manage the assets owned by a company or a person. It keeps track of the depreciation occurred on those assets, and creates account move for those depreciation lines.">
+ <h2 groups="account.group_account_user">Automated Entries</h2>
+ <div class="row mt16 o_settings_container" id="recommended_apps" groups="account.group_account_user">
+ <div class="col-xs-12 col-md-6 o_setting_box" id="asset_types" title="This allows you to manage the assets owned by a company or a person. It keeps track of the depreciation occurred on those assets, and creates account move for those depreciation lines.">
<div class="o_setting_left_pane">
<field name="module_account_asset"/>
</div>
@@ -475,7 +464,7 @@
</div>
</div>
</div>
- <div groups="account.group_account_user" class="col-xs-12 col-md-6 o_setting_box" title="This allows you to manage the revenue recognition on selling products. It keeps track of the installments occurred on those revenue recognitions, and creates account moves for those installment lines.">
+ <div class="col-xs-12 col-md-6 o_setting_box" title="This allows you to manage the revenue recognition on selling products. It keeps track of the installments occurred on those revenue recognitions, and creates account moves for those installment lines.">
<div class="o_setting_left_pane">
<field name="module_account_deferred_revenue" widget="upgrade_boolean"/>
</div>
diff --git a/addons/account_analytic_default/models/account_analytic_default.py b/addons/account_analytic_default/models/account_analytic_default.py
index ac9c6e57be3..77f9b5fcad8 100644
--- a/addons/account_analytic_default/models/account_analytic_default.py
+++ b/addons/account_analytic_default/models/account_analytic_default.py
@@ -66,7 +66,7 @@ class AccountInvoiceLine(models.Model):
@api.onchange('product_id')
def _onchange_product_id(self):
res = super(AccountInvoiceLine, self)._onchange_product_id()
- rec = self.env['account.analytic.default'].account_get(self.product_id.id, self.invoice_id.partner_id.id, self.env.uid,
+ rec = self.env['account.analytic.default'].account_get(self.product_id.id, self.invoice_id.commercial_partner_id.id, self.env.uid,
fields.Date.today(), company_id=self.company_id.id)
self.account_analytic_id = rec.analytic_id.id
self.analytic_tag_ids = rec.analytic_tag_ids.ids
@@ -75,7 +75,7 @@ class AccountInvoiceLine(models.Model):
def _set_additional_fields(self, invoice):
if not self.account_analytic_id or not self.analytic_tag_ids:
rec = self.env['account.analytic.default'].account_get(
- self.product_id.id, self.invoice_id.partner_id.id, self.env.uid,
+ self.product_id.id, self.invoice_id.commercial_partner_id.id, self.env.uid,
fields.Date.today(), company_id=self.company_id.id)
if rec:
if self.account_analytic_id:
diff --git a/addons/account_analytic_default/models/sale_order_line.py b/addons/account_analytic_default/models/sale_order_line.py
index 9997f9ba1cd..51f8d308188 100644
--- a/addons/account_analytic_default/models/sale_order_line.py
+++ b/addons/account_analytic_default/models/sale_order_line.py
@@ -13,4 +13,5 @@ class SaleOrderLine(models.Model):
default_analytic_account = self.env['account.analytic.default'].account_get(self.product_id.id, self.order_id.partner_id.id, self.order_id.user_id.id, fields.Date.today())
if default_analytic_account:
res.update({'account_analytic_id': default_analytic_account.analytic_id.id})
+ res.update({'analytic_tag_ids' : [(6, 0, default_analytic_account.analytic_tag_ids.ids)]})
return res
diff --git a/addons/account_invoicing/__init__.py b/addons/account_invoicing/__init__.py
index 91206cf6094..371975a409a 100644
--- a/addons/account_invoicing/__init__.py
+++ b/addons/account_invoicing/__init__.py
@@ -11,3 +11,12 @@ def uninstall_hook(cr, registry):
('module', '=', 'account')
]).mapped('res_id')
env['ir.ui.menu'].browse(res_ids).update({'active': False})
+
+
+def post_init_hook(cr, registry):
+ env = Environment(cr, SUPERUSER_ID, {})
+ res_ids = env['ir.model.data'].search([
+ ('model', '=', 'ir.ui.menu'),
+ ('module', '=', 'account'),
+ ]).mapped('res_id')
+ env['ir.ui.menu'].browse(res_ids).update({'active': True})
diff --git a/addons/account_invoicing/__manifest__.py b/addons/account_invoicing/__manifest__.py
index faa54037c69..880c5b899c3 100644
--- a/addons/account_invoicing/__manifest__.py
+++ b/addons/account_invoicing/__manifest__.py
@@ -28,4 +28,5 @@ You could use this simplified accounting in case you work with an (external) acc
],
'application': True,
'uninstall_hook': 'uninstall_hook',
+ 'post_init_hook': 'post_init_hook',
}
diff --git a/addons/auth_signup/models/__init__.py b/addons/auth_signup/models/__init__.py
index 44dc1ee6b11..a25160266e7 100644
--- a/addons/auth_signup/models/__init__.py
+++ b/addons/auth_signup/models/__init__.py
@@ -4,3 +4,4 @@ from . import res_config_settings
from . import ir_http
from . import res_partner
from . import res_users
+from . import ir_model_fields
diff --git a/addons/auth_signup/models/ir_model_fields.py b/addons/auth_signup/models/ir_model_fields.py
new file mode 100644
index 00000000000..828b9de8346
--- /dev/null
+++ b/addons/auth_signup/models/ir_model_fields.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details
+
+from odoo import models
+
+
+# This is a nasty hack, targeted for V11 only
+class IrModelFields(models.Model):
+ _inherit = 'ir.model.fields'
+
+ def unlink(self):
+ # Prevent the deletion of some `shared` fields... -_-
+ self = self.filtered(
+ lambda rec: not (
+ rec.model == 'res.config.settings'
+ and rec.name == 'auth_signup_uninvited'
+ )
+ )
+ return super(IrModelFields, self).unlink()
diff --git a/addons/barcodes/static/src/js/barcode_form_view.js b/addons/barcodes/static/src/js/barcode_form_view.js
index fa5b27e2a9c..1730b42290e 100644
--- a/addons/barcodes/static/src/js/barcode_form_view.js
+++ b/addons/barcodes/static/src/js/barcode_form_view.js
@@ -155,7 +155,7 @@ FormController.include({
id: candidate.id,
data: candidateChanges,
};
- return this.model.notifyChanges(this.handle, changes);
+ return this.model.notifyChanges(this.handle, changes, {notifyChange: activeBarcode.notifyChange});
},
/**
* @private
@@ -211,6 +211,8 @@ FormController.include({
* @param {OdooEvent} event
* @param {string} event.data.name: the current field name
* @param {string} [event.data.fieldName] optional for x2many sub field
+ * @param {boolean} [event.data.notifyChange] optional for x2many sub field
+ * do not trigger on change server side if a candidate has been found
* @param {string} [event.data.quantity] optional field to increase quantity
* @param {Object} [event.data.commands] optional added methods
* can use comand with specific barcode (with ReservedBarcodePrefixes)
@@ -225,7 +227,9 @@ FormController.include({
handle: this.handle,
target: event.target,
widget: event.target.attrs && event.target.attrs.widget,
+ setQuantityWithKeypress: !! event.data.setQuantityWithKeypress,
fieldName: event.data.fieldName,
+ notifyChange: (event.data.notifyChange !== undefined) ? event.data.notifyChange : true,
quantity: event.data.quantity,
commands: event.data.commands || {},
candidate: this.activeBarcode[name] && this.activeBarcode[name].handle === this.handle ?
@@ -319,7 +323,7 @@ FormController.include({
// only catch the event if we're not focused in
// another field and it's a number
- if (!$(event.target).is('body') || !/[0-9]/.test(character)) {
+ if (!$(event.target).is('body, .modal') || !/[0-9]/.test(character)) {
return;
}
diff --git a/addons/base_gengo/wizard/base_gengo_translations.py b/addons/base_gengo/wizard/base_gengo_translations.py
index 43d2e81ca33..3b260dd5a61 100644
--- a/addons/base_gengo/wizard/base_gengo_translations.py
+++ b/addons/base_gengo/wizard/base_gengo_translations.py
@@ -63,7 +63,7 @@ class BaseGengoTranslations(models.TransientModel):
'view_type': 'form',
'view_mode': 'form',
'res_model': 'res.config.settings',
- 'target': 'current',
+ 'target': 'inline',
'context': {'module' : 'general_settings'},
}
diff --git a/addons/base_iban/models/res_partner_bank.py b/addons/base_iban/models/res_partner_bank.py
index 53c33f2b897..0b5d6c47f16 100644
--- a/addons/base_iban/models/res_partner_bank.py
+++ b/addons/base_iban/models/res_partner_bank.py
@@ -91,6 +91,7 @@ _map_iban_template = {
'bg': 'BGkk BBBB SSSS DDCC CCCC CC', # Bulgaria
'bh': 'BHkk BBBB CCCC CCCC CCCC CC', # Bahrain
'br': 'BRkk BBBB BBBB SSSS SCCC CCCC CCCT N', # Brazil
+ 'by': 'BYkk BBBB AAAA CCCC CCCC CCCC CCCC', # Belarus
'ch': 'CHkk BBBB BCCC CCCC CCCC C', # Switzerland
'cr': 'CRkk BBBC CCCC CCCC CCCC C', # Costa Rica
'cy': 'CYkk BBBS SSSS CCCC CCCC CCCC CCCC', # Cyprus
diff --git a/addons/base_import/models/base_import.py b/addons/base_import/models/base_import.py
index e6c53740f73..fd4dcddb4df 100644
--- a/addons/base_import/models/base_import.py
+++ b/addons/base_import/models/base_import.py
@@ -302,7 +302,8 @@ class Import(models.TransientModel):
try:
thousand_separator = decimal_separator = False
for val in preview_values:
- if val == '':
+ val = val.strip()
+ if not val:
continue
# value might have the currency symbol left or right from the value
val = self._remove_currency_symbol(val)
@@ -587,6 +588,7 @@ class Import(models.TransientModel):
thousand_separator = options.get('float_thousand_separator', ' ')
decimal_separator = options.get('float_decimal_separator', '.')
for line in data:
+ line[index] = line[index].strip()
if not line[index]:
continue
line[index] = line[index].replace(thousand_separator, '').replace(decimal_separator, '.')
diff --git a/addons/base_import_module/models/ir_module.py b/addons/base_import_module/models/ir_module.py
index aaa1e69946a..d9c3d4092a2 100644
--- a/addons/base_import_module/models/ir_module.py
+++ b/addons/base_import_module/models/ir_module.py
@@ -145,12 +145,13 @@ def _is_studio_custom(path):
Returns True if any of the records contains a context with the key
studio in it, False if none of the records do
"""
- path = os.path.join(path, 'data')
- filenames = next(iter(os.walk(path)))[2]
- filenames = [f for f in filenames if f.lower().endswith('.xml')]
+ filepaths = []
+ for level in os.walk(path):
+ filepaths += [os.path.join(level[0], fn) for fn in level[2]]
+ filepaths = [fp for fp in filepaths if fp.lower().endswith('.xml')]
- for filename in filenames:
- root = lxml.etree.parse(os.path.join(path, filename)).getroot()
+ for fp in filepaths:
+ root = lxml.etree.parse(fp).getroot()
for record in root:
# there might not be a context if it's a non-studio module
diff --git a/addons/calendar_sms/__manifest__.py b/addons/calendar_sms/__manifest__.py
index 0e734074aaa..70156156a8a 100644
--- a/addons/calendar_sms/__manifest__.py
+++ b/addons/calendar_sms/__manifest__.py
@@ -2,7 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
- 'name': "Calendar SMS",
+ 'name': "Calendar - SMS",
'summary': 'Send text messages as event reminders',
'description': "Send text messages as event reminders",
'category': 'Hidden',
diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py
index 9a96a892b29..9fdb29a19f6 100644
--- a/addons/crm/models/crm_lead.py
+++ b/addons/crm/models/crm_lead.py
@@ -973,6 +973,8 @@ class Lead(models.Model):
'nb_opportunities': 0,
}
+ today = fields.Date.from_string(fields.Date.context_today(self))
+
opportunities = self.search([('type', '=', 'opportunity'), ('user_id', '=', self._uid)])
for opp in opportunities:
@@ -980,28 +982,28 @@ class Lead(models.Model):
if opp.activity_date_deadline:
if opp.date_deadline:
date_deadline = fields.Date.from_string(opp.date_deadline)
- if date_deadline == date.today():
+ if date_deadline == today:
result['closing']['today'] += 1
- if date.today() <= date_deadline <= date.today() + timedelta(days=7):
+ if today <= date_deadline <= today + timedelta(days=7):
result['closing']['next_7_days'] += 1
- if date_deadline < date.today() and not opp.date_closed:
+ if date_deadline < today and not opp.date_closed:
result['closing']['overdue'] += 1
# Next activities
for activity in opp.activity_ids:
date_deadline = fields.Date.from_string(activity.date_deadline)
- if date_deadline == date.today():
+ if date_deadline == today:
result['activity']['today'] += 1
- if date.today() <= date_deadline <= date.today() + timedelta(days=7):
+ if today <= date_deadline <= today + timedelta(days=7):
result['activity']['next_7_days'] += 1
- if date_deadline < date.today():
+ if date_deadline < today:
result['activity']['overdue'] += 1
# Won in Opportunities
if opp.date_closed and opp.stage_id.probability == 100:
date_closed = fields.Date.from_string(opp.date_closed)
- if date.today().replace(day=1) <= date_closed <= date.today():
+ if today.replace(day=1) <= date_closed <= today:
if opp.planned_revenue:
result['won']['this_month'] += opp.planned_revenue
- elif date.today() + relativedelta(months=-1, day=1) <= date_closed < date.today().replace(day=1):
+ elif today + relativedelta(months=-1, day=1) <= date_closed < today.replace(day=1):
if opp.planned_revenue:
result['won']['last_month'] += opp.planned_revenue
@@ -1026,9 +1028,9 @@ class Lead(models.Model):
for activity in activites_done:
if activity['date']:
date_act = fields.Date.from_string(activity['date'])
- if date.today().replace(day=1) <= date_act <= date.today():
+ if today.replace(day=1) <= date_act <= today:
result['done']['this_month'] += 1
- elif date.today() + relativedelta(months=-1, day=1) <= date_act < date.today().replace(day=1):
+ elif today + relativedelta(months=-1, day=1) <= date_act < today.replace(day=1):
result['done']['last_month'] += 1
# Meetings
@@ -1043,9 +1045,9 @@ class Lead(models.Model):
for meeting in meetings:
if meeting['start']:
start = datetime.strptime(meeting['start'], tools.DEFAULT_SERVER_DATETIME_FORMAT).date()
- if start == date.today():
+ if start == today:
result['meeting']['today'] += 1
- if date.today() <= start <= date.today() + timedelta(days=7):
+ if today <= start <= today + timedelta(days=7):
result['meeting']['next_7_days'] += 1
result['done']['target'] = self.env.user.target_sales_done
diff --git a/addons/crm/models/res_config_settings.py b/addons/crm/models/res_config_settings.py
index a944c9d6b71..c2ca5a60735 100644
--- a/addons/crm/models/res_config_settings.py
+++ b/addons/crm/models/res_config_settings.py
@@ -10,7 +10,7 @@ class ResConfigSettings(models.TransientModel):
crm_alias_prefix = fields.Char('Default Alias Name for Leads')
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 Validation")
+ module_crm_phone_validation = fields.Boolean("Phone Formatting")
module_web_clearbit = fields.Boolean("Customer Autocomplete")
def _find_default_lead_alias_id(self):
@@ -33,7 +33,7 @@ class ResConfigSettings(models.TransientModel):
@api.onchange('generate_lead_from_alias')
def _onchange_generate_lead_from_alias(self):
- self.crm_alias_prefix = 'info' if self.generate_lead_from_alias else False
+ self.crm_alias_prefix = (self.crm_alias_prefix or 'info') if self.generate_lead_from_alias else False
@api.model
def get_values(self):
diff --git a/addons/crm/views/res_config_settings_views.xml b/addons/crm/views/res_config_settings_views.xml
index 582063ae90a..4b421d928f1 100644
--- a/addons/crm/views/res_config_settings_views.xml
+++ b/addons/crm/views/res_config_settings_views.xml
@@ -62,7 +62,7 @@
<div class="o_setting_right_pane">
<label for="module_crm_phone_validation"/>
<div class="text-muted" id="module_crm_phone_validation">
- Formats phone numbers
+ Format phone numbers based on national conventions
</div>
</div>
</div>
diff --git a/addons/delivery/models/__init__.py b/addons/delivery/models/__init__.py
index 5229017a189..9e162e76592 100644
--- a/addons/delivery/models/__init__.py
+++ b/addons/delivery/models/__init__.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from . import res_config_settings
from . import delivery_carrier
from . import delivery_grid
from . import product_template
diff --git a/addons/delivery/models/delivery_carrier.py b/addons/delivery/models/delivery_carrier.py
index fdc568725d6..41ea4cc7682 100644
--- a/addons/delivery/models/delivery_carrier.py
+++ b/addons/delivery/models/delivery_carrier.py
@@ -210,8 +210,11 @@ class DeliveryCarrier(models.Model):
carrier.product_id.list_price = carrier.fixed_price
def fixed_rate_shipment(self, order):
+ price = self.fixed_price
+ if self.company_id.currency_id.id != order.currency_id.id:
+ price = self.env['res.currency']._compute(self.company_id.currency_id, order.currency_id, price)
return {'success': True,
- 'price': self.fixed_price,
+ 'price': price,
'error_message': False,
'warning_message': False}
diff --git a/addons/delivery/models/res_config_settings.py b/addons/delivery/models/res_config_settings.py
new file mode 100644
index 00000000000..070f8dcdb29
--- /dev/null
+++ b/addons/delivery/models/res_config_settings.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import models
+
+class ResConfigSettings(models.TransientModel):
+ _inherit = 'res.config.settings'
+
+ def set_values(self):
+ super(ResConfigSettings, self).set_values()
+ rule = self.env.ref('delivery.delivery_carrier_comp_rule', False)
+ if rule:
+ rule.write({'active': not bool(self.company_share_product)})
diff --git a/addons/delivery/security/delivery_carrier_security.xml b/addons/delivery/security/delivery_carrier_security.xml
index 8fad0891676..db718fbf361 100644
--- a/addons/delivery/security/delivery_carrier_security.xml
+++ b/addons/delivery/security/delivery_carrier_security.xml
@@ -6,6 +6,7 @@
<field name="model_id" ref="model_delivery_carrier"/>
<field name="global" eval="True"/>
<field name="domain_force"> ['|',('company_id','=',user.company_id.id),('company_id','=',False)]</field>
+ <field name="active" eval="False"/>
</record>
</data>
</odoo>
diff --git a/addons/event/data/email_template_data.xml b/addons/event/data/email_template_data.xml
index f2a742d9318..b7788cba9a3 100644
--- a/addons/event/data/email_template_data.xml
+++ b/addons/event/data/email_template_data.xml
@@ -86,18 +86,6 @@
<td style="padding:15px 20px 0px 20px;">
<table style="width:100%;border-top:1px solid #e1e1e1;">
<tr>
- <td style="padding:25px 0px; text-align:center;">
- <p>
- Don't forget to <strong>add it to your calendar</strong>:
- </p>
- <a href="https://www.google.com/calendar/render?action=TEMPLATE&text=${object.event_id.name}&dates=${date_begin}/${date_end}&location=${location}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16"> Google</a>
- <a href="https://bay02.calendar.live.com/calendar/calendar.aspx?rru=addevent&summary=${object.event_id.name}&dtstart=${date_begin}&dtend=${date_end}&location=${location}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16"> Outlook</a>
- <a href="https://calendar.yahoo.com/?v=60&view=d&type=20&title=${object.event_id.name}&in_loc=${location}&st=${format_tz(object.event_id.date_begin, tz='UTC', format='%Y%m%dT%H%M%S')}&et=${format_tz(object.event_id.date_end, tz='UTC', format='%Y%m%dT%H%M%S')}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16"> Yahoo</a>
- </td>
- </tr>
- </table>
- <table style="width:100%;border-top:1px solid #e1e1e1;">
- <tr>
<td style="padding:25px 0px;vertical-align:top;">
<img src="/web_editor/font_to_img/61555/rgb(81,81,102)/34" style="padding:4px;max-width:inherit;" height="34">
</td>
@@ -165,6 +153,16 @@
</tr>
</table>
% endif
+ <table style="width:100%;border-top:1px solid #e1e1e1;">
+ <tr>
+ <td style="padding:25px 0px;">
+ <strong>Add this event to your calendar</strong>
+ <a href="https://www.google.com/calendar/render?action=TEMPLATE&text=${object.event_id.name}&dates=${date_begin}/${date_end}&location=${location}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16"> Google</a>
+ <a href="https://bay02.calendar.live.com/calendar/calendar.aspx?rru=addevent&summary=${object.event_id.name}&dtstart=${date_begin}&dtend=${date_end}&location=${location}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16"> Outlook</a>
+ <a href="https://calendar.yahoo.com/?v=60&view=d&type=20&title=${object.event_id.name}&in_loc=${location}&st=${format_tz(object.event_id.date_begin, tz='UTC', format='%Y%m%dT%H%M%S')}&et=${format_tz(object.event_id.date_end, tz='UTC', format='%Y%m%dT%H%M%S')}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16"> Yahoo</a>
+ </td>
+ </tr>
+ </table>
</td>
</tr>
</tbody>
diff --git a/addons/fleet/models/fleet_vehicle.py b/addons/fleet/models/fleet_vehicle.py
index da5d9fb69b1..213c5716059 100644
--- a/addons/fleet/models/fleet_vehicle.py
+++ b/addons/fleet/models/fleet_vehicle.py
@@ -247,9 +247,11 @@ class FleetVehicle(models.Model):
@return: the costs log view
"""
self.ensure_one()
+ copy_context = dict(self.env.context)
+ copy_context.pop('group_by', None)
res = self.env['ir.actions.act_window'].for_xml_id('fleet', 'fleet_vehicle_costs_action')
res.update(
- context=dict(self.env.context, default_vehicle_id=self.id, search_default_parent_false=True),
+ context=dict(copy_context, default_vehicle_id=self.id, search_default_parent_false=True),
domain=[('vehicle_id', '=', self.id)]
)
return res
diff --git a/addons/google_calendar/README.md b/addons/google_calendar/README.md
index 47b00887fc2..2473da7b922 100644
--- a/addons/google_calendar/README.md
+++ b/addons/google_calendar/README.md
@@ -48,5 +48,5 @@ a notification in you Odoo which ever the page you are.
Google Calendar
---------------
-With the plugin Google\_calendar, you can synchronize your Odoo calendar with Google Calendar.
+With this plugin, you can synchronize your Odoo calendar with Google Calendar.
diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py
index 60b308dc36e..a97052e0445 100644
--- a/addons/hr_expense/models/hr_expense.py
+++ b/addons/hr_expense/models/hr_expense.py
@@ -416,7 +416,8 @@ class HrExpense(models.Model):
product = default_product
else:
expense_description = expense_description.replace(product_code.group(), '')
- product = self.env['product.product'].search([('default_code', 'ilike', product_code.group(1))]) or default_product
+ products = self.env['product.product'].search([('default_code', 'ilike', product_code.group(1))]) or default_product
+ product = products.filtered(lambda p: p.default_code == product_code.group(1)) or products[0]
pattern = '[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?'
# Match the last occurence of a float in the string
@@ -591,10 +592,13 @@ class HrExpenseSheet(models.Model):
MailFollowers = self.env['mail.followers']
for partner in users.mapped('partner_id'):
values['message_follower_ids'] += MailFollowers._add_follower_command(self._name, [], {partner.id: None}, {})[0]
-
- if values.get('user_id') and values.get('user_id') != employee.user_id.id:
+
+ if values.get('user_id') and values.get('user_id') != employee.user_id.id and values.get('user_id') != self.env.uid:
resp_partner = self.env['res.users'].browse(values['user_id'])
- values['message_follower_ids'] += MailFollowers._add_follower_command(self._name, [], {resp_partner.partner_id.id: None}, {})[0]
+ # Parsing ORM command to avoid adding several times the same partner
+ # TDE note: a better implementation should come in saas 11.3 or 11.4, allowing to normally remove that code
+ if resp_partner.partner_id.id not in (x[2].get('partner_id') for x in values['message_follower_ids'] if len(x) == 3):
+ values['message_follower_ids'] += MailFollowers._add_follower_command(self._name, [], {resp_partner.partner_id.id: None}, {})[0]
# --------------------------------------------
# Actions
diff --git a/addons/hr_expense/views/hr_expense_views.xml b/addons/hr_expense/views/hr_expense_views.xml
index 6d27b2b9cdb..3d5343ca5ec 100644
--- a/addons/hr_expense/views/hr_expense_views.xml
+++ b/addons/hr_expense/views/hr_expense_views.xml
@@ -8,7 +8,7 @@
<field name="arch" type="xml">
<xpath expr="//group[@name='active_group']" position="before">
<group string="Expenses" name="timesheet">
- <field name="expense_manager_id"/>
+ <field name="expense_manager_id"/>
</group>
</xpath>
</field>
@@ -35,14 +35,29 @@
</field>
</record>
- <record id="hr_expense_view_form_without_header" model="ir.ui.view">
+ <record id="hr_expense_view_form" model="ir.ui.view">
<field name="name">hr.expense.view.form</field>
<field name="model">hr.expense</field>
- <field eval="35" name="priority"/>
<field name="arch" type="xml">
<form string="Expenses" class="o_expense_form">
- <field name="state" invisible="True"/>
+ <header>
+ <widget name="attach_document" string="Attach Document" action="message_post" attrs="{'invisible': [('attachment_number', '&lt;=', 0)]}"/>
+ <widget name="attach_document" string="Attach Document" action="message_post" class="oe_highlight" attrs="{'invisible': [('attachment_number', '&gt;=', 1)]}"/>
+ <button name="action_submit_expenses" string="Submit to Manager" type="object" class="o_expense_submit" attrs="{'invisible': ['|', ('attachment_number', '&gt;=', 1), ('sheet_id', '!=', False)]}"/>
+ <button name="action_submit_expenses" string="Submit to Manager" type="object" class="oe_highlight o_expense_submit" attrs="{'invisible': ['|', ('attachment_number', '&lt;=', 0), ('sheet_id', '!=', False)]}"/>
+ <field name="state" widget="statusbar" statusbar_visible="draft,reported,approved,done,refused"/>
+ <button name="action_view_sheet" type="object" string="View Report" class="oe_highlight" attrs="{'invisible': [('state', '=','draft')]}"/>
+ </header>
<sheet>
+ <div class="oe_button_box">
+ <button name="action_get_attachment_view"
+ class="oe_stat_button"
+ icon="fa-book"
+ type="object"
+ attrs="{'invisible': [('attachment_number', '=', 0)]}">
+ <field name="attachment_number" widget="statinfo" string="Documents" options="{'reload_on_button': true}"/>
+ </button>
+ </div>
<div class="oe_title">
<label for="name"/>
<h1>
@@ -91,32 +106,18 @@
</field>
</record>
- <record id="hr_expense_view_form" model="ir.ui.view">
+ <record id="hr_expense_view_form_without_header" model="ir.ui.view">
<field name="name">hr.expense.view.form</field>
<field name="model">hr.expense</field>
- <field name="inherit_id" ref="hr_expense.hr_expense_view_form_without_header"/>
+ <field name="inherit_id" ref="hr_expense.hr_expense_view_form"/>
+ <field eval="35" name="priority"/>
<field name="mode">primary</field>
<field name="arch" type="xml">
- <xpath expr="/form/*" position="before">
- <header>
- <widget name="attach_document" string="Attach Document" action="message_post" attrs="{'invisible': [('attachment_number', '&lt;=', 0)]}"/>
- <widget name="attach_document" string="Attach Document" action="message_post" class="oe_highlight" attrs="{'invisible': [('attachment_number', '&gt;=', 1)]}"/>
- <button name="action_submit_expenses" string="Submit to Manager" type="object" class="o_expense_submit" attrs="{'invisible': ['|', ('attachment_number', '&gt;=', 1), ('sheet_id', '!=', False)]}"/>
- <button name="action_submit_expenses" string="Submit to Manager" type="object" class="oe_highlight o_expense_submit" attrs="{'invisible': ['|', ('attachment_number', '&lt;=', 0), ('sheet_id', '!=', False)]}"/>
- <field name="state" widget="statusbar" statusbar_visible="draft,reported,approved,done,refused"/>
- <button name="action_view_sheet" type="object" string="View Report" class="oe_highlight" attrs="{'invisible': [('state', '=','draft')]}"/>
- </header>
+ <xpath expr="/form/header" position="attributes">
+ <attribute name="invisible">1</attribute>
</xpath>
- <xpath expr="/form/sheet/*" position="before">
- <div class="oe_button_box">
- <button name="action_get_attachment_view"
- class="oe_stat_button"
- icon="fa-book"
- type="object"
- attrs="{'invisible': [('attachment_number', '=', 0)]}">
- <field name="attachment_number" widget="statinfo" string="Documents" options="{'reload_on_button': true}"/>
- </button>
- </div>
+ <xpath expr="/form/sheet/div[hasclass('oe_button_box')]" position="attributes">
+ <attribute name="invisible">1</attribute>
</xpath>
</field>
</record>
diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py
index c7f50a175c7..4a58d151f55 100644
--- a/addons/hr_holidays/models/hr_leave.py
+++ b/addons/hr_holidays/models/hr_leave.py
@@ -100,7 +100,7 @@ class HolidaysRequest(models.Model):
@api.onchange('employee_id')
def _onchange_employee_id(self):
self.manager_id = self.employee_id and self.employee_id.parent_id
- self.department_id = self.employee_id and self.employee_id.department_id
+ self.department_id = self.employee_id.department_id
@api.multi
@api.depends('number_of_days_temp')
diff --git a/addons/hr_payroll/models/hr_payslip.py b/addons/hr_payroll/models/hr_payslip.py
index 726e5efe375..c9d14c47495 100644
--- a/addons/hr_payroll/models/hr_payslip.py
+++ b/addons/hr_payroll/models/hr_payslip.py
@@ -190,7 +190,8 @@ class HrPayslip(models.Model):
leave_time = (interval[1] - interval[0]).seconds / 3600
current_leave_struct['number_of_hours'] += leave_time
work_hours = contract.employee_id.get_day_work_hours_count(interval[0].date(), calendar=contract.resource_calendar_id)
- current_leave_struct['number_of_days'] += leave_time / work_hours
+ if work_hours:
+ current_leave_struct['number_of_days'] += leave_time / work_hours
# compute worked days
work_data = contract.employee_id.get_work_days_data(day_from, day_to, calendar=contract.resource_calendar_id)
diff --git a/addons/hr_payroll/wizard/hr_payroll_payslips_by_employees.py b/addons/hr_payroll/wizard/hr_payroll_payslips_by_employees.py
index 73a304daa2e..d9a59cba3a0 100644
--- a/addons/hr_payroll/wizard/hr_payroll_payslips_by_employees.py
+++ b/addons/hr_payroll/wizard/hr_payroll_payslips_by_employees.py
@@ -35,6 +35,7 @@ class HrPayslipEmployees(models.TransientModel):
'date_from': from_date,
'date_to': to_date,
'credit_note': run_data.get('credit_note'),
+ 'company_id': employee.company_id.id,
}
payslips += self.env['hr.payslip'].create(res)
payslips.compute_sheet()
diff --git a/addons/hr_timesheet/models/hr_timesheet.py b/addons/hr_timesheet/models/hr_timesheet.py
index ee1d5833fb0..ddfd68e0d56 100644
--- a/addons/hr_timesheet/models/hr_timesheet.py
+++ b/addons/hr_timesheet/models/hr_timesheet.py
@@ -14,7 +14,7 @@ class AccountAnalyticLine(models.Model):
result['employee_id'] = self.env['hr.employee'].search([('user_id', '=', result['user_id'])], limit=1).id
return result
- task_id = fields.Many2one('project.task', 'Task')
+ task_id = fields.Many2one('project.task', 'Task', index=True)
project_id = fields.Many2one('project.project', 'Project', domain=[('allow_timesheets', '=', True)])
employee_id = fields.Many2one('hr.employee', "Employee")
diff --git a/addons/hr_timesheet/models/project.py b/addons/hr_timesheet/models/project.py
index e335789a230..72a88277886 100644
--- a/addons/hr_timesheet/models/project.py
+++ b/addons/hr_timesheet/models/project.py
@@ -15,13 +15,18 @@ class Project(models.Model):
class Task(models.Model):
_inherit = "project.task"
- remaining_hours = fields.Float("Remaining Hours", compute='_compute_progress_hours', store=True, help="Total remaining time, can be re-estimated periodically by the assignee of the task.")
+ remaining_hours = fields.Float("Remaining Hours", compute='_compute_progress_hours', inverse='_inverse_remaining_hours', store=True, help="Total remaining time, can be re-estimated periodically by the assignee of the task.")
effective_hours = fields.Float("Hours Spent", compute='_compute_effective_hours', compute_sudo=True, store=True, help="Computed using the sum of the task work done.")
total_hours_spent = fields.Float("Total Hours", compute='_compute_progress_hours', store=True, help="Computed as: Time Spent + Sub-tasks Hours.")
- progress = fields.Float("Progress", compute='_compute_progress_hours', store=True, group_operator="avg", help="Display progress of current task. In case if the total spent hours exceeds planned hours then the progress bar may go above 100%")
+ progress = fields.Float("Progress", compute='_compute_progress_hours', store=True, group_operator="avg", help="Display progress of current task.")
subtask_effective_hours = fields.Float("Sub-tasks Hours Spent", compute='_compute_subtask_effective_hours', store=True, help="Sum of actually spent hours on the subtask(s)", oldname='children_hours')
timesheet_ids = fields.One2many('account.analytic.line', 'task_id', 'Timesheets')
+ @api.onchange('remaining_hours')
+ def _inverse_remaining_hours(self):
+ for task in self:
+ task.planned_hours = task.remaining_hours + task.effective_hours + task.subtask_effective_hours
+
@api.depends('timesheet_ids.unit_amount')
def _compute_effective_hours(self):
for task in self:
@@ -31,7 +36,11 @@ class Task(models.Model):
def _compute_progress_hours(self):
for task in self:
if (task.planned_hours > 0.0):
- task.progress = round(100.0 * (task.effective_hours + task.subtask_effective_hours) / task.planned_hours, 2)
+ task_total_hours = task.effective_hours + task.subtask_effective_hours
+ if task_total_hours > task.planned_hours:
+ task.progress = 100
+ else:
+ task.progress = round(100.0 * task_total_hours / task.planned_hours, 2)
else:
task.progress = 0.0
diff --git a/addons/hr_timesheet/views/project_views.xml b/addons/hr_timesheet/views/project_views.xml
index 47224328b99..e9c2d94808a 100644
--- a/addons/hr_timesheet/views/project_views.xml
+++ b/addons/hr_timesheet/views/project_views.xml
@@ -62,13 +62,16 @@
<group>
<group>
<field name="planned_hours" widget="float_time"/>
- <field name="subtask_planned_hours" widget="float_time" groups="project.group_subtask_project" attrs="{'invisible': [('subtask_count', '=', 0)]}"/>
+ <label for="subtask_planned_hours" groups="project.group_subtask_project" attrs="{'invisible': [('subtask_count', '=', 0)]}"/>
+ <div class="o_row" groups="project.group_subtask_project" attrs="{'invisible': [('subtask_count', '=', 0)]}">
+ <field name="subtask_planned_hours" widget="float_time"/><span> planned hours</span>
+ </div>
</group>
<group>
<field name="progress" widget="progressbar"/>
</group>
</group>
- <field name="timesheet_ids" context="{'default_project_id': project_id}">
+ <field name="timesheet_ids" context="{'default_project_id': project_id, 'default_name':''}">
<tree editable="bottom" string="Timesheet Activities" default_order="date">
<field name="date"/>
<field name="user_id" invisible="1"/>
@@ -83,7 +86,7 @@
<field name="effective_hours" widget="float_time" />
<field name="subtask_effective_hours" widget="float_time" groups="project.group_subtask_project" attrs="{'invisible' : [('subtask_count', '=', 0)]}" />
<field name="total_hours_spent" widget="float_time" class="oe_subtotal_footer_separator" attrs="{'invisible' : [('subtask_effective_hours', '=', 0.0)]}" />
- <field name="remaining_hours" widget="float_time" class="oe_subtotal_footer_separator" readonly="1"/>
+ <field name="remaining_hours" widget="float_time" class="oe_subtotal_footer_separator"/>
</group>
</group>
</page>
diff --git a/addons/im_livechat/controllers/main.py b/addons/im_livechat/controllers/main.py
index 11eabd18423..e228524893a 100644
--- a/addons/im_livechat/controllers/main.py
+++ b/addons/im_livechat/controllers/main.py
@@ -110,7 +110,8 @@ class LivechatController(http.Controller):
@http.route('/im_livechat/history', type="json", auth="public")
def history_pages(self, pid, channel_uuid, page_history=None):
- channel = request.env['mail.channel'].search([('uuid', '=', channel_uuid)])
+ partner_ids = (pid, request.env.user.partner_id.id)
+ channel = request.env['mail.channel'].sudo().search([('uuid', '=', channel_uuid), ('channel_partner_ids', 'in', partner_ids)])
if channel:
channel._send_history_message(pid, page_history)
return True
diff --git a/addons/im_livechat/models/im_livechat_channel.py b/addons/im_livechat/models/im_livechat_channel.py
index c2741fcef92..783fbd417ef 100644
--- a/addons/im_livechat/models/im_livechat_channel.py
+++ b/addons/im_livechat/models/im_livechat_channel.py
@@ -238,7 +238,7 @@ class ImLivechatChannelRule(models.Model):
"""
def _match(rules):
for rule in rules:
- if re.search(rule.regex_url, url):
+ if re.search(rule.regex_url or '', url):
return rule
return False
# first, search the country specific rules (the first match is returned)
diff --git a/addons/im_livechat/static/src/js/im_livechat.js b/addons/im_livechat/static/src/js/im_livechat.js
index d652325d757..e4efb5906ff 100644
--- a/addons/im_livechat/static/src/js/im_livechat.js
+++ b/addons/im_livechat/static/src/js/im_livechat.js
@@ -155,8 +155,8 @@ var LivechatButton = Widget.extend({
self.send_welcome_message();
self.render_messages();
- this.busBus.add_channel(channel.uuid);
- this.busBus.start_polling();
+ self.busBus.add_channel(channel.uuid);
+ self.busBus.start_polling();
utils.set_cookie('im_livechat_session', JSON.stringify(channel), 60*60);
utils.set_cookie('im_livechat_auto_popup', JSON.stringify(false), 60*60);
diff --git a/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py b/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py
index 0d182afcc72..46c0748f5fa 100644
--- a/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py
+++ b/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py
@@ -95,7 +95,7 @@ class HrContract(models.Model):
+ 12.0 * contract.internet \
+ 12.0 * (contract.mobile + contract.mobile_plus) \
+ 12.0 * contract.transport_employer_cost \
- + (1.326 / 1.05 * 12.0) * contract.commission_on_target \
+ + contract.warrants_cost \
+ 220.0 * contract.meal_voucher_paid_by_employer
contract.final_yearly_costs = yearly_cost / (1.0 - (contract.holidays - 20.0) / 231.0)
contract.wage = contract._get_gross_from_employer_costs(contract.final_yearly_costs)
@@ -116,8 +116,8 @@ class HrContract(models.Model):
@api.depends('commission_on_target')
def _compute_warrants_cost(self):
for contract in self:
- contract.warrants_cost = contract.commission_on_target * 1.05 * 12.0
- contract.warrant_value_employee = contract.commission_on_target * (1.00 - 0.535) * 12.0
+ contract.warrants_cost = contract.commission_on_target * 1.326 * 1.05 * 12.0
+ contract.warrant_value_employee = contract.commission_on_target * 1.326 * (1.00 - 0.535) * 12.0
@api.depends('wage', 'fuel_card', 'representation_fees', 'transport_employer_cost',
'internet', 'mobile', 'mobile_plus')
diff --git a/addons/l10n_be_hr_payroll_fleet/views/res_config_settings_views.xml b/addons/l10n_be_hr_payroll_fleet/views/res_config_settings_views.xml
index 4c095e2eea5..f4800282ec2 100644
--- a/addons/l10n_be_hr_payroll_fleet/views/res_config_settings_views.xml
+++ b/addons/l10n_be_hr_payroll_fleet/views/res_config_settings_views.xml
@@ -15,9 +15,9 @@
<div class="o_setting_left_pane"/>
<div class="o_setting_right_pane">
<label string="New Car Request"/>
- <div class="text-muted o_row">
+ <div class="text-muted content-group mt16">
<span>An employee cannot request a new car if there are more than </span>
- <field name="max_unused_cars"/>
+ <field name="max_unused_cars" class="text-center oe_inline" />
<span> existing vehicles availables.</span>
</div>
</div>
diff --git a/addons/l10n_fr/data/account_chart_template_data.xml b/addons/l10n_fr/data/account_chart_template_data.xml
index db836b0fd3b..aac0137d150 100644
--- a/addons/l10n_fr/data/account_chart_template_data.xml
+++ b/addons/l10n_fr/data/account_chart_template_data.xml
@@ -2836,98 +2836,98 @@
<record id="pcg_501" model="account.account.template">
<field name="name">Valeurs mobilières de placement - Parts dans entreprises liées</field>
<field name="code">501</field>
- <field name="user_type_id" ref="account.data_account_type_liquidity"/>
+ <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
<record id="pcg_502" model="account.account.template">
<field name="name">Valeurs mobilières de placement - Actions propres</field>
<field name="code">502</field>
- <field name="user_type_id" ref="account.data_account_type_liquidity"/>
+ <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
<record id="pcg_5031" model="account.account.template">
<field name="name">Valeurs mobilières de placement - Titres cotés</field>
<field name="code">5031</field>
- <field name="user_type_id" ref="account.data_account_type_liquidity"/>
+ <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
<record id="pcg_5035" model="account.account.template">
<field name="name">Valeurs mobilières de placement - Titres non cotés</field>
<field name="code">5035</field>
- <field name="user_type_id" ref="account.data_account_type_liquidity"/>
+ <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
<record id="pcg_504" model="account.account.template">
<field name="name">Valeurs mobilières de placement - Autres titres conférant un droit de propriété</field>
<field name="code">504</field>
- <field name="user_type_id" ref="account.data_account_type_liquidity"/>
+ <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
<record id="pcg_505" model="account.account.template">
<field name="name">Obligations et bons émis par la société et rachetés par elle</field>
<field name="code">505</field>
- <field name="user_type_id" ref="account.data_account_type_liquidity"/>
+ <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
<record id="pcg_5061" model="account.account.template">
<field name="name">Obligations cotés</field>
<field name="code">5061</field>
- <field name="user_type_id" ref="account.data_account_type_liquidity"/>
+ <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
<record id="pcg_5065" model="account.account.template">
<field name="name">Obligations non cotés</field>
<field name="code">5065</field>
- <field name="user_type_id" ref="account.data_account_type_liquidity"/>
+ <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
<record id="pcg_507" model="account.account.template">
<field name="name">Bons du Trésor et bons de caisse à court terme</field>
<field name="code">507</field>
- <field name="user_type_id" ref="account.data_account_type_liquidity"/>
+ <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
<record id="pcg_5081" model="account.account.template">
<field name="name">Autres valeurs mobilières de placement</field>
<field name="code">5081</field>
- <field name="user_type_id" ref="account.data_account_type_liquidity"/>
+ <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
<record id="pcg_5082" model="account.account.template">
<field name="name">Bons de souscription</field>
<field name="code">5082</field>
- <field name="user_type_id" ref="account.data_account_type_liquidity"/>
+ <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
<record id="pcg_5088" model="account.account.template">
<field name="name">Intérêts courus sur obligations, bons et valeurs assimilées</field>
<field name="code">5088</field>
- <field name="user_type_id" ref="account.data_account_type_liquidity"/>
+ <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
<record id="pcg_509" model="account.account.template">
<field name="name">Versements restant à effectuer sur valeurs mobilières de placement non libérées</field>
<field name="code">509</field>
- <field name="user_type_id" ref="account.data_account_type_liquidity"/>
+ <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
<record id="pcg_5111" model="account.account.template">
<field name="name">Coupons échus à l'encaissement</field>
<field name="code">5111</field>
- <field name="user_type_id" ref="account.data_account_type_liquidity"/>
+ <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="reconcile" eval="True"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
@@ -2935,7 +2935,7 @@
<record id="pcg_5112" model="account.account.template">
<field name="name">Chèques à encaisser</field>
<field name="code">5112</field>
- <field name="user_type_id" ref="account.data_account_type_liquidity"/>
+ <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="reconcile" eval="True"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
@@ -2943,7 +2943,7 @@
<record id="pcg_5113" model="account.account.template">
<field name="name">Effets à l'encaissement</field>
<field name="code">5113</field>
- <field name="user_type_id" ref="account.data_account_type_liquidity"/>
+ <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="reconcile" eval="True"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
@@ -2951,7 +2951,7 @@
<record id="pcg_5114" model="account.account.template">
<field name="name">Effets à l'escompte</field>
<field name="code">5114</field>
- <field name="user_type_id" ref="account.data_account_type_liquidity"/>
+ <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="reconcile" eval="True"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
@@ -2980,56 +2980,56 @@
<record id="pcg_516" model="account.account.template">
<field name="name">Sociétés de bourse</field>
<field name="code">516</field>
- <field name="user_type_id" ref="account.data_account_type_liquidity"/>
+ <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
<record id="pcg_517" model="account.account.template">
<field name="name">Autres organismes financiers</field>
<field name="code">517</field>
- <field name="user_type_id" ref="account.data_account_type_liquidity"/>
+ <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
<record id="pcg_5181" model="account.account.template">
<field name="name">Intérêts courus à payer</field>
<field name="code">5181</field>
- <field name="user_type_id" ref="account.data_account_type_liquidity"/>
+ <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
<record id="pcg_5188" model="account.account.template">
<field name="name">Intérêts courus à recevoir</field>
<field name="code">5188</field>
- <field name="user_type_id" ref="account.data_account_type_liquidity"/>
+ <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
<record id="pcg_5191" model="account.account.template">
<field name="name">Concours bancaires courants - Crédit de mobilisation de créances commerciales (CMCC)</field>
<field name="code">5191</field>
- <field name="user_type_id" ref="account.data_account_type_liquidity"/>
+ <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
<record id="pcg_5193" model="account.account.template">
<field name="name">Concours bancaires courants - Mobilisation de créances nées à l'étranger</field>
<field name="code">5193</field>
- <field name="user_type_id" ref="account.data_account_type_liquidity"/>
+ <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
<record id="pcg_5198" model="account.account.template">
<field name="name">Concours bancaires courants - Intérêts courus sur concours bancaires courants</field>
<field name="code">5198</field>
- <field name="user_type_id" ref="account.data_account_type_liquidity"/>
+ <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
<record id="pcg_52" model="account.account.template">
<field name="name">Instruments de trésorerie</field>
<field name="code">52</field>
- <field name="user_type_id" ref="account.data_account_type_liquidity"/>
+ <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
@@ -3064,35 +3064,35 @@
<record id="pcg_54" model="account.account.template">
<field name="name">Régies d'avances et accréditifs</field>
<field name="code">54</field>
- <field name="user_type_id" ref="account.data_account_type_liquidity"/>
+ <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
<record id="pcg_5903" model="account.account.template">
<field name="name">Provisions pour dépréciation des actions</field>
<field name="code">5903</field>
- <field name="user_type_id" ref="account.data_account_type_liquidity"/>
+ <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
<record id="pcg_5904" model="account.account.template">
<field name="name">Provisions pour dépréciation des autres titres conférant un droit de propriété</field>
<field name="code">5904</field>
- <field name="user_type_id" ref="account.data_account_type_liquidity"/>
+ <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
<record id="pcg_5906" model="account.account.template">
<field name="name">Provisions pour dépréciation des obligations</field>
<field name="code">5906</field>
- <field name="user_type_id" ref="account.data_account_type_liquidity"/>
+ <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
<record id="pcg_5908" model="account.account.template">
<field name="name">Provisions pour dépréciation des autres valeurs mobilières de placement et créances assimilées (provisions)</field>
<field name="code">5908</field>
- <field name="user_type_id" ref="account.data_account_type_liquidity"/>
+ <field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
diff --git a/addons/l10n_in/views/report_invoice.xml b/addons/l10n_in/views/report_invoice.xml
index 3441689d3d5..7fb7337becf 100644
--- a/addons/l10n_in/views/report_invoice.xml
+++ b/addons/l10n_in/views/report_invoice.xml
@@ -73,21 +73,21 @@
<td class="text-right" t-if="is_cgst">
<t t-foreach="tax_datas[l.id]" t-as="tax_data">
<t t-if="tag_cgst_id in tax_data['tag_ids']">
- <t t-esc="tax_data['amount']"/>
+ <t t-esc="tax_data['amount']" t-options="{'widget': 'monetary', 'display_currency': o.currency_id}"/>
</t>
</t>
</td>
<td class="text-right" t-if="is_cgst">
<t t-foreach="tax_datas[l.id]" t-as="tax_data">
<t t-if="tag_sgst_id in tax_data['tag_ids']">
- <t t-esc="tax_data['amount']"/>
+ <t t-esc="tax_data['amount']" t-options="{'widget': 'monetary', 'display_currency': o.currency_id}"/>
</t>
</t>
</td>
<td class="text-right" t-if="is_igst">
<t t-foreach="tax_datas[l.id]" t-as="tax_data">
<t t-if="tag_igst_id in tax_data['tag_ids']">
- <t t-esc="tax_data['amount']"/>
+ <t t-esc="tax_data['amount']" t-options="{'widget': 'monetary', 'display_currency': o.currency_id}"/>
</t>
</t>
</td>
@@ -95,10 +95,10 @@
<td class="text-right" t-if="is_cess">
<t t-foreach="tax_datas[l.id]" t-as="tax_data">
<t t-if="tag_cess_id in tax_data['tag_ids']">
- <t t-set="total_cess" t-value="total_cess + tax_data['amount']"/>
+ <t t-set="total_cess" t-value="total_cess + tax_data['amount']" t-options="{'widget': 'monetary', 'display_currency': o.currency_id}"/>
</t>
</t>
- <t t-esc="total_cess"/>
+ <t t-esc="total_cess" t-options="{'widget': 'monetary', 'display_currency': o.currency_id}"/>
</td>
</t>
</tr>
diff --git a/addons/l10n_mx/data/account_tax_data.xml b/addons/l10n_mx/data/account_tax_data.xml
index 8675040c026..168b1955609 100644
--- a/addons/l10n_mx/data/account_tax_data.xml
+++ b/addons/l10n_mx/data/account_tax_data.xml
@@ -18,6 +18,10 @@
<field name="name">DIOT: 16%</field>
<field name="applicability">taxes</field>
</record>
+ <record id="tag_diot_16_imp" model="account.account.tag">
+ <field name="name">DIOT: 16% IMP</field>
+ <field name="applicability">taxes</field>
+ </record>
<record id="tag_diot_0" model="account.account.tag">
<field name="name">DIOT: 0%</field>
<field name="applicability">taxes</field>
diff --git a/addons/mail/data/mail_shortcode_data.xml b/addons/mail/data/mail_shortcode_data.xml
index afd0cf91146..ac648e3c738 100644
--- a/addons/mail/data/mail_shortcode_data.xml
+++ b/addons/mail/data/mail_shortcode_data.xml
@@ -5,18 +5,25 @@
<!-- Smileys -->
<record id="mail_smiley_normal_smile" model="mail.shortcode">
<field name="source">:)</field>
- <field name="unicode_source">😊</field>
- <field name="substitution">&lt;img src='/mail/static/src/img/smiley/normal_smile.png' alt='😊'/&gt;</field>
+ <field name="unicode_source">🙂</field>
+ <field name="substitution">&lt;img src='/mail/static/src/img/smiley/smile.png' alt='🙂'/&gt;</field>
<field name="description">Smile</field>
<field name="shortcode_type">image</field>
</record>
<record id="mail_smiley_smile" model="mail.shortcode">
<field name="source">:D</field>
- <field name="unicode_source">😄</field>
- <field name="substitution">&lt;img src='/mail/static/src/img/smiley/smile.png' alt='😄'/&gt;</field>
+ <field name="unicode_source">😀</field>
+ <field name="substitution">&lt;img src='/mail/static/src/img/smiley/smile_open_mouth.png' alt='😀'/&gt;</field>
<field name="description">Smile with open mouth</field>
<field name="shortcode_type">image</field>
</record>
+ <record id="mail_smiley_smile_eyes" model="mail.shortcode">
+ <field name="source">^^</field>
+ <field name="unicode_source">😊</field>
+ <field name="substitution">&lt;img src='/mail/static/src/img/smiley/normal_smile.png' alt='😊'/&gt;</field>
+ <field name="description">Smile with with smiling eyes</field>
+ <field name="shortcode_type">image</field>
+ </record>
<record id="mail_smiley_laugh" model="mail.shortcode">
<field name="source">xD</field>
<field name="unicode_source">😆</field>
@@ -25,7 +32,7 @@
<field name="shortcode_type">image</field>
</record>
<record id="mail_smiley_tears_of_joy" model="mail.shortcode">
- <field name="source">:-)</field>
+ <field name="source">:joy</field>
<field name="unicode_source">😂</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/tears_of_joy.png' alt='😂'/&gt;</field>
<field name="description">Tears of joy</field>
diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py
index 33ebd368b79..c08866fa6a4 100644
--- a/addons/mail/models/mail_activity.py
+++ b/addons/mail/models/mail_activity.py
@@ -358,6 +358,15 @@ class MailActivityMixin(models.AbstractModel):
return [('activity_ids.summary', operator, operand)]
@api.multi
+ def write(self, vals):
+ # Delete activities of archived record.
+ if 'active' in vals and vals['active'] is False:
+ self.env['mail.activity'].sudo().search(
+ [('res_model', '=', self._name), ('res_id', 'in', self.ids)]
+ ).unlink()
+ return super(MailActivityMixin, self).write(vals)
+
+ @api.multi
def unlink(self):
""" Override unlink to delete records activities through (res_model, res_id). """
record_ids = self.ids
diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py
index 5f663963bfa..f94eead0736 100644
--- a/addons/mail/models/mail_channel.py
+++ b/addons/mail/models/mail_channel.py
@@ -56,7 +56,7 @@ class Channel(models.Model):
('channel', 'Channel')],
'Channel Type', default='channel')
description = fields.Text('Description')
- uuid = fields.Char('UUID', size=50, index=True, default=lambda self: '%s' % uuid.uuid4())
+ uuid = fields.Char('UUID', size=50, index=True, default=lambda self: '%s' % uuid.uuid4(), copy=False)
email_send = fields.Boolean('Send messages by email', default=False)
# multi users channel
channel_last_seen_partner_ids = fields.One2many('mail.channel.partner', 'channel_id', string='Last Seen')
diff --git a/addons/mail/static/src/img/smiley/smile_open_mouth.png b/addons/mail/static/src/img/smiley/smile_open_mouth.png
new file mode 100644
index 00000000000..7dcdd3f55fe
--- /dev/null
+++ b/addons/mail/static/src/img/smiley/smile_open_mouth.png
Binary files differ
diff --git a/addons/mail/static/src/js/activity.js b/addons/mail/static/src/js/activity.js
index 37903e3861f..4bb81489789 100644
--- a/addons/mail/static/src/js/activity.js
+++ b/addons/mail/static/src/js/activity.js
@@ -2,7 +2,7 @@ odoo.define('mail.Activity', function (require) {
"use strict";
var AbstractField = require('web.AbstractField');
-var concurrency = require('web.concurrency');
+var BasicModel = require('web.BasicModel');
var core = require('web.core');
var field_registry = require('web.field_registry');
var time = require('web.time');
@@ -12,6 +12,63 @@ var QWeb = core.qweb;
var _t = core._t;
/**
+ * Fetches activities and postprocesses them.
+ *
+ * This standalone function performs an RPC, but to do so, it needs an instance
+ * of a widget that implements the _rpc() function.
+ *
+ * @todo i'm not very proud of the widget instance given in arguments, we should
+ * probably try to do it a better way in the future.
+ *
+ * @param {Widget} self a widget instance that can perform RPCs
+ * @param {Array} ids the ids of activities to read
+ * @return {Deferred<Array>} resolved with the activities
+ */
+function _readActivities(self, ids) {
+ if (!ids.length) {
+ return $.when([]);
+ }
+ return self._rpc({
+ model: 'mail.activity',
+ method: 'read',
+ args: [ids],
+ }).then(function (activities) {
+ // convert create_date and date_deadline to moments
+ _.each(activities, function (activity) {
+ activity.create_date = moment(time.auto_str_to_date(activity.create_date));
+ activity.date_deadline = moment(time.auto_str_to_date(activity.date_deadline));
+ });
+
+ // sort activities by due date
+ activities = _.sortBy(activities, 'date_deadline');
+
+ return activities;
+ });
+}
+
+BasicModel.include({
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Fetches the activities displayed by the activity field widget in form
+ * views.
+ *
+ * @private
+ * @param {Object} record - an element from the localData
+ * @param {string} fieldName
+ * @return {Deferred<Array>} resolved with the activities
+ */
+ _fetchSpecialActivity: function (record, fieldName) {
+ var localID = (record._changes && fieldName in record._changes) ?
+ record._changes[fieldName] :
+ record.data[fieldName];
+ return _readActivities(this, this.localData[localID].res_ids);
+ },
+});
+
+/**
* Set the 'label_delay' entry in activity data according to the deadline date
* @param {Array} activities list of activity Object
* @return {Array} : list of modified activity Object
@@ -44,12 +101,6 @@ var setDelayLabel = function(activities){
};
var AbstractActivityField = AbstractField.extend({
- // inherited
- init: function () {
- this._super.apply(this, arguments);
- this.activities = [];
- },
-
// private
_markActivityDone: function (id, feedback) {
return this._rpc({
@@ -59,33 +110,6 @@ var AbstractActivityField = AbstractField.extend({
kwargs: {feedback: feedback},
});
},
- _readActivities: function () {
- var self = this;
- var missing_ids = _.difference(this.value.res_ids, _.pluck(this.activities, 'id'));
- var fetch_def;
- if (missing_ids.length) {
- fetch_def = this._rpc({
- model: 'mail.activity',
- method: 'read',
- args: [missing_ids],
- });
- }
- return $.when(fetch_def).then(function (results) {
- // convert create_date and date_deadline to moments
- _.each(results, function (activity) {
- activity.create_date = moment(time.auto_str_to_date(activity.create_date));
- activity.date_deadline = moment(time.auto_str_to_date(activity.date_deadline));
- });
-
- // filter out activities that are no longer linked to this record
- self.activities = _.filter(self.activities.concat(results || []), function (activity) {
- return _.contains(self.value.res_ids, activity.id);
- });
-
- // sort activities by due date
- self.activities = _.sortBy(self.activities, 'date_deadline');
- });
- },
_scheduleActivity: function (id, previous_activity_type_id, callback) {
var action = {
type: 'ir.actions.act_window',
@@ -116,41 +140,37 @@ var Activity = AbstractActivityField.extend({
'click .o_activity_unlink': '_onUnlinkActivity',
'click .o_activity_done': '_onMarkActivityDone',
},
+ specialData: '_fetchSpecialActivity',
// inherited
init: function () {
this._super.apply(this, arguments);
- this.dp = new concurrency.DropPrevious();
+ this.activities = this.record.specialData[this.name];
},
_render: function () {
- // note: the rendering of this widget is asynchronous as it needs to
- // fetch the details of the linked activities
- var self = this;
- var fetch_def = this.dp.add(this._readActivities());
- return fetch_def.then(function () {
- _.each(self.activities, function (activity) {
- if (activity.note) {
- activity.note = utils.parse_and_transform(activity.note, utils.add_link);
- }
- });
- var activities = setDelayLabel(self.activities);
- if (activities.length) {
- var nbActivities = _.countBy(activities, 'state');
- self.$el.html(QWeb.render('mail.activity_items', {
- activities: activities,
- nbPlannedActivities: nbActivities.planned,
- nbTodayActivities: nbActivities.today,
- nbOverdueActivities: nbActivities.overdue,
- date_format: time.getLangDateFormat(),
- datetime_format: time.getLangDatetimeFormat(),
- }));
- } else {
- self.$el.empty();
+ _.each(this.activities, function (activity) {
+ if (activity.note) {
+ activity.note = utils.parse_and_transform(activity.note, utils.add_link);
}
});
+ var activities = setDelayLabel(this.activities);
+ if (activities.length) {
+ var nbActivities = _.countBy(activities, 'state');
+ this.$el.html(QWeb.render('mail.activity_items', {
+ activities: activities,
+ nbPlannedActivities: nbActivities.planned,
+ nbTodayActivities: nbActivities.today,
+ nbOverdueActivities: nbActivities.overdue,
+ date_format: time.getLangDateFormat(),
+ datetime_format: time.getLangDatetimeFormat(),
+ }));
+ } else {
+ this.$el.empty();
+ }
},
_reset: function (record) {
this._super.apply(this, arguments);
+ this.activities = this.record.specialData[this.name];
// the mail widgets being persistent, one need to update the res_id on reset
this.res_id = record.res_id;
},
@@ -196,9 +216,6 @@ var Activity = AbstractActivityField.extend({
});
return this.do_action(action, {
on_close: function () {
- // remove the edited activity from the array of fetched activities to
- // force a reload of that activity
- self.activities = _.reject(self.activities, {id: activity_id});
self._reload({activity: true, thread: true});
},
});
@@ -312,10 +329,10 @@ var KanbanActivity = AbstractActivityField.extend({
_renderDropdown: function () {
var self = this;
this.$('.o_activity').html(QWeb.render("mail.KanbanActivityLoading"));
- return this._readActivities().then(function () {
+ return _readActivities(this, this.value.res_ids).then(function (activities) {
self.$('.o_activity').html(QWeb.render("mail.KanbanActivityDropdown", {
selection: self.selection,
- records: _.groupBy(setDelayLabel(self.activities), 'state'),
+ records: _.groupBy(setDelayLabel(activities), 'state'),
uid: self.getSession().uid,
}));
});
@@ -328,7 +345,9 @@ var KanbanActivity = AbstractActivityField.extend({
// handlers
_onButtonClicked: function (event) {
event.preventDefault();
- this._renderDropdown();
+ if (!this.$el.hasClass('open')) {
+ this._renderDropdown();
+ }
},
_onMarkActivityDone: function (event) {
event.stopPropagation();
diff --git a/addons/mail/static/src/js/composer.js b/addons/mail/static/src/js/composer.js
index 8de8a242e68..3a7b9c320e1 100644
--- a/addons/mail/static/src/js/composer.js
+++ b/addons/mail/static/src/js/composer.js
@@ -591,7 +591,7 @@ var BasicComposer = Widget.extend({
*/
_onKeydown: function (event) {
switch(event.which) {
- // UP, DOWN: prevent moving cursor if navigation in mention propositions
+ // UP, DOWN, TAB: prevent moving cursor if navigation in mention propositions
case $.ui.keyCode.UP:
case $.ui.keyCode.DOWN:
case $.ui.keyCode.TAB:
diff --git a/addons/mail/static/src/js/discuss_mobile.js b/addons/mail/static/src/js/discuss_mobile.js
index 52622b0ce32..b427ab4b98c 100644
--- a/addons/mail/static/src/js/discuss_mobile.js
+++ b/addons/mail/static/src/js/discuss_mobile.js
@@ -41,7 +41,7 @@ Discuss.include({
*/
on_attach_callback: function () {
if (this.channel && this._isInInboxTab()) {
- this.thread.scroll_to({offset: this.channels_scrolltop[this.channel.id]});
+ this.thread.scroll_to({offset: this.channelsScrolltop[this.channel.id]});
}
},
/**
@@ -49,7 +49,7 @@ Discuss.include({
*/
on_detach_callback: function () {
if (this._isInInboxTab()) {
- this.channels_scrolltop[this.channel.id] = this.thread.get_scrolltop();
+ this.channelsScrolltop[this.channel.id] = this.thread.get_scrolltop();
}
},
diff --git a/addons/mail/static/src/js/document_viewer.js b/addons/mail/static/src/js/document_viewer.js
index 2fa34b613ba..0aac950cf5b 100644
--- a/addons/mail/static/src/js/document_viewer.js
+++ b/addons/mail/static/src/js/document_viewer.js
@@ -17,6 +17,7 @@ var DocumentViewer = Widget.extend({
'click .o_viewer_video': '_onVideoClicked',
'click .move_next': '_onNext',
'click .move_previous': '_onPrevious',
+ 'click .o_rotate': '_onRotate',
'click .o_zoom_in': '_onZoomIn',
'click .o_zoom_out': '_onZoomOut',
'click .o_close_btn, .o_viewer_img_wrapper': '_onClose',
@@ -104,6 +105,30 @@ var DocumentViewer = Widget.extend({
this._reset();
},
/**
+ * Get CSS transform property based on scale and angle
+ *
+ * @private
+ * @param {float} scale
+ * @param {float} angle
+ */
+ _getTransform: function(scale, angle) {
+ return 'scale3d(' + scale + ', ' + scale + ', 1) rotate(' + angle + 'deg)'
+ },
+ /**
+ * Rotate image clockwise by provided angle
+ *
+ * @private
+ * @param {float} angle
+ */
+ _rotate: function (angle) {
+ this._reset();
+ var new_angle = (this.angle || 0) + angle;
+ this.$('.o_viewer_img').css('transform', this._getTransform(this.scale, new_angle));
+ this.$('.o_viewer_img').css('max-width', new_angle % 180 !== 0 ? $(document).height() : '100%');
+ this.$('.o_viewer_img').css('max-height', new_angle % 180 !== 0 ? $(document).width() : '100%');
+ this.angle = new_angle;
+ },
+ /**
* Zoom in/out image by provided scale
*
* @private
@@ -111,7 +136,7 @@ var DocumentViewer = Widget.extend({
*/
_zoom: function (scale) {
if (scale > 0.5) {
- this.$('.o_viewer_img').css('transform', 'scale3d(' + scale + ', ' + scale + ', 1)');
+ this.$('.o_viewer_img').css('transform', this._getTransform(scale, this.angle || 0));
this.scale = scale;
}
},
@@ -284,6 +309,14 @@ var DocumentViewer = Widget.extend({
* @private
* @param {MouseEvent} e
*/
+ _onRotate: function (e) {
+ e.preventDefault();
+ this._rotate(90);
+ },
+ /**
+ * @private
+ * @param {MouseEvent} e
+ */
_onZoomIn: function (e) {
e.preventDefault();
var scale = this.scale + ZOOM_STEP;
diff --git a/addons/mail/static/src/less/composer.less b/addons/mail/static/src/less/composer.less
index 9df25c895a3..e2fae8b106e 100644
--- a/addons/mail/static/src/less/composer.less
+++ b/addons/mail/static/src/less/composer.less
@@ -264,6 +264,7 @@
img {
width: @o-mail-attachment-image-size;
height: @o-mail-attachment-image-size;
+ image-orientation: from-image; // Only supported in Firefox
}
.o_image_overlay{
display: none;
@@ -365,6 +366,9 @@
.o-flex-display();
justify-content: center;
align-items: center;
+ img {
+ image-orientation: from-image; // Only supported in Firefox
+ }
img, video {
max-width: 100%;
max-height: 100%;
diff --git a/addons/mail/static/src/less/discuss.less b/addons/mail/static/src/less/discuss.less
index 46dc6d41bfd..4e0ae26dccc 100644
--- a/addons/mail/static/src/less/discuss.less
+++ b/addons/mail/static/src/less/discuss.less
@@ -321,15 +321,11 @@
.o_mail_chat_content {
.o-flex(1, 0, 0);
- margin-left: 0;
+ margin-left: 10px;
+ margin-right: 10px;
background-color: transparent;
- .o_mail_no_content {
- .o-position-absolute(@top: 30%);
- text-align: center;
- }
}
-
.o_mail_chat_tab_pane {
overflow-x: scroll;
.o_mail_add_channel {
diff --git a/addons/mail/static/src/less/thread.less b/addons/mail/static/src/less/thread.less
index 8d8bc761829..81dee297011 100644
--- a/addons/mail/static/src/less/thread.less
+++ b/addons/mail/static/src/less/thread.less
@@ -208,11 +208,14 @@
.o_thread_title {
margin-top: 20px;
margin-bottom: 20px;
- font-size: 20px;
+ font-weight: bold;
+ font-size: 125%;
}
.o_mail_no_content {
- .o-webclient-padding();
+ .o-position-absolute(30%, 0, 0, 0);
+ text-align: center;
+ font-size: 115%;
}
.o_thread_message .o_thread_message_core .o_mail_read_more {
diff --git a/addons/mail/static/src/xml/thread.xml b/addons/mail/static/src/xml/thread.xml
index 4a7ba086ebf..26c093c3e84 100644
--- a/addons/mail/static/src/xml/thread.xml
+++ b/addons/mail/static/src/xml/thread.xml
@@ -51,6 +51,11 @@
<div t-if="widget.activeAttachment.type == 'image'" class="o_viewer_toolbar">
<ul class="list-inline">
<li>
+ <a class="o_rotate" href="#">
+ <i class="fa fa-2x fa-repeat"></i>
+ </a>
+ </li>
+ <li>
<a class="o_zoom_out" href="#">
<i class="fa fa-2x fa-search-minus"></i>
</a>
@@ -105,18 +110,14 @@
<!-- When a channel is empty-->
<div t-name="mail.EmptyChannel" class="o_mail_no_content">
- <div class="o_view_nocontent">
- <div class="o_nocontent_help">
- <t t-if="options.channel_id==='channel_inbox'">
- <p>Congratulations, your inbox is empty</p>
- <p>New messages appear here.</p>
- </t>
- <t t-if="options.channel_id==='channel_starred'">
- <p>No starred message</p>
- <p>You can mark any message as 'starred', and it shows up in this channel.</p>
- </t>
- </div>
- </div>
+ <t t-if="options.channel_id==='channel_inbox'">
+ <div class="o_thread_title">Congratulations, your inbox is empty</div>
+ <div>New messages appear here.</div>
+ </t>
+ <t t-if="options.channel_id==='channel_starred'">
+ <div class="o_thread_title">No starred message</div>
+ <div>You can mark any message as 'starred', and it shows up in this channel.</div>
+ </t>
</div>
<t t-name="mail.ChatThread.Content">
@@ -266,7 +267,7 @@
</t>
<t t-name="mail.Attachment">
- <t t-set="type" t-value="attachment.mimetype.split('/').shift()"/>
+ <t t-set="type" t-value="attachment.mimetype and attachment.mimetype.split('/').shift()"/>
<div t-attf-class="o_attachment #{attachment.upload ? 'o_attachment_uploading' : ''}" t-att-title="attachment.name">
<t t-if="type == 'image'">
<div class="o_image_box">
diff --git a/addons/mail/static/tests/chatter_tests.js b/addons/mail/static/tests/chatter_tests.js
index 4a23f9b1d3b..a63e074ef4a 100644
--- a/addons/mail/static/tests/chatter_tests.js
+++ b/addons/mail/static/tests/chatter_tests.js
@@ -351,12 +351,12 @@ QUnit.test('kanban activity widget with an activity', function (assert) {
// click on the activity button to re-open dropdown
$record.find('.o_activity_btn').click();
- assert.strictEqual(rpcCount, 2, 'no RPC should be done as the activities are now in cache');
+ assert.strictEqual(rpcCount, 3, 'should have reloaded the activities');
// mark activity as done
$record.find('.o_mark_as_done').click();
$record = kanban.$('.o_kanban_record').first(); // the record widget has been reset
- assert.strictEqual(rpcCount, 4, 'should have done an RPC to mark activity as done, and a read');
+ assert.strictEqual(rpcCount, 5, 'should have done an RPC to mark activity as done, and a read');
assert.ok($record.find('.o_mail_activity .o_activity_color_default:not(.o_activity_color_today)').length,
"activity widget should have been updated correctly");
assert.strictEqual($record.find('.o_mail_activity.open').length, 1,
@@ -707,6 +707,128 @@ QUnit.test('chatter: Attachment viewer', function (assert) {
form.destroy();
});
+QUnit.test('form activity widget: read RPCs', function (assert) {
+ // Records of model 'mail.activity' may be updated in business flows (e.g.
+ // the date of a 'Meeting' activity is updated when the associated meeting
+ // is dragged&dropped in the Calendar view). Because of that, the activities
+ // must be reloaded when the form is reloaded (the widget can't keep an
+ // internal cache).
+ assert.expect(6);
+ this.data.partner.records[0].activity_ids = [1];
+ this.data.partner.records[0].activity_state = 'today';
+ this.data['mail.activity'].records = [{
+ id: 1,
+ display_name: "An activity",
+ date_deadline: moment().format("YYYY-MM-DD"), // now
+ state: "today",
+ user_id: 2,
+ activity_type_id: 2,
+ }];
+
+ var nbReads = 0;
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ services: this.services,
+ arch: '<form string="Partners">' +
+ '<div class="oe_chatter">' +
+ '<field name="activity_ids" widget="mail_activity"/>' +
+ '</div>' +
+ '</form>',
+ res_id: 2,
+ mockRPC: function (route, args) {
+ if (args.method === 'read' && args.model === 'mail.activity') {
+ nbReads++;
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(nbReads, 1, "should have read the activities");
+ assert.strictEqual(form.$('.o_mail_activity .o_thread_message').length, 1,
+ "should display an activity");
+ assert.strictEqual(form.$('.o_mail_activity .o_thread_message .o_activity_date').text(),
+ 'Today', "the activity should be today");
+
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$buttons.find('.o_form_button_save').click();
+
+ assert.strictEqual(nbReads, 1, "should not have re-read the activities");
+
+ // simulate a date change, and a reload of the form view
+ var tomorrow = moment().add(1, 'day').format("YYYY-MM-DD");
+ this.data['mail.activity'].records[0].date_deadline = tomorrow;
+ form.reload();
+
+ assert.strictEqual(nbReads, 2, "should have re-read the activities");
+ assert.strictEqual(form.$('.o_mail_activity .o_thread_message .o_activity_date').text(),
+ 'Tomorrow', "the activity should be tomorrow");
+
+ form.destroy();
+});
+
+QUnit.test('form activity widget on a new record', function (assert) {
+ assert.expect(0);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ services: this.services,
+ arch: '<form string="Partners">' +
+ '<div class="oe_chatter">' +
+ '<field name="activity_ids" widget="mail_activity"/>' +
+ '</div>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'read' && args.model === 'mail.activity') {
+ throw new Error("should not do a read on mail.activity");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ form.destroy();
+});
+
+QUnit.test('form activity widget with another x2many field in view', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.fields.m2m = {string: "M2M", type: 'many2many', relation: 'partner'};
+
+ this.data.partner.records[0].m2m = [2];
+ this.data.partner.records[0].activity_ids = [1];
+ this.data.partner.records[0].activity_state = 'today';
+ this.data['mail.activity'].records = [{
+ id: 1,
+ display_name: "An activity",
+ date_deadline: moment().format("YYYY-MM-DD"), // now
+ state: "today",
+ user_id: 2,
+ activity_type_id: 2,
+ }];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ services: this.services,
+ arch: '<form string="Partners">' +
+ '<field name="m2m" widget="many2many_tags"/>' +
+ '<div class="oe_chatter">' +
+ '<field name="activity_ids" widget="mail_activity"/>' +
+ '</div>' +
+ '</form>',
+ res_id: 2,
+ });
+
+ assert.strictEqual(form.$('.o_mail_activity .o_thread_message').length, 1,
+ "should display an activity");
+
+ form.destroy();
+});
+
QUnit.test('form activity widget: schedule next activity', function (assert) {
assert.expect(5);
this.data.partner.records[0].activity_ids = [1];
@@ -746,7 +868,7 @@ QUnit.test('form activity widget: schedule next activity', function (assert) {
"the feedback should be sent correctly");
return $.when();
}
- if (args.method === 'read' && checkReadArgs) {
+ if (args.method === 'read' && args.model === 'partner' && checkReadArgs) {
assert.deepEqual(args.args[1], ['activity_ids', 'message_ids', 'display_name'],
"should only read the mail fields");
}
diff --git a/addons/mail/static/tests/discuss_mobile_tests.js b/addons/mail/static/tests/discuss_mobile_tests.js
index d17364b53ad..921436d4657 100644
--- a/addons/mail/static/tests/discuss_mobile_tests.js
+++ b/addons/mail/static/tests/discuss_mobile_tests.js
@@ -65,6 +65,29 @@ QUnit.test('mobile basic rendering', function (assert) {
});
});
+QUnit.test('on_{attach/detach}_callback', function (assert) {
+ assert.expect(2);
+ var done = assert.async();
+
+ createDiscuss({
+ id: 1,
+ context: {},
+ params: {},
+ data: this.data,
+ services: this.services,
+ }).then(function (discuss) {
+ try {
+ discuss.on_attach_callback();
+ assert.ok(true, 'should not crash on attach callback');
+ discuss.on_detach_callback();
+ assert.ok(true, 'should not crash on detach callback');
+ } finally {
+ discuss.destroy();
+ done();
+ }
+ });
+});
+
});
});
diff --git a/addons/mail/static/tests/discuss_tests.js b/addons/mail/static/tests/discuss_tests.js
index 4338db1cfaf..6990a04efba 100644
--- a/addons/mail/static/tests/discuss_tests.js
+++ b/addons/mail/static/tests/discuss_tests.js
@@ -44,10 +44,9 @@ QUnit.test('basic rendering', function (assert) {
assert.strictEqual($sidebar.length, 1,
"should have rendered a sidebar");
- var $nocontent = discuss.$('.o_mail_chat_content');
- assert.strictEqual($nocontent.length, 1,
+ assert.strictEqual(discuss.$('.o_mail_chat_content').length, 1,
"should have rendered the content");
- assert.strictEqual($nocontent.find('.o_mail_no_content').length, 1,
+ assert.strictEqual(discuss.$('.o_mail_no_content').length, 1,
"should display no content message");
var $inbox = $sidebar.find('.o_mail_chat_channel_item[data-channel-id=channel_inbox]');
diff --git a/addons/mail/static/tests/helpers/test_utils.js b/addons/mail/static/tests/helpers/test_utils.js
index 095da27be58..cd933bc2483 100644
--- a/addons/mail/static/tests/helpers/test_utils.js
+++ b/addons/mail/static/tests/helpers/test_utils.js
@@ -77,6 +77,15 @@ function createDiscuss(params) {
var selector = params.debug ? 'body' : '#qunit-fixture';
discuss.appendTo($(selector));
+ // override 'destroy' of discuss so that it calls 'destroy' on the parent
+ // instead, which is the parent of discuss and the mockServer.
+ discuss.destroy = function () {
+ // remove the override to properly destroy discuss and its children
+ // when it will be called the second time (by its parent)
+ delete discuss.destroy;
+ parent.destroy();
+ };
+
return discuss.call('chat_manager', 'isReady').then(function () {
return discuss;
});
diff --git a/addons/mrp/wizard/mrp_product_produce.py b/addons/mrp/wizard/mrp_product_produce.py
index b2705050636..1c570d858cc 100644
--- a/addons/mrp/wizard/mrp_product_produce.py
+++ b/addons/mrp/wizard/mrp_product_produce.py
@@ -199,7 +199,7 @@ class MrpProductProduceLine(models.TransientModel):
res = {}
if self.product_id.tracking == 'serial':
if float_compare(self.qty_done, 1.0, precision_rounding=self.move_id.product_id.uom_id.rounding) != 0:
- message = _('You can only process 1.0 %s for products with unique serial number.') % self.product_id.uom_id.name
+ message = _('You can only process 1.0 %s of products with unique serial number.') % self.product_id.uom_id.name
res['warning'] = {'title': _('Warning'), 'message': message}
return res
diff --git a/addons/note_pad/models/note.py b/addons/note_pad/models/note.py
index 080e313a873..49536d79b88 100644
--- a/addons/note_pad/models/note.py
+++ b/addons/note_pad/models/note.py
@@ -11,4 +11,4 @@ class NotePad(models.Model):
_pad_fields = ['note_pad']
- note_pad_url = fields.Char('Pad Url', pad_content_field='memo')
+ note_pad_url = fields.Char('Pad Url', pad_content_field='memo', copy=False)
diff --git a/addons/pad/models/pad.py b/addons/pad/models/pad.py
index 0a90175bdc5..deb920ec69e 100644
--- a/addons/pad/models/pad.py
+++ b/addons/pad/models/pad.py
@@ -101,16 +101,19 @@ class PadCommon(models.AbstractModel):
@api.multi
def write(self, vals):
- self._set_pad_value(vals)
+ self._set_field_to_pad(vals)
+ self._set_pad_to_field(vals)
return super(PadCommon, self).write(vals)
@api.model
def create(self, vals):
- self._set_pad_value(vals)
+ # Case of a regular creation: we receive the pad url, so we need to update the
+ # corresponding field
+ self._set_pad_to_field(vals)
pad = super(PadCommon, self).create(vals)
- # In case the pad is created programmatically, the content is not filled in yet since it is
- # normally initialized by the JS layer
+ # Case of a programmatical creation (e.g. copy): we receive the field content, so we need
+ # to create the corresponding pad
if self.env.context.get('pad_no_create', False):
return pad
for k, field in self._fields.items():
@@ -124,9 +127,7 @@ class PadCommon(models.AbstractModel):
pad[k] = pad_info.get('url')
return pad
- # Set the pad content in vals
- def _set_pad_value(self, vals):
-
+ def _set_field_to_pad(self, vals):
# Update the pad if the `pad_content_field` is modified
for k, field in self._fields.items():
if hasattr(field, 'pad_content_field') and vals.get(field.pad_content_field) and self[k]:
@@ -135,23 +136,9 @@ class PadCommon(models.AbstractModel):
path = self[k].split('/p/')[1]
myPad.setHtmlFallbackText(path, vals[field.pad_content_field])
+ def _set_pad_to_field(self, vals):
# Update the `pad_content_field` if the pad is modified
for k, v in list(vals.items()):
field = self._fields.get(k)
if hasattr(field, 'pad_content_field'):
vals[field.pad_content_field] = self.pad_get_content(v)
-
- @api.multi
- @api.returns('self', lambda value: value.id)
- def copy(self, default=None):
- self.ensure_one()
- if not default:
- default = {}
- for k, field in self._fields.items():
- if hasattr(field, 'pad_content_field'):
- if self.env.context.get('pad_no_create', False):
- default[k] = ''
- else:
- pad = self.pad_generate_url()
- default[k] = pad.get('url')
- return super(PadCommon, self).copy(default)
diff --git a/addons/pad_project/models/project.py b/addons/pad_project/models/project.py
index 6062135f7ea..f994b3a5ea7 100644
--- a/addons/pad_project/models/project.py
+++ b/addons/pad_project/models/project.py
@@ -8,7 +8,7 @@ class ProjectTask(models.Model):
_name = "project.task"
_inherit = ["project.task", 'pad.common']
- description_pad = fields.Char('Pad URL', pad_content_field='description')
+ description_pad = fields.Char('Pad URL', pad_content_field='description', copy=False)
use_pad = fields.Boolean(related="project_id.use_pads", string="Use collaborative pad")
@api.model
@@ -19,13 +19,6 @@ class ProjectTask(models.Model):
self = self.with_context(pad_no_create=True)
return super(ProjectTask, self).create(vals)
- @api.multi
- @api.returns('self', lambda value: value.id)
- def copy(self, default=None):
- if not self.use_pad:
- self = self.with_context(pad_no_create=True)
- return super(ProjectTask, self).copy(default)
-
class ProjectProject(models.Model):
_inherit = "project.project"
diff --git a/addons/payment/controllers/portal.py b/addons/payment/controllers/portal.py
index 444d936429b..7e023e01c7a 100644
--- a/addons/payment/controllers/portal.py
+++ b/addons/payment/controllers/portal.py
@@ -73,7 +73,8 @@ class WebsitePayment(http.Controller):
return tx
- @http.route(['/website_payment/transaction/<string:reference>/<string:amount>/<string:currency_id>',], type='json', auth='public')
+ @http.route(['/website_payment/transaction/<string:reference>/<string:amount>/<string:currency_id>',
+ '/website_payment/transaction/v2/<string:amount>/<string:currency_id>/<path:reference>',], type='json', auth='public')
def transaction(self, acquirer_id, reference, amount, currency_id, **kwargs):
partner_id = request.env.user.partner_id.id if not request.env.user._is_public() else False
acquirer = request.env['payment.acquirer'].browse(acquirer_id)
@@ -100,7 +101,8 @@ class WebsitePayment(http.Controller):
return acquirer.sudo().render(reference, float(amount), int(currency_id), values=render_values)
- @http.route(['/website_payment/token/<string:reference>/<float:amount>/<int:currency_id>'], type='http', auth='public', website=True)
+ @http.route(['/website_payment/token/<string:reference>/<string:amount>/<string:currency_id>',
+ '/website_payment/token/v2/<string:amount>/<string:currency_id>/<path:reference>'], type='http', auth='public', website=True)
def payment_token(self, pm_id, reference, amount, currency_id, return_url=None, **kwargs):
token = request.env['payment.token'].browse(int(pm_id))
diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py
index 879c7b60d08..d68df6f9934 100644
--- a/addons/payment/models/payment_acquirer.py
+++ b/addons/payment/models/payment_acquirer.py
@@ -78,8 +78,8 @@ class PaymentAcquirer(models.Model):
help="Capture the amount from Odoo, when the delivery is completed.")
# Formerly associated to `generate_and_pay_invoice` option from auto_confirm
journal_id = fields.Many2one(
- 'account.journal', 'Payment Journal', domain=[('type', '=', 'bank')],
- default=lambda self: self.env['account.journal'].search([('type', '=', 'bank')], limit=1),
+ 'account.journal', 'Payment Journal', domain=[('type', 'in', ['bank', 'cash'])],
+ default=lambda self: self.env['account.journal'].search([('type', 'in', ['bank', 'cash'])], limit=1),
help="""Payments will be registered into this journal. If you get paid straight on your bank account,
select your bank account. If you get paid in batch for several transactions, create a specific
payment journal for this payment acquirer to easily manage the bank reconciliation. You hold
@@ -200,22 +200,13 @@ class PaymentAcquirer(models.Model):
@api.model
def create(self, vals):
image_resize_images(vals)
- vals = self._check_journal_id(vals)
return super(PaymentAcquirer, self).create(vals)
@api.multi
def write(self, vals):
image_resize_images(vals)
- vals = self._check_journal_id(vals)
return super(PaymentAcquirer, self).write(vals)
- def _check_journal_id(self, vals):
- if not vals.get('journal_id', False):
- default_journal = self.env['account.journal'].search([('type', '=', 'bank')], limit=1)
- if default_journal:
- vals.update({'journal_id': default_journal.id})
- return vals
-
@api.multi
def toggle_website_published(self):
self.write({'website_published': not self.website_published})
diff --git a/addons/payment/security/ir.model.access.csv b/addons/payment/security/ir.model.access.csv
index 2982f1ab0c0..76d11991b88 100644
--- a/addons/payment/security/ir.model.access.csv
+++ b/addons/payment/security/ir.model.access.csv
@@ -1,6 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
payment_acquirer_all,payment.acquirer.all,model_payment_acquirer,,1,0,0,0
-payment_acquirer_user,payment.acquirer.user,model_payment_acquirer,base.group_user,1,1,1,0
payment_acquirer_system,payment.acquirer.system,model_payment_acquirer,base.group_system,1,1,1,1
payment_transaction_all,payment.transaction.all,model_payment_transaction,,1,0,0,0
payment_transaction_user,payment.transaction.user,model_payment_transaction,base.group_user,1,1,1,0
diff --git a/addons/payment/views/payment_portal_templates.xml b/addons/payment/views/payment_portal_templates.xml
index 166cd429718..2224eeec179 100644
--- a/addons/payment/views/payment_portal_templates.xml
+++ b/addons/payment/views/payment_portal_templates.xml
@@ -55,8 +55,8 @@
<p><b>Amount:</b> <t t-esc="amount" t-options="{'widget': 'monetary', 'display_currency': currency}"/></p>
<t t-call="payment.payment_tokens_list" t-if="reference and amount and currency">
<t t-set="mode" t-value="'payment'"/>
- <t t-set="prepare_tx_url" t-value="'/website_payment/transaction/' + reference + '/' + str(amount) + '/' + str(currency.id)"/>
- <t t-set="form_action" t-value="'/website_payment/token/' + reference + '/' + str(amount) + '/' + str(currency.id)"/>
+ <t t-set="prepare_tx_url" t-value="'/website_payment/transaction/v2/' + str(amount) + '/' + str(currency.id) + '/' + reference"/>
+ <t t-set="form_action" t-value="'/website_payment/token/v2/' + str(amount) + '/' + str(currency.id) + '/' + reference"/>
</t>
<div t-if="not (s2s_acquirers or form_acquirers)" class="alert alert-danger">
<p>No payment acquirer found.</p>
diff --git a/addons/payment/views/payment_views.xml b/addons/payment/views/payment_views.xml
index 2a72f94978b..f3c8c0af16d 100644
--- a/addons/payment/views/payment_views.xml
+++ b/addons/payment/views/payment_views.xml
@@ -81,7 +81,7 @@
</page>
<page string="Configuration">
<group name="acquirer_config">
- <field name="journal_id" context="{'default_type': 'bank'}"/>
+ <field name="journal_id" context="{'default_type': 'bank'}" required="1"/>
<field name="capture_manually" attrs="{'invisible': [('authorize_implemented', '=', False)]}"/>
<field name="save_token" widget="radio" attrs="{'invisible': ['|', ('token_implemented', '=', False), ('payment_flow', '=', 's2s')]}"/>
<field name="fees_active" attrs="{'invisible': [('fees_implemented', '=', False)]}"/>
diff --git a/addons/payment_ogone/controllers/main.py b/addons/payment_ogone/controllers/main.py
index 8680e99fda5..1d03a688630 100644
--- a/addons/payment_ogone/controllers/main.py
+++ b/addons/payment_ogone/controllers/main.py
@@ -2,6 +2,7 @@
import logging
import pprint
import werkzeug
+from werkzeug.urls import url_unquote_plus
from odoo import http
from odoo.http import request
@@ -26,7 +27,7 @@ class OgoneController(http.Controller):
""" Ogone contacts using GET, at least for accept """
_logger.info('Ogone: entering form_feedback with post data %s', pprint.pformat(post)) # debug
request.env['payment.transaction'].sudo().form_feedback(post, 'ogone')
- return werkzeug.utils.redirect(post.pop('return_url', '/'))
+ return werkzeug.utils.redirect(url_unquote_plus(post.pop('return_url', '/')))
@http.route(['/payment/ogone/s2s/create_json'], type='json', auth='public', csrf=False)
def ogone_s2s_create_json(self, **kwargs):
diff --git a/addons/payment_ogone/models/payment.py b/addons/payment_ogone/models/payment.py
index 09fe0ef8a39..09df4b52bc4 100644
--- a/addons/payment_ogone/models/payment.py
+++ b/addons/payment_ogone/models/payment.py
@@ -15,9 +15,8 @@ from odoo import api, fields, models, _
from odoo.addons.payment.models.payment_acquirer import ValidationError
from odoo.addons.payment_ogone.controllers.main import OgoneController
from odoo.addons.payment_ogone.data import ogone
-from odoo.tools import float_round, DEFAULT_SERVER_DATE_FORMAT
-from odoo.tools.float_utils import float_compare, float_repr
-from odoo.tools.safe_eval import safe_eval
+from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, ustr
+from odoo.tools.float_utils import float_compare, float_repr, float_round
_logger = logging.getLogger(__name__)
@@ -151,6 +150,9 @@ class PaymentAcquirerOgone(models.Model):
def ogone_form_generate_values(self, values):
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
ogone_tx_values = dict(values)
+ param_plus = {
+ 'return_url': ogone_tx_values.pop('return_url', False)
+ }
temp_ogone_tx_values = {
'PSPID': self.ogone_pspid,
'ORDERID': values['reference'],
@@ -168,7 +170,7 @@ class PaymentAcquirerOgone(models.Model):
'DECLINEURL': urls.url_join(base_url, OgoneController._decline_url),
'EXCEPTIONURL': urls.url_join(base_url, OgoneController._exception_url),
'CANCELURL': urls.url_join(base_url, OgoneController._cancel_url),
- 'PARAMPLUS': 'return_url=%s' % ogone_tx_values.pop('return_url') if ogone_tx_values.get('return_url') else False,
+ 'PARAMPLUS': url_encode(param_plus),
}
if self.save_token in ['ask', 'always']:
temp_ogone_tx_values.update({
@@ -211,8 +213,8 @@ class PaymentAcquirerOgone(models.Model):
class PaymentTxOgone(models.Model):
_inherit = 'payment.transaction'
# ogone status
- _ogone_valid_tx_status = [5, 9]
- _ogone_wait_tx_status = [41, 50, 51, 52, 55, 56, 91, 92, 99]
+ _ogone_valid_tx_status = [5, 9, 8]
+ _ogone_wait_tx_status = [41, 50, 51, 52, 55, 56, 91, 92, 99, 81, 82]
_ogone_pending_tx_status = [46] # 3DS HTML response
_ogone_cancel_tx_status = [1]
@@ -411,13 +413,12 @@ class PaymentTxOgone(models.Model):
'ORDERID': reference,
'AMOUNT': int(self.amount * 100),
'CURRENCY': self.currency_id.name,
- 'OPERATION': 'RFD',
- 'ECI': 2, # Recurring (from MOTO)
- 'ALIAS': self.payment_token_id.acquirer_ref,
- 'RTIMEOUT': 30,
+ 'OPERATION': 'RFS',
+ 'PAYID': self.acquirer_reference,
}
+ data['SHASIGN'] = self.acquirer_id._ogone_generate_shasign('in', data)
- direct_order_url = 'https://secure.ogone.com/ncol/%s/orderdirect.asp' % (self.acquirer_id.environment)
+ direct_order_url = 'https://secure.ogone.com/ncol/%s/maintenancedirect.asp' % (self.acquirer_id.environment)
logged_data = data.copy()
logged_data.pop('PSWD')
@@ -476,7 +477,7 @@ class PaymentTxOgone(models.Model):
self.write({
'state': new_state,
'acquirer_reference': tree.get('PAYID'),
- 'html_3ds': base64.b64decode(tree.HTML_ANSWER.decode('ascii')),
+ 'html_3ds': ustr(base64.b64decode(tree.HTML_ANSWER.text)),
})
elif status in self._ogone_wait_tx_status and tries > 0:
time.sleep(0.5)
@@ -585,4 +586,4 @@ class PaymentToken(models.Model):
'acquirer_ref': alias,
'name': 'XXXXXXXXXXXX%s - %s' % (values['cc_number'][-4:], values['cc_holder_name'])
}
- return {} \ No newline at end of file
+ return {}
diff --git a/addons/payment_stripe/static/src/js/stripe.js b/addons/payment_stripe/static/src/js/stripe.js
index 7ecf25284f6..e4474fe72ce 100644
--- a/addons/payment_stripe/static/src/js/stripe.js
+++ b/addons/payment_stripe/static/src/js/stripe.js
@@ -109,8 +109,8 @@ odoo.define('payment_stripe.stripe', function(require) {
if ($('.o_website_payment').length !== 0) {
var invoice_num = $("input[name='invoice_num']").val();
- var url = _.str.sprintf("/website_payment/transaction/%s/%f/%s",
- invoice_num, amount, currency_id);
+ var url = _.str.sprintf("/website_payment/transaction/v2/%f/%s/%s",
+ amount, currency_id, invoice_num);
var create_tx = ajax.jsonRpc(url, 'call', {
acquirer_id: acquirer_id
diff --git a/addons/point_of_sale/README.md b/addons/point_of_sale/README.md
index d45c48a84ff..dc00e4a8f40 100644
--- a/addons/point_of_sale/README.md
+++ b/addons/point_of_sale/README.md
@@ -52,7 +52,7 @@ designed for modern retailer.
### Designed for Productivity
-Whether it's for a restaurant or a shop, you can activate the multiple tickets
+Whether it's for a restaurant or a shop, you can activate the multiple orders
in parallel to not make your customers wait.
### Blazing fast search
diff --git a/addons/point_of_sale/data/point_of_sale_demo.xml b/addons/point_of_sale/data/point_of_sale_demo.xml
index 701b4ce2e13..7da2d82079b 100644
--- a/addons/point_of_sale/data/point_of_sale_demo.xml
+++ b/addons/point_of_sale/data/point_of_sale_demo.xml
@@ -51,6 +51,7 @@
<field name="available_in_pos">True</field>
<field name="list_price">2.83</field>
<field name="name">Orange Butterfly</field>
+ <field name="taxes_id">False</field>
<field name="pos_categ_id" ref="fruits_vegetables"/>
<field name="to_weight">True</field>
<field name="uom_id" ref="uom.product_uom_kgm" />
diff --git a/addons/point_of_sale/models/pos_config.py b/addons/point_of_sale/models/pos_config.py
index e4775d4a4e3..1b41565401d 100644
--- a/addons/point_of_sale/models/pos_config.py
+++ b/addons/point_of_sale/models/pos_config.py
@@ -64,7 +64,8 @@ class PosConfig(models.Model):
return self.env['ir.qweb'].render('point_of_sale.customer_facing_display_html')
name = fields.Char(string='Point of Sale Name', index=True, required=True, help="An internal identification of the point of sale.")
- is_installed_account_accountant = fields.Boolean(compute="_compute_is_installed_account_accountant")
+ is_installed_account_accountant = fields.Boolean(string="Is the Full Accounting Installed",
+ compute="_compute_is_installed_account_accountant")
journal_ids = fields.Many2many(
'account.journal', 'pos_config_journal_rel',
'pos_config_id', 'journal_id', string='Available Payment Methods',
diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py
index f9604dbb1e6..d29dbadb5a1 100644
--- a/addons/point_of_sale/models/pos_order.py
+++ b/addons/point_of_sale/models/pos_order.py
@@ -160,13 +160,14 @@ class PosOrder(models.Model):
"""
Prepare the dict of values to create the new invoice for a pos order.
"""
+ invoice_type = 'out_invoice' if self.amount_total >= 0 else 'out_refund'
return {
'name': self.name,
'origin': self.name,
'account_id': self.partner_id.property_account_receivable_id.id,
'journal_id': self.session_id.config_id.invoice_journal_id.id,
'company_id': self.company_id.id,
- 'type': 'out_invoice',
+ 'type': invoice_type,
'reference': self.name,
'partner_id': self.partner_id.id,
'comment': self.note or '',
@@ -208,7 +209,7 @@ class PosOrder(models.Model):
inv_line = {
'invoice_id': invoice_id,
'product_id': line.product_id.id,
- 'quantity': line.qty,
+ 'quantity': line.qty if self.amount_total >= 0 else -line.qty,
'account_analytic_id': self._prepare_analytic_account(line),
'name': inv_name,
}
@@ -732,7 +733,7 @@ class PosOrder(models.Model):
# when the pos.config has no picking_type_id set only the moves will be created
if moves and not return_picking and not order_picking:
moves._action_assign()
- moves.filtered(lambda m: m.state in ['confirmed', 'waiting']).force_assign()
+ moves.filtered(lambda m: m.state in ['confirmed', 'waiting'])._force_assign()
moves.filtered(lambda m: m.product_id.tracking == 'none')._action_done()
return True
@@ -792,7 +793,7 @@ class PosOrder(models.Model):
'location_dest_id': move.location_dest_id.id,
'lot_id': lot_id,
})
- if not pack_lots:
+ if not pack_lots and not float_is_zero(qty_done, precision_rounding=move.product_uom.rounding):
move.quantity_done = qty_done
return has_wrong_lots
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index dba6b7fb0c9..591488c225c 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -1305,6 +1305,7 @@ exports.Orderline = Backbone.Model.extend({
this.type = 'unit';
this.selected = false;
this.id = orderline_id++;
+ this.price_manually_set = false;
if (options.price) {
this.set_unit_price(options.price);
@@ -1345,6 +1346,7 @@ exports.Orderline = Backbone.Model.extend({
orderline.price = this.price;
orderline.type = this.type;
orderline.selected = false;
+ orderline.price_manually_set = this.price_manually_set;
return orderline;
},
set_product_lot: function(product){
@@ -1395,7 +1397,7 @@ exports.Orderline = Backbone.Model.extend({
}
// just like in sale.order changing the quantity will recompute the unit price
- if(! keep_price){
+ if(! keep_price && ! this.price_manually_set){
this.set_unit_price(this.product.get_price(this.order.pricelist, this.get_quantity()));
this.order.fix_tax_included_price(this);
}
@@ -2232,7 +2234,11 @@ exports.Order = Backbone.Model.extend({
set_pricelist: function (pricelist) {
var self = this;
this.pricelist = pricelist;
- _.each(this.get_orderlines(), function (line) {
+
+ var lines_to_recompute = _.filter(this.get_orderlines(), function (line) {
+ return ! line.price_manually_set;
+ });
+ _.each(lines_to_recompute, function (line) {
line.set_unit_price(line.product.get_price(self.pricelist, line.get_quantity()));
self.fix_tax_included_price(line);
});
@@ -2355,7 +2361,7 @@ exports.Order = Backbone.Model.extend({
this.assert_editable();
var newPaymentline = new exports.Paymentline({},{order: this, cashregister:cashregister, pos: this.pos});
if(cashregister.journal.type !== 'cash' || this.pos.config.iface_precompute_cash){
- newPaymentline.set_amount( Math.max(this.get_due(),0) );
+ newPaymentline.set_amount( this.get_due() );
}
this.paymentlines.add(newPaymentline);
this.select_paymentline(newPaymentline);
@@ -2519,10 +2525,10 @@ exports.Order = Backbone.Model.extend({
}
}
}
- return round_pr(Math.max(0,due), this.pos.currency.rounding);
+ return round_pr(due, this.pos.currency.rounding);
},
is_paid: function(){
- return this.get_due() === 0;
+ return this.get_due() <= 0;
},
is_paid_with_cash: function(){
return !!this.paymentlines.find( function(pl){
diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js
index dadd9cffbb8..0b2d94dd71f 100644
--- a/addons/point_of_sale/static/src/js/screens.js
+++ b/addons/point_of_sale/static/src/js/screens.js
@@ -502,7 +502,9 @@ var OrderWidget = PosBaseWidget.extend({
}else if( mode === 'discount'){
order.get_selected_orderline().set_discount(val);
}else if( mode === 'price'){
- order.get_selected_orderline().set_unit_price(val);
+ var selected_orderline = order.get_selected_orderline();
+ selected_orderline.price_manually_set = true;
+ selected_orderline.set_unit_price(val);
}
}
},
@@ -1973,17 +1975,6 @@ var PaymentScreenWidget = ScreenWidget.extend({
return false;
}
- var plines = order.get_paymentlines();
- for (var i = 0; i < plines.length; i++) {
- if (plines[i].get_type() === 'bank' && plines[i].get_amount() < 0) {
- this.gui.show_popup('error',{
- 'message': _t('Negative Bank Payment'),
- 'comment': _t('You cannot have a negative amount in a Bank payment. Use a cash payment method to return money to the customer.'),
- });
- return false;
- }
- }
-
if (!order.is_paid() || this.invoicing) {
return false;
}
diff --git a/addons/point_of_sale/static/src/js/tests.js b/addons/point_of_sale/static/src/js/tests.js
index 6c5c970f545..0d303df8c7e 100644
--- a/addons/point_of_sale/static/src/js/tests.js
+++ b/addons/point_of_sale/static/src/js/tests.js
@@ -167,14 +167,27 @@ odoo.define('point_of_sale.tour.pricelist', function (require) {
content: "verify that unit price of oranges changed to $1",
trigger: ".total > .value:contains('$ 2.00')",
}, {
+ content: "order different oranges",
+ trigger: ".product:contains('Orange Butterfly')",
+ }, {
+ content: "change to price mode",
+ trigger: ".numpad button:contains('Price')",
+ }, {
+ content: "manually override the unit price of these oranges to $5",
+ trigger: ".numpad button.input-button:visible:contains('5')",
+ }, {
+ content: "change back to qty mode",
+ trigger: ".numpad button:contains('Qty')",
+ }, {
content: "click pricelist button",
trigger: ".control-button.o_pricelist_button",
}, {
content: "select public pricelist",
trigger: ".selection-item:contains('Public Pricelist')",
}, {
- content: "verify that order has been recomputed",
- trigger: ".total > .value:contains('$ 3.96')",
+ content: "verify that the boni oranges have been recomputed and the\
+butterfly oranges have not (their price was manually overriden)",
+ trigger: ".total > .value:contains('$ 8.96')",
}, {
content: "click pricelist button",
trigger: ".control-button.o_pricelist_button",
diff --git a/addons/point_of_sale/views/account_journal_view.xml b/addons/point_of_sale/views/account_journal_view.xml
index 11cff71da4d..5d79164a30b 100644
--- a/addons/point_of_sale/views/account_journal_view.xml
+++ b/addons/point_of_sale/views/account_journal_view.xml
@@ -45,4 +45,13 @@
<field name="context">{'search_default_available_in_pos': 1, 'default_type': 'cash'}</field>
</record>
+
+ <!-- View Taxes -->
+
+ <menuitem id="menu_action_tax_form_open"
+ action="account.action_tax_form"
+ parent="point_of_sale.menu_point_config_product"
+ groups="base.group_no_one"
+ sequence="40"/>
+
</odoo>
diff --git a/addons/portal/controllers/mail.py b/addons/portal/controllers/mail.py
index 3a59ecd70b3..b8ff6f8f2fd 100644
--- a/addons/portal/controllers/mail.py
+++ b/addons/portal/controllers/mail.py
@@ -5,6 +5,7 @@ from werkzeug.exceptions import NotFound, Forbidden
from odoo import http
from odoo.http import request
+from odoo.osv import expression
from odoo.tools import consteq
@@ -95,6 +96,9 @@ class PortalChatter(http.Controller):
access_as_sudo = _has_token_access(res_model, res_id, token=kw.get('token'))
if not access_as_sudo: # if token is not correct, raise Forbidden
raise Forbidden()
+ # Non-employee see only messages with not internal subtype (aka, no internal logs)
+ if not request.env['res.users'].has_group('base.group_user'):
+ domain = expression.AND([['&', '&', ('subtype_id', '!=', False), ('subtype_id.internal', '=', False)], domain])
Message = request.env['mail.message'].sudo()
return {
'messages': Message.search(domain, limit=limit, offset=offset).portal_message_format(),
diff --git a/addons/portal/static/src/js/portal_chatter.js b/addons/portal/static/src/js/portal_chatter.js
index 66414d70b5d..c2f0e5fb49b 100644
--- a/addons/portal/static/src/js/portal_chatter.js
+++ b/addons/portal/static/src/js/portal_chatter.js
@@ -6,6 +6,7 @@ var ajax = require('web.ajax');
var core = require('web.core');
var Widget = require('web.Widget');
var rpc = require('web.rpc');
+var time = require('web.time');
var qweb = core.qweb;
var _t = core._t;
@@ -105,7 +106,7 @@ var PortalChatter = Widget.extend({
preprocessMessages: function(messages){
_.each(messages, function(m){
m['author_avatar_url'] = _.str.sprintf('/web/image/%s/%s/author_avatar/50x50', 'mail.message', m.id);
- m['published_date_str'] = _.str.sprintf(_t('Published on %s'), moment(m.date).format('MMMM Do YYYY, h:mm:ss a'));
+ m['published_date_str'] = _.str.sprintf(_t('Published on %s'), moment(time.str_to_datetime(m.date)).format('MMMM Do YYYY, h:mm:ss a'));
});
return messages;
},
diff --git a/addons/pos_discount/models/pos_config.py b/addons/pos_discount/models/pos_config.py
index 0c21d482f17..46272218f67 100644
--- a/addons/pos_discount/models/pos_config.py
+++ b/addons/pos_discount/models/pos_config.py
@@ -7,17 +7,18 @@ from odoo import api, fields, models
class PosConfig(models.Model):
_inherit = 'pos.config'
- def _get_default_discount_product(self):
- return self.env.ref('point_of_sale.product_product_consumable')
-
iface_discount = fields.Boolean(string='Order Discounts', help='Allow the cashier to give discounts on the whole order.')
- discount_pc = fields.Float(string='Discount Percentage', default=10, help='The default discount percentage')
- discount_product_id = fields.Many2one('product.product', string='Discount Product', domain="[('available_in_pos', '=', True)]", help='The product used to model the discount.', default=_get_default_discount_product)
+ discount_pc = fields.Float(string='Discount Percentage', help='The default discount percentage')
+ discount_product_id = fields.Many2one('product.product', string='Discount Product',
+ domain="[('available_in_pos', '=', True), ('sale_ok', '=', True)]", help='The product used to model the discount.')
@api.onchange('module_pos_discount')
def _onchange_module_pos_discount(self):
if self.module_pos_discount:
- self.discount_product_id = self.env['product.product'].search([('available_in_pos', '=', True)], limit=1)
+ self.discount_product_id = self.env.ref('point_of_sale.product_product_consumable', raise_if_not_found=False)
+ if not self.discount_product_id or not self.discount_product_id.available_in_pos or not self.discount_product_id.sale_ok:
+ domain = [('available_in_pos', '=', True), ('sale_ok', '=', True)]
+ self.discount_product_id = self.env['product.product'].search(domain, limit=1)
self.discount_pc = 10.0
else:
self.discount_product_id = False
diff --git a/addons/pos_discount/static/src/js/discount.js b/addons/pos_discount/static/src/js/discount.js
index e77e4559e0c..771992bb01b 100644
--- a/addons/pos_discount/static/src/js/discount.js
+++ b/addons/pos_discount/static/src/js/discount.js
@@ -23,6 +23,13 @@ var DiscountButton = screens.ActionButtonWidget.extend({
var order = this.pos.get_order();
var lines = order.get_orderlines();
var product = this.pos.db.get_product_by_id(this.pos.config.discount_product_id[0]);
+ if (product === undefined) {
+ this.gui.show_popup('error', {
+ title : _t("No discount product found"),
+ body : _t("The discount product seems misconfigured. Make sure it is flagged as 'Can be Sold' and 'Available in Point of Sale'."),
+ });
+ return;
+ }
// Remove existing discounts
var i = 0;
diff --git a/addons/pos_mercury/static/src/js/pos_mercury.js b/addons/pos_mercury/static/src/js/pos_mercury.js
index 710420c62b4..e403b9bcf12 100644
--- a/addons/pos_mercury/static/src/js/pos_mercury.js
+++ b/addons/pos_mercury/static/src/js/pos_mercury.js
@@ -146,82 +146,82 @@ var lookUpCodeTransaction = {
'000000': _t('Transaction approved'),
},
'TimeoutError': {
- '001006': _t('Global API Not Initialized'),
- '001007': _t('Timeout on Response'),
- '003003': _t('Socket Error sending request'),
- '003004': _t('Socket already open or in use'),
- '003005': _t('Socket Creation Failed'),
- '003006': _t('Socket Connection Failed'),
- '003007': _t('Connection Lost'),
- '003008': _t('TCP/IP Failed to Initialize'),
- '003010': _t('Time Out waiting for server response'),
- '003011': _t('Connect Canceled'),
- '003053': _t('Initialize Failed'),
- '009999': _t('Unknown Error'),
+ '001006': 'Global API Not Initialized',
+ '001007': 'Timeout on Response',
+ '003003': 'Socket Error sending request',
+ '003004': 'Socket already open or in use',
+ '003005': 'Socket Creation Failed',
+ '003006': 'Socket Connection Failed',
+ '003007': 'Connection Lost',
+ '003008': 'TCP/IP Failed to Initialize',
+ '003010': 'Time Out waiting for server response',
+ '003011': 'Connect Canceled',
+ '003053': 'Initialize Failed',
+ '009999': 'Unknown Error',
},
'FatalError': {
- '-1': _t('Timeout error'),
- '001001': _t('General Failure'),
- '001003': _t('Invalid Command Format'),
- '001004': _t('Insufficient Fields'),
- '001011': _t('Empty Command String'),
- '002000': _t('Password Verified'),
- '002001': _t('Queue Full'),
- '002002': _t('Password Failed – Disconnecting'),
- '002003': _t('System Going Offline'),
- '002004': _t('Disconnecting Socket'),
- '002006': _t('Refused ‘Max Connections’'),
- '002008': _t('Duplicate Serial Number Detected'),
- '002009': _t('Password Failed (Client / Server)'),
- '002010': _t('Password failed (Challenge / Response)'),
- '002011': _t('Internal Server Error – Call Provider'),
- '003002': _t('In Process with server'),
- '003009': _t('Control failed to find branded serial (password lookup failed)'),
- '003012': _t('128 bit CryptoAPI failed'),
- '003014': _t('Threaded Auth Started Expect Response'),
- '003017': _t('Failed to start Event Thread.'),
- '003050': _t('XML Parse Error'),
- '003051': _t('All Connections Failed'),
- '003052': _t('Server Login Failed'),
- '004001': _t('Global Response Length Error (Too Short)'),
- '004002': _t('Unable to Parse Response from Global (Indistinguishable)'),
- '004003': _t('Global String Error'),
- '004004': _t('Weak Encryption Request Not Supported'),
- '004005': _t('Clear Text Request Not Supported'),
- '004010': _t('Unrecognized Request Format'),
- '004011': _t('Error Occurred While Decrypting Request'),
- '004017': _t('Invalid Check Digit'),
- '004018': _t('Merchant ID Missing'),
- '004019': _t('TStream Type Missing'),
- '004020': _t('Could Not Encrypt Response- Call Provider'),
- '100201': _t('Invalid Transaction Type'),
- '100202': _t('Invalid Operator ID'),
- '100203': _t('Invalid Memo'),
- '100204': _t('Invalid Account Number'),
- '100205': _t('Invalid Expiration Date'),
- '100206': _t('Invalid Authorization Code'),
- '100207': _t('Invalid Authorization Code'),
- '100208': _t('Invalid Authorization Amount'),
- '100209': _t('Invalid Cash Back Amount'),
- '100210': _t('Invalid Gratuity Amount'),
- '100211': _t('Invalid Purchase Amount'),
- '100212': _t('Invalid Magnetic Stripe Data'),
- '100213': _t('Invalid PIN Block Data'),
- '100214': _t('Invalid Derived Key Data'),
- '100215': _t('Invalid State Code'),
- '100216': _t('Invalid Date of Birth'),
- '100217': _t('Invalid Check Type'),
- '100218': _t('Invalid Routing Number'),
- '100219': _t('Invalid TranCode'),
- '100220': _t('Invalid Merchant ID'),
- '100221': _t('Invalid TStream Type'),
- '100222': _t('Invalid Batch Number'),
- '100223': _t('Invalid Batch Item Count'),
- '100224': _t('Invalid MICR Input Type'),
- '100225': _t('Invalid Driver’s License'),
- '100226': _t('Invalid Sequence Number'),
- '100227': _t('Invalid Pass Data'),
- '100228': _t('Invalid Card Type'),
+ '-1': 'Timeout error',
+ '001001': 'General Failure',
+ '001003': 'Invalid Command Format',
+ '001004': 'Insufficient Fields',
+ '001011': 'Empty Command String',
+ '002000': 'Password Verified',
+ '002001': 'Queue Full',
+ '002002': 'Password Failed – Disconnecting',
+ '002003': 'System Going Offline',
+ '002004': 'Disconnecting Socket',
+ '002006': 'Refused ‘Max Connections’',
+ '002008': 'Duplicate Serial Number Detected',
+ '002009': 'Password Failed (Client / Server)',
+ '002010': 'Password failed (Challenge / Response)',
+ '002011': 'Internal Server Error – Call Provider',
+ '003002': 'In Process with server',
+ '003009': 'Control failed to find branded serial (password lookup failed)',
+ '003012': '128 bit CryptoAPI failed',
+ '003014': 'Threaded Auth Started Expect Response',
+ '003017': 'Failed to start Event Thread.',
+ '003050': 'XML Parse Error',
+ '003051': 'All Connections Failed',
+ '003052': 'Server Login Failed',
+ '004001': 'Global Response Length Error (Too Short)',
+ '004002': 'Unable to Parse Response from Global (Indistinguishable)',
+ '004003': 'Global String Error',
+ '004004': 'Weak Encryption Request Not Supported',
+ '004005': 'Clear Text Request Not Supported',
+ '004010': 'Unrecognized Request Format',
+ '004011': 'Error Occurred While Decrypting Request',
+ '004017': 'Invalid Check Digit',
+ '004018': 'Merchant ID Missing',
+ '004019': 'TStream Type Missing',
+ '004020': 'Could Not Encrypt Response- Call Provider',
+ '100201': 'Invalid Transaction Type',
+ '100202': 'Invalid Operator ID',
+ '100203': 'Invalid Memo',
+ '100204': 'Invalid Account Number',
+ '100205': 'Invalid Expiration Date',
+ '100206': 'Invalid Authorization Code',
+ '100207': 'Invalid Authorization Code',
+ '100208': 'Invalid Authorization Amount',
+ '100209': 'Invalid Cash Back Amount',
+ '100210': 'Invalid Gratuity Amount',
+ '100211': 'Invalid Purchase Amount',
+ '100212': 'Invalid Magnetic Stripe Data',
+ '100213': 'Invalid PIN Block Data',
+ '100214': 'Invalid Derived Key Data',
+ '100215': 'Invalid State Code',
+ '100216': 'Invalid Date of Birth',
+ '100217': 'Invalid Check Type',
+ '100218': 'Invalid Routing Number',
+ '100219': 'Invalid TranCode',
+ '100220': 'Invalid Merchant ID',
+ '100221': 'Invalid TStream Type',
+ '100222': 'Invalid Batch Number',
+ '100223': 'Invalid Batch Item Count',
+ '100224': 'Invalid MICR Input Type',
+ '100225': 'Invalid Driver’s License',
+ '100226': 'Invalid Sequence Number',
+ '100227': 'Invalid Pass Data',
+ '100228': 'Invalid Card Type',
},
};
// Popup to show all transaction state for the payment.
@@ -339,6 +339,15 @@ PaymentScreenWidget.include({
// Handler to manage the card reader string
credit_code_transaction: function (parsed_result, old_deferred, retry_nr) {
+ var order = this.pos.get_order();
+ if (order.get_due(order.selected_paymentline) < 0) {
+ this.gui.show_popup('error',{
+ 'title': _t('Refunds not supported'),
+ 'body': _t('Credit card refunds are not supported. Instead select your credit card payment method, click \'Validate\' and refund the original charge manually through the Vantiv backend.'),
+ });
+ return;
+ }
+
if(this.pos.getOnlinePaymentJournals().length === 0) {
return;
}
@@ -643,9 +652,11 @@ PaymentScreenWidget.include({
});
} else {
this._super(id);
- order.selected_paymentline.mercury_swipe_pending = true;
- this.render_paymentlines();
- order.trigger('change', order); // needed so that export_to_JSON gets triggered
+ if (order.get_due(order.selected_paymentline) > 0) {
+ order.selected_paymentline.mercury_swipe_pending = true;
+ this.render_paymentlines();
+ order.trigger('change', order); // needed so that export_to_JSON gets triggered
+ }
}
} else {
this._super(id);
diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py
index b4ec38f05db..166a0a44506 100644
--- a/addons/product/models/product_template.py
+++ b/addons/product/models/product_template.py
@@ -428,7 +428,7 @@ class ProductTemplate(models.Model):
]
# get the value (id) sets of existing variants
- existing_variants = {frozenset(variant.attribute_value_ids.ids) for variant in tmpl_id.product_variant_ids}
+ existing_variants = {frozenset(variant.attribute_value_ids.filtered(lambda r: r.attribute_id.create_variant).ids) for variant in tmpl_id.product_variant_ids}
# -> for each value set, create a recordset of values to create a
# variant for if the value set isn't already a variant
to_create_variants = [
diff --git a/addons/product/tests/test_variants.py b/addons/product/tests/test_variants.py
index 1a621d689cb..7917a0d4a2b 100644
--- a/addons/product/tests/test_variants.py
+++ b/addons/product/tests/test_variants.py
@@ -330,3 +330,23 @@ class TestVariantsNoCreate(common.TestProductCommon):
{variant.attribute_value_ids for variant in template.product_variant_ids},
{self.prod_attr1_v1, self.prod_attr1_v2},
)
+
+ def test_update_variant_with_nocreate(self):
+ """ update variants with a 'nocreate' value on variant """
+ template = self.env['product.template'].create({
+ 'name': 'Sofax',
+ 'uom_id': self.uom_unit.id,
+ 'uom_po_id': self.uom_unit.id,
+ 'attribute_line_ids': [
+ (0, 0, { # one variant for this one
+ 'attribute_id': self.prod_att_1.id,
+ 'value_ids': [(4, self.prod_attr1_v1.id)],
+ }),
+ ],
+ })
+ self.assertEqual(len(template.product_variant_ids), 1)
+
+ for variant_id in template.product_variant_ids:
+ variant_id.attribute_value_ids += self.size_S
+ template.attribute_line_ids += template.attribute_line_ids.browse()
+ self.assertEqual(len(template.product_variant_ids), 1)
diff --git a/addons/product_expiry/models/product_product.py b/addons/product_expiry/models/product_product.py
index 86094cfed16..a055f650286 100644
--- a/addons/product_expiry/models/product_product.py
+++ b/addons/product_expiry/models/product_product.py
@@ -10,7 +10,7 @@ class ProductTemplate(models.Model):
life_time = fields.Integer(string='Product Life Time',
help='Number of days before the goods may become dangerous and must not be consumed. It will be computed on the lot/serial number.')
use_time = fields.Integer(string='Product Use Time',
- help='Number of days before the goods starts deteriorating, without being dangerous yet. It will be computed on the lot/serial number.')
+ help='Number of days before the goods starts deteriorating, without being dangerous yet. It will be computed using the lot/serial number.')
removal_time = fields.Integer(string='Product Removal Time',
help='Number of days before the goods should be removed from the stock. It will be computed on the lot/serial number.')
alert_time = fields.Integer(string='Product Alert Time',
diff --git a/addons/project/models/project.py b/addons/project/models/project.py
index 85aea626c09..8a7ef37a487 100644
--- a/addons/project/models/project.py
+++ b/addons/project/models/project.py
@@ -435,7 +435,7 @@ class Task(models.Model):
_date_name = "date_start"
_inherit = ['portal.mixin', 'mail.thread', 'mail.activity.mixin', 'rating.mixin']
_mail_post_access = 'read'
- _order = "priority desc, sequence, date_start, name, id"
+ _order = "priority desc, sequence, id desc"
@api.model
def default_get(self, fields_list):
@@ -492,7 +492,7 @@ class Task(models.Model):
index=True, copy=False)
date_end = fields.Datetime(string='Ending Date', index=True, copy=False)
date_assign = fields.Datetime(string='Assigning Date', index=True, copy=False, readonly=True)
- date_deadline = fields.Date(string='Deadline', index=True, copy=False)
+ date_deadline = fields.Date(string='Deadline', index=True, copy=False, track_visibility='onchange')
date_last_stage_update = fields.Datetime(string='Last Stage Update',
default=fields.Datetime.now,
index=True,
@@ -505,8 +505,8 @@ class Task(models.Model):
track_visibility='onchange',
change_default=True)
notes = fields.Text(string='Notes')
- planned_hours = fields.Float("Planned Hours", help='It is the time planned to achieve the task. If this document has sub-tasks, it means the time needed to achieve this tasks and its childs.')
- subtask_planned_hours = fields.Float("Subtask Planned Hours", compute='_compute_subtask_planned_hours', help="Computed using sum of hours planned of all subtasks created from main task. Usually these hours are less or equal to the Planned Hours (of main task).")
+ planned_hours = fields.Float("Planned Hours", help='It is the time planned to achieve the task. If this document has sub-tasks, it means the time needed to achieve this tasks and its childs.',track_visibility='onchange')
+ subtask_planned_hours = fields.Float("Subtasks", compute='_compute_subtask_planned_hours', help="Computed using sum of hours planned of all subtasks created from main task. Usually these hours are less or equal to the Planned Hours (of main task).")
remaining_hours = fields.Float(string='Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task.")
user_id = fields.Many2one('res.users',
string='Assigned to',
@@ -528,7 +528,7 @@ class Task(models.Model):
legend_blocked = fields.Char(related='stage_id.legend_blocked', string='Kanban Blocked Explanation', readonly=True, related_sudo=False)
legend_done = fields.Char(related='stage_id.legend_done', string='Kanban Valid Explanation', readonly=True, related_sudo=False)
legend_normal = fields.Char(related='stage_id.legend_normal', string='Kanban Ongoing Explanation', readonly=True, related_sudo=False)
- parent_id = fields.Many2one('project.task', string='Parent Task')
+ parent_id = fields.Many2one('project.task', string='Parent Task', index=True)
child_ids = fields.One2many('project.task', 'parent_id', string="Sub-tasks")
subtask_count = fields.Integer("Sub-task count", compute='_compute_subtask_count')
email_from = fields.Char(string='Email', help="These people will receive email.", index=True)
@@ -539,6 +539,8 @@ class Task(models.Model):
working_hours_close = fields.Float(compute='_compute_elapsed', string='Working hours to close', store=True, group_operator="avg")
working_days_open = fields.Float(compute='_compute_elapsed', string='Working days to assign', store=True, group_operator="avg")
working_days_close = fields.Float(compute='_compute_elapsed', string='Working days to close', store=True, group_operator="avg")
+ # customer portal: include comment and incoming emails in communication history
+ website_message_ids = fields.One2many(domain=lambda self: [('model', '=', self._name), ('message_type', 'in', ['email', 'comment'])])
_constraints = [(models.BaseModel._check_recursion, 'Circular references are not permitted between tasks and sub-tasks', ['parent_id'])]
diff --git a/addons/project/static/src/less/project_dashboard.less b/addons/project/static/src/less/project_dashboard.less
index 0b96966711d..9983bd435b1 100644
--- a/addons/project/static/src/less/project_dashboard.less
+++ b/addons/project/static/src/less/project_dashboard.less
@@ -23,10 +23,10 @@
font-weight: 800;
}
- > a {
+ > div {
font-weight: 500;
- &.o_needaction{
+ button.o_needaction {
font-size: small;
font-weight: 400;
margin-left: 4px;
diff --git a/addons/project/views/project_portal_templates.xml b/addons/project/views/project_portal_templates.xml
index a86176bd10c..c4c7261c893 100644
--- a/addons/project/views/project_portal_templates.xml
+++ b/addons/project/views/project_portal_templates.xml
@@ -139,8 +139,9 @@
<div class="table-responsive"><table class="table table-hover o_portal_my_doc_table">
<thead>
<tr class="active">
- <th class="col-md-10">Task</th>
- <th>Stage</th>
+ <th class="col-md-9">Task</th>
+ <th class="col-md-2">Stage</th>
+ <th>Ref</th>
</tr>
</thead>
<tbody>
@@ -152,6 +153,9 @@
<td>
<span class="label label-info" title="Current stage of the task" t-esc="task.stage_id.name" />
</td>
+ <td>
+ <span t-esc="task.id"/>
+ </td>
</tr>
</t>
</tbody>
@@ -171,7 +175,7 @@
<div class="col-md-12">
<h4>
<t t-call='portal.record_pager'/>
- Task - <span t-field="task.name"/>
+ Task - <span t-field="task.name"/> (#<span t-esc="task.id"/>)
<span t-field="task.stage_id.name" class="pull-right label label-info" title="Current stage of this task"/>
<a class="btn btn-info" t-att-href="'/web#return_label=Website&amp;model=project.task&amp;id=%s&amp;view_type=form' % (task.id)" groups="project.group_project_user">Edit Task</a>
</h4>
diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml
index ccafde64533..dc4930c9fb5 100644
--- a/addons/project/views/project_views.xml
+++ b/addons/project/views/project_views.xml
@@ -349,9 +349,9 @@
<div>
<span class="o_value"><t t-esc="record.task_count.value"/></span>
<span class="o_label"><t t-esc="record.label_tasks.value"/></span>
- <a t-if="record.task_needaction_count.raw_value" class="o_needaction" name="%(act_project_project_2_project_task_all)d" type="action" context="{'search_default_message_needaction': 1}">
+ <button t-if="record.task_needaction_count.raw_value" class="btn-link o_needaction" name="%(act_project_project_2_project_task_all)d" type="action" context="{'search_default_message_needaction': 1}">
<t t-esc="record.task_needaction_count.raw_value"/>
- </a>
+ </button>
</div>
</a>
</div>
@@ -922,7 +922,7 @@
</p>
</field>
</record>
- <menuitem action="project_tags_action" id="menu_project_tags_act" parent="menu_project_config" groups="base.group_no_one"/>
+ <menuitem action="project_tags_action" id="menu_project_tags_act" parent="menu_project_config"/>
<!-- Reporting menus -->
<menuitem id="menu_project_report" name="Reporting"
diff --git a/addons/purchase/data/mail_template_data.xml b/addons/purchase/data/mail_template_data.xml
index a5cc40f5696..dc51e377149 100644
--- a/addons/purchase/data/mail_template_data.xml
+++ b/addons/purchase/data/mail_template_data.xml
@@ -68,7 +68,7 @@ Please find in attachment a <strong>${object.state in ('draft', 'sent') and 'req
% if object.origin:
(RFQ origin: ${object.origin})
% endif
-amounting <strong>${object.amount_total} ${object.currency_id.name}</strong>
+amounting <strong>${format_amount(object.amount_total, object.currency_id)}</strong>
from ${object.company_id.name}.
</p>
diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py
index 0e8a070ff00..40e0ee89c34 100644
--- a/addons/purchase/models/purchase.py
+++ b/addons/purchase/models/purchase.py
@@ -352,7 +352,7 @@ class PurchaseOrder(models.Model):
@api.multi
def button_approve(self, force=False):
- self.write({'state': 'purchase'})
+ self.write({'state': 'purchase', 'date_approve': fields.Date.context_today(self)})
self._create_picking()
if self.company_id.po_lock == 'lock':
self.write({'state': 'done'})
@@ -919,14 +919,6 @@ class PurchaseOrderLine(models.Model):
self.product_qty = 1.0
-class ProcurementGroup(models.Model):
- _inherit = 'procurement.group'
-
- @api.model
- def _get_exceptions_domain(self):
- return super(ProcurementGroup, self)._get_exceptions_domain() + [('created_purchase_line_id', '=', False)]
-
-
class ProcurementRule(models.Model):
_inherit = 'procurement.rule'
action = fields.Selection(selection_add=[('buy', 'Buy')])
diff --git a/addons/purchase/models/stock.py b/addons/purchase/models/stock.py
index bb5fd985e0b..fa38fa44bae 100644
--- a/addons/purchase/models/stock.py
+++ b/addons/purchase/models/stock.py
@@ -34,14 +34,14 @@ class StockMove(models.Model):
@api.model
def _prepare_merge_moves_distinct_fields(self):
distinct_fields = super(StockMove, self)._prepare_merge_moves_distinct_fields()
- distinct_fields.append('purchase_line_id')
+ distinct_fields += ['purchase_line_id', 'created_purchase_line_id']
return distinct_fields
@api.model
def _prepare_merge_move_sort_method(self, move):
move.ensure_one()
keys_sorted = super(StockMove, self)._prepare_merge_move_sort_method(move)
- keys_sorted.append(move.purchase_line_id.id)
+ keys_sorted += [move.purchase_line_id.id, move.created_purchase_line_id.id]
return keys_sorted
@api.multi
@@ -181,3 +181,19 @@ class Orderpoint(models.Model):
for poline in self.env['purchase.order.line'].search([('state','in',('draft','sent','to approve')),('orderpoint_id','in',self.ids)]):
res[poline.orderpoint_id.id] += poline.product_uom._compute_quantity(poline.product_qty, poline.orderpoint_id.product_uom, round=False)
return res
+
+ def action_view_purchase(self):
+ """ This function returns an action that display existing
+ purchase orders of given orderpoint.
+ """
+ action = self.env.ref('purchase.purchase_rfq')
+ result = action.read()[0]
+
+ # Remvove the context since the action basically display RFQ and not PO.
+ result['context'] = {}
+ order_line_ids = self.env['purchase.order.line'].search([('orderpoint_id', '=', self.id)])
+ purchase_ids = order_line_ids.mapped('order_id')
+
+ result['domain'] = "[('id','in',%s)]" % (purchase_ids.ids)
+
+ return result
diff --git a/addons/purchase/tests/test_create_picking.py b/addons/purchase/tests/test_create_picking.py
index 49711eb32a9..c4c4e78e342 100644
--- a/addons/purchase/tests/test_create_picking.py
+++ b/addons/purchase/tests/test_create_picking.py
@@ -224,3 +224,92 @@ class TestCreatePicking(common.TestProductCommon):
# check the delivered quantity
self.assertEqual(po.order_line.qty_received, 3.0)
+ def test_04_mto_multiple_po(self):
+ """ Simulate a mto chain with 2 purchase order.
+ Create a move with qty 1, confirm the RFQ then create a new
+ move that will not be merged in the first one(simulate an increase
+ order quantity on a SO). It should generate a new RFQ, validate
+ and receipt the picking then try to reserve the delivery
+ picking.
+ """
+ stock_location = self.env['ir.model.data'].xmlid_to_object('stock.stock_location_stock')
+ customer_location = self.env['ir.model.data'].xmlid_to_object('stock.stock_location_customers')
+ picking_type_out = self.env['ir.model.data'].xmlid_to_object('stock.picking_type_out')
+ # route buy should be there by default
+ partner = self.env['res.partner'].create({
+ 'name': 'Jhon'
+ })
+
+ seller = self.env['product.supplierinfo'].create({
+ 'name': partner.id,
+ 'price': 12.0,
+ })
+
+ product = self.env['product.product'].create({
+ 'name': 'product',
+ 'type': 'product',
+ 'route_ids': [(4, self.ref('stock.route_warehouse0_mto')), (4, self.ref('purchase.route_warehouse0_buy'))],
+ 'seller_ids': [(6, 0, [seller.id])],
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ })
+
+ # A picking is require since only moves inside the same picking are merged.
+ customer_picking = self.env['stock.picking'].create({
+ 'location_id': stock_location.id,
+ 'location_dest_id': customer_location.id,
+ 'partner_id': partner.id,
+ 'picking_type_id': picking_type_out.id,
+ })
+
+ customer_move = self.env['stock.move'].create({
+ 'name': 'move out',
+ 'location_id': stock_location.id,
+ 'location_dest_id': customer_location.id,
+ 'product_id': product.id,
+ 'product_uom': product.uom_id.id,
+ 'product_uom_qty': 80.0,
+ 'procure_method': 'make_to_order',
+ 'picking_id': customer_picking.id,
+ })
+
+ customer_move._action_confirm()
+
+ purchase_order = self.env['purchase.order'].search([('partner_id', '=', partner.id)])
+ self.assertTrue(purchase_order, 'No purchase order created.')
+
+ # Check purchase order line data.
+ purchase_order_line = purchase_order.order_line
+ self.assertEqual(purchase_order_line.product_id, product, 'The product on the purchase order line is not correct.')
+ self.assertEqual(purchase_order_line.price_unit, seller.price, 'The purchase order line price should be the same than the seller.')
+ self.assertEqual(purchase_order_line.product_qty, customer_move.product_uom_qty, 'The purchase order line qty should be the same than the move.')
+
+ purchase_order.button_confirm()
+
+ customer_move_2 = self.env['stock.move'].create({
+ 'name': 'move out',
+ 'location_id': stock_location.id,
+ 'location_dest_id': customer_location.id,
+ 'product_id': product.id,
+ 'product_uom': product.uom_id.id,
+ 'product_uom_qty': 20.0,
+ 'procure_method': 'make_to_order',
+ 'picking_id': customer_picking.id,
+ })
+
+ customer_move_2._action_confirm()
+
+ self.assertTrue(customer_move_2.exists(), 'The second customer move should not be merged in the first.')
+ self.assertEqual(sum(customer_picking.move_lines.mapped('product_uom_qty')), 100.0)
+
+ purchase_order_2 = self.env['purchase.order'].search([('partner_id', '=', partner.id), ('state', '=', 'draft')])
+ self.assertTrue(purchase_order_2, 'No purchase order created.')
+
+ purchase_order_2.button_confirm()
+
+ purchase_order.picking_ids.move_lines.quantity_done = 80.0
+ purchase_order.picking_ids.button_validate()
+
+ purchase_order_2.picking_ids.move_lines.quantity_done = 20.0
+ purchase_order_2.picking_ids.button_validate()
+
+ self.assertEqual(sum(customer_picking.move_lines.mapped('reserved_availability')), 100.0, 'The total quantity for the customer move should be available and reserved.')
diff --git a/addons/purchase/tests/test_fifo_price.py b/addons/purchase/tests/test_fifo_price.py
index a562a0fbfdb..8041ed12fd7 100644
--- a/addons/purchase/tests/test_fifo_price.py
+++ b/addons/purchase/tests/test_fifo_price.py
@@ -13,9 +13,6 @@ class TestFifoPrice(TestPurchase):
self._load('account', 'test', 'account_minimal_test.xml')
self._load('stock_account', 'test', 'stock_valuation_account.xml')
- # Set the company currency as EURO for the sake of repeatibility
- self.env.ref('base.main_company').currency_id = self.env.ref('base.EUR')
-
# Set a product as using fifo price
product_icecream = self.env['product.product'].create({
'default_code': 'FIFO',
diff --git a/addons/purchase/views/account_invoice_views.xml b/addons/purchase/views/account_invoice_views.xml
index ff14c42659f..f3eabb91534 100644
--- a/addons/purchase/views/account_invoice_views.xml
+++ b/addons/purchase/views/account_invoice_views.xml
@@ -58,7 +58,7 @@
<field name="inherit_id" ref="account.invoice_supplier_form"/>
<field name="arch" type="xml">
<field name="reference" position="after" >
- <field name="purchase_id" attrs="{'readonly': [('state','not in',['draft'])], 'invisible': [('state', '=', 'purchase')]}" class="oe_edit_only"
+ <field name="purchase_id" attrs="{'readonly': [('state','not in',['draft'])], 'invisible': ['|', ('state', '=', 'purchase'), ('type', '=', 'in_refund')]}" class="oe_edit_only"
options="{'no_create': True}" context="{'show_total_amount': True}"/>
</field>
<xpath expr="//field[@name='invoice_line_ids']/tree/field[@name='company_id']" position="after">
diff --git a/addons/purchase/views/stock_views.xml b/addons/purchase/views/stock_views.xml
index 80c9382ca1d..bc24cf74be0 100644
--- a/addons/purchase/views/stock_views.xml
+++ b/addons/purchase/views/stock_views.xml
@@ -30,9 +30,12 @@
<field name="inherit_id" ref="stock.view_warehouse_orderpoint_form"/>
<field name="arch" type="xml">
<div name="button_box" position="inside">
- <button name="%(purchase.purchase_order_action_generic)d" type="action"
- string="Purchases" class="oe_stat_button" icon="fa-refresh"
- domain="[('orderpoint_id','=',active_id)]"/>
+ <button type="object"
+ name="action_view_purchase"
+ class="oe_stat_button"
+ string="Purchases"
+ icon="fa-refresh">
+ </button>
</div>
</field>
</record>
diff --git a/addons/purchase_requisition/models/purchase_requisition.py b/addons/purchase_requisition/models/purchase_requisition.py
index 4b03b15fb3f..7aa518675f7 100644
--- a/addons/purchase_requisition/models/purchase_requisition.py
+++ b/addons/purchase_requisition/models/purchase_requisition.py
@@ -73,7 +73,6 @@ class PurchaseRequisition(models.Model):
'Status', track_visibility='onchange', required=True,
copy=False, default='draft')
state_blanket_order = fields.Selection(PURCHASE_REQUISITION_STATES, compute='_set_state')
- account_analytic_id = fields.Many2one('account.analytic.account', 'Analytic Account')
picking_type_id = fields.Many2one('stock.picking.type', 'Operation Type', required=True, default=_get_picking_in)
is_quantity_copy = fields.Selection(related='type_id.quantity_copy', readonly=True)
currency_id = fields.Many2one('res.currency', 'Currency', required=True,
@@ -201,11 +200,12 @@ class PurchaseRequisitionLine(models.Model):
requisition_id = fields.Many2one('purchase.requisition', required=True, string='Purchase Agreement', ondelete='cascade')
company_id = fields.Many2one('res.company', related='requisition_id.company_id', string='Company', store=True, readonly=True, default= lambda self: self.env['res.company']._company_default_get('purchase.requisition.line'))
account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic Account')
+ analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags')
schedule_date = fields.Date(string='Scheduled Date')
move_dest_id = fields.Many2one('stock.move', 'Downstream Move')
supplier_info_ids = fields.One2many('product.supplierinfo', 'purchase_requisition_line_id')
- @api.multi
+ @api.model
def create(self,vals):
res = super(PurchaseRequisitionLine, self).create(vals)
if res.requisition_id.state not in ['draft', 'cancel', 'done'] and res.requisition_id.is_quantity_copy == 'none':
@@ -265,8 +265,6 @@ class PurchaseRequisitionLine(models.Model):
if self.product_id:
self.product_uom_id = self.product_id.uom_id
self.product_qty = 1.0
- if not self.account_analytic_id:
- self.account_analytic_id = self.requisition_id.account_analytic_id
if not self.schedule_date:
self.schedule_date = self.requisition_id.schedule_date
@@ -283,6 +281,7 @@ class PurchaseRequisitionLine(models.Model):
'taxes_id': [(6, 0, taxes_ids)],
'date_planned': requisition.schedule_date or fields.Date.today(),
'account_analytic_id': self.account_analytic_id.id,
+ 'analytic_tag_ids': self.analytic_tag_ids.ids,
'move_dest_ids': self.move_dest_id and [(4, self.move_dest_id.id)] or []
}
diff --git a/addons/purchase_requisition/views/purchase_requisition_views.xml b/addons/purchase_requisition/views/purchase_requisition_views.xml
index d1a84d5b4a0..52953440ab1 100644
--- a/addons/purchase_requisition/views/purchase_requisition_views.xml
+++ b/addons/purchase_requisition/views/purchase_requisition_views.xml
@@ -173,7 +173,6 @@
<field name="type_id" attrs="{'readonly': [('state','!=','draft')]}" widget="selection"/>
<field name="vendor_id" context="{'search_default_supplier':1, 'default_supplier':1, 'default_customer':0}" domain="[('supplier','=',True)]" attrs="{'required': [('is_quantity_copy', '=', 'none')], 'readonly': [('state', 'in', ['ongoing','done'])]}"/>
<field name="currency_id" groups="base.group_multi_currency"/>
- <field name="account_analytic_id" colspan="2" groups="analytic.group_analytic_accounting" attrs="{'readonly': [('state','not in',('draft'))]}"/>
</group>
<group>
<field name="date_end" attrs="{'readonly': [('state','not in',('draft','in_progress','open','ongoing'))]}"/>
@@ -194,6 +193,7 @@
<field name="product_uom_id" groups="uom.group_uom"/>
<field name="schedule_date" groups="base.group_no_one"/>
<field name="account_analytic_id" groups="analytic.group_analytic_accounting"/>
+ <field name="analytic_tag_ids" groups="analytic.group_analytic_tags" widget="many2many_tags"/>
<field name="price_unit"/>
</tree>
<form string="Products">
diff --git a/addons/resource/models/resource.py b/addons/resource/models/resource.py
index 292d24c6a87..0742444b555 100644
--- a/addons/resource/models/resource.py
+++ b/addons/resource/models/resource.py
@@ -758,7 +758,7 @@ class ResourceCalendarLeaves(models.Model):
date_from = fields.Datetime('Start Date', required=True)
date_to = fields.Datetime('End Date', required=True)
tz = fields.Selection(
- _tz_get, string='Timezone', default=lambda self: self._context.get('tz', self.env.user.tz or 'UTC'),
+ _tz_get, string='Timezone', default=lambda self: self._context.get('tz') or self.env.user.tz or 'UTC',
help="Timezone used when encoding the leave. It is used to correctly "
"localize leave hours when computing time intervals.")
resource_id = fields.Many2one(
diff --git a/addons/resource/models/resource_mixin.py b/addons/resource/models/resource_mixin.py
index 20a641ede26..c08bd66d628 100644
--- a/addons/resource/models/resource_mixin.py
+++ b/addons/resource/models/resource_mixin.py
@@ -63,7 +63,8 @@ class ResourceMixin(models.AbstractModel):
theoric_hours = self.get_day_work_hours_count(day_intervals[0][0].date(), calendar=calendar)
work_time = sum((interval[1] - interval[0] for interval in day_intervals), timedelta())
total_work_time += work_time
- days_count += float_utils.round((work_time.total_seconds() / 3600 / theoric_hours) * 4) / 4
+ if theoric_hours:
+ days_count += float_utils.round((work_time.total_seconds() / 3600 / theoric_hours) * 4) / 4
return {
'days': days_count,
'hours': total_work_time.total_seconds() / 3600,
@@ -97,7 +98,8 @@ class ResourceMixin(models.AbstractModel):
theoric_hours = self.get_day_work_hours_count(day_intervals[0][0].date(), calendar=calendar)
leave_time = sum((interval[1] - interval[0] for interval in day_intervals), timedelta())
total_leave_time += leave_time
- days_count += float_utils.round((leave_time.total_seconds() / 3600 / theoric_hours) * 4) / 4
+ if theoric_hours:
+ days_count += float_utils.round((leave_time.total_seconds() / 3600 / theoric_hours) * 4) / 4
return {
'days': days_count,
'hours': total_leave_time.total_seconds() / 3600,
diff --git a/addons/sale/__init__.py b/addons/sale/__init__.py
index ba3e6977f3c..17753815848 100644
--- a/addons/sale/__init__.py
+++ b/addons/sale/__init__.py
@@ -11,12 +11,13 @@ import odoo
from odoo import api, SUPERUSER_ID
-
def uninstall_hook(cr, registry):
def update_dashboard_graph_model(dbname):
db_registry = odoo.modules.registry.Registry.new(dbname)
with api.Environment.manage(), db_registry.cursor() as cr:
env = api.Environment(cr, SUPERUSER_ID, {})
+ if 'account.invoice.report' in env:
+ env['account.invoice.report'].init()
if 'crm.team' in env:
recs = env['crm.team'].search([])
for rec in recs:
diff --git a/addons/sale/controllers/mail.py b/addons/sale/controllers/mail.py
index cd0701e21db..d257bfa2736 100644
--- a/addons/sale/controllers/mail.py
+++ b/addons/sale/controllers/mail.py
@@ -11,6 +11,7 @@ from odoo.tools.misc import consteq
class MailController(MailController):
+ @classmethod
def _redirect_to_record(cls, model, res_id, access_token=None):
# If the current user doesn't have access to the sales order, but provided
# a valid access token, redirect him to the front-end view.
diff --git a/addons/sale/models/__init__.py b/addons/sale/models/__init__.py
index 5f6e7ba2857..7a157566790 100644
--- a/addons/sale/models/__init__.py
+++ b/addons/sale/models/__init__.py
@@ -12,3 +12,4 @@ from . import sale
from . import res_config_settings
from . import sale_layout
from . import sales_team
+from . import ir_model_fields
diff --git a/addons/sale/models/account_invoice.py b/addons/sale/models/account_invoice.py
index 9878e75ec04..a03df305b16 100644
--- a/addons/sale/models/account_invoice.py
+++ b/addons/sale/models/account_invoice.py
@@ -96,15 +96,6 @@ class AccountInvoiceLine(models.Model):
_inherit = 'account.invoice.line'
_order = 'invoice_id, layout_category_id, sequence, id'
- @api.depends('price_unit', 'discount', 'invoice_line_tax_ids', 'quantity',
- 'product_id', 'invoice_id.partner_id', 'invoice_id.currency_id', 'invoice_id.company_id',
- 'invoice_id.date_invoice')
- def _compute_total_price(self):
- for line in self:
- price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
- taxes = line.invoice_line_tax_ids.compute_all(price, line.invoice_id.currency_id, line.quantity, product=line.product_id, partner=line.invoice_id.partner_id)
- line.price_total = taxes['total_included']
-
sale_line_ids = fields.Many2many(
'sale.order.line',
'sale_order_line_invoice_rel',
@@ -113,4 +104,3 @@ class AccountInvoiceLine(models.Model):
layout_category_id = fields.Many2one('sale.layout_category', string='Section')
layout_category_sequence = fields.Integer(string='Layout Sequence')
# TODO: remove layout_category_sequence in master or make it work properly
- price_total = fields.Monetary(compute='_compute_total_price', string='Total Amount', store=True)
diff --git a/addons/sale/models/ir_model_fields.py b/addons/sale/models/ir_model_fields.py
new file mode 100644
index 00000000000..828b9de8346
--- /dev/null
+++ b/addons/sale/models/ir_model_fields.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details
+
+from odoo import models
+
+
+# This is a nasty hack, targeted for V11 only
+class IrModelFields(models.Model):
+ _inherit = 'ir.model.fields'
+
+ def unlink(self):
+ # Prevent the deletion of some `shared` fields... -_-
+ self = self.filtered(
+ lambda rec: not (
+ rec.model == 'res.config.settings'
+ and rec.name == 'auth_signup_uninvited'
+ )
+ )
+ return super(IrModelFields, self).unlink()
diff --git a/addons/sale/models/product_product.py b/addons/sale/models/product_product.py
index 4c6223fd269..b26353142b6 100644
--- a/addons/sale/models/product_product.py
+++ b/addons/sale/models/product_product.py
@@ -10,6 +10,8 @@ class ProductProduct(models.Model):
@api.multi
def _sales_count(self):
r = {}
+ if not self.user_has_groups('sales_team.group_sale_salesman'):
+ return r
domain = [
('state', 'in', ['sale', 'done']),
('product_id', 'in', self.ids),
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index 911e2eb7ef9..1913f5859ed 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -501,7 +501,7 @@ class SaleOrder(models.Model):
@api.returns('self', lambda value: value.id)
def message_post(self, **kwargs):
if self.env.context.get('mark_so_as_sent'):
- self.filtered(lambda o: o.state == 'draft').write({'state': 'sent'})
+ self.filtered(lambda o: o.state == 'draft').with_context(tracking_disable=True).write({'state': 'sent'})
return super(SaleOrder, self.with_context(mail_post_autofollow=True)).message_post(**kwargs)
@api.multi
diff --git a/addons/sale/views/sale_views.xml b/addons/sale/views/sale_views.xml
index d134fa41179..b1686253c0e 100644
--- a/addons/sale/views/sale_views.xml
+++ b/addons/sale/views/sale_views.xml
@@ -644,7 +644,7 @@
<field name="arch" type="xml">
<button name="toggle_active" position="before">
<button class="oe_stat_button" name="%(action_product_sale_list)d"
- type="action" icon="fa-usd">
+ type="action" icon="fa-usd" groups="sales_team.group_sale_salesman">
<field string="Sales" name="sales_count" widget="statinfo" />
</button>
</button>
@@ -666,7 +666,8 @@
<field name="arch" type="xml">
<button name="toggle_active" position="before">
<button class="oe_stat_button" name="action_view_sales"
- type="object" icon="fa-usd" attrs="{'invisible': [('sale_ok', '=', False)]}">
+ type="object" icon="fa-usd" attrs="{'invisible': [('sale_ok', '=', False)]}"
+ groups="sales_team.group_sale_salesman">
<field string="Sales" name="sales_count" widget="statinfo" />
</button>
</button>
@@ -689,6 +690,10 @@
<group string="Invoicing" name="invoicing">
<field name="invoice_policy" widget="radio"/>
<field name="service_type" widget="radio" invisible="True"/>
+ </group>
+ </group>
+ <group name="purchase" position="inside">
+ <group string="Invoicing" name="invoicing">
<field name="expense_policy" widget="radio"/>
</group>
</group>
diff --git a/addons/sale/wizard/sale_make_invoice_advance.py b/addons/sale/wizard/sale_make_invoice_advance.py
index e7c42f92fa3..90f6d6ad6c5 100644
--- a/addons/sale/wizard/sale_make_invoice_advance.py
+++ b/addons/sale/wizard/sale_make_invoice_advance.py
@@ -111,6 +111,7 @@ class SaleAdvancePaymentInv(models.TransientModel):
'product_id': self.product_id.id,
'sale_line_ids': [(6, 0, [so_line.id])],
'invoice_line_tax_ids': [(6, 0, tax_ids)],
+ 'analytic_tag_ids': [(6, 0, so_line.analytic_tag_ids.ids)],
'account_analytic_id': order.analytic_account_id.id or False,
})],
'currency_id': order.pricelist_id.currency_id.id,
@@ -157,6 +158,9 @@ class SaleAdvancePaymentInv(models.TransientModel):
else:
tax_ids = taxes.ids
context = {'lang': order.partner_id.lang}
+ analytic_tag_ids = []
+ for line in order.order_line:
+ analytic_tag_ids = [(4, analytic_tag.id, None) for analytic_tag in line.analytic_tag_ids]
so_line = sale_line_obj.create({
'name': _('Advance: %s') % (time.strftime('%m %Y'),),
'price_unit': amount,
@@ -165,6 +169,7 @@ class SaleAdvancePaymentInv(models.TransientModel):
'discount': 0.0,
'product_uom': self.product_id.uom_id.id,
'product_id': self.product_id.id,
+ 'analytic_tag_ids': analytic_tag_ids,
'tax_id': [(6, 0, tax_ids)],
'is_downpayment': True,
})
diff --git a/addons/sale_crm/models/crm_lead.py b/addons/sale_crm/models/crm_lead.py
index 7409fdccaa5..b4eeb43f565 100644
--- a/addons/sale_crm/models/crm_lead.py
+++ b/addons/sale_crm/models/crm_lead.py
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from datetime import date
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models
@@ -31,7 +30,7 @@ class CrmLead(models.Model):
@api.model
def retrieve_sales_dashboard(self):
res = super(CrmLead, self).retrieve_sales_dashboard()
- date_today = date.today()
+ date_today = fields.Date.from_string(fields.Date.context_today(self))
res['invoiced'] = {
'this_month': 0,
diff --git a/addons/sale_management/__init__.py b/addons/sale_management/__init__.py
index 1b2e5977577..086ab956265 100644
--- a/addons/sale_management/__init__.py
+++ b/addons/sale_management/__init__.py
@@ -11,3 +11,12 @@ def uninstall_hook(cr, registry):
('module', '=', 'sale')
]).mapped('res_id')
env['ir.ui.menu'].browse(res_ids).update({'active': False})
+
+
+def post_init_hook(cr, registry):
+ env = Environment(cr, SUPERUSER_ID, {})
+ res_ids = env['ir.model.data'].search([
+ ('model', '=', 'ir.ui.menu'),
+ ('module', '=', 'sale'),
+ ]).mapped('res_id')
+ env['ir.ui.menu'].browse(res_ids).update({'active': True})
diff --git a/addons/sale_management/__manifest__.py b/addons/sale_management/__manifest__.py
index 4202ac31f78..c4584b0acc9 100644
--- a/addons/sale_management/__manifest__.py
+++ b/addons/sale_management/__manifest__.py
@@ -48,4 +48,5 @@ The Dashboard for the Sales Manager will include
],
'application': True,
'uninstall_hook': 'uninstall_hook',
+ 'post_init_hook': 'post_init_hook',
}
diff --git a/addons/sale_stock/models/account_invoice.py b/addons/sale_stock/models/account_invoice.py
index 4b9212beb8f..3b4c60cc413 100644
--- a/addons/sale_stock/models/account_invoice.py
+++ b/addons/sale_stock/models/account_invoice.py
@@ -51,7 +51,9 @@ class AccountInvoiceLine(models.Model):
qty_to_consider = invoiced_qty - qty_done
qty_to_consider = min(qty_to_consider, quantity - qty_delivered)
qty_delivered += qty_to_consider
- average_price_unit = (average_price_unit * (qty_delivered - qty_to_consider) + (-1 * move.price_unit) * qty_to_consider) / qty_delivered
+ # `move.price_unit` is negative if the move is out and positive if the move is
+ # dropshipped. Use its absolute value to compute the average price unit.
+ average_price_unit = (average_price_unit * (qty_delivered - qty_to_consider) + abs(move.price_unit) * qty_to_consider) / qty_delivered
if qty_delivered == quantity:
break
return average_price_unit
diff --git a/addons/sale_stock/views/stock_views.xml b/addons/sale_stock/views/stock_views.xml
index c9a0a76d054..196b8e8aa77 100644
--- a/addons/sale_stock/views/stock_views.xml
+++ b/addons/sale_stock/views/stock_views.xml
@@ -92,7 +92,8 @@
<field name="arch" type="xml">
<button name="action_view_orderpoints" position="after">
<button class="oe_stat_button" name="action_view_sales"
- type="object" icon="fa-usd" attrs="{'invisible': [('sale_ok', '=', False)]}">
+ type="object" icon="fa-usd" attrs="{'invisible': [('sale_ok', '=', False)]}"
+ groups="sales_team.group_sale_salesman">
<field string="Sales" name="sales_count" widget="statinfo" />
</button>
</button>
diff --git a/addons/sale_timesheet/models/account.py b/addons/sale_timesheet/models/account.py
index 00a4b708e2c..23a5b3a5f67 100644
--- a/addons/sale_timesheet/models/account.py
+++ b/addons/sale_timesheet/models/account.py
@@ -141,7 +141,7 @@ class AccountAnalyticLine(models.Model):
values['timesheet_invoice_type'] = 'billable_fixed'
# if the so line is already invoiced, and the delivered qty is still smaller than the ordered, then link the timesheet to the invoice
if so_line.invoice_status == 'invoiced':
- values['timesheet_invoice_id'] = so_line.invoice_lines[0].invoice_id.id
+ values['timesheet_invoice_id'] = so_line.invoice_lines and so_line.invoice_lines[0].invoice_id.id
elif so_line.product_id.invoice_policy == 'order' and so_line.product_id.service_type != 'timesheet':
values['timesheet_invoice_type'] = 'billable_fixed'
diff --git a/addons/sale_timesheet/models/sale_order.py b/addons/sale_timesheet/models/sale_order.py
index 335a6b657fe..2116722d9d0 100644
--- a/addons/sale_timesheet/models/sale_order.py
+++ b/addons/sale_timesheet/models/sale_order.py
@@ -137,8 +137,8 @@ class SaleOrderLine(models.Model):
_inherit = "sale.order.line"
qty_delivered_method = fields.Selection(selection_add=[('timesheet', 'Timesheets')])
- task_id = fields.Many2one('project.task', 'Task', help="Task generated by the sales order item")
- is_service = fields.Boolean("Is a Service", compute='_compute_is_service', store=True, help="Sales Order item should generate a task and/or a project, depending on the product settings.")
+ task_id = fields.Many2one('project.task', 'Task', index=True, help="Task generated by the sales order item")
+ is_service = fields.Boolean("Is a Service", compute='_compute_is_service', store=True, compute_sudo=True, help="Sales Order item should generate a task and/or a project, depending on the product settings.")
analytic_line_ids = fields.One2many(domain=[('project_id', '=', False)]) # only analytic lines, not timesheets (since this field determine if SO line came from expense)
@api.multi
diff --git a/addons/sale_timesheet/views/hr_timesheet_templates.xml b/addons/sale_timesheet/views/hr_timesheet_templates.xml
index 64e14f6a003..59327067366 100644
--- a/addons/sale_timesheet/views/hr_timesheet_templates.xml
+++ b/addons/sale_timesheet/views/hr_timesheet_templates.xml
@@ -60,9 +60,7 @@
<a type="action" t-att-domain="json.dumps(domain)" context="{'pivot_row_groupby': ['timesheet_invoice_id'], 'pivot_column_groupby': ['date:month'],'pivot_measures': ['amount', 'timesheet_revenue']}">Profitability</a>
</th>
<td class="o_timesheet_plan_dashboard_cell">
- <t t-if="currency.position == 'before'" t-esc="currency.symbol"/>
- <t t-esc="dashboard['money_amount']['invoiced']"/>
- <t t-if="currency.position == 'after'" t-esc="currency.symbol"/>
+ <t t-esc="dashboard['money_amount']['invoiced']" t-options='{"widget": "monetary", "display_currency": currency}'/>
</td>
<td>Invoiced</td>
</tr>
@@ -76,9 +74,7 @@
</td>
<td>Fixed</td>
<td class="o_timesheet_plan_dashboard_cell">
- <t t-if="currency.position == 'before'" t-esc="currency.symbol"/>
- <t t-esc="dashboard['money_amount']['to_invoice']"/>
- <t t-if="currency.position == 'after'" t-esc="currency.symbol"/>
+ <t t-esc="dashboard['money_amount']['to_invoice']" t-options='{"widget": "monetary", "display_currency": currency}'/>
</td>
<td>To invoice</td>
</tr>
@@ -92,9 +88,7 @@
</td>
<td>No task found</td>
<td class="o_timesheet_plan_dashboard_cell">
- <t t-if="currency.position == 'before'" t-esc="currency.symbol"/>
- <t t-esc="dashboard['money_amount']['cost']"/>
- <t t-if="currency.position == 'after'" t-esc="currency.symbol"/>
+ <t t-esc="dashboard['money_amount']['cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
</td>
<td>Cost</td>
</tr>
@@ -118,9 +112,7 @@
<td></td>
<td></td>
<td class="o_timesheet_plan_dashboard_total">
- <t t-if="currency.position == 'before'" t-esc="currency.symbol"/>
- <t t-esc="dashboard['money_amount']['total']"/>
- <t t-if="currency.position == 'after'" t-esc="currency.symbol"/>
+ <t t-esc="dashboard['money_amount']['total']" t-options='{"widget": "monetary", "display_currency": currency}'/>
</td>
<td><b>Total</b></td>
</tr>
diff --git a/addons/stock/__init__.py b/addons/stock/__init__.py
index 7039d459cb8..66322ee14f3 100644
--- a/addons/stock/__init__.py
+++ b/addons/stock/__init__.py
@@ -4,4 +4,16 @@
from . import controllers
from . import models
from . import report
-from . import wizard \ No newline at end of file
+from . import wizard
+
+
+from odoo import api, SUPERUSER_ID
+
+
+# TODO: Apply proper fix & remove in master
+def pre_init_hook(cr):
+ env = api.Environment(cr, SUPERUSER_ID, {})
+ env['ir.model.data'].search([
+ ('model', 'like', '%stock%'),
+ ('module', '=', 'stock')
+ ]).unlink()
diff --git a/addons/stock/__manifest__.py b/addons/stock/__manifest__.py
index a5c03d60e51..e314dcbbde1 100644
--- a/addons/stock/__manifest__.py
+++ b/addons/stock/__manifest__.py
@@ -74,4 +74,5 @@
'installable': True,
'application': True,
'auto_install': False,
+ 'pre_init_hook': 'pre_init_hook',
}
diff --git a/addons/stock/data/stock_demo2.xml b/addons/stock/data/stock_demo2.xml
index dd10fa62160..079be6db035 100644
--- a/addons/stock/data/stock_demo2.xml
+++ b/addons/stock/data/stock_demo2.xml
@@ -419,13 +419,6 @@
obj().env.ref('stock.incomming_chicago_warehouse3').id]"/>
</function>
- <function model="stock.picking" name="force_assign">
- <value model="stock.picking" eval="[
- obj().env.ref('stock.outgoing_shipment_main_warehouse').id,
- obj().env.ref('stock.outgoing_chicago_warehouse3').id,
- obj().env.ref('stock.outgoing_shipment_main_warehouse2').id]"/>
- </function>
-
<function model="stock.picking" name="action_done">
<value model="stock.picking" eval="[
obj().env.ref('stock.outgoing_shipment_main_warehouse1').id,
diff --git a/addons/stock/models/procurement.py b/addons/stock/models/procurement.py
index 38b9af72a2b..ba047badef8 100644
--- a/addons/stock/models/procurement.py
+++ b/addons/stock/models/procurement.py
@@ -231,30 +231,17 @@ class ProcurementGroup(models.Model):
('product_id', '=', values['product_id'].id)]
@api.model
- def _get_exceptions_domain(self):
- return [('procure_method', '=', 'make_to_order'), ('move_orig_ids', '=', False)]
-
- @api.model
def _run_scheduler_tasks(self, use_new_cursor=False, company_id=False):
# Minimum stock rules
self.sudo()._procure_orderpoint_confirm(use_new_cursor=use_new_cursor, company_id=company_id)
# Search all confirmed stock_moves and try to assign them
- confirmed_moves = self.env['stock.move'].search([('state', '=', 'confirmed')], limit=None, order='priority desc, date_expected asc')
+ confirmed_moves = self.env['stock.move'].search([('state', '=', 'confirmed'), ('product_uom_qty', '!=', 0.0)], limit=None, order='priority desc, date_expected asc')
for moves_chunk in split_every(100, confirmed_moves.ids):
self.env['stock.move'].browse(moves_chunk)._action_assign()
if use_new_cursor:
self._cr.commit()
- exception_moves = self.env['stock.move'].search(self._get_exceptions_domain())
- for move in exception_moves:
- values = move._prepare_procurement_values()
- try:
- with self._cr.savepoint():
- origin = (move.group_id and (move.group_id.name + ":") or "") + (move.rule_id and move.rule_id.name or move.origin or move.picking_id.name or "/")
- self.run(move.product_id, move.product_uom_qty, move.product_uom, move.location_id, move.rule_id and move.rule_id.name or "/", origin, values)
- except UserError as error:
- self.env['procurement.rule']._log_next_activity(move.product_id, error.name)
if use_new_cursor:
self._cr.commit()
diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py
index a75d7cbee24..92f3a9d1351 100644
--- a/addons/stock/models/stock_move.py
+++ b/addons/stock/models/stock_move.py
@@ -159,6 +159,7 @@ class StockMove(models.Model):
is_initial_demand_editable = fields.Boolean('Is initial demand editable', compute='_compute_is_initial_demand_editable')
is_quantity_done_editable = fields.Boolean('Is quantity done editable', compute='_compute_is_quantity_done_editable')
reference = fields.Char(compute='_compute_reference', string="Reference", store=True)
+ has_move_lines = fields.Boolean(compute='_compute_has_move_lines')
@api.depends('picking_id.is_locked')
def _compute_is_locked(self):
@@ -218,6 +219,8 @@ class StockMove(models.Model):
move.is_quantity_done_editable = False
elif move.show_details_visible:
move.is_quantity_done_editable = False
+ elif move.show_operations:
+ move.is_quantity_done_editable = False
else:
move.is_quantity_done_editable = True
@@ -226,6 +229,11 @@ class StockMove(models.Model):
for move in self:
move.reference = move.picking_id.name if move.picking_id else move.name
+ @api.depends('move_line_ids')
+ def _compute_has_move_lines(self):
+ for move in self:
+ move.has_move_lines = bool(move.move_line_ids)
+
@api.one
@api.depends('product_id', 'product_uom', 'product_uom_qty')
def _compute_product_qty(self):
diff --git a/addons/stock/models/stock_move_line.py b/addons/stock/models/stock_move_line.py
index a66391b0427..9f4eacbd725 100644
--- a/addons/stock/models/stock_move_line.py
+++ b/addons/stock/models/stock_move_line.py
@@ -143,7 +143,7 @@ class StockMoveLine(models.Model):
res = {}
if self.product_id.tracking == 'serial':
if float_compare(self.qty_done, 1.0, precision_rounding=self.move_id.product_id.uom_id.rounding) != 0:
- message = _('You can only process 1.0 %s for products with unique serial number.') % self.product_id.uom_id.name
+ message = _('You can only process 1.0 %s of products with unique serial number.') % self.product_id.uom_id.name
res['warning'] = {'title': _('Warning'), 'message': message}
return res
@@ -167,7 +167,7 @@ class StockMoveLine(models.Model):
# If the move line is directly create on the picking view.
# If this picking is already done we should generate an
# associated done move.
- if 'picking_id' in vals and 'move_id' not in vals:
+ if 'picking_id' in vals and not vals.get('move_id'):
picking = self.env['stock.picking'].browse(vals['picking_id'])
if picking.state == 'done':
product = self.env['product.product'].browse(vals['product_id'])
diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py
index 948be5aa4d6..f812df8ea35 100644
--- a/addons/stock/models/stock_picking.py
+++ b/addons/stock/models/stock_picking.py
@@ -514,7 +514,18 @@ class Picking(models.Model):
if after_vals:
self.mapped('move_lines').filtered(lambda move: not move.scrapped).write(after_vals)
if vals.get('move_lines'):
- self._autoconfirm_picking()
+ # Do not run autoconfirm if any of the moves has an initial demand. If an initial demand
+ # is present in any of the moves, it means the picking was created through the "planned
+ # transfer" mechanism.
+ pickings_to_not_autoconfirm = self.env['stock.picking']
+ for picking in self:
+ if picking.state != 'draft':
+ continue
+ for move in picking.move_lines:
+ if not float_is_zero(move.product_uom_qty, precision_rounding=move.product_uom.rounding):
+ pickings_to_not_autoconfirm |= picking
+ break
+ (self - pickings_to_not_autoconfirm)._autoconfirm_picking()
return res
@api.multi
diff --git a/addons/stock/models/stock_quant.py b/addons/stock/models/stock_quant.py
index 438d726dd95..11b34b51413 100644
--- a/addons/stock/models/stock_quant.py
+++ b/addons/stock/models/stock_quant.py
@@ -58,10 +58,14 @@ class StockQuant(models.Model):
action = self.env.ref('stock.stock_move_line_action').read()[0]
action['domain'] = [
('product_id', '=', self.product_id.id),
- '|', ('location_id', '=', self.location_id.id),
- ('location_dest_id', '=', self.location_id.id),
+ '|',
+ ('location_id', '=', self.location_id.id),
+ ('location_dest_id', '=', self.location_id.id),
('lot_id', '=', self.lot_id.id),
- ('package_id', '=', self.package_id.id)]
+ '|',
+ ('package_id', '=', self.package_id.id),
+ ('result_package_id', '=', self.package_id.id),
+ ]
return action
@api.constrains('product_id')
diff --git a/addons/stock/models/stock_scrap.py b/addons/stock/models/stock_scrap.py
index d9281233803..bb985547ede 100644
--- a/addons/stock/models/stock_scrap.py
+++ b/addons/stock/models/stock_scrap.py
@@ -11,10 +11,14 @@ class StockScrap(models.Model):
_order = 'id desc'
def _get_default_scrap_location_id(self):
- return self.env['stock.location'].search([('scrap_location', '=', True)], limit=1).id
+ return self.env['stock.location'].search([('scrap_location', '=', True), ('company_id', 'in', [self.env.user.company_id.id, False])], limit=1).id
def _get_default_location_id(self):
- return self.env.ref('stock.stock_location_stock', raise_if_not_found=False)
+ company_user = self.env.user.company_id
+ warehouse = self.env['stock.warehouse'].search([('company_id', '=', company_user.id)], limit=1)
+ if warehouse:
+ return warehouse.lot_stock_id.id
+ return None
name = fields.Char(
'Reference', default=lambda self: _('New'),
diff --git a/addons/stock/report/report_deliveryslip.xml b/addons/stock/report/report_deliveryslip.xml
index 3d708d88024..60627d9f596 100644
--- a/addons/stock/report/report_deliveryslip.xml
+++ b/addons/stock/report/report_deliveryslip.xml
@@ -117,7 +117,7 @@
</td>
<td class="text-right" t-if="move_line.state == 'done'">
<t t-if="move_line.ordered_qty != move_line.qty_done">
- <t t-set="backorder" t-value="True"/>
+ <t t-set="backorder" t-value="o.search([('backorder_id', '=', o.id), ('state', 'not in', ['cancel', 'done'])])"/>
</t>
<span t-field="move_line.qty_done"/>
<span t-field="move_line.product_uom_id"/>
diff --git a/addons/stock/tests/test_move2.py b/addons/stock/tests/test_move2.py
index 78835d1db9a..4589fb12a56 100644
--- a/addons/stock/tests/test_move2.py
+++ b/addons/stock/tests/test_move2.py
@@ -185,6 +185,51 @@ class TestPickShip(TestStockCommon):
# the client picking should not be assigned anymore, as we returned partially what we took
self.assertEqual(picking_client.state, 'confirmed')
+ def test_mto_resupply_cancel_ship(self):
+ """ This test simulates a pick pack ship with a resupply route
+ set. Pick and pack are validated, ship is cancelled. This test
+ ensure that new picking are not created from the cancelled
+ ship after the scheduler task. The supply route is only set in
+ order to make the scheduler run without mistakes (no next
+ activity).
+ """
+ picking_pick, picking_pack, picking_ship = self.create_pick_pack_ship()
+ stock_location = self.env['stock.location'].browse(self.stock_location)
+ warehouse_1 = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
+ warehouse_1.write({'delivery_steps': 'pick_pack_ship'})
+ warehouse_2 = self.env['stock.warehouse'].create({
+ 'name': 'Small Warehouse',
+ 'code': 'SWH'
+ })
+ warehouse_1.write({
+ 'default_resupply_wh_id': warehouse_2.id,
+ 'resupply_wh_ids': [(6, 0, [warehouse_2.id])]
+ })
+ resupply_route = self.env['stock.location.route'].search([('supplier_wh_id', '=', warehouse_2.id), ('supplied_wh_id', '=', warehouse_1.id)])
+ self.assertTrue(resupply_route)
+ self.productA.write({'route_ids': [(4, resupply_route.id), (4, self.env.ref('stock.route_warehouse0_mto').id)]})
+
+ self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 10.0)
+
+ picking_pick.action_assign()
+ picking_pick.move_lines[0].move_line_ids[0].qty_done = 10.0
+ picking_pick.action_done()
+
+ picking_pack.action_assign()
+ picking_pack.move_lines[0].move_line_ids[0].qty_done = 10.0
+ picking_pack.action_done()
+
+ picking_ship.action_cancel()
+ picking_ship.move_lines.write({'procure_method': 'make_to_order'})
+
+ self.env['procurement.group'].run_scheduler()
+ next_activity = self.env['mail.activity'].search([('res_model', '=', 'product.template'), ('res_id', '=', self.productA.product_tmpl_id.id)])
+ self.assertEqual(picking_ship.state, 'cancel')
+ self.assertFalse(next_activity, 'If a next activity has been created if means that scheduler failed\
+ and the end of this test do not have sense.')
+ self.assertEqual(len(picking_ship.move_lines.mapped('move_orig_ids')), 0,
+ 'Scheduler should not create picking pack and pick since ship has been manually cancelled.')
+
def test_no_backorder_1(self):
""" Check the behavior of doing less than asked in the picking pick and chosing not to
create a backorder. In this behavior, the second picking should obviously only be able to
diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml
index 3c69dd929f2..16bdbe44738 100644
--- a/addons/stock/views/stock_picking_views.xml
+++ b/addons/stock/views/stock_picking_views.xml
@@ -257,7 +257,8 @@
<page string="Detailed Operations" attrs="{'invisible': [('show_operations', '=', False)]}">
<field name="move_line_ids" attrs="{'readonly': [('state', '=', 'done'), ('is_locked', '=', True)]}" context="{'default_picking_id': id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id}">
<tree editable="bottom" decoration-muted="(state == 'done' and is_locked == True)" decoration-danger="qty_done&gt;product_uom_qty and state!='done'" decoration-success="qty_done==product_uom_qty and state!='done' and not result_package_id">
- <field name="product_id" required="1"/>
+ <field name="product_id" required="1" attrs="{'readonly': ['|', ('state', '=', 'done'), ('move_id', '!=', False)]}"/>
+ <field name="move_id" invisible="1"/>
<field name="in_entire_package" invisible="1"/>
<field name="product_uom_id" force_save="1" attrs="{'readonly': [('state', '!=', 'draft')]}" groups="uom.group_uom"/>
<field name="package_id" attrs="{'readonly': [('in_entire_package', '=', True)]}" groups="stock.group_tracking_lot" domain="[('location_id', '=', location_id)]"/>
@@ -270,7 +271,7 @@
<field name="lot_id" groups="stock.group_production_lot" attrs="{'column_invisible': [('parent.show_lots_text', '=', True)], 'invisible': [('lots_visible', '=', False)], 'readonly': [('in_entire_package', '=', True)]}" domain="[('product_id', '=', product_id)]" context="{'default_product_id': product_id}"/>
<field name="lot_name" groups="stock.group_production_lot" attrs="{'column_invisible': [('parent.show_lots_text', '=', False)], 'invisible': [('lots_visible', '=', False)], 'readonly': [('in_entire_package', '=', True)]}" context="{'default_product_id': product_id}"/>
<field name="is_initial_demand_editable" invisible="1"/>
- <field name="product_uom_qty" attrs="{'readonly': ['|', ('is_initial_demand_editable', '=', False), ('in_entire_package', '=', True)], 'required': [('product_id', '!=', False)]}"/>
+ <field name="product_uom_qty" readonly="1"/>
<field name="is_locked" invisible="1"/>
<field name="qty_done" attrs="{'readonly': ['|', ('state', 'in', ('done', 'cancel')), ('is_locked', '=', True), ('in_entire_package', '=', True)]}" force_save="1"/>
</tree>
@@ -280,7 +281,7 @@
</page>
<page string="Operations">
<field name="id" invisible="1"/>
- <field name="move_lines" attrs="{'readonly': ['|', ('show_operations', '=', True), '&amp;', ('state', '=', 'done'), ('is_locked', '=', True)]}" context="{'picking_type_code': picking_type_code, 'default_picking_id': id, 'form_view_ref':'stock.view_move_picking_form', 'address_in_id': partner_id, 'default_picking_type_id': picking_type_id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id}">
+ <field name="move_lines" attrs="{'readonly': ['|', '&amp;', ('show_operations', '=', True), '|', ('is_locked', '=', True), ('state', '=', 'done'), '&amp;', ('state', '=', 'done'), ('is_locked', '=', True)]}" context="{'picking_type_code': picking_type_code, 'default_picking_id': id, 'form_view_ref':'stock.view_move_picking_form', 'address_in_id': partner_id, 'default_picking_type_id': picking_type_id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id}">
<tree decoration-danger="state != 'done' and quantity_done > reserved_availability and show_reserved_availability" decoration-muted="scrapped == True or state == 'cancel' or (state == 'done' and is_locked == True)" string="Stock Moves" editable="bottom">
<field name="name" invisible="1"/>
<field name="date_expected" invisible="1"/>
@@ -295,8 +296,9 @@
<field name="show_reserved_availability" invisible="1"/>
<field name="show_operations" invisible="1" readonly="1"/>
<field name="additional" invisible="1"/>
+ <field name="has_move_lines" invisible="1"/>
<field name="is_locked" invisible="1"/>
- <field name="product_id" required="1" attrs="{'readonly': ['&amp;', ('state', '!=', 'draft'), ('additional', '=', False)]}"/>
+ <field name="product_id" required="1" attrs="{'readonly': ['|', '&amp;', ('state', '!=', 'draft'), ('additional', '=', False), ('has_move_lines', '=', True)]}"/>
<field name="is_initial_demand_editable" invisible="1"/>
<field name="is_quantity_done_editable" invisible="1"/>
<field name="product_uom_qty" string="Initial Demand" attrs="{'readonly': [('is_initial_demand_editable', '=', False)]}"/>
diff --git a/addons/stock/wizard/stock_immediate_transfer_views.xml b/addons/stock/wizard/stock_immediate_transfer_views.xml
index 4987bd3cb17..e84fb1f3fea 100644
--- a/addons/stock/wizard/stock_immediate_transfer_views.xml
+++ b/addons/stock/wizard/stock_immediate_transfer_views.xml
@@ -7,7 +7,7 @@
<form string="Immediate transfer?">
<group>
<p>
- You haven't entered <i>done</i> quantities, by clicking on <i>apply</i>
+ You have not recorded <i>done</i> quantities yet, by clicking on <i>apply</i>
Odoo will process all the <i>reserved</i> quantities.
</p>
</group>
diff --git a/addons/stock_account/__init__.py b/addons/stock_account/__init__.py
index 17a52b35e20..e6ecd5c5e46 100644
--- a/addons/stock_account/__init__.py
+++ b/addons/stock_account/__init__.py
@@ -66,13 +66,14 @@ def _configure_journals(cr, registry):
'company_id': company_id.id,
'fields_id': field_id,
'value': value,
- 'res_id': 'product.category,'+str(env.ref('product.product_category_all').id),
}
properties = env['ir.property'].search([
('name', '=', record),
('company_id', '=', company_id.id),
- ('value_reference', '!=', False)])
- if not properties:
+ ])
+ if properties:
+ properties.write(vals)
+ else:
# create the property
env['ir.property'].create(vals)
diff --git a/addons/stock_account/models/res_config_settings.py b/addons/stock_account/models/res_config_settings.py
index 3fa7481036f..0889e5e0983 100644
--- a/addons/stock_account/models/res_config_settings.py
+++ b/addons/stock_account/models/res_config_settings.py
@@ -8,4 +8,4 @@ class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
module_stock_landed_costs = fields.Boolean("Landed Costs",
- help="Affect landed costs on receipt operations and split them among products to update their cost price.")
+ help="Affect landed costs on reception operations and split them among products to update their cost price.")
diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py
index 0c1fdf09cb3..a680bd4d8c1 100644
--- a/addons/stock_account/models/stock.py
+++ b/addons/stock_account/models/stock.py
@@ -200,6 +200,14 @@ class StockMove(models.Model):
return True
return False
+ def _is_dropshipped(self):
+ """ Check if the move should be considered as a dropshipping move so that the cost method
+ will be able to apply the correct logic.
+
+ :return: True if the move is a dropshipping one else False
+ """
+ return self.location_id.usage == 'supplier' and self.location_dest_id.usage == 'customer'
+
@api.model
def _run_fifo(self, move, quantity=None):
move.ensure_one()
@@ -298,6 +306,21 @@ class StockMove(models.Model):
'value': value if quantity is None else self.value + value,
'price_unit': value / valued_quantity,
})
+ elif self._is_dropshipped():
+ curr_rounding = self.company_id.currency_id.rounding
+ if self.product_id.cost_method in ['fifo']:
+ price_unit = self._get_price_unit()
+ # see test_dropship_fifo_perpetual_anglosaxon_ordered
+ self.product_id.standard_price = price_unit
+ else:
+ price_unit = self.product_id.standard_price
+ value = float_round(self.product_qty * price_unit, precision_rounding=curr_rounding)
+ # In move have a positive value, out move have a negative value, let's arbitrary say
+ # dropship are positive.
+ self.write({
+ 'value': value,
+ 'price_unit': price_unit,
+ })
def _action_done(self):
self.product_price_update_before_done()
@@ -319,7 +342,7 @@ class StockMove(models.Model):
if company_src and company_dst and company_src.id != company_dst.id:
raise UserError(_("The move lines are not in a consistent states: they are doing an intercompany in a single step while they should go through the intercompany transit location."))
move._run_valuation()
- for move in res.filtered(lambda m: m.product_id.valuation == 'real_time' and (m._is_in() or m._is_out())):
+ for move in res.filtered(lambda m: m.product_id.valuation == 'real_time' and (m._is_in() or m._is_out() or m._is_dropshipped())):
move._account_entry_move()
return res
@@ -402,6 +425,13 @@ class StockMove(models.Model):
if remaining_value_before_vacuum < 0:
corrected_value += remaining_value_before_vacuum
+ # If `corrected_value` is 0, absolutely do *not* call `_account_entry_move`. We
+ # force the amount in the context, but in the case it is 0 it'll create an entry
+ # for the entire cost of the move. This case happens when the candidates moves
+ # entirely compensate the problematic move.
+ if not corrected_value:
+ continue
+
if move._is_in():
# If we just compensated an IN move that has a negative remaining
# quantity, it means the move has returned more items than it received.
@@ -582,7 +612,7 @@ class StockMove(models.Model):
else:
self.with_context(force_company=company_from.id)._create_account_move_line(acc_valuation, acc_dest, journal_id)
- if self.company_id.anglo_saxon_accounting and self.location_id.usage == 'supplier' and self.location_dest_id.usage == 'customer':
+ if self.company_id.anglo_saxon_accounting and self._is_dropshipped():
# Creates an account entry from stock_input to stock_output on a dropship move. https://github.com/odoo/odoo/issues/12687
journal_id, acc_src, acc_dest, acc_valuation = self._get_accounting_data_for_valuation()
self.with_context(force_company=self.company_id.id)._create_account_move_line(acc_src, acc_dest, journal_id)
diff --git a/addons/stock_account/tests/test_stockvaluation.py b/addons/stock_account/tests/test_stockvaluation.py
index fa0c8e394af..bcd9cd5e21d 100644
--- a/addons/stock_account/tests/test_stockvaluation.py
+++ b/addons/stock_account/tests/test_stockvaluation.py
@@ -1363,7 +1363,8 @@ class TestStockValuation(TransactionCase):
def test_fifo_negative_2(self):
""" Receives 10 units, send more, the extra quantity should be valued at the last fifo
- price, running the vacuum should not do anything.
+ price, running the vacuum should not do anything. Receive 2 units at the price the two
+ extra units were sent, check that no accounting entries are created.
"""
self.product1.product_tmpl_id.cost_method = 'fifo'
@@ -1462,9 +1463,6 @@ class TestStockValuation(TransactionCase):
self.assertEqual(len(move1.account_move_ids), 1)
self.assertEqual(len(move2.account_move_ids), 1)
- # ---------------------------------------------------------------------
- # Ending
- # ---------------------------------------------------------------------
self.assertEqual(self.product1.qty_available, -2)
self.assertEqual(self.product1.stock_value, -20)
self.assertEqual(sum(self._get_stock_input_move_lines().mapped('debit')), 0)
@@ -1474,6 +1472,64 @@ class TestStockValuation(TransactionCase):
self.assertEqual(sum(self._get_stock_output_move_lines().mapped('debit')), 120)
self.assertEqual(sum(self._get_stock_output_move_lines().mapped('credit')), 0)
+ # Now receive exactly the extra units at exactly the price sent, no
+ # accounting entries should be created after the vacuum.
+ # ---------------------------------------------------------------------
+ # Receive 2@10
+ # ---------------------------------------------------------------------
+ move3 = self.env['stock.move'].create({
+ 'name': '10 in',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ 'price_unit': 10,
+ 'move_line_ids': [(0, 0, {
+ 'product_id': self.product1.id,
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 2.0,
+ })]
+ })
+ move3._action_confirm()
+ move3._action_done()
+
+ # ---------------------------------------------------------------------
+ # Run the vacuum
+ # ---------------------------------------------------------------------
+ self.env['stock.move']._run_fifo_vacuum()
+
+ # ---------------------------------------------------------------------
+ # Ending
+ # ---------------------------------------------------------------------
+ self.assertEqual(move1.value, 100.0)
+ self.assertEqual(move1.remaining_qty, 0.0)
+ self.assertEqual(move1.price_unit, 10.0)
+ self.assertEqual(move1.remaining_value, 0.0)
+ self.assertEqual(move2.value, -120.0)
+ self.assertEqual(move2.remaining_qty, 0)
+ self.assertEqual(move2.remaining_value, 0)
+ self.assertEqual(move3.value, 20)
+ self.assertEqual(move3.remaining_qty, 0.0)
+ self.assertEqual(move3.price_unit, 10.0)
+ self.assertEqual(move3.remaining_value, 0.0)
+
+ self.assertEqual(len(move1.account_move_ids), 1)
+ self.assertEqual(len(move2.account_move_ids), 1)
+ self.assertEqual(len(move3.account_move_ids), 1) # the created account move is due to the receipt
+
+ # nothing should have changed in the accounting regarding the output
+ self.assertEqual(self.product1.qty_available, 0)
+ self.assertEqual(self.product1.stock_value, 0)
+ self.assertEqual(sum(self._get_stock_input_move_lines().mapped('debit')), 0)
+ self.assertEqual(sum(self._get_stock_input_move_lines().mapped('credit')), 120)
+ self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('debit')), 120)
+ self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('credit')), 120)
+ self.assertEqual(sum(self._get_stock_output_move_lines().mapped('debit')), 120)
+ self.assertEqual(sum(self._get_stock_output_move_lines().mapped('credit')), 0)
+
def test_fifo_negative_3(self):
""" Receives 10 units, send 10 units, then send more: the extra quantity should be valued
at the last fifo price, running the vacuum should not do anything.
diff --git a/addons/stock_account/views/res_config_settings_views.xml b/addons/stock_account/views/res_config_settings_views.xml
index 9453a8c763f..332e1307ba5 100644
--- a/addons/stock_account/views/res_config_settings_views.xml
+++ b/addons/stock_account/views/res_config_settings_views.xml
@@ -9,7 +9,7 @@
<div id="production_lot_info" position="after">
<h2>Costing</h2>
<div class="row mt16 o_settings_container">
- <div class="col-xs-12 col-md-6 o_setting_box" title="Affect landed costs on receipt operations and split them among products to update their cost price.">
+ <div class="col-xs-12 col-md-6 o_setting_box" title="Affect landed costs on reception operations and split them among products to update their cost price.">
<div class="o_setting_left_pane">
<field name="module_stock_landed_costs"/>
</div>
diff --git a/addons/stock_account/views/stock_account_views.xml b/addons/stock_account/views/stock_account_views.xml
index bd9f7f523f8..70c1d04e9da 100644
--- a/addons/stock_account/views/stock_account_views.xml
+++ b/addons/stock_account/views/stock_account_views.xml
@@ -19,7 +19,7 @@
<field name="inherit_id" ref="stock.view_inventory_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='date']" position="after">
- <field name="accounting_date"/>
+ <field name="accounting_date" attrs="{'readonly':[('state','!=', 'draft')]}"/>
</xpath>
</field>
</record>
diff --git a/addons/stock_dropshipping/tests/__init__.py b/addons/stock_dropshipping/tests/__init__.py
index f4159ef6e7f..c060ad7247b 100644
--- a/addons/stock_dropshipping/tests/__init__.py
+++ b/addons/stock_dropshipping/tests/__init__.py
@@ -5,3 +5,5 @@ from . import test_crossdock
from . import test_dropship
from . import test_lifo_price
from . import test_procurement_exception
+from . import test_stockvaluation
+
diff --git a/addons/stock_dropshipping/tests/test_lifo_price.py b/addons/stock_dropshipping/tests/test_lifo_price.py
index 36574f186bc..5fc1864de1c 100644
--- a/addons/stock_dropshipping/tests/test_lifo_price.py
+++ b/addons/stock_dropshipping/tests/test_lifo_price.py
@@ -17,9 +17,6 @@ class TestLifoPrice(common.TransactionCase):
self._load('account', 'test', 'account_minimal_test.xml')
self._load('stock_account', 'test', 'stock_valuation_account.xml')
- # Set the company currency as EURO for the sake of repeatibility
- self.env.ref('base.main_company').write({'currency_id': self.env.ref('base.EUR').id})
-
# Set product category removal strategy as LIFO
product_category_001 = self.env['product.category'].create({
'name': 'Lifo Category',
diff --git a/addons/stock_dropshipping/tests/test_stockvaluation.py b/addons/stock_dropshipping/tests/test_stockvaluation.py
new file mode 100644
index 00000000000..64bcbd0d77f
--- /dev/null
+++ b/addons/stock_dropshipping/tests/test_stockvaluation.py
@@ -0,0 +1,269 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.addons.account.tests.account_test_classes import AccountingTestCase
+from odoo.tests import tagged
+
+@tagged('post_install', '-at_install')
+class TestStockValuation(AccountingTestCase):
+ def setUp(self):
+ super(TestStockValuation, self).setUp()
+ self.supplier_location = self.env.ref('stock.stock_location_suppliers')
+ self.stock_location = self.env.ref('stock.stock_location_stock')
+ self.partner_id = self.env.ref('base.res_partner_1')
+ self.product1 = self.env.ref('product.product_product_8')
+
+ self.acc_payable = self.env['account.account'].search([('name', '=', 'Account Payable')]).id
+ self.acc_expense = self.env['account.account'].search([('name', '=', 'Expenses')]).id
+ self.acc_receivable = self.env['account.account'].search([('name', '=', 'Account Receivable')]).id
+ self.acc_sale = self.env['account.account'].search([('name', '=', 'Product Sales')]).id
+ self.acc_stock_in = self.env['account.account'].search([('name', '=', 'Stock Interim Account (Received)')]).id
+ self.acc_stock_out = self.env['account.account'].search([('name', '=', 'Stock Interim Account (Delivered)')]).id
+
+ def _dropship_product1(self):
+ # enable the dropship and MTO route on the product
+ dropshipping_route = self.env.ref('stock_dropshipping.route_drop_shipping')
+ mto_route = self.env.ref('stock.route_warehouse0_mto')
+ self.product1.write({'route_ids': [(6, 0, [dropshipping_route.id, mto_route.id])]})
+
+ # add a vendor
+ vendor1 = self.env['res.partner'].create({'name': 'vendor1'})
+ seller1 = self.env['product.supplierinfo'].create({
+ 'name': vendor1.id,
+ 'price': 8,
+ })
+ self.product1.write({'seller_ids': [(6, 0, [seller1.id])]})
+
+ # sell one unit of this product
+ customer1 = self.env['res.partner'].create({'name': 'customer1'})
+ sale_order1 = self.env['sale.order'].create({
+ 'partner_id': customer1.id,
+ 'partner_invoice_id': customer1.id,
+ 'partner_shipping_id': customer1.id,
+ 'order_line': [(0, 0, {
+ 'name': self.product1.name,
+ 'product_id': self.product1.id,
+ 'product_uom_qty': 1,
+ 'product_uom': self.product1.uom_id.id,
+ 'price_unit': 12,
+ })],
+ 'pricelist_id': self.env.ref('product.list0').id,
+ 'picking_policy': 'direct',
+ })
+ sale_order1.action_confirm()
+
+ # confirm the purchase order
+ purchase_order1 = self.env['purchase.order'].search([('group_id', '=', sale_order1.procurement_group_id.id)])
+ purchase_order1.button_confirm()
+
+ # validate the dropshipping picking
+ self.assertEqual(len(sale_order1.picking_ids), 1)
+ self.assertEqual(sale_order1.picking_ids.move_lines._is_dropshipped(), True)
+ wizard = sale_order1.picking_ids.button_validate()
+ immediate_transfer = self.env[wizard['res_model']].browse(wizard['res_id'])
+ immediate_transfer.process()
+ self.assertEqual(sale_order1.picking_ids.state, 'done')
+
+ # create the vendor bill
+ vendor_bill1 = self.env['account.invoice'].create({
+ 'partner_id': vendor1.id,
+ 'purchase_id': purchase_order1.id,
+ 'account_id': vendor1.property_account_payable_id.id,
+ 'type': 'in_invoice',
+ })
+ vendor_bill1.purchase_order_change()
+ vendor_bill1.action_invoice_open()
+
+ # create the customer invoice
+ customer_invoice1_id = sale_order1.action_invoice_create()
+ customer_invoice1 = self.env['account.invoice'].browse(customer_invoice1_id)
+ customer_invoice1.action_invoice_open()
+
+ all_amls = vendor_bill1.move_id.line_ids + customer_invoice1.move_id.line_ids
+ if sale_order1.picking_ids.move_lines.account_move_ids:
+ all_amls |= sale_order1.picking_ids.move_lines.account_move_ids.line_ids
+ return all_amls
+
+ def _check_results(self, expected_aml, expected_aml_count, all_amls):
+ # Construct a dict similar to `expected_aml` with `all_amls` in order to
+ # compare them.
+ result_aml = {}
+ for aml in all_amls:
+ account_id = aml.account_id.id
+ if result_aml.get(account_id):
+ debit = result_aml[account_id][0]
+ credit = result_aml[account_id][1]
+ result_aml[account_id] = (debit + aml.debit, credit + aml.credit)
+ else:
+ result_aml[account_id] = (aml.debit, aml.credit)
+
+ self.assertEqual(len(all_amls), expected_aml_count)
+
+ for k, v in expected_aml.items():
+ self.assertEqual(result_aml[k], v)
+
+ # -------------------------------------------------------------------------
+ # Continental
+ # -------------------------------------------------------------------------
+ def test_dropship_standard_perpetual_continental_ordered(self):
+ self.env.user.company_id.anglo_saxon_accounting = False
+ self.product1.product_tmpl_id.cost_method = 'standard'
+ self.product1.product_tmpl_id.standard_price = 10
+ self.product1.product_tmpl_id.valuation = 'real_time'
+ self.product1.product_tmpl_id.invoice_policy = 'order'
+
+ all_amls = self._dropship_product1()
+
+ expected_aml = {
+ self.acc_payable: (0.0, 8.0),
+ self.acc_expense: (8.0, 0.0),
+ self.acc_receivable: (12.0, 0.0),
+ self.acc_sale: (0.0, 12.0),
+ }
+
+ self._check_results(expected_aml, 4, all_amls)
+
+ def test_dropship_standard_perpetual_continental_delivered(self):
+ self.env.user.company_id.anglo_saxon_accounting = False
+ self.product1.product_tmpl_id.cost_method = 'standard'
+ self.product1.product_tmpl_id.standard_price = 10
+ self.product1.product_tmpl_id.valuation = 'real_time'
+ self.product1.product_tmpl_id.invoice_policy = 'delivery'
+
+ all_amls = self._dropship_product1()
+
+ expected_aml = {
+ self.acc_payable: (0.0, 8.0),
+ self.acc_expense: (8.0, 0.0),
+ self.acc_receivable: (12.0, 0.0),
+ self.acc_sale: (0.0, 12.0),
+ }
+
+ self._check_results(expected_aml, 4, all_amls)
+
+ def test_dropship_fifo_perpetual_continental_ordered(self):
+ self.env.user.company_id.anglo_saxon_accounting = False
+ self.product1.product_tmpl_id.cost_method = 'fifo'
+ self.product1.product_tmpl_id.standard_price = 10
+ self.product1.product_tmpl_id.valuation = 'real_time'
+ self.product1.product_tmpl_id.invoice_policy = 'order'
+
+ all_amls = self._dropship_product1()
+
+ expected_aml = {
+ self.acc_payable: (0.0, 8.0),
+ self.acc_expense: (8.0, 0.0),
+ self.acc_receivable: (12.0, 0.0),
+ self.acc_sale: (0.0, 12.0),
+ }
+
+ self._check_results(expected_aml, 4, all_amls)
+
+ def test_dropship_fifo_perpetual_continental_delivered(self):
+ self.env.user.company_id.anglo_saxon_accounting = False
+
+ self.product1.product_tmpl_id.cost_method = 'fifo'
+ self.product1.product_tmpl_id.standard_price = 10
+ self.product1.product_tmpl_id.valuation = 'real_time'
+ self.product1.product_tmpl_id.invoice_policy = 'delivery'
+
+ all_amls = self._dropship_product1()
+
+ expected_aml = {
+ self.acc_payable: (0.0, 8.0),
+ self.acc_expense: (8.0, 0.0),
+ self.acc_receivable: (12.0, 0.0),
+ self.acc_sale: (0.0, 12.0),
+ }
+
+ self._check_results(expected_aml, 4, all_amls)
+
+ # -------------------------------------------------------------------------
+ # Anglosaxon
+ # -------------------------------------------------------------------------
+ def test_dropship_standard_perpetual_anglosaxon_ordered(self):
+ self.env.user.company_id.anglo_saxon_accounting = True
+ self.product1.product_tmpl_id.cost_method = 'standard'
+ self.product1.product_tmpl_id.standard_price = 10
+ self.product1.product_tmpl_id.valuation = 'real_time'
+ self.product1.product_tmpl_id.invoice_policy = 'order'
+
+ all_amls = self._dropship_product1()
+
+ expected_aml = {
+ self.acc_payable: (0.0, 8.0),
+ self.acc_expense: (10.0, 0.0),
+ self.acc_receivable: (12.0, 0.0),
+ self.acc_sale: (0.0, 12.0),
+ self.acc_stock_in: (8.0, 10.0),
+ self.acc_stock_out: (10.0, 10.0),
+ }
+ # Interim IN is not balanced because because there's a difference between the po line
+ # price unit and the standard price. We could set a price difference account on the
+ # category to compensate.
+
+ self._check_results(expected_aml, 8, all_amls)
+
+ def test_dropship_standard_perpetual_anglosaxon_delivered(self):
+ self.env.user.company_id.anglo_saxon_accounting = True
+ self.product1.product_tmpl_id.cost_method = 'standard'
+ self.product1.product_tmpl_id.standard_price = 10
+ self.product1.product_tmpl_id.valuation = 'real_time'
+ self.product1.product_tmpl_id.invoice_policy = 'delivery'
+
+ all_amls = self._dropship_product1()
+
+ expected_aml = {
+ self.acc_payable: (0.0, 8.0),
+ self.acc_expense: (10.0, 0.0),
+ self.acc_receivable: (12.0, 0.0),
+ self.acc_sale: (0.0, 12.0),
+ self.acc_stock_in: (8.0, 10.0),
+ self.acc_stock_out: (10.0, 10.0),
+ }
+ # Interim IN is not balanced because because there's a difference between the po line
+ # price unit and the standard price. We could set a price difference account on the
+ # category to compensate.
+
+ self._check_results(expected_aml, 8, all_amls)
+
+ def test_dropship_fifo_perpetual_anglosaxon_ordered(self):
+ self.env.user.company_id.anglo_saxon_accounting = True
+ self.product1.product_tmpl_id.cost_method = 'fifo'
+ self.product1.product_tmpl_id.standard_price = 10
+ self.product1.product_tmpl_id.valuation = 'real_time'
+ self.product1.product_tmpl_id.invoice_policy = 'order'
+
+ all_amls = self._dropship_product1()
+
+ expected_aml = {
+ self.acc_payable: (0.0, 8.0),
+ self.acc_expense: (8.0, 0.0),
+ self.acc_receivable: (12.0, 0.0),
+ self.acc_sale: (0.0, 12.0),
+ self.acc_stock_in: (8.0, 8.0),
+ self.acc_stock_out: (8.0, 8.0),
+ }
+
+ self._check_results(expected_aml, 8, all_amls)
+
+ def test_dropship_fifo_perpetual_anglosaxon_delivered(self):
+ self.env.user.company_id.anglo_saxon_accounting = True
+ self.product1.product_tmpl_id.cost_method = 'fifo'
+ self.product1.product_tmpl_id.standard_price = 10
+ self.product1.product_tmpl_id.valuation = 'real_time'
+ self.product1.product_tmpl_id.invoice_policy = 'delivery'
+
+ all_amls = self._dropship_product1()
+
+ expected_aml = {
+ self.acc_payable: (0.0, 8.0),
+ self.acc_expense: (8.0, 0.0),
+ self.acc_receivable: (12.0, 0.0),
+ self.acc_sale: (0.0, 12.0),
+ self.acc_stock_in: (8.0, 8.0),
+ self.acc_stock_out: (8.0, 8.0),
+ }
+
+ self._check_results(expected_aml, 8, all_amls)
+
diff --git a/addons/stock_landed_costs/views/stock_landed_cost_views.xml b/addons/stock_landed_costs/views/stock_landed_cost_views.xml
index 3b9c650227f..bf712eb6474 100644
--- a/addons/stock_landed_costs/views/stock_landed_cost_views.xml
+++ b/addons/stock_landed_costs/views/stock_landed_cost_views.xml
@@ -22,7 +22,7 @@
<group>
<group>
<field name="date"/>
- <field name="picking_ids" widget="many2many_tags" options="{'no_create_edit': True}" domain="[('state', '=', 'done')]"/>
+ <field name="picking_ids" widget="many2many_tags" options="{'no_create_edit': True}" domain="[('state', '=', 'done'), ('picking_type_code', '!=', 'internal')]"/>
</group>
<group>
<field name="account_journal_id"/>
diff --git a/addons/survey/models/survey.py b/addons/survey/models/survey.py
index 04ff7ccd558..7350d598ab0 100644
--- a/addons/survey/models/survey.py
+++ b/addons/survey/models/survey.py
@@ -680,6 +680,7 @@ class SurveyLabel(models.Model):
value = fields.Char('Suggested value', translate=True, required=True)
quizz_mark = fields.Float('Score for this choice', help="A positive score indicates a correct choice; a negative or null score indicates a wrong answer")
+ @api.one
@api.constrains('question_id', 'question_id_2')
def _check_question_not_empty(self):
"""Ensure that field question_id XOR field question_id_2 is not null"""
diff --git a/addons/test_mail/tests/test_performance.py b/addons/test_mail/tests/test_performance.py
index c51a0ec19dc..4b69f0da6d7 100644
--- a/addons/test_mail/tests/test_performance.py
+++ b/addons/test_mail/tests/test_performance.py
@@ -177,7 +177,7 @@ class TestAdvMailPerformance(TransactionCase):
self.user_test.write({'notification_type': 'email'})
record = self.env['mail.test.track'].create({'name': 'Test'})
- with self.assertQueryCount(admin=92, emp=118): # test_mail only: 90 - 116
+ with self.assertQueryCount(admin=92, emp=120): # test_mail only: 90 - 116
record.write({
'user_id': self.user_test.id,
})
@@ -455,7 +455,7 @@ class TestHeavyMailPerformance(TransactionCase):
})
self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id)
- with self.assertQueryCount(admin=94, emp=122): # test_mail only: 92 - 120
+ with self.assertQueryCount(admin=94, emp=123): # test_mail only: 92 - 120
rec.write({'user_id': self.user_portal.id})
self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id)
@@ -478,7 +478,7 @@ class TestHeavyMailPerformance(TransactionCase):
customer_id = self.customer.id
user_id = self.user_portal.id
- with self.assertQueryCount(admin=326, emp=386): # test_mail only: 319 - 379
+ with self.assertQueryCount(admin=326, emp=388): # test_mail only: 319 - 379
rec = self.env['mail.test.full'].create({
'name': 'Test',
'umbrella_id': umbrella_id,
diff --git a/addons/web/static/lib/qweb/qweb2.js b/addons/web/static/lib/qweb/qweb2.js
index c4bb7128e9b..94a0afaa32a 100644
--- a/addons/web/static/lib/qweb/qweb2.js
+++ b/addons/web/static/lib/qweb/qweb2.js
@@ -358,7 +358,12 @@ QWeb2.Engine = (function() {
req.open('GET', s, async);
if (async) {
req.addEventListener("load", function() {
- if (req.status == 200) {
+ // 0, not being a valid HTTP status code, is used by browsers
+ // to indicate success for a non-http xhr response
+ // (for example, using the file:// protocol)
+ // https://developer.mozilla.org/fr/docs/Web/API/XMLHttpRequest
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=331610
+ if (req.status == 200 || req.status == 0) {
callback(null, self._parse_from_request(req));
} else {
callback(new Error("Can't load template " + s + ", http status " + req.status));
diff --git a/addons/web/static/src/js/chrome/abstract_web_client.js b/addons/web/static/src/js/chrome/abstract_web_client.js
index b4b192c1ea9..9fe38adbde6 100644
--- a/addons/web/static/src/js/chrome/abstract_web_client.js
+++ b/addons/web/static/src/js/chrome/abstract_web_client.js
@@ -68,6 +68,7 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, {
.load_filters(event.data.dataset, event.data.action_id)
.then(event.data.on_success);
},
+ create_filter: '_onCreateFilter',
push_state: '_onPushState',
show_effect: '_onShowEffect',
// session
@@ -338,6 +339,18 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, {
//--------------------------------------------------------------------------
/**
+ * @private
+ * @param {OdooEvent} e
+ * @param {Object} e.data.filter the filter description
+ * @param {function} e.data.on_success called when the RPC succeeds with its
+ * returned value as argument
+ */
+ _onCreateFilter: function (e) {
+ data_manager
+ .create_filter(e.data.filter)
+ .then(e.data.on_success);
+ },
+ /**
* Displays a warning in a dialog of with the NotificationManager
*
* @private
diff --git a/addons/web/static/src/js/chrome/action_manager.js b/addons/web/static/src/js/chrome/action_manager.js
index e213b55189d..1e9c52238bc 100644
--- a/addons/web/static/src/js/chrome/action_manager.js
+++ b/addons/web/static/src/js/chrome/action_manager.js
@@ -136,9 +136,10 @@ var ActionManager = Widget.extend({
* is useful when we come from a loadState())
* @param {boolean} [options.replace_last_action=false] set to true to
* replace last part of the breadcrumbs with the action
- * @return {Deferred} resolved when the action is loaded and appended to the
- * DOM ; rejected if the action can't be executed (e.g. if doAction has
- * been called to execute another action before this one was complete).
+ * @return {$.Deferred<Object>} resolved with the action when the action is
+ * loaded and appended to the DOM ; rejected if the action can't be
+ * executed (e.g. if doAction has been called to execute another action
+ * before this one was complete).
*/
doAction: function (action, options) {
var self = this;
@@ -175,7 +176,9 @@ var ActionManager = Widget.extend({
self._preprocessAction(action, options);
- return self._handleAction(action, options);
+ return self._handleAction(action, options).then(function () {
+ return action;
+ });
});
},
/**
@@ -349,6 +352,11 @@ var ActionManager = Widget.extend({
// toggle the fullscreen mode for actions in target='fullscreen'
self._toggleFullscreen();
+ // now that the action has been executed, force its 'pushState'
+ // flag to 'true', as we don't want to prevent its controller
+ // from pushing its state if it changes in the future
+ action.pushState = true;
+
return action;
})
.fail(function () {
diff --git a/addons/web/static/src/js/chrome/action_manager_act_window.js b/addons/web/static/src/js/chrome/action_manager_act_window.js
index 9cfa99a9e7d..dfb512900e8 100644
--- a/addons/web/static/src/js/chrome/action_manager_act_window.js
+++ b/addons/web/static/src/js/chrome/action_manager_act_window.js
@@ -21,6 +21,7 @@ ActionManager.include({
custom_events: _.extend({}, ActionManager.prototype.custom_events, {
env_updated: '_onEnvUpdated',
execute_action: '_onExecuteAction',
+ get_controller_context: '_onGetControllerContext',
search: '_onSearch',
switch_view: '_onSwitchView',
}),
@@ -254,18 +255,26 @@ ActionManager.include({
action.env = self._generateActionEnv(action, options);
action.controllers = {};
- // select the first view to display
- var lazyLoadFirstView = false;
+ // select the first view to display, and optionally the main view
+ // which will be lazyloaded
var firstView = options.viewType && _.findWhere(views, {type: options.viewType});
+ var mainView;
if (firstView) {
if (!firstView.multiRecord && views[0].multiRecord) {
- lazyLoadFirstView = true;
+ mainView = views[0];
}
} else {
firstView = views[0];
}
- if (config.device.isMobile && !firstView.isMobileFriendly) {
- firstView = _.findWhere(action.views, {isMobileFriendly: true}) || firstView;
+
+ // use mobile-friendly view by default in mobile, if possible
+ if (config.device.isMobile) {
+ if (!firstView.isMobileFriendly) {
+ firstView = self._findMobileView(views, firstView.multiRecord) || firstView;
+ }
+ if (mainView && !mainView.isMobileFriendly) {
+ mainView = self._findMobileView(views, mainView.multiRecord) || mainView;
+ }
}
var def;
@@ -279,8 +288,8 @@ ActionManager.include({
return $.when(def).then(function () {
var defs = [];
defs.push(self._createViewController(action, firstView.type));
- if (lazyLoadFirstView) {
- defs.push(self._createViewController(action, views[0].type, {}, {lazy: true}));
+ if (mainView) {
+ defs.push(self._createViewController(action, mainView.type, {}, {lazy: true}));
}
return $.when.apply($, defs);
}).then(function (controller, lazyLoadedController) {
@@ -297,6 +306,22 @@ ActionManager.include({
});
},
/**
+ * Helper function to find the first mobile-friendly view, if any.
+ *
+ * @private
+ * @param {Array} views an array of views
+ * @param {boolean} multiRecord set to true iff we search for a multiRecord
+ * view
+ * @returns {Object|undefined} a mobile-friendly view of the requested
+ * multiRecord type, undefined if there is no such view
+ */
+ _findMobileView: function (views, multiRecord) {
+ return _.findWhere(views, {
+ isMobileFriendly: true,
+ multiRecord: multiRecord,
+ });
+ },
+ /**
* Generates the initial environment of a given action, which is a dict
* containing information like the model name, the domain, the context...
* That information is shared between the controllers of the corresponding
@@ -716,6 +741,20 @@ ActionManager.include({
});
},
/**
+ * Handles a context request: provides to the caller the context of the
+ * current controller.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ * @param {function} ev.data.callback used to send the requested context
+ */
+ _onGetControllerContext: function (ev) {
+ ev.stopPropagation();
+ var currentController = this.getCurrentController();
+ var context = currentController && currentController.widget.getContext();
+ ev.data.callback(context || {});
+ },
+ /**
* Called when there is a change in the search view, so the current action's
* environment needs to be updated with the new domain, context and groupby.
*
diff --git a/addons/web/static/src/js/chrome/control_panel.js b/addons/web/static/src/js/chrome/control_panel.js
index 16b22761581..03b9e756bb7 100644
--- a/addons/web/static/src/js/chrome/control_panel.js
+++ b/addons/web/static/src/js/chrome/control_panel.js
@@ -154,7 +154,9 @@ var ControlPanel = Widget.extend({
this._attach_content(new_cp_content);
// Update the searchview and switch buttons
- this._update_search_view(status.searchview, status.search_view_hidden, status.groupable);
+ if (status.searchview || options.clear) {
+ this._update_search_view(status.searchview, status.search_view_hidden, status.groupable);
+ }
if (status.active_view_selector) {
this._update_switch_buttons(status.active_view_selector);
}
diff --git a/addons/web/static/src/js/chrome/search_menus.js b/addons/web/static/src/js/chrome/search_menus.js
index 38f2bb7374c..d33cdce6500 100644
--- a/addons/web/static/src/js/chrome/search_menus.js
+++ b/addons/web/static/src/js/chrome/search_menus.js
@@ -97,10 +97,15 @@ return Widget.extend({
}
var user_context = this.getSession().user_context;
var search = this.searchview.build_search_data();
- var view_context = this.action.env.context;
+ var controllerContext;
+ this.trigger_up('get_controller_context', {
+ callback: function (ctx) {
+ controllerContext = ctx;
+ },
+ });
var results = pyeval.eval_domains_and_contexts({
domains: search.domains,
- contexts: [user_context].concat(search.contexts.concat(view_context || [])),
+ contexts: [user_context].concat(search.contexts.concat(controllerContext || [])),
group_by_seq: search.groupbys || [],
});
if (!_.isEmpty(results.group_by)) {
@@ -123,7 +128,7 @@ return Widget.extend({
is_default: default_filter,
action_id: this.action_id,
};
- return data_manager.create_filter(filter).done(function (id) {
+ return this._createFilter(filter).then(function (id) {
filter.id = id;
self.toggle_save_menu(false);
self.$save_name.find('input').val('').prop('checked', false);
@@ -273,6 +278,26 @@ return Widget.extend({
}
});
},
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Creates a new search view filter.
+ *
+ * @private
+ * @param {Object} filter the filter description
+ * @returns {$.Deferred} resolved with the RPC's result when it succeeds
+ */
+ _createFilter: function (filter) {
+ var def = $.Deferred();
+ this.trigger_up('create_filter', {
+ filter: filter,
+ on_success: def.resolve.bind(def),
+ });
+ return def;
+ },
});
});
diff --git a/addons/web/static/src/js/core/utils.js b/addons/web/static/src/js/core/utils.js
index 251cd2b508c..ffeb4a1de87 100644
--- a/addons/web/static/src/js/core/utils.js
+++ b/addons/web/static/src/js/core/utils.js
@@ -228,7 +228,7 @@ var utils = {
* @returns {boolean}
*/
is_bin_size: function (v) {
- return (/^\d+(\.\d*)? \w+$/).test(v);
+ return (/^\d+(\.\d*)? [^0-9]+$/).test(v);
},
/**
* @param {any} node
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index 64783e09cdd..2a9a2906501 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -1008,7 +1008,10 @@ var FieldX2Many = AbstractField.extend({
* @param {OdooEvent} ev
*/
_onDiscardChanges: function (ev) {
- ev.data.fieldName = this.name;
+ if (ev.target !== this) {
+ ev.stopPropagation();
+ this.trigger_up('discard_changes', _.extend({}, ev.data, {fieldName: this.name}));
+ }
},
/**
* Called when the renderer asks to edit a line, in that case simply tells
diff --git a/addons/web/static/src/js/services/crash_manager.js b/addons/web/static/src/js/services/crash_manager.js
index 9c7914eeebf..ccc7e089e65 100644
--- a/addons/web/static/src/js/services/crash_manager.js
+++ b/addons/web/static/src/js/services/crash_manager.js
@@ -103,7 +103,7 @@ var CrashManager = core.Class.extend({
if (!this.active) {
return;
}
- new Dialog(this, {
+ return new Dialog(this, {
size: 'medium',
title: _.str.capitalize(error.type || error.message) || _t("Odoo Warning"),
subtitle: error.data.title,
@@ -149,10 +149,10 @@ var CrashManager = core.Class.extend({
clipboard.destroy();
});
- dialog.open();
+ return dialog.open();
},
show_message: function(exception) {
- this.show_error({
+ return this.show_error({
type: _t("Odoo Client Error"),
message: exception,
data: {debug: ""}
diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js
index 7998a6ca8f1..05031ca2463 100644
--- a/addons/web/static/src/js/views/basic/basic_controller.js
+++ b/addons/web/static/src/js/views/basic/basic_controller.js
@@ -272,6 +272,13 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
* @returns {Deferred}
*/
_confirmChange: function (id, fields, e) {
+ if (e.name === 'discard_changes' && e.target.reset) {
+ // the target of the discard event is a field widget. In that
+ // case, we simply want to reset the specific field widget,
+ // not the full view
+ return e.target.reset(this.model.get(e.target.dataPointID), e, true);
+ }
+
var state = this.model.get(this.handle);
return this.renderer.confirmChange(state, id, fields, e);
},
diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js
index 2ef65cbd1b6..3013cb32f85 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -198,6 +198,90 @@ var BasicModel = AbstractModel.extend({
});
},
/**
+ * Add and process default values for a given record. Those values are
+ * parsed and stored in the '_changes' key of the record. For relational
+ * fields, sub-dataPoints are created, and missing relational data is
+ * fetched. Also generate default values for fields with no given value.
+ * Typically, this function is called with the result of a 'default_get'
+ * RPC, to populate a newly created dataPoint. It may also be called when a
+ * one2many subrecord is open in a form view (dialog), to generate the
+ * default values for the fields displayed in the o2m form view, but not in
+ * the list or kanban (mainly to correctly create sub-dataPoints for
+ * relational fields).
+ *
+ * @param {string} recordID local id for a record
+ * @param {Object} values dict of default values for the given record
+ * @param {Object} [options]
+ * @param {string} [options.viewType] current viewType. If not set, we will
+ * assume main viewType from the record
+ * @param {Array} [options.fieldNames] list of field names for which a
+ * default value must be generated (used to complete the values dict)
+ * @returns {Deferred}
+ */
+ applyDefaultValues: function (recordID, values, options) {
+ options = options || {};
+ var record = this.localData[recordID];
+ var viewType = options.viewType || record.viewType;
+ var fieldNames = options.fieldNames || Object.keys(record.fieldsInfo[viewType]);
+ var field;
+ var fieldName;
+ record._changes = record._changes || {};
+
+ // ignore values for non requested fields (for instance, fields that are
+ // not in the view)
+ values = _.pick(values, fieldNames);
+
+ // fill default values for missing fields
+ for (var i = 0; i < fieldNames.length; i++) {
+ fieldName = fieldNames[i];
+ if (!(fieldName in values) && !(fieldName in record._changes)) {
+ field = record.fields[fieldName];
+ if (field.type === 'float' ||
+ field.type === 'integer' ||
+ field.type === 'monetary') {
+ values[fieldName] = 0;
+ } else if (field.type === 'one2many' || field.type === 'many2many') {
+ values[fieldName] = [];
+ } else {
+ values[fieldName] = null;
+ }
+ }
+ }
+
+ // parse each value and create dataPoints for relational fields
+ var defs = [];
+ for (fieldName in values) {
+ field = record.fields[fieldName];
+ record.data[fieldName] = null;
+ var dp;
+ if (field.type === 'many2one' && values[fieldName]) {
+ dp = this._makeDataPoint({
+ context: record.context,
+ data: {id: values[fieldName]},
+ modelName: field.relation,
+ parentID: record.id,
+ });
+ record._changes[fieldName] = dp.id;
+ } else if (field.type === 'reference' && values[fieldName]) {
+ var ref = values[fieldName].split(',');
+ dp = this._makeDataPoint({
+ context: record.context,
+ data: {id: parseInt(ref[1])},
+ modelName: ref[0],
+ parentID: record.id,
+ });
+ defs.push(this._fetchNameGet(dp));
+ record._changes[fieldName] = dp.id;
+ } else if (field.type === 'one2many' || field.type === 'many2many') {
+ defs.push(this._processX2ManyCommands(record, fieldName, values[fieldName], options));
+ } else {
+ record._changes[fieldName] = this._parseServerValue(field, values[fieldName]);
+ }
+ }
+
+ return $.when.apply($, defs);
+ },
+ /**
* Onchange RPCs may return values for fields that are not in the current
* view. Those fields might even be unknown when the onchange returns (e.g.
* in x2manys, we only know the fields that are used in the inner view, but
@@ -1414,6 +1498,7 @@ var BasicModel = AbstractModel.extend({
list = self._makeDataPoint({
fields: view ? view.fields : fieldInfo.relatedFields,
fieldsInfo: view ? view.fieldsInfo : fieldInfo.fieldsInfo,
+ limit: fieldInfo.limit,
modelName: field.relation,
parentID: record.id,
static: true,
@@ -2980,7 +3065,7 @@ var BasicModel = AbstractModel.extend({
if (field.type === 'one2many' || field.type === 'many2many') {
var ids;
if (!context[fieldName] || _.isArray(context[fieldName])) { // no dataPoint created yet
- ids = context[fieldName] || [];
+ ids = context[fieldName] ? context[fieldName].slice(0) : [];
} else {
relDataPoint = this._applyX2ManyOperations(this.localData[context[fieldName]]);
ids = relDataPoint.res_ids.slice(0);
@@ -3260,27 +3345,8 @@ var BasicModel = AbstractModel.extend({
context: params.context,
})
.then(function (result) {
- // fill default values for missing fields
- for (var i = 0; i < fieldNames.length; i++) {
- var fieldName = fieldNames[i];
- if (!(fieldName in result)) {
- var field = params.fields[fieldName];
- if (field.type === 'float' ||
- field.type === 'integer' ||
- field.type === 'monetary') {
- result[fieldName] = 0;
- } else if (field.type === 'one2many' || field.type === 'many2many') {
- result[fieldName] = [];
- } else {
- result[fieldName] = null;
- }
- }
- }
-
- var data = {};
var record = self._makeDataPoint({
modelName: modelName,
- data: data,
fields: params.fields,
fieldsInfo: params.fieldsInfo,
context: params.context,
@@ -3289,37 +3355,7 @@ var BasicModel = AbstractModel.extend({
viewType: params.viewType,
});
- var defs = [];
- _.each(fieldNames, function (name) {
- var field = params.fields[name];
- data[name] = null;
- record._changes = record._changes || {};
- var dp;
- if (field.type === 'many2one' && result[name]) {
- dp = self._makeDataPoint({
- context: record.context,
- data: {id: result[name]},
- modelName: field.relation,
- parentID: record.id,
- });
- record._changes[name] = dp.id;
- } else if (field.type === 'reference' && result[name]) {
- var ref = result[name].split(',');
- dp = self._makeDataPoint({
- context: record.context,
- data: {id: parseInt(ref[1])},
- modelName: ref[0],
- parentID: record.id,
- });
- defs.push(self._fetchNameGet(dp));
- record._changes[name] = dp.id;
- } else if (field.type === 'one2many' || field.type === 'many2many') {
- defs.push(self._processX2ManyCommands(record, name, result[name]));
- } else {
- record._changes[name] = self._parseServerValue(field, result[name]);
- }
- });
- return $.when.apply($, defs)
+ return self.applyDefaultValues(record.id, result)
.then(function () {
var def = $.Deferred();
self._performOnChange(record, fields_key).always(function () {
@@ -3502,13 +3538,17 @@ var BasicModel = AbstractModel.extend({
* @param {Object} record
* @param {string} fieldName
* @param {Array[Array]} commands
+ * @param {Object} [options]
+ * @param {string} [options.viewType] current viewType. If not set, we will
+ * assume main viewType from the record
* @returns {Deferred}
*/
- _processX2ManyCommands: function (record, fieldName, commands) {
+ _processX2ManyCommands: function (record, fieldName, commands, options) {
var self = this;
+ options = options || {};
var defs = [];
var field = record.fields[fieldName];
- var fieldInfo = record.fieldsInfo[record.viewType][fieldName];
+ var fieldInfo = record.fieldsInfo[options.viewType || record.viewType][fieldName];
var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode];
var fieldsInfo = view ? view.fieldsInfo : fieldInfo.fieldsInfo;
var fields = view ? view.fields : fieldInfo.relatedFields;
@@ -3802,14 +3842,14 @@ var BasicModel = AbstractModel.extend({
// for multi-pages list datapoints, we might need to read the
// order field first to apply the order on all pages
- if (list.res_ids.length > list.limit) {
- if (!list.orderedResIDs && list.orderedBy.length) {
+ if (list.res_ids.length > list.limit && list.orderedBy.length) {
+ if (!list.orderedResIDs) {
var fieldNames = _.pluck(list.orderedBy, 'name');
def = this._readMissingFields(list, _.filter(list.res_ids, _.isNumber), fieldNames);
- def.then(function () {
- self._sortList(list);
- });
}
+ def.then(function () {
+ self._sortList(list);
+ });
}
return def.then(function () {
var resIDs = [];
diff --git a/addons/web/static/src/js/views/basic/basic_view.js b/addons/web/static/src/js/views/basic/basic_view.js
index df9f9807bfd..b4df9c8e09e 100644
--- a/addons/web/static/src/js/views/basic/basic_view.js
+++ b/addons/web/static/src/js/views/basic/basic_view.js
@@ -45,7 +45,7 @@ var BasicView = AbstractView.extend({
this.rendererParams.viewType = this.viewType;
this.controllerParams.confirmOnDelete = true;
- this.controllerParams.archiveEnabled = 'active' in viewInfo.fields;
+ this.controllerParams.archiveEnabled = 'active' in this.fields;
this.controllerParams.hasButtons =
'action_buttons' in params ? params.action_buttons : true;
@@ -161,7 +161,12 @@ var BasicView = AbstractView.extend({
// (because those fields were unknow at that time). So we ask
// the model to process them.
def = this.model.applyRawChanges(record.id, viewType).then(function () {
- if (!self.model.isNew(record.id)) {
+ if (self.model.isNew(record.id)) {
+ return self.model.applyDefaultValues(record.id, {}, {
+ fieldNames: fieldNames,
+ viewType: viewType,
+ });
+ } else {
return self.model.reload(record.id, {
fieldNames: fieldNames,
keepChanges: true,
diff --git a/addons/web/static/src/js/views/calendar/calendar_controller.js b/addons/web/static/src/js/views/calendar/calendar_controller.js
index 2755c4c39fd..d49c7c33f5c 100644
--- a/addons/web/static/src/js/views/calendar/calendar_controller.js
+++ b/addons/web/static/src/js/views/calendar/calendar_controller.js
@@ -183,7 +183,7 @@ var CalendarController = AbstractController.extend({
// When clicking on a random day of a random other week, switch to week view
this.model.setScale('week');
}
- this.model.setDate(event.data.date, true);
+ this.model.setDate(event.data.date);
this.reload();
},
/**
diff --git a/addons/web/static/src/js/views/calendar/calendar_model.js b/addons/web/static/src/js/views/calendar/calendar_model.js
index ca28e90713f..5542ab06476 100644
--- a/addons/web/static/src/js/views/calendar/calendar_model.js
+++ b/addons/web/static/src/js/views/calendar/calendar_model.js
@@ -218,9 +218,9 @@ return AbstractModel.extend({
filters: params.filters,
};
+ this.setDate(params.initialDate);
// Use mode attribute in xml file to specify zoom timeline (day,week,month)
// by default month.
- this.setDate(params.initialDate, true);
this.setScale(params.mode);
_.each(this.data.filters, function (filter) {
@@ -260,9 +260,8 @@ return AbstractModel.extend({
},
/**
* @param {Moment} start
- * @param {boolean} highlight
*/
- setDate: function (start, highlight) {
+ setDate: function (start) {
this.data.start_date = this.data.end_date = this.data.target_date = this.data.highlight_date = start;
this.data.start_date.utc().add(this.getSession().getTZOffset(this.data.start_date), 'minutes');
@@ -279,9 +278,6 @@ return AbstractModel.extend({
this.data.start_date = this.data.start_date.clone().startOf('day');
this.data.end_date = this.data.end_date.clone().endOf('day');
}
- if (highlight) {
- this.data.highlight_date = this.data.target_date;
- }
},
/**
* @param {string} scale the scale to set
diff --git a/addons/web/static/src/js/views/calendar/calendar_renderer.js b/addons/web/static/src/js/views/calendar/calendar_renderer.js
index 469130c723a..a8d5d07ed0d 100644
--- a/addons/web/static/src/js/views/calendar/calendar_renderer.js
+++ b/addons/web/static/src/js/views/calendar/calendar_renderer.js
@@ -467,11 +467,6 @@ return AbstractRenderer.extend({
this.target_date = this.state.target_date.toString();
}
- var highlightDate = moment(this.state.highlight_date).format('YYYY-MM-DD');
- $calendar.find('.o_target_date').removeClass('o_target_date');
- $calendar.find('.fc-bg .fc-day[data-date="'+highlightDate+'"]')
- .addClass('o_target_date');
-
this.$small_calendar.datepicker("setDate", this.state.highlight_date.toDate())
.find('.o_selected_range')
.removeClass('o_color o_selected_range');
diff --git a/addons/web/static/src/js/views/pivot/pivot_controller.js b/addons/web/static/src/js/views/pivot/pivot_controller.js
index 731183e7efe..5f31c7103c7 100644
--- a/addons/web/static/src/js/views/pivot/pivot_controller.js
+++ b/addons/web/static/src/js/views/pivot/pivot_controller.js
@@ -126,6 +126,7 @@ var PivotController = AbstractController.extend({
var table = this.model.exportData();
if(table.measure_row.length + 1 > 256) {
crash_manager.show_message(_t("For Excel compatibility, data cannot be exported if there are more than 256 columns.\n\nTip: try to flip axis, filter further or reduce the number of measures."));
+ framework.unblockUI();
return;
}
framework.blockUI();
diff --git a/addons/web/static/src/js/views/pivot/pivot_model.js b/addons/web/static/src/js/views/pivot/pivot_model.js
index 99a5e4ee334..1c76c41ab71 100644
--- a/addons/web/static/src/js/views/pivot/pivot_model.js
+++ b/addons/web/static/src/js/views/pivot/pivot_model.js
@@ -135,7 +135,7 @@ var PivotModel = AbstractModel.extend({
exportData: function () {
var measureNbr = this.data.measures.length;
var headers = this._computeHeaders();
- var measureRow = measureNbr > 1 ? _.last(headers) : [];
+ var measureRow = measureNbr >= 1 ? _.last(headers) : [];
var rows = this._computeRows();
var i, j, value;
headers[0].splice(0,1);
diff --git a/addons/web/static/src/js/views/view_dialogs.js b/addons/web/static/src/js/views/view_dialogs.js
index 4be84c23b71..1b74068d44d 100644
--- a/addons/web/static/src/js/views/view_dialogs.js
+++ b/addons/web/static/src/js/views/view_dialogs.js
@@ -260,6 +260,7 @@ var SelectCreateDialog = ViewDialog.extend({
var searchData = this._process_search_data(d.domains, d.contexts, d.groupbys);
this.list_controller.reload(searchData);
},
+ get_controller_context: '_onGetControllerContext',
}),
/**
@@ -416,6 +417,24 @@ var SelectCreateDialog = ViewDialog.extend({
dialog.on('closed', this, this.close);
return dialog;
},
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Handles a context request: provides to the caller the context of the
+ * list controller.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ * @param {function} ev.data.callback used to send the requested context
+ */
+ _onGetControllerContext: function (ev) {
+ ev.stopPropagation();
+ var context = this.list_controller && this.list_controller.getContext();
+ ev.data.callback(context || {});
+ }
});
return {
diff --git a/addons/web/static/src/js/widgets/data_export.js b/addons/web/static/src/js/widgets/data_export.js
index 4b9f6a29a47..dd0096f269b 100644
--- a/addons/web/static/src/js/widgets/data_export.js
+++ b/addons/web/static/src/js/widgets/data_export.js
@@ -455,7 +455,7 @@ var DataExport = Dialog.extend({
Dialog.alert(this, _t("Please select fields to export..."));
return;
}
- exported_fields.unshift({name: 'id', label: 'External ID'});
+ exported_fields.unshift({name: 'id', label: _t('External ID')});
var export_format = this.$export_format_inputs.filter(':checked').val();
diff --git a/addons/web/static/src/js/widgets/domain_selector_dialog.js b/addons/web/static/src/js/widgets/domain_selector_dialog.js
index 5f41973951d..46b54089c5f 100644
--- a/addons/web/static/src/js/widgets/domain_selector_dialog.js
+++ b/addons/web/static/src/js/widgets/domain_selector_dialog.js
@@ -40,7 +40,11 @@ return Dialog.extend({
this.domainSelector = new DomainSelector(this, model, domain, options);
},
start: function () {
- this.$el.css("overflow", "visible").closest(".modal-dialog").css("height", "auto"); // This restores default modal height (bootstrap) and allows field selector to overflow
+ var self = this;
+ this.opened().then(function () {
+ // this restores default modal height (bootstrap) and allows field selector to overflow
+ self.$el.css('overflow', 'visible').closest('.modal-dialog').css('height', 'auto');
+ });
return $.when(
this._super.apply(this, arguments),
this.domainSelector.appendTo(this.$el)
diff --git a/addons/web/static/src/js/widgets/notification.js b/addons/web/static/src/js/widgets/notification.js
index 21c24c99e1b..d05a0be380c 100644
--- a/addons/web/static/src/js/widgets/notification.js
+++ b/addons/web/static/src/js/widgets/notification.js
@@ -34,7 +34,8 @@ var Notification = Widget.extend({
this.title = params.title;
this.text = params.text;
this.sticky = !!params.sticky;
- if (params.type === 'warning') {
+ this.type = params.type;
+ if (this.type === 'warning') {
this.template = 'Warning';
}
},
@@ -43,6 +44,9 @@ var Notification = Widget.extend({
*/
start: function () {
var self = this;
+ if (this.type === 'warning') {
+ this.$el.addClass('o_error');
+ }
return this._super.apply(this, arguments).then(function () {
self.$el.animate({opacity: 1.0}, 400, "swing", function () {
if(!self.sticky) {
diff --git a/addons/web/static/src/less/form_view.less b/addons/web/static/src/less/form_view.less
index 78246df9e6c..96e8bd2ac97 100644
--- a/addons/web/static/src/less/form_view.less
+++ b/addons/web/static/src/less/form_view.less
@@ -165,7 +165,9 @@
.o-flex-flow(row, wrap);
> .btn {
- margin-right: 3px;
+ @o-statusbar-buttons-vmargin: 4px;
+ min-height: @odoo-statusbar-height - 2 * @o-statusbar-buttons-vmargin;
+ margin: @o-statusbar-buttons-vmargin 3px @o-statusbar-buttons-vmargin 0;
padding-top: 2px;
padding-bottom: 2px;
}
diff --git a/addons/web/static/src/less/views.less b/addons/web/static/src/less/views.less
index 104d4e1b2a5..0c7936fc570 100644
--- a/addons/web/static/src/less/views.less
+++ b/addons/web/static/src/less/views.less
@@ -61,6 +61,10 @@
font-size: 125%;
}
+ a {
+ cursor: pointer;
+ }
+
.o-init-image() {
content: "";
display: block;
diff --git a/addons/web/static/src/less/web_calendar.less b/addons/web/static/src/less/web_calendar.less
index a3ead61f011..5179552b3b6 100644
--- a/addons/web/static/src/less/web_calendar.less
+++ b/addons/web/static/src/less/web_calendar.less
@@ -68,11 +68,6 @@
overflow-x: auto;
}
}
-
- .o_target_date:not(.fc-today) {
- background-color: @odoo-brand-primary;
- opacity: 0.1;
- }
}
.o_calendar_sidebar_container {
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index 65b75fa00e0..748addc4b87 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -125,7 +125,6 @@
</div>
</t>
<t t-name="Warning" t-extend="Notification">
- <t t-jquery=".o_notification">$(this).addClass('o_error');</t>
<t t-jquery=".o_icon" t-operation="replace">
<span class="o_icon fa fa-3x fa-exclamation"/>
</t>
diff --git a/addons/web/static/tests/chrome/action_manager_mobile_tests.js b/addons/web/static/tests/chrome/action_manager_mobile_tests.js
index c23d7be19f8..491830578e6 100644
--- a/addons/web/static/tests/chrome/action_manager_mobile_tests.js
+++ b/addons/web/static/tests/chrome/action_manager_mobile_tests.js
@@ -80,6 +80,49 @@ QUnit.module('ActionManager', {
actionManager.destroy();
});
+
+ QUnit.test('lazy load mobile-friendly view', function (assert) {
+ assert.expect(11);
+
+ var actionManager = createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ mockRPC: function (route, args) {
+ assert.step(args.method || route);
+ return this._super.apply(this, arguments);
+ },
+ });
+ actionManager.loadState({
+ action: 1,
+ view_type: 'form',
+ });
+
+ assert.strictEqual(actionManager.$('.o_list_view').length, 0,
+ "should not have rendered a list view");
+ assert.strictEqual(actionManager.$('.o_kanban_view').length, 0,
+ "should not have rendered a kanban view either");
+ assert.strictEqual(actionManager.$('.o_form_view').length, 1,
+ "should have rendered a form view");
+
+ // go back to lazy loaded view
+ $('.o_control_panel .breadcrumb a').click();
+ assert.strictEqual(actionManager.$('.o_form_view').length, 0,
+ "should not display the form view anymore");
+ assert.strictEqual(actionManager.$('.o_list_view').length, 0,
+ "should not display the list view either");
+ assert.strictEqual(actionManager.$('.o_kanban_view').length, 1,
+ "should have lazy loaded the kanban view");
+
+ assert.verifySteps([
+ '/web/action/load',
+ 'load_views',
+ 'default_get', // default_get to open form view
+ '/web/dataset/search_read', // search read when coming back to Kanban
+ ]);
+
+ actionManager.destroy();
+ });
});
});
diff --git a/addons/web/static/tests/chrome/action_manager_tests.js b/addons/web/static/tests/chrome/action_manager_tests.js
index 4c4f6f9e338..9f7bacc1689 100644
--- a/addons/web/static/tests/chrome/action_manager_tests.js
+++ b/addons/web/static/tests/chrome/action_manager_tests.js
@@ -6,6 +6,7 @@ var ReportClientAction = require('report.client_action');
var AbstractAction = require('web.AbstractAction');
var ControlPanelMixin = require('web.ControlPanelMixin');
var core = require('web.core');
+var ListController = require('web.ListController');
var ReportService = require('web.ReportService');
var testUtils = require('web.test_utils');
var Widget = require('web.Widget');
@@ -760,7 +761,7 @@ QUnit.module('ActionManager', {
});
QUnit.test('should not push a loaded state', function (assert) {
- assert.expect(1);
+ assert.expect(3);
var actionManager = createActionManager({
actions: this.actions,
@@ -772,9 +773,14 @@ QUnit.module('ActionManager', {
},
},
});
- actionManager.loadState({action: 1});
+ actionManager.loadState({action: 3});
- assert.verifySteps([]);
+ assert.verifySteps([], "should not push the loaded state");
+
+ actionManager.$('tr.o_data_row:first').click();
+
+ assert.verifySteps(['push_state'],
+ "should push the state of it changes afterwards");
actionManager.destroy();
});
@@ -2350,6 +2356,70 @@ QUnit.module('ActionManager', {
actionManager.destroy();
});
+ QUnit.test('save current search', function (assert) {
+ assert.expect(4);
+
+ testUtils.patch(ListController, {
+ getContext: function () {
+ return {
+ shouldBeInFilterContext: true,
+ };
+ },
+ });
+
+ _.extend(this.archs, {
+ 'partner,7,search':
+ '<search>' +
+ '<filter name="bar" help="Bar" domain="[(\'bar\', \'=\', 1)]"/>' +
+ '</search>',
+ });
+
+ this.actions.push({
+ id: 33,
+ context: {
+ shouldNotBeInFilterContext: false,
+ },
+ name: 'Partners',
+ res_model: 'partner',
+ search_view_id: [7, 'a custom search view'],
+ type: 'ir.actions.act_window',
+ views: [[false, 'list']],
+ });
+
+ var actionManager = createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ intercepts: {
+ create_filter: function (event) {
+ var filter = event.data.filter;
+ assert.deepEqual(filter.domain, [['bar', '=', 1]],
+ "should save the correct domain");
+ assert.deepEqual(filter.context, {shouldBeInFilterContext: true},
+ "should save the correct context");
+ },
+ },
+ });
+ actionManager.doAction(33);
+
+ assert.strictEqual(actionManager.$('.o_data_row').length, 5,
+ "should contain 5 records");
+
+ // filter on bar
+ $('.o_control_panel .o_filters_menu a:contains(Bar)').click();
+
+ assert.strictEqual(actionManager.$('.o_data_row').length, 2,
+ "should contain 2 records");
+
+ // save filter
+ $('.o_control_panel .o_save_search a').click(); // toggle 'Save current search'
+ $('.o_control_panel .o_save_name input[type=text]').val('some name'); // name the filter
+ $('.o_control_panel .o_save_name button').click(); // click on 'Save'
+
+ testUtils.unpatch(ListController);
+ actionManager.destroy();
+ });
+
QUnit.module('Actions in target="new"');
QUnit.test('can execute act_window actions in target="new"', function (assert) {
@@ -2528,6 +2598,35 @@ QUnit.module('ActionManager', {
actionManager.destroy();
});
+
+ QUnit.test('doAction resolved with an action', function (assert) {
+ assert.expect(4);
+ var done = assert.async();
+
+ this.actions.push({
+ id: 21,
+ name: 'A Close Action',
+ type: 'ir.actions.act_window_close',
+ });
+
+ var actionManager = createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ });
+
+ actionManager.doAction(21).then(function (action) {
+ assert.ok(action, "doAction should be resolved with an action");
+ assert.strictEqual(action.id, 21,
+ "should be resolved with correct action id");
+ assert.strictEqual(action.name, 'A Close Action',
+ "should be resolved with correct action name");
+ assert.strictEqual(action.type, 'ir.actions.act_window_close',
+ "should be resolved with correct action type");
+ actionManager.destroy();
+ done();
+ });
+ });
});
});
diff --git a/addons/web/static/tests/core/util_tests.js b/addons/web/static/tests/core/util_tests.js
index 204503117e6..c7158083dcb 100644
--- a/addons/web/static/tests/core/util_tests.js
+++ b/addons/web/static/tests/core/util_tests.js
@@ -44,6 +44,17 @@ QUnit.module('core', {}, function () {
assert.strictEqual(intersperse("12345678", [3,0], '.'), '12.345.678');
});
+ QUnit.test('is_bin_size', function (assert) {
+ assert.expect(3);
+
+ var is_bin_size = utils.is_bin_size;
+
+ assert.strictEqual(is_bin_size('Cg=='), false);
+ assert.strictEqual(is_bin_size('2.5 Mb'), true);
+ // should also work for non-latin languages (e.g. russian)
+ assert.strictEqual(is_bin_size('64.2 Кб'), true);
+ });
+
});
}); \ No newline at end of file
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index 2e40d7cc5ec..9b8df65ab29 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -368,10 +368,6 @@ QUnit.module('relational_fields', {
"should have an open record button");
form.$('input.o_input').click();
- form.$('input.o_input').trigger($.Event('keyup', {
- which: $.ui.keyCode.ESC,
- keyCode: $.ui.keyCode.ESC,
- }));
assert.strictEqual(form.$('button.o_external_button:visible').length, 1,
"should still have an open record button");
@@ -3064,6 +3060,108 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('onchange on one2many containing x2many in form view', function (assert) {
+ assert.expect(16);
+
+ this.data.partner.onchanges = {
+ foo: function (obj) {
+ obj.turtles = [[0, false, {turtle_foo: 'new record'}]];
+ },
+ };
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form>' +
+ '<field name="foo"/>' +
+ '<field name="turtles">' +
+ '<tree>' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '<form>' +
+ '<field name="partner_ids">' +
+ '<tree editable="top">' +
+ '<field name="foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ archs: {
+ 'partner,false,list': '<tree><field name="foo"/></tree>',
+ 'partner,false,search': '<search></search>',
+ },
+ });
+
+ assert.strictEqual(form.$('.o_data_row').length, 1,
+ "the onchange should have created one record in the relation");
+
+ // open the created o2m record in a form view, and add a m2m subrecord
+ // in its relation
+ form.$('.o_data_row').click();
+
+ assert.strictEqual($('.modal').length, 1, "should have opened a dialog");
+ assert.strictEqual($('.modal .o_data_row').length, 0,
+ "there should be no record in the one2many in the dialog");
+
+ // add a many2many subrecord
+ $('.modal .o_field_x2many_list_row_add a').click();
+
+ assert.strictEqual($('.modal').length, 2,
+ "should have opened a second dialog");
+
+ // select a many2many subrecord
+ $('.modal:nth(1) .o_list_view .o_data_cell:first').click();
+
+ assert.strictEqual($('.modal').length, 1,
+ "second dialog should be closed");
+ assert.strictEqual($('.modal .o_data_row').length, 1,
+ "there should be one record in the one2many in the dialog");
+ assert.notOk($('.modal .o_x2m_control_panel .o_cp_pager div').is(':visible'),
+ 'm2m pager should be hidden');
+
+ // click on 'Save & Close'
+ $('.modal .modal-footer .btn-primary:first').click();
+
+ assert.strictEqual($('.modal').length, 0, "dialog should be closed");
+
+ // reopen o2m record, and another m2m subrecord in its relation, but
+ // discard the changes
+ form.$('.o_data_row').click();
+
+ assert.strictEqual($('.modal').length, 1, "should have opened a dialog");
+ assert.strictEqual($('.modal .o_data_row').length, 1,
+ "there should be one record in the one2many in the dialog");
+
+ // add another m2m subrecord
+ $('.modal .o_field_x2many_list_row_add a').click();
+
+ assert.strictEqual($('.modal').length, 2,
+ "should have opened a second dialog");
+
+ $('.modal:nth(1) .o_list_view .o_data_cell:first').click();
+
+ assert.strictEqual($('.modal').length, 1,
+ "second dialog should be closed");
+ assert.strictEqual($('.modal .o_data_row').length, 2,
+ "there should be two records in the one2many in the dialog");
+
+ // click on 'Discard'
+ $('.modal .modal-footer .btn-default').click();
+
+ assert.strictEqual($('.modal').length, 0, "dialog should be closed");
+
+ // reopen o2m record to check that second changes have properly been discarded
+ form.$('.o_data_row').click();
+
+ assert.strictEqual($('.modal').length, 1, "should have opened a dialog");
+ assert.strictEqual($('.modal .o_data_row').length, 1,
+ "there should be one record in the one2many in the dialog");
+
+ form.destroy();
+ });
+
QUnit.test('embedded one2many with handle widget with minimum setValue calls', function (assert) {
var done = assert.async();
assert.expect(20);
@@ -8017,6 +8115,80 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('one2many with several pages, onchange and default order', function (assert) {
+ // This test reproduces a specific scenario where a one2many is displayed
+ // over several pages, and has a default order such that a record that
+ // would normally be on page 1 is actually on another page. Moreover,
+ // there is an onchange on that one2many which converts all commands 4
+ // (LINK_TO) into commands 1 (UPDATE), which is standard in the ORM.
+ // This test ensures that the record displayed on page 2 is never fully
+ // read.
+ assert.expect(8);
+
+ var data = this.data;
+ data.partner.records[0].turtles = [1, 2, 3];
+ data.turtle.records[0].partner_ids = [1];
+ data.partner.onchanges = {
+ turtles: function (obj) {
+ var res = _.map(obj.turtles, function (command) {
+ if (command[0] === 1) { // already an UPDATE command: do nothing
+ return command;
+ }
+ // convert LINK_TO commands to UPDATE commands
+ var id = command[1];
+ var record = _.findWhere(data.turtle.records, {id: id});
+ return [1, id, _.pick(record, ['turtle_int', 'turtle_foo', 'partner_ids'])];
+ });
+ obj.turtles = [[5]].concat(res);
+ },
+ };
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="top" limit="2" default_order="turtle_foo">' +
+ '<field name="turtle_int"/>' +
+ '<field name="turtle_foo" class="foo"/>' +
+ '<field name="partner_ids" widget="many2many_tags"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ var ids = args.method === 'read' ? ' [' + args.args[0] + ']' : '';
+ assert.step(args.method + ids);
+ return this._super.apply(this, arguments);
+ },
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.strictEqual(form.$('.o_data_cell.foo').text(), 'blipkawa',
+ "should display two records out of three, in the correct order");
+
+ // edit turtle_int field of first row
+ form.$('.o_data_cell:first').click();
+ form.$('.o_field_widget[name=turtle_int]').val(3).trigger('input');
+ form.$el.click(); // leave edition
+
+ assert.strictEqual(form.$('.o_data_cell.foo').text(), 'blipkawa',
+ "should still display the same two records");
+
+ assert.verifySteps([
+ 'read [1]', // main record
+ 'read [1,2,3]', // one2many (turtle_foo, all records)
+ 'read [2,3]', // one2many (all fields in view, records of first page)
+ 'read [2,4]', // many2many inside one2many (partner_ids), first page only
+ 'onchange',
+ ]);
+
+ form.destroy();
+ });
+
QUnit.test('new record, with one2many with more default values than limit', function (assert) {
assert.expect(2);
diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js
index fb5af01fdcd..99b85dcfa56 100644
--- a/addons/web/static/tests/helpers/mock_server.js
+++ b/addons/web/static/tests/helpers/mock_server.js
@@ -565,9 +565,13 @@ var MockServer = Class.extend({
return record.display_name.indexOf(str) !== -1;
});
}
- return _.map(records, function (record) {
+ var result = _.map(records, function (record) {
return [record.id, record.display_name];
});
+ if (args.limit) {
+ return result.slice(0, args.limit);
+ }
+ return result;
},
/**
* Simulate an 'onchange' rpc
diff --git a/addons/web/static/tests/views/basic_model_tests.js b/addons/web/static/tests/views/basic_model_tests.js
index 492dfd4224c..bf5261c5b6e 100644
--- a/addons/web/static/tests/views/basic_model_tests.js
+++ b/addons/web/static/tests/views/basic_model_tests.js
@@ -669,6 +669,42 @@ QUnit.module('Views', {
model.destroy();
});
+ QUnit.test('default_get returning a non requested field', function (assert) {
+ // 'default_get' returns a default value for the fields given in
+ // arguments. It should not return a value for fields that have not be
+ // requested. However, it happens (e.g. res.users), and the webclient
+ // should not crash when this situation occurs (the field should simply
+ // be ignored).
+ assert.expect(2);
+
+ this.params.context = {};
+ this.params.fieldNames = ['category'];
+ this.params.res_id = undefined;
+ this.params.type = 'record';
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ mockRPC: function (route, args) {
+ var result = this._super(route, args);
+ if (args.method === 'default_get') {
+ result.product_ids = [[6, 0, [37, 41]]];
+ }
+ return result;
+ },
+ });
+
+ model.load(this.params).then(function (resultID) {
+ var record = model.get(resultID);
+ assert.ok('category' in record.data,
+ "should have processed 'category'");
+ assert.notOk('product_ids' in record.data,
+ "should have ignored 'product_ids'");
+ });
+
+ model.destroy();
+ });
+
QUnit.test('can make a default_record with default relational values', function (assert) {
assert.expect(7);
diff --git a/addons/web/static/tests/views/view_dialogs_tests.js b/addons/web/static/tests/views/view_dialogs_tests.js
index 014435194f3..6ec4f50ab4b 100644
--- a/addons/web/static/tests/views/view_dialogs_tests.js
+++ b/addons/web/static/tests/views/view_dialogs_tests.js
@@ -1,8 +1,9 @@
odoo.define('web.view_dialogs_tests', function (require) {
"use strict";
-var testUtils = require('web.test_utils');
var dialogs = require('web.view_dialogs');
+var ListController = require('web.ListController');
+var testUtils = require('web.test_utils');
var Widget = require('web.Widget');
QUnit.module('Views', {
@@ -66,6 +67,41 @@ QUnit.module('Views', {
parent.destroy();
});
+ QUnit.test('formviewdialog buttons in footer are not duplicated', function (assert) {
+ assert.expect(2);
+ this.data.partner.fields.poney_ids = {string: "Poneys", type: "one2many", relation: 'partner'};
+ this.data.partner.records[0].poney_ids = [];
+
+ var parent = createParent({
+ data: this.data,
+ archs: {
+ 'partner,false,form':
+ '<form string="Partner">' +
+ '<field name="poney_ids"><tree editable="top"><field name="display_name"/></tree></field>' +
+ '<footer><button string="Custom Button" type="object" class="btn-primary"/></footer>' +
+ '</form>',
+ },
+ });
+
+ new dialogs.FormViewDialog(parent, {
+ res_model: 'partner',
+ res_id: 1,
+ }).open();
+
+ assert.strictEqual($('div.modal button.btn-primary').length, 1,
+ "should have 1 buttons in modal");
+
+ $('.o_field_x2many_list_row_add a').click();
+ $('input.o_input').trigger($.Event('keydown', {
+ which: $.ui.keyCode.ESCAPE,
+ keyCode: $.ui.keyCode.ESCAPE,
+ }));
+
+ assert.strictEqual($('div.modal button.btn-primary').length, 1,
+ "should still have 1 buttons in modal");
+ parent.destroy();
+ });
+
QUnit.test('SelectCreateDialog use domain, group_by and search default', function (assert) {
assert.expect(3);
@@ -206,6 +242,63 @@ QUnit.module('Views', {
parent.destroy();
});
+ QUnit.test('SelectCreateDialog: save current search', function (assert) {
+ assert.expect(4);
+
+ testUtils.patch(ListController, {
+ getContext: function () {
+ return {
+ shouldBeInFilterContext: true,
+ };
+ },
+ });
+
+ var parent = createParent({
+ data: this.data,
+ archs: {
+ 'partner,false,list':
+ '<tree>' +
+ '<field name="display_name"/>' +
+ '</tree>',
+ 'partner,false,search':
+ '<search>' +
+ '<filter name="bar" help="Bar" domain="[(\'bar\', \'=\', True)]"/>' +
+ '</search>',
+
+ },
+ intercepts: {
+ create_filter: function (event) {
+ var filter = event.data.filter;
+ assert.deepEqual(filter.domain, [['bar', '=', true]],
+ "should save the correct domain");
+ assert.deepEqual(filter.context, {shouldBeInFilterContext: true},
+ "should save the correct context");
+ },
+ },
+ });
+
+ var dialog = new dialogs.SelectCreateDialog(parent, {
+ context: {shouldNotBeInFilterContext: false},
+ res_model: 'partner',
+ }).open();
+
+ assert.strictEqual(dialog.$('.o_data_row').length, 3,
+ "should contain 3 records");
+
+ // filter on bar
+ dialog.$('.o_filters_menu a:contains(Bar)').click();
+
+ assert.strictEqual(dialog.$('.o_data_row').length, 2,
+ "should contain 2 records");
+
+ // save filter
+ dialog.$('.o_save_search a').click(); // toggle 'Save current search'
+ dialog.$('.o_save_name input[type=text]').val('some name'); // name the filter
+ dialog.$('.o_save_name button').click(); // click on 'Save'
+
+ testUtils.unpatch(ListController);
+ parent.destroy();
+ });
});
});
diff --git a/addons/web_editor/static/src/js/editor/summernote.js b/addons/web_editor/static/src/js/editor/summernote.js
index 0d1860d236e..6939c90cefa 100644
--- a/addons/web_editor/static/src/js/editor/summernote.js
+++ b/addons/web_editor/static/src/js/editor/summernote.js
@@ -2167,7 +2167,6 @@ $.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color
}
// remove node without attributes (move content), and merge the same nodes
- if (!dom.isImgFont(node)) {
var className2, style, style2;
for (i=0; i<nodes.length; i++) {
node = nodes[i];
@@ -2208,7 +2207,6 @@ $.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color
}
}
}
- }
range.create(startPoint.node, startPoint.offset, endPoint.node, endPoint.offset).select();
};
diff --git a/addons/web_settings_dashboard/static/src/js/dashboard.js b/addons/web_settings_dashboard/static/src/js/dashboard.js
index 1cdd7e55178..06f24ab8735 100644
--- a/addons/web_settings_dashboard/static/src/js/dashboard.js
+++ b/addons/web_settings_dashboard/static/src/js/dashboard.js
@@ -194,29 +194,40 @@ var DashboardShare = Widget.extend({
'click .li_share': 'share_linkedin',
},
- init: function(parent, data){
+ init: function (parent, data) {
this.data = data;
this.parent = parent;
this.share_url = 'https://www.odoo.com';
this.share_text = encodeURIComponent("I am using #Odoo - Awesome open source business apps.");
},
- share_twitter: function(){
+ /**
+ * @param {MouseEvent} ev
+ */
+ share_twitter: function (ev) {
+ ev.preventDefault();
var popup_url = _.str.sprintf( 'https://twitter.com/intent/tweet?tw_p=tweetbutton&text=%s %s',this.share_text,this.share_url);
this.sharer(popup_url);
},
-
- share_facebook: function(){
+ /**
+ * @param {MouseEvent} ev
+ */
+ share_facebook: function (ev) {
+ ev.preventDefault();
var popup_url = _.str.sprintf('https://www.facebook.com/sharer/sharer.php?u=%s', encodeURIComponent(this.share_url));
this.sharer(popup_url);
},
- share_linkedin: function(){
+ /**
+ * @param {MouseEvent} ev
+ */
+ share_linkedin: function (ev) {
+ ev.preventDefault();
var popup_url = _.str.sprintf('http://www.linkedin.com/shareArticle?mini=true&url=%s&title=I am using odoo&summary=%s&source=www.odoo.com', encodeURIComponent(this.share_url), this.share_text);
this.sharer(popup_url);
},
- sharer: function(popup_url){
+ sharer: function (popup_url) {
window.open(
popup_url,
'Share Dialog',
diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py
index 40d3c32fc87..dad87420ecb 100644
--- a/addons/website/controllers/main.py
+++ b/addons/website/controllers/main.py
@@ -118,7 +118,7 @@ class Website(Home):
def robots(self):
return request.render('website.robots', {'url_root': request.httprequest.url_root}, mimetype='text/plain')
- @http.route('/sitemap.xml', type='http', auth="public", website=True)
+ @http.route('/sitemap.xml', type='http', auth="public", website=True, multilang=False)
def sitemap_xml_index(self, **kwargs):
current_website = request.website
Attachment = request.env['ir.attachment'].sudo()
diff --git a/addons/website/models/ir_model_fields.py b/addons/website/models/ir_model_fields.py
index 1e963bf650e..51f5c2a7dc5 100644
--- a/addons/website/models/ir_model_fields.py
+++ b/addons/website/models/ir_model_fields.py
@@ -1,17 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details
-from odoo import api, models
+from odoo import models
# This is a nasty hack, targeted for V11 only
class IrModelFields(models.Model):
_inherit = 'ir.model.fields'
- @api.multi
def unlink(self):
- # Prevent the deletion of social_* fields defined in res.company if
- # the other module is installed
+ # Prevent the deletion of some `shared` fields... -_-
social = (
'social_facebook',
'social_github',
@@ -23,8 +21,9 @@ class IrModelFields(models.Model):
self = self.filtered(
lambda rec: not (
- rec.model == 'res.company' and
- rec.name in social
+ (rec.model == 'res.company' and rec.name in social)
+ or (rec.model == 'res.config.settings'
+ and rec.name == 'auth_signup_uninvited')
)
)
return super(IrModelFields, self).unlink()
diff --git a/addons/website/models/website.py b/addons/website/models/website.py
index ba5b168c9b6..f8e00eb4494 100644
--- a/addons/website/models/website.py
+++ b/addons/website/models/website.py
@@ -119,6 +119,10 @@ class Website(models.Model):
'arch': template_record.arch.replace(template, key),
'name': name,
})
+
+ if view.arch_fs:
+ view.arch_fs = False
+
if ispage:
page = self.env['website.page'].create({
'url': page_url,
@@ -711,7 +715,7 @@ class Page(models.Model):
'website_id': website.id,
})
- return True
+ return url
@api.multi
@api.returns('self', lambda value: value.id)
diff --git a/addons/website/static/src/js/menu/content.js b/addons/website/static/src/js/menu/content.js
index b6d72df3d2a..a931cc057ef 100644
--- a/addons/website/static/src/js/menu/content.js
+++ b/addons/website/static/src/js/menu/content.js
@@ -205,7 +205,7 @@ var PagePropertiesDialog = weWidgets.Dialog.extend({
method: 'save_page_info',
args: [[context.website_id], params],
context: context,
- }).then(function () {
+ }).then(function (url) {
// If from page manager: reload url, if from page itself: go to
// (possibly) new url
if (self._getMainObject().model === 'website.page') {
diff --git a/addons/website_crm_partner_assign/models/crm_lead.py b/addons/website_crm_partner_assign/models/crm_lead.py
index 4115e397f64..b5f0c2d49c4 100644
--- a/addons/website_crm_partner_assign/models/crm_lead.py
+++ b/addons/website_crm_partner_assign/models/crm_lead.py
@@ -233,7 +233,7 @@ class CrmLead(models.Model):
# will be modified by the portal form. If no activity exist we create a new one instead
# that we assign to the portal user.
- user_activity = lead.activity_ids.filtered(lambda activity: activity.user_id == self.env.user)[:1]
+ user_activity = lead.sudo().activity_ids.filtered(lambda activity: activity.user_id == self.env.user)[:1]
if values['activity_date_deadline']:
if user_activity:
user_activity.sudo().write({
diff --git a/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml b/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml
index 96346a3814d..35a5d8cf256 100644
--- a/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml
+++ b/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml
@@ -330,7 +330,7 @@
</td>
<td><span t-field="opp.contact_name" /></td>
<td>
- <span class="label label-primary" title="Current stage of the opp" t-esc="opp.stage_id.name" />
+ <span class="label label-primary" title="Current stage of the opportunity" t-esc="opp.stage_id.name" />
</td>
</tr>
</tbody>
diff --git a/addons/website_crm_partner_assign/wizard/crm_forward_to_partner.py b/addons/website_crm_partner_assign/wizard/crm_forward_to_partner.py
index 5aa8073192f..3d85f0f4189 100644
--- a/addons/website_crm_partner_assign/wizard/crm_forward_to_partner.py
+++ b/addons/website_crm_partner_assign/wizard/crm_forward_to_partner.py
@@ -134,7 +134,7 @@ class CrmLeadAssignation(models.TransientModel):
lead_location = fields.Char('Lead Location')
partner_assigned_id = fields.Many2one('res.partner', 'Assigned Partner')
partner_location = fields.Char('Partner Location')
- lead_link = fields.Char('Lead Single Links')
+ lead_link = fields.Char('Link to Lead')
@api.onchange('lead_id')
def _onchange_lead_id(self):
diff --git a/addons/website_customer/controllers/main.py b/addons/website_customer/controllers/main.py
index 421a809b36d..063d4b9bb5f 100644
--- a/addons/website_customer/controllers/main.py
+++ b/addons/website_customer/controllers/main.py
@@ -24,7 +24,7 @@ class WebsiteCustomer(http.Controller):
if not qs or qs.lower() in loc:
yield {'loc': loc}
- dom = [('website_published', '=', True), ('assigned_partner_id', '!=', False)]
+ dom = [('website_published', '=', True), ('assigned_partner_id', '!=', False), ('country_id', '!=', False)]
dom += sitemap_qs2dom(qs, '/customers/country')
countries = env['res.partner'].sudo().read_group(dom, ['id', 'country_id'], groupby='country_id')
for country in countries:
diff --git a/addons/website_form/controllers/main.py b/addons/website_form/controllers/main.py
index f4fd9716aa2..b37ca97ea79 100644
--- a/addons/website_form/controllers/main.py
+++ b/addons/website_form/controllers/main.py
@@ -171,7 +171,8 @@ class WebsiteForm(http.Controller):
return data
def insert_record(self, request, model, values, custom, meta=None):
- record = request.env[model.model].sudo().with_context(mail_create_nosubscribe=True).create(values)
+ model_name = model.sudo().model
+ record = request.env[model_name].sudo().with_context(mail_create_nosubscribe=True).create(values)
if custom or meta:
default_field = model.website_form_default_field_id
@@ -183,13 +184,13 @@ class WebsiteForm(http.Controller):
# If there is a default field configured for this model, use it.
# If there isn't, put the custom data in a message instead
if default_field.name:
- if default_field.ttype == 'html' or model.model == 'mail.mail':
+ if default_field.ttype == 'html' or model_name == 'mail.mail':
custom_content = nl2br(custom_content)
record.update({default_field.name: custom_content})
else:
values = {
'body': nl2br(custom_content),
- 'model': model.model,
+ 'model': model_name,
'message_type': 'comment',
'no_auto_thread': False,
'res_id': record.id,
@@ -201,7 +202,8 @@ class WebsiteForm(http.Controller):
# Link all files attached on the form
def insert_attachment(self, model, id_record, files):
orphan_attachment_ids = []
- record = model.env[model.model].browse(id_record)
+ model_name = model.sudo().model
+ record = model.env[model_name].browse(id_record)
authorized_fields = model.sudo()._get_form_writable_fields()
for file in files:
custom_field = file.field_name not in authorized_fields
@@ -209,7 +211,7 @@ class WebsiteForm(http.Controller):
'name': file.filename,
'datas': base64.encodestring(file.read()),
'datas_fname': file.filename,
- 'res_model': model.model,
+ 'res_model': model_name,
'res_id': record.id,
}
attachment_id = request.env['ir.attachment'].sudo().create(attachment_value)
@@ -221,10 +223,10 @@ class WebsiteForm(http.Controller):
# If some attachments didn't match a field on the model,
# we create a mail.message to link them to the record
if orphan_attachment_ids:
- if model.model != 'mail.mail':
+ if model_name != 'mail.mail':
values = {
'body': _('<p>Attached files : </p>'),
- 'model': model.model,
+ 'model': model_name,
'message_type': 'comment',
'no_auto_thread': False,
'res_id': id_record,
diff --git a/addons/website_form/static/src/js/website_form.js b/addons/website_form/static/src/js/website_form.js
index 47ba66c244b..5318a84067d 100644
--- a/addons/website_form/static/src/js/website_form.js
+++ b/addons/website_form/static/src/js/website_form.js
@@ -58,7 +58,7 @@ odoo.define('website_form.animation', function (require) {
send: function (e) {
e.preventDefault(); // Prevent the default submit behavior
- this.$target.find('.o_website_form_send').off(); // Prevent users from crazy clicking
+ this.$target.find('.o_website_form_send').off().addClass('disabled'); // Prevent users from crazy clicking
var self = this;
@@ -231,7 +231,7 @@ odoo.define('website_form.animation', function (require) {
update_status: function (status) {
var self = this;
if (status !== 'success') { // Restore send button behavior if result is an error
- this.$target.find('.o_website_form_send').on('click',function (e) {self.send(e);});
+ this.$target.find('.o_website_form_send').on('click',function (e) {self.send(e);}).removeClass('disabled');
}
var $result = this.$('#o_website_form_result');
this.templates_loaded.done(function () {
diff --git a/addons/website_mail_channel/views/website_mail_channel_templates.xml b/addons/website_mail_channel/views/website_mail_channel_templates.xml
index 5c6b79d6e22..541683b7e8a 100644
--- a/addons/website_mail_channel/views/website_mail_channel_templates.xml
+++ b/addons/website_mail_channel/views/website_mail_channel_templates.xml
@@ -300,7 +300,7 @@
</div>
</li>
</ul>
- <p t-if="messages and msg_more_count > 0 and thread_header" class="well well-sm">
+ <p t-if="messages and (msg_more_count or 0) > 0 and thread_header" class="well well-sm">
<button class="fa btn-link o_mg_read_more"
t-attf-data-href="/groups/#{slug(group)}/#{slug(thread_header)}/get_replies"
t-attf-data-msg-id="#{messages[-1].id}">
diff --git a/addons/website_quote/controllers/main.py b/addons/website_quote/controllers/main.py
index f7ba4bc160b..77306fc34de 100644
--- a/addons/website_quote/controllers/main.py
+++ b/addons/website_quote/controllers/main.py
@@ -45,8 +45,8 @@ class sale_quote(http.Controller):
else:
Order = request.env['sale.order'].search([('id', '=', order_id)])
# Log only once a day
- if Order and request.session.get('view_quote') != now and request.env.user.share:
- request.session['view_quote'] = now
+ if Order and request.session.get('view_quote_%s' % Order.id) != now and request.env.user.share:
+ request.session['view_quote_%s' % Order.id] = now
body = _('Quotation viewed by customer')
_message_post_helper(res_model='sale.order', res_id=Order.id, message=body, token=token, message_type='notification', subtype="mail.mt_note", partner_ids=Order.user_id.sudo().partner_id.ids)
if not Order:
diff --git a/addons/website_quote/models/sale_order.py b/addons/website_quote/models/sale_order.py
index cce60910618..294cce55d26 100644
--- a/addons/website_quote/models/sale_order.py
+++ b/addons/website_quote/models/sale_order.py
@@ -52,7 +52,15 @@ class SaleOrder(models.Model):
return template and template.active and template or False
def _get_default_online_payment(self):
- return 1 if self.env['ir.config_parameter'].sudo().get_param('sale.sale_portal_confirmation_options', default='none') == 'pay' else 0
+ default_template = self._get_default_template()
+ if self.template_id:
+ return self.template_id.require_payment
+ elif default_template:
+ return default_template.require_payment
+ elif self.env['ir.config_parameter'].sudo().get_param('sale.sale_portal_confirmation_options', default='none') == 'pay':
+ return 1
+ else:
+ return 0
template_id = fields.Many2one(
'sale.quote.template', 'Quotation Template',
@@ -70,16 +78,9 @@ class SaleOrder(models.Model):
require_payment = fields.Selection([
(0, 'Online Signature'),
(1, 'Online Payment')], default=_get_default_online_payment, string='Confirmation Mode',
- compute='_compute_require_payment', store=True,
help="Choose how you want to confirm an order to launch the delivery process. You can either "
"request a digital signature or an upfront payment. With a digital signature, you can "
"request the payment when issuing the invoice.")
- @api.one
- @api.depends('template_id')
- def _compute_require_payment(self):
- # Do not overwrite if the field is already set
- if self.template_id and (self.require_payment is None or self.require_payment is False):
- self.require_payment = self.template_id.require_payment
@api.multi
@api.returns('self', lambda value: value.id)
diff --git a/addons/website_sale/models/website.py b/addons/website_sale/models/website.py
index 5de0f524fc7..9f43a6791d8 100644
--- a/addons/website_sale/models/website.py
+++ b/addons/website_sale/models/website.py
@@ -14,7 +14,7 @@ class Website(models.Model):
_inherit = 'website'
pricelist_id = fields.Many2one('product.pricelist', compute='_compute_pricelist_id', string='Default Pricelist')
- currency_id = fields.Many2one('res.currency', related='pricelist_id.currency_id', string='Default Currency')
+ currency_id = fields.Many2one('res.currency', related='pricelist_id.currency_id', related_sudo=False, string='Default Currency')
salesperson_id = fields.Many2one('res.users', string='Salesperson')
salesteam_id = fields.Many2one('crm.team', string='Sales Channel')
pricelist_ids = fields.One2many('product.pricelist', compute="_compute_pricelist_ids",
diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml
index 4325d669fda..af6c2617d60 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -173,7 +173,7 @@
<t t-set="additional_title">Shop</t>
<div id="wrap" class="js_sale">
<div class="oe_structure" />
- <div class="container oe_website_sale">
+ <div t-attf-class="container oe_website_sale #{'oe_website_sale_search' if search else None}">
<div class="products_pager">
<div class="row" style="width: 100%;">
<t t-call="website_sale.search">
diff --git a/addons/website_sale_delivery/models/res_country.py b/addons/website_sale_delivery/models/res_country.py
index 8b4648a7105..601ac731787 100644
--- a/addons/website_sale_delivery/models/res_country.py
+++ b/addons/website_sale_delivery/models/res_country.py
@@ -35,5 +35,7 @@ class ResCountry(models.Model):
states = res
break
states |= carrier.state_ids
+ if not states:
+ states = states.search([('country_id', '=', self.id)])
res = res & states
return res
diff --git a/addons/website_sale_delivery/views/website_sale_delivery_templates.xml b/addons/website_sale_delivery/views/website_sale_delivery_templates.xml
index 63f877a0a52..3a39f64c27f 100644
--- a/addons/website_sale_delivery/views/website_sale_delivery_templates.xml
+++ b/addons/website_sale_delivery/views/website_sale_delivery_templates.xml
@@ -35,12 +35,12 @@
<label class="label-optional" t-field="delivery.name" t-att-for="'delivery_%i' % delivery.id"/>
<t t-if="delivery.delivery_type == 'fixed'">
<span t-if="delivery.fixed_price > 0.0" class="badge pull-right"
- t-field="delivery.fixed_price" t-options='{"widget": "monetary", "display_currency": website_sale_order.currency_id}'/>
+ t-field="delivery.fixed_price" t-options='{"widget": "monetary",
+ "from_currency": delivery.product_id.company_id.currency_id, "display_currency": website_sale_order.currency_id}'/>
<span t-else="" class="badge pull-right">Free</span>
</t>
<t t-else="">
- <span class="badge pull-right hidden" t-field="delivery.fixed_price" t-options='{"widget": "monetary",
- "display_currency": website_sale_order.currency_id}'/>
+ <span class="badge pull-right hidden" t-field="delivery.fixed_price" t-options='{"widget": "monetary", "from_currency": delivery.product_id.company_id.currency_id, "display_currency": website_sale_order.currency_id}'/>
<span class="badge pull-right o_delivery_compute">Select to compute delivery rate</span>
</t>
<t t-if="delivery.website_description">
diff --git a/addons/website_slides/models/slides.py b/addons/website_slides/models/slides.py
index d2e24ba52a8..d390570d448 100644
--- a/addons/website_slides/models/slides.py
+++ b/addons/website_slides/models/slides.py
@@ -348,7 +348,7 @@ class Slide(models.Model):
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
for record in self:
if record.datas and (not record.document_id or record.slide_type in ['document', 'presentation']):
- record.embed_code = '<iframe src="%s/slides/embed/%s?page=1" allowFullScreen="true" height="%s" width="%s" frameborder="0"></iframe>' % (base_url, record.id, 315, 420)
+ record.embed_code = '<iframe src="%s/slides/embed/%s?page=1" class="o_wslides_iframe_viewer" allowFullScreen="true" height="%s" width="%s" frameborder="0"></iframe>' % (base_url, record.id, 315, 420)
elif record.slide_type == 'video' and record.document_id:
if not record.mime_type:
# embed youtube video
diff --git a/addons/website_slides/static/src/js/slides.js b/addons/website_slides/static/src/js/slides.js
index 7d945e9eb28..b331ebf366c 100644
--- a/addons/website_slides/static/src/js/slides.js
+++ b/addons/website_slides/static/src/js/slides.js
@@ -109,11 +109,12 @@ var page_widgets = {};
},
});
- $('iframe').ready(function() {
+ $('iframe.o_wslides_iframe_viewer').ready(function() {
// TODO : make it work. For now, once the iframe is loaded, the value of #page_count is
// still now set (the pdf is still loading)
- var max_page = $('iframe').contents().find('#page_count').val();
- new SlideSocialEmbed($(this), max_page).setElement($('.oe_slide_js_embed_code_widget'));
+ var $iframe = $(this);
+ var max_page = $iframe.contents().find('#page_count').val();
+ new SlideSocialEmbed($iframe, max_page).setElement($('.oe_slide_js_embed_code_widget'));
});
diff --git a/addons/website_slides/static/src/js/slides_embed.js b/addons/website_slides/static/src/js/slides_embed.js
index 4e7d3e71652..d3d1321e6c8 100644
--- a/addons/website_slides/static/src/js/slides_embed.js
+++ b/addons/website_slides/static/src/js/slides_embed.js
@@ -50,12 +50,12 @@ $(document).ready(function () {
},
// page switching
render_page: function (page_number) {
- this.pdf_viewer.renderPage(page_number).then(this.on_rendered_page);
+ this.pdf_viewer.renderPage(page_number).then(this.on_rendered_page.bind(this));
},
change_page: function () {
var page_asked = parseInt(this.$('#page_number').val(), 10);
if(1 <= page_asked && page_asked <= this.pdf_viewer.pdf_page_total){
- this.pdf_viewer.changePage(page_asked).then(this.on_rendered_page);
+ this.pdf_viewer.changePage(page_asked).then(this.on_rendered_page.bind(this));
}else{
// if page number out of range, reset the page_counter to the actual page
this.$('#page_number').val(this.pdf_viewer.pdf_page_current);
diff --git a/doc/_extensions/odoo_ext/layout.html b/doc/_extensions/odoo_ext/layout.html
index cb1015d2ebe..793ed47806f 100644
--- a/doc/_extensions/odoo_ext/layout.html
+++ b/doc/_extensions/odoo_ext/layout.html
@@ -102,8 +102,8 @@
<div>It's all about efficiency</div>
</div>
<ul>
- <li><a href="https://www.odoo.com/page/accounting/">Accounting</a></li>
- <li><a href="https://www.odoo.com/page/project-management/">Project</a></li>
+ <li><a href="https://www.odoo.com/page/accounting">Accounting</a></li>
+ <li><a href="https://www.odoo.com/page/project-management">Project</a></li>
<li class="dropdown">
<a href="#0" class="dropdown-toggle">Human Resources</a>
<ul>
diff --git a/doc/cla/corporate/camptocamp.md b/doc/cla/corporate/camptocamp.md
index c934c831a83..777bb974dc5 100644
--- a/doc/cla/corporate/camptocamp.md
+++ b/doc/cla/corporate/camptocamp.md
@@ -32,3 +32,4 @@ Denis Leemann denis.leemann@camptocamp.com https://github.com/leemannd
Akim Juillerat akim.juillerat@camptocamp.com https://github.com/grindtildeath
Damien Crier damien.crier@camptocamp.com https://github.com/damdam-s
Simone Orsi simahawk@gmail.com https://github.com/simahawk
+Patrick Tombez patrick.tombez@camptocamp.com https://github.com/p-tombez
diff --git a/doc/cla/corporate/d2c.md b/doc/cla/corporate/d2c.md
new file mode 100644
index 00000000000..37ffc7931ad
--- /dev/null
+++ b/doc/cla/corporate/d2c.md
@@ -0,0 +1,13 @@
+Russia, 2018-02-09
+
+D2C agrees to the terms of the Odoo Corporate Contributor License Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this declaration.
+
+Signed,
+
+Denis Zaripov mastappl@gmail.com https://github.com/mastappl
+
+List of contributors:
+
+Denis Zaripov mastappl@gmail.com https://github.com/mastappl
diff --git a/doc/cla/corporate/merchise.md b/doc/cla/corporate/merchise.md
index d8d5e75f830..e4cbd3c3f95 100644
--- a/doc/cla/corporate/merchise.md
+++ b/doc/cla/corporate/merchise.md
@@ -20,3 +20,4 @@ Sergio Valdés sergiov@merchise.org
Medardo Rodríguez med@merchise.org https://github.com/med-merchise
Abel Firvida firvida@merchise.org
Abel Firvida abel@merchise.org
+Mónica Díaz Pena monicadp@merchise.org https://github.com/mdpena
diff --git a/doc/cla/corporate/odoogap.md b/doc/cla/corporate/odoogap.md
new file mode 100644
index 00000000000..08a10ddd683
--- /dev/null
+++ b/doc/cla/corporate/odoogap.md
@@ -0,0 +1,18 @@
+PromptEQUATION, 2017-09-03
+
+PromptEquation/ OdooGap agrees to the terms of the Odoo Corporate Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Diogo Duarte dduarte@odoogap.com https://github.com/dduarte-odoogap
+
+List of contributors:
+
+Diogo Duarte dduarte@odoogap.com https://github.com/dduarte-odoogap
+Diogo Duarte diogocarvalhoduarte@gmail.com https://github.com/dduarte-odoogap
+Carlos Fonseca cfonseca@odoogap.com https://github.com/cfonseca-odoogap
+Carlos Fonseca carlosmigueldafonseca@gmail.com https://github.com/cpintofonseca
diff --git a/doc/cla/individual/apesquero.md b/doc/cla/individual/apesquero.md
new file mode 100644
index 00000000000..8c04cee5dc7
--- /dev/null
+++ b/doc/cla/individual/apesquero.md
@@ -0,0 +1,11 @@
+Spain, 2018-02-26
+
+I hereby agree to the terms of the Odoo Individual Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Amaro Pesquero apesquero@gmail.com https://github.com/apesquero
diff --git a/doc/cla/individual/aymanadam5.md b/doc/cla/individual/aymanadam5.md
new file mode 100644
index 00000000000..c8ee9b812c7
--- /dev/null
+++ b/doc/cla/individual/aymanadam5.md
@@ -0,0 +1,11 @@
+Sudan, 2018-02-23
+
+I hereby agree to the terms of the Odoo Individual Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Ayman adam Dawood notime4love112@gmail.com https://github.com/aymanadam5
diff --git a/doc/cla/individual/hilarak.md b/doc/cla/individual/hilarak.md
new file mode 100644
index 00000000000..4e164fe88d1
--- /dev/null
+++ b/doc/cla/individual/hilarak.md
@@ -0,0 +1,11 @@
+India, 18-02-2018
+
+I hereby agree to the terms of the Odoo Individual Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Hilar AK hilarak@gmail.com https://github.com/hilarak
diff --git a/doc/cla/individual/lbelorgey.md b/doc/cla/individual/lbelorgey.md
index 6cdacb5cdf1..c8db490ec4d 100644
--- a/doc/cla/individual/lbelorgey.md
+++ b/doc/cla/individual/lbelorgey.md
@@ -8,4 +8,4 @@ declaration.
Signed,
-Laurent Bélorgey lb@lalieutenante.com https://github.com/lbelorgey \ No newline at end of file
+Laurent Bélorgey lb@lalieutenante.com https://github.com/lbelorgey \ No newline at end of file
diff --git a/doc/cla/individual/novawish.md b/doc/cla/individual/novawish.md
new file mode 100644
index 00000000000..7cb30078983
--- /dev/null
+++ b/doc/cla/individual/novawish.md
@@ -0,0 +1,11 @@
+Vietnam, 2018-02-10
+
+I hereby agree to the terms of the Odoo Individual Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Duc Dao duc.dd@komit-consulting.com https://github.com/novawish
diff --git a/doc/cla/individual/true-datura.md b/doc/cla/individual/true-datura.md
new file mode 100644
index 00000000000..e0d0faa8237
--- /dev/null
+++ b/doc/cla/individual/true-datura.md
@@ -0,0 +1,11 @@
+Ukraine, 2018-03-07
+
+I hereby agree to the terms of the Odoo Individual Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Demid Avramenko grrrr@protonmail.com https://github.com/true-datura
diff --git a/doc/howtos/profile/flamegraph.svg b/doc/howtos/profile/flamegraph.svg
new file mode 100644
index 00000000000..cb57f882cb4
--- /dev/null
+++ b/doc/howtos/profile/flamegraph.svg
@@ -0,0 +1,1774 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" width="1200" height="1078" onload="init(evt)" viewBox="0 0 1200 1078" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<!-- Flame graph stack visualization. See https://github.com/brendangregg/FlameGraph for latest version, and http://www.brendangregg.com/flamegraphs.html for examples. -->
+<!-- NOTES: -->
+<defs >
+ <linearGradient id="background" y1="0" y2="1" x1="0" x2="0" >
+ <stop stop-color="#eeeeee" offset="5%" />
+ <stop stop-color="#eeeeb0" offset="95%" />
+ </linearGradient>
+</defs>
+<style type="text/css">
+ .func_g:hover { stroke:black; stroke-width:0.5; cursor:pointer; }
+</style>
+<script type="text/ecmascript">
+<![CDATA[
+ var details, searchbtn, matchedtxt, svg;
+ function init(evt) {
+ details = document.getElementById("details").firstChild;
+ searchbtn = document.getElementById("search");
+ matchedtxt = document.getElementById("matched");
+ svg = document.getElementsByTagName("svg")[0];
+ searching = 0;
+ }
+
+ // mouse-over for info
+ function s(node) { // show
+ info = g_to_text(node);
+ details.nodeValue = "Function: " + info;
+ }
+ function c() { // clear
+ details.nodeValue = ' ';
+ }
+
+ // ctrl-F for search
+ window.addEventListener("keydown",function (e) {
+ if (e.keyCode === 114 || (e.ctrlKey && e.keyCode === 70)) {
+ e.preventDefault();
+ search_prompt();
+ }
+ })
+
+ // functions
+ function find_child(parent, name, attr) {
+ var children = parent.childNodes;
+ for (var i=0; i<children.length;i++) {
+ if (children[i].tagName == name)
+ return (attr != undefined) ? children[i].attributes[attr].value : children[i];
+ }
+ return;
+ }
+ function orig_save(e, attr, val) {
+ if (e.attributes["_orig_"+attr] != undefined) return;
+ if (e.attributes[attr] == undefined) return;
+ if (val == undefined) val = e.attributes[attr].value;
+ e.setAttribute("_orig_"+attr, val);
+ }
+ function orig_load(e, attr) {
+ if (e.attributes["_orig_"+attr] == undefined) return;
+ e.attributes[attr].value = e.attributes["_orig_"+attr].value;
+ e.removeAttribute("_orig_"+attr);
+ }
+ function g_to_text(e) {
+ var text = find_child(e, "title").firstChild.nodeValue;
+ return (text)
+ }
+ function g_to_func(e) {
+ var func = g_to_text(e);
+ // if there's any manipulation we want to do to the function
+ // name before it's searched, do it here before returning.
+ return (func);
+ }
+ function update_text(e) {
+ var r = find_child(e, "rect");
+ var t = find_child(e, "text");
+ var w = parseFloat(r.attributes["width"].value) -3;
+ var txt = find_child(e, "title").textContent.replace(/\([^(]*\)$/,"");
+ t.attributes["x"].value = parseFloat(r.attributes["x"].value) +3;
+
+ // Smaller than this size won't fit anything
+ if (w < 2*12*0.59) {
+ t.textContent = "";
+ return;
+ }
+
+ t.textContent = txt;
+ // Fit in full text width
+ if (/^ *$/.test(txt) || t.getSubStringLength(0, txt.length) < w)
+ return;
+
+ for (var x=txt.length-2; x>0; x--) {
+ if (t.getSubStringLength(0, x+2) <= w) {
+ t.textContent = txt.substring(0,x) + "..";
+ return;
+ }
+ }
+ t.textContent = "";
+ }
+
+ // zoom
+ function zoom_reset(e) {
+ if (e.attributes != undefined) {
+ orig_load(e, "x");
+ orig_load(e, "width");
+ }
+ if (e.childNodes == undefined) return;
+ for(var i=0, c=e.childNodes; i<c.length; i++) {
+ zoom_reset(c[i]);
+ }
+ }
+ function zoom_child(e, x, ratio) {
+ if (e.attributes != undefined) {
+ if (e.attributes["x"] != undefined) {
+ orig_save(e, "x");
+ e.attributes["x"].value = (parseFloat(e.attributes["x"].value) - x - 10) * ratio + 10;
+ if(e.tagName == "text") e.attributes["x"].value = find_child(e.parentNode, "rect", "x") + 3;
+ }
+ if (e.attributes["width"] != undefined) {
+ orig_save(e, "width");
+ e.attributes["width"].value = parseFloat(e.attributes["width"].value) * ratio;
+ }
+ }
+
+ if (e.childNodes == undefined) return;
+ for(var i=0, c=e.childNodes; i<c.length; i++) {
+ zoom_child(c[i], x-10, ratio);
+ }
+ }
+ function zoom_parent(e) {
+ if (e.attributes) {
+ if (e.attributes["x"] != undefined) {
+ orig_save(e, "x");
+ e.attributes["x"].value = 10;
+ }
+ if (e.attributes["width"] != undefined) {
+ orig_save(e, "width");
+ e.attributes["width"].value = parseInt(svg.width.baseVal.value) - (10*2);
+ }
+ }
+ if (e.childNodes == undefined) return;
+ for(var i=0, c=e.childNodes; i<c.length; i++) {
+ zoom_parent(c[i]);
+ }
+ }
+ function zoom(node) {
+ var attr = find_child(node, "rect").attributes;
+ var width = parseFloat(attr["width"].value);
+ var xmin = parseFloat(attr["x"].value);
+ var xmax = parseFloat(xmin + width);
+ var ymin = parseFloat(attr["y"].value);
+ var ratio = (svg.width.baseVal.value - 2*10) / width;
+
+ // XXX: Workaround for JavaScript float issues (fix me)
+ var fudge = 0.0001;
+
+ var unzoombtn = document.getElementById("unzoom");
+ unzoombtn.style["opacity"] = "1.0";
+
+ var el = document.getElementsByTagName("g");
+ for(var i=0;i<el.length;i++){
+ var e = el[i];
+ var a = find_child(e, "rect").attributes;
+ var ex = parseFloat(a["x"].value);
+ var ew = parseFloat(a["width"].value);
+ // Is it an ancestor
+ if (0 == 0) {
+ var upstack = parseFloat(a["y"].value) > ymin;
+ } else {
+ var upstack = parseFloat(a["y"].value) < ymin;
+ }
+ if (upstack) {
+ // Direct ancestor
+ if (ex <= xmin && (ex+ew+fudge) >= xmax) {
+ e.style["opacity"] = "0.5";
+ zoom_parent(e);
+ e.onclick = function(e){unzoom(); zoom(this);};
+ update_text(e);
+ }
+ // not in current path
+ else
+ e.style["display"] = "none";
+ }
+ // Children maybe
+ else {
+ // no common path
+ if (ex < xmin || ex + fudge >= xmax) {
+ e.style["display"] = "none";
+ }
+ else {
+ zoom_child(e, xmin, ratio);
+ e.onclick = function(e){zoom(this);};
+ update_text(e);
+ }
+ }
+ }
+ }
+ function unzoom() {
+ var unzoombtn = document.getElementById("unzoom");
+ unzoombtn.style["opacity"] = "0.0";
+
+ var el = document.getElementsByTagName("g");
+ for(i=0;i<el.length;i++) {
+ el[i].style["display"] = "block";
+ el[i].style["opacity"] = "1";
+ zoom_reset(el[i]);
+ update_text(el[i]);
+ }
+ }
+
+ // search
+ function reset_search() {
+ var el = document.getElementsByTagName("rect");
+ for (var i=0; i < el.length; i++) {
+ orig_load(el[i], "fill")
+ }
+ }
+ function search_prompt() {
+ if (!searching) {
+ var term = prompt("Enter a search term (regexp " +
+ "allowed, eg: ^ext4_)", "");
+ if (term != null) {
+ search(term)
+ }
+ } else {
+ reset_search();
+ searching = 0;
+ searchbtn.style["opacity"] = "0.1";
+ searchbtn.firstChild.nodeValue = "Search"
+ matchedtxt.style["opacity"] = "0.0";
+ matchedtxt.firstChild.nodeValue = ""
+ }
+ }
+ function search(term) {
+ var re = new RegExp(term);
+ var el = document.getElementsByTagName("g");
+ var matches = new Object();
+ var maxwidth = 0;
+ for (var i = 0; i < el.length; i++) {
+ var e = el[i];
+ if (e.attributes["class"].value != "func_g")
+ continue;
+ var func = g_to_func(e);
+ var rect = find_child(e, "rect");
+ if (rect == null) {
+ // the rect might be wrapped in an anchor
+ // if nameattr href is being used
+ if (rect = find_child(e, "a")) {
+ rect = find_child(r, "rect");
+ }
+ }
+ if (func == null || rect == null)
+ continue;
+
+ // Save max width. Only works as we have a root frame
+ var w = parseFloat(rect.attributes["width"].value);
+ if (w > maxwidth)
+ maxwidth = w;
+
+ if (func.match(re)) {
+ // highlight
+ var x = parseFloat(rect.attributes["x"].value);
+ orig_save(rect, "fill");
+ rect.attributes["fill"].value =
+ "rgb(230,0,230)";
+
+ // remember matches
+ if (matches[x] == undefined) {
+ matches[x] = w;
+ } else {
+ if (w > matches[x]) {
+ // overwrite with parent
+ matches[x] = w;
+ }
+ }
+ searching = 1;
+ }
+ }
+ if (!searching)
+ return;
+
+ searchbtn.style["opacity"] = "1.0";
+ searchbtn.firstChild.nodeValue = "Reset Search"
+
+ // calculate percent matched, excluding vertical overlap
+ var count = 0;
+ var lastx = -1;
+ var lastw = 0;
+ var keys = Array();
+ for (k in matches) {
+ if (matches.hasOwnProperty(k))
+ keys.push(k);
+ }
+ // sort the matched frames by their x location
+ // ascending, then width descending
+ keys.sort(function(a, b){
+ return a - b;
+ });
+ // Step through frames saving only the biggest bottom-up frames
+ // thanks to the sort order. This relies on the tree property
+ // where children are always smaller than their parents.
+ var fudge = 0.0001; // JavaScript floating point
+ for (var k in keys) {
+ var x = parseFloat(keys[k]);
+ var w = matches[keys[k]];
+ if (x >= lastx + lastw - fudge) {
+ count += w;
+ lastx = x;
+ lastw = w;
+ }
+ }
+ // display matched percent
+ matchedtxt.style["opacity"] = "1.0";
+ pct = 100 * count / maxwidth;
+ if (pct == 100)
+ pct = "100"
+ else
+ pct = pct.toFixed(1)
+ matchedtxt.firstChild.nodeValue = "Matched: " + pct + "%";
+ }
+ function searchover(e) {
+ searchbtn.style["opacity"] = "1.0";
+ }
+ function searchout(e) {
+ if (searching) {
+ searchbtn.style["opacity"] = "1.0";
+ } else {
+ searchbtn.style["opacity"] = "0.1";
+ }
+ }
+]]>
+</script>
+<rect x="0.0" y="0" width="1200.0" height="1078.0" fill="url(#background)" />
+<text text-anchor="middle" x="600.00" y="24" font-size="17" font-family="Verdana" fill="rgb(0,0,0)" >Flame Graph</text>
+<text text-anchor="" x="10.00" y="1061" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" id="details" > </text>
+<text text-anchor="" x="10.00" y="24" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" id="unzoom" onclick="unzoom()" style="opacity:0.0;cursor:pointer" >Reset Zoom</text>
+<text text-anchor="" x="1090.00" y="24" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" id="search" onmouseover="searchover()" onmouseout="searchout()" onclick="search_prompt()" style="opacity:0.1;cursor:pointer" >Search</text>
+<text text-anchor="" x="1090.00" y="1061" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" id="matched" > </text>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:browse:4237 (11 samples, 8.33%)</title><rect x="760.9" y="85" width="98.3" height="15.0" fill="rgb(217,55,7)" rx="2" ry="2" />
+<text text-anchor="" x="763.91" y="95.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/prog..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:__get__:948 (1 samples, 0.76%)</title><rect x="1181.1" y="357" width="8.9" height="15.0" fill="rgb(205,141,36)" rx="2" ry="2" />
+<text text-anchor="" x="1184.06" y="367.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__new__:735 (3 samples, 2.27%)</title><rect x="188.8" y="197" width="26.8" height="15.0" fill="rgb(217,179,29)" rx="2" ry="2" />
+<text text-anchor="" x="191.79" y="207.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:__get__:948 (38 samples, 28.79%)</title><rect x="617.9" y="181" width="339.7" height="15.0" fill="rgb(223,45,46)" rx="2" ry="2" />
+<text text-anchor="" x="620.88" y="191.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/fields.py:__get__:948</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__new__:736 (5 samples, 3.79%)</title><rect x="1064.8" y="213" width="44.7" height="15.0" fill="rgb(214,74,47)" rx="2" ry="2" />
+<text text-anchor="" x="1067.85" y="223.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/dat..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>&lt;string&gt;:__new__:14 (1 samples, 0.76%)</title><rect x="144.1" y="213" width="8.9" height="15.0" fill="rgb(221,53,21)" rx="2" ry="2" />
+<text text-anchor="" x="147.09" y="223.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_normalize_ids:5206 (1 samples, 0.76%)</title><rect x="752.0" y="69" width="8.9" height="15.0" fill="rgb(243,142,41)" rx="2" ry="2" />
+<text text-anchor="" x="754.97" y="79.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>&lt;frozen importlib._bootstrap&gt;:_call_with_frames_removed:219 (1 samples, 0.76%)</title><rect x="10.0" y="389" width="8.9" height="15.0" fill="rgb(239,101,4)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="399.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:sudo:4310 (3 samples, 2.27%)</title><rect x="510.6" y="197" width="26.8" height="15.0" fill="rgb(219,219,42)" rx="2" ry="2" />
+<text text-anchor="" x="513.61" y="207.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>&lt;frozen importlib._bootstrap&gt;:_call_with_frames_removed:219 (1 samples, 0.76%)</title><rect x="10.0" y="181" width="8.9" height="15.0" fill="rgb(212,152,44)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="191.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__new__:736 (1 samples, 0.76%)</title><rect x="528.5" y="165" width="8.9" height="15.0" fill="rgb(249,121,35)" rx="2" ry="2" />
+<text text-anchor="" x="531.48" y="175.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>&lt;frozen importlib._bootstrap_external&gt;:exec_module:678 (1 samples, 0.76%)</title><rect x="10.0" y="197" width="8.9" height="15.0" fill="rgb(214,75,51)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="207.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:convert_to_cache:1230 (6 samples, 4.55%)</title><rect x="957.6" y="261" width="53.6" height="15.0" fill="rgb(223,116,48)" rx="2" ry="2" />
+<text text-anchor="" x="960.58" y="271.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/data..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__new__:736 (1 samples, 0.76%)</title><rect x="179.8" y="181" width="9.0" height="15.0" fill="rgb(216,82,40)" rx="2" ry="2" />
+<text text-anchor="" x="182.85" y="191.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__hash__:777 (1 samples, 0.76%)</title><rect x="1002.3" y="165" width="8.9" height="15.0" fill="rgb(206,8,25)" rx="2" ry="2" />
+<text text-anchor="" x="1005.27" y="175.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__new__:736 (1 samples, 0.76%)</title><rect x="376.5" y="165" width="9.0" height="15.0" fill="rgb(209,201,14)" rx="2" ry="2" />
+<text text-anchor="" x="379.52" y="175.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/sql_db.py:execute:242 (1 samples, 0.76%)</title><rect x="153.0" y="229" width="9.0" height="15.0" fill="rgb(228,193,32)" rx="2" ry="2" />
+<text text-anchor="" x="156.03" y="239.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_read_group_raw:1924 (7 samples, 5.30%)</title><rect x="448.0" y="245" width="62.6" height="15.0" fill="rgb(233,3,38)" rx="2" ry="2" />
+<text text-anchor="" x="451.03" y="255.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:qualify:2612 (1 samples, 0.76%)</title><rect x="877.1" y="85" width="9.0" height="15.0" fill="rgb(210,2,30)" rx="2" ry="2" />
+<text text-anchor="" x="880.12" y="95.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/addons/auth_signup/models/ir_http.py:_dispatch:19 (128 samples, 96.97%)</title><rect x="45.8" y="613" width="1144.2" height="15.0" fill="rgb(222,215,9)" rx="2" ry="2" />
+<text text-anchor="" x="48.76" y="623.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/addons/auth_signup/models/ir_http.py:_dispatch:19</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__new__:742 (1 samples, 0.76%)</title><rect x="993.3" y="213" width="9.0" height="15.0" fill="rgb(239,195,13)" rx="2" ry="2" />
+<text text-anchor="" x="996.33" y="223.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>&lt;string&gt;:change_digit:9 (6 samples, 4.55%)</title><rect x="957.6" y="229" width="53.6" height="15.0" fill="rgb(205,82,12)" rx="2" ry="2" />
+<text text-anchor="" x="960.58" y="239.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >&lt;stri..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/encodings/utf_8.py:decode:16 (1 samples, 0.76%)</title><rect x="886.1" y="69" width="8.9" height="15.0" fill="rgb(233,11,36)" rx="2" ry="2" />
+<text text-anchor="" x="889.06" y="79.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:convert_to_cache:1230 (7 samples, 5.30%)</title><rect x="1109.5" y="261" width="62.6" height="15.0" fill="rgb(227,91,0)" rx="2" ry="2" />
+<text text-anchor="" x="1112.55" y="271.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_read_group_resolve_many2one_fields:1944 (6 samples, 4.55%)</title><rect x="457.0" y="229" width="53.6" height="15.0" fill="rgb(243,3,5)" rx="2" ry="2" />
+<text text-anchor="" x="459.97" y="239.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/data..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/addons/stock_account/models/product.py:do_change_standard_price:126 (20 samples, 15.15%)</title><rect x="135.2" y="373" width="178.7" height="15.0" fill="rgb(251,96,44)" rx="2" ry="2" />
+<text text-anchor="" x="138.15" y="383.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/addon..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:read_group:1828 (13 samples, 9.85%)</title><rect x="394.4" y="261" width="116.2" height="15.0" fill="rgb(212,7,8)" rx="2" ry="2" />
+<text text-anchor="" x="397.39" y="271.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/o..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__new__:736 (1 samples, 0.76%)</title><rect x="215.6" y="197" width="8.9" height="15.0" fill="rgb(233,70,10)" rx="2" ry="2" />
+<text text-anchor="" x="218.61" y="207.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:convert_to_record:1989 (3 samples, 2.27%)</title><rect x="930.8" y="85" width="26.8" height="15.0" fill="rgb(254,216,54)" rx="2" ry="2" />
+<text text-anchor="" x="933.76" y="95.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:recompute:4791 (1 samples, 0.76%)</title><rect x="1172.1" y="309" width="9.0" height="15.0" fill="rgb(254,165,3)" rx="2" ry="2" />
+<text text-anchor="" x="1175.12" y="319.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:__get__:944 (1 samples, 0.76%)</title><rect x="921.8" y="101" width="9.0" height="15.0" fill="rgb(212,166,14)" rx="2" ry="2" />
+<text text-anchor="" x="924.82" y="111.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:browse:4235 (2 samples, 1.52%)</title><rect x="743.0" y="85" width="17.9" height="15.0" fill="rgb(250,174,44)" rx="2" ry="2" />
+<text text-anchor="" x="746.03" y="95.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:add:939 (1 samples, 0.76%)</title><rect x="1002.3" y="197" width="8.9" height="15.0" fill="rgb(218,222,13)" rx="2" ry="2" />
+<text text-anchor="" x="1005.27" y="207.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/osv/expression.py:&lt;listcomp&gt;:1204 (1 samples, 0.76%)</title><rect x="421.2" y="181" width="9.0" height="15.0" fill="rgb(215,5,53)" rx="2" ry="2" />
+<text text-anchor="" x="424.21" y="191.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>&lt;decorator-gen-112&gt;:precision_get:2 (1 samples, 0.76%)</title><rect x="242.4" y="197" width="9.0" height="15.0" fill="rgb(238,179,11)" rx="2" ry="2" />
+<text text-anchor="" x="245.42" y="207.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/osv/expression.py:get_unaccent_wrapper:455 (1 samples, 0.76%)</title><rect x="394.4" y="197" width="8.9" height="15.0" fill="rgb(224,116,13)" rx="2" ry="2" />
+<text text-anchor="" x="397.39" y="207.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/site-packages/werkzeug/routing.py:add:1197 (1 samples, 0.76%)</title><rect x="45.8" y="517" width="8.9" height="15.0" fill="rgb(216,213,29)" rx="2" ry="2" />
+<text text-anchor="" x="48.76" y="527.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_read_group_raw:1924 (3 samples, 2.27%)</title><rect x="510.6" y="245" width="26.8" height="15.0" fill="rgb(249,80,31)" rx="2" ry="2" />
+<text text-anchor="" x="513.61" y="255.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/site-packages/werkzeug/serving.py:run_wsgi:193 (132 samples, 100.00%)</title><rect x="10.0" y="741" width="1180.0" height="15.0" fill="rgb(239,203,23)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="751.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/usr/local/lib/python3.6/site-packages/werkzeug/serving.py:run_wsgi:193</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:get_todo:911 (1 samples, 0.76%)</title><rect x="1172.1" y="293" width="9.0" height="15.0" fill="rgb(249,96,3)" rx="2" ry="2" />
+<text text-anchor="" x="1175.12" y="303.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>odoo-bin:&lt;module&gt;:8 (132 samples, 100.00%)</title><rect x="10.0" y="1013" width="1180.0" height="15.0" fill="rgb(212,189,25)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="1023.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >odoo-bin:&lt;module&gt;:8</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:__set__:966 (7 samples, 5.30%)</title><rect x="242.4" y="261" width="62.6" height="15.0" fill="rgb(233,18,33)" rx="2" ry="2" />
+<text text-anchor="" x="245.42" y="271.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/modules/loading.py:load_modules:339 (1 samples, 0.76%)</title><rect x="10.0" y="565" width="8.9" height="15.0" fill="rgb(217,166,20)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="575.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/_weakrefset.py:__iter__:60 (1 samples, 0.76%)</title><rect x="510.6" y="149" width="8.9" height="15.0" fill="rgb(231,103,26)" rx="2" ry="2" />
+<text text-anchor="" x="513.61" y="159.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_read_from_database:2618 (1 samples, 0.76%)</title><rect x="877.1" y="117" width="9.0" height="15.0" fill="rgb(225,32,35)" rx="2" ry="2" />
+<text text-anchor="" x="880.12" y="127.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:&lt;listcomp&gt;:2618 (1 samples, 0.76%)</title><rect x="877.1" y="101" width="9.0" height="15.0" fill="rgb(218,43,39)" rx="2" ry="2" />
+<text text-anchor="" x="880.12" y="111.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>all (132 samples, 100%)</title><rect x="10.0" y="1029" width="1180.0" height="15.0" fill="rgb(210,86,49)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="1039.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/sre_compile.py:compile:566 (1 samples, 0.76%)</title><rect x="45.8" y="437" width="8.9" height="15.0" fill="rgb(248,45,37)" rx="2" ry="2" />
+<text text-anchor="" x="48.76" y="447.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/service/server.py:run:743 (132 samples, 100.00%)</title><rect x="10.0" y="885" width="1180.0" height="15.0" fill="rgb(242,175,1)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="895.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/service/server.py:run:743</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__call__:789 (9 samples, 6.82%)</title><rect x="537.4" y="245" width="80.5" height="15.0" fill="rgb(242,157,8)" rx="2" ry="2" />
+<text text-anchor="" x="540.42" y="255.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/pr..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/cli/server.py:main:169 (132 samples, 100.00%)</title><rect x="10.0" y="965" width="1180.0" height="15.0" fill="rgb(217,171,7)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="975.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/cli/server.py:main:169</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/addons/stock/models/product.py:_compute_quantities_dict:124 (13 samples, 9.85%)</title><rect x="394.4" y="277" width="116.2" height="15.0" fill="rgb(222,114,2)" rx="2" ry="2" />
+<text text-anchor="" x="397.39" y="287.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/o..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:__setitem__:4666 (7 samples, 5.30%)</title><rect x="242.4" y="277" width="62.6" height="15.0" fill="rgb(245,99,32)" rx="2" ry="2" />
+<text text-anchor="" x="245.42" y="287.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_read_group_raw:1889 (1 samples, 0.76%)</title><rect x="430.2" y="245" width="8.9" height="15.0" fill="rgb(234,203,31)" rx="2" ry="2" />
+<text text-anchor="" x="433.15" y="255.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:__bool__:4521 (1 samples, 0.76%)</title><rect x="859.2" y="101" width="9.0" height="15.0" fill="rgb(251,104,38)" rx="2" ry="2" />
+<text text-anchor="" x="862.24" y="111.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/addons/base/res/ir_property.py:get_multi:160 (6 samples, 4.55%)</title><rect x="188.8" y="277" width="53.6" height="15.0" fill="rgb(246,42,45)" rx="2" ry="2" />
+<text text-anchor="" x="191.79" y="287.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/data..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:__get__:948 (96 samples, 72.73%)</title><rect x="313.9" y="357" width="858.2" height="15.0" fill="rgb(213,57,32)" rx="2" ry="2" />
+<text text-anchor="" x="316.94" y="367.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/fields.py:__get__:948</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:convert_to_cache:1227 (1 samples, 0.76%)</title><rect x="1038.0" y="261" width="9.0" height="15.0" fill="rgb(223,62,12)" rx="2" ry="2" />
+<text text-anchor="" x="1041.03" y="271.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/addons/base/res/ir_property.py:get_multi:150 (5 samples, 3.79%)</title><rect x="144.1" y="277" width="44.7" height="15.0" fill="rgb(225,170,33)" rx="2" ry="2" />
+<text text-anchor="" x="147.09" y="287.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/dat..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_prefetch_field:2533 (27 samples, 20.45%)</title><rect x="617.9" y="149" width="241.3" height="15.0" fill="rgb(216,23,42)" rx="2" ry="2" />
+<text text-anchor="" x="620.88" y="159.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/models.py..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/_weakrefset.py:__iter__:60 (3 samples, 2.27%)</title><rect x="537.4" y="213" width="26.8" height="15.0" fill="rgb(235,207,6)" rx="2" ry="2" />
+<text text-anchor="" x="540.42" y="223.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_search:3664 (4 samples, 3.03%)</title><rect x="188.8" y="245" width="35.7" height="15.0" fill="rgb(205,45,53)" rx="2" ry="2" />
+<text text-anchor="" x="191.79" y="255.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/da..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/_weakrefset.py:__iter__:61 (3 samples, 2.27%)</title><rect x="1118.5" y="197" width="26.8" height="15.0" fill="rgb(247,10,46)" rx="2" ry="2" />
+<text text-anchor="" x="1121.48" y="207.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_where_calc:3469 (3 samples, 2.27%)</title><rect x="394.4" y="229" width="26.8" height="15.0" fill="rgb(236,20,44)" rx="2" ry="2" />
+<text text-anchor="" x="397.39" y="239.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/addons/base/ir/ir_http.py:_find_handler:84 (1 samples, 0.76%)</title><rect x="45.8" y="565" width="8.9" height="15.0" fill="rgb(213,124,8)" rx="2" ry="2" />
+<text text-anchor="" x="48.76" y="575.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__new__:736 (2 samples, 1.52%)</title><rect x="1020.2" y="213" width="17.8" height="15.0" fill="rgb(235,151,44)" rx="2" ry="2" />
+<text text-anchor="" x="1023.15" y="223.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__new__:103 (1 samples, 0.76%)</title><rect x="10.0" y="149" width="8.9" height="15.0" fill="rgb(215,72,7)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="159.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__call__:789 (6 samples, 4.55%)</title><rect x="457.0" y="181" width="53.6" height="15.0" fill="rgb(240,32,21)" rx="2" ry="2" />
+<text text-anchor="" x="459.97" y="191.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/data..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/addons/stock/models/product.py:&lt;lambda&gt;:200 (38 samples, 28.79%)</title><rect x="617.9" y="197" width="339.7" height="15.0" fill="rgb(207,192,20)" rx="2" ry="2" />
+<text text-anchor="" x="620.88" y="207.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/addons/stock/models/product..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/modules/registry.py:setup_models:276 (3 samples, 2.27%)</title><rect x="18.9" y="549" width="26.9" height="15.0" fill="rgb(254,108,35)" rx="2" ry="2" />
+<text text-anchor="" x="21.94" y="559.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__new__:735 (2 samples, 1.52%)</title><rect x="162.0" y="181" width="17.8" height="15.0" fill="rgb(233,102,44)" rx="2" ry="2" />
+<text text-anchor="" x="164.97" y="191.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:call_kw_multi:680 (1 samples, 0.76%)</title><rect x="1181.1" y="405" width="8.9" height="15.0" fill="rgb(232,100,36)" rx="2" ry="2" />
+<text text-anchor="" x="1184.06" y="415.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>&lt;frozen importlib._bootstrap&gt;:_handle_fromlist:1017 (1 samples, 0.76%)</title><rect x="10.0" y="405" width="8.9" height="15.0" fill="rgb(218,205,3)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="415.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/http.py:dispatch:683 (127 samples, 96.21%)</title><rect x="54.7" y="549" width="1135.3" height="15.0" fill="rgb(223,58,50)" rx="2" ry="2" />
+<text text-anchor="" x="57.70" y="559.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/http.py:dispatch:683</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:determine_value:1049 (38 samples, 28.79%)</title><rect x="617.9" y="165" width="339.7" height="15.0" fill="rgb(243,10,45)" rx="2" ry="2" />
+<text text-anchor="" x="620.88" y="175.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/fields.py:determine_va..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/site-packages/werkzeug/serving.py:handle_one_request:251 (132 samples, 100.00%)</title><rect x="10.0" y="757" width="1180.0" height="15.0" fill="rgb(213,136,27)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="767.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/usr/local/lib/python3.6/site-packages/werkzeug/serving.py:handle_one_request:251</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/sql_db.py:wrapper:155 (2 samples, 1.52%)</title><rect x="144.1" y="245" width="17.9" height="15.0" fill="rgb(242,159,41)" rx="2" ry="2" />
+<text text-anchor="" x="147.09" y="255.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/http.py:routing_map:978 (1 samples, 0.76%)</title><rect x="45.8" y="533" width="8.9" height="15.0" fill="rgb(248,134,14)" rx="2" ry="2" />
+<text text-anchor="" x="48.76" y="543.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>&lt;frozen importlib._bootstrap&gt;:_handle_fromlist:1017 (1 samples, 0.76%)</title><rect x="10.0" y="277" width="8.9" height="15.0" fill="rgb(247,49,3)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="287.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/addons/fetchmail/models/fetchmail.py:&lt;module&gt;:21 (1 samples, 0.76%)</title><rect x="10.0" y="165" width="8.9" height="15.0" fill="rgb(226,203,49)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="175.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:read:2509 (1 samples, 0.76%)</title><rect x="1181.1" y="229" width="8.9" height="15.0" fill="rgb(254,35,12)" rx="2" ry="2" />
+<text text-anchor="" x="1184.06" y="239.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/http.py:_call_function:339 (127 samples, 96.21%)</title><rect x="54.7" y="533" width="1135.3" height="15.0" fill="rgb(228,43,15)" rx="2" ry="2" />
+<text text-anchor="" x="57.70" y="543.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/http.py:_call_function:339</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:compute_value:1015 (1 samples, 0.76%)</title><rect x="1181.1" y="325" width="8.9" height="15.0" fill="rgb(249,30,19)" rx="2" ry="2" />
+<text text-anchor="" x="1184.06" y="335.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/_weakrefset.py:__iter__:59 (1 samples, 0.76%)</title><rect x="188.8" y="181" width="8.9" height="15.0" fill="rgb(219,15,45)" rx="2" ry="2" />
+<text text-anchor="" x="191.79" y="191.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/sre_compile.py:_code:551 (1 samples, 0.76%)</title><rect x="45.8" y="421" width="8.9" height="15.0" fill="rgb(238,179,12)" rx="2" ry="2" />
+<text text-anchor="" x="48.76" y="431.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/osv/expression.py:get_alias_from_query:380 (1 samples, 0.76%)</title><rect x="430.2" y="197" width="8.9" height="15.0" fill="rgb(233,132,37)" rx="2" ry="2" />
+<text text-anchor="" x="433.15" y="207.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:determine_value:1049 (1 samples, 0.76%)</title><rect x="1181.1" y="261" width="8.9" height="15.0" fill="rgb(240,170,2)" rx="2" ry="2" />
+<text text-anchor="" x="1184.06" y="271.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/addons/stock_account/models/product.py:do_change_standard_price:131 (96 samples, 72.73%)</title><rect x="313.9" y="373" width="858.2" height="15.0" fill="rgb(253,48,28)" rx="2" ry="2" />
+<text text-anchor="" x="316.94" y="383.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/addons/stock_account/models/product.py:do_change_standard_price:131</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:__get__:951 (1 samples, 0.76%)</title><rect x="305.0" y="357" width="8.9" height="15.0" fill="rgb(219,227,33)" rx="2" ry="2" />
+<text text-anchor="" x="308.00" y="367.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:read:2509 (5 samples, 3.79%)</title><rect x="859.2" y="133" width="44.7" height="15.0" fill="rgb(235,41,51)" rx="2" ry="2" />
+<text text-anchor="" x="862.24" y="143.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/dat..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/addons/web/controllers/main.py:call_button:928 (126 samples, 95.45%)</title><rect x="54.7" y="453" width="1126.4" height="15.0" fill="rgb(216,220,14)" rx="2" ry="2" />
+<text text-anchor="" x="57.70" y="463.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/addons/web/controllers/main.py:call_button:928</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_read_group_raw:1854 (1 samples, 0.76%)</title><rect x="331.8" y="245" width="9.0" height="15.0" fill="rgb(249,106,42)" rx="2" ry="2" />
+<text text-anchor="" x="334.82" y="255.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_where_calc:3462 (1 samples, 0.76%)</title><rect x="224.5" y="229" width="9.0" height="15.0" fill="rgb(206,135,8)" rx="2" ry="2" />
+<text text-anchor="" x="227.55" y="239.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:determine_value:1059 (1 samples, 0.76%)</title><rect x="1181.1" y="341" width="8.9" height="15.0" fill="rgb(210,78,8)" rx="2" ry="2" />
+<text text-anchor="" x="1184.06" y="351.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/service/server.py:process_request:767 (132 samples, 100.00%)</title><rect x="10.0" y="853" width="1180.0" height="15.0" fill="rgb(244,6,16)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="863.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/service/server.py:process_request:767</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/addons/stock/models/product.py:_compute_quantities_dict:123 (9 samples, 6.82%)</title><rect x="313.9" y="277" width="80.5" height="15.0" fill="rgb(250,183,23)" rx="2" ry="2" />
+<text text-anchor="" x="316.94" y="287.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/pr..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/tools/cache.py:lookup:81 (1 samples, 0.76%)</title><rect x="242.4" y="181" width="9.0" height="15.0" fill="rgb(220,103,38)" rx="2" ry="2" />
+<text text-anchor="" x="245.42" y="191.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/modules/module.py:load_module:82 (1 samples, 0.76%)</title><rect x="10.0" y="437" width="8.9" height="15.0" fill="rgb(219,72,39)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="447.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/modules/loading.py:load_modules:346 (3 samples, 2.27%)</title><rect x="18.9" y="565" width="26.9" height="15.0" fill="rgb(254,123,30)" rx="2" ry="2" />
+<text text-anchor="" x="21.94" y="575.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:__set__:966 (6 samples, 4.55%)</title><rect x="957.6" y="277" width="53.6" height="15.0" fill="rgb(205,100,42)" rx="2" ry="2" />
+<text text-anchor="" x="960.58" y="287.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/data..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/sre_compile.py:_compile:81 (1 samples, 0.76%)</title><rect x="45.8" y="405" width="8.9" height="15.0" fill="rgb(237,14,18)" rx="2" ry="2" />
+<text text-anchor="" x="48.76" y="415.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_browse:4218 (1 samples, 0.76%)</title><rect x="903.9" y="101" width="9.0" height="15.0" fill="rgb(231,212,17)" rx="2" ry="2" />
+<text text-anchor="" x="906.94" y="111.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>&lt;frozen importlib._bootstrap&gt;:_call_with_frames_removed:219 (1 samples, 0.76%)</title><rect x="10.0" y="261" width="8.9" height="15.0" fill="rgb(206,7,48)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="271.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>&lt;frozen importlib._bootstrap&gt;:_load_unlocked:656 (1 samples, 0.76%)</title><rect x="10.0" y="469" width="8.9" height="15.0" fill="rgb(221,132,11)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="479.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:&lt;listcomp&gt;:2618 (1 samples, 0.76%)</title><rect x="1181.1" y="197" width="8.9" height="15.0" fill="rgb(243,158,6)" rx="2" ry="2" />
+<text text-anchor="" x="1184.06" y="207.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>&lt;frozen importlib._bootstrap&gt;:_load_backward_compatible:626 (1 samples, 0.76%)</title><rect x="10.0" y="453" width="8.9" height="15.0" fill="rgb(214,30,9)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="463.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/osv/query.py:get_sql:144 (1 samples, 0.76%)</title><rect x="430.2" y="229" width="8.9" height="15.0" fill="rgb(247,73,28)" rx="2" ry="2" />
+<text text-anchor="" x="433.15" y="239.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__new__:735 (2 samples, 1.52%)</title><rect x="510.6" y="165" width="17.9" height="15.0" fill="rgb(236,221,21)" rx="2" ry="2" />
+<text text-anchor="" x="513.61" y="175.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_read_from_database:2589 (1 samples, 0.76%)</title><rect x="859.2" y="117" width="9.0" height="15.0" fill="rgb(243,173,11)" rx="2" ry="2" />
+<text text-anchor="" x="862.24" y="127.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_browse:4224 (5 samples, 3.79%)</title><rect x="814.5" y="69" width="44.7" height="15.0" fill="rgb(240,32,18)" rx="2" ry="2" />
+<text text-anchor="" x="817.55" y="79.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/dat..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/osv/expression.py:is_leaf:422 (1 samples, 0.76%)</title><rect x="403.3" y="181" width="9.0" height="15.0" fill="rgb(250,162,12)" rx="2" ry="2" />
+<text text-anchor="" x="406.33" y="191.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/service/server.py:process_work:776 (132 samples, 100.00%)</title><rect x="10.0" y="869" width="1180.0" height="15.0" fill="rgb(245,53,45)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="879.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/service/server.py:process_work:776</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:digits:1209 (3 samples, 2.27%)</title><rect x="1011.2" y="245" width="26.8" height="15.0" fill="rgb(252,152,31)" rx="2" ry="2" />
+<text text-anchor="" x="1014.21" y="255.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:__get__:956 (3 samples, 2.27%)</title><rect x="930.8" y="101" width="26.8" height="15.0" fill="rgb(212,219,12)" rx="2" ry="2" />
+<text text-anchor="" x="933.76" y="111.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/http.py:dispatch:1456 (4 samples, 3.03%)</title><rect x="10.0" y="629" width="35.8" height="15.0" fill="rgb(224,44,49)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="639.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/da..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__call__:789 (4 samples, 3.03%)</title><rect x="188.8" y="213" width="35.7" height="15.0" fill="rgb(233,68,37)" rx="2" ry="2" />
+<text text-anchor="" x="191.79" y="223.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/da..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/_weakrefset.py:__iter__:60 (1 samples, 0.76%)</title><rect x="1055.9" y="197" width="8.9" height="15.0" fill="rgb(247,151,21)" rx="2" ry="2" />
+<text text-anchor="" x="1058.91" y="207.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_where_calc:3469 (1 samples, 0.76%)</title><rect x="313.9" y="229" width="9.0" height="15.0" fill="rgb(244,168,14)" rx="2" ry="2" />
+<text text-anchor="" x="316.94" y="239.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/_weakrefset.py:__iter__:60 (2 samples, 1.52%)</title><rect x="197.7" y="181" width="17.9" height="15.0" fill="rgb(215,190,28)" rx="2" ry="2" />
+<text text-anchor="" x="200.73" y="191.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/addons/stock/models/product.py:_compute_quantities:85 (8 samples, 6.06%)</title><rect x="1038.0" y="293" width="71.5" height="15.0" fill="rgb(222,91,2)" rx="2" ry="2" />
+<text text-anchor="" x="1041.03" y="303.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/p..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_where_calc:3471 (1 samples, 0.76%)</title><rect x="233.5" y="229" width="8.9" height="15.0" fill="rgb(225,220,50)" rx="2" ry="2" />
+<text text-anchor="" x="236.48" y="239.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/tools/lru.py:__getitem__:45 (1 samples, 0.76%)</title><rect x="340.8" y="133" width="8.9" height="15.0" fill="rgb(235,178,15)" rx="2" ry="2" />
+<text text-anchor="" x="343.76" y="143.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/site-packages/werkzeug/serving.py:handle:216 (132 samples, 100.00%)</title><rect x="10.0" y="789" width="1180.0" height="15.0" fill="rgb(224,40,32)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="799.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/usr/local/lib/python3.6/site-packages/werkzeug/serving.py:handle:216</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/addons/stock/models/product.py:_compute_quantities:86 (7 samples, 5.30%)</title><rect x="1109.5" y="293" width="62.6" height="15.0" fill="rgb(248,193,54)" rx="2" ry="2" />
+<text text-anchor="" x="1112.55" y="303.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:__getitem__:4657 (5 samples, 3.79%)</title><rect x="912.9" y="117" width="44.7" height="15.0" fill="rgb(224,141,31)" rx="2" ry="2" />
+<text text-anchor="" x="915.88" y="127.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/dat..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/service/server.py:process_spawn:539 (132 samples, 100.00%)</title><rect x="10.0" y="917" width="1180.0" height="15.0" fill="rgb(218,127,20)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="927.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/service/server.py:process_spawn:539</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/http/server.py:handle:418 (132 samples, 100.00%)</title><rect x="10.0" y="773" width="1180.0" height="15.0" fill="rgb(229,69,54)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="783.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/usr/local/lib/python3.6/http/server.py:handle:418</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__new__:744 (1 samples, 0.76%)</title><rect x="296.1" y="197" width="8.9" height="15.0" fill="rgb(216,7,12)" rx="2" ry="2" />
+<text text-anchor="" x="299.06" y="207.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/_weakrefset.py:__iter__:60 (1 samples, 0.76%)</title><rect x="1109.5" y="197" width="9.0" height="15.0" fill="rgb(241,122,38)" rx="2" ry="2" />
+<text text-anchor="" x="1112.55" y="207.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:qualify:2612 (1 samples, 0.76%)</title><rect x="1181.1" y="181" width="8.9" height="15.0" fill="rgb(208,202,50)" rx="2" ry="2" />
+<text text-anchor="" x="1184.06" y="191.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/_weakrefset.py:__iter__:60 (1 samples, 0.76%)</title><rect x="1011.2" y="197" width="9.0" height="15.0" fill="rgb(228,71,50)" rx="2" ry="2" />
+<text text-anchor="" x="1014.21" y="207.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>&lt;frozen importlib._bootstrap&gt;:_find_and_load_unlocked:955 (1 samples, 0.76%)</title><rect x="10.0" y="229" width="8.9" height="15.0" fill="rgb(240,79,11)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="239.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/addons/product/models/product.py:name_get:372 (1 samples, 0.76%)</title><rect x="340.8" y="213" width="8.9" height="15.0" fill="rgb(245,86,42)" rx="2" ry="2" />
+<text text-anchor="" x="343.76" y="223.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/addons/stock/models/product.py:_compute_quantities:81 (72 samples, 54.55%)</title><rect x="313.9" y="293" width="643.7" height="15.0" fill="rgb(238,220,37)" rx="2" ry="2" />
+<text text-anchor="" x="316.94" y="303.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/addons/stock/models/product.py:_compute_quantities:81</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/addons/stock/models/product.py:_compute_quantities:84 (3 samples, 2.27%)</title><rect x="1011.2" y="293" width="26.8" height="15.0" fill="rgb(240,126,18)" rx="2" ry="2" />
+<text text-anchor="" x="1014.21" y="303.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/addons/base/res/ir_property.py:_get_domain:134 (2 samples, 1.52%)</title><rect x="144.1" y="261" width="17.9" height="15.0" fill="rgb(207,181,8)" rx="2" ry="2" />
+<text text-anchor="" x="147.09" y="271.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_read_from_database:2628 (1 samples, 0.76%)</title><rect x="886.1" y="117" width="8.9" height="15.0" fill="rgb(205,174,27)" rx="2" ry="2" />
+<text text-anchor="" x="889.06" y="127.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__call__:789 (8 samples, 6.06%)</title><rect x="63.6" y="341" width="71.6" height="15.0" fill="rgb(228,167,24)" rx="2" ry="2" />
+<text text-anchor="" x="66.64" y="351.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/p..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_in_cache_without:4684 (27 samples, 20.45%)</title><rect x="617.9" y="133" width="241.3" height="15.0" fill="rgb(253,7,52)" rx="2" ry="2" />
+<text text-anchor="" x="620.88" y="143.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/models.py..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/osv/expression.py:__leaf_to_sql:1204 (1 samples, 0.76%)</title><rect x="421.2" y="197" width="9.0" height="15.0" fill="rgb(219,140,30)" rx="2" ry="2" />
+<text text-anchor="" x="424.21" y="207.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/_weakrefset.py:__iter__:61 (1 samples, 0.76%)</title><rect x="483.8" y="149" width="8.9" height="15.0" fill="rgb(213,16,53)" rx="2" ry="2" />
+<text text-anchor="" x="486.79" y="159.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__new__:735 (1 samples, 0.76%)</title><rect x="1011.2" y="213" width="9.0" height="15.0" fill="rgb(221,27,43)" rx="2" ry="2" />
+<text text-anchor="" x="1014.21" y="223.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_read_group_resolve_many2one_fields:1944 (3 samples, 2.27%)</title><rect x="510.6" y="229" width="26.8" height="15.0" fill="rgb(215,38,2)" rx="2" ry="2" />
+<text text-anchor="" x="513.61" y="239.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:_compute_company_dependent:636 (11 samples, 8.33%)</title><rect x="144.1" y="293" width="98.3" height="15.0" fill="rgb(243,90,34)" rx="2" ry="2" />
+<text text-anchor="" x="147.09" y="303.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/prog..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:convert_to_cache:1230 (3 samples, 2.27%)</title><rect x="1011.2" y="261" width="26.8" height="15.0" fill="rgb(249,97,52)" rx="2" ry="2" />
+<text text-anchor="" x="1014.21" y="271.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__new__:735 (4 samples, 3.03%)</title><rect x="537.4" y="229" width="35.8" height="15.0" fill="rgb(248,196,1)" rx="2" ry="2" />
+<text text-anchor="" x="540.42" y="239.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/da..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>&lt;frozen importlib._bootstrap&gt;:_load_unlocked:665 (1 samples, 0.76%)</title><rect x="10.0" y="213" width="8.9" height="15.0" fill="rgb(250,58,20)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="223.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/addons/stock/models/product.py:_compute_quantities_dict:93 (38 samples, 28.79%)</title><rect x="617.9" y="277" width="339.7" height="15.0" fill="rgb(227,94,9)" rx="2" ry="2" />
+<text text-anchor="" x="620.88" y="287.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/addons/stock/models/product..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/addons/stock/models/product.py:_get_domain_locations:191 (38 samples, 28.79%)</title><rect x="617.9" y="261" width="339.7" height="15.0" fill="rgb(249,71,37)" rx="2" ry="2" />
+<text text-anchor="" x="620.88" y="271.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/addons/stock/models/product..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/enum.py:__and__:804 (1 samples, 0.76%)</title><rect x="45.8" y="389" width="8.9" height="15.0" fill="rgb(246,179,18)" rx="2" ry="2" />
+<text text-anchor="" x="48.76" y="399.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:call_kw_multi:680 (126 samples, 95.45%)</title><rect x="54.7" y="405" width="1126.4" height="15.0" fill="rgb(249,186,34)" rx="2" ry="2" />
+<text text-anchor="" x="57.70" y="415.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/api.py:call_kw_multi:680</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/addons/base/res/res_company.py:_company_default_get:181 (3 samples, 2.27%)</title><rect x="162.0" y="245" width="26.8" height="15.0" fill="rgb(221,53,46)" rx="2" ry="2" />
+<text text-anchor="" x="164.97" y="255.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/_weakrefset.py:__iter__:61 (1 samples, 0.76%)</title><rect x="564.2" y="213" width="9.0" height="15.0" fill="rgb(216,44,25)" rx="2" ry="2" />
+<text text-anchor="" x="567.24" y="223.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:with_context:4333 (9 samples, 6.82%)</title><rect x="537.4" y="261" width="80.5" height="15.0" fill="rgb(209,7,39)" rx="2" ry="2" />
+<text text-anchor="" x="540.42" y="271.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/pr..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_read_from_database:2618 (1 samples, 0.76%)</title><rect x="1181.1" y="213" width="8.9" height="15.0" fill="rgb(247,1,16)" rx="2" ry="2" />
+<text text-anchor="" x="1184.06" y="223.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_add_field:288 (1 samples, 0.76%)</title><rect x="27.9" y="517" width="8.9" height="15.0" fill="rgb(208,219,21)" rx="2" ry="2" />
+<text text-anchor="" x="30.88" y="527.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__getitem__:760 (1 samples, 0.76%)</title><rect x="939.7" y="69" width="8.9" height="15.0" fill="rgb(213,97,47)" rx="2" ry="2" />
+<text text-anchor="" x="942.70" y="79.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:determine_value:1059 (96 samples, 72.73%)</title><rect x="313.9" y="341" width="858.2" height="15.0" fill="rgb(248,157,53)" rx="2" ry="2" />
+<text text-anchor="" x="316.94" y="351.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/fields.py:determine_value:1059</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>&lt;string&gt;:change_digit:9 (6 samples, 4.55%)</title><rect x="251.4" y="213" width="53.6" height="15.0" fill="rgb(221,199,17)" rx="2" ry="2" />
+<text text-anchor="" x="254.36" y="223.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >&lt;stri..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:browse:4237 (1 samples, 0.76%)</title><rect x="448.0" y="213" width="9.0" height="15.0" fill="rgb(221,104,36)" rx="2" ry="2" />
+<text text-anchor="" x="451.03" y="223.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:read:2520 (5 samples, 3.79%)</title><rect x="912.9" y="133" width="44.7" height="15.0" fill="rgb(214,191,26)" rx="2" ry="2" />
+<text text-anchor="" x="915.88" y="143.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/dat..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/osv/expression.py:parse:813 (1 samples, 0.76%)</title><rect x="412.3" y="197" width="8.9" height="15.0" fill="rgb(233,73,10)" rx="2" ry="2" />
+<text text-anchor="" x="415.27" y="207.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_prefetch_field:2565 (1 samples, 0.76%)</title><rect x="1181.1" y="245" width="8.9" height="15.0" fill="rgb(222,102,37)" rx="2" ry="2" />
+<text text-anchor="" x="1184.06" y="255.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/_weakrefset.py:__iter__:61 (1 samples, 0.76%)</title><rect x="984.4" y="197" width="8.9" height="15.0" fill="rgb(216,83,10)" rx="2" ry="2" />
+<text text-anchor="" x="987.39" y="207.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/service/wsgi_server.py:application:166 (132 samples, 100.00%)</title><rect x="10.0" y="709" width="1180.0" height="15.0" fill="rgb(206,37,23)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="719.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/service/wsgi_server.py:application:166</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/service/server.py:run:635 (132 samples, 100.00%)</title><rect x="10.0" y="933" width="1180.0" height="15.0" fill="rgb(228,221,45)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="943.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/service/server.py:run:635</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_inherits_join_calc:1985 (1 samples, 0.76%)</title><rect x="877.1" y="69" width="9.0" height="15.0" fill="rgb(220,31,16)" rx="2" ry="2" />
+<text text-anchor="" x="880.12" y="79.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_browse:4219 (2 samples, 1.52%)</title><rect x="778.8" y="69" width="17.9" height="15.0" fill="rgb(252,116,41)" rx="2" ry="2" />
+<text text-anchor="" x="781.79" y="79.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__new__:736 (5 samples, 3.79%)</title><rect x="573.2" y="229" width="44.7" height="15.0" fill="rgb(249,183,19)" rx="2" ry="2" />
+<text text-anchor="" x="576.18" y="239.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/dat..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:convert_to_cache:1230 (7 samples, 5.30%)</title><rect x="1047.0" y="261" width="62.5" height="15.0" fill="rgb(226,70,1)" rx="2" ry="2" />
+<text text-anchor="" x="1049.97" y="271.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/tools/lru.py:__setitem__:56 (1 samples, 0.76%)</title><rect x="340.8" y="101" width="8.9" height="15.0" fill="rgb(211,125,32)" rx="2" ry="2" />
+<text text-anchor="" x="343.76" y="111.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>&lt;frozen importlib._bootstrap_external&gt;:exec_module:678 (1 samples, 0.76%)</title><rect x="10.0" y="325" width="8.9" height="15.0" fill="rgb(212,198,25)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="335.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/__init__.py:registry:76 (4 samples, 3.03%)</title><rect x="10.0" y="613" width="35.8" height="15.0" fill="rgb(238,185,28)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="623.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/da..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:convert_to_column:776 (1 samples, 0.76%)</title><rect x="421.2" y="165" width="9.0" height="15.0" fill="rgb(223,154,31)" rx="2" ry="2" />
+<text text-anchor="" x="424.21" y="175.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__new__:735 (4 samples, 3.03%)</title><rect x="957.6" y="213" width="35.7" height="15.0" fill="rgb(210,72,4)" rx="2" ry="2" />
+<text text-anchor="" x="960.58" y="223.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/da..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>&lt;frozen importlib._bootstrap&gt;:_load_unlocked:665 (1 samples, 0.76%)</title><rect x="10.0" y="341" width="8.9" height="15.0" fill="rgb(245,225,7)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="351.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>&lt;string&gt;:change_digit:10 (1 samples, 0.76%)</title><rect x="242.4" y="213" width="9.0" height="15.0" fill="rgb(238,225,19)" rx="2" ry="2" />
+<text text-anchor="" x="245.42" y="223.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:__set__:966 (3 samples, 2.27%)</title><rect x="1011.2" y="277" width="26.8" height="15.0" fill="rgb(207,108,1)" rx="2" ry="2" />
+<text text-anchor="" x="1014.21" y="287.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:_compute_company_dependent:638 (7 samples, 5.30%)</title><rect x="242.4" y="293" width="62.6" height="15.0" fill="rgb(227,180,38)" rx="2" ry="2" />
+<text text-anchor="" x="245.42" y="303.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/addons/stock/models/product.py:_get_domain_locations_new:200 (38 samples, 28.79%)</title><rect x="617.9" y="245" width="339.7" height="15.0" fill="rgb(210,73,18)" rx="2" ry="2" />
+<text text-anchor="" x="620.88" y="255.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/addons/stock/models/product..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/modules/loading.py:load_marked_modules:242 (1 samples, 0.76%)</title><rect x="10.0" y="549" width="8.9" height="15.0" fill="rgb(242,22,23)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="559.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/sql_db.py:execute:232 (1 samples, 0.76%)</title><rect x="144.1" y="229" width="8.9" height="15.0" fill="rgb(212,182,23)" rx="2" ry="2" />
+<text text-anchor="" x="147.09" y="239.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:call_kw:689 (1 samples, 0.76%)</title><rect x="1181.1" y="421" width="8.9" height="15.0" fill="rgb(243,140,33)" rx="2" ry="2" />
+<text text-anchor="" x="1184.06" y="431.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/modules/registry.py:new:85 (4 samples, 3.03%)</title><rect x="10.0" y="581" width="35.8" height="15.0" fill="rgb(211,220,43)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="591.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/da..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>&lt;decorator-gen-37&gt;:check:2 (1 samples, 0.76%)</title><rect x="340.8" y="181" width="8.9" height="15.0" fill="rgb(234,42,43)" rx="2" ry="2" />
+<text text-anchor="" x="343.76" y="191.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>&lt;frozen importlib._bootstrap&gt;:_find_and_load:971 (1 samples, 0.76%)</title><rect x="10.0" y="373" width="8.9" height="15.0" fill="rgb(212,98,39)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="383.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/addons/web/controllers/main.py:_call_kw:916 (126 samples, 95.45%)</title><rect x="54.7" y="437" width="1126.4" height="15.0" fill="rgb(228,98,43)" rx="2" ry="2" />
+<text text-anchor="" x="57.70" y="447.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/addons/web/controllers/main.py:_call_kw:916</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:digits:1209 (7 samples, 5.30%)</title><rect x="242.4" y="229" width="62.6" height="15.0" fill="rgb(213,150,14)" rx="2" ry="2" />
+<text text-anchor="" x="245.42" y="239.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/socketserver.py:__init__:696 (132 samples, 100.00%)</title><rect x="10.0" y="805" width="1180.0" height="15.0" fill="rgb(245,71,43)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="815.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/usr/local/lib/python3.6/socketserver.py:__init__:696</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/service/model.py:wrapper:97 (127 samples, 96.21%)</title><rect x="54.7" y="517" width="1135.3" height="15.0" fill="rgb(227,56,41)" rx="2" ry="2" />
+<text text-anchor="" x="57.70" y="527.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/service/model.py:wrapper:97</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/addons/base/ir/ir_http.py:_dispatch:208 (127 samples, 96.21%)</title><rect x="54.7" y="565" width="1135.3" height="15.0" fill="rgb(240,91,14)" rx="2" ry="2" />
+<text text-anchor="" x="57.70" y="575.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/addons/base/ir/ir_http.py:_dispatch:208</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/http.py:__call__:1272 (132 samples, 100.00%)</title><rect x="10.0" y="661" width="1180.0" height="15.0" fill="rgb(245,139,7)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="671.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/http.py:__call__:1272</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/_weakrefset.py:__iter__:60 (2 samples, 1.52%)</title><rect x="966.5" y="197" width="17.9" height="15.0" fill="rgb(248,100,48)" rx="2" ry="2" />
+<text text-anchor="" x="969.52" y="207.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>&lt;frozen importlib._bootstrap&gt;:_find_and_load_unlocked:955 (1 samples, 0.76%)</title><rect x="10.0" y="357" width="8.9" height="15.0" fill="rgb(245,135,22)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="367.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_read_group_raw:1918 (1 samples, 0.76%)</title><rect x="439.1" y="245" width="8.9" height="15.0" fill="rgb(207,15,53)" rx="2" ry="2" />
+<text text-anchor="" x="442.09" y="255.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:filtered:4439 (38 samples, 28.79%)</title><rect x="617.9" y="229" width="339.7" height="15.0" fill="rgb(220,55,34)" rx="2" ry="2" />
+<text text-anchor="" x="620.88" y="239.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/models.py:filtered:4439</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/addons/stock_account/models/product.py:do_change_standard_price:156 (1 samples, 0.76%)</title><rect x="1172.1" y="373" width="9.0" height="15.0" fill="rgb(216,167,42)" rx="2" ry="2" />
+<text text-anchor="" x="1175.12" y="383.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__new__:735 (4 samples, 3.03%)</title><rect x="1109.5" y="213" width="35.8" height="15.0" fill="rgb(237,162,42)" rx="2" ry="2" />
+<text text-anchor="" x="1112.55" y="223.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/da..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/service/server.py:worker_spawn:457 (132 samples, 100.00%)</title><rect x="10.0" y="901" width="1180.0" height="15.0" fill="rgb(246,66,8)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="911.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/service/server.py:worker_spawn:457</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/addons/stock/models/product.py:_compute_quantities_dict:125 (3 samples, 2.27%)</title><rect x="510.6" y="277" width="26.8" height="15.0" fill="rgb(214,173,53)" rx="2" ry="2" />
+<text text-anchor="" x="513.61" y="287.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_read_from_database:2597 (1 samples, 0.76%)</title><rect x="868.2" y="117" width="8.9" height="15.0" fill="rgb(250,151,5)" rx="2" ry="2" />
+<text text-anchor="" x="871.18" y="127.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/addons/stock/models/product.py:_compute_quantities_dict:134 (9 samples, 6.82%)</title><rect x="537.4" y="277" width="80.5" height="15.0" fill="rgb(251,201,38)" rx="2" ry="2" />
+<text text-anchor="" x="540.42" y="287.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/pr..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/osv/expression.py:__init__:668 (1 samples, 0.76%)</title><rect x="412.3" y="213" width="8.9" height="15.0" fill="rgb(235,59,3)" rx="2" ry="2" />
+<text text-anchor="" x="415.27" y="223.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_where_calc:3471 (1 samples, 0.76%)</title><rect x="421.2" y="229" width="9.0" height="15.0" fill="rgb(254,7,0)" rx="2" ry="2" />
+<text text-anchor="" x="424.21" y="239.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:__iter__:4531 (1 samples, 0.76%)</title><rect x="903.9" y="117" width="9.0" height="15.0" fill="rgb(229,31,49)" rx="2" ry="2" />
+<text text-anchor="" x="906.94" y="127.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:cache_key:766 (2 samples, 1.52%)</title><rect x="725.2" y="85" width="17.8" height="15.0" fill="rgb(217,103,8)" rx="2" ry="2" />
+<text text-anchor="" x="728.15" y="95.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:__set__:966 (8 samples, 6.06%)</title><rect x="1038.0" y="277" width="71.5" height="15.0" fill="rgb(232,100,42)" rx="2" ry="2" />
+<text text-anchor="" x="1041.03" y="287.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/p..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_read_group_raw:1851 (2 samples, 1.52%)</title><rect x="313.9" y="245" width="17.9" height="15.0" fill="rgb(248,131,19)" rx="2" ry="2" />
+<text text-anchor="" x="316.94" y="255.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_prefetch_field:2565 (11 samples, 8.33%)</title><rect x="859.2" y="149" width="98.4" height="15.0" fill="rgb(205,212,45)" rx="2" ry="2" />
+<text text-anchor="" x="862.24" y="159.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/prog..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:check_access_rights:2781 (1 samples, 0.76%)</title><rect x="340.8" y="197" width="8.9" height="15.0" fill="rgb(212,225,39)" rx="2" ry="2" />
+<text text-anchor="" x="343.76" y="207.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_generate_translated_field:3553 (1 samples, 0.76%)</title><rect x="877.1" y="53" width="9.0" height="15.0" fill="rgb(236,23,17)" rx="2" ry="2" />
+<text text-anchor="" x="880.12" y="63.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/_weakrefset.py:__iter__:61 (1 samples, 0.76%)</title><rect x="519.5" y="149" width="9.0" height="15.0" fill="rgb(235,84,51)" rx="2" ry="2" />
+<text text-anchor="" x="522.55" y="159.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/site-packages/werkzeug/wsgi.py:__call__:599 (132 samples, 100.00%)</title><rect x="10.0" y="645" width="1180.0" height="15.0" fill="rgb(233,77,24)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="655.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/usr/local/lib/python3.6/site-packages/werkzeug/wsgi.py:__call__:599</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:read_group:1830 (1 samples, 0.76%)</title><rect x="385.5" y="261" width="8.9" height="15.0" fill="rgb(243,195,2)" rx="2" ry="2" />
+<text text-anchor="" x="388.45" y="271.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/http.py:response_wrap:512 (127 samples, 96.21%)</title><rect x="54.7" y="469" width="1135.3" height="15.0" fill="rgb(220,87,24)" rx="2" ry="2" />
+<text text-anchor="" x="57.70" y="479.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/http.py:response_wrap:512</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_browse:4224 (1 samples, 0.76%)</title><rect x="948.6" y="69" width="9.0" height="15.0" fill="rgb(254,10,50)" rx="2" ry="2" />
+<text text-anchor="" x="951.64" y="79.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>&lt;decorator-gen-112&gt;:precision_get:2 (1 samples, 0.76%)</title><rect x="1047.0" y="213" width="8.9" height="15.0" fill="rgb(233,140,5)" rx="2" ry="2" />
+<text text-anchor="" x="1049.97" y="223.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__new__:735 (1 samples, 0.76%)</title><rect x="63.6" y="325" width="9.0" height="15.0" fill="rgb(225,19,13)" rx="2" ry="2" />
+<text text-anchor="" x="66.64" y="335.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:&lt;listcomp&gt;:4439 (38 samples, 28.79%)</title><rect x="617.9" y="213" width="339.7" height="15.0" fill="rgb(246,165,32)" rx="2" ry="2" />
+<text text-anchor="" x="620.88" y="223.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/models.py:&lt;listcomp&gt;:4..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/sql_db.py:execute:239 (1 samples, 0.76%)</title><rect x="439.1" y="213" width="8.9" height="15.0" fill="rgb(210,181,30)" rx="2" ry="2" />
+<text text-anchor="" x="442.09" y="223.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/addons/fetchmail/models/__init__.py:&lt;module&gt;:4 (1 samples, 0.76%)</title><rect x="10.0" y="293" width="8.9" height="15.0" fill="rgb(238,98,0)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="303.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_read_group_resolve_many2one_fields:1943 (1 samples, 0.76%)</title><rect x="448.0" y="229" width="9.0" height="15.0" fill="rgb(240,192,38)" rx="2" ry="2" />
+<text text-anchor="" x="451.03" y="239.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_read_group_raw:1924 (5 samples, 3.79%)</title><rect x="340.8" y="245" width="44.7" height="15.0" fill="rgb(253,29,13)" rx="2" ry="2" />
+<text text-anchor="" x="343.76" y="255.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/dat..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__call__:789 (4 samples, 3.03%)</title><rect x="349.7" y="181" width="35.8" height="15.0" fill="rgb(215,214,50)" rx="2" ry="2" />
+<text text-anchor="" x="352.70" y="191.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/da..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/_weakrefset.py:__enter__:24 (1 samples, 0.76%)</title><rect x="188.8" y="165" width="8.9" height="15.0" fill="rgb(223,181,36)" rx="2" ry="2" />
+<text text-anchor="" x="191.79" y="175.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/addons/web_editor/models/ir_http.py:_dispatch:21 (128 samples, 96.97%)</title><rect x="45.8" y="597" width="1144.2" height="15.0" fill="rgb(219,202,34)" rx="2" ry="2" />
+<text text-anchor="" x="48.76" y="607.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/addons/web_editor/models/ir_http.py:_dispatch:21</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/sql_db.py:execute:232 (1 samples, 0.76%)</title><rect x="886.1" y="85" width="8.9" height="15.0" fill="rgb(222,118,34)" rx="2" ry="2" />
+<text text-anchor="" x="889.06" y="95.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_in_cache_without:4684 (1 samples, 0.76%)</title><rect x="135.2" y="325" width="8.9" height="15.0" fill="rgb(246,20,14)" rx="2" ry="2" />
+<text text-anchor="" x="138.15" y="335.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:_setup_attrs:446 (1 samples, 0.76%)</title><rect x="27.9" y="485" width="8.9" height="15.0" fill="rgb(230,119,19)" rx="2" ry="2" />
+<text text-anchor="" x="30.88" y="495.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/addons/http_routing/models/ir_http.py:_dispatch:325 (1 samples, 0.76%)</title><rect x="45.8" y="581" width="8.9" height="15.0" fill="rgb(244,36,3)" rx="2" ry="2" />
+<text text-anchor="" x="48.76" y="591.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/enum.py:__call__:291 (1 samples, 0.76%)</title><rect x="45.8" y="373" width="8.9" height="15.0" fill="rgb(220,104,4)" rx="2" ry="2" />
+<text text-anchor="" x="48.76" y="383.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/osv/query.py:_get_alias_mapping:75 (1 samples, 0.76%)</title><rect x="430.2" y="213" width="8.9" height="15.0" fill="rgb(251,78,10)" rx="2" ry="2" />
+<text text-anchor="" x="433.15" y="223.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/addons/base/res/res_users.py:_get_company:217 (3 samples, 2.27%)</title><rect x="162.0" y="229" width="26.8" height="15.0" fill="rgb(216,162,1)" rx="2" ry="2" />
+<text text-anchor="" x="164.97" y="239.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/service/wsgi_server.py:application_unproxied:154 (132 samples, 100.00%)</title><rect x="10.0" y="693" width="1180.0" height="15.0" fill="rgb(218,88,9)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="703.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/service/wsgi_server.py:application_unproxied:154</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__new__:736 (5 samples, 3.79%)</title><rect x="251.4" y="197" width="44.7" height="15.0" fill="rgb(247,46,11)" rx="2" ry="2" />
+<text text-anchor="" x="254.36" y="207.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/dat..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/inspect.py:getmembers:333 (1 samples, 0.76%)</title><rect x="18.9" y="517" width="9.0" height="15.0" fill="rgb(228,188,31)" rx="2" ry="2" />
+<text text-anchor="" x="21.94" y="527.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/http.py:dispatch:1473 (128 samples, 96.97%)</title><rect x="45.8" y="629" width="1144.2" height="15.0" fill="rgb(237,140,21)" rx="2" ry="2" />
+<text text-anchor="" x="48.76" y="639.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/http.py:dispatch:1473</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_add_field:284 (1 samples, 0.76%)</title><rect x="36.8" y="501" width="9.0" height="15.0" fill="rgb(243,42,25)" rx="2" ry="2" />
+<text text-anchor="" x="39.82" y="511.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/osv/expression.py:distribute_not:302 (1 samples, 0.76%)</title><rect x="403.3" y="197" width="9.0" height="15.0" fill="rgb(233,211,21)" rx="2" ry="2" />
+<text text-anchor="" x="406.33" y="207.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_browse:4224 (1 samples, 0.76%)</title><rect x="939.7" y="53" width="8.9" height="15.0" fill="rgb(248,144,42)" rx="2" ry="2" />
+<text text-anchor="" x="942.70" y="63.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__new__:747 (1 samples, 0.76%)</title><rect x="1002.3" y="213" width="8.9" height="15.0" fill="rgb(207,176,30)" rx="2" ry="2" />
+<text text-anchor="" x="1005.27" y="223.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/osv/expression.py:to_sql:1271 (1 samples, 0.76%)</title><rect x="322.9" y="213" width="8.9" height="15.0" fill="rgb(244,38,34)" rx="2" ry="2" />
+<text text-anchor="" x="325.88" y="223.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>&lt;string&gt;:change_digit:10 (1 samples, 0.76%)</title><rect x="1047.0" y="229" width="8.9" height="15.0" fill="rgb(211,92,35)" rx="2" ry="2" />
+<text text-anchor="" x="1049.97" y="239.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/addons/http_routing/models/ir_http.py:_dispatch:397 (127 samples, 96.21%)</title><rect x="54.7" y="581" width="1135.3" height="15.0" fill="rgb(251,32,14)" rx="2" ry="2" />
+<text text-anchor="" x="57.70" y="591.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/addons/http_routing/models/ir_http.py:_dispatch:397</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/sql_db.py:wrapper:155 (1 samples, 0.76%)</title><rect x="439.1" y="229" width="8.9" height="15.0" fill="rgb(224,179,12)" rx="2" ry="2" />
+<text text-anchor="" x="442.09" y="239.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_setup_base:2311 (1 samples, 0.76%)</title><rect x="27.9" y="533" width="8.9" height="15.0" fill="rgb(220,162,22)" rx="2" ry="2" />
+<text text-anchor="" x="30.88" y="543.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:__get__:2523 (1 samples, 0.76%)</title><rect x="54.7" y="357" width="8.9" height="15.0" fill="rgb(243,50,9)" rx="2" ry="2" />
+<text text-anchor="" x="57.70" y="367.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/modules/registry.py:__new__:61 (4 samples, 3.03%)</title><rect x="10.0" y="597" width="35.8" height="15.0" fill="rgb(244,97,2)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="607.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/da..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>&lt;frozen importlib._bootstrap&gt;:_find_and_load_unlocked:955 (1 samples, 0.76%)</title><rect x="10.0" y="485" width="8.9" height="15.0" fill="rgb(234,52,39)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="495.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:compute_value:1015 (96 samples, 72.73%)</title><rect x="313.9" y="325" width="858.2" height="15.0" fill="rgb(214,100,2)" rx="2" ry="2" />
+<text text-anchor="" x="316.94" y="335.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/fields.py:compute_value:1015</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:cache_key:765 (2 samples, 1.52%)</title><rect x="707.3" y="85" width="17.9" height="15.0" fill="rgb(249,146,23)" rx="2" ry="2" />
+<text text-anchor="" x="710.27" y="95.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:convert_to_cache:1230 (7 samples, 5.30%)</title><rect x="242.4" y="245" width="62.6" height="15.0" fill="rgb(213,124,35)" rx="2" ry="2" />
+<text text-anchor="" x="245.42" y="255.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/sql_db.py:wrapper:155 (1 samples, 0.76%)</title><rect x="886.1" y="101" width="8.9" height="15.0" fill="rgb(247,92,39)" rx="2" ry="2" />
+<text text-anchor="" x="889.06" y="111.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_browse:4222 (2 samples, 1.52%)</title><rect x="796.7" y="69" width="17.8" height="15.0" fill="rgb(254,57,6)" rx="2" ry="2" />
+<text text-anchor="" x="799.67" y="79.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/_weakrefset.py:__iter__:61 (1 samples, 0.76%)</title><rect x="63.6" y="309" width="9.0" height="15.0" fill="rgb(228,207,33)" rx="2" ry="2" />
+<text text-anchor="" x="66.64" y="319.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_read_group_resolve_many2one_fields:1944 (5 samples, 3.79%)</title><rect x="340.8" y="229" width="44.7" height="15.0" fill="rgb(235,221,33)" rx="2" ry="2" />
+<text text-anchor="" x="343.76" y="239.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/dat..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:sudo:4310 (4 samples, 3.03%)</title><rect x="349.7" y="197" width="35.8" height="15.0" fill="rgb(233,1,28)" rx="2" ry="2" />
+<text text-anchor="" x="352.70" y="207.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/da..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>&lt;frozen importlib._bootstrap&gt;:_call_with_frames_removed:219 (1 samples, 0.76%)</title><rect x="10.0" y="309" width="8.9" height="15.0" fill="rgb(237,176,38)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="319.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/osv/expression.py:__leaf_to_sql:1197 (1 samples, 0.76%)</title><rect x="322.9" y="197" width="8.9" height="15.0" fill="rgb(239,84,12)" rx="2" ry="2" />
+<text text-anchor="" x="325.88" y="207.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/osv/expression.py:to_sql:1271 (1 samples, 0.76%)</title><rect x="421.2" y="213" width="9.0" height="15.0" fill="rgb(238,68,47)" rx="2" ry="2" />
+<text text-anchor="" x="424.21" y="223.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/cli/command.py:main:60 (132 samples, 100.00%)</title><rect x="10.0" y="997" width="1180.0" height="15.0" fill="rgb(244,180,22)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="1007.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/cli/command.py:main:60</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:read:2516 (1 samples, 0.76%)</title><rect x="903.9" y="133" width="9.0" height="15.0" fill="rgb(227,90,38)" rx="2" ry="2" />
+<text text-anchor="" x="906.94" y="143.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/re.py:compile:233 (1 samples, 0.76%)</title><rect x="45.8" y="469" width="8.9" height="15.0" fill="rgb(231,154,32)" rx="2" ry="2" />
+<text text-anchor="" x="48.76" y="479.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/addons/stock/models/product.py:_compute_quantities:83 (6 samples, 4.55%)</title><rect x="957.6" y="293" width="53.6" height="15.0" fill="rgb(243,132,16)" rx="2" ry="2" />
+<text text-anchor="" x="960.58" y="303.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/data..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/addons/stock_account/models/product.py:do_change_standard_price:125 (9 samples, 6.82%)</title><rect x="54.7" y="373" width="80.5" height="15.0" fill="rgb(227,17,6)" rx="2" ry="2" />
+<text text-anchor="" x="57.70" y="383.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/pr..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>&lt;frozen importlib._bootstrap&gt;:_find_and_load:971 (1 samples, 0.76%)</title><rect x="10.0" y="501" width="8.9" height="15.0" fill="rgb(250,47,11)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="511.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:create:3278 (1 samples, 0.76%)</title><rect x="1172.1" y="341" width="9.0" height="15.0" fill="rgb(240,145,32)" rx="2" ry="2" />
+<text text-anchor="" x="1175.12" y="351.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/sql_db.py:dbname:477 (1 samples, 0.76%)</title><rect x="993.3" y="197" width="9.0" height="15.0" fill="rgb(230,214,23)" rx="2" ry="2" />
+<text text-anchor="" x="996.33" y="207.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/addons/product/models/product.py:name_get:376 (6 samples, 4.55%)</title><rect x="457.0" y="213" width="53.6" height="15.0" fill="rgb(213,191,31)" rx="2" ry="2" />
+<text text-anchor="" x="459.97" y="223.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/data..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__call__:789 (3 samples, 2.27%)</title><rect x="510.6" y="181" width="26.8" height="15.0" fill="rgb(253,165,0)" rx="2" ry="2" />
+<text text-anchor="" x="513.61" y="191.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/http.py:__call__:927 (127 samples, 96.21%)</title><rect x="54.7" y="485" width="1135.3" height="15.0" fill="rgb(240,160,35)" rx="2" ry="2" />
+<text text-anchor="" x="57.70" y="495.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/http.py:__call__:927</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:__get__:948 (1 samples, 0.76%)</title><rect x="1181.1" y="277" width="8.9" height="15.0" fill="rgb(207,72,41)" rx="2" ry="2" />
+<text text-anchor="" x="1184.06" y="287.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_add_magic_fields:330 (1 samples, 0.76%)</title><rect x="36.8" y="517" width="9.0" height="15.0" fill="rgb(219,128,16)" rx="2" ry="2" />
+<text text-anchor="" x="39.82" y="527.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/modules/registry.py:&lt;dictcomp&gt;:201 (1 samples, 0.76%)</title><rect x="1172.1" y="245" width="9.0" height="15.0" fill="rgb(254,15,40)" rx="2" ry="2" />
+<text text-anchor="" x="1175.12" y="255.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:_compute_value:1006 (1 samples, 0.76%)</title><rect x="1181.1" y="309" width="8.9" height="15.0" fill="rgb(232,139,54)" rx="2" ry="2" />
+<text text-anchor="" x="1184.06" y="319.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:read_group:1828 (8 samples, 6.06%)</title><rect x="313.9" y="261" width="71.6" height="15.0" fill="rgb(251,205,7)" rx="2" ry="2" />
+<text text-anchor="" x="316.94" y="271.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/p..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/site-packages/werkzeug/routing.py:bind:681 (1 samples, 0.76%)</title><rect x="45.8" y="501" width="8.9" height="15.0" fill="rgb(230,71,28)" rx="2" ry="2" />
+<text text-anchor="" x="48.76" y="511.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/tools/misc.py:__init__:990 (1 samples, 0.76%)</title><rect x="331.8" y="229" width="9.0" height="15.0" fill="rgb(237,63,17)" rx="2" ry="2" />
+<text text-anchor="" x="334.82" y="239.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:determine_value:1057 (1 samples, 0.76%)</title><rect x="135.2" y="341" width="8.9" height="15.0" fill="rgb(221,138,8)" rx="2" ry="2" />
+<text text-anchor="" x="138.15" y="351.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/addons/base/ir/ir_http.py:routing_map:238 (1 samples, 0.76%)</title><rect x="45.8" y="549" width="8.9" height="15.0" fill="rgb(228,12,11)" rx="2" ry="2" />
+<text text-anchor="" x="48.76" y="559.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/site-packages/werkzeug/routing.py:compile:742 (1 samples, 0.76%)</title><rect x="45.8" y="485" width="8.9" height="15.0" fill="rgb(235,72,27)" rx="2" ry="2" />
+<text text-anchor="" x="48.76" y="495.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/http.py:checked_call:332 (127 samples, 96.21%)</title><rect x="54.7" y="501" width="1135.3" height="15.0" fill="rgb(244,205,24)" rx="2" ry="2" />
+<text text-anchor="" x="57.70" y="511.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/http.py:checked_call:332</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/osv/expression.py:to_sql:1280 (1 samples, 0.76%)</title><rect x="233.5" y="213" width="8.9" height="15.0" fill="rgb(250,60,17)" rx="2" ry="2" />
+<text text-anchor="" x="236.48" y="223.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/addons/web/controllers/main.py:_call_kw:916 (1 samples, 0.76%)</title><rect x="1181.1" y="437" width="8.9" height="15.0" fill="rgb(208,166,8)" rx="2" ry="2" />
+<text text-anchor="" x="1184.06" y="447.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/addons/product/models/product.py:name_get:376 (4 samples, 3.03%)</title><rect x="349.7" y="213" width="35.8" height="15.0" fill="rgb(219,217,54)" rx="2" ry="2" />
+<text text-anchor="" x="352.70" y="223.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/da..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_inherits_join_calc:1973 (1 samples, 0.76%)</title><rect x="1181.1" y="165" width="8.9" height="15.0" fill="rgb(250,176,30)" rx="2" ry="2" />
+<text text-anchor="" x="1184.06" y="175.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_setup_base:2308 (1 samples, 0.76%)</title><rect x="18.9" y="533" width="9.0" height="15.0" fill="rgb(215,207,28)" rx="2" ry="2" />
+<text text-anchor="" x="21.94" y="543.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:__get__:948 (19 samples, 14.39%)</title><rect x="135.2" y="357" width="169.8" height="15.0" fill="rgb(250,38,25)" rx="2" ry="2" />
+<text text-anchor="" x="138.15" y="367.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odo..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/modules/module.py:load_openerp_module:368 (1 samples, 0.76%)</title><rect x="10.0" y="517" width="8.9" height="15.0" fill="rgb(232,156,22)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="527.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:call_kw:689 (126 samples, 95.45%)</title><rect x="54.7" y="421" width="1126.4" height="15.0" fill="rgb(247,169,22)" rx="2" ry="2" />
+<text text-anchor="" x="57.70" y="431.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/api.py:call_kw:689</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:digits:1209 (6 samples, 4.55%)</title><rect x="957.6" y="245" width="53.6" height="15.0" fill="rgb(222,3,33)" rx="2" ry="2" />
+<text text-anchor="" x="960.58" y="255.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/data..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_where_calc:3471 (1 samples, 0.76%)</title><rect x="322.9" y="229" width="8.9" height="15.0" fill="rgb(254,193,48)" rx="2" ry="2" />
+<text text-anchor="" x="325.88" y="239.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/http.py:__call__:1298 (132 samples, 100.00%)</title><rect x="10.0" y="677" width="1180.0" height="15.0" fill="rgb(232,29,27)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="687.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/http.py:__call__:1298</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/addons/account/models/account_move.py:create:126 (1 samples, 0.76%)</title><rect x="1172.1" y="357" width="9.0" height="15.0" fill="rgb(243,7,37)" rx="2" ry="2" />
+<text text-anchor="" x="1175.12" y="367.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__new__:735 (4 samples, 3.03%)</title><rect x="457.0" y="165" width="35.7" height="15.0" fill="rgb(224,172,45)" rx="2" ry="2" />
+<text text-anchor="" x="459.97" y="175.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/da..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/osv/expression.py:__init__:665 (1 samples, 0.76%)</title><rect x="403.3" y="213" width="9.0" height="15.0" fill="rgb(238,229,35)" rx="2" ry="2" />
+<text text-anchor="" x="406.33" y="223.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/tools/cache.py:lookup:84 (1 samples, 0.76%)</title><rect x="340.8" y="165" width="8.9" height="15.0" fill="rgb(232,53,14)" rx="2" ry="2" />
+<text text-anchor="" x="343.76" y="175.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__new__:736 (2 samples, 1.52%)</title><rect x="492.7" y="165" width="17.9" height="15.0" fill="rgb(207,83,49)" rx="2" ry="2" />
+<text text-anchor="" x="495.73" y="175.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/socketserver.py:finish_request:361 (132 samples, 100.00%)</title><rect x="10.0" y="821" width="1180.0" height="15.0" fill="rgb(246,11,27)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="831.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/usr/local/lib/python3.6/socketserver.py:finish_request:361</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/osv/query.py:add_join:136 (1 samples, 0.76%)</title><rect x="877.1" y="37" width="9.0" height="15.0" fill="rgb(239,108,47)" rx="2" ry="2" />
+<text text-anchor="" x="880.12" y="47.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:set_all_attrs:360 (1 samples, 0.76%)</title><rect x="27.9" y="469" width="8.9" height="15.0" fill="rgb(207,219,5)" rx="2" ry="2" />
+<text text-anchor="" x="30.88" y="479.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/osv/expression.py:__init__:660 (1 samples, 0.76%)</title><rect x="394.4" y="213" width="8.9" height="15.0" fill="rgb(245,214,34)" rx="2" ry="2" />
+<text text-anchor="" x="397.39" y="223.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/re.py:_compile:301 (1 samples, 0.76%)</title><rect x="45.8" y="453" width="8.9" height="15.0" fill="rgb(254,19,1)" rx="2" ry="2" />
+<text text-anchor="" x="48.76" y="463.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:_compute_value:1008 (18 samples, 13.64%)</title><rect x="144.1" y="309" width="160.9" height="15.0" fill="rgb(219,173,41)" rx="2" ry="2" />
+<text text-anchor="" x="147.09" y="319.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/od..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:digits:1209 (7 samples, 5.30%)</title><rect x="1109.5" y="245" width="62.6" height="15.0" fill="rgb(235,174,48)" rx="2" ry="2" />
+<text text-anchor="" x="1112.55" y="255.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:get:959 (1 samples, 0.76%)</title><rect x="921.8" y="85" width="9.0" height="15.0" fill="rgb(250,173,3)" rx="2" ry="2" />
+<text text-anchor="" x="924.82" y="95.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/cli/server.py:run:175 (132 samples, 100.00%)</title><rect x="10.0" y="981" width="1180.0" height="15.0" fill="rgb(207,129,54)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="991.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/cli/server.py:run:175</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:determine_value:1059 (18 samples, 13.64%)</title><rect x="144.1" y="341" width="160.9" height="15.0" fill="rgb(228,14,25)" rx="2" ry="2" />
+<text text-anchor="" x="147.09" y="351.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/od..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_read_from_database:2646 (1 samples, 0.76%)</title><rect x="895.0" y="117" width="8.9" height="15.0" fill="rgb(229,65,17)" rx="2" ry="2" />
+<text text-anchor="" x="898.00" y="127.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:search:1407 (6 samples, 4.55%)</title><rect x="188.8" y="261" width="53.6" height="15.0" fill="rgb(248,98,19)" rx="2" ry="2" />
+<text text-anchor="" x="191.79" y="271.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/data..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/addons/stock_account/wizard/stock_change_standard_price.py:change_price:43 (126 samples, 95.45%)</title><rect x="54.7" y="389" width="1126.4" height="15.0" fill="rgb(228,32,34)" rx="2" ry="2" />
+<text text-anchor="" x="57.70" y="399.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/addons/stock_account/wizard/stock_change_standard_price.py:change_price:43</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:compute_concurrency_field_with_access:356 (1 samples, 0.76%)</title><rect x="1181.1" y="293" width="8.9" height="15.0" fill="rgb(206,53,50)" rx="2" ry="2" />
+<text text-anchor="" x="1184.06" y="303.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/_weakrefset.py:__iter__:60 (3 samples, 2.27%)</title><rect x="457.0" y="149" width="26.8" height="15.0" fill="rgb(238,65,26)" rx="2" ry="2" />
+<text text-anchor="" x="459.97" y="159.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_search:3674 (2 samples, 1.52%)</title><rect x="224.5" y="245" width="17.9" height="15.0" fill="rgb(206,229,2)" rx="2" ry="2" />
+<text text-anchor="" x="227.55" y="255.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:read_group:1828 (3 samples, 2.27%)</title><rect x="510.6" y="261" width="26.8" height="15.0" fill="rgb(251,17,11)" rx="2" ry="2" />
+<text text-anchor="" x="513.61" y="271.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/_weakrefset.py:add:84 (1 samples, 0.76%)</title><rect x="1002.3" y="181" width="8.9" height="15.0" fill="rgb(249,132,18)" rx="2" ry="2" />
+<text text-anchor="" x="1005.27" y="191.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/osv/expression.py:get_unaccent_wrapper:455 (1 samples, 0.76%)</title><rect x="313.9" y="197" width="9.0" height="15.0" fill="rgb(246,20,51)" rx="2" ry="2" />
+<text text-anchor="" x="316.94" y="207.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__new__:735 (3 samples, 2.27%)</title><rect x="349.7" y="165" width="26.8" height="15.0" fill="rgb(233,34,9)" rx="2" ry="2" />
+<text text-anchor="" x="352.70" y="175.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:__set__:966 (7 samples, 5.30%)</title><rect x="1109.5" y="277" width="62.6" height="15.0" fill="rgb(249,102,21)" rx="2" ry="2" />
+<text text-anchor="" x="1112.55" y="287.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__new__:735 (1 samples, 0.76%)</title><rect x="1055.9" y="213" width="8.9" height="15.0" fill="rgb(231,218,17)" rx="2" ry="2" />
+<text text-anchor="" x="1058.91" y="223.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__call__:789 (3 samples, 2.27%)</title><rect x="162.0" y="197" width="26.8" height="15.0" fill="rgb(230,152,24)" rx="2" ry="2" />
+<text text-anchor="" x="164.97" y="207.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/tools/func.py:wrapper:68 (1 samples, 0.76%)</title><rect x="340.8" y="149" width="8.9" height="15.0" fill="rgb(246,133,16)" rx="2" ry="2" />
+<text text-anchor="" x="343.76" y="159.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/service/server.py:start:991 (132 samples, 100.00%)</title><rect x="10.0" y="949" width="1180.0" height="15.0" fill="rgb(235,78,16)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="959.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/service/server.py:start:991</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/modules/registry.py:field_sequence:200 (1 samples, 0.76%)</title><rect x="1172.1" y="261" width="9.0" height="15.0" fill="rgb(234,89,47)" rx="2" ry="2" />
+<text text-anchor="" x="1175.12" y="271.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/tools/func.py:wrapper:68 (1 samples, 0.76%)</title><rect x="340.8" y="117" width="8.9" height="15.0" fill="rgb(217,125,27)" rx="2" ry="2" />
+<text text-anchor="" x="343.76" y="127.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>&lt;string&gt;:change_digit:9 (3 samples, 2.27%)</title><rect x="1011.2" y="229" width="26.8" height="15.0" fill="rgb(212,176,50)" rx="2" ry="2" />
+<text text-anchor="" x="1014.21" y="239.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >&lt;..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:sudo:4310 (6 samples, 4.55%)</title><rect x="457.0" y="197" width="53.6" height="15.0" fill="rgb(219,94,44)" rx="2" ry="2" />
+<text text-anchor="" x="459.97" y="207.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/data..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__new__:736 (3 samples, 2.27%)</title><rect x="1145.3" y="213" width="26.8" height="15.0" fill="rgb(218,152,13)" rx="2" ry="2" />
+<text text-anchor="" x="1148.30" y="223.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_read_group_raw:1851 (4 samples, 3.03%)</title><rect x="394.4" y="245" width="35.8" height="15.0" fill="rgb(225,104,39)" rx="2" ry="2" />
+<text text-anchor="" x="397.39" y="255.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/da..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:read:2520 (1 samples, 0.76%)</title><rect x="1181.1" y="389" width="8.9" height="15.0" fill="rgb(210,0,34)" rx="2" ry="2" />
+<text text-anchor="" x="1184.06" y="399.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/addons/product/models/product.py:name_get:376 (3 samples, 2.27%)</title><rect x="510.6" y="213" width="26.8" height="15.0" fill="rgb(220,43,34)" rx="2" ry="2" />
+<text text-anchor="" x="513.61" y="223.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>&lt;string&gt;:&lt;module&gt;:4 (1 samples, 0.76%)</title><rect x="10.0" y="421" width="8.9" height="15.0" fill="rgb(247,108,31)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="431.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/socketserver.py:process_request:348 (132 samples, 100.00%)</title><rect x="10.0" y="837" width="1180.0" height="15.0" fill="rgb(205,99,49)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="847.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/usr/local/lib/python3.6/socketserver.py:process_request:348</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:get_records:1009 (27 samples, 20.45%)</title><rect x="617.9" y="117" width="241.3" height="15.0" fill="rgb(223,40,14)" rx="2" ry="2" />
+<text text-anchor="" x="620.88" y="127.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/api.py:ge..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>&lt;frozen importlib._bootstrap&gt;:_find_and_load:971 (1 samples, 0.76%)</title><rect x="10.0" y="245" width="8.9" height="15.0" fill="rgb(238,115,50)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="255.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/tools/func.py:__get__:23 (1 samples, 0.76%)</title><rect x="1172.1" y="277" width="9.0" height="15.0" fill="rgb(249,35,36)" rx="2" ry="2" />
+<text text-anchor="" x="1175.12" y="287.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:__getitem__:4657 (1 samples, 0.76%)</title><rect x="1181.1" y="373" width="8.9" height="15.0" fill="rgb(243,188,4)" rx="2" ry="2" />
+<text text-anchor="" x="1184.06" y="383.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/_weakrefset.py:__iter__:61 (2 samples, 1.52%)</title><rect x="162.0" y="165" width="17.8" height="15.0" fill="rgb(239,166,38)" rx="2" ry="2" />
+<text text-anchor="" x="164.97" y="175.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:__new__:736 (7 samples, 5.30%)</title><rect x="72.6" y="325" width="62.6" height="15.0" fill="rgb(235,15,23)" rx="2" ry="2" />
+<text text-anchor="" x="75.58" y="335.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/osv/expression.py:__init__:660 (1 samples, 0.76%)</title><rect x="313.9" y="213" width="9.0" height="15.0" fill="rgb(219,92,5)" rx="2" ry="2" />
+<text text-anchor="" x="316.94" y="223.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/_weakrefset.py:__iter__:61 (1 samples, 0.76%)</title><rect x="367.6" y="149" width="8.9" height="15.0" fill="rgb(254,11,12)" rx="2" ry="2" />
+<text text-anchor="" x="370.58" y="159.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:user:798 (3 samples, 2.27%)</title><rect x="162.0" y="213" width="26.8" height="15.0" fill="rgb(251,143,13)" rx="2" ry="2" />
+<text text-anchor="" x="164.97" y="223.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_create:3435 (1 samples, 0.76%)</title><rect x="1172.1" y="325" width="9.0" height="15.0" fill="rgb(214,12,14)" rx="2" ry="2" />
+<text text-anchor="" x="1175.12" y="335.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:_compute_value:1006 (96 samples, 72.73%)</title><rect x="313.9" y="309" width="858.2" height="15.0" fill="rgb(223,46,17)" rx="2" ry="2" />
+<text text-anchor="" x="316.94" y="319.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/fields.py:_compute_value:1006</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_normalize_ids:5197 (1 samples, 0.76%)</title><rect x="743.0" y="69" width="9.0" height="15.0" fill="rgb(225,198,21)" rx="2" ry="2" />
+<text text-anchor="" x="746.03" y="79.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:setup_base:389 (1 samples, 0.76%)</title><rect x="27.9" y="501" width="8.9" height="15.0" fill="rgb(252,137,45)" rx="2" ry="2" />
+<text text-anchor="" x="30.88" y="511.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:sudo:4310 (4 samples, 3.03%)</title><rect x="188.8" y="229" width="35.7" height="15.0" fill="rgb(241,171,41)" rx="2" ry="2" />
+<text text-anchor="" x="191.79" y="239.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/da..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:with_context:4333 (8 samples, 6.06%)</title><rect x="63.6" y="357" width="71.6" height="15.0" fill="rgb(253,113,17)" rx="2" ry="2" />
+<text text-anchor="" x="66.64" y="367.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/p..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/modules/loading.py:load_module_graph:121 (1 samples, 0.76%)</title><rect x="10.0" y="533" width="8.9" height="15.0" fill="rgb(224,88,32)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="543.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_browse:4218 (1 samples, 0.76%)</title><rect x="769.8" y="69" width="9.0" height="15.0" fill="rgb(249,188,0)" rx="2" ry="2" />
+<text text-anchor="" x="772.85" y="79.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>&lt;string&gt;:change_digit:9 (7 samples, 5.30%)</title><rect x="1109.5" y="229" width="62.6" height="15.0" fill="rgb(224,119,46)" rx="2" ry="2" />
+<text text-anchor="" x="1112.55" y="239.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >&lt;strin..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/addons/base/res/ir_property.py:_get_domain:138 (3 samples, 2.27%)</title><rect x="162.0" y="261" width="26.8" height="15.0" fill="rgb(248,72,12)" rx="2" ry="2" />
+<text text-anchor="" x="164.97" y="271.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:compute_value:1015 (18 samples, 13.64%)</title><rect x="144.1" y="325" width="160.9" height="15.0" fill="rgb(248,79,16)" rx="2" ry="2" />
+<text text-anchor="" x="147.09" y="335.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/od..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>&lt;string&gt;:change_digit:9 (6 samples, 4.55%)</title><rect x="1055.9" y="229" width="53.6" height="15.0" fill="rgb(246,40,42)" rx="2" ry="2" />
+<text text-anchor="" x="1058.91" y="239.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >&lt;stri..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/usr/local/lib/python3.6/site-packages/werkzeug/serving.py:execute:181 (132 samples, 100.00%)</title><rect x="10.0" y="725" width="1180.0" height="15.0" fill="rgb(238,220,17)" rx="2" ry="2" />
+<text text-anchor="" x="13.00" y="735.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/usr/local/lib/python3.6/site-packages/werkzeug/serving.py:execute:181</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/api.py:&lt;listcomp&gt;:1010 (27 samples, 20.45%)</title><rect x="617.9" y="101" width="241.3" height="15.0" fill="rgb(243,183,28)" rx="2" ry="2" />
+<text text-anchor="" x="620.88" y="111.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas/progs/odoo/odoo/api.py:&lt;l..</text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/addons/web/controllers/main.py:call_kw:924 (1 samples, 0.76%)</title><rect x="1181.1" y="453" width="8.9" height="15.0" fill="rgb(210,71,32)" rx="2" ry="2" />
+<text text-anchor="" x="1184.06" y="463.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/models.py:_setup_base:2312 (1 samples, 0.76%)</title><rect x="36.8" y="533" width="9.0" height="15.0" fill="rgb(221,104,41)" rx="2" ry="2" />
+<text text-anchor="" x="39.82" y="543.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" ></text>
+</g>
+<g class="func_g" onmouseover="s(this)" onmouseout="c()" onclick="zoom(this)">
+<title>/datas/progs/odoo/odoo/fields.py:digits:1209 (7 samples, 5.30%)</title><rect x="1047.0" y="245" width="62.5" height="15.0" fill="rgb(205,0,31)" rx="2" ry="2" />
+<text text-anchor="" x="1049.97" y="255.5" font-size="12" font-family="Verdana" fill="rgb(0,0,0)" >/datas..</text>
+</g>
+</svg>
diff --git a/doc/howtos/profilecode.rst b/doc/howtos/profilecode.rst
new file mode 100644
index 00000000000..34591fcf274
--- /dev/null
+++ b/doc/howtos/profilecode.rst
@@ -0,0 +1,88 @@
+===================
+Profiling Odoo code
+===================
+
+.. warning::
+
+ This tutorial requires :ref:`having installed Odoo <setup/install>`
+ and :ref:`writing Odoo code <backend>`
+
+Graph a method Up to 10.0
+=========================
+
+Odoo embeds a profiler of code. This embeded profiler output can be used to
+generate a graph of calls triggered by the method, percentage of time taken in
+the method itself as well as time taken in method and it's sub-called methods.
+
+.. code:: python
+
+ from openerp.tools.misc import profile
+ [...]
+ @profile('/temp/prof.profile')
+ @api.multi
+ def mymethod(...)
+
+This produce a file called /temp/prof.profile
+
+A tool called *gprof2dot* will produce a graph with this result:
+
+.. code:: bash
+
+ gprof2dot -f pstats -o /temp/prof.xdot /temp/prof.profile
+
+A tool called *xdot* will display the resulting graph:
+
+.. code:: bash
+
+ xdot /temp/prof.xdot
+
+Dump stack
+==========
+
+Sending the SIGQUIT signal to an odoo process (only available on POSIX) makes
+this process output the current stack trace to log, with info level. When an
+odoo process seems stucked, sending this signal to the process permit to know
+what the process is doing, and letting the process continue his job.
+
+Tracing code execution
+======================
+
+Instead of sending the SIGQUIT signal to an odoo process often enough, to check
+where processes is performing worse than expected, we can use pyflame tool to
+do it for us.
+
+Install pyflame and flamegraph
+------------------------------
+
+.. code:: bash
+
+ sudo apt install autoconf automake autotools-dev g++ pkg-config python-dev python3-dev libtool make
+ git clone https://github.com/uber/pyflame.git
+ git clone https://github.com/brendangregg/FlameGraph.git
+ cd pyflame
+ ./autogen.sh
+ ./configure
+ make
+ sudo make install
+
+Record executed code
+--------------------
+
+As pyflame is installed, we now record the executed code lines with pyflame.
+This tool will record, multiple times a second, the stacktrace of the process.
+Once done, we'll display them as an execution graph.
+
+.. code:: bash
+
+ pyflame --exclude-idle -s 3600 -r 0.2 -p <PID> -o test.flame
+
+where <PID> is the process ID of the odoo process you want to graph. This will
+wait until the dead of the process, with a maximum of one hour, and and get 5
+traces a second. With the output of pyflame, we can produce an svg graph with
+the flamegraph tool:
+
+.. code:: bash
+
+ flamegraph.pl ./test.flame > ~/mycode.svg
+
+.. image:: profile/flamegraph.svg
diff --git a/doc/howtos/themes.rst b/doc/howtos/themes.rst
index 30583cfd758..5252ed5b776 100644
--- a/doc/howtos/themes.rst
+++ b/doc/howtos/themes.rst
@@ -377,27 +377,37 @@ Imagine that we want to create a specific layout for a Services page.
For this page, we need to add a list of services to the top and give the client the possibility of setting the rest of the page’s layout using snippets.
Inside your *views* folder, create a **pages.xml** file and add the
-default Odoo markup. Inside ``<odoo>`` create a ``<template>`` tag, set the
-``page`` attribute to ``True`` and add your code into it.
+default Odoo markup. Inside ``<odoo>``, instead of defining a ``<template>``,
+we will create a *page* object.
.. code-block:: xml
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
- <!-- === Services Page === -->
- <template name="Services page" id="website.services" page="True">
- <h1>Our Services</h1>
- <ul class="services">
- <li>Cloud Hosting</li>
- <li>Support</li>
- <li>Unlimited space</li>
- </ul>
- </template>
-
- </odoo>
-
-The page title will be the template ID. In our case *Services* (from ``website.services``)
+ <!-- === Services Page === -->
+ <record id="services_page" model="website.page">
+ <field name="name">Services page</field>
+ <field name="website_published">True</field>
+ <field name="url">/services</field>
+ <field name="type">qweb</field>
+ <field name="key">theme_tutorial.services_page</field>
+ <field name="arch" type="xml">
+ <t t-name="theme_tutorial.services_page_template">
+ <h1>Our Services</h1>
+ <ul class="services">
+ <li>Cloud Hosting</li>
+ <li>Support</li>
+ <li>Unlimited space</li>
+ </ul>
+ </t>
+ </field>
+ </record>
+
+ </odoo>
+
+As you can see, pages come with many additional properties like the *name* or
+the *url* where it is reachable.
We successfully created a new page layout, but we haven't told the
system **how to use it**. To do that, we can use **QWeb**. Wrap the
@@ -405,21 +415,30 @@ html code into a ``<t>`` tag, like in this example.
.. code-block:: xml
- <!-- === Services Page === -->
- <template name="Services page" id="website.services" page="True">
- <t t-call="website.layout">
- <div id="wrap">
- <div class="container">
- <h1>Our Services</h1>
- <ul class="services">
- <li>Cloud Hosting</li>
- <li>Support</li>
- <li>Unlimited space</li>
- </ul>
- </div>
- </div>
- </t>
- </template>
+ <!-- === Services Page === -->
+ <record id="services_page" model="website.page">
+ <field name="name">Services page</field>
+ <field name="website_published">True</field>
+ <field name="url">/services</field>
+ <field name="type">qweb</field>
+ <field name="key">theme_tutorial.services_page</field>
+ <field name="arch" type="xml">
+ <t t-name="theme_tutorial.services_page_template">
+ <t t-call="website.layout">
+ <div id="wrap">
+ <div class="container">
+ <h1>Our Services</h1>
+ <ul class="services">
+ <li>Cloud Hosting</li>
+ <li>Support</li>
+ <li>Unlimited space</li>
+ </ul>
+ </div>
+ </div>
+ </t>
+ </t>
+ </field>
+ </record>
Using ``<t t-call="website.layout">`` we will extend the Odoo
default page layout with our code.
@@ -432,33 +451,80 @@ can fill with snippets. To achieve this, just create a ``div`` with
.. code-block:: xml
- <?xml version="1.0" encoding="utf-8" ?>
- <odoo>
-
- <!-- === Services Page === -->
- <template name="Services page" id="website.services" page="True">
- <t t-call="website.layout">
- <div id="wrap">
- <div class="container">
- <h1>Our Services</h1>
- <ul class="services">
- <li>Cloud Hosting</li>
- <li>Support</li>
- <li>Unlimited space</li>
- </ul>
- <!-- === Snippets' area === -->
- <div class="oe_structure" />
- </div>
- </div>
- </t>
- </template>
-
- </odoo>
+ <?xml version="1.0" encoding="utf-8" ?>
+ <odoo>
+
+ <!-- === Services Page === -->
+ <record id="services_page" model="website.page">
+ <field name="name">Services page</field>
+ <field name="website_published">True</field>
+ <field name="url">/services</field>
+ <field name="type">qweb</field>
+ <field name="key">theme_tutorial.services_page</field>
+ <field name="arch" type="xml">
+ <t t-name="theme_tutorial.services_page_template">
+ <t t-call="website.layout">
+ <div id="wrap">
+ <div class="container">
+ <h1>Our Services</h1>
+ <ul class="services">
+ <li>Cloud Hosting</li>
+ <li>Support</li>
+ <li>Unlimited space</li>
+ </ul>
+
+ <!-- === Snippets' area === -->
+ <div class="oe_structure" />
+ </div>
+ </div>
+ </t>
+ </t>
+ </field>
+ </record>
+
+ </odoo>
.. tip::
You can create as many snippet areas as you like and place them anywhere in your pages.
+It is worth mentioning there is an alternative to create pages using the
+``<template>`` directive we saw before.
+
+.. code-block:: xml
+
+ <?xml version="1.0" encoding="utf-8" ?>
+ <odoo>
+
+ <!-- === Services Page === -->
+ <template id="services_page_template">
+ <t t-call="website.layout">
+ <div id="wrap">
+ <div class="container">
+ <h1>Our Services</h1>
+ <ul class="services">
+ <li>Cloud Hosting</li>
+ <li>Support</li>
+ <li>Unlimited space</li>
+ </ul>
+
+ <!-- === Snippets' area === -->
+ <div class="oe_structure" />
+ </div>
+ </div>
+ </t>
+ </template>
+ <record id="services_page" model="website.page">
+ <field name="name">Services page</field>
+ <field name="website_published">True</field>
+ <field name="url">/services</field>
+ <field name="view_id" ref="services_page_template"/>
+ </record>
+
+ </odoo>
+
+This would allow your page content to be further customized using ``<xpath>``.
+
Our page is almost ready. Now all we have to do is add **pages.xml** in our **__manifest__.py** file
.. code-block:: python
@@ -472,7 +538,7 @@ Update your theme
.. image:: theme_tutorial_assets/img/restart.png
-Great, our Services page is ready and you’ll be able to access it by navigating to ``/yourwebsite/page/services``.
+Great, our Services page is ready and you’ll be able to access it by navigating to ``<yourwebsite>/services`` (the URL we chose above).
You will notice that it's possible to drag/drop snippets underneath the
*Our Services* list.
@@ -487,12 +553,12 @@ copy/paste the following code.
<record id="services_page_link" model="website.menu">
<field name="name">Services</field>
- <field name="url">/page/services</field>
+ <field name="page_id" ref="services_page"/>
<field name="parent_id" ref="website.main_menu" />
<field name="sequence" type="int">99</field>
</record>
-This code will add a link to the main menu.
+This code will add a link to the main menu, referring to the page we created.
.. image:: theme_tutorial_assets/img/services_page_menu.png
:class: shadow-0
diff --git a/doc/reference/cmdline.rst b/doc/reference/cmdline.rst
index 2f1626f6dac..4171f12c5e1 100644
--- a/doc/reference/cmdline.rst
+++ b/doc/reference/cmdline.rst
@@ -15,7 +15,9 @@ Running the server
.. option:: -d <database>, --database <database>
- database used when installing or updating modules.
+ database(s) used when installing or updating modules.
+ Providing a comma-separated list restrict access to databases provided in
+ list.
.. option:: -i <modules>, --init <modules>
@@ -165,6 +167,44 @@ database
databases (so domain ``odoo.com`` using ``(?i)%d`` matches the database
``Odoo``).
+ Since version 11, it's also possible to restrict access to a given database
+ listen by using the --database parameter and specifying a comma-separated
+ list of databases
+
+ When combining the two parameters, db-filter superseed the comma-separated
+ database list for restricting database list, while the comma-separated list
+ is used for performing requested operations like upgrade of modules.
+
+ .. code-block:: bash
+
+ odoo-bin --db-filter ^11.*$
+
+ Restrict access to databases whose name starts with 11
+
+ .. code-block:: bash
+
+ odoo-bin --database 11firstdatabase,11seconddatabase
+
+ Restrict access to only two databases, 11firstdatabase and 11seconddatabase
+
+ .. code-block:: bash
+
+ odoo-bin --database 11firstdatabase,11seconddatabase -u base
+
+ Restrict access to only two databases, 11firstdatabase and 11seconddatabase,
+ and update base module on one database: 11firstdatabase
+ If database 11seconddatabase doesn't exist, the database is created and base modules
+ is installed
+
+ .. code-block:: bash
+
+ odoo-bin --db-filter ^11.*$ --database 11firstdatabase,11seconddatabase -u base
+
+ Restrict access to databases whose name starts with 11,
+ and update base module on one database: 11firstdatabase
+ If database 11seconddatabase doesn't exist, the database is created and base modules
+ is installed
+
.. option:: --db-template <template>
when creating new databases from the database-management screens, use the
@@ -173,6 +213,13 @@ database
.. option:: --no-database-list
Suppresses the ability to list databases available on the system
+
+.. option:: --db_sslmode
+
+ Control the SSL security of the connection between Odoo and PostgreSQL.
+ Value should bve one of 'disable', 'allow', 'prefer', 'require',
+ 'verify-ca' or 'verify-full'
+ Default value is 'prefer'
built-in HTTP
-------------
diff --git a/doc/reference/views.rst b/doc/reference/views.rst
index 7337f861c9f..5920b1ff7c6 100644
--- a/doc/reference/views.rst
+++ b/doc/reference/views.rst
@@ -230,8 +230,7 @@ Possible children elements of the list view are:
context of the current row's record, if ``True`` the corresponding
attribute is set on the cell.
- Possible attributes are ``invisible`` (hides the button) and
- ``readonly`` (disables the button but still shows it)
+ Possible attribute is ``invisible`` (hides the button).
``states``
shorthand for ``invisible`` ``attrs``: a list of states, comma separated,
requires that the model has a ``state`` field and that it is
diff --git a/doc/setup/deploy.rst b/doc/setup/deploy.rst
index 6a1e22f886c..5aea431caf4 100644
--- a/doc/setup/deploy.rst
+++ b/doc/setup/deploy.rst
@@ -165,6 +165,18 @@ in ``/etc/odoo.conf`` set:
db_password = pwd
dbfilter = ^mycompany.*$
+.. _postgresql_ssl_connect:
+
+SSL Between Odoo and PostgreSQL
+-------------------------------
+
+Since Odoo 11.0, you can enforce ssl connection between Odoo and PostgreSQL.
+in Odoo the db_sslmode control the ssl security of the connection
+with value choosed out of 'disable', 'allow', 'prefer', 'require', 'verify-ca'
+or 'verify-full'
+
+`PostgreSQL Doc <https://www.postgresql.org/docs/current/static/libpq-ssl.html>`_
+
.. _builtin_server:
Builtin server
@@ -541,8 +553,8 @@ of the supported browsers by Odoo version is the following:
.. _socat: http://www.dest-unreach.org/socat/
.. _PostgreSQL connection settings:
.. _listen to network interfaces:
- http://www.postgresql.org/docs/9.3/static/runtime-config-connection.html
+ http://www.postgresql.org/docs/9.6/static/runtime-config-connection.html
.. _use an SSH tunnel:
- http://www.postgresql.org/docs/9.3/static/ssh-tunnels.html
+ http://www.postgresql.org/docs/9.6/static/ssh-tunnels.html
.. _WSGI: http://wsgi.readthedocs.org/
.. _POSBox: https://www.odoo.com/page/point-of-sale-hardware#part_2
diff --git a/doc/tutorials.rst b/doc/tutorials.rst
index 090b82ec8c9..ee30b3c681b 100644
--- a/doc/tutorials.rst
+++ b/doc/tutorials.rst
@@ -9,3 +9,4 @@ Tutorials
howtos/website
howtos/backend
howtos/web
+ howtos/profilecode \ No newline at end of file
diff --git a/doc/webservices.rst b/doc/webservices.rst
index 41e73c7604e..3af3a363902 100644
--- a/doc/webservices.rst
+++ b/doc/webservices.rst
@@ -11,4 +11,4 @@ Web Services
webservices/odoo
webservices/iap
webservices/upgrade
- webservices/localization \ No newline at end of file
+ webservices/localization
diff --git a/odoo/addons/base/data/ir_mail_server_data.xml b/odoo/addons/base/data/ir_mail_server_data.xml
index 475b59a15e1..783429b3de0 100644
--- a/odoo/addons/base/data/ir_mail_server_data.xml
+++ b/odoo/addons/base/data/ir_mail_server_data.xml
@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
- <record id="ir_mail_server_localhost0" model="ir.mail_server">
+ <record id="ir_mail_server_localhost0" model="ir.mail_server" forcecreate="0">
<field name="name">localhost</field>
<field name="smtp_host">localhost</field>
<field eval="25" name="smtp_port"/>
<field eval="10" name="sequence"/>
</record>
</data>
-</odoo> \ No newline at end of file
+</odoo>
diff --git a/odoo/addons/base/models/ir_actions_report.py b/odoo/addons/base/models/ir_actions_report.py
index f3effecced8..f5537b631d2 100644
--- a/odoo/addons/base/models/ir_actions_report.py
+++ b/odoo/addons/base/models/ir_actions_report.py
@@ -95,7 +95,7 @@ class IrActionsReport(models.Model):
paperformat_id = fields.Many2one('report.paperformat', 'Paper format')
print_report_name = fields.Char('Printed Report Name',
- help="This is the filename of the report going to download. Keep empty to not change the report filename. You can use a python expression with the object and time variables.")
+ help="This is the filename of the report going to download. Keep empty to not change the report filename. You can use a python expression with the 'object' and 'time' variables.")
attachment_use = fields.Boolean(string='Reload from Attachment',
help='If you check this, then the second time the user prints with same attachment name, it returns the previous report.')
attachment = fields.Char(string='Save as Attachment Prefix',
diff --git a/odoo/addons/base/models/ir_autovacuum.py b/odoo/addons/base/models/ir_autovacuum.py
index 804f7bc827d..8ffdb0b1b3e 100644
--- a/odoo/addons/base/models/ir_autovacuum.py
+++ b/odoo/addons/base/models/ir_autovacuum.py
@@ -17,7 +17,11 @@ class AutoVacuum(models.AbstractModel):
for mname in self.env:
model = self.env[mname]
if model.is_transient():
- model._transient_vacuum(force=True)
+ try:
+ with self._cr.savepoint():
+ model._transient_vacuum(force=True)
+ except Exception as e:
+ _logger.warning("Failed to clean transient model %s\n%s", model, str(e))
@api.model
def _gc_user_logs(self):
diff --git a/odoo/addons/base/models/ir_cron.py b/odoo/addons/base/models/ir_cron.py
index 58dc0f06fb7..0af75822329 100644
--- a/odoo/addons/base/models/ir_cron.py
+++ b/odoo/addons/base/models/ir_cron.py
@@ -71,6 +71,7 @@ class ir_cron(models.Model):
@api.multi
def method_direct_trigger(self):
+ self.check_access_rights('write')
for cron in self:
self.sudo(user=cron.user_id.id).ir_actions_server_id.run()
return True
diff --git a/odoo/addons/base/models/ir_ui_view.py b/odoo/addons/base/models/ir_ui_view.py
index 02199375f87..5a53839b628 100644
--- a/odoo/addons/base/models/ir_ui_view.py
+++ b/odoo/addons/base/models/ir_ui_view.py
@@ -115,33 +115,31 @@ def _hasclass(context, *cls):
def get_view_arch_from_file(filename, xmlid):
doc = etree.parse(filename)
- node = None
- for n in doc.xpath('//*[@id="%s"]' % (xmlid)):
- if n.tag in ('template', 'record'):
- node = n
- break
- if node is None:
- # fallback search on template with implicit module name
- for n in doc.xpath('//*[@id="%s"]' % (xmlid.split('.')[1])):
- if n.tag in ('template', 'record'):
- node = n
- break
- if node is not None:
- if node.tag == 'record':
- field = node.find('field[@name="arch"]')
- _fix_multiple_roots(field)
- inner = u''.join([etree.tostring(child, encoding='unicode') for child in field.iterchildren()])
- return field.text + inner
- elif node.tag == 'template':
- # The following dom operations has been copied from convert.py's _tag_template()
- if not node.get('inherit_id'):
- node.set('t-name', xmlid)
- node.tag = 't'
- else:
- node.tag = 'data'
- node.attrib.pop('id', None)
- return etree.tostring(node, encoding='unicode')
- _logger.warning("Could not find view arch definition in file '%s' for xmlid '%s'", filename, xmlid)
+
+ xmlid_search = (xmlid, xmlid.split('.')[1])
+
+ # when view is created from model with inheritS of ir_ui_view, the xml id has been suffixed by '_ir_ui_view'
+ suffix = '_ir_ui_view'
+ if xmlid.endswith(suffix):
+ xmlid_search += (xmlid.rsplit(suffix, 1)[0], xmlid.split('.')[1].rsplit(suffix, 1)[0])
+
+ for node in doc.xpath('//*[%s]' % ' or '.join(["@id='%s'" % _id for _id in xmlid_search])):
+ if node.tag in ('template', 'record'):
+ if node.tag == 'record':
+ field = node.find('field[@name="arch"]')
+ _fix_multiple_roots(field)
+ inner = u''.join([etree.tostring(child, encoding='unicode') for child in field.iterchildren()])
+ return field.text + inner
+ elif node.tag == 'template':
+ # The following dom operations has been copied from convert.py's _tag_template()
+ if not node.get('inherit_id'):
+ node.set('t-name', xmlid)
+ node.tag = 't'
+ else:
+ node.tag = 'data'
+ node.attrib.pop('id', None)
+ return etree.tostring(node, encoding='unicode')
+ _logger.warning("Could not find view arch definition in file '%s' for xmlid '%s'", filename, xmlid_search)
return None
def add_text_before(node, text):
@@ -258,14 +256,12 @@ actual arch.
data = dict(arch_db=view.arch)
if 'install_mode_data' in self._context:
imd = self._context['install_mode_data']
- if '.' not in imd['xml_id']:
- imd['xml_id'] = '%s.%s' % (imd['module'], imd['xml_id'])
- if self._name == imd['model'] and (not view.xml_id or view.xml_id == imd['xml_id']):
- # we store the relative path to the resource instead of the absolute path, if found
- # (it will be missing e.g. when importing data-only modules using base_import_module)
- path_info = get_resource_from_path(imd['xml_file'])
- if path_info:
- data['arch_fs'] = '/'.join(path_info[0:2])
+
+ # we store the relative path to the resource instead of the absolute path, if found
+ # (it will be missing e.g. when importing data-only modules using base_import_module)
+ path_info = get_resource_from_path(imd['xml_file'])
+ if path_info:
+ data['arch_fs'] = '/'.join(path_info[0:2])
view.write(data)
@api.depends('arch')
diff --git a/odoo/addons/base/models/res_company.py b/odoo/addons/base/models/res_company.py
index 599f34ce214..84330680a82 100644
--- a/odoo/addons/base/models/res_company.py
+++ b/odoo/addons/base/models/res_company.py
@@ -5,7 +5,7 @@ import os
import re
from odoo import api, fields, models, tools, _
-from odoo.exceptions import ValidationError
+from odoo.exceptions import ValidationError, UserError
class Company(models.Model):
@@ -15,16 +15,7 @@ class Company(models.Model):
@api.multi
def copy(self, default=None):
- """
- Duplicating a company without specifying a partner duplicate the partner
- """
- self.ensure_one()
- default = dict(default or {})
- if not default.get('name') and not default.get('partner_id'):
- copy_partner = self.partner_id.copy()
- default['partner_id'] = copy_partner.id
- default['name'] = copy_partner.name
- return super(Company, self).copy(default)
+ raise UserError(_('Duplicating a company is not allowed. Please create a new company instead.'))
def _get_logo(self):
return base64.b64encode(open(os.path.join(tools.config['root_path'], 'addons', 'base', 'static', 'img', 'res_company_logo.png'), 'rb') .read())
diff --git a/odoo/addons/base/models/res_partner.py b/odoo/addons/base/models/res_partner.py
index 4f81b8b6e95..786c74473fb 100644
--- a/odoo/addons/base/models/res_partner.py
+++ b/odoo/addons/base/models/res_partner.py
@@ -204,7 +204,7 @@ class Partner(models.Model):
user_ids = fields.One2many('res.users', 'partner_id', string='Users', auto_join=True)
partner_share = fields.Boolean(
'Share Partner', compute='_compute_partner_share', store=True,
- help="Either customer (no user), either shared user. Indicated the current partner is a customer without "
+ help="Either customer (not a user), either shared user. Indicated the current partner is a customer without "
"access or with a limited access created for sharing data.")
contact_address = fields.Char(compute='_compute_contact_address', string='Complete Address')
diff --git a/odoo/addons/base/models/res_users.py b/odoo/addons/base/models/res_users.py
index b0dd4a8ff78..abb25a4bc2e 100644
--- a/odoo/addons/base/models/res_users.py
+++ b/odoo/addons/base/models/res_users.py
@@ -751,7 +751,11 @@ class GroupsView(models.Model):
xml = E.field(E.group(*(xml1), col="2"), E.group(*(xml2), col="4"), name="groups_id", position="replace")
xml.addprevious(etree.Comment("GENERATED AUTOMATICALLY BY GROUPS"))
xml_content = etree.tostring(xml, pretty_print=True, encoding="unicode")
- view.with_context(lang=None).write({'arch': xml_content, 'arch_fs': False})
+
+ new_context = dict(view._context)
+ new_context.pop('install_mode_data', None) # don't set arch_fs for this computed view
+ new_context['lang'] = None
+ view.with_context(new_context).write({'arch': xml_content})
def get_application_groups(self, domain):
""" Return the non-share groups that satisfy ``domain``. """
diff --git a/odoo/addons/base/views/ir_qweb_widget_templates.xml b/odoo/addons/base/views/ir_qweb_widget_templates.xml
index 63057f1a25a..1fc0e5d921a 100644
--- a/odoo/addons/base/views/ir_qweb_widget_templates.xml
+++ b/odoo/addons/base/views/ir_qweb_widget_templates.xml
@@ -7,9 +7,9 @@
<t t-if="not object.name and object.parent_name">
<span itemprop="name" t-esc="object.parent_name"/>
</t>
- <div t-if="options.get('country_image') and 'country_id' in fields and object.country_id and object.country_id.image">
+ <t t-if="options.get('country_image') and 'country_id' in fields and object.country_id and object.country_id.image">
<span t-field="object.country_id.image" t-options='{"widget": "image", "class": "country_flag"}'/>
- </div>
+ </t>
</div>
</template>
<template id="contact">
diff --git a/odoo/addons/test_new_api/models.py b/odoo/addons/test_new_api/models.py
index 026159bbf00..eb1c6532814 100644
--- a/odoo/addons/test_new_api/models.py
+++ b/odoo/addons/test_new_api/models.py
@@ -324,6 +324,9 @@ class Related(models.Model):
related_name = fields.Char(related='name', string='A related on Name')
related_related_name = fields.Char(related='related_name', string='A related on a related on Name')
+ message = fields.Many2one('test_new_api.message')
+ message_name = fields.Text(related="message.body", related_sudo=False, string='Message Body')
+ message_currency = fields.Many2one(related="message.author", string='Message Author')
class ComputeProtected(models.Model):
_name = 'test_new_api.compute.protected'
@@ -336,7 +339,6 @@ class ComputeProtected(models.Model):
for record in self:
record.bar = record.foo
-
class ComputeInverse(models.Model):
_name = 'test_new_api.compute.inverse'
diff --git a/odoo/addons/test_new_api/tests/test_new_fields.py b/odoo/addons/test_new_api/tests/test_new_fields.py
index c1bf14fd05c..bd33123af89 100644
--- a/odoo/addons/test_new_api/tests/test_new_fields.py
+++ b/odoo/addons/test_new_api/tests/test_new_fields.py
@@ -733,16 +733,17 @@ class TestFields(common.TransactionCase):
env = self.env(user=self.env.ref('base.user_demo'))
self.assertEqual(env.user.login, "demo")
- # create a new message as demo user
- discussion = self.env.ref('test_new_api.discussion_0')
- message = env['test_new_api.message'].new({'discussion': discussion})
- self.assertEqual(message.discussion, discussion)
-
- # read the related field discussion_name
- self.assertEqual(message.discussion.env, env)
- self.assertEqual(message.discussion_name, discussion.name)
- with self.assertRaises(AccessError):
- message.discussion.name
+ with self.env.do_in_onchange():
+ # create a new message as demo user
+ discussion = self.env.ref('test_new_api.discussion_0')
+ message = env['test_new_api.message'].new({'discussion': discussion})
+ self.assertEqual(message.discussion, discussion)
+
+ # read the related field discussion_name
+ self.assertEqual(message.discussion.env, env)
+ self.assertEqual(message.discussion_name, discussion.name)
+ with self.assertRaises(AccessError):
+ message.discussion.name
@mute_logger('odoo.addons.base.models.ir_model')
def test_42_new_related(self):
@@ -755,14 +756,15 @@ class TestFields(common.TransactionCase):
env = self.env(user=self.env.ref('base.user_demo'))
self.assertEqual(env.user.login, "demo")
- # create a new discussion and a new message as demo user
- discussion = env['test_new_api.discussion'].new({'name': 'Stuff'})
- message = env['test_new_api.message'].new({'discussion': discussion})
- self.assertEqual(message.discussion, discussion)
+ with self.env.do_in_onchange():
+ # create a new discussion and a new message as demo user
+ discussion = env['test_new_api.discussion'].new({'name': 'Stuff'})
+ message = env['test_new_api.message'].new({'discussion': discussion})
+ self.assertEqual(message.discussion, discussion)
- # read the related field discussion_name
- self.assertNotEqual(message.sudo().env, message.env)
- self.assertEqual(message.discussion_name, discussion.name)
+ # read the related field discussion_name
+ self.assertNotEqual(message.sudo().env, message.env)
+ self.assertEqual(message.discussion_name, discussion.name)
def test_50_defaults(self):
""" test default values. """
diff --git a/odoo/addons/test_new_api/tests/test_onchange.py b/odoo/addons/test_new_api/tests/test_onchange.py
index a87006c76cb..7caf433a467 100644
--- a/odoo/addons/test_new_api/tests/test_onchange.py
+++ b/odoo/addons/test_new_api/tests/test_onchange.py
@@ -425,3 +425,32 @@ class TestOnChange(common.TransactionCase):
'size': email.size,
})]
)
+
+ def test_onchange_related(self):
+ value = {
+ 'message': 1,
+ 'message_name': False,
+ 'message_currency': 2,
+ }
+ field_onchange = {
+ 'message': '1',
+ 'message_name': None,
+ 'message_currency': None,
+ }
+
+ onchange_result = {
+ 'message_name': 'Hey dude!',
+ 'message_currency': (1, 'Administrator')
+ }
+
+ self.env.cache.invalidate()
+ Message = self.env['test_new_api.related']
+ result = Message.onchange(value, ['message', 'message_name', 'message_currency'], field_onchange)
+
+ self.assertEqual(result['value'], onchange_result)
+
+ self.env.cache.invalidate()
+ Message = self.env(user=self.env.ref('base.user_demo').id)['test_new_api.related']
+ result = Message.onchange(value, ['message', 'message_name', 'message_currency'], field_onchange)
+
+ self.assertEqual(result['value'], onchange_result)
diff --git a/odoo/addons/test_pylint/tests/test_pylint.py b/odoo/addons/test_pylint/tests/test_pylint.py
index f5ac83ffb24..5681e013d38 100644
--- a/odoo/addons/test_pylint/tests/test_pylint.py
+++ b/odoo/addons/test_pylint/tests/test_pylint.py
@@ -105,7 +105,7 @@ class TestPyLint(TransactionCase):
'UserDict',
'UserString',
'UserList',
- ]
+ ] + list(tools.SUPPORTED_DEBUGGER)
def _skip_test(self, reason):
_logger.warn(reason)
diff --git a/odoo/fields.py b/odoo/fields.py
index 4050c07224c..46898e3fcd4 100644
--- a/odoo/fields.py
+++ b/odoo/fields.py
@@ -566,7 +566,7 @@ class Field(MetaField('DummyField', (object,), {})):
# when related_sudo, bypass access rights checks when reading values
others = records.sudo() if self.related_sudo else records
# copy the cache of draft records into others' cache
- if records.env != others.env:
+ if records.env.in_onchange and records.env != others.env:
copy_cache(records - records.filtered('id'), others.env)
#
# Traverse fields one by one for all records, in order to take advantage
diff --git a/odoo/models.py b/odoo/models.py
index 3fa61eb728d..6834dca5b6a 100644
--- a/odoo/models.py
+++ b/odoo/models.py
@@ -4919,15 +4919,16 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
done = set()
# dummy assignment: trigger invalidations on the record
- for name in todo:
- if name == 'id':
- continue
- value = record[name]
- field = self._fields[name]
- if field.type == 'many2one' and field.delegate and not value:
- # do not nullify all fields of parent record for new records
- continue
- record[name] = value
+ with env.do_in_onchange():
+ for name in todo:
+ if name == 'id':
+ continue
+ value = record[name]
+ field = self._fields[name]
+ if field.type == 'many2one' and field.delegate and not value:
+ # do not nullify all fields of parent record for new records
+ continue
+ record[name] = value
result = {}
dirty = set()