diff options
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="['&', ('state','in',['draft','open','paid']),('type','in',('out_invoice','in_invoice'))]"/> - <filter name="refunds" string="Credit Notes" domain="['&', ('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="['&', ('date_due', '<', 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', '<=', 0)]}"/> + <widget name="attach_document" string="Attach Document" action="message_post" class="oe_highlight" attrs="{'invisible': [('attachment_number', '>=', 1)]}"/> + <button name="action_submit_expenses" string="Submit to Manager" type="object" class="o_expense_submit" attrs="{'invisible': ['|', ('attachment_number', '>=', 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', '<=', 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', '<=', 0)]}"/> - <widget name="attach_document" string="Attach Document" action="message_post" class="oe_highlight" attrs="{'invisible': [('attachment_number', '>=', 1)]}"/> - <button name="action_submit_expenses" string="Submit to Manager" type="object" class="o_expense_submit" attrs="{'invisible': ['|', ('attachment_number', '>=', 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', '<=', 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"><img src='/mail/static/src/img/smiley/normal_smile.png' alt='😊'/></field> + <field name="unicode_source">🙂</field> + <field name="substitution"><img src='/mail/static/src/img/smiley/smile.png' alt='🙂'/></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"><img src='/mail/static/src/img/smiley/smile.png' alt='😄'/></field> + <field name="unicode_source">😀</field> + <field name="substitution"><img src='/mail/static/src/img/smiley/smile_open_mouth.png' alt='😀'/></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"><img src='/mail/static/src/img/smiley/normal_smile.png' alt='😊'/></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"><img src='/mail/static/src/img/smiley/tears_of_joy.png' alt='😂'/></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 Binary files differnew file mode 100644 index 00000000000..7dcdd3f55fe --- /dev/null +++ b/addons/mail/static/src/img/smiley/smile_open_mouth.png 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&model=project.task&id=%s&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>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), '&', ('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': ['|', '&', ('show_operations', '=', True), '|', ('is_locked', '=', True), ('state', '=', 'done'), '&', ('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': ['&', ('state', '!=', 'draft'), ('additional', '=', False)]}"/> + <field name="product_id" required="1" attrs="{'readonly': ['|', '&', ('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><string>:__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><frozen importlib._bootstrap>:_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><frozen importlib._bootstrap>:_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><frozen importlib._bootstrap_external>: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><string>: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)" ><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:<listcomp>: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><decorator-gen-112>: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:<module>: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:<module>: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:<listcomp>: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:<lambda>: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><frozen importlib._bootstrap>:_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><frozen importlib._bootstrap>:_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:<module>: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><frozen importlib._bootstrap>:_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><frozen importlib._bootstrap>:_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:<listcomp>: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><frozen importlib._bootstrap>:_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><frozen importlib._bootstrap>:_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><frozen importlib._bootstrap>:_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><string>: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)" ><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><frozen importlib._bootstrap_external>: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><frozen importlib._bootstrap>:_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><string>: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><decorator-gen-37>: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><frozen importlib._bootstrap>:_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><frozen importlib._bootstrap>:_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><decorator-gen-112>: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:<listcomp>: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:<listcomp>: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:<module>: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><string>: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><frozen importlib._bootstrap>:_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><frozen importlib._bootstrap>:_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><frozen importlib._bootstrap>:_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:<dictcomp>: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><string>: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)" ><..</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><string>:<module>: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><frozen importlib._bootstrap>:_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><string>: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)" ><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><string>: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)" ><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:<listcomp>: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:<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() |
