From 3ae382f8df28b3b7482325b147796b73b1e357eb Mon Sep 17 00:00:00 2001 From: Joren Van Onder Date: Tue, 20 Feb 2018 17:26:34 -0800 Subject: [PATCH 001/111] [FIX] point_of_sale: do not recompute manually set prices The POS recomputes order lines when the pricelist changes. This is different from the sale app on purpose: in a usual POS flow the customer and thus pricelist is selected after taking the order. But users commonly use a 'Miscellaneous' product when they are selling something that is not in their POS system. The user can select this product and manually set the price. When now a customer is selected (even if the pricelist has not changed) these miscellaneous order lines get recomputed. So now the cashier is forced to input these prices again. To avoid this we keep track of lines that have had their price changed through the UI and we don't update them. opw-817353 --- addons/point_of_sale/static/src/js/models.js | 10 ++++++++-- addons/point_of_sale/static/src/js/screens.js | 4 +++- addons/point_of_sale/static/src/js/tests.js | 17 +++++++++++++++-- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index aa5982091d3..e99d0a1255a 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -1304,6 +1304,7 @@ exports.Orderline = Backbone.Model.extend({ this.type = 'unit'; this.selected = false; this.id = orderline_id++; + this.price_manually_set = false; if (options.price) { this.set_unit_price(options.price); @@ -1344,6 +1345,7 @@ exports.Orderline = Backbone.Model.extend({ orderline.price = this.price; orderline.type = this.type; orderline.selected = false; + orderline.price_manually_set = this.price_manually_set; return orderline; }, set_product_lot: function(product){ @@ -1394,7 +1396,7 @@ exports.Orderline = Backbone.Model.extend({ } // just like in sale.order changing the quantity will recompute the unit price - if(! keep_price){ + if(! keep_price && ! this.price_manually_set){ this.set_unit_price(this.product.get_price(this.order.pricelist, this.get_quantity())); this.order.fix_tax_included_price(this); } @@ -2231,7 +2233,11 @@ exports.Order = Backbone.Model.extend({ set_pricelist: function (pricelist) { var self = this; this.pricelist = pricelist; - _.each(this.get_orderlines(), function (line) { + + var lines_to_recompute = _.filter(this.get_orderlines(), function (line) { + return ! line.price_manually_set; + }); + _.each(lines_to_recompute, function (line) { line.set_unit_price(line.product.get_price(self.pricelist, line.get_quantity())); self.fix_tax_included_price(line); }); diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index dadd9cffbb8..6117767b32c 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -502,7 +502,9 @@ var OrderWidget = PosBaseWidget.extend({ }else if( mode === 'discount'){ order.get_selected_orderline().set_discount(val); }else if( mode === 'price'){ - order.get_selected_orderline().set_unit_price(val); + var selected_orderline = order.get_selected_orderline(); + selected_orderline.price_manually_set = true; + selected_orderline.set_unit_price(val); } } }, diff --git a/addons/point_of_sale/static/src/js/tests.js b/addons/point_of_sale/static/src/js/tests.js index 6c5c970f545..0d303df8c7e 100644 --- a/addons/point_of_sale/static/src/js/tests.js +++ b/addons/point_of_sale/static/src/js/tests.js @@ -166,6 +166,18 @@ odoo.define('point_of_sale.tour.pricelist', function (require) { }, { content: "verify that unit price of oranges changed to $1", trigger: ".total > .value:contains('$ 2.00')", + }, { + content: "order different oranges", + trigger: ".product:contains('Orange Butterfly')", + }, { + content: "change to price mode", + trigger: ".numpad button:contains('Price')", + }, { + content: "manually override the unit price of these oranges to $5", + trigger: ".numpad button.input-button:visible:contains('5')", + }, { + content: "change back to qty mode", + trigger: ".numpad button:contains('Qty')", }, { content: "click pricelist button", trigger: ".control-button.o_pricelist_button", @@ -173,8 +185,9 @@ odoo.define('point_of_sale.tour.pricelist', function (require) { content: "select public pricelist", trigger: ".selection-item:contains('Public Pricelist')", }, { - content: "verify that order has been recomputed", - trigger: ".total > .value:contains('$ 3.96')", + content: "verify that the boni oranges have been recomputed and the\ +butterfly oranges have not (their price was manually overriden)", + trigger: ".total > .value:contains('$ 8.96')", }, { content: "click pricelist button", trigger: ".control-button.o_pricelist_button", From 97cae87812e60b63d61e09b5974c1e50c316de16 Mon Sep 17 00:00:00 2001 From: Romain Derie Date: Wed, 21 Feb 2018 18:26:02 +0400 Subject: [PATCH 002/111] [FIX] portal: use user timezone to display hours on portal chatter Before this commit: User timezone would be taken into account when displaying message date on chatter. It would always display the hour in the server format (UTC). Now, we correctly use the user timezone to format the date being sent by the server. This closes #22812 --- addons/portal/static/src/js/portal_chatter.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/portal/static/src/js/portal_chatter.js b/addons/portal/static/src/js/portal_chatter.js index a35a97ed09b..977a1576c77 100644 --- a/addons/portal/static/src/js/portal_chatter.js +++ b/addons/portal/static/src/js/portal_chatter.js @@ -6,6 +6,7 @@ var ajax = require('web.ajax'); var core = require('web.core'); var Widget = require('web.Widget'); var rpc = require('web.rpc'); +var time = require('web.time'); var qweb = core.qweb; var _t = core._t; @@ -98,7 +99,7 @@ var PortalChatter = Widget.extend({ preprocessMessages: function(messages){ _.each(messages, function(m){ m['author_avatar_url'] = _.str.sprintf('/web/image/%s/%s/author_avatar/50x50', 'mail.message', m.id); - m['published_date_str'] = _.str.sprintf(_t('Published on %s'), moment(m.date).format('MMMM Do YYYY, h:mm:ss a')); + m['published_date_str'] = _.str.sprintf(_t('Published on %s'), moment(time.str_to_datetime(m.date)).format('MMMM Do YYYY, h:mm:ss a')); }); return messages; }, From 8e7aaf8c82df80b28fa5f3fc7973ef9e49f29d97 Mon Sep 17 00:00:00 2001 From: Adrian Torres Date: Thu, 22 Feb 2018 09:17:23 +0100 Subject: [PATCH 003/111] [FIX] sale_management/account_invoicing: properly reinstall Commit 985c224 introduced a way to properly uninstall these two modules, however if a user were to reinstall them the views would stay inactive, in the case of sale_management this would mean some menus were missing, in the case of account_invoicing this meant that not even the top-level menu would not appear, effectively misleading users into thinking the module was not installed. The solution is to create a post_init_hook (to ensure that all potentially missing menus are already installed) that activates the previously deactivated menus. Fixes #23245 --- addons/account_invoicing/__init__.py | 9 +++++++++ addons/account_invoicing/__manifest__.py | 1 + addons/sale_management/__init__.py | 9 +++++++++ addons/sale_management/__manifest__.py | 1 + 4 files changed, 20 insertions(+) diff --git a/addons/account_invoicing/__init__.py b/addons/account_invoicing/__init__.py index 91206cf6094..371975a409a 100644 --- a/addons/account_invoicing/__init__.py +++ b/addons/account_invoicing/__init__.py @@ -11,3 +11,12 @@ def uninstall_hook(cr, registry): ('module', '=', 'account') ]).mapped('res_id') env['ir.ui.menu'].browse(res_ids).update({'active': False}) + + +def post_init_hook(cr, registry): + env = Environment(cr, SUPERUSER_ID, {}) + res_ids = env['ir.model.data'].search([ + ('model', '=', 'ir.ui.menu'), + ('module', '=', 'account'), + ]).mapped('res_id') + env['ir.ui.menu'].browse(res_ids).update({'active': True}) diff --git a/addons/account_invoicing/__manifest__.py b/addons/account_invoicing/__manifest__.py index 103f72fdd12..789d02c43d3 100644 --- a/addons/account_invoicing/__manifest__.py +++ b/addons/account_invoicing/__manifest__.py @@ -29,4 +29,5 @@ You could use this simplified accounting in case you work with an (external) acc ], 'application': True, 'uninstall_hook': 'uninstall_hook', + 'post_init_hook': 'post_init_hook', } diff --git a/addons/sale_management/__init__.py b/addons/sale_management/__init__.py index 1b2e5977577..086ab956265 100644 --- a/addons/sale_management/__init__.py +++ b/addons/sale_management/__init__.py @@ -11,3 +11,12 @@ def uninstall_hook(cr, registry): ('module', '=', 'sale') ]).mapped('res_id') env['ir.ui.menu'].browse(res_ids).update({'active': False}) + + +def post_init_hook(cr, registry): + env = Environment(cr, SUPERUSER_ID, {}) + res_ids = env['ir.model.data'].search([ + ('model', '=', 'ir.ui.menu'), + ('module', '=', 'sale'), + ]).mapped('res_id') + env['ir.ui.menu'].browse(res_ids).update({'active': True}) diff --git a/addons/sale_management/__manifest__.py b/addons/sale_management/__manifest__.py index 4202ac31f78..c4584b0acc9 100644 --- a/addons/sale_management/__manifest__.py +++ b/addons/sale_management/__manifest__.py @@ -48,4 +48,5 @@ The Dashboard for the Sales Manager will include ], 'application': True, 'uninstall_hook': 'uninstall_hook', + 'post_init_hook': 'post_init_hook', } From ab37094ec44ff841a0d9a20781622565b52b844c Mon Sep 17 00:00:00 2001 From: "Lucas Perais (lpe)" Date: Thu, 22 Feb 2018 09:45:47 +0100 Subject: [PATCH 004/111] [FIX] website_quote: default require_payment instead of compute Commit a0f8ee92c3692f7d12652ec38a5e62f2cbddf0a8 solved the use case where a quotation was created through the website shop and where the default quote's template's require payment should be transmitted to the quote. However, this commit made the field readonly This present commit solves both problems by assigning a default to the problematic field rather than computing it OPW 817510 --- addons/website_quote/models/sale_order.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/addons/website_quote/models/sale_order.py b/addons/website_quote/models/sale_order.py index b66e09f87c4..96b34ef657b 100644 --- a/addons/website_quote/models/sale_order.py +++ b/addons/website_quote/models/sale_order.py @@ -49,6 +49,15 @@ class SaleOrder(models.Model): def _get_default_template_id(self): return self.env.ref('website_quote.website_quote_template_default', raise_if_not_found=False) + def _default_require_payment(self): + default_template = self._get_default_template_id() + if self.template_id: + return self.template_id.require_payment + elif default_template: + return default_template.require_payment + else: + return 0 + access_token = fields.Char( 'Security Token', copy=False, default=lambda self: str(uuid.uuid4()), required=True) @@ -68,14 +77,7 @@ class SaleOrder(models.Model): (0, 'Not mandatory on website quote validation'), (1, 'Immediate after website order validation'), (2, 'Immediate after website order validation and save a token'), - ], 'Payment', help="Require immediate payment by the customer when validating the order from the website quote", compute='_compute_require_payment', store=True) - - @api.one - @api.depends('template_id') - def _compute_require_payment(self): - # Do not overwrite if the field is already set - if self.template_id and (self.require_payment is None or self.require_payment is False): - self.require_payment = self.template_id.require_payment + ], 'Payment', help="Require immediate payment by the customer when validating the order from the website quote", default=_default_require_payment) @api.multi def copy(self, default=None): From cd21ea414e9b16e0a1044cfdf26aba744b269c8b Mon Sep 17 00:00:00 2001 From: Christophe Monniez Date: Thu, 22 Feb 2018 10:22:42 +0100 Subject: [PATCH 005/111] [FIX] packaging: include missing files needed for tests The test_mimetypes and base_import modules need various files for their tests. Before this commit some of those files (.ods, .odt, .xlsx, .docx, .zip) were not included in the packages. Fixes #22778 --- MANIFEST.in | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/MANIFEST.in b/MANIFEST.in index baa2ef45201..426b97fbcf6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,6 +4,7 @@ include README.md recursive-include openerp *.css recursive-include openerp *.csv recursive-include openerp *.doc +recursive-include openerp *.docx recursive-include openerp *.eml recursive-include openerp *.eot recursive-include openerp *.gif @@ -15,6 +16,8 @@ recursive-include openerp *.js recursive-include openerp *.less recursive-include openerp *.md recursive-include openerp *.mp3 +recursive-include openerp *.ods +recursive-include openerp *.odt recursive-include openerp *.ogg recursive-include openerp *.otf recursive-include openerp *.pdf @@ -33,9 +36,11 @@ recursive-include openerp *.woff recursive-include openerp *.woff2 recursive-include openerp *.wsdl recursive-include openerp *.xls +recursive-include openerp *.xlsx recursive-include openerp *.xsd recursive-include openerp *.xsl recursive-include openerp *.xml recursive-include openerp *.yml +recursive-include openerp *.zip recursive-exclude * *.py[co] recursive-exclude * *.hg* From 521c29317c843d2f069650c98a1aff1565d65220 Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Thu, 22 Feb 2018 10:15:58 +0100 Subject: [PATCH 006/111] [FIX] l10n_be_hr_payroll: Adapt the warrant costs to the new 20% law https://www.claeysengels.be/en-gb/news-events/m/a/rulingdienst-begrenst-gebruik-warrants-en-opties-op-bevek-aandelen-invoering-van-de-20-grens/ The law seems to change every 2 days. --- addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py b/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py index 9c85c8ac50b..2009f4e2f03 100644 --- a/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py +++ b/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py @@ -116,8 +116,8 @@ class HrContract(models.Model): @api.depends('commission_on_target') def _compute_warrants_cost(self): for contract in self: - contract.warrants_cost = contract.commission_on_target * 1.05 * 12.0 - contract.warrant_value_employee = contract.commission_on_target * (1.00 - 0.535) * 12.0 + contract.warrants_cost = contract.commission_on_target * 1.326 * 1.05 * 12.0 + contract.warrant_value_employee = contract.commission_on_target * 1.326 * (1.00 - 0.535) * 12.0 @api.depends('wage', 'fuel_card', 'representation_fees', 'transport_employer_cost', 'internet', 'mobile', 'mobile_plus') From 310e3c15fd34b0a5d38bc4bfe3eb3e013ce17948 Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Wed, 21 Feb 2018 15:58:50 +0100 Subject: [PATCH 007/111] [FIX] pos_discount: default product Several issues: - If product `product_product_consumable` was deleted, an error is raised. - The default values are inconsistent with the fact that `module_pos_discount` is `False` at POS creation. - The domain should ensures that both `available_in_pos` and `sale_ok` are `True`, otherwise the product won't be loaded by the POS. opw-814426 --- addons/pos_discount/models/pos_config.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/addons/pos_discount/models/pos_config.py b/addons/pos_discount/models/pos_config.py index 0c21d482f17..46272218f67 100644 --- a/addons/pos_discount/models/pos_config.py +++ b/addons/pos_discount/models/pos_config.py @@ -7,17 +7,18 @@ from odoo import api, fields, models class PosConfig(models.Model): _inherit = 'pos.config' - def _get_default_discount_product(self): - return self.env.ref('point_of_sale.product_product_consumable') - iface_discount = fields.Boolean(string='Order Discounts', help='Allow the cashier to give discounts on the whole order.') - discount_pc = fields.Float(string='Discount Percentage', default=10, help='The default discount percentage') - discount_product_id = fields.Many2one('product.product', string='Discount Product', domain="[('available_in_pos', '=', True)]", help='The product used to model the discount.', default=_get_default_discount_product) + discount_pc = fields.Float(string='Discount Percentage', help='The default discount percentage') + discount_product_id = fields.Many2one('product.product', string='Discount Product', + domain="[('available_in_pos', '=', True), ('sale_ok', '=', True)]", help='The product used to model the discount.') @api.onchange('module_pos_discount') def _onchange_module_pos_discount(self): if self.module_pos_discount: - self.discount_product_id = self.env['product.product'].search([('available_in_pos', '=', True)], limit=1) + self.discount_product_id = self.env.ref('point_of_sale.product_product_consumable', raise_if_not_found=False) + if not self.discount_product_id or not self.discount_product_id.available_in_pos or not self.discount_product_id.sale_ok: + domain = [('available_in_pos', '=', True), ('sale_ok', '=', True)] + self.discount_product_id = self.env['product.product'].search(domain, limit=1) self.discount_pc = 10.0 else: self.discount_product_id = False From 075f13ac63ec27fc5d7d86879a285b95c4dc38d8 Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Thu, 22 Feb 2018 10:02:21 +0100 Subject: [PATCH 008/111] [FIX] pos_discount: error if misconfigured product In case the discount product is misconfigured and therefore not loaded by the POS, a traceback appears when applying a discount. Add a comprehensive error message instead. opw-817527 --- addons/pos_discount/i18n/pos_discount.pot | 14 ++++++++++++++ addons/pos_discount/static/src/js/discount.js | 7 +++++++ 2 files changed, 21 insertions(+) diff --git a/addons/pos_discount/i18n/pos_discount.pot b/addons/pos_discount/i18n/pos_discount.pot index 50ac5572f33..57ba3ea7e2c 100644 --- a/addons/pos_discount/i18n/pos_discount.pot +++ b/addons/pos_discount/i18n/pos_discount.pot @@ -46,6 +46,13 @@ msgstr "" msgid "Discount Product" msgstr "" +#. module: pos_discount +#. openerp-web +#: code:addons/pos_discount/static/src/js/discount.js:28 +#, python-format +msgid "No discount product found" +msgstr "" + #. module: pos_discount #: model:ir.model.fields,field_description:pos_discount.field_pos_config_iface_discount msgid "Order Discounts" @@ -56,6 +63,13 @@ msgstr "" msgid "The default discount percentage" msgstr "" +#. module: pos_discount +#. openerp-web +#: code:addons/pos_discount/static/src/js/discount.js:29 +#, python-format +msgid "The discount product seems misconfigured. Make sure it is flagged as 'Can be Sold' and 'Available in Point of Sale'." +msgstr "" + #. module: pos_discount #: model:ir.model.fields,help:pos_discount.field_pos_config_discount_product_id msgid "The product used to model the discount." diff --git a/addons/pos_discount/static/src/js/discount.js b/addons/pos_discount/static/src/js/discount.js index e77e4559e0c..771992bb01b 100644 --- a/addons/pos_discount/static/src/js/discount.js +++ b/addons/pos_discount/static/src/js/discount.js @@ -23,6 +23,13 @@ var DiscountButton = screens.ActionButtonWidget.extend({ var order = this.pos.get_order(); var lines = order.get_orderlines(); var product = this.pos.db.get_product_by_id(this.pos.config.discount_product_id[0]); + if (product === undefined) { + this.gui.show_popup('error', { + title : _t("No discount product found"), + body : _t("The discount product seems misconfigured. Make sure it is flagged as 'Can be Sold' and 'Available in Point of Sale'."), + }); + return; + } // Remove existing discounts var i = 0; From 5cdefcc39a75f7f77f00bf0ab6e999d8461667d0 Mon Sep 17 00:00:00 2001 From: Goffin Simon Date: Wed, 21 Feb 2018 15:31:40 +0100 Subject: [PATCH 009/111] [FIX] purchase: Adding PO on refund vendor bill The function _prepare_invoice_line_from_po_line has not been designed to compute in a right the quantity for a line in an in_refund invoice. So it has no sens to display the field purchase_id for a refund vendor bill. Closes #23095 opw:816717 --- addons/purchase/views/account_invoice_views.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/purchase/views/account_invoice_views.xml b/addons/purchase/views/account_invoice_views.xml index 3f23f6777d0..583f2f40b56 100644 --- a/addons/purchase/views/account_invoice_views.xml +++ b/addons/purchase/views/account_invoice_views.xml @@ -59,7 +59,7 @@ - From 0da5d63f526398022f918bcfdf082c75f26a8b2e Mon Sep 17 00:00:00 2001 From: Goffin Simon Date: Wed, 21 Feb 2018 10:26:42 +0100 Subject: [PATCH 010/111] [FIX] account: Register a partial payment as fully paid Due to this commit: 3267e765209ee7a19b327620622fa1b5126eed00 Steps to reproduce the bug: When registering a payment for a customer invoice in an other currency than the company currency and marking it as fully paid, it raised an error: "Wrong credit or debit value in accounting entry !". Reason: counterpart_aml['debit'] and counterpart_aml['credit'] were both incremented and it violated the constraint credit*debit=0 opw:815465 --- addons/account/models/account_payment.py | 4 +- addons/account/tests/test_payment.py | 50 ++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index 2456f86a066..a6e01c16fe5 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -571,9 +571,9 @@ class account_payment(models.Model): writeoff_line['amount_currency'] = amount_currency_wo writeoff_line['currency_id'] = currency_id writeoff_line = aml_obj.create(writeoff_line) - if counterpart_aml['debit'] or writeoff_line['credit']: + if counterpart_aml['debit'] or (writeoff_line['credit'] and not counterpart_aml['credit']): counterpart_aml['debit'] += credit_wo - debit_wo - if counterpart_aml['credit'] or writeoff_line['debit']: + if counterpart_aml['credit'] or (writeoff_line['debit'] and not counterpart_aml['debit']): counterpart_aml['credit'] += debit_wo - credit_wo counterpart_aml['amount_currency'] -= amount_currency_wo diff --git a/addons/account/tests/test_payment.py b/addons/account/tests/test_payment.py index 329402d99f8..46a75581fc2 100644 --- a/addons/account/tests/test_payment.py +++ b/addons/account/tests/test_payment.py @@ -318,3 +318,53 @@ class TestPayment(AccountingTestCase): self.assertEqual(payment_id.payment_type, 'outbound') self.assertEqual(payment_id.partner_id, self.partner_china_exp) self.assertEqual(payment_id.partner_type, 'supplier') + + def test_payment_and_writeoff_in_other_currency(self): + # Use case: + # Company is in EUR, create a customer invoice for 25 EUR and register payment of 25 USD. + # Mark invoice as fully paid with a write_off + # Check that all the aml are correctly created. + invoice = self.create_invoice(amount=25, type='out_invoice', currency_id=self.currency_eur_id, partner=self.partner_agrolait.id) + # register payment on invoice + payment = self.payment_model.create({'payment_type': 'inbound', + 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id, + 'partner_type': 'customer', + 'partner_id': self.partner_agrolait.id, + 'amount': 25, + 'currency_id': self.currency_usd_id, + 'payment_date': time.strftime('%Y') + '-07-15', + 'payment_difference_handling': 'reconcile', + 'writeoff_account_id': self.account_payable.id, + 'journal_id': self.bank_journal_euro.id, + 'invoice_ids': [(4, invoice.id, None)] + }) + payment.post() + self.check_journal_items(payment.move_line_ids, [ + {'account_id': self.account_eur.id, 'debit': 16.35, 'credit': 0.0, 'amount_currency': 25.0, 'currency_id': self.currency_usd_id}, + {'account_id': self.account_payable.id, 'debit': 8.65, 'credit': 0.0, 'amount_currency': 13.22, 'currency_id': self.currency_usd_id}, + {'account_id': self.account_receivable.id, 'debit': 0.0, 'credit': 25.0, 'amount_currency': -38.22, 'currency_id': self.currency_usd_id}, + ]) + # Use case: + # Company is in EUR, create a vendor bill for 25 EUR and register payment of 25 USD. + # Mark invoice as fully paid with a write_off + # Check that all the aml are correctly created. + invoice = self.create_invoice(amount=25, type='in_invoice', currency_id=self.currency_eur_id, partner=self.partner_agrolait.id) + # register payment on invoice + payment = self.payment_model.create({'payment_type': 'inbound', + 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id, + 'partner_type': 'supplier', + 'partner_id': self.partner_agrolait.id, + 'amount': 25, + 'currency_id': self.currency_usd_id, + 'payment_date': time.strftime('%Y') + '-07-15', + 'payment_difference_handling': 'reconcile', + 'writeoff_account_id': self.account_payable.id, + 'journal_id': self.bank_journal_euro.id, + 'invoice_ids': [(4, invoice.id, None)] + }) + payment.post() + self.check_journal_items(payment.move_line_ids, [ + {'account_id': self.account_eur.id, 'debit': 16.35, 'credit': 0.0, 'amount_currency': 25.0, 'currency_id': self.currency_usd_id}, + {'account_id': self.account_payable.id, 'debit': 0.0, 'credit': 8.65, 'amount_currency': -13.22, 'currency_id': self.currency_usd_id}, + {'account_id': self.account_receivable.id, 'debit': 0.0, 'credit': 7.7, 'amount_currency': -11.78, 'currency_id': self.currency_usd_id}, + ]) From f2501bfd3c01bd8d98897da70af537a3526cdaaf Mon Sep 17 00:00:00 2001 From: Dennis Sluijk Date: Thu, 22 Feb 2018 12:01:57 +0100 Subject: [PATCH 011/111] [FIX] web: return dialog in web.CrashManager functions This is more a architecture fix than a bug fix. With this commit, it is easier to modify the behavior of the dialog using the super function. like this: this._super.apply(this, arguments).opened(...) PR #23255 --- addons/web/static/src/js/services/crash_manager.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/addons/web/static/src/js/services/crash_manager.js b/addons/web/static/src/js/services/crash_manager.js index 9c7914eeebf..ccc7e089e65 100644 --- a/addons/web/static/src/js/services/crash_manager.js +++ b/addons/web/static/src/js/services/crash_manager.js @@ -103,7 +103,7 @@ var CrashManager = core.Class.extend({ if (!this.active) { return; } - new Dialog(this, { + return new Dialog(this, { size: 'medium', title: _.str.capitalize(error.type || error.message) || _t("Odoo Warning"), subtitle: error.data.title, @@ -149,10 +149,10 @@ var CrashManager = core.Class.extend({ clipboard.destroy(); }); - dialog.open(); + return dialog.open(); }, show_message: function(exception) { - this.show_error({ + return this.show_error({ type: _t("Odoo Client Error"), message: exception, data: {debug: ""} From ad3cc1b231f0284c15b515f36454b67fcdb2cd0e Mon Sep 17 00:00:00 2001 From: Duc Dao Date: Sat, 10 Feb 2018 01:18:20 +0700 Subject: [PATCH 012/111] [FIX] resource: Fix default empty timezone Since the method `_get_leave_intervals` assumed that field `tz` is always filled in. There is a case in default function, when `self._context` had key `tz` but its value was `False`, which may causes null value of field `tz` and lead to mismatch interval between leave and payslip --- addons/resource/models/resource.py | 4 ++-- doc/cla/individual/novawish.md | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 doc/cla/individual/novawish.md diff --git a/addons/resource/models/resource.py b/addons/resource/models/resource.py index 7002961fd21..87bb3ebd37a 100644 --- a/addons/resource/models/resource.py +++ b/addons/resource/models/resource.py @@ -699,8 +699,8 @@ class ResourceCalendarLeaves(models.Model): date_from = fields.Datetime('Start Date', required=True) date_to = fields.Datetime('End Date', required=True) tz = fields.Selection( - _tz_get, string='Timezone', default=lambda self: self._context.get('tz', self.env.user.tz or 'UTC'), - help="Timezone used when encoding the leave. It is used to correctly" + _tz_get, string='Timezone', default=lambda self: self._context.get('tz') or self.env.user.tz or 'UTC', + help="Timezone used when encoding the leave. It is used to correctly " "localize leave hours when computing time intervals.") resource_id = fields.Many2one( "resource.resource", 'Resource', diff --git a/doc/cla/individual/novawish.md b/doc/cla/individual/novawish.md new file mode 100644 index 00000000000..7cb30078983 --- /dev/null +++ b/doc/cla/individual/novawish.md @@ -0,0 +1,11 @@ +Vietnam, 2018-02-10 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Duc Dao duc.dd@komit-consulting.com https://github.com/novawish From fbbd070a21139819fd146e5bbdbe60fdfb726874 Mon Sep 17 00:00:00 2001 From: Nicolas Lempereur Date: Thu, 22 Feb 2018 14:27:54 +0100 Subject: [PATCH 013/111] [FIX] point_of_sale: make pricelist tour pass The change in 3ae382f8d3a caused a similar error to ef91c141f6d. So the tour failed in enterprise because account_taxcloud caused a customer tax to be added to the "Orange Butterfly" product. --- addons/point_of_sale/data/point_of_sale_demo.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/point_of_sale/data/point_of_sale_demo.xml b/addons/point_of_sale/data/point_of_sale_demo.xml index 1c18cb8e74c..759d3fbec74 100644 --- a/addons/point_of_sale/data/point_of_sale_demo.xml +++ b/addons/point_of_sale/data/point_of_sale_demo.xml @@ -51,6 +51,7 @@ True 2.83 Orange Butterfly + False True From 58eea6b3b32a83748b0a960a64ae9fe65c550b7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20K=C3=BChn?= Date: Thu, 22 Feb 2018 15:13:09 +0100 Subject: [PATCH 014/111] [FIX] mail: backport fixes on the composer from saas-11.2 This commit back-port some fixes from saas-11.2 on composer. Tests are available only in saas-11.2 and up. List of fixes: 1. Pressing TAB with @ mention propositions should cycle through them (see https://github.com/odoo/odoo/commit/497a44ab7f04e377decf5fde42f075616f764f26) 2. Prevent a crash when focusing out the emoji button (see https://github.com/odoo/odoo/commit/1753017255f3f078ede05a68842f440e02b64630) --- addons/mail/static/src/js/composer.js | 56 ++++++++++++++++----------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/addons/mail/static/src/js/composer.js b/addons/mail/static/src/js/composer.js index 184f1d66e82..e342074c5bd 100644 --- a/addons/mail/static/src/js/composer.js +++ b/addons/mail/static/src/js/composer.js @@ -145,24 +145,6 @@ var MentionManager = Widget.extend({ return selections; }, - proposition_navigation: function (keycode) { - var $active = this.$('.o_mention_proposition.active'); - if (keycode === $.ui.keyCode.ENTER) { // selecting proposition - $active.click(); - } else { // navigation in propositions - var $to; - if (keycode === $.ui.keyCode.DOWN) { - $to = $active.nextAll('.o_mention_proposition').first(); - } else { - $to = $active.prevAll('.o_mention_proposition').first(); - } - if ($to.length) { - $active.removeClass('active'); - $to.addClass('active'); - } - } - }, - /** * Detects if the user is currently typing a mention word * @return the search string if it is, false otherwise @@ -286,6 +268,32 @@ var MentionManager = Widget.extend({ } return result; }, + /** + * @private + * @param {integer} keycode + */ + _propositionNavigation: function (keycode) { + var $active = this.$('.o_mention_proposition.active'); + if (keycode === $.ui.keyCode.ENTER) { // selecting proposition + $active.click(); + } else { // navigation in propositions + var $to; + if (keycode === $.ui.keyCode.DOWN) { + $to = $active.nextAll('.o_mention_proposition').first(); + } else if (keycode === $.ui.keyCode.UP) { + $to = $active.prevAll('.o_mention_proposition').first(); + } else if (keycode === $.ui.keyCode.TAB) { + $to = $active.nextAll('.o_mention_proposition').first(); + if (!$to.length) { + $to = $active.prevAll('.o_mention_proposition').last(); + } + } + if ($to && $to.length) { + $active.removeClass('active'); + $to.addClass('active'); + } + } + }, _render_suggestions: function () { var suggestions = []; if (_.find(this.get('mention_suggestions'), _.isArray)) { @@ -522,9 +530,10 @@ var BasicComposer = Widget.extend(chat_mixin, { on_keydown: function (event) { switch(event.which) { - // UP, DOWN: prevent moving cursor if navigation in mention propositions + // UP, DOWN, TAB: prevent moving cursor if navigation in mention propositions case $.ui.keyCode.UP: case $.ui.keyCode.DOWN: + case $.ui.keyCode.TAB: if (this.mention_manager.is_open()) { event.preventDefault(); } @@ -560,12 +569,13 @@ var BasicComposer = Widget.extend(chat_mixin, { this.trigger_up("escape_pressed"); } break; - // ENTER, UP, DOWN: check if navigation in mention propositions + // ENTER, UP, DOWN, TAB: check if navigation in mention propositions case $.ui.keyCode.ENTER: case $.ui.keyCode.UP: case $.ui.keyCode.DOWN: + case $.ui.keyCode.TAB: if (this.mention_manager.is_open()) { - this.mention_manager.proposition_navigation(event.which); + this.mention_manager._propositionNavigation(event.which); } break; // Otherwise, check if a mention is typed @@ -814,7 +824,9 @@ var BasicComposer = Widget.extend(chat_mixin, { * @private */ _onEmojiButtonFocusout: function () { - this._hideEmojisTimeout = setTimeout(this._hideEmojis.bind(this), 0); + if (this.$emojisContainer) { + this._hideEmojisTimeout = setTimeout(this._hideEmojis.bind(this), 0); + } }, /** * Called when an emoji is focused -> @see _onEmojiButtonFocusout From 5ce76a0e7099c7da21787d6cfb82d3305678bfe0 Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Thu, 22 Feb 2018 14:55:29 +0100 Subject: [PATCH 015/111] [FIX] hr_payroll, resource: no attendances Method `get_day_work_hours_count` can return 0 if no attendance is found in the calendar. Therefore, we must make sure to not divide by zero, otherwise, boom boom. Closes #22900 opw-1815095 --- addons/hr_payroll/models/hr_payslip.py | 3 ++- addons/resource/models/resource_mixin.py | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/addons/hr_payroll/models/hr_payslip.py b/addons/hr_payroll/models/hr_payslip.py index 726e5efe375..c9d14c47495 100644 --- a/addons/hr_payroll/models/hr_payslip.py +++ b/addons/hr_payroll/models/hr_payslip.py @@ -190,7 +190,8 @@ class HrPayslip(models.Model): leave_time = (interval[1] - interval[0]).seconds / 3600 current_leave_struct['number_of_hours'] += leave_time work_hours = contract.employee_id.get_day_work_hours_count(interval[0].date(), calendar=contract.resource_calendar_id) - current_leave_struct['number_of_days'] += leave_time / work_hours + if work_hours: + current_leave_struct['number_of_days'] += leave_time / work_hours # compute worked days work_data = contract.employee_id.get_work_days_data(day_from, day_to, calendar=contract.resource_calendar_id) diff --git a/addons/resource/models/resource_mixin.py b/addons/resource/models/resource_mixin.py index fd531467998..fcee4086ca3 100644 --- a/addons/resource/models/resource_mixin.py +++ b/addons/resource/models/resource_mixin.py @@ -57,7 +57,8 @@ class ResourceMixin(models.AbstractModel): theoric_hours = self.get_day_work_hours_count(day_intervals[0][0].date(), calendar=calendar) work_time = sum((interval[1] - interval[0] for interval in day_intervals), timedelta()) total_work_time += work_time - days_count += float_utils.round((work_time.total_seconds() / 3600 / theoric_hours) * 4) / 4 + if theoric_hours: + days_count += float_utils.round((work_time.total_seconds() / 3600 / theoric_hours) * 4) / 4 return { 'days': days_count, 'hours': total_work_time.total_seconds() / 3600, @@ -80,7 +81,8 @@ class ResourceMixin(models.AbstractModel): for day_intervals in calendar._iter_leave_intervals(from_datetime, to_datetime, self.resource_id.id): theoric_hours = self.get_day_work_hours_count(day_intervals[0][0].date(), calendar=calendar) leave_time = sum((interval[1] - interval[0] for interval in day_intervals), timedelta()) - days_count += float_utils.round((leave_time.total_seconds() / 3600 / theoric_hours) * 4) / 4 + if theoric_hours: + days_count += float_utils.round((leave_time.total_seconds() / 3600 / theoric_hours) * 4) / 4 return days_count def iter_leaves(self, from_datetime, to_datetime, calendar=None): From 69ef302aa07ccffcf817fc7b8590d73be55c58e0 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Thu, 22 Feb 2018 19:45:42 +0100 Subject: [PATCH 016/111] [FIX] website_quote: call existing method --- addons/website_quote/models/sale_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/website_quote/models/sale_order.py b/addons/website_quote/models/sale_order.py index 777b7af3aad..1dec2fcb023 100644 --- a/addons/website_quote/models/sale_order.py +++ b/addons/website_quote/models/sale_order.py @@ -52,7 +52,7 @@ class SaleOrder(models.Model): return template and template.active and template or False def _get_default_online_payment(self): - default_template = self._get_default_template_id() + default_template = self._get_default_template() if self.template_id: return self.template_id.require_payment elif default_template: From 7b29f410a53f9c2296117f621c146e9172597ed2 Mon Sep 17 00:00:00 2001 From: jem-odoo Date: Thu, 22 Feb 2018 16:56:33 +0100 Subject: [PATCH 017/111] [CLA] Add Monica to Merchise's CLA --- doc/cla/corporate/merchise.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/cla/corporate/merchise.md b/doc/cla/corporate/merchise.md index d8d5e75f830..e4cbd3c3f95 100644 --- a/doc/cla/corporate/merchise.md +++ b/doc/cla/corporate/merchise.md @@ -20,3 +20,4 @@ Sergio Valdés sergiov@merchise.org Medardo Rodríguez med@merchise.org https://github.com/med-merchise Abel Firvida firvida@merchise.org Abel Firvida abel@merchise.org +Mónica Díaz Pena monicadp@merchise.org https://github.com/mdpena From 088d00f2a7968c58ec9bafea0cd90685a5bd2933 Mon Sep 17 00:00:00 2001 From: Monica Diaz Pena Date: Thu, 22 Feb 2018 16:57:17 +0100 Subject: [PATCH 018/111] [FIX] mail: Don't duplicate uuid of mail channels. If you duplicate a channel the UUID should not be copied, otherwise it might introduce security issues and the method 'channel_minimize' will fail with an "expected singleton error". Closes #23102 --- addons/mail/models/mail_channel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index 06a272948ea..fb33fbf1323 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -49,7 +49,7 @@ class Channel(models.Model): ('channel', 'Channel')], 'Channel Type', default='channel') description = fields.Text('Description') - uuid = fields.Char('UUID', size=50, index=True, default=lambda self: '%s' % uuid.uuid4()) + uuid = fields.Char('UUID', size=50, index=True, default=lambda self: '%s' % uuid.uuid4(), copy=False) email_send = fields.Boolean('Send messages by email', default=False) # multi users channel channel_last_seen_partner_ids = fields.One2many('mail.channel.partner', 'channel_id', string='Last Seen') From ab60d773eb84a527b3167d7efaf169f70c5c9786 Mon Sep 17 00:00:00 2001 From: Akim Juillerat Date: Thu, 22 Feb 2018 17:01:50 +0100 Subject: [PATCH 019/111] [FIX] hr_timesheet_sheet: set default company on the company instead of department Code is explicit enough ... Closes #23120 --- addons/hr_timesheet_sheet/models/hr_timesheet_sheet.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/addons/hr_timesheet_sheet/models/hr_timesheet_sheet.py b/addons/hr_timesheet_sheet/models/hr_timesheet_sheet.py index 62f4685a84b..d560569d669 100644 --- a/addons/hr_timesheet_sheet/models/hr_timesheet_sheet.py +++ b/addons/hr_timesheet_sheet/models/hr_timesheet_sheet.py @@ -68,9 +68,8 @@ class HrTimesheetSheet(models.Model): '\n* The \'Waiting Approval\' status is used to confirm the timesheet by user. ' '\n* The \'Approved\' status is used when the users timesheet is accepted by his/her senior.') account_ids = fields.One2many('hr_timesheet_sheet.sheet.account', 'sheet_id', string='Analytic accounts', readonly=True) - company_id = fields.Many2one('res.company', string='Company') - department_id = fields.Many2one('hr.department', string='Department', - default=lambda self: self.env['res.company']._company_default_get()) + company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env['res.company']._company_default_get()) + department_id = fields.Many2one('hr.department', string='Department') @api.constrains('date_to', 'date_from', 'employee_id') def _check_sheet_date(self, forced_user_id=False): From 370c944c2ff5a719fee6cdf7287ad840285c7e2c Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Thu, 22 Feb 2018 09:48:10 +0100 Subject: [PATCH 020/111] [IMP] pos_mercury: do not translate technical error messages Error messages are given by vendor and have specific meaning Translating it does not specifally help to find out more about the problem pos_mecury is intended for US market anyway --- addons/pos_mercury/i18n/pos_mercury.pot | 516 +----------------- .../pos_mercury/static/src/js/pos_mercury.js | 148 ++--- 2 files changed, 76 insertions(+), 588 deletions(-) diff --git a/addons/pos_mercury/i18n/pos_mercury.pot b/addons/pos_mercury/i18n/pos_mercury.pot index d9029e712a2..9e1f7b776a0 100644 --- a/addons/pos_mercury/i18n/pos_mercury.pot +++ b/addons/pos_mercury/i18n/pos_mercury.pot @@ -6,8 +6,8 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 11.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-02-21 15:41+0000\n" -"PO-Revision-Date: 2018-02-21 15:41+0000\n" +"POT-Creation-Date: 2018-02-23 07:43+0000\n" +"PO-Revision-Date: 2018-02-23 07:43+0000\n" "Last-Translator: <>\n" "Language-Team: \n" "MIME-Version: 1.0\n" @@ -22,13 +22,6 @@ msgstr "" msgid "  APPROVAL CODE:" msgstr "" -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:180 -#, python-format -msgid "128 bit CryptoAPI failed" -msgstr "" - #. module: pos_mercury #: model:ir.ui.view,arch_db:pos_mercury.pos_config_view_form_inherit_pos_mercury msgid "Buy a card reader" @@ -57,13 +50,6 @@ msgstr "" msgid "APPROVAL CODE:" msgstr "" -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:184 -#, python-format -msgid "All Connections Failed" -msgstr "" - #. module: pos_mercury #: model:ir.model,name:pos_mercury.model_account_bank_statement_line msgid "Bank Statement Line" @@ -98,46 +84,11 @@ msgstr "" msgid "Card Reader" msgstr "" -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:190 -#, python-format -msgid "Clear Text Request Not Supported" -msgstr "" - #. module: pos_mercury #: model:ir.actions.act_window,help:pos_mercury.action_configuration_form msgid "Click to configure your card reader." msgstr "" -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:158 -#, python-format -msgid "Connect Canceled" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:155 -#, python-format -msgid "Connection Lost" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:179 -#, python-format -msgid "Control failed to find branded serial (password lookup failed)" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:196 -#, python-format -msgid "Could Not Encrypt Response- Call Provider" -msgstr "" - #. module: pos_mercury #. openerp-web #: code:addons/pos_mercury/static/src/js/pos_mercury.js:351 @@ -163,26 +114,12 @@ msgstr "" msgid "Credit Card" msgstr "" -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:172 -#, python-format -msgid "Disconnecting Socket" -msgstr "" - #. module: pos_mercury #: model:ir.model.fields,field_description:pos_mercury.field_pos_mercury_configuration_display_name #: model:ir.model.fields,field_description:pos_mercury.field_pos_mercury_mercury_transaction_display_name msgid "Display Name" msgstr "" -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:174 -#, python-format -msgid "Duplicate Serial Number Detected" -msgstr "" - #. module: pos_mercury #. openerp-web #: code:addons/pos_mercury/static/src/xml/pos_mercury.xml:7 @@ -190,13 +127,6 @@ msgstr "" msgid "Electronic Payment" msgstr "" -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:167 -#, python-format -msgid "Empty Command String" -msgstr "" - #. module: pos_mercury #. openerp-web #: code:addons/pos_mercury/static/src/js/pos_mercury.js:641 @@ -204,20 +134,6 @@ msgstr "" msgid "Error" msgstr "" -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:192 -#, python-format -msgid "Error Occurred While Decrypting Request" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:182 -#, python-format -msgid "Failed to start Event Thread." -msgstr "" - #. module: pos_mercury #: model:ir.ui.view,arch_db:pos_mercury.view_pos_mercury_configuration_form msgid "For quickly handling orders: just swiping a credit card when on the payment screen\n" @@ -225,34 +141,6 @@ msgid "For quickly handling orders: just swiping a credit card when on the payme " the card." msgstr "" -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:164 -#, python-format -msgid "General Failure" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:149 -#, python-format -msgid "Global API Not Initialized" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:186 -#, python-format -msgid "Global Response Length Error (Too Short)" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:188 -#, python-format -msgid "Global String Error" -msgstr "" - #. module: pos_mercury #. openerp-web #: code:addons/pos_mercury/static/src/js/pos_mercury.js:256 @@ -284,238 +172,6 @@ msgid "If you don't already have a Mercury account, contact Mercury at +1 (800) " to create one." msgstr "" -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:178 -#, python-format -msgid "In Process with server" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:159 -#, python-format -msgid "Initialize Failed" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:166 -#, python-format -msgid "Insufficient Fields" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:177 -#, python-format -msgid "Internal Server Error – Call Provider" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:200 -#, python-format -msgid "Invalid Account Number" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:204 -#, python-format -msgid "Invalid Authorization Amount" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:202 -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:203 -#, python-format -msgid "Invalid Authorization Code" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:219 -#, python-format -msgid "Invalid Batch Item Count" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:218 -#, python-format -msgid "Invalid Batch Number" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:224 -#, python-format -msgid "Invalid Card Type" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:205 -#, python-format -msgid "Invalid Cash Back Amount" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:193 -#, python-format -msgid "Invalid Check Digit" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:213 -#, python-format -msgid "Invalid Check Type" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:165 -#, python-format -msgid "Invalid Command Format" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:212 -#, python-format -msgid "Invalid Date of Birth" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:210 -#, python-format -msgid "Invalid Derived Key Data" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:221 -#, python-format -msgid "Invalid Driver’s License" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:201 -#, python-format -msgid "Invalid Expiration Date" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:206 -#, python-format -msgid "Invalid Gratuity Amount" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:220 -#, python-format -msgid "Invalid MICR Input Type" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:208 -#, python-format -msgid "Invalid Magnetic Stripe Data" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:199 -#, python-format -msgid "Invalid Memo" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:216 -#, python-format -msgid "Invalid Merchant ID" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:198 -#, python-format -msgid "Invalid Operator ID" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:209 -#, python-format -msgid "Invalid PIN Block Data" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:223 -#, python-format -msgid "Invalid Pass Data" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:207 -#, python-format -msgid "Invalid Purchase Amount" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:214 -#, python-format -msgid "Invalid Routing Number" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:222 -#, python-format -msgid "Invalid Sequence Number" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:211 -#, python-format -msgid "Invalid State Code" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:217 -#, python-format -msgid "Invalid TStream Type" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:215 -#, python-format -msgid "Invalid TranCode" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:197 -#, python-format -msgid "Invalid Transaction Type" -msgstr "" - #. module: pos_mercury #: model:ir.model.fields,help:pos_mercury.field_account_bank_statement_line_mercury_invoice_no msgid "Invoice number from Mercury Pay" @@ -549,13 +205,6 @@ msgstr "" msgid "Merchant ID" msgstr "" -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:194 -#, python-format -msgid "Merchant ID Missing" -msgstr "" - #. module: pos_mercury #: model:ir.model.fields,field_description:pos_mercury.field_pos_mercury_configuration_merchant_pwd msgid "Merchant Password" @@ -645,34 +294,6 @@ msgstr "" msgid "Partially approved" msgstr "" -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:175 -#, python-format -msgid "Password Failed (Client / Server)" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:170 -#, python-format -msgid "Password Failed – Disconnecting" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:168 -#, python-format -msgid "Password Verified" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:176 -#, python-format -msgid "Password failed (Challenge / Response)" -msgstr "" - #. module: pos_mercury #: model:ir.model.fields,help:pos_mercury.field_pos_mercury_configuration_merchant_pwd msgid "Password of the merchant to authenticate him on the payment provider server" @@ -700,20 +321,6 @@ msgstr "" msgid "Point of Sale Orders" msgstr "" -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:169 -#, python-format -msgid "Queue Full" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:173 -#, python-format -msgid "Refused ‘Max Connections’" -msgstr "" - #. module: pos_mercury #. openerp-web #: code:addons/pos_mercury/static/src/js/pos_mercury.js:539 @@ -735,55 +342,6 @@ msgstr "" msgid "Sending reversal..." msgstr "" -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:185 -#, python-format -msgid "Server Login Failed" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:154 -#, python-format -msgid "Socket Connection Failed" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:153 -#, python-format -msgid "Socket Creation Failed" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:151 -#, python-format -msgid "Socket Error sending request" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:152 -#, python-format -msgid "Socket already open or in use" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:171 -#, python-format -msgid "System Going Offline" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:156 -#, python-format -msgid "TCP/IP Failed to Initialize" -msgstr "" - #. module: pos_mercury #. openerp-web #: code:addons/pos_mercury/static/src/xml/pos_mercury.xml:47 @@ -791,13 +349,6 @@ msgstr "" msgid "TO CARDHOLDER AGREEMENT" msgstr "" -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:195 -#, python-format -msgid "TStream Type Missing" -msgstr "" - #. module: pos_mercury #: model:ir.model.fields,help:pos_mercury.field_account_bank_statement_line_mercury_card_brand msgid "The brand of the payment card (e.g. Visa, AMEX, ...)" @@ -830,34 +381,6 @@ msgstr "" msgid "This can be caused by a badly executed swipe or by not having your keyboard layout set to US QWERTY (not US International)." msgstr "" -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:181 -#, python-format -msgid "Threaded Auth Started Expect Response" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:157 -#, python-format -msgid "Time Out waiting for server response" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:163 -#, python-format -msgid "Timeout error" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:150 -#, python-format -msgid "Timeout on Response" -msgstr "" - #. module: pos_mercury #. openerp-web #: code:addons/pos_mercury/static/src/js/pos_mercury.js:146 @@ -865,27 +388,6 @@ msgstr "" msgid "Transaction approved" msgstr "" -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:187 -#, python-format -msgid "Unable to Parse Response from Global (Indistinguishable)" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:160 -#, python-format -msgid "Unknown Error" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:191 -#, python-format -msgid "Unrecognized Request Format" -msgstr "" - #. module: pos_mercury #: model:ir.ui.view,arch_db:pos_mercury.view_pos_mercury_configuration_form msgid "Using the Mercury integration in the Point Of Sale is easy: just press the\n" @@ -919,20 +421,6 @@ msgid "We currently support the MagTek Dynamag card reader device. It can be con " directly to the Point Of Sale device or it can be connected to the POSBox." msgstr "" -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:189 -#, python-format -msgid "Weak Encryption Request Not Supported" -msgstr "" - -#. module: pos_mercury -#. openerp-web -#: code:addons/pos_mercury/static/src/js/pos_mercury.js:183 -#, python-format -msgid "XML Parse Error" -msgstr "" - #. module: pos_mercury #. openerp-web #: code:addons/pos_mercury/static/src/xml/pos_mercury.xml:50 diff --git a/addons/pos_mercury/static/src/js/pos_mercury.js b/addons/pos_mercury/static/src/js/pos_mercury.js index 710420c62b4..f6ab924dbec 100644 --- a/addons/pos_mercury/static/src/js/pos_mercury.js +++ b/addons/pos_mercury/static/src/js/pos_mercury.js @@ -146,82 +146,82 @@ var lookUpCodeTransaction = { '000000': _t('Transaction approved'), }, 'TimeoutError': { - '001006': _t('Global API Not Initialized'), - '001007': _t('Timeout on Response'), - '003003': _t('Socket Error sending request'), - '003004': _t('Socket already open or in use'), - '003005': _t('Socket Creation Failed'), - '003006': _t('Socket Connection Failed'), - '003007': _t('Connection Lost'), - '003008': _t('TCP/IP Failed to Initialize'), - '003010': _t('Time Out waiting for server response'), - '003011': _t('Connect Canceled'), - '003053': _t('Initialize Failed'), - '009999': _t('Unknown Error'), + '001006': 'Global API Not Initialized', + '001007': 'Timeout on Response', + '003003': 'Socket Error sending request', + '003004': 'Socket already open or in use', + '003005': 'Socket Creation Failed', + '003006': 'Socket Connection Failed', + '003007': 'Connection Lost', + '003008': 'TCP/IP Failed to Initialize', + '003010': 'Time Out waiting for server response', + '003011': 'Connect Canceled', + '003053': 'Initialize Failed', + '009999': 'Unknown Error', }, 'FatalError': { - '-1': _t('Timeout error'), - '001001': _t('General Failure'), - '001003': _t('Invalid Command Format'), - '001004': _t('Insufficient Fields'), - '001011': _t('Empty Command String'), - '002000': _t('Password Verified'), - '002001': _t('Queue Full'), - '002002': _t('Password Failed – Disconnecting'), - '002003': _t('System Going Offline'), - '002004': _t('Disconnecting Socket'), - '002006': _t('Refused ‘Max Connections’'), - '002008': _t('Duplicate Serial Number Detected'), - '002009': _t('Password Failed (Client / Server)'), - '002010': _t('Password failed (Challenge / Response)'), - '002011': _t('Internal Server Error – Call Provider'), - '003002': _t('In Process with server'), - '003009': _t('Control failed to find branded serial (password lookup failed)'), - '003012': _t('128 bit CryptoAPI failed'), - '003014': _t('Threaded Auth Started Expect Response'), - '003017': _t('Failed to start Event Thread.'), - '003050': _t('XML Parse Error'), - '003051': _t('All Connections Failed'), - '003052': _t('Server Login Failed'), - '004001': _t('Global Response Length Error (Too Short)'), - '004002': _t('Unable to Parse Response from Global (Indistinguishable)'), - '004003': _t('Global String Error'), - '004004': _t('Weak Encryption Request Not Supported'), - '004005': _t('Clear Text Request Not Supported'), - '004010': _t('Unrecognized Request Format'), - '004011': _t('Error Occurred While Decrypting Request'), - '004017': _t('Invalid Check Digit'), - '004018': _t('Merchant ID Missing'), - '004019': _t('TStream Type Missing'), - '004020': _t('Could Not Encrypt Response- Call Provider'), - '100201': _t('Invalid Transaction Type'), - '100202': _t('Invalid Operator ID'), - '100203': _t('Invalid Memo'), - '100204': _t('Invalid Account Number'), - '100205': _t('Invalid Expiration Date'), - '100206': _t('Invalid Authorization Code'), - '100207': _t('Invalid Authorization Code'), - '100208': _t('Invalid Authorization Amount'), - '100209': _t('Invalid Cash Back Amount'), - '100210': _t('Invalid Gratuity Amount'), - '100211': _t('Invalid Purchase Amount'), - '100212': _t('Invalid Magnetic Stripe Data'), - '100213': _t('Invalid PIN Block Data'), - '100214': _t('Invalid Derived Key Data'), - '100215': _t('Invalid State Code'), - '100216': _t('Invalid Date of Birth'), - '100217': _t('Invalid Check Type'), - '100218': _t('Invalid Routing Number'), - '100219': _t('Invalid TranCode'), - '100220': _t('Invalid Merchant ID'), - '100221': _t('Invalid TStream Type'), - '100222': _t('Invalid Batch Number'), - '100223': _t('Invalid Batch Item Count'), - '100224': _t('Invalid MICR Input Type'), - '100225': _t('Invalid Driver’s License'), - '100226': _t('Invalid Sequence Number'), - '100227': _t('Invalid Pass Data'), - '100228': _t('Invalid Card Type'), + '-1': 'Timeout error', + '001001': 'General Failure', + '001003': 'Invalid Command Format', + '001004': 'Insufficient Fields', + '001011': 'Empty Command String', + '002000': 'Password Verified', + '002001': 'Queue Full', + '002002': 'Password Failed – Disconnecting', + '002003': 'System Going Offline', + '002004': 'Disconnecting Socket', + '002006': 'Refused ‘Max Connections’', + '002008': 'Duplicate Serial Number Detected', + '002009': 'Password Failed (Client / Server)', + '002010': 'Password failed (Challenge / Response)', + '002011': 'Internal Server Error – Call Provider', + '003002': 'In Process with server', + '003009': 'Control failed to find branded serial (password lookup failed)', + '003012': '128 bit CryptoAPI failed', + '003014': 'Threaded Auth Started Expect Response', + '003017': 'Failed to start Event Thread.', + '003050': 'XML Parse Error', + '003051': 'All Connections Failed', + '003052': 'Server Login Failed', + '004001': 'Global Response Length Error (Too Short)', + '004002': 'Unable to Parse Response from Global (Indistinguishable)', + '004003': 'Global String Error', + '004004': 'Weak Encryption Request Not Supported', + '004005': 'Clear Text Request Not Supported', + '004010': 'Unrecognized Request Format', + '004011': 'Error Occurred While Decrypting Request', + '004017': 'Invalid Check Digit', + '004018': 'Merchant ID Missing', + '004019': 'TStream Type Missing', + '004020': 'Could Not Encrypt Response- Call Provider', + '100201': 'Invalid Transaction Type', + '100202': 'Invalid Operator ID', + '100203': 'Invalid Memo', + '100204': 'Invalid Account Number', + '100205': 'Invalid Expiration Date', + '100206': 'Invalid Authorization Code', + '100207': 'Invalid Authorization Code', + '100208': 'Invalid Authorization Amount', + '100209': 'Invalid Cash Back Amount', + '100210': 'Invalid Gratuity Amount', + '100211': 'Invalid Purchase Amount', + '100212': 'Invalid Magnetic Stripe Data', + '100213': 'Invalid PIN Block Data', + '100214': 'Invalid Derived Key Data', + '100215': 'Invalid State Code', + '100216': 'Invalid Date of Birth', + '100217': 'Invalid Check Type', + '100218': 'Invalid Routing Number', + '100219': 'Invalid TranCode', + '100220': 'Invalid Merchant ID', + '100221': 'Invalid TStream Type', + '100222': 'Invalid Batch Number', + '100223': 'Invalid Batch Item Count', + '100224': 'Invalid MICR Input Type', + '100225': 'Invalid Driver’s License', + '100226': 'Invalid Sequence Number', + '100227': 'Invalid Pass Data', + '100228': 'Invalid Card Type', }, }; // Popup to show all transaction state for the payment. From 8a29e37ddaf29d2736394762469b410d84dd583b Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Thu, 22 Feb 2018 17:08:04 +0100 Subject: [PATCH 021/111] [FIX] sale_timesheet: format amount Due to the representation of floats, it may happen that the amounts in the project overview appear with much more decimals than expected. We use the usual `monetary` widget to render the amounts. opw-1814843 --- .../views/hr_timesheet_templates.xml | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/addons/sale_timesheet/views/hr_timesheet_templates.xml b/addons/sale_timesheet/views/hr_timesheet_templates.xml index 5f36367cbac..00f3d8b6858 100644 --- a/addons/sale_timesheet/views/hr_timesheet_templates.xml +++ b/addons/sale_timesheet/views/hr_timesheet_templates.xml @@ -52,9 +52,7 @@ Profitability - - - + Invoiced @@ -68,9 +66,7 @@ Fixed - - - + To invoice @@ -84,9 +80,7 @@ No task found - - - + Cost @@ -110,9 +104,7 @@ - - - + Total From ad1cceb4fbd2f054af647833bd34e4e06012bb6c Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Tue, 20 Feb 2018 11:01:22 +0100 Subject: [PATCH 022/111] [FIX] web: one2many and onchange corner case issue Let's assume a one2many list view inside a form view, displaying a single char field A. There is an onchange on the form view returning a command 0 (CREATE) for the one2many. The command 0 only specifies a value for field A. The one2many list isn't editable, so when a sub-record is clicked, it is open in a form view (in a dialog). In this form view, an x2many field B is displayed in a sub list or kanban view. Before this rev., it crashed when the user tried to open the created one2many sub-record (returned by the onchange) in the form view, because of the presence of the x2many field B, which wasn't correctly processed by the BasicModel (its value was undefined, whereas it must be a valid, empty, dataPoint). Moreover, the limit of the x2many field B wasn't set, so a pager was displayed as soon as it contained at least one record. Fixes #22050 --- .../static/src/js/views/basic/basic_model.js | 143 +++++++++++------- .../static/src/js/views/basic/basic_view.js | 7 +- .../tests/fields/relational_fields_tests.js | 102 +++++++++++++ 3 files changed, 199 insertions(+), 53 deletions(-) diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index 6eef41f149d..e2cc60f7fc4 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -189,6 +189,89 @@ var BasicModel = AbstractModel.extend({ return id; }); }, + /** + * Add and process default values for a given record. Those values are + * parsed and stored in the '_changes' key of the record. For relational + * fields, sub-dataPoints are created, and missing relational data is + * fetched. Also generate default values for fields with no given value. + * Typically, this function is called with the result of a 'default_get' + * RPC, to populate a newly created dataPoint. It may also be called when a + * one2many subrecord is open in a form view (dialog), to generate the + * default values for the fields displayed in the o2m form view, but not in + * the list or kanban (mainly to correctly create sub-dataPoints for + * relational fields). + * + * @param {string} recordID local id for a record + * @param {Object} values dict of default values for the given record + * @param {Object} [options] + * @param {string} [options.viewType] current viewType. If not set, we will + * assume main viewType from the record + * @param {Array} [options.fieldNames] list of field names for which a + * default value must be generated (used to complete the values dict) + * @returns {Deferred} + */ + applyDefaultValues: function (recordID, values, options) { + options = options || {}; + var record = this.localData[recordID]; + var viewType = options.viewType || record.viewType; + var fieldNames = options.fieldNames || Object.keys(record.fieldsInfo[viewType]); + var field; + var fieldName; + record._changes = record._changes || {}; + + // fill default values for missing fields + for (var i = 0; i < fieldNames.length; i++) { + fieldName = fieldNames[i]; + if (!(fieldName in values) && !(fieldName in record._changes)) { + field = record.fields[fieldName]; + if (field.type === 'float' || + field.type === 'integer' || + field.type === 'monetary') { + values[fieldName] = 0; + } else if (field.type === 'one2many' || field.type === 'many2many') { + values[fieldName] = []; + } else { + values[fieldName] = null; + } + } + } + + // parse each value and create dataPoints for relational fields + var defs = []; + for (fieldName in values) { + field = record.fields[fieldName]; + if (!field) { + continue; // ignore values for unknown fields + } + record.data[fieldName] = null; + var dp; + if (field.type === 'many2one' && values[fieldName]) { + dp = this._makeDataPoint({ + context: record.context, + data: {id: values[fieldName]}, + modelName: field.relation, + parentID: record.id, + }); + record._changes[fieldName] = dp.id; + } else if (field.type === 'reference' && values[fieldName]) { + var ref = values[fieldName].split(','); + dp = this._makeDataPoint({ + context: record.context, + data: {id: parseInt(ref[1])}, + modelName: ref[0], + parentID: record.id, + }); + defs.push(this._fetchNameGet(dp)); + record._changes[fieldName] = dp.id; + } else if (field.type === 'one2many' || field.type === 'many2many') { + defs.push(this._processX2ManyCommands(record, fieldName, values[fieldName], options)); + } else { + record._changes[fieldName] = this._parseServerValue(field, values[fieldName]); + } + } + + return $.when.apply($, defs); + }, /** * Onchange RPCs may return values for fields that are not in the current * view. Those fields might even be unknown when the onchange returns (e.g. @@ -1397,6 +1480,7 @@ var BasicModel = AbstractModel.extend({ list = self._makeDataPoint({ fields: view ? view.fields : fieldInfo.relatedFields, fieldsInfo: view ? view.fieldsInfo : fieldInfo.fieldsInfo, + limit: fieldInfo.limit, modelName: field.relation, parentID: record.id, static: true, @@ -3227,27 +3311,8 @@ var BasicModel = AbstractModel.extend({ context: params.context, }) .then(function (result) { - // fill default values for missing fields - for (var i = 0; i < fieldNames.length; i++) { - var fieldName = fieldNames[i]; - if (!(fieldName in result)) { - var field = params.fields[fieldName]; - if (field.type === 'float' || - field.type === 'integer' || - field.type === 'monetary') { - result[fieldName] = 0; - } else if (field.type === 'one2many' || field.type === 'many2many') { - result[fieldName] = []; - } else { - result[fieldName] = null; - } - } - } - - var data = {}; var record = self._makeDataPoint({ modelName: modelName, - data: data, fields: params.fields, fieldsInfo: params.fieldsInfo, context: params.context, @@ -3256,37 +3321,7 @@ var BasicModel = AbstractModel.extend({ viewType: params.viewType, }); - var defs = []; - _.each(fieldNames, function (name) { - var field = params.fields[name]; - data[name] = null; - record._changes = record._changes || {}; - var dp; - if (field.type === 'many2one' && result[name]) { - dp = self._makeDataPoint({ - context: record.context, - data: {id: result[name]}, - modelName: field.relation, - parentID: record.id, - }); - record._changes[name] = dp.id; - } else if (field.type === 'reference' && result[name]) { - var ref = result[name].split(','); - dp = self._makeDataPoint({ - context: record.context, - data: {id: parseInt(ref[1])}, - modelName: ref[0], - parentID: record.id, - }); - defs.push(self._fetchNameGet(dp)); - record._changes[name] = dp.id; - } else if (field.type === 'one2many' || field.type === 'many2many') { - defs.push(self._processX2ManyCommands(record, name, result[name])); - } else { - record._changes[name] = self._parseServerValue(field, result[name]); - } - }); - return $.when.apply($, defs) + return self.applyDefaultValues(record.id, result) .then(function () { var def = $.Deferred(); self._performOnChange(record, fields_key).always(function () { @@ -3469,13 +3504,17 @@ var BasicModel = AbstractModel.extend({ * @param {Object} record * @param {string} fieldName * @param {Array[Array]} commands + * @param {Object} [options] + * @param {string} [options.viewType] current viewType. If not set, we will + * assume main viewType from the record * @returns {Deferred} */ - _processX2ManyCommands: function (record, fieldName, commands) { + _processX2ManyCommands: function (record, fieldName, commands, options) { var self = this; + options = options || {}; var defs = []; var field = record.fields[fieldName]; - var fieldInfo = record.fieldsInfo[record.viewType][fieldName]; + var fieldInfo = record.fieldsInfo[options.viewType || record.viewType][fieldName]; var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode]; var fieldsInfo = view ? view.fieldsInfo : fieldInfo.fieldsInfo; var fields = view ? view.fields : fieldInfo.relatedFields; diff --git a/addons/web/static/src/js/views/basic/basic_view.js b/addons/web/static/src/js/views/basic/basic_view.js index 8a91e2b90c7..c76020cacac 100644 --- a/addons/web/static/src/js/views/basic/basic_view.js +++ b/addons/web/static/src/js/views/basic/basic_view.js @@ -120,7 +120,12 @@ var BasicView = AbstractView.extend({ // (because those fields were unknow at that time). So we ask // the model to process them. def = this.model.applyRawChanges(record.id, viewType).then(function () { - if (!self.model.isNew(record.id)) { + if (self.model.isNew(record.id)) { + return self.model.applyDefaultValues(record.id, {}, { + fieldNames: fieldNames, + viewType: viewType, + }); + } else { return self.model.reload(record.id, { fieldNames: fieldNames, keepChanges: true, diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index 4f7041ad2d5..89764ce319b 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -3063,6 +3063,108 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('onchange on one2many containing x2many in form view', function (assert) { + assert.expect(16); + + this.data.partner.onchanges = { + foo: function (obj) { + obj.turtles = [[0, false, {turtle_foo: 'new record'}]]; + }, + }; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '', + archs: { + 'partner,false,list': '', + 'partner,false,search': '', + }, + }); + + assert.strictEqual(form.$('.o_data_row').length, 1, + "the onchange should have created one record in the relation"); + + // open the created o2m record in a form view, and add a m2m subrecord + // in its relation + form.$('.o_data_row').click(); + + assert.strictEqual($('.modal').length, 1, "should have opened a dialog"); + assert.strictEqual($('.modal .o_data_row').length, 0, + "there should be no record in the one2many in the dialog"); + + // add a many2many subrecord + $('.modal .o_field_x2many_list_row_add a').click(); + + assert.strictEqual($('.modal').length, 2, + "should have opened a second dialog"); + + // select a many2many subrecord + $('.modal:nth(1) .o_list_view .o_data_cell:first').click(); + + assert.strictEqual($('.modal').length, 1, + "second dialog should be closed"); + assert.strictEqual($('.modal .o_data_row').length, 1, + "there should be one record in the one2many in the dialog"); + assert.notOk($('.modal .o_x2m_control_panel .o_cp_pager div').is(':visible'), + 'm2m pager should be hidden'); + + // click on 'Save & Close' + $('.modal .modal-footer .btn-primary:first').click(); + + assert.strictEqual($('.modal').length, 0, "dialog should be closed"); + + // reopen o2m record, and another m2m subrecord in its relation, but + // discard the changes + form.$('.o_data_row').click(); + + assert.strictEqual($('.modal').length, 1, "should have opened a dialog"); + assert.strictEqual($('.modal .o_data_row').length, 1, + "there should be one record in the one2many in the dialog"); + + // add another m2m subrecord + $('.modal .o_field_x2many_list_row_add a').click(); + + assert.strictEqual($('.modal').length, 2, + "should have opened a second dialog"); + + $('.modal:nth(1) .o_list_view .o_data_cell:first').click(); + + assert.strictEqual($('.modal').length, 1, + "second dialog should be closed"); + assert.strictEqual($('.modal .o_data_row').length, 2, + "there should be two records in the one2many in the dialog"); + + // click on 'Discard' + $('.modal .modal-footer .btn-default').click(); + + assert.strictEqual($('.modal').length, 0, "dialog should be closed"); + + // reopen o2m record to check that second changes have properly been discarded + form.$('.o_data_row').click(); + + assert.strictEqual($('.modal').length, 1, "should have opened a dialog"); + assert.strictEqual($('.modal .o_data_row').length, 1, + "there should be one record in the one2many in the dialog"); + + form.destroy(); + }); + QUnit.test('embedded one2many with handle widget with minimum setValue calls', function (assert) { var done = assert.async(); assert.expect(20); From ff7efbb1e81cd6987601c03f0b75a37d2df8e331 Mon Sep 17 00:00:00 2001 From: Adrian Torres Date: Fri, 23 Feb 2018 10:28:10 +0100 Subject: [PATCH 023/111] [FIX] website,sale,auth_signup: prevent shared field deletion The field `auth_signup_uninvited` is defined in the 3 aforementioned modules, yet they are independant modules in respects to one another. This means that whichever module is installed first will create an XMLID for the field, if this module is uninstalled before the other two: boom! crash when changing general settings because the res.config.settings field set by the other two modules no longer exists. To solve this, we simply ignore the field during ir_model_field.unlink() Fixes #23171 --- addons/auth_signup/models/__init__.py | 1 + addons/auth_signup/models/ir_model_fields.py | 19 +++++++++++++++++++ addons/sale/models/__init__.py | 1 + addons/sale/models/ir_model_fields.py | 19 +++++++++++++++++++ addons/website/models/ir_model_fields.py | 11 +++++------ 5 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 addons/auth_signup/models/ir_model_fields.py create mode 100644 addons/sale/models/ir_model_fields.py diff --git a/addons/auth_signup/models/__init__.py b/addons/auth_signup/models/__init__.py index 44dc1ee6b11..a25160266e7 100644 --- a/addons/auth_signup/models/__init__.py +++ b/addons/auth_signup/models/__init__.py @@ -4,3 +4,4 @@ from . import res_config_settings from . import ir_http from . import res_partner from . import res_users +from . import ir_model_fields diff --git a/addons/auth_signup/models/ir_model_fields.py b/addons/auth_signup/models/ir_model_fields.py new file mode 100644 index 00000000000..828b9de8346 --- /dev/null +++ b/addons/auth_signup/models/ir_model_fields.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details + +from odoo import models + + +# This is a nasty hack, targeted for V11 only +class IrModelFields(models.Model): + _inherit = 'ir.model.fields' + + def unlink(self): + # Prevent the deletion of some `shared` fields... -_- + self = self.filtered( + lambda rec: not ( + rec.model == 'res.config.settings' + and rec.name == 'auth_signup_uninvited' + ) + ) + return super(IrModelFields, self).unlink() diff --git a/addons/sale/models/__init__.py b/addons/sale/models/__init__.py index 5f6e7ba2857..7a157566790 100644 --- a/addons/sale/models/__init__.py +++ b/addons/sale/models/__init__.py @@ -12,3 +12,4 @@ from . import sale from . import res_config_settings from . import sale_layout from . import sales_team +from . import ir_model_fields diff --git a/addons/sale/models/ir_model_fields.py b/addons/sale/models/ir_model_fields.py new file mode 100644 index 00000000000..828b9de8346 --- /dev/null +++ b/addons/sale/models/ir_model_fields.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details + +from odoo import models + + +# This is a nasty hack, targeted for V11 only +class IrModelFields(models.Model): + _inherit = 'ir.model.fields' + + def unlink(self): + # Prevent the deletion of some `shared` fields... -_- + self = self.filtered( + lambda rec: not ( + rec.model == 'res.config.settings' + and rec.name == 'auth_signup_uninvited' + ) + ) + return super(IrModelFields, self).unlink() diff --git a/addons/website/models/ir_model_fields.py b/addons/website/models/ir_model_fields.py index 1e963bf650e..51f5c2a7dc5 100644 --- a/addons/website/models/ir_model_fields.py +++ b/addons/website/models/ir_model_fields.py @@ -1,17 +1,15 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details -from odoo import api, models +from odoo import models # This is a nasty hack, targeted for V11 only class IrModelFields(models.Model): _inherit = 'ir.model.fields' - @api.multi def unlink(self): - # Prevent the deletion of social_* fields defined in res.company if - # the other module is installed + # Prevent the deletion of some `shared` fields... -_- social = ( 'social_facebook', 'social_github', @@ -23,8 +21,9 @@ class IrModelFields(models.Model): self = self.filtered( lambda rec: not ( - rec.model == 'res.company' and - rec.name in social + (rec.model == 'res.company' and rec.name in social) + or (rec.model == 'res.config.settings' + and rec.name == 'auth_signup_uninvited') ) ) return super(IrModelFields, self).unlink() From dee3b87ae4b7c67f21a445ed8caecdd6fa36277f Mon Sep 17 00:00:00 2001 From: Jeremy Kersten Date: Fri, 23 Feb 2018 10:09:33 +0100 Subject: [PATCH 024/111] [FIX] website_customer: fix sitemap when customer had no country Before this commit, the sitemap will crash if you have some res.partner without country that are publish and with an assigned_partner_id. Now, we only search for customer with country that are set and avoid to have a traceback: File "/home/odoo/src/odoo/11.0/addons/website_customer/controllers/main.py", line 31, in sitemap_industry loc = '/customers/country/%s' % slug(country['country_id']) File "/home/odoo/src/odoo/11.0/addons/http_routing/models/ir_http.py", line 91, in slug identifier, name = value TypeError: 'bool' object is not iterable --- addons/website_customer/controllers/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/website_customer/controllers/main.py b/addons/website_customer/controllers/main.py index 421a809b36d..063d4b9bb5f 100644 --- a/addons/website_customer/controllers/main.py +++ b/addons/website_customer/controllers/main.py @@ -24,7 +24,7 @@ class WebsiteCustomer(http.Controller): if not qs or qs.lower() in loc: yield {'loc': loc} - dom = [('website_published', '=', True), ('assigned_partner_id', '!=', False)] + dom = [('website_published', '=', True), ('assigned_partner_id', '!=', False), ('country_id', '!=', False)] dom += sitemap_qs2dom(qs, '/customers/country') countries = env['res.partner'].sudo().read_group(dom, ['id', 'country_id'], groupby='country_id') for country in countries: From 7179008ffaa43dc8482164ce66291864e085c9cd Mon Sep 17 00:00:00 2001 From: Jeremy Kersten Date: Fri, 23 Feb 2018 10:14:06 +0100 Subject: [PATCH 025/111] [FIX] doc: fix link to our website MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove wrong slash trailing Now, in V11, matching of page are done on the exact name. Previously, the error was transparent because we was using a dedicated controller that allow trailing slash. https://github.com/odoo/odoo/blob/11.0/odoo/http.py#L936 https://webmasters.googleblog.com/2010/04/to-slash-or-not-to-slash.html http://example.com/foo/ (with trailing slash, conventionally a directory) http://example.com/foo (without trailing slash, conventionally a file) Google treats each URL above separately (and equally) regardless of whether it’s a file or a directory, or it contains a trailing slash or it doesn’t contain a trailing slash. Different content on / and no-/ URLs okay for Google, often less ideal for users Special case for homepage Rest assured that for your root URL specifically, http://example.com is equivalent to http://example.com/ and can’t be redirected even if you’re Chuck Norris. --- doc/_extensions/odoo_ext/layout.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/_extensions/odoo_ext/layout.html b/doc/_extensions/odoo_ext/layout.html index cb1015d2ebe..793ed47806f 100644 --- a/doc/_extensions/odoo_ext/layout.html +++ b/doc/_extensions/odoo_ext/layout.html @@ -102,8 +102,8 @@
It's all about efficiency
    -
  • Accounting
  • -
  • Project
  • +
  • Accounting
  • +
  • Project