diff options
| author | Christophe Simonis <chs@odoo.com> | 2016-10-19 18:16:14 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2016-10-19 18:16:14 +0200 |
| commit | 938ffb92da1ea898f661aec04745f7630e2f378c (patch) | |
| tree | 7faeed8433101836cd0239cf6c23629ef1f1289d | |
| parent | 1f9e92dfef325b6727e1bee3de55bff095ec6cf2 (diff) | |
| parent | 6466aa499fc37bba5f4feeeecac99448ef88e62e (diff) | |
[MERGE] forward port branch 10.0 up to 6466aa4
80 files changed, 956 insertions, 408 deletions
diff --git a/README.md b/README.md index 4432013ba16..f8bfd4cf38f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [](http://runbot.odoo.com/runbot) -[](http://www.odoo.com/documentation/master) -[](https://www.odoo.com/forum/help-1) -[](http://nightly.odoo.com/) +[](http://www.odoo.com/documentation/master) +[](https://www.odoo.com/forum/help-1) +[](http://nightly.odoo.com/) Odoo ---- @@ -27,14 +27,14 @@ a full-featured <a href="https://www.odoo.com">Open Source ERP</a> when you inst Getting started with Odoo ------------------------- -For a standard installation please follow the <a href="https://www.odoo.com/documentation/9.0/setup/install.html">Setup instructions</a> +For a standard installation please follow the <a href="https://www.odoo.com/documentation/master/setup/install.html">Setup instructions</a> from the documentation. If you are a developer you may type the following command at your terminal: - wget -O- https://raw.githubusercontent.com/odoo/odoo/10.0/setup/setup_dev.py | python + wget -O- https://raw.githubusercontent.com/odoo/odoo/master/setup/setup_dev.py | python -Then follow <a href="https://www.odoo.com/documentation/9.0/tutorials.html">the developer tutorials</a> +Then follow <a href="https://www.odoo.com/documentation/master/tutorials.html">the developer tutorials</a> For Odoo employees diff --git a/addons/account/models/account.py b/addons/account/models/account.py index 0271cad8aa5..eec37901431 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -731,6 +731,9 @@ class AccountTax(models.Model): else: total_included += tax_amount + # Keep base amount used for the current tax + tax_base = base + if tax.include_base_amount: base += tax_amount @@ -738,6 +741,7 @@ class AccountTax(models.Model): 'id': tax.id, 'name': tax.with_context(**{'lang': partner.lang} if partner else {}).name, 'amount': tax_amount, + 'base': tax_base, 'sequence': tax.sequence, 'account_id': tax.account_id.id, 'refund_account_id': tax.refund_account_id.id, diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index a23e1d12b5c..204f0161a85 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -250,13 +250,13 @@ class AccountBankStatement(models.Model): context = dict(self._context or {}) context['journal_id'] = self.journal_id.id return { - 'name': _('Journal Items'), + 'name': _('Journal Entries'), 'view_type': 'form', 'view_mode': 'tree,form', - 'res_model': 'account.move.line', + 'res_model': 'account.move', 'view_id': False, 'type': 'ir.actions.act_window', - 'domain': [('statement_id', 'in', self.ids)], + 'domain': [('id', 'in', self.mapped('move_line_ids').mapped('move_id').ids)], 'context': context, } @@ -366,6 +366,7 @@ class AccountBankStatementLine(models.Model): journal_entry_ids = fields.One2many('account.move', 'statement_line_id', 'Journal Entries', copy=False, readonly=True) amount_currency = fields.Monetary(help="The amount expressed in an optional other currency if it is a multi-currency entry.") currency_id = fields.Many2one('res.currency', string='Currency', help="The optional other currency if it is a multi-currency entry.") + state = fields.Selection(related='statement_id.state' , string='Status', readonly=True) move_name = fields.Char(string='Journal Entry Name', readonly=True, default=False, copy=False, help="Technical field holding the number given to the journal entry, automatically set when the statement line is reconciled then stored to set the same number again if the line is cancelled, set to draft and re-processed again.") diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 04488a6ea8e..d7bec0cb579 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -12,6 +12,9 @@ from odoo.tools.misc import formatLang from odoo.exceptions import UserError, RedirectWarning, ValidationError import odoo.addons.decimal_precision as dp +import logging + +_logger = logging.getLogger(__name__) # mapping invoice type to journal type TYPE2JOURNAL = { @@ -592,6 +595,25 @@ class AccountInvoice(models.Model): else: return self.env.ref('account.invoice_form').id + def _prepare_tax_line_vals(self, line, tax): + """ Prepare values to create an account.invoice.tax line + + The line parameter is an account.invoice.line, and the + tax parameter is the output of account.tax.compute_all(). + """ + vals = { + 'invoice_id': self.id, + 'name': tax['name'], + 'tax_id': tax['id'], + 'amount': tax['amount'], + 'base': tax['base'], + 'manual': False, + 'sequence': tax['sequence'], + 'account_analytic_id': tax['analytic'] and line.account_analytic_id.id or False, + 'account_id': self.type in ('out_invoice', 'in_invoice') and (tax['account_id'] or line.account_id.id) or (tax['refund_account_id'] or line.account_id.id), + } + return vals + @api.multi def get_taxes_values(self): tax_grouped = {} @@ -599,16 +621,7 @@ class AccountInvoice(models.Model): price_unit = line.price_unit * (1 - (line.discount or 0.0) / 100.0) taxes = line.invoice_line_tax_ids.compute_all(price_unit, self.currency_id, line.quantity, line.product_id, self.partner_id)['taxes'] for tax in taxes: - val = { - 'invoice_id': self.id, - 'name': tax['name'], - 'tax_id': tax['id'], - 'amount': tax['amount'], - 'manual': False, - 'sequence': tax['sequence'], - 'account_analytic_id': tax['analytic'] and line.account_analytic_id.id or False, - 'account_id': self.type in ('out_invoice', 'in_invoice') and (tax['account_id'] or line.account_id.id) or (tax['refund_account_id'] or line.account_id.id), - } + val = self._prepare_tax_line_vals(line, tax) # If the taxes generate moves on the same financial account as the invoice line, # propagate the analytic account from the invoice line to the tax line. @@ -623,6 +636,7 @@ class AccountInvoice(models.Model): tax_grouped[key] = val else: tax_grouped[key]['amount'] += val['amount'] + tax_grouped[key]['base'] += val['base'] return tax_grouped @api.multi @@ -1336,15 +1350,19 @@ class AccountInvoiceTax(models.Model): @api.depends('invoice_id.invoice_line_ids') def _compute_base_amount(self): + tax_grouped = {} + for invoice in self.mapped('invoice_id'): + tax_grouped[invoice.id] = invoice.get_taxes_values() for tax in self: - base = 0.0 - for line in tax.invoice_id.invoice_line_ids: - if tax.tax_id in line.invoice_line_tax_ids: - price_unit = line.price_unit * (1 - (line.discount or 0.0) / 100.0) - base += (line.invoice_line_tax_ids - tax.tax_id).compute_all( - price_unit, line.invoice_id.currency_id, line.quantity, line.product_id, line.invoice_id.partner_id - )['base'] - tax.base = base + key = self.env['account.tax'].browse(tax.tax_id.id).get_grouping_key({ + 'tax_id': tax.tax_id.id, + 'account_id': tax.account_id.id, + 'account_analytic_id': tax.account_analytic_id.id, + }) + if tax.invoice_id and key in tax_grouped[tax.invoice_id.id]: + tax.base = tax_grouped[tax.invoice_id.id][key]['base'] + else: + _logger.warning('Tax Base Amount not computable probably due to a change in an underlying tax (%s).', tax.tax_id.name) invoice_id = fields.Many2one('account.invoice', string='Invoice', ondelete='cascade', index=True) name = fields.Char(string='Tax Description', required=True) diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index b9d80ea21cc..5ff889f6025 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -1037,10 +1037,13 @@ class AccountMoveLine(models.Model): # the provided values were not already multi-currency if account.currency_id and 'amount_currency' not in vals and account.currency_id.id != account.company_id.currency_id.id: vals['currency_id'] = account.currency_id.id - ctx = {} - if 'date' in vals: - ctx['date'] = vals['date'] - vals['amount_currency'] = account.company_id.currency_id.with_context(ctx).compute(amount, account.currency_id) + if self._context.get('skip_full_reconcile_check') == 'amount_currency_excluded': + vals['amount_currency'] = 0.0 + else: + ctx = {} + if 'date' in vals: + ctx['date'] = vals['date'] + vals['amount_currency'] = account.company_id.currency_id.with_context(ctx).compute(amount, account.currency_id) if not ok: raise UserError(_('You cannot use this general account in this journal, check the tab \'Entry Controls\' on the related journal.')) diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py index 0bf832c133d..a7841f307c4 100644 --- a/addons/account/models/chart_template.py +++ b/addons/account/models/chart_template.py @@ -39,7 +39,7 @@ def migrate_tags_on_taxes(cr, registry): ('description', '=', tax_template.description) ]) if len(tax_id.ids) == 1: - tax_id.sudo().write({'tag_ids': [(6, 0, [tax_template.tag_ids.ids])]}) + tax_id.sudo().write({'tag_ids': [(6, 0, tax_template.tag_ids.ids)]}) # --------------------------------------------------------------- # Account Templates: Account, Tax, Tax Code and chart. + Wizard @@ -399,7 +399,9 @@ class AccountChartTemplate(models.Model): :rtype: dict """ self.ensure_one() - account_reconcile_models = self.env['account.reconcile.model.template'].search([]) + account_reconcile_models = self.env['account.reconcile.model.template'].search([ + ('account_id.chart_template_id', '=', self.id) + ]) for account_reconcile_model in account_reconcile_models: vals = { 'name': account_reconcile_model.name, diff --git a/addons/account/models/partner.py b/addons/account/models/partner.py index 7272a77345e..3ca9f8f10ee 100644 --- a/addons/account/models/partner.py +++ b/addons/account/models/partner.py @@ -445,5 +445,5 @@ class ResPartner(models.Model): ''' action = self.env.ref('account.action_invoice_refund_out_tree') result = action.read()[0] - result['domain'] = "[('id','in',[" + ','.join(map(str, self.ids)) + "])]" + result['domain'] = [('partner_id', 'in', self.ids)] return result diff --git a/addons/account/tests/test_account_supplier_invoice.py b/addons/account/tests/test_account_supplier_invoice.py index 671cfb2dcb0..232bd39ddc7 100644 --- a/addons/account/tests/test_account_supplier_invoice.py +++ b/addons/account/tests/test_account_supplier_invoice.py @@ -41,3 +41,65 @@ class TestAccountSupplierInvoice(AccountingTestCase): #I cancel the account move which is in posted state and verifies that it gives warning message with self.assertRaises(Warning): invoice.move_id.button_cancel() + + def test_supplier_invoice2(self): + tax_fixed = self.env['account.tax'].create({ + 'sequence': 10, + 'name': 'Tax 10.0 (Fixed)', + 'amount': 10.0, + 'amount_type': 'fixed', + 'include_base_amount': True, + }) + tax_percent_included_base_incl = self.env['account.tax'].create({ + 'sequence': 20, + 'name': 'Tax 50.0% (Percentage of Price Tax Included)', + 'amount': 50.0, + 'amount_type': 'division', + 'include_base_amount': True, + }) + tax_percentage = self.env['account.tax'].create({ + 'sequence': 30, + 'name': 'Tax 20.0% (Percentage of Price)', + 'amount': 20.0, + 'amount_type': 'percent', + 'include_base_amount': False, + }) + analytic_account = self.env['account.analytic.account'].create({ + 'name': 'test account', + }) + + # Should be changed by automatic on_change later + invoice_account = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_receivable').id)], limit=1).id + invoice_line_account = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_expenses').id)], limit=1).id + + invoice = self.env['account.invoice'].create({'partner_id': self.env.ref('base.res_partner_2').id, + 'account_id': invoice_account, + 'type': 'in_invoice', + }) + + invoice_line = self.env['account.invoice.line'].create({'product_id': self.env.ref('product.product_product_4').id, + 'quantity': 5.0, + 'price_unit': 100.0, + 'invoice_id': invoice.id, + 'name': 'product that cost 100', + 'account_id': invoice_line_account, + 'invoice_line_tax_ids': [(6, 0, [tax_fixed.id, tax_percent_included_base_incl.id, tax_percentage.id])], + 'account_analytic_id': analytic_account.id, + }) + invoice.compute_taxes() + + # check that Initially supplier bill state is "Draft" + self.assertTrue((invoice.state == 'draft'), "Initially vendor bill state is Draft") + + #change the state of invoice to open by clicking Validate button + invoice.signal_workflow('invoice_open') + + # Check if amount and corresponded base is correct for all tax scenarios given on a computational base + # Keep in mind that tax amount can be changed by the user at any time before validating (based on the invoice and tax laws applicable) + invoice_tax = invoice.tax_line_ids.sorted(key=lambda r: r.sequence) + self.assertEquals(invoice_tax.mapped('amount'), [50.0, 550.0, 220.0]) + self.assertEquals(invoice_tax.mapped('base'), [500.0, 550.0, 1100.0]) + + #I cancel the account move which is in posted state and verifies that it gives warning message + with self.assertRaises(Warning): + invoice.move_id.button_cancel() diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml index 23021226ace..033394aa7e7 100644 --- a/addons/account/views/account_view.xml +++ b/addons/account/views/account_view.xml @@ -556,7 +556,7 @@ <sheet> <div class="oe_button_box" name="button_box"> <button class="oe_stat_button" name="button_journal_entries" - string="Journal Items" type="object" + string="Journal Entries" type="object" attrs="{'invisible':[('move_line_ids','=',[])]}" icon="fa-bars"/> <field name="move_line_ids" invisible="1"/> </div> @@ -884,6 +884,7 @@ <field name="type"/> </group> <group> + <field name="include_initial_balance"/> </group> </group> <separator string="Description"/> diff --git a/addons/account/views/partner_view.xml b/addons/account/views/partner_view.xml index f4bb90a1236..025be73ab0e 100644 --- a/addons/account/views/partner_view.xml +++ b/addons/account/views/partner_view.xml @@ -102,7 +102,7 @@ icon="fa-pencil-square-o" name="open_partner_history" attrs="{'invisible': [('customer', '=', False)]}" - context="{'search_default_partner_id': active_id,'default_partner_id': active_id}"> + context="{'default_partner_id': active_id}"> <div class="o_form_field o_stat_info"> <span class="o_stat_value"><field name="total_invoiced" widget='monetary' options="{'currency_field': 'currency_id'}"/></span> <span class="o_stat_text">Invoiced</span> diff --git a/addons/account/views/web_planner_data.xml b/addons/account/views/web_planner_data.xml index ff796c2859b..de159a85cf0 100644 --- a/addons/account/views/web_planner_data.xml +++ b/addons/account/views/web_planner_data.xml @@ -447,7 +447,7 @@ <span class="fa fa-copy fa-4x"></span> <h3><strong>Contracts & Subscriptions</strong></h3> <p>Manage time & material contracts or fixed-price recurring subscriptions.</p> - <a t-att-href="prepare_backend_url('base.open_module_tree', 'form', 'account_analytic_analysis')" + <a t-att-href="prepare_backend_url('base.open_module_tree', 'form', 'sale_contract')" class="btn odoo_purple"> <span class="fa fa-arrow-circle-o-down"/> Install Now </a> diff --git a/addons/account_asset/models/account_asset.py b/addons/account_asset/models/account_asset.py index 7c5585b174b..942c54dfdf1 100644 --- a/addons/account_asset/models/account_asset.py +++ b/addons/account_asset/models/account_asset.py @@ -153,7 +153,7 @@ class AccountAssetAsset(models.Model): else: if self.method == 'linear': amount = amount_to_depr / (undone_dotation_number - len(posted_depreciation_line_ids)) - if self.prorata and self.category_id.type == 'purchase': + if self.prorata: amount = amount_to_depr / self.method_number if sequence == 1: days = (self.company_id.compute_fiscalyear_dates(depreciation_date)['date_to'] - depreciation_date).days + 1 @@ -174,7 +174,7 @@ class AccountAssetAsset(models.Model): while depreciation_date <= end_date: depreciation_date = date(depreciation_date.year, depreciation_date.month, depreciation_date.day) + relativedelta(months=+self.method_period) undone_dotation_number += 1 - if self.prorata and self.category_id.type == 'purchase': + if self.prorata: undone_dotation_number += 1 return undone_dotation_number @@ -378,7 +378,7 @@ class AccountAssetAsset(models.Model): @api.multi def _compute_entries(self, date, group_entries=False): - depreciation_ids = self.env['account.asset.depreciation.line'].with_context(depreciation_date=date).search([ + depreciation_ids = self.env['account.asset.depreciation.line'].search([ ('asset_id', 'in', self.ids), ('depreciation_date', '<=', date), ('move_check', '=', False)]) if group_entries: diff --git a/addons/account_cancel/views/account_views.xml b/addons/account_cancel/views/account_views.xml index 41e8371efe1..54478d06960 100644 --- a/addons/account_cancel/views/account_views.xml +++ b/addons/account_cancel/views/account_views.xml @@ -53,7 +53,8 @@ <attribute name="options">{'reload_on_button': true}</attribute> </field> <xpath expr="//field[@name='bank_account_id']" position="after"> - <button name="button_cancel_reconciliation" attrs="{'invisible': [('journal_entry_ids', '=', [])]}" string="Cancel" type="object" icon="fa-undo text-warning"/> + <field name="state" invisible="1"/> + <button name="button_cancel_reconciliation" attrs="{'invisible': ['|',('journal_entry_ids', '=', []), ('state', '=', 'confirm')]}" string="Cancel" type="object" icon="fa fa-undo text-warning"/> </xpath> </field> </record> diff --git a/addons/barcodes/static/src/js/form_view_barcode_handler.js b/addons/barcodes/static/src/js/form_view_barcode_handler.js index b00bce51c5e..7f2614f0ef1 100644 --- a/addons/barcodes/static/src/js/form_view_barcode_handler.js +++ b/addons/barcodes/static/src/js/form_view_barcode_handler.js @@ -7,6 +7,7 @@ var common = require('web.form_common'); var BarcodeEvents = require('barcodes.BarcodeEvents'); var BarcodeHandlerMixin = require('barcodes.BarcodeHandlerMixin'); var KanbanRecord = require('web_kanban.Record'); +var Dialog = require('web.Dialog'); var _t = core._t; @@ -77,22 +78,38 @@ var FormViewBarcodeHandler = common.AbstractField.extend(BarcodeHandlerMixin, { } else { var field = this.form_view.fields[this.m2x_field]; var view = field.viewmanager.active_view; + var $content = $('<div>').append($('<input>', {type: 'text', class: 'o_set_qty_input'})); if (this.last_scanned_barcode) { - var new_qty = window.prompt(_t('Set quantity'), character) || "0"; - new_qty = new_qty.replace(',', '.'); - var record = this._get_records(field).find(function(record) { - return record.get('product_barcode') === self.last_scanned_barcode; + this.dialog = new Dialog(this, { + title: _t('Set quantity'), + buttons: [{text: _t('Select'), classes: 'btn-primary', close: true, click: function () { + var new_qty = this.$content.find('.o_set_qty_input').val(); + var record = _.find(self._get_records(field), function (record) { + return record.get('product_barcode') === self.last_scanned_barcode; + }); + if (record) { + var values = {}; + values[self.quantity_field] = parseFloat(new_qty); + field.data_update(record.get('id'), values).then(function () { + view.controller.reload_record(record); + }); + } else { + self._display_no_last_scanned_warning(); + } + }}, {text: _t('Discard'), close: true}], + $content: $content, + }).open(); + // This line set the value of the key which triggered the _set_quantity in the input + this.dialog.$content.find('.o_set_qty_input').focus().val(character); + + var $selectBtn = this.dialog.$footer.find('.btn-primary'); + core.bus.on('keypress', this.dialog, function(event){ + if (event.which === 13) { + event.preventDefault(); + $selectBtn.click(); + } }); - if (record) { - var values = {}; - values[this.quantity_field] = parseFloat(new_qty); - field.data_update(record.get('id'), values).then(function () { - view.controller.reload_record(record); - }); - } else { - this._display_no_last_scanned_warning(); - } } else { this._display_no_last_scanned_warning(); } @@ -159,8 +176,13 @@ var FormViewBarcodeHandler = common.AbstractField.extend(BarcodeHandlerMixin, { }, _get_records: function(field) { - return field.viewmanager.active_view.controller.records || // tree view - field.viewmanager.active_view.controller.widgets; // kanban view + var active_view = field.viewmanager.active_view; + if (active_view.type === "kanban") { + return active_view.controller.widgets; + } else { + // tree view case + return active_view.controller.records.records; + } }, }); diff --git a/addons/base_action_rule/models/base_action_rule.py b/addons/base_action_rule/models/base_action_rule.py index 9c0151fba53..7f13bf9f3ef 100644 --- a/addons/base_action_rule/models/base_action_rule.py +++ b/addons/base_action_rule/models/base_action_rule.py @@ -198,7 +198,14 @@ class BaseActionRule(models.Model): # subscribe followers if self.act_followers and hasattr(records, 'message_subscribe'): - records.message_subscribe(self.act_followers.ids) + followers = self.env['mail.followers'].sudo().search( + [('res_model', '=', records._name), + ('res_id', 'in', records.ids), + ('partner_id', 'in', self.act_followers.ids), + ] + ) + if not len(followers) == len(self.act_followers): + records.message_subscribe(self.act_followers.ids) # execute server actions if self.server_action_ids: diff --git a/addons/base_import/models/base_import.py b/addons/base_import/models/base_import.py index 32dfcbd0cf9..2a497106735 100644 --- a/addons/base_import/models/base_import.py +++ b/addons/base_import/models/base_import.py @@ -14,6 +14,7 @@ import re from odoo import api, fields, models from odoo.tools.translate import _ from odoo.tools.mimetypes import guess_mimetype +from odoo.tools.misc import ustr from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT try: @@ -346,29 +347,37 @@ class Import(models.TransientModel): if not date_patterns: date_patterns = [pattern.replace('r', sep) for sep in separator for pattern in date_format] date_patterns.extend([p.replace('Y', 'y') for p in date_patterns]) + datetime_patterns = [options['datetime_format']] if options.get('datetime_format') else [] + if not datetime_patterns: + datetime_patterns = [pattern + ' %H:%M:%S' for pattern in date_patterns] + current_date_pattern = False - for date_pattern in date_patterns: - date_ok = True - datetime_ok = False - for val in preview_values: - if val == '': - continue - try: - dt.strptime(val, date_pattern) - except ValueError: - date_ok = False + current_datetime_pattern = False + + def check_patterns(patterns, preview_values): + for pattern in patterns: + match = True + for val in preview_values: + if not val: + continue try: - dt.strptime(val, date_pattern+' %H:%M:%S') - datetime_ok = True + dt.strptime(val, pattern) except ValueError: - datetime_ok = False + match = False break - if date_ok or datetime_ok: - current_date_pattern = date_pattern - break + if match: + return pattern + return False + + current_date_pattern = check_patterns(date_patterns, preview_values) if current_date_pattern: options['date_format'] = current_date_pattern - return ['date'] if date_ok else ['datetime'] + return ['date'] + + current_datetime_pattern = check_patterns(datetime_patterns, preview_values) + if current_datetime_pattern: + options['datetime_format'] = current_datetime_pattern + return ['datetime'] return ['text', 'char', 'datetime', 'selection', 'many2one', 'one2many', 'many2many', 'html'] @@ -598,14 +607,19 @@ class Import(models.TransientModel): # Parse date index = import_fields.index(name) dt = datetime.datetime - field_date_format = DEFAULT_SERVER_DATE_FORMAT if field['type'] == 'date' else DEFAULT_SERVER_DATETIME_FORMAT - if options.get('date_format', field_date_format) != field_date_format: - for line in data: + server_format = DEFAULT_SERVER_DATE_FORMAT if field['type'] == 'date' else DEFAULT_SERVER_DATETIME_FORMAT + + if options.get('%s_format' % field['type'], server_format) != server_format: + user_format = ustr(options.get('%s_format' % field['type'])).encode('utf-8') + for num, line in enumerate(data): if line[index]: try: - line[index] = dt.strftime(dt.strptime(line[index], options['date_format']), field_date_format) - except ValueError: - raise ValueError(_("Column %s contains incorrect values (value: %s does not match date format" % (name, line[index]))) + line[index] = dt.strftime(dt.strptime(ustr(line[index]).encode('utf-8'), user_format), server_format) + except ValueError, e: + raise ValueError(_("Column %s contains incorrect values. Error in line %d: %s") % (name, num + 1, ustr(e.message))) + except Exception, e: + raise ValueError(_("Error Parsing Date [%s:L%d]: %s") % (name, num + 1, ustr(e.message))) + elif field['type'] in ('float', 'monetary') and name in import_fields: # Parse float, sometimes float values from file have currency symbol or () to denote a negative value # We should be able to manage both case diff --git a/addons/base_import/static/src/js/base_import.js b/addons/base_import/static/src/js/base_import.js index 1901a2c180e..b3b905b36a5 100644 --- a/addons/base_import/static/src/js/base_import.js +++ b/addons/base_import/static/src/js/base_import.js @@ -119,11 +119,12 @@ var DataImport = Widget.extend(ControlPanelMixin, { opts: [ {name: 'encoding', label: _lt("Encoding:"), value: 'utf-8'}, {name: 'separator', label: _lt("Separator:"), value: ','}, - {name: 'quoting', label: _lt("Quoting:"), value: '"'} + {name: 'quoting', label: _lt("Text Delimiter:"), value: '"'} ], parse_opts: [ {name: 'date_format', label: _lt("Date Format:"), value: ''}, - {name: 'float_thousand_separator', label: _lt("Thousand Separator:"), value: ','}, + {name: 'datetime_format', label: _lt("Datetime Format:"), value: ''}, + {name: 'float_thousand_separator', label: _lt("Thousands Separator:"), value: ','}, {name: 'float_decimal_separator', label: _lt("Decimal Separator:"), value: '.'} ], events: { @@ -295,8 +296,8 @@ var DataImport = Widget.extend(ControlPanelMixin, { self.$('input.oe_import_' + opt.name).val(); }); _(this.parse_opts).each(function (opt) { - if (opt.name === 'date_format') { - options[opt.name] = time.moment_to_strftime_format(self.$('input.oe_import_date_format').val()); + if (opt.name === 'date_format' || opt.name === 'datetime_format') { + options[opt.name] = time.moment_to_strftime_format(self.$('input.oe_import_' + opt.name).val()); } else { options[opt.name] = self.$('input.oe_import_' + opt.name).val(); @@ -320,6 +321,7 @@ var DataImport = Widget.extend(ControlPanelMixin, { .prop('disabled', true); if (!this.$('input.oe_import_file').val()) { return this['settings_changed'](); } this.$('.oe_import_date_format').val(''); + this.$('.oe_import_datetime_format').val(''); this.$el.removeClass('oe_import_preview oe_import_error'); var import_toggle = false; @@ -375,6 +377,7 @@ var DataImport = Widget.extend(ControlPanelMixin, { } this.$('.oe_import_date_format').val(time.strftime_to_moment_format(result.options.date_format)); + this.$('.oe_import_datetime_format').val(time.strftime_to_moment_format(result.options.datetime_format)); this.$('.oe_import_float_thousand_separator').val(result.options.float_thousand_separator).change(); this.$('.oe_import_float_decimal_separator').val(result.options.float_decimal_separator).change(); if (result.debug === false){ diff --git a/addons/base_import/tests/test_base_import.py b/addons/base_import/tests/test_base_import.py index 9fcadd27e5a..68a562971c4 100644 --- a/addons/base_import/tests/test_base_import.py +++ b/addons/base_import/tests/test_base_import.py @@ -379,6 +379,31 @@ class test_convert_import_data(TransactionCase): ['qux', '5', '6'], ]) + def test_date_fields(self): + import_wizard = self.env['base_import.import'].create({ + 'res_model': 'res.partner', + 'file': 'name,date,create_date\n' + '"foo","2013年07月18日","2016-10-12 06:06"\n', + 'file_type': 'text/csv' + + }) + + results = import_wizard.do( + ['name', 'date', 'create_date'], + { + 'date_format': '%Y年%m月%d日', + 'datetime_format': '%Y-%m-%d %H:%M', + 'quoting': '"', + 'separator': ',', + 'headers': True + } + ) + + # if results empty, no errors + self.assertItemsEqual(results, []) + + + def test_filtered(self): """ If ``False`` is provided as field mapping for a column, that column should be removed from importable data diff --git a/addons/calendar/models/calendar.py b/addons/calendar/models/calendar.py index 01b5a2c714d..24b0aa71b33 100644 --- a/addons/calendar/models/calendar.py +++ b/addons/calendar/models/calendar.py @@ -1372,7 +1372,7 @@ class Meeting(models.Model): if not self._context.get('dont_notify'): if len(meeting.alarm_ids) > 0 or values.get('alarm_ids'): partners_to_notify = meeting.partner_ids.ids - event_attendees_changes = attendees_create and attendees_create[real_ids[0]] + event_attendees_changes = attendees_create and real_ids and attendees_create[real_ids[0]] if event_attendees_changes: partners_to_notify.append(event_attendees_changes['removed_partners'].ids) self.env['calendar.alarm_manager'].notify_next_alarm(partners_to_notify) diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml index 2abb85b6ea8..f3071eb0a6c 100644 --- a/addons/crm/views/crm_lead_views.xml +++ b/addons/crm/views/crm_lead_views.xml @@ -526,7 +526,7 @@ <field name="source_id" invisible="1"/> <field name="stage_id"/> <field name="planned_revenue" sum="Expected Revenues"/> - <field name="probability" /> + <field name="probability" avg="Avg. of Probability"/> <field name="team_id"/> <field name="user_id"/> <field name="referred" invisible="1"/> diff --git a/addons/delivery/views/delivery_view.xml b/addons/delivery/views/delivery_view.xml index 271b86e1bae..7758b26a914 100644 --- a/addons/delivery/views/delivery_view.xml +++ b/addons/delivery/views/delivery_view.xml @@ -202,7 +202,7 @@ <label for="weight" string="Weight"/> <div> <field name="weight" class="oe_inline"/> - <field name="weight_uom_id" nolabel="1" class="oe_inline"/> + <field name="weight_uom_id" nolabel="1" class="oe_inline" style="margin-left:5px"/> </div> <field name="shipping_weight"/> <field name="number_of_packages" attrs="{'readonly': [('state', '=', 'done')]}"/> diff --git a/addons/hr_attendance/static/src/js/greeting_message.js b/addons/hr_attendance/static/src/js/greeting_message.js index 113012dbb18..c0f120983ad 100644 --- a/addons/hr_attendance/static/src/js/greeting_message.js +++ b/addons/hr_attendance/static/src/js/greeting_message.js @@ -1,6 +1,8 @@ odoo.define('hr_attendance.greeting_message', function (require) { "use strict"; +var BarcodeHandlerMixin = require('barcodes.BarcodeHandlerMixin'); + var core = require('web.core'); var Model = require('web.Model'); var Widget = require('web.Widget'); @@ -8,27 +10,39 @@ var Widget = require('web.Widget'); var _t = core._t; -var GreetingMessage = Widget.extend({ +var GreetingMessage = Widget.extend(BarcodeHandlerMixin, { template: 'HrAttendanceGreetingMessage', events: { - "click .o_hr_attendance_button_dismiss": function () { this.do_action(this.next_action, {clear_breadcrumbs: true}); }, + "click .o_hr_attendance_button_dismiss": function() { this.do_action(this.next_action, {clear_breadcrumbs: true}); }, }, init: function(parent, action) { - this._super.apply(this, arguments); var self = this; - var default_action = { type: "ir.actions.client", name: 'Attendances', tag: 'hr_attendance_my_attendances', params: {} }; - if(!action.attendance){ - self.session.user_has_group('hr_attendance.group_hr_attendance_user').then(function(has_group){ - if(has_group){ - self.next_action = { type: "ir.actions.client", name: 'Attendances', tag: 'hr_attendance_kiosk_mode', target: 'fullscreen', params: {} }; + this._super.apply(this, arguments); + BarcodeHandlerMixin.init.apply(this, arguments); + + // if no correct action given (due to an erroneous back or refresh from the browser), we set the dismiss button to return + // to the (likely) appropriate menu, according to the user access rights + if(!action.attendance) { + this.stop_listening(); + this.session.user_has_group('hr_attendance.group_hr_attendance_user').then(function(has_group) { + if(has_group) { + self.next_action = 'hr_attendance.hr_attendance_action_kiosk_mode'; + } else { + self.next_action = 'hr_attendance.hr_attendance_action_my_attendances'; } }); return; } - this.next_action = action.next_action || default_action; + + this.next_action = action.next_action || 'hr_attendance.hr_attendance_action_my_attendances'; + // no listening to barcode scans if we aren't coming from the kiosk mode (and thus not going back to it with next_action) + if (this.next_action != 'hr_attendance.hr_attendance_action_kiosk_mode' && this.next_action.tag != 'hr_attendance_kiosk_mode') { + this.stop_listening(); + } this.attendance = action.attendance; + // check in/out times displayed in the greeting message template. this.attendance.check_in_time = (new Date((new Date(this.attendance.check_in)).valueOf() - (new Date()).getTimezoneOffset()*60*1000)).toTimeString().slice(0,8); this.attendance.check_out_time = this.attendance.check_out && (new Date((new Date(this.attendance.check_out)).valueOf() - (new Date()).getTimezoneOffset()*60*1000)).toTimeString().slice(0,8); this.previous_attendance_change_date = action.previous_attendance_change_date; @@ -36,48 +50,42 @@ var GreetingMessage = Widget.extend({ }, start: function() { - if (!this.attendance){ - return; - } - if (this.attendance.check_out) { - this.farewell_message(); - } else { - this.welcome_message(); + if (this.attendance) { + this.attendance.check_out ? this.farewell_message() : this.welcome_message(); } }, welcome_message: function() { var self = this; var now = new Date((new Date(this.attendance.check_in)).valueOf() - (new Date()).getTimezoneOffset()*60*1000); - self.return_to_main_menu = setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000); - - if(now.getHours()<5){ - self.$('.o_hr_attendance_message_message').append(_t("Good night")); - } else if(now.getHours() < 12){ - if(now.getHours() < 8 && Math.random() < .3){ - if(Math.random()<.75){ - self.$('.o_hr_attendance_message_message').append(_t("The early bird catches the worm")); + this.return_to_main_menu = setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000); + + if (now.getHours() < 5) { + this.$('.o_hr_attendance_message_message').append(_t("Good night")); + } else if (now.getHours() < 12) { + if (now.getHours() < 8 && Math.random() < 0.3) { + if (Math.random() < 0.75) { + this.$('.o_hr_attendance_message_message').append(_t("The early bird catches the worm")); } else { - self.$('.o_hr_attendance_message_message').append(_t("First come, first served")); + this.$('.o_hr_attendance_message_message').append(_t("First come, first served")); } - } else { - self.$('.o_hr_attendance_message_message').append(_t("Good morning")); + this.$('.o_hr_attendance_message_message').append(_t("Good morning")); } - } else if(now.getHours()<17){ - self.$('.o_hr_attendance_message_message').append(_t("Good afternoon")); - } else if(now.getHours()<23){ - self.$('.o_hr_attendance_message_message').append(_t("Good evening")); + } else if (now.getHours() < 17){ + this.$('.o_hr_attendance_message_message').append(_t("Good afternoon")); + } else if (now.getHours() < 23){ + this.$('.o_hr_attendance_message_message').append(_t("Good evening")); } else { - self.$('.o_hr_attendance_message_message').append(_t("Good night")); + this.$('.o_hr_attendance_message_message').append(_t("Good night")); } if(this.previous_attendance_change_date){ var last_check_out_date = new Date((new Date(this.previous_attendance_change_date)).valueOf() - (new Date()).getTimezoneOffset()*60*1000); if(now.valueOf() - last_check_out_date.valueOf() > 1000*60*60*24*7){ - self.$('.o_hr_attendance_random_message').html(_t("Glad to have you back, it's been a while!")); + this.$('.o_hr_attendance_random_message').html(_t("Glad to have you back, it's been a while!")); } else { - if(Math.random() < .02){ - self.$('.o_hr_attendance_random_message').html(_t("If a job is worth doing, it is worth doing well!")); + if(Math.random() < 0.02){ + this.$('.o_hr_attendance_random_message').html(_t("If a job is worth doing, it is worth doing well!")); } } } @@ -86,38 +94,55 @@ var GreetingMessage = Widget.extend({ farewell_message: function() { var self = this; var now = new Date((new Date(this.attendance.check_out)).valueOf() - (new Date()).getTimezoneOffset()*60*1000); - self.return_to_main_menu = setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000); + this.return_to_main_menu = setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000); if(this.previous_attendance_change_date){ var last_check_in_date = new Date((new Date(this.previous_attendance_change_date)).valueOf() - (new Date()).getTimezoneOffset()*60*1000); if(now.valueOf() - last_check_in_date.valueOf() > 1000*60*60*12){ - self.$('.o_hr_attendance_warning_message').append(_t("Warning! Last check in was over 12 hours ago.<br/>If this isn't right, please contact Human Resources.")); - clearTimeout(self.return_to_main_menu); + this.$('.o_hr_attendance_warning_message').append(_t("Warning! Last check in was over 12 hours ago.<br/>If this isn't right, please contact Human Resources.")); + clearTimeout(this.return_to_main_menu); + this.stop_listening(); } else if(now.valueOf() - last_check_in_date.valueOf() > 1000*60*60*8){ - self.$('.o_hr_attendance_random_message').html(_t("Another good day's work! See you soon!")); + this.$('.o_hr_attendance_random_message').html(_t("Another good day's work! See you soon!")); } } - if(now.getHours()<12){ - self.$('.o_hr_attendance_message_message').append(_t("Have a good day!")); - } else if(now.getHours()<14){ - self.$('.o_hr_attendance_message_message').append(_t("Have a nice lunch!")); - if(Math.random() < 0.05){ - self.$('.o_hr_attendance_random_message').html(_t("Eat breakfast as a king, lunch as a merchant and supper as a beggar")); - } else if(Math.random() < 0.06){ - self.$('.o_hr_attendance_random_message').html(_t("An apple a day keeps the doctor away")); + if (now.getHours() < 12) { + this.$('.o_hr_attendance_message_message').append(_t("Have a good day!")); + } else if (now.getHours() < 14) { + this.$('.o_hr_attendance_message_message').append(_t("Have a nice lunch!")); + if (Math.random() < 0.05) { + this.$('.o_hr_attendance_random_message').html(_t("Eat breakfast as a king, lunch as a merchant and supper as a beggar")); + } else if (Math.random() < 0.06) { + this.$('.o_hr_attendance_random_message').html(_t("An apple a day keeps the doctor away")); } - } else if(now.getHours()<17){ - self.$('.o_hr_attendance_message_message').append(_t("Have a good afternoon")); + } else if (now.getHours() < 17) { + this.$('.o_hr_attendance_message_message').append(_t("Have a good afternoon")); } else { - if(now.getHours()<18 && Math.random()<0.2){ - self.$('.o_hr_attendance_message_message').append(_t("Early to bed and early to rise, makes a man healthy, wealthy and wise")); + if (now.getHours() < 18 && Math.random() < 0.2) { + this.$('.o_hr_attendance_message_message').append(_t("Early to bed and early to rise, makes a man healthy, wealthy and wise")); } else { - self.$('.o_hr_attendance_message_message').append(_t("Have a good evening")); + this.$('.o_hr_attendance_message_message').append(_t("Have a good evening")); } } }, + on_barcode_scanned: function(barcode) { + var self = this; + if (this.return_to_main_menu) { // in case of multiple scans in the greeting message view, delete the timer, a new one will be created. + clearTimeout(this.return_to_main_menu); + } + var hr_employee = new Model('hr.employee'); + hr_employee.call('attendance_scan', [barcode, ]) + .then(function (result) { + if (result.action) { + self.do_action(result.action); + } else if (result.warning) { + self.do_warn(result.warning); + } + }); + }, + destroy: function () { clearTimeout(this.return_to_main_menu); this._super.apply(this, arguments); diff --git a/addons/hr_attendance/static/src/js/kiosk_confirm.js b/addons/hr_attendance/static/src/js/kiosk_confirm.js index 60e75b73328..f8d113d27d6 100644 --- a/addons/hr_attendance/static/src/js/kiosk_confirm.js +++ b/addons/hr_attendance/static/src/js/kiosk_confirm.js @@ -55,7 +55,7 @@ var KioskConfirm = Widget.extend({ init: function (parent, action) { this._super.apply(this, arguments); - this.next_action = { type: "ir.actions.client", name: "Attendances", tag: 'hr_attendance_kiosk_mode', target: "fullscreen", params: {} }; + this.next_action = 'hr_attendance.hr_attendance_action_kiosk_mode'; this.employee_id = action.employee_id; this.employee_name = action.employee_name; this.employee_state = action.employee_state; diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index f8dc372f39e..fa4c6498366 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -136,6 +136,7 @@ class HrExpense(models.Model): 'product_id': line.get('product_id'), 'product_uom_id': line.get('uom_id'), 'analytic_account_id': line.get('analytic_account_id'), + 'payment_id': line.get('payment_id'), } @api.multi @@ -185,7 +186,10 @@ class HrExpense(models.Model): 'journal_id': journal.id, 'company_id': self.env.user.company_id.id, 'date': acc_date, - 'ref': ref + 'ref': ref, + # force the name to the default value, to avoid an eventual 'default_name' in the context + # to set it to '' which cause no number to be given to the account.move when posted. + 'name': '/', }) for expense in expense_list: company_currency = expense.company_id.currency_id @@ -194,11 +198,29 @@ class HrExpense(models.Model): move_lines = expense._move_line_get() #create one more move line, a counterline for the total on payable account + payment_id = False total, total_currency, move_lines = expense._compute_expense_totals(company_currency, move_lines, acc_date) if expense.payment_mode == 'company_account': if not expense.sheet_id.bank_journal_id.default_credit_account_id: raise UserError(_("No credit account found for the %s journal, please configure one.") % (expense.sheet_id.bank_journal_id.name)) emp_account = expense.sheet_id.bank_journal_id.default_credit_account_id.id + journal = expense.bank_journal_id + #create payment + payment_methods = (total < 0) and journal.outbound_payment_method_ids or journal.inbound_payment_method_ids + journal_currency = journal.currency_id or journal.company_id.currency_id + payment = self.env['account.payment'].create({ + 'payment_method_id': payment_methods and payment_methods[0].id or False, + 'payment_type': total < 0 and 'outbound' or 'inbound', + 'partner_id': expense.employee_id.address_home_id.commercial_partner_id.id, + 'partner_type': 'supplier', + 'journal_id': journal.id, + 'payment_date': expense.date, + 'state': 'reconciled', + 'currency_id': diff_currency_p and expense.currency_id.id or journal_currency.id, + 'amount': diff_currency_p and abs(total_currency) or abs(total), + 'name': expense.name, + }) + payment_id = payment.id else: if not expense.employee_id.address_home_id: raise UserError(_("No Home Address found for the employee %s, please configure one.") % (expense.employee_id.name)) @@ -212,6 +234,7 @@ class HrExpense(models.Model): 'date_maturity': acc_date, 'amount_currency': diff_currency_p and total_currency or False, 'currency_id': diff_currency_p and expense.currency_id.id or False, + 'payment_id': payment_id, }) #convert eml into an osv-valid format diff --git a/addons/hr_holidays/models/hr.py b/addons/hr_holidays/models/hr.py index fdec5c0ad5e..fe52be16430 100644 --- a/addons/hr_holidays/models/hr.py +++ b/addons/hr_holidays/models/hr.py @@ -213,5 +213,5 @@ class Employee(models.Model): ('date_from', '<=', today_end), ('date_to', '>=', today_start), ('type', '=', 'remove') - ], ['employee_id']) + ]) return [('id', 'in', holidays.mapped('employee_id').ids)] diff --git a/addons/hw_escpos/controllers/main.py b/addons/hw_escpos/controllers/main.py index 6caad2b3aaf..fbb9b58f876 100644 --- a/addons/hw_escpos/controllers/main.py +++ b/addons/hw_escpos/controllers/main.py @@ -77,10 +77,15 @@ class EscposDriver(Thread): printers = usb.core.find(find_all=True, idVendor=0x0519) for printer in printers: + try: + description = usb.util.get_string(printer, 256, printer.iManufacturer) + " " + usb.util.get_string(printer, 256, printer.iProduct) + except Exception as e: + _logger.error("Can not get printer description: %s" % (e.message or repr(e))) + description = 'Unknown printer' connected.append({ 'vendor': printer.idVendor, 'product': printer.idProduct, - 'name': usb.util.get_string(printer, 256, printer.iManufacturer) + " " + usb.util.get_string(printer, 256, printer.iProduct) + 'name': description }) return connected diff --git a/addons/l10n_in/data/account_tax_template_data.xml b/addons/l10n_in/data/account_tax_template_data.xml index 43251bc2ad8..85fb8c4c6d6 100644 --- a/addons/l10n_in/data/account_tax_template_data.xml +++ b/addons/l10n_in/data/account_tax_template_data.xml @@ -462,7 +462,7 @@ <field name="account_id" ref="p11235"/> <field name="refund_account_id" ref="p20011"/> <field name="chart_template_id" ref="indian_chart_template_standard"/> - <field name="tag_ids" eval="[(6,0,[ref('st_tag_in_70'), ref('st_tag_in_21')])]"/> + <field name="tag_ids" eval="[(6, 0, [ref('st_tag_in_21'), ref('st_tag_in_66')])]"/> </record> <record id="input_tax_12" model="account.tax.template"> <field name="name">(standard)Input Sales Tax @ 12%</field> @@ -474,7 +474,7 @@ <field name="account_id" ref="p11235"/> <field name="refund_account_id" ref="p20011"/> <field name="chart_template_id" ref="indian_chart_template_standard"/> - <field name="tag_ids" eval="[(6,0,[ref('st_tag_in_56')])]"/> + <field name="tag_ids" eval="[(6, 0, [ref('st_tag_in_18'), ref('st_tag_in_63')])]"/> </record> <record id="input_tax_4" model="account.tax.template"> <field name="name">(standard)Input Sales Tax @ 4%</field> @@ -486,7 +486,7 @@ <field name="account_id" ref="p11235"/> <field name="refund_account_id" ref="p20011"/> <field name="chart_template_id" ref="indian_chart_template_standard"/> - <field name="tag_ids" eval="[(6,0,[ref('st_tag_in_15'), ref('st_tag_in_70')])]"/> + <field name="tag_ids" eval="[(6, 0, [ref('st_tag_in_15'), ref('st_tag_in_60')])]"/> </record> <record id="add_input_vat_1" model="account.tax.template"> <field name="name">(standard)Additional Input VAT @ 1%</field> @@ -605,7 +605,7 @@ <field name="account_id" ref="p112341"/> <field name="refund_account_id" ref="p20011"/> <field name="chart_template_id" ref="indian_chart_template_standard"/> - <field name="tag_ids" eval="[(6,0,[ref('st_tag_in_25')])]"/> + <field name="tag_ids" eval="[(6, 0, [ref('st_tag_in_25'), ref('st_tag_in_63')])]"/> </record> <record id="sale_edu_cess_service_tax_2" model="account.tax.template"> <field name="name">(standard)Input Education Cess @ 2% On Service Tax</field> diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 599cec0493c..a733f2f329b 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -1109,37 +1109,23 @@ class MailThread(models.AbstractModel): reply_match = False if reply_match: - compat_mode = False msg_references = tools.mail_header_msgid_re.findall(thread_references) mail_messages = MailMessage.sudo().search([('message_id', 'in', msg_references)], limit=1) - # message is a reply to an existing thread (6.1 compatibility) - # do not match forwarded emails from another OpenERP system (thread_id collision!) - if not mail_messages and local_hostname == reply_hostname and reply_thread_id and reply_model in self.env: - mail_messages = MailMessage.search([ - ('message_id', '=', False), - ('model', '=', reply_model), - ('res_id', '=', reply_thread_id)]) - compat_mode = True - if mail_messages: model, thread_id = mail_messages.model, mail_messages.res_id if not reply_private: # TDE note: not sure why private mode as no alias search, copying existing behavior dest_aliases = Alias.search([('alias_name', 'in', rcpt_tos_localparts)], limit=1) - # TDE Note: compat mode = without context key, why ? because route = self.message_route_verify( message, message_dict, (model, thread_id, custom_values, self._uid, dest_aliases), update_author=True, assert_model=reply_private, create_fallback=True, allow_private=reply_private, drop_alias=True) if route: - # TDE Note: compat mode: parent is invalid for a compat-reply - # message_dict.pop('parent_id', None) - # TDE note: add compat mode for compat mode in debug _logger.info( - 'Routing mail from %s to %s with Message-Id %s%s: direct reply to msg: model: %s, thread_id: %s, custom_values: %s, uid: %s', - email_from, email_to, message_id, compat_mode and ' (compat mode)' or '', model, thread_id, custom_values, self._uid) + 'Routing mail from %s to %s with Message-Id %s: direct reply to msg: model: %s, thread_id: %s, custom_values: %s, uid: %s', + email_from, email_to, message_id, model, thread_id, custom_values, self._uid) return [route] elif route is False: return [] diff --git a/addons/mail/tests/test_mail_gateway.py b/addons/mail/tests/test_mail_gateway.py index 707e316bca5..b729a105b98 100644 --- a/addons/mail/tests/test_mail_gateway.py +++ b/addons/mail/tests/test_mail_gateway.py @@ -580,25 +580,29 @@ class TestMailgateway(TestMail): msg_id='<1198923581.41972151344608186802.JavaMail.diff1@agrolait.com>') # when 6.1 messages are present, compat mode is available + # Odoo 10 update: compat mode has been removed and should not work anymore self.fake_email.write({'message_id': False}) # Do: compat mode accepts partial-matching emails - frog_groups = self.format_and_process( + self.assertRaises( + ValueError, + self.format_and_process, MAIL_TEMPLATE, email_from='other5@gmail.com', msg_id='<1.2.JavaMail.new@agrolait.com>', to='noone@example.com>', subject='spam', extra='In-Reply-To: <12321321-openerp-%d-mail.channel@%s>' % (self.group_public.id, socket.gethostname())) # 3''. 6.1 compat mode should not work if hostname does not match! + # Odoo 10 update: compat mode has been removed and should not work anymore and does not depend from hostname self.assertRaises(ValueError, self.format_and_process, MAIL_TEMPLATE, email_from='other5@gmail.com', msg_id='<1.3.JavaMail.new@agrolait.com>', to='noone@example.com>', subject='spam', - extra='In-Reply-To: <12321321-openerp-%d-mail.channel@neighbor.com>' % frog_groups.id) + extra='In-Reply-To: <12321321-openerp-%d-mail.channel@neighbor.com>' % self.group_public.id) # Test created messages - self.assertEqual(len(self.group_public.message_ids), 2, 'message_process: group should contain 6 messages') - self.assertEqual(len(self.group_public.message_ids[0].child_ids), 0, 'message_process: msg1 should not have children') + self.assertEqual(len(self.group_public.message_ids), 1) + self.assertEqual(len(self.group_public.message_ids[0].child_ids), 0) @mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models') def test_message_process_duplicate(self): diff --git a/addons/point_of_sale/static/src/js/db.js b/addons/point_of_sale/static/src/js/db.js index 62bf848aa44..c2b3fad52f0 100644 --- a/addons/point_of_sale/static/src/js/db.js +++ b/addons/point_of_sale/static/src/js/db.js @@ -410,6 +410,10 @@ var PosDB = core.Class.extend({ } } + // Only necessary when we store a new, validated order. Orders + // that where already stored should already have been removed. + this.remove_unpaid_order(order); + orders.push({id: order_id, data: order}); this.save('orders',orders); return order_id; diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index 68476c9f5e8..164fd5f7040 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -1895,7 +1895,7 @@ var PaymentScreenWidget = ScreenWidget.extend({ } // if the change is too large, it's probably an input error, make the user confirm. - if (!force_validation && (order.get_total_with_tax() * 1000 < order.get_total_paid())) { + if (!force_validation && order.get_total_with_tax() > 0 && (order.get_total_with_tax() * 1000 < order.get_total_paid())) { this.gui.show_popup('confirm',{ title: _t('Please Confirm Large Amount'), body: _t('Are you sure that the customer wants to pay') + diff --git a/addons/pos_cache/models/pos_cache.py b/addons/pos_cache/models/pos_cache.py index c082a8cfe94..fb0fb20641e 100644 --- a/addons/pos_cache/models/pos_cache.py +++ b/addons/pos_cache/models/pos_cache.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. - +import base64 from ast import literal_eval from odoo import models, fields, api @@ -29,7 +29,7 @@ class pos_cache(models.Model): prod_ctx = prod_ctx.sudo(self.compute_user_id.id) res = prod_ctx.read(self.get_product_fields()) datas = { - 'cache': cPickle.dumps(res), + 'cache': base64.encodestring(cPickle.dumps(res)), } self.write(datas) @@ -49,7 +49,8 @@ class pos_cache(models.Model): self.product_fields = str(fields) self.refresh_cache() - return cPickle.loads(self.cache) + cache = base64.decodestring(self.cache) + return cPickle.loads(cache) class pos_config(models.Model): diff --git a/addons/purchase/models/account_invoice.py b/addons/purchase/models/account_invoice.py index fdfdf7c49e1..dd351d2d564 100644 --- a/addons/purchase/models/account_invoice.py +++ b/addons/purchase/models/account_invoice.py @@ -30,6 +30,35 @@ class AccountInvoice(models.Model): ]} return result + def _prepare_invoice_line_from_po_line(self, line): + if line.product_id.purchase_method == 'purchase': + qty = line.product_qty - line.qty_invoiced + else: + qty = line.qty_received - line.qty_invoiced + if float_compare(qty, 0.0, precision_rounding=line.product_uom.rounding) <= 0: + qty = 0.0 + taxes = line.taxes_id + invoice_line_tax_ids = self.purchase_id.fiscal_position_id.map_tax(taxes) + invoice_line = self.env['account.invoice.line'] + data = { + 'purchase_line_id': line.id, + 'name': self.purchase_id.name+': '+line.name, + 'origin': self.purchase_id.origin, + 'uom_id': line.product_uom.id, + 'product_id': line.product_id.id, + 'account_id': invoice_line.with_context({'journal_id': self.journal_id.id, 'type': 'in_invoice'})._default_account(), + 'price_unit': line.order_id.currency_id.compute(line.price_unit, self.currency_id, round=False), + 'quantity': qty, + 'discount': 0.0, + 'account_analytic_id': line.account_analytic_id.id, + 'analytic_tag_ids': line.analytic_tag_ids.ids, + 'invoice_line_tax_ids': invoice_line_tax_ids.ids + } + account = invoice_line.get_invoice_line_account('in_invoice', line.product_id, self.purchase_id.fiscal_position_id, self.env.user.company_id) + if account: + data['account_id'] = account.id + return data + # Load all unsold PO lines @api.onchange('purchase_id') def purchase_order_change(self): @@ -43,31 +72,7 @@ class AccountInvoice(models.Model): # Load a PO line only once if line in self.invoice_line_ids.mapped('purchase_line_id'): continue - if line.product_id.purchase_method == 'purchase': - qty = line.product_qty - line.qty_invoiced - else: - qty = line.qty_received - line.qty_invoiced - if float_compare(qty, 0.0, precision_rounding=line.product_uom.rounding) <= 0: - qty = 0.0 - taxes = line.taxes_id - invoice_line_tax_ids = self.purchase_id.fiscal_position_id.map_tax(taxes) - data = { - 'purchase_line_id': line.id, - 'name': self.purchase_id.name+': '+line.name, - 'origin': self.purchase_id.origin, - 'uom_id': line.product_uom.id, - 'product_id': line.product_id.id, - 'account_id': self.env['account.invoice.line'].with_context({'journal_id': self.journal_id.id, 'type': 'in_invoice'})._default_account(), - 'price_unit': line.order_id.currency_id.compute(line.price_unit, self.currency_id, round=False), - 'quantity': qty, - 'discount': 0.0, - 'account_analytic_id': line.account_analytic_id.id, - 'analytic_tag_ids': line.analytic_tag_ids.ids, - 'invoice_line_tax_ids': invoice_line_tax_ids.ids - } - account = new_lines.get_invoice_line_account('in_invoice', line.product_id, self.purchase_id.fiscal_position_id, self.env.user.company_id) - if account: - data['account_id'] = account.id + data = self._prepare_invoice_line_from_po_line(line) new_line = new_lines.new(data) new_line._set_additional_fields(self) new_lines += new_line diff --git a/addons/purchase_requisition/models/purchase_requisition.py b/addons/purchase_requisition/models/purchase_requisition.py index 0731b4456d5..86ad0cc1ca4 100644 --- a/addons/purchase_requisition/models/purchase_requisition.py +++ b/addons/purchase_requisition/models/purchase_requisition.py @@ -194,16 +194,16 @@ class PurchaseOrder(models.Model): taxes_ids = line.product_id.supplier_taxes_id.filtered(lambda tax: tax.company_id == requisition.company_id).ids # Compute quantity and price_unit - if requisition.type_id.quantity_copy != 'copy': - product_qty = 0 - price_unit = line.price_unit - elif line.product_uom_id != line.product_id.uom_po_id: + if line.product_uom_id != line.product_id.uom_po_id: product_qty = line.product_uom_id._compute_quantity(line.product_qty, line.product_id.uom_po_id) price_unit = line.product_uom_id._compute_price(line.price_unit, line.product_id.uom_po_id) else: product_qty = line.product_qty price_unit = line.price_unit + if requisition.type_id.quantity_copy != 'copy': + product_qty = 0 + # Compute price_unit in appropriate currency if requisition.company_id.currency_id != currency: price_unit = requisition.company_id.currency_id.compute(price_unit, currency) @@ -258,6 +258,24 @@ class PurchaseOrder(models.Model): return result +class PurchaseOrderLine(models.Model): + _inherit = "purchase.order.line" + + @api.onchange('product_qty', 'product_uom') + def _onchange_quantity(self): + res = super(PurchaseOrderLine, self)._onchange_quantity() + if self.order_id.requisition_id: + for line in self.order_id.requisition_id.line_ids: + if line.product_id == self.product_id: + if line.product_uom_id != self.product_uom: + self.price_unit = self.env['product.uom']._compute_price( + line.product_uom_id.id, line.price_unit, to_uom_id=self.product_uom.id) + else: + self.price_unit = line.price_unit + break + return res + + class ProductTemplate(models.Model): _inherit = 'product.template' diff --git a/addons/rating_project/models/project.py b/addons/rating_project/models/project.py index a82ca8cc1b9..345576768ae 100644 --- a/addons/rating_project/models/project.py +++ b/addons/rating_project/models/project.py @@ -10,10 +10,13 @@ class ProjectTaskType(models.Model): _inherit = 'project.task.type' + def _default_domain_rating_template_id(self): + return [('model', '=', 'project.task')] + rating_template_id = fields.Many2one( 'mail.template', string='Rating Email Template', - domain=[('model', '=', 'project.task')], + domain=lambda self: self._default_domain_rating_template_id(), help="Select an email template. An email will be sent to the customer when the task reach this step.") auto_validation_kanban_state = fields.Boolean('Automatic kanban status', default=False, help="Automatically modify the kanban state when the customer reply to the feedback for this stage.\n" diff --git a/addons/rating_project_issue/models/project_issue.py b/addons/rating_project_issue/models/project_issue.py index f771f328f84..5b2b126cad2 100644 --- a/addons/rating_project_issue/models/project_issue.py +++ b/addons/rating_project_issue/models/project_issue.py @@ -28,6 +28,16 @@ class ProjectIssue(models.Model): return super(ProjectIssue, self).rating_apply(rate, token=token, feedback=feedback, subtype="rating_project_issue.mt_issue_rating") + +class Stage(models.Model): + + _inherit = ['project.task.type'] + + def _default_domain_rating_template_id(self): + domain = super(Stage, self)._default_domain_rating_template_id() + return ['|'] + domain + [('model', '=', 'project.issue')] + + class Project(models.Model): _inherit = "project.project" diff --git a/addons/sale/wizard/sale_make_invoice_advance.py b/addons/sale/wizard/sale_make_invoice_advance.py index e3dbc4ed176..84ab9490389 100644 --- a/addons/sale/wizard/sale_make_invoice_advance.py +++ b/addons/sale/wizard/sale_make_invoice_advance.py @@ -83,6 +83,10 @@ class SaleAdvancePaymentInv(models.TransientModel): else: amount = self.amount name = _('Down Payment') + if order.fiscal_position_id and self.product_id.taxes_id: + tax_ids = order.fiscal_position_id.map_tax(self.product_id.taxes_id).ids + else: + tax_ids = self.product_id.taxes_id.ids invoice = inv_obj.create({ 'name': order.client_order_ref or order.name, @@ -102,7 +106,7 @@ class SaleAdvancePaymentInv(models.TransientModel): 'uom_id': self.product_id.uom_id.id, 'product_id': self.product_id.id, 'sale_line_ids': [(6, 0, [so_line.id])], - 'invoice_line_tax_ids': [(6, 0, [x.id for x in self.product_id.taxes_id])], + 'invoice_line_tax_ids': [(6, 0, tax_ids)], 'account_analytic_id': order.project_id.id or False, })], 'currency_id': order.pricelist_id.currency_id.id, @@ -142,6 +146,10 @@ class SaleAdvancePaymentInv(models.TransientModel): raise UserError(_('The product used to invoice a down payment should have an invoice policy set to "Ordered quantities". Please update your deposit product to be able to create a deposit invoice.')) if self.product_id.type != 'service': raise UserError(_("The product used to invoice a down payment should be of type 'Service'. Please use another product or update this product.")) + if order.fiscal_position_id and self.product_id.taxes_id: + tax_ids = order.fiscal_position_id.map_tax(self.product_id.taxes_id).ids + else: + tax_ids = self.product_id.taxes_id.ids so_line = sale_line_obj.create({ 'name': _('Advance: %s') % (time.strftime('%m %Y'),), 'price_unit': amount, @@ -150,7 +158,7 @@ class SaleAdvancePaymentInv(models.TransientModel): 'discount': 0.0, 'product_uom': self.product_id.uom_id.id, 'product_id': self.product_id.id, - 'tax_id': [(6, 0, self.product_id.taxes_id.ids)], + 'tax_id': [(6, 0, tax_ids)], 'is_downpayment': True, }) self._create_invoice(order, so_line, amount) diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py index dc4b52bdf14..1738bb54216 100644 --- a/addons/stock/models/product.py +++ b/addons/stock/models/product.py @@ -393,16 +393,16 @@ class ProductTemplate(models.Model): ('none', 'No Tracking')], string="Tracking", default='none', required=True) description_picking = fields.Text('Description on Picking', translate=True) qty_available = fields.Float( - 'Quantity On Hand', compute='_compute_quantities', search='_search_quantities', + 'Quantity On Hand', compute='_compute_quantities', search='_search_qty_available', digits=dp.get_precision('Product Unit of Measure')) virtual_available = fields.Float( - 'Forecasted Quantity', compute='_compute_quantities', search='_search_quantities', + 'Forecasted Quantity', compute='_compute_quantities', search='_search_virtual_available', digits=dp.get_precision('Product Unit of Measure')) incoming_qty = fields.Float( - 'Incoming', compute='_compute_quantities', search='_search_quantities', + 'Incoming', compute='_compute_quantities', search='_search_incoming_qty', digits=dp.get_precision('Product Unit of Measure')) outgoing_qty = fields.Float( - 'Outgoing', compute='_compute_quantities', search='_search_quantities', + 'Outgoing', compute='_compute_quantities', search='_search_outgoing_qty', digits=dp.get_precision('Product Unit of Measure')) location_id = fields.Many2one('stock.location', 'Location') warehouse_id = fields.Many2one('stock.warehouse', 'Warehouse') @@ -452,9 +452,23 @@ class ProductTemplate(models.Model): } return prod_available - def _search_quantities(self, operator, value): - # TDE FIXME: does this work anyway ? - domain = [] # TDE ADDED + def _search_qty_available(self, operator, value): + domain = [('qty_available', operator, value)] + product_variant_ids = self.env['product.product'].search(domain) + return [('product_variant_ids', 'in', product_variant_ids.ids)] + + def _search_virtual_available(self, operator, value): + domain = [('virtual_available', operator, value)] + product_variant_ids = self.env['product.product'].search(domain) + return [('product_variant_ids', 'in', product_variant_ids.ids)] + + def _search_incoming_qty(self, operator, value): + domain = [('incoming_qty', operator, value)] + product_variant_ids = self.env['product.product'].search(domain) + return [('product_variant_ids', 'in', product_variant_ids.ids)] + + def _search_outgoing_qty(self, operator, value): + domain = [('outgoing_qty', operator, value)] product_variant_ids = self.env['product.product'].search(domain) return [('product_variant_ids', 'in', product_variant_ids.ids)] diff --git a/addons/stock/models/stock_inventory.py b/addons/stock/models/stock_inventory.py index 228e921c259..47f080a1109 100644 --- a/addons/stock/models/stock_inventory.py +++ b/addons/stock/models/stock_inventory.py @@ -91,14 +91,12 @@ class Inventory(models.Model): ('category', _('One product category')), ('product', _('One product only')), ('partial', _('Select products manually'))] - stock_settings = self.env['stock.config.settings'].search([], limit=1, order='id DESC') - if not stock_settings: - return res_filter - if stock_settings.group_stock_tracking_owner: + + if self.user_has_groups('stock.group_tracking_owner'): res_filter += [('owner', _('One owner only')), ('product_owner', _('One product for a specific owner'))] - if stock_settings.group_stock_production_lot: + if self.user_has_groups('stock.group_production_lot'): res_filter.append(('lot', _('One Lot/Serial Number'))) - if stock_settings.group_stock_tracking_lot: + if self.user_has_groups('stock.group_tracking_lot'): res_filter.append(('pack', _('A Pack'))) return res_filter diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 7cb80c8b360..0ddd9de24d2 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -194,7 +194,6 @@ class StockMove(models.Model): @api.multi def _compute_string_qty_information(self): - StockConfig = self.env['stock.config.settings'] precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') void_moves = self.filtered(lambda move: move.state in ('draft', 'done', 'cancel') or move.location_id.usage != 'internal') other_moves = self - void_moves @@ -205,9 +204,7 @@ class StockMove(models.Model): total_available = move.product_id.uom_id._compute_quantity(total_available, move.product_uom, round=False) total_available = float_round(total_available, precision_digits=precision) info = str(total_available) - # look in the settings if we need to display the UoM name or not - config = StockConfig.search([], limit=1, order='id DESC') - if config and config.group_uom: + if self.user_has_groups('product.group_uom'): info += ' ' + move.product_uom.name if move.reserved_availability: if move.reserved_availability != total_available: @@ -380,7 +377,7 @@ class StockMove(models.Model): # if no specialized push rule has been found yet, we try to find a general one (without route) rules = Push.search(domain + [('route_id', '=', False)], order='sequence') # Make sure it is not returning the return - if rules and (not move.origin_returned_move_id or move.origin_returned_move_id.location_id.id != rules.location_dest_id.id): + if rules and (not move.origin_returned_move_id or move.origin_returned_move_id.location_dest_id.id != rules.location_dest_id.id): rules._apply(move) return True diff --git a/addons/stock_account/models/account_invoice.py b/addons/stock_account/models/account_invoice.py index 9ddc1c686f4..5dba13166ae 100644 --- a/addons/stock_account/models/account_invoice.py +++ b/addons/stock_account/models/account_invoice.py @@ -77,7 +77,7 @@ class AccountInvoiceLine(models.Model): def _get_price(self, company_currency, price_unit): if self.invoice_id.currency_id.id != company_currency: - price = company_currency.with_context(date=self.invoice_id.date_invoice).compute(price_unit * self.quantity, self.invoice_id.currency_id.id) + price = company_currency.with_context(date=self.invoice_id.date_invoice).compute(price_unit * self.quantity, self.invoice_id.currency_id) else: price = price_unit * self.quantity return round(price, self.invoice_id.currency_id.decimal_places) diff --git a/addons/stock_account/models/product.py b/addons/stock_account/models/product.py index 5ab32db789a..a81b0260253 100644 --- a/addons/stock_account/models/product.py +++ b/addons/stock_account/models/product.py @@ -3,6 +3,7 @@ from odoo import api, fields, models, tools, _ from odoo.exceptions import UserError +from odoo.tools import float_is_zero class ProductTemplate(models.Model): @@ -105,7 +106,7 @@ class ProductProduct(models.Model): for location in locations: for product in self.with_context(location=location.id, compute_child=False): diff = product.standard_price - new_price - if diff: + if float_is_zero(diff, precision_rounding=product.currency_id.rounding): raise UserError(_("No difference between standard price and new price!")) if not product_accounts[product.id].get('stock_valuation', False): raise UserError(_('You don\'t have any stock valuation account defined on your product category. You must define one before processing this operation.')) diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py index 4cf59568085..634c3cfbdd0 100644 --- a/addons/stock_account/models/stock.py +++ b/addons/stock_account/models/stock.py @@ -115,6 +115,11 @@ class StockQuant(models.Model): else: self.with_context(force_company=company_from.id)._create_account_move_line(move, acc_valuation, acc_dest, journal_id) + if move.company_id.anglo_saxon_accounting and move.location_id.usage == 'supplier' and move.location_dest_id.usage == 'customer': + # 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 = move._get_accounting_data_for_valuation() + self.with_context(force_company=move.company_id.id)._create_account_move_line(move, acc_src, acc_dest, journal_id) + def _create_account_move_line(self, move, credit_account_id, debit_account_id, journal_id): # group quants by cost quant_cost_qty = defaultdict(lambda: 0.0) diff --git a/addons/web/static/src/js/view_manager.js b/addons/web/static/src/js/view_manager.js index 4255f163068..e0d0135185f 100644 --- a/addons/web/static/src/js/view_manager.js +++ b/addons/web/static/src/js/view_manager.js @@ -459,6 +459,9 @@ var ViewManager = Widget.extend(ControlPanelMixin, { } }, do_load_state: function(state, warm) { + if (state.view_type && state.view_type !== this.active_view.type) { + this.switch_mode(state.view_type, true); + } this.active_view.controller.do_load_state(state, warm); }, }); diff --git a/addons/web/static/src/js/views/form_common.js b/addons/web/static/src/js/views/form_common.js index 82c3bc8c4a7..65e94e671eb 100644 --- a/addons/web/static/src/js/views/form_common.js +++ b/addons/web/static/src/js/views/form_common.js @@ -430,6 +430,7 @@ var FormWidget = Widget.extend(InvisibilityChangerMixin, { this.process_modifiers(); this._super(); this.$el.addClass(this.node.attrs["class"] || ""); + this.$el.attr('style', this.node.attrs.style); }, destroy: function() { $.fn.tooltip('destroy'); diff --git a/addons/web/static/src/js/views/form_relational_widgets.js b/addons/web/static/src/js/views/form_relational_widgets.js index dd4c4fb0fb6..00640696b41 100644 --- a/addons/web/static/src/js/views/form_relational_widgets.js +++ b/addons/web/static/src/js/views/form_relational_widgets.js @@ -1275,6 +1275,9 @@ var FieldOne2Many = FieldX2Many.extend({ return self.mutex.def; }); }, + is_false: function() { + return false; + }, }); var Many2ManyListView = X2ManyListView.extend({ diff --git a/addons/web/static/src/js/views/list_view.js b/addons/web/static/src/js/views/list_view.js index f2e4f565a79..9382b255b01 100644 --- a/addons/web/static/src/js/views/list_view.js +++ b/addons/web/static/src/js/views/list_view.js @@ -1733,7 +1733,7 @@ var Column = Class.extend({ } var aggregation_func = (this.sum && 'sum') || (this.avg && 'avg') || - (this.max && 'max') || (this.min && 'min') || this.group_operator; + (this.max && 'max') || (this.min && 'min'); if (!aggregation_func) { return {}; diff --git a/addons/web/static/src/js/web_client.js b/addons/web/static/src/js/web_client.js index d94f686c440..52962ed284b 100644 --- a/addons/web/static/src/js/web_client.js +++ b/addons/web/static/src/js/web_client.js @@ -113,25 +113,37 @@ return AbstractWebClient.extend({ } }, on_hashchange: function(event) { + if (this._ignore_hashchange) { + this._ignore_hashchange = false; + return; + } + var self = this; - var stringstate = event.getState(false); - if (!_.isEqual(this._current_state, stringstate)) { - var state = event.getState(true); - if(!state.action && state.menu_id) { - self.menu.is_bound.done(function() { - self.menu.menu_click(state.menu_id); - }); - } else { - state._push_me = false; // no need to push state back... - this.action_manager.do_load_state(state, !!this._current_state).then(function () { - var action = self.action_manager.get_inner_action(); - if (action) { - self.menu.open_action(action.action_descr.id); - } - }); + this.clear_uncommitted_changes().then(function () { + var stringstate = event.getState(false); + if (!_.isEqual(self._current_state, stringstate)) { + var state = event.getState(true); + if(!state.action && state.menu_id) { + self.menu.is_bound.done(function() { + self.menu.menu_click(state.menu_id); + }); + } else { + state._push_me = false; // no need to push state back... + self.action_manager.do_load_state(state, !!self._current_state).then(function () { + var action = self.action_manager.get_inner_action(); + if (action) { + self.menu.open_action(action.action_descr.id); + } + }); + } } - } - this._current_state = stringstate; + self._current_state = stringstate; + }, function () { + if (event) { + self._ignore_hashchange = true; + window.location = event.originalEvent.oldURL; + } + }); }, on_menu_action: function(options) { var self = this; diff --git a/addons/web/static/src/less/form_view.less b/addons/web/static/src/less/form_view.less index e992ab5a748..7d870122502 100644 --- a/addons/web/static/src/less/form_view.less +++ b/addons/web/static/src/less/form_view.less @@ -460,6 +460,15 @@ } } } + + // Notebooks + .o_notebook { + .nav-tabs { + > .tab-pane { + min-height: 100px; + } + } + } } // Overriden style when form view in modal diff --git a/addons/web_editor/controllers/main.py b/addons/web_editor/controllers/main.py index 923b78492b3..5ea843bcaae 100644 --- a/addons/web_editor/controllers/main.py +++ b/addons/web_editor/controllers/main.py @@ -226,7 +226,7 @@ class Web_Editor(http.Controller): @http.route('/web_editor/customize_template_get', type='json', auth='user', website=True) def customize_template_get(self, key, full=False, bundles=False): """ Get inherit view's informations of the template ``key``. - returns templates (which can be active or not) + returns templates info (which can be active or not) ``bundles=True`` returns also the asset bundles """ return request.env["ir.ui.view"].customize_template_get(key, full=full, bundles=bundles) diff --git a/addons/web_editor/models/ir_ui_view.py b/addons/web_editor/models/ir_ui_view.py index 64ee45b567d..2b0af052c23 100644 --- a/addons/web_editor/models/ir_ui_view.py +++ b/addons/web_editor/models/ir_ui_view.py @@ -175,42 +175,21 @@ class IrUiView(models.Model): return views_to_return @api.model - def customize_template_get(self, key, full=False, bundles=False): - """ Get inherit view's informations of the template ``key``. - returns templates (which can be active or not) + def _customize_template_get_views(self, key, full=False, bundles=False): + """ Get inherit views of the template ``key``. + returns views (which can be active or not) ``bundles=True`` returns also the asset bundles """ - user = self.env.user - user_groups = set(user.groups_id) + user_groups = set(self.env.user.groups_id) views = self.with_context(active_test=False)._views_get(key, bundles=bundles) + views = views.filtered(lambda v: user_groups.issuperset(v.groups_id)) + return views - view_theme_id = self.env['ir.model.data'].xmlid_to_res_id('website.theme') - done = set() - result = [] - for view in views: - if full or (view.customize_show and view.inherit_id.id != view_theme_id): - if not user_groups.issuperset(view.groups_id): - continue - if view.inherit_id not in done: - result.append({ - 'name': view.inherit_id.name, - 'id': view.id, - 'key': view.key, - 'xml_id': view.xml_id, - 'arch': view.arch, - 'inherit_id': view.inherit_id.id, - 'header': True, - 'active': False - }) - done.add(view.inherit_id) - result.append({ - 'name': view.name, - 'id': view.id, - 'key': view.key, - 'xml_id': view.xml_id, - 'arch': view.arch, - 'inherit_id': view.inherit_id.id, - 'header': False, - 'active': view.active, - }) - return result + @api.model + def customize_template_get(self, key, full=False, bundles=False): + """ Get inherit view's informations of the template ``key``. + returns templates info (which can be active or not) + ``bundles=True`` returns also the asset bundles + """ + views = self._customize_template_get_views(key, full=full, bundles=bundles) + return views.read(['name', 'id', 'key', 'xml_id', 'arch', 'active', 'inherit_id']) diff --git a/addons/web_editor/static/src/js/ace.js b/addons/web_editor/static/src/js/ace.js index 5fbe7589265..56500383851 100644 --- a/addons/web_editor/static/src/js/ace.js +++ b/addons/web_editor/static/src/js/ace.js @@ -149,7 +149,7 @@ var ViewEditor = Widget.extend({ view.children = []; }); _.each(index, function (view) { - var parentId = view.inherit_id; + var parentId = view.inherit_id[0]; if (parentId && index[parentId]) { index[parentId].children.push(view); } else { diff --git a/addons/web_editor/static/src/js/widgets.js b/addons/web_editor/static/src/js/widgets.js index 7a3ab09dd76..61ec1564ac5 100644 --- a/addons/web_editor/static/src/js/widgets.js +++ b/addons/web_editor/static/src/js/widgets.js @@ -1145,7 +1145,7 @@ var LinkDialog = Dialog.extend({ $preview.attr("target", new_window ? '_blank' : "") .attr("href", url && url.length ? url : "#") .html((label && label.length ? label : url)) - .attr("class", classes.replace(/pull-\w+/, '')); + .attr("class", classes.replace(/pull-\w+/, '') + " o_btn_preview"); }); } }); diff --git a/addons/web_editor/static/src/less/web_editor.ui.components.less b/addons/web_editor/static/src/less/web_editor.ui.components.less index 7dd882ffbd0..44f3923c35e 100644 --- a/addons/web_editor/static/src/less/web_editor.ui.components.less +++ b/addons/web_editor/static/src/less/web_editor.ui.components.less @@ -189,7 +189,7 @@ body .modal { ul.pager { margin: 0; > li > a { - border-radius: 3px; + border-radius: 0px; margin-left: 2px; padding: 6px 14px; } @@ -206,20 +206,34 @@ body .modal { } } } - .dropdown-toggle { - line-height: 0; - height: 34px; + + .well { + border-top-width: 0; + + label { + margin: auto 10px; + } + + .btn-group { + margin-right: 10px; + } } + .o_existing_attachment_cell { @odoo-border-image: 1px; .o_existing_attachment_remove { .o-position-absolute(@odoo-border-image, 15px + @odoo-border-image); - padding: 3px 3px 12px 12px; - background-color: white; - border-bottom: 1px solid black; - border-left: 1px solid black; - border-radius: 0 0 0 100%; - opacity: 0.8; + padding: 3px 5px 5px 5px; + background-color: fade(white, 80%); + text-shadow: 0 0 1px white; + border-radius: 0 0 0 2px; + opacity: 0; + cursor: pointer; + + &:hover { + background-color: white; + color: @o-we-color-danger; + } } .o_image { border: @odoo-border-image solid transparent; @@ -235,15 +249,29 @@ body .modal { border-color: @odoo-brand-primary; box-shadow: 0px 0px 2px 2px @odoo-brand-secondary; } + &:hover { + .o_existing_attachment_remove { + opacity: 1; + } + } } - .font-icons-icons > span { - font-size: 30px; - margin: 5px; - padding: 3px; - cursor: pointer; + .font-icons-icons { + padding: 20px 0; - &.o_selected { - box-shadow: 0px 0px 2px 2px @odoo-brand-secondary; + > span { + text-align: center; + font-size: 22px; + margin: 5px; + width: 50px; + height: 50px; + padding: 15px; + cursor: pointer; + + &.o_selected { + cursor: auto; + background-color: @o-we-color-paper; + box-shadow: 0px 0px 0px 1px @o-we-color-success; + } } } } diff --git a/addons/web_editor/static/src/less/web_editor.variables.less b/addons/web_editor/static/src/less/web_editor.variables.less index 9e0c7a2a014..37e219302a0 100644 --- a/addons/web_editor/static/src/less/web_editor.variables.less +++ b/addons/web_editor/static/src/less/web_editor.variables.less @@ -9,6 +9,9 @@ @o-we-color-text-normal: #999999; @o-we-color-text-light: #d4d5d7; @o-we-color-danger: #e6586c; +@o-we-color-warning: #f0ad4e; +@o-we-color-success: #40ad67; +@o-we-color-info: #6999a8; @o-we-sidebar-width: 210px; @@ -76,3 +79,119 @@ from { background: @odoo-brand-optional; } to { width: 0; background: transparent; } } + + +// ============ Preservetion mixins =============== +// Used to preserve web_editor and website UI from themes customizations + +.o-w-preserve-base() { + font-family: @o-we-font-family; + line-height: 1.42857143; + color: @o-we-color-divider; + + .text-muted { + color: @o-we-color-text-normal; + } +} + +.o-w-preserve-headings() { + h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 { + font-family: @o-we-font-family; + line-height: 1.42857143; + color: @o-we-color-dark; + font-weight: bold; + } +} + +.o-w-preserve-links() { + a:not(.o_btn_preview) { + color: @odoo-brand-optional; + + &:focus, &:active, &:focus:active { + outline: none!important; + } + } +} + +.o-w-preserve-forms() { + .form-control { + height: 34px; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857143; + border: 1px solid @o-we-color-text-light; + color: #555; + background-color: #fff; + border-radius: 0; + } +} + +.o-w-preserve-modals() { + background: 0; + + .modal-content { + border-radius: 0; + background-color: @o-we-color-paper; + + .modal-body { + background-color: white; + } + .modal-footer { + text-align: left; + } + } +} + +.o-w-preserve-btn() { + .btn:not(.o_btn_preview) { + border-width: 1px; + border-radius: 0; + font-weight: normal; + .button-size(6px; 12px; 14px; 1.42857143; 0); + + &.btn-primary { + .button-variant(white, @odoo-brand-optional, darken(@odoo-brand-optional,5%)); + text-transform: uppercase; + } + &.btn-default { + .button-variant(@odoo-brand-optional, white, @o-we-color-text-light); + text-transform: uppercase; + } + &.btn-link { + .button-variant(@odoo-brand-optional, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0)); + } + &.btn-success { + .button-variant(white; @o-we-color-success; darken(@o-we-color-success, 5%)); + } + &.btn-info { + .button-variant(white; @o-we-color-info; darken(@o-we-color-info, 5%)); + } + &.btn-warning { + .button-variant(white; @o-we-color-warning; darken(@o-we-color-warning, 5%)); + } + &.btn-danger { + .button-variant(white; @o-we-color-danger; darken(@o-we-color-danger, 5%)); + } + + &.btn-lg { + .button-size(10px; 16px; 18px; 1.3333333; 0); + } + &.btn-sm { + .button-size(5px; 10px; 12px; 1.5; 0); + } + &.btn-xs { + .button-size(1px; 5px; 12px; 1.5; 0); + } + } +} + +.o-w-preserve-wells() { + .well { + padding: 19px; + margin-bottom: 20px; + background-color: @o-we-color-paper; + border: 1px solid darken(@o-we-color-paper,5%); + border-radius: 0; + .box-shadow(none); + } +} diff --git a/addons/web_editor/static/src/xml/editor.xml b/addons/web_editor/static/src/xml/editor.xml index 27213d7fdd8..2ad47703700 100644 --- a/addons/web_editor/static/src/xml/editor.xml +++ b/addons/web_editor/static/src/xml/editor.xml @@ -251,31 +251,31 @@ <span class="caret"/></a> <ul aria-labelledby="dLabel" class="dropdown-menu" role="menu"> <li> - <label class="btn btn-link">Link + <label class="btn btn-link o_btn_preview">Link <input checked="checked" class="link-style" name="link-style-type" type="radio" value=""/></label> </li> <li> - <label class="btn btn-default">Basic + <label class="btn btn-default o_btn_preview">Basic <input class="link-style" name="link-style-type" type="radio" value="btn-default"/></label> </li> <li> - <label class="btn btn-success">Success + <label class="btn btn-success o_btn_preview">Success <input class="link-style" name="link-style-type" type="radio" value="btn-success"/></label> </li> <li> - <label class="btn btn-primary">Primary + <label class="btn btn-primary o_btn_preview">Primary <input class="link-style" name="link-style-type" type="radio" value="btn-primary"/></label> </li> <li> - <label class="btn btn-info">Info + <label class="btn btn-info o_btn_preview">Info <input class="link-style" name="link-style-type" type="radio" value="btn-info"/></label> </li> <li> - <label class="btn btn-warning">Warning + <label class="btn btn-warning o_btn_preview">Warning <input class="link-style" name="link-style-type" type="radio" value="btn-warning"/></label> </li> <li> - <label class="btn btn-danger">Danger + <label class="btn btn-danger o_btn_preview">Danger <input class="link-style" name="link-style-type" type="radio" value="btn-danger"/></label> </li> </ul> @@ -287,19 +287,19 @@ <span class="caret"/></a> <ul aria-labelledby="dLabel" class="dropdown-menu" role="menu"> <li> - <label class="btn btn-default btn-xs">Extra Small + <label class="btn btn-default btn-xs o_btn_preview">Extra Small <input class="link-style" name="link-style-size" type="radio" value="btn-xs"/></label> </li> <li> - <label class="btn btn-default btn-sm">Small + <label class="btn btn-default btn-sm o_btn_preview">Small <input class="link-style" name="link-style-size" type="radio" value="btn-sm"/></label> </li> <li> - <label class="btn btn-default">Default + <label class="btn btn-default o_btn_preview">Default <input checked="checked" class="link-style" name="link-style-size" type="radio" value=""/></label> </li> <li> - <label class="btn btn-default btn-lg">Large + <label class="btn btn-default btn-lg o_btn_preview">Large <input class="link-style" name="link-style-size" type="radio" value="btn-lg"/></label> </li> </ul> diff --git a/addons/web_kanban/static/src/js/kanban_view.js b/addons/web_kanban/static/src/js/kanban_view.js index b755c7559dd..7cfde911a92 100644 --- a/addons/web_kanban/static/src/js/kanban_view.js +++ b/addons/web_kanban/static/src/js/kanban_view.js @@ -62,7 +62,7 @@ var KanbanView = View.extend({ this.qweb = new QWeb(session.debug, {_s: session.origin}); - this.limit = this.options.limit || 40; + this.limit = this.options.limit || parseInt(this.fields_view.arch.attrs.limit, 10) || 40; this.fields = {}; this.fields_keys = _.keys(this.fields_view.fields); this.grouped = undefined; diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index 0826ac839d7..3a64816a248 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -262,10 +262,10 @@ class Website(Home): @http.route('/website/customize_template_get', type='json', auth='user', website=True) def customize_template_get(self, key, full=False, bundles=False): - """ Get inherit view's informations of the template ``key``. By default, only - :returns ``customize_show`` templates (which can be active or not), if - ``full=True`` returns inherit view's informations of the template ``key``. - ``bundles=True`` returns also the asset bundles + """ Get inherit view's informations of the template ``key``. + returns templates info (which can be active or not) + ``full=False`` returns only the customize_show template + ``bundles=True`` returns also the asset bundles """ return request.env["ir.ui.view"].customize_template_get(key, full=full, bundles=bundles) @@ -370,6 +370,7 @@ class Website(Home): ], type='http', auth="public", website=True) def actions_server(self, path_or_xml_id_or_id, **post): ServerActions = request.env['ir.actions.server'] + action = action_id = None # find the action_id: either an xml_id, the path, or an ID if isinstance(path_or_xml_id_or_id, basestring) and '.' in path_or_xml_id_or_id: diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py index acd9256ea52..110ce7e39ee 100644 --- a/addons/website/models/ir_ui_view.py +++ b/addons/website/models/ir_ui_view.py @@ -136,25 +136,13 @@ class View(models.Model): return qcontext @api.model - def customize_template_get(self, key, full=False, bundles=False): - """ Get inherit view's informations of the template ``key``. By default, only - :returns ``customize_show`` templates (which can be active or not), if - ``full=True`` returns inherit view's informations of the template ``key``. - ``bundles=True`` returns also the asset bundles + def _customize_template_get_views(self, key, full=False, bundles=False): + """ Get inherit view's informations of the template ``key``. + returns views (which can be active or not) + ``full=False`` returns only the customize_show template + ``bundles=True`` returns also the asset bundles """ - result = super(View, self).customize_template_get(key, full=full, bundles=bundles) - - imd = self.env['ir.model.data'] - view_theme_id = imd.xmlid_to_res_id('website.theme') - - result_filtered = [] - for x in result: - if not full: - view = self.browse(x['id']) - if not view.customize_show: - continue - if view_theme_id and x['inherit_id'] == view_theme_id: - continue - result_filtered.append(x) - - return result_filtered + views = super(View, self)._customize_template_get_views(key, full=full, bundles=bundles) + if full: + return views + return views.filtered(lambda v: v.customize_show) diff --git a/addons/website/static/src/js/website.editor.js b/addons/website/static/src/js/website.editor.js index 5d2d7706560..a110e6eca3e 100644 --- a/addons/website/static/src/js/website.editor.js +++ b/addons/website/static/src/js/website.editor.js @@ -74,16 +74,17 @@ website.TopBarCustomize = Widget.extend({ key: this.view_name, full: false }).then(function (result) { + var current_group = ""; _.each(result, function (item) { - if (item.header) { - $menu.append('<li class="dropdown-header">' + item.name + '</li>'); - } else { - var $li = $('<li/>', {role: 'presentation'}) - .append($('<a/>', {href: '#', 'data-view-id': item.id, role: 'menuitem'}) - .append(qweb.render('web_editor.components.switch', {id: 'switch-' + item.id, label: item.name}))); - $li.find('input').prop('checked', !!item.active); - $menu.append($li); + if (current_group !== item.inherit_id[1]) { + current_group = item.inherit_id[1]; + $menu.append("<li class=\"dropdown-header\">" + current_group + "</li>"); } + var $li = $('<li/>', {role: 'presentation'}) + .append($('<a/>', {href: '#', 'data-view-id': item.id, role: 'menuitem'}) + .append(qweb.render('web_editor.components.switch', {id: 'switch-' + item.id, label: item.name}))); + $li.find('input').prop('checked', !!item.active); + $menu.append($li); }); }); }, diff --git a/addons/website/static/src/js/website.snippets.animation.js b/addons/website/static/src/js/website.snippets.animation.js index 5d55a25bdb0..572e3a0f0ef 100644 --- a/addons/website/static/src/js/website.snippets.animation.js +++ b/addons/website/static/src/js/website.snippets.animation.js @@ -122,7 +122,7 @@ animation.registry.share = animation.Class.extend({ animation.registry.media_video = animation.Class.extend({ selector: ".media_iframe_video", start: function () { - if (!this.$target.has('.media_iframe_video_size').length) { + if (!this.$target.has('> iframe').length) { var editor = '<div class="css_editable_mode_display"> </div>'; var size = '<div class="media_iframe_video_size"> </div>'; this.$target.html(editor+size+'<iframe src="'+_.escape(this.$target.data("src"))+'" frameborder="0" allowfullscreen="allowfullscreen"></iframe>'); diff --git a/addons/website/static/src/js/website.snippets.editor.js b/addons/website/static/src/js/website.snippets.editor.js index cfd0b561778..4fbb79dd89e 100644 --- a/addons/website/static/src/js/website.snippets.editor.js +++ b/addons/website/static/src/js/website.snippets.editor.js @@ -311,8 +311,10 @@ options.registry.parallax = options.Class.extend({ this.$target.data("snippet-view", new animation.registry.parallax(this.$target)); } this.scroll(); - this.buildingBlock.$el.on("snippet-dropped snippet-activated", this._refresh.bind(this)); - this.$target.on('snippet-option-change snippet-option-preview', this._refresh.bind(this)); + + this._refresh_callback = this._refresh.bind(this); + this.buildingBlock.$el.on("snippet-dropped snippet-activated", this._refresh_callback); + this.$target.on('snippet-option-change snippet-option-preview', this._refresh_callback); }, scroll: function (type, value) { this.$target.attr("data-scroll-background-ratio", value); @@ -330,6 +332,11 @@ options.registry.parallax = options.Class.extend({ this._super.apply(this, arguments); this._refresh(); }, + on_remove: function () { + this._super.apply(this, arguments); + this.$target.off("snippet-option-change snippet-option-preview", this._refresh_callback); + this.buildingBlock.$el.off("snippet-dropped snippet-activated", this._refresh_callback); + }, }); options.registry.ul = options.Class.extend({ diff --git a/addons/website/static/src/less/website.ui.components.less b/addons/website/static/src/less/website.ui.components.less index e1230d2b358..5c2aa821e13 100644 --- a/addons/website/static/src/less/website.ui.components.less +++ b/addons/website/static/src/less/website.ui.components.less @@ -1,52 +1,25 @@ // MODALS body .modal { - a { - &:focus, &:active, &:focus:active { - outline: none!important; - } - } &.o_website_modal { - font-family: @o-we-font-family; - - h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 { - font-family: @o-we-font-family; - } + .o-w-preserve-base(); + .o-w-preserve-headings(); + .o-w-preserve-forms(); + .o-w-preserve-links(); + .o-w-preserve-btn(); + .o-w-preserve-wells(); + .o-w-preserve-modals(); .modal-content { - border-radius: 0; - color: initial!important; - background-color: @o-we-color-paper!important; .modal-header .o_subtitle { margin-left: 10px; } .modal-body { - color: inherit!important; - background-color: transparent!important; .o_modal_header { .o-webclient-padding(@top: 10px, @bottom: 10px); .clearfix(); } } - .modal-footer { - text-align: left; - - .btn { - border-radius: 0; - - &.btn-primary { - .button-variant(white, @odoo-brand-optional, @odoo-brand-optional); - text-transform: uppercase; - } - &.btn-default { - .button-variant(@odoo-brand-optional, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0)); - text-transform: uppercase; - } - &.btn-link { - .button-variant(@odoo-brand-optional, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0)); - } - } - } } @media (min-width: @screen-sm-min) { @@ -89,7 +62,6 @@ body .modal { .modal-content { background-color: black!important; - color: lightgray!important; border: 3px outset gray; border-radius: 20px; @@ -99,6 +71,7 @@ body .modal { font-family: @o-we-font-family; h4 { + color: lightgray; font-family:inherit; font-weight: normal; @@ -109,6 +82,8 @@ body .modal { } .modal-body { + background-color: inherit!important; + border-radius: 20px; padding: 15px; @mobile-preview-width: 320px; @@ -324,6 +299,10 @@ body .modal { // ACE EDITOR .o_ace_view_editor { + .o-w-preserve-base(); + .o-w-preserve-btn(); + .o-w-preserve-forms(); + .o-position-absolute(@odoo-navbar-height, 0, 0); position: fixed; z-index: 1001; diff --git a/addons/website/tests/test_views.py b/addons/website/tests/test_views.py index 5199b8b9f46..1d963c68026 100644 --- a/addons/website/tests/test_views.py +++ b/addons/website/tests/test_views.py @@ -139,6 +139,24 @@ class TestViewSaving(common.TransactionCase): Company = self.env['res.company'] View = self.env['ir.ui.view'] + # create a view with an xmlid, like the file import would + with self.env.norecompute(): + self.view_id = View.create(self.cr, self.uid, { + 'name': "Test View", + 'type': 'qweb', + 'arch': ET.tostring(self.arch, encoding='utf-8').decode('utf-8') + }) + imd = self.env['ir.model.data'].create({ + 'module': 'website', + 'name': 'test_view', + 'model': 'ir.ui.view', + 'res_id': self.view_id, + }) + + # the xml_id of the view should not be flagged as 'noupdate' + self.assertEqual(View.browse(self.cr, self.uid, self.view_id).model_data_id, imd) + self.assertFalse(imd.noupdate) + replacement = ET.tostring(h.DIV( h.H3("Column 2"), h.UL( @@ -149,6 +167,9 @@ class TestViewSaving(common.TransactionCase): ), encoding='utf-8') self.view_id.save(value=replacement, xpath='/div/div[2]') + # the xml_id of the view should be flagged as 'noupdate' + self.assertTrue(imd.noupdate) + company = Company.browse(1) self.assertEqual(company.name, "Acme Corporation") self.assertEqual(company.phone, "+12 3456789") diff --git a/addons/website_blog/models/website_blog.py b/addons/website_blog/models/website_blog.py index 63911cf92ff..ddc1f1a04c2 100644 --- a/addons/website_blog/models/website_blog.py +++ b/addons/website_blog/models/website_blog.py @@ -32,6 +32,20 @@ class Blog(models.Model): return res @api.multi + def message_post(self, parent_id=False, subtype=None, **kwargs): + """ Temporary workaround to avoid spam. If someone replies on a channel + through the 'Presentation Published' email, it should be considered as a + note as we don't want all channel followers to be notified of this answer. """ + self.ensure_one() + if parent_id: + parent_message = self.env['mail.message'].sudo().browse(parent_id) + if parent_message.subtype_id and parent_message.subtype_id == self.env.ref('website_blog.mt_blog_blog_published'): + if kwargs.get('subtype_id'): + kwargs['subtype_id'] = False + subtype = 'mail.mt_note' + return super(Blog, self).message_post(parent_id=parent_id, subtype=subtype, **kwargs) + + @api.multi def all_tags(self, min_limit=1): req = """ SELECT @@ -87,11 +101,13 @@ class BlogPost(models.Model): blog_post.website_url = "/blog/%s/post/%s" % (slug(blog_post.blog_id), slug(blog_post)) @api.multi + @api.depends('post_date', 'visits') def _compute_ranking(self): res = {} for blog_post in self: - age = datetime.now() - fields.Datetime.from_string(blog_post.post_date) - res[blog_post.id] = blog_post.visits * (0.5 + random.random()) / max(3, age.days) + if blog_post.id: # avoid to rank one post not yet saved and so withtout post_date in case of an onchange. + age = datetime.now() - fields.Datetime.from_string(blog_post.post_date) + res[blog_post.id] = blog_post.visits * (0.5 + random.random()) / max(3, age.days) return res def _default_content(self): @@ -167,6 +183,8 @@ class BlogPost(models.Model): def _set_post_date(self): for blog_post in self: blog_post.published_date = blog_post.post_date + if not blog_post.published_date: + blog_post._write(dict(post_date=blog_post.create_date)) # dont trigger inverse function def _check_for_publication(self, vals): if vals.get('website_published'): diff --git a/addons/website_quote/views/website_quote_templates.xml b/addons/website_quote/views/website_quote_templates.xml index 1bfa7e47a81..463f10b80d4 100644 --- a/addons/website_quote/views/website_quote_templates.xml +++ b/addons/website_quote/views/website_quote_templates.xml @@ -298,7 +298,7 @@ <div class="modal-body" id="sign-dialog"> <p> <span>I agree that by signing this proposal, I - accept it on the behalf of </span><b t-field="quotation.company_id"/><span>, for an amount of </span> + accept it on the behalf of </span><b t-field="quotation.partner_id.commercial_partner_id"/><span>, for an amount of </span> <b data-id="total_amount" t-field="quotation.amount_total" t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'/> <span>with payment terms: </span><b t-field="quotation.payment_term_id"/>. @@ -325,7 +325,7 @@ <div class="modal-body" id="sign-dialog"> <p> <span>I agree that by paying this proposal, I - accept it on the behalf of </span><b t-field="quotation.company_id"/><span>, for an amount of </span> + accept it on the behalf of </span><b t-field="quotation.partner_id.commercial_partner_id"/><span>, for an amount of </span> <b data-id="total_amount" t-field="quotation.amount_total" t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'/> <span>with payment terms: </span><b t-field="quotation.payment_term_id"/>. diff --git a/addons/website_sale/models/product.py b/addons/website_sale/models/product.py index 9e53f039407..d5a8f77a72d 100644 --- a/addons/website_sale/models/product.py +++ b/addons/website_sale/models/product.py @@ -207,7 +207,6 @@ class Product(models.Model): self.ensure_one() return self.product_tmpl_id.website_publish_button() - class ProductAttribute(models.Model): _inherit = "product.attribute" diff --git a/addons/website_sale/static/src/js/website_sale_payment.js b/addons/website_sale/static/src/js/website_sale_payment.js index 32d9a8dbca9..c4c4861654d 100644 --- a/addons/website_sale/static/src/js/website_sale_payment.js +++ b/addons/website_sale/static/src/js/website_sale_payment.js @@ -55,7 +55,7 @@ $(document).ready(function () { } $form.off('submit'); ajax.jsonRpc('/shop/payment/transaction/' + acquirer_id, 'call', params).then(function (data) { - $(data).submit(); + $(data).appendTo('body').submit(); }); return false; }); diff --git a/addons/website_slides/models/slides.py b/addons/website_slides/models/slides.py index b6dc690cc99..d02da6b19cb 100644 --- a/addons/website_slides/models/slides.py +++ b/addons/website_slides/models/slides.py @@ -164,6 +164,20 @@ class Channel(models.Model): self.with_context(active_test=False).mapped('slide_ids').write({'active': vals['active']}) return res + @api.multi + def message_post(self, parent_id=False, subtype=None, **kwargs): + """ Temporary workaround to avoid spam. If someone replies on a channel + through the 'Presentation Published' email, it should be considered as a + note as we don't want all channel followers to be notified of this answer. """ + self.ensure_one() + if parent_id: + parent_message = self.env['mail.message'].sudo().browse(parent_id) + if parent_message.subtype_id and parent_message.subtype_id == self.env.ref('website_slides.mt_channel_slide_published'): + if kwargs.get('subtype_id'): + kwargs['subtype_id'] = False + subtype = 'mail.mt_note' + return super(Channel, self).message_post(parent_id=parent_id, subtype=subtype, **kwargs) + class Category(models.Model): """ Channel contain various categories to manage its slides """ diff --git a/odoo/addons/base/ir/ir_model.py b/odoo/addons/base/ir/ir_model.py index b4376c99467..1e23c311da2 100644 --- a/odoo/addons/base/ir/ir_model.py +++ b/odoo/addons/base/ir/ir_model.py @@ -1208,6 +1208,7 @@ class IrModelData(models.Model): datas = self.search([('module', 'in', modules_to_remove)]) wkf_todo = [] to_unlink = tools.OrderedSet() + undeletable = self.browse([]) for data in datas.sorted(key='id', reverse=True): model = data.model @@ -1232,6 +1233,7 @@ class IrModelData(models.Model): _logger.info('Unable to force processing of workflow for item %s@%s in order to leave activity to be deleted', res_id, model, exc_info=True) def unlink_if_refcount(to_unlink): + undeletable = self.browse() for model, res_id in to_unlink: external_ids = self.search([('model', '=', model), ('res_id', '=', res_id)]) if external_ids - datas: @@ -1255,28 +1257,30 @@ class IrModelData(models.Model): self.env[model].browse(res_id).unlink() except Exception: _logger.info('Unable to delete %s@%s', res_id, model, exc_info=True) + undeletable += external_ids self._cr.execute('ROLLBACK TO SAVEPOINT record_unlink_save') else: self._cr.execute('RELEASE SAVEPOINT record_unlink_save') + return undeletable # Remove non-model records first, then model fields, and finish with models - unlink_if_refcount(item for item in to_unlink if item[0] not in ('ir.model', 'ir.model.fields', 'ir.model.constraint')) - unlink_if_refcount(item for item in to_unlink if item[0] == 'ir.model.constraint') + undeletable += unlink_if_refcount(item for item in to_unlink if item[0] not in ('ir.model', 'ir.model.fields', 'ir.model.constraint')) + undeletable += unlink_if_refcount(item for item in to_unlink if item[0] == 'ir.model.constraint') modules = self.env['ir.module.module'].search([('name', 'in', modules_to_remove)]) constraints = self.env['ir.model.constraint'].search([('module', 'in', modules.ids)]) constraints._module_data_uninstall() - unlink_if_refcount(item for item in to_unlink if item[0] == 'ir.model.fields') + undeletable += unlink_if_refcount(item for item in to_unlink if item[0] == 'ir.model.fields') relations = self.env['ir.model.relation'].search([('module', 'in', modules.ids)]) relations._module_data_uninstall() - unlink_if_refcount(item for item in to_unlink if item[0] == 'ir.model') + undeletable += unlink_if_refcount(item for item in to_unlink if item[0] == 'ir.model') self._cr.commit() - datas.unlink() + (datas - undeletable).unlink() @api.model def _process_end(self, modules): diff --git a/odoo/addons/base/ir/ir_ui_view.py b/odoo/addons/base/ir/ir_ui_view.py index 67c755581d3..10bf7c627b4 100644 --- a/odoo/addons/base/ir/ir_ui_view.py +++ b/odoo/addons/base/ir/ir_ui_view.py @@ -229,10 +229,11 @@ actual arch. for view, view_wo_lang in zip(self, self.with_context(lang=None)): view_wo_lang.arch = view.arch_base + @api.depends('write_date') def _compute_model_data_id(self): - # get the last ir_model_data record corresponding to self + # get the first ir_model_data record corresponding to self domain = [('model', '=', 'ir.ui.view'), ('res_id', 'in', self.ids)] - for data in self.env['ir.model.data'].search_read(domain, ['res_id']): + for data in self.env['ir.model.data'].search_read(domain, ['res_id'], order='id desc'): view = self.browse(data['res_id']) view.model_data_id = data['id'] @@ -343,7 +344,15 @@ actual arch. values['name'] = "%s %s" % (values.get('model'), values['type']) self.clear_caches() - return super(View, self).create(self._compute_defaults(values)) + + if 'install_mode_data' in self._context: + # the view is created from a data file by installing a module; delay + # the recomputation of field 'model_data_id' until its xmlid record + # is created + with self.env.norecompute(): + return super(View, self).create(self._compute_defaults(values)) + else: + return super(View, self).create(self._compute_defaults(values)) @api.multi def write(self, vals): @@ -1168,4 +1177,4 @@ actual arch. try: self.browse(vid)._check_xml() except Exception as e: - self.raise_view_error("Can't validate view: %s" % e.message, vid) + self.raise_view_error("Can't validate view:\n%s" % (e.message or repr(e)), vid) diff --git a/odoo/addons/base/tests/test_views.py b/odoo/addons/base/tests/test_views.py index 38f30308fab..adbabdc48ad 100644 --- a/odoo/addons/base/tests/test_views.py +++ b/odoo/addons/base/tests/test_views.py @@ -12,6 +12,14 @@ from odoo.tests import common from odoo.tools import mute_logger +class ViewXMLID(common.TransactionCase): + def test_model_data_id(self): + """ Check whether views know their xmlid record. """ + view = self.env.ref('base.view_company_form') + self.assertTrue(view) + self.assertTrue(view.model_data_id) + self.assertEqual(view.model_data_id.complete_name, 'base.view_company_form') + class ViewCase(common.TransactionCase): def setUp(self): super(ViewCase, self).setUp() diff --git a/odoo/addons/test_impex/tests/test_export.py b/odoo/addons/test_impex/tests/test_export.py index e80f6df0866..226ba9943f6 100644 --- a/odoo/addons/test_impex/tests/test_export.py +++ b/odoo/addons/test_impex/tests/test_export.py @@ -23,7 +23,7 @@ class CreatorCase(common.TransactionCase): def export(self, value, fields=('value',), context=None): record = self.make(value) record.invalidate_cache() - return record._BaseModel__export_rows([f.split('/') for f in fields]) + return record._export_rows([f.split('/') for f in fields]) class test_boolean_field(CreatorCase): @@ -449,7 +449,7 @@ class test_o2m_multiple(CreatorCase): def export(self, value=None, fields=('child1', 'child2',), context=None, **values): record = self.make(value, **values) - return record._BaseModel__export_rows([f.split('/') for f in fields]) + return record._export_rows([f.split('/') for f in fields]) def test_empty(self): self.assertEqual( diff --git a/odoo/addons/test_new_api/tests/__init__.py b/odoo/addons/test_new_api/tests/__init__.py index 2bbcfe40990..b90ccc96825 100644 --- a/odoo/addons/test_new_api/tests/__init__.py +++ b/odoo/addons/test_new_api/tests/__init__.py @@ -3,5 +3,6 @@ from . import test_new_fields from . import test_onchange from . import test_attributes +from . import test_one2many from . import test_ui from . import test_domain diff --git a/odoo/addons/test_new_api/tests/test_onchange.py b/odoo/addons/test_new_api/tests/test_onchange.py index 87fc8229b7c..a6e2bff1b35 100644 --- a/odoo/addons/test_new_api/tests/test_onchange.py +++ b/odoo/addons/test_new_api/tests/test_onchange.py @@ -176,7 +176,7 @@ class TestOnChange(common.TransactionCase): self.assertEqual(values, { 'name': partner.name, 'partner': partner.id, - 'lines': [(5,), (4, line.id)], + 'lines': [(6, 0, [line.id])], }) # modify 'partner' diff --git a/odoo/addons/test_new_api/tests/test_one2many.py b/odoo/addons/test_new_api/tests/test_one2many.py new file mode 100644 index 00000000000..38dee13ab54 --- /dev/null +++ b/odoo/addons/test_new_api/tests/test_one2many.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +from openerp.tests.common import TransactionCase + + +class One2manyCase(TransactionCase): + def setUp(self): + super(One2manyCase, self).setUp() + self.Line = self.env["test_new_api.multi.line"] + self.multi = self.env["test_new_api.multi"].create({ + "name": "What is up?" + }) + + def operations(self): + """Run operations on o2m fields to check all works fine.""" + # Check the lines first + self.assertItemsEqual( + self.multi.lines.mapped('name'), + map(str, range(10))) + # Modify the first line and drop the last one + self.multi.lines[0].name = "hello" + self.multi.lines = self.multi.lines[:-1] + self.assertEqual(len(self.multi.lines), 9) + self.assertIn("hello", self.multi.lines.mapped('name')) + # Invalidate the cache and check again; this crashes if the value + # of self.multi.lines in cache contains new records + self.multi.invalidate_cache() + self.assertEqual(len(self.multi.lines), 9) + self.assertIn("hello", self.multi.lines.mapped('name')) + + def test_new_one_by_one(self): + """Check lines created with ``new()`` and appended one by one.""" + for name in range(10): + self.multi.lines |= self.Line.new({"name": str(name)}) + self.operations() + + def test_new_single(self): + """Check lines created with ``new()`` and added in one step.""" + self.multi.lines = self.Line.browse( + [self.Line.new({"name": str(name)}).id for name in range(10)] + ) + self.operations() + + def test_create_one_by_one(self): + """Check lines created with ``create()`` and appended one by one.""" + for name in range(10): + self.multi.lines |= self.Line.create({"name": str(name)}) + self.operations() + + def test_create_single(self): + """Check lines created with ``create()`` and added in one step.""" + self.multi.lines = self.Line.browse( + [self.Line.create({"name": str(name)}).id for name in range(10)] + ) + self.operations() + + def test_rpcstyle_one_by_one(self): + """Check lines created with RPC style and appended one by one.""" + for name in range(10): + self.multi.lines = [(0, 0, {"name": str(name)})] + self.operations() + + def test_rpcstyle_single(self): + """Check lines created with RPC style and added in one step""" + self.multi.lines = [(0, 0, {'name': str(name)}) for name in range(10)] + self.operations() diff --git a/odoo/addons/test_pylint/tests/test_pylint.py b/odoo/addons/test_pylint/tests/test_pylint.py index 251db1f6eba..38fa4c3d51a 100644 --- a/odoo/addons/test_pylint/tests/test_pylint.py +++ b/odoo/addons/test_pylint/tests/test_pylint.py @@ -22,6 +22,7 @@ _logger = logging.getLogger(__name__) class TestPyLint(TransactionCase): ENABLED_CODES = [ + 'E0601', # using variable before assignment 'W0123', # eval used 'W0101', # unreachable code ] diff --git a/odoo/fields.py b/odoo/fields.py index ee732b5b62f..c542945a781 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -894,10 +894,12 @@ class Field(object): env.invalidate(spec) else: - # simply write to the database, and update cache + # Write to database write_value = self.convert_to_write(self.convert_to_record(value, record), record) record.write({self.name: write_value}) - record._cache[self] = value + # Update the cache unless value contains a new record + if not (self.relational and not all(value)): + record._cache[self] = value ############################################################################ # @@ -1987,7 +1989,7 @@ class _RelationalMulti(_Relational): def convert_to_write(self, value, record): # make result with new and existing records - result = [(5,)] + result = [(6, 0, [])] for record in value: if not record.id: values = {name: record[name] for name in record._cache} @@ -1998,7 +2000,7 @@ class _RelationalMulti(_Relational): values = record._convert_to_write(values) result.append((1, record.id, values)) else: - result.append((4, record.id)) + result[0][2].append(record.id) return result def convert_to_onchange(self, value, record, fnames=()): @@ -2171,7 +2173,11 @@ class One2many(_RelationalMulti): query = "SELECT id FROM %s WHERE %s=%%s AND id <> ALL(%%s)" % (comodel._table, inverse) comodel._cr.execute(query, (record.id, act[2] or [0])) lines = comodel.browse([row[0] for row in comodel._cr.fetchall()]) - lines.write({inverse: False}) + inverse_field = comodel._fields[inverse] + if inverse_field.ondelete == 'cascade': + lines.unlink() + else: + lines.write({inverse: False}) class Many2many(_RelationalMulti): diff --git a/odoo/models.py b/odoo/models.py index 9270782eadb..8f15a59d7cd 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -738,7 +738,7 @@ class BaseModel(object): return '__export__.' + name @api.multi - def __export_rows(self, fields): + def _export_rows(self, fields): """ Export fields of the records in ``self``. :param fields: list of lists of fields to traverse @@ -785,7 +785,7 @@ class BaseModel(object): # recursively export the fields that follow name fields2 = [(p[1:] if p and p[0] == name else []) for p in fields] - lines2 = value.__export_rows(fields2) + lines2 = value._export_rows(fields2) if lines2: # merge first line with record's main line for j, val in enumerate(lines2[0]): @@ -804,6 +804,9 @@ class BaseModel(object): return lines + # backward compatibility + __export_rows = _export_rows + @api.multi def export_data(self, fields_to_export, raw_data=False): """ Export fields for selected objects @@ -817,7 +820,7 @@ class BaseModel(object): fields_to_export = map(fix_import_export_id_paths, fields_to_export) if raw_data: self = self.with_context(export_raw_data=True) - return {'datas': self.__export_rows(fields_to_export)} + return {'datas': self._export_rows(fields_to_export)} @api.model def load(self, fields, data): @@ -3505,8 +3508,7 @@ class BaseModel(object): ``(6, _, ids)`` replaces all existing records in the set by the ``ids`` list, equivalent to using the command ``5`` followed by a command - ``4`` for each ``id`` in ``ids``. Can not be used on - :class:`~odoo.fields.One2many`. + ``4`` for each ``id`` in ``ids``. .. note:: Values marked as ``_`` in the list above are ignored and can be anything, generally ``0`` or ``False``. diff --git a/odoo/tools/config.py b/odoo/tools/config.py index 5584002c520..ea030a80f10 100644 --- a/odoo/tools/config.py +++ b/odoo/tools/config.py @@ -363,7 +363,7 @@ class configmanager(object): rcfilepath = old_rcfilepath self.rcfile = os.path.abspath( - self.config_file or opt.config or os.environ.get('OPENERP_SERVER') or rcfilepath) + self.config_file or opt.config or os.environ.get('ODOO_RC') or os.environ.get('OPENERP_SERVER') or rcfilepath) self.load() # Verify that we want to log or not, if not the output will go to stdout |
