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 @@
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 Invoice
+
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)]}"/>
+
+
+
+ Operations
+
+
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)):