diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 2908bf76b45..3a52a0c8392 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -739,7 +739,7 @@ class AccountMoveLine(models.Model): vals['debit'] = amount < 0 and abs(amount) or 0.0 vals['partner_id'] = self.env['res.partner']._find_accounting_partner(self[0].partner_id).id company_currency = self[0].account_id.company_id.currency_id - writeoff_currency = self[0].currency_id or company_currency + writeoff_currency = self[0].account_id.currency_id or company_currency if not self._context.get('skip_full_reconcile_check') == 'amount_currency_excluded' and 'amount_currency' not in vals and writeoff_currency != company_currency: vals['currency_id'] = writeoff_currency.id sign = 1 if vals['debit'] > 0 else -1 diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index 7cee63c3122..26433ea3325 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -724,3 +724,17 @@ class account_payment(models.Model): }) return vals + + def _get_invoice_payment_amount(self, inv): + """ + Computes the amount covered by the current payment in the given invoice. + + :param inv: an invoice object + :returns: the amount covered by the payment in the invoice + """ + self.ensure_one() + return sum([ + data['amount'] + for data in inv._get_payments_vals() + if data['account_payment_id'] == self.id + ]) diff --git a/addons/account/report/account_report_payment_receipt_templates.xml b/addons/account/report/account_report_payment_receipt_templates.xml index 05c7421140d..15c45641e52 100644 --- a/addons/account/report/account_report_payment_receipt_templates.xml +++ b/addons/account/report/account_report_payment_receipt_templates.xml @@ -46,7 +46,7 @@ - + diff --git a/addons/account_asset/report/account_asset_report_views.xml b/addons/account_asset/report/account_asset_report_views.xml index fce17a56065..089fa7f5c23 100644 --- a/addons/account_asset/report/account_asset_report_views.xml +++ b/addons/account_asset/report/account_asset_report_views.xml @@ -33,6 +33,7 @@ + @@ -62,7 +63,7 @@ graph,pivot [('asset_category_id.type', '=', 'purchase')] - {} + {'search_default_only_active': 1}

No content diff --git a/addons/l10n_in_hr_payroll/views/l10n_in_hr_payroll_view.xml b/addons/l10n_in_hr_payroll/views/l10n_in_hr_payroll_view.xml index 56ba9b00694..175b8e2fe9a 100644 --- a/addons/l10n_in_hr_payroll/views/l10n_in_hr_payroll_view.xml +++ b/addons/l10n_in_hr_payroll/views/l10n_in_hr_payroll_view.xml @@ -3,10 +3,10 @@ hr.contract.form.in.inherit hr.contract - + - + diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 2e88bf0cff4..b1fc92605e3 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -729,7 +729,7 @@ class PosOrder(models.Model): if to_invoice: pos_order.action_pos_order_invoice() - pos_order.invoice_id.sudo().action_invoice_open() + pos_order.invoice_id.sudo().with_context(force_company=self.env.user.company_id.id).action_invoice_open() pos_order.account_move = pos_order.invoice_id.move_id return order_ids diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index 045f0be7718..a2b34025c08 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -178,6 +178,61 @@ var ScreenWidget = PosBaseWidget.extend({ } } }, + /** + * Handles the error response from the server when we push + * an invoiceable order + * Displays appropriates warnings and errors and + * proposes subsequent actions + * + * @private + * @param {PosModel} order: the order to consider, defaults to current order + * @param {Boolean} refresh_screens: whether or not displayed screens should refresh + * @param {Object} error: the error provided by Ajax + */ + _handleFailedPushForInvoice: function (order, refresh_screen, error) { + var self = this; + order = order || this.pos.get_order(); + this.invoicing = false; + order.finalized = false; + if (error.message === 'Missing Customer') { + this.gui.show_popup('confirm',{ + 'title': _t('Please select the Customer'), + 'body': _t('You need to select the customer before you can invoice an order.'), + confirm: function(){ + self.gui.show_screen('clientlist', null, refresh_screen); + }, + }); + } else if (error.message === 'Backend Invoice') { + this.gui.show_popup('confirm',{ + 'title': _t('Please print the invoice from the backend'), + 'body': _t('The order has been synchronized earlier. Please make the invoice from the backend for the order: ') + error.data.order.name, + confirm: function () { + this.gui.show_screen('receipt', null, refresh_screen); + }, + cancel: function () { + this.gui.show_screen('receipt', null, refresh_screen); + }, + }); + } else if (error.code < 0) { // XmlHttpRequest Errors + this.gui.show_popup('error',{ + 'title': _t('The order could not be sent'), + 'body': _t('Check your internet connection and try again.'), + cancel: function () { + this.gui.show_screen('receipt', {button_print_invoice: true}, refresh_screen); // refresh if necessary + }, + }); + } else if (error.code === 200) { // OpenERP Server Errors + this.gui.show_popup('error-traceback',{ + 'title': error.data.message || _t("Server Error"), + 'body': error.data.debug || _t('The server encountered an error while receiving your order.'), + }); + } else { // ??? + this.gui.show_popup('error',{ + 'title': _t("Unknown Error"), + 'body': _t("The order could not be sent to the server due to an unknown error"), + }); + } + }, }); /*--------------------------------------*\ @@ -1536,7 +1591,9 @@ var ReceiptScreenWidget = ScreenWidget.extend({ return this.pos.config.iface_print_auto && !this.pos.get_order()._printed; }, should_close_immediately: function() { - return this.pos.config.iface_print_via_proxy && this.pos.config.iface_print_skip_screen; + var order = this.pos.get_order(); + var invoiced_finalized = order.is_to_invoice() ? order.finalized : true; + return this.pos.config.iface_print_via_proxy && this.pos.config.iface_print_skip_screen && invoiced_finalized; }, lock_screen: function(locked) { this._locked = locked; @@ -1640,9 +1697,32 @@ var ReceiptScreenWidget = ScreenWidget.extend({ self.print(); } }); + var button_print_invoice = this.$('.button.print_invoice'); + button_print_invoice.click(function () { + var order = self.pos.get_order(); + var invoiced = self.pos.push_and_invoice_order(order); + self.invoicing = true; + + invoiced.fail(self._handleFailedPushForInvoice.bind(self, order, true)); // refresh + + invoiced.done(function(){ + self.invoicing = false; + self.gui.show_screen('receipt', {button_print_invoice: false}, true); // refresh + }); + }); + }, render_change: function() { + var self = this; this.$('.change-value').html(this.format_currency(this.pos.get_order().get_change())); + var order = this.pos.get_order(); + var order_screen_params = order.get_screen_data('params'); + var button_print_invoice = this.$('.button.print_invoice'); + if (order_screen_params && order_screen_params.button_print_invoice) { + button_print_invoice.show(); + } else { + button_print_invoice.hide(); + } }, render_receipt: function() { this.$('.pos-receipt-container').html(QWeb.render('PosTicket', this.get_receipt_render_env())); @@ -2067,45 +2147,7 @@ var PaymentScreenWidget = ScreenWidget.extend({ var invoiced = this.pos.push_and_invoice_order(order); this.invoicing = true; - invoiced.fail(function(error){ - self.invoicing = false; - order.finalized = false; - if (error.message === 'Missing Customer') { - self.gui.show_popup('confirm',{ - 'title': _t('Please select the Customer'), - 'body': _t('You need to select the customer before you can invoice an order.'), - confirm: function(){ - self.gui.show_screen('clientlist'); - }, - }); - } else if (error.message === 'Backend Invoice') { - self.gui.show_popup('confirm',{ - 'title': _t('Please print the invoice from the backend'), - 'body': _t('The order has been synchronized earlier. Please make the invoice from the backend for the order: ') + error.data.order.name, - confirm: function () { - this.gui.show_screen('receipt'); - }, - cancel: function () { - this.gui.show_screen('receipt'); - }, - }); - } else if (error.code < 0) { // XmlHttpRequest Errors - self.gui.show_popup('error',{ - 'title': _t('The order could not be sent'), - 'body': _t('Check your internet connection and try again.'), - }); - } else if (error.code === 200) { // OpenERP Server Errors - self.gui.show_popup('error-traceback',{ - 'title': error.data.message || _t("Server Error"), - 'body': error.data.debug || _t('The server encountered an error while receiving your order.'), - }); - } else { // ??? - self.gui.show_popup('error',{ - 'title': _t("Unknown Error"), - 'body': _t("The order could not be sent to the server due to an unknown error"), - }); - } - }); + invoiced.fail(this._handleFailedPushForInvoice.bind(this, order, false)); invoiced.done(function(){ self.invoicing = false; diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index b594d5f6b9d..12077862e10 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -712,6 +712,9 @@

+
Print Receipt
diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py index 8a31a68fc8b..34672cb459f 100644 --- a/addons/product/models/product_template.py +++ b/addons/product/models/product_template.py @@ -333,6 +333,9 @@ class ProductTemplate(models.Model): related_vals['volume'] = vals['volume'] if vals.get('weight'): related_vals['weight'] = vals['weight'] + # Please do forward port + if vals.get('packaging_ids'): + related_vals['packaging_ids'] = vals['packaging_ids'] if related_vals: template.write(related_vals) return template diff --git a/addons/product/views/product_views.xml b/addons/product/views/product_views.xml index 465c84dddcc..3b7fbd53543 100644 --- a/addons/product/views/product_views.xml +++ b/addons/product/views/product_views.xml @@ -112,7 +112,7 @@ colspan="4" attrs="{'invisible':['|', ('type', 'not in', ['product', 'consu']), ('product_variant_count', '>', 1)]}" groups="product.group_stock_packaging"> - + @@ -507,6 +507,18 @@ + + + product.packaging.form.view2 + product.packaging + primary + + + + + + + Product Packagings product.packaging diff --git a/addons/project/views/project_portal_templates.xml b/addons/project/views/project_portal_templates.xml index e240f64d150..9b5cdd5873e 100644 --- a/addons/project/views/project_portal_templates.xml +++ b/addons/project/views/project_portal_templates.xml @@ -112,16 +112,6 @@
-
-
-

Message and communication history

-
-
- - - -
-
diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py index 7f173c8e511..ddf29241d9d 100644 --- a/addons/stock/models/stock_picking.py +++ b/addons/stock/models/stock_picking.py @@ -1059,3 +1059,12 @@ class Picking(models.Model): action['domain'] = [('id', 'in', packages.ids)] action['context'] = {'picking_id': self.id} return action + + def action_picking_move_tree(self): + action = self.env.ref('stock.stock_move_action').read()[0] + action['views'] = [ + (self.env.ref('stock.view_picking_move_tree').id, 'tree'), + ] + action['context'] = self.env.context + action['domain'] = [('picking_id', 'in', self.ids)] + return action diff --git a/addons/stock/views/stock_move_views.xml b/addons/stock/views/stock_move_views.xml index c7d1601eeb3..86299b7a987 100644 --- a/addons/stock/views/stock_move_views.xml +++ b/addons/stock/views/stock_move_views.xml @@ -66,6 +66,38 @@
+ + stock.picking.move.tree + stock.move + + + + + + + + + + + + + + + + + + + + + + + + + + + + + stock.move.kanban stock.move diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml index 211d2afba95..1028aaadc69 100644 --- a/addons/stock/views/stock_picking_views.xml +++ b/addons/stock/views/stock_picking_views.xml @@ -233,6 +233,19 @@ class="oe_stat_button" icon="fa-cubes" attrs="{'invisible': [('has_packages', '=', False)]}"/>

diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py index d8fa7d0aea1..607e61dec4d 100644 --- a/addons/stock_account/models/stock.py +++ b/addons/stock_account/models/stock.py @@ -558,7 +558,7 @@ class StockMove(models.Model): debit_value = self.company_id.currency_id.round(valuation_amount) # check that all data is correct - if self.company_id.currency_id.is_zero(debit_value): + if self.company_id.currency_id.is_zero(debit_value) and not self.env['ir.config_parameter'].sudo().get_param('stock_account.allow_zero_cost'): raise UserError(_("The cost of %s is currently equal to 0. Change the cost or the configuration of your product to avoid an incorrect valuation.") % (self.product_id.display_name,)) credit_value = debit_value diff --git a/addons/web/static/src/js/chrome/abstract_web_client.js b/addons/web/static/src/js/chrome/abstract_web_client.js index cc22c2b9af0..88c701247c3 100644 --- a/addons/web/static/src/js/chrome/abstract_web_client.js +++ b/addons/web/static/src/js/chrome/abstract_web_client.js @@ -204,6 +204,10 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMi }); } } else { + // ignore Chrome video internal error: https://crbug.com/809574 + if (!error && message === 'ResizeObserver loop limit exceeded') { + return; + } var traceback = error ? error.stack : ''; crash_manager.show_error({ type: _t("Odoo Client Error"), diff --git a/addons/web/static/src/js/views/calendar/calendar_model.js b/addons/web/static/src/js/views/calendar/calendar_model.js index 4ca1b56973e..71290708318 100644 --- a/addons/web/static/src/js/views/calendar/calendar_model.js +++ b/addons/web/static/src/js/views/calendar/calendar_model.js @@ -60,10 +60,11 @@ return AbstractModel.extend({ end.add(-1, 'days'); } + var isDateEvent = this.fields[this.mapping.date_start].type === 'date'; // An "allDay" event without the "all_day" option is not considered // as a 24h day. It's just a part of the day (by default: 7h-19h). if (event.allDay) { - if (!this.mapping.all_day) { + if (!this.mapping.all_day && !isDateEvent) { if (event.r_start) { start.hours(event.r_start.hours()) .minutes(event.r_start.minutes()) diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js index 4414d48d47a..63da502859b 100644 --- a/addons/web/static/tests/views/calendar_tests.js +++ b/addons/web/static/tests/views/calendar_tests.js @@ -2236,6 +2236,67 @@ QUnit.module('Views', { calendar.destroy(); }); + QUnit.test('timzeone does not affect calendar with date field', function (assert) { + assert.expect(8); + + var calendar = createView({ + View: CalendarView, + model: 'event', + data: this.data, + arch: + ''+ + ''+ + ''+ + '', + archs: archs, + viewOptions: { + initialDate: initialDate, + }, + mockRPC: function (route, args) { + if (args.method === "create") { + assert.strictEqual(args.args[0].start_date, "2016-12-20 00:00:00"); + } + if (args.method === "write") { + assert.step(args.args[1].start_date); + } + return this._super(route, args); + }, + session: { + getTZOffset: function () { + return 120; // 2 hours timezone + }, + }, + }); + + // Create event (on 20 december) + var $cell = calendar.$('.fc-day-grid .fc-row:eq(3) .fc-day:eq(2)'); + testUtils.triggerMouseEvent($cell, "mousedown"); + testUtils.triggerMouseEvent($cell, "mouseup"); + var $input = $('.modal-body input:first'); + $input.val("An event").trigger('input'); + $('.modal button.btn:contains(Create)').trigger('click'); + + assert.strictEqual(calendar.$('.o_field_start_date').text().trim(), "12/20/2016") + + // Move event to another day (on 27 november) + testUtils.dragAndDrop( + calendar.$('.fc-event').first(), + calendar.$('.fc-day-top').first() + ); + assert.verifySteps(["2016-11-27 00:00:00"]); + assert.strictEqual(calendar.$('.o_field_start_date').text().trim(), "11/27/2016") + + // Move event to last day (on 7 january) + testUtils.dragAndDrop( + calendar.$('.fc-event').first(), + calendar.$('.fc-day-top').last() + ); + assert.verifySteps(["2016-11-27 00:00:00", "2017-01-07 00:00:00"]); + assert.strictEqual(calendar.$('.o_field_start_date').text().trim(), "01/07/2017") + + calendar.destroy(); + }); + QUnit.test('form_view_id attribute works (for creating events)', function (assert) { assert.expect(1); diff --git a/addons/website_forum/controllers/main.py b/addons/website_forum/controllers/main.py index 4e493e5017e..cb01ebfd6ee 100644 --- a/addons/website_forum/controllers/main.py +++ b/addons/website_forum/controllers/main.py @@ -22,12 +22,8 @@ class WebsiteForum(http.Controller): _user_per_page = 30 def _get_notifications(self): - badge_subtype = request.env.ref('gamification.mt_badge_granted') - if badge_subtype: - msg = request.env['mail.message'].search([('subtype_id', '=', badge_subtype.id), ('needaction', '=', True)]) - else: - msg = list() - return msg + """Deprecated""" + return list() def _prepare_forum_values(self, forum=None, **kwargs): values = { diff --git a/addons/website_hr_recruitment/models/hr_recruitment.py b/addons/website_hr_recruitment/models/hr_recruitment.py index 7d6e88c31c8..ed5c0b422cc 100644 --- a/addons/website_hr_recruitment/models/hr_recruitment.py +++ b/addons/website_hr_recruitment/models/hr_recruitment.py @@ -45,7 +45,7 @@ class Job(models.Model): default_description = self.env["ir.model.data"].xmlid_to_object("website_hr_recruitment.default_website_description") return (default_description.render() if default_description else "") - website_description = fields.Html('Website description', translate=html_translate, sanitize_attributes=False, default=_get_default_website_description) + website_description = fields.Html('Website description', translate=html_translate, sanitize_attributes=False, default=_get_default_website_description, prefetch=False) @api.multi def _compute_website_url(self): diff --git a/odoo/addons/base/models/ir_qweb_fields.py b/odoo/addons/base/models/ir_qweb_fields.py index b461c9fa8f5..2c1bb732f8e 100644 --- a/odoo/addons/base/models/ir_qweb_fields.py +++ b/odoo/addons/base/models/ir_qweb_fields.py @@ -370,6 +370,10 @@ class FloatTimeConverter(models.AbstractModel): def value_to_html(self, value, options): sign = math.copysign(1.0, value) hours, minutes = divmod(abs(value) * 60, 60) + minutes = round(minutes) + if minutes == 60: + minutes = 0 + hours += 1 return '%02d:%02d' % (sign * hours, minutes) diff --git a/odoo/addons/test_new_api/tests/test_one2many.py b/odoo/addons/test_new_api/tests/test_one2many.py index 8531396fe48..03abaec4f2d 100644 --- a/odoo/addons/test_new_api/tests/test_one2many.py +++ b/odoo/addons/test_new_api/tests/test_one2many.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from openerp.tests.common import TransactionCase +from odoo.tests.common import TransactionCase class One2manyCase(TransactionCase): @@ -51,6 +51,8 @@ class One2manyCase(TransactionCase): self.multi.lines = self.multi.lines[:-1] self.assertEqual(len(self.multi.lines), 9) self.assertIn("hello", self.multi.lines.mapped('name')) + if not self.multi.id: + return # Invalidate the cache and check again; this crashes if the value # of self.multi.lines in cache contains new records self.multi.invalidate_cache() @@ -89,11 +91,26 @@ class One2manyCase(TransactionCase): self.multi.lines = [(0, 0, {"name": str(name)})] self.operations() + def test_rpcstyle_one_by_one_on_new(self): + self.multi = self.env["test_new_api.multi"].new({ + "name": "What is up?" + }) + 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() + def test_rpcstyle_single_on_new(self): + self.multi = self.env["test_new_api.multi"].new({ + "name": "What is up?" + }) + self.multi.lines = [(0, 0, {'name': str(name)}) for name in range(10)] + self.operations() + def test_many2one_integer(self): """Test several models one2many with same inverse Integer field""" # utility function to convert records to tuples with id,name diff --git a/odoo/fields.py b/odoo/fields.py index 0acacc0a6d3..f74f9e53a8e 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -2115,8 +2115,8 @@ class _RelationalMulti(_Relational): elif isinstance(value, (list, tuple)): # value is a list/tuple of commands, dicts or record ids comodel = record.env[self.comodel_name] - # determine the value ids; by convention empty on new records - ids = OrderedSet(record[self.name].ids if record.id else ()) + # determine the value ids + ids = OrderedSet(record[self.name]._ids) # modify ids with the commands for command in value: if isinstance(command, (tuple, list)):