summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2019-05-03 18:47:49 +0200
committerChristophe Simonis <chs@odoo.com>2019-05-03 18:47:49 +0200
commit04acf0cc7ccd4ff500cac6187c6be3f28f272fd2 (patch)
treeff659994848ebdef19010bb72d74801ac50c9a07
parent8d9c054842b5fbaa205c7658c25cf66320b8e197 (diff)
parent2420492c26410c2d01cd18bacbbf2fb2d76f7f80 (diff)
[MERGE] forward port branch saas-12.1 up to 2420492c264
-rw-r--r--addons/account/models/account_invoice.py20
-rw-r--r--addons/account/models/account_move.py2
-rw-r--r--addons/account/models/account_payment.py14
-rw-r--r--addons/account/static/src/js/reconciliation/reconciliation_renderer.js6
-rw-r--r--addons/account/static/tests/reconciliation_tests.js9
-rw-r--r--addons/account/views/account_invoice_view.xml4
-rw-r--r--addons/account/views/report_payment_receipt_templates.xml2
-rw-r--r--addons/account_cancel/views/account_views.xml4
-rw-r--r--addons/calendar/models/calendar.py5
-rw-r--r--addons/l10n_ch/models/mail_template.py7
-rw-r--r--addons/l10n_in_hr_payroll/views/l10n_in_hr_payroll_view.xml4
-rw-r--r--addons/mrp_byproduct/models/mrp_production.py3
-rw-r--r--addons/payment_authorize/controllers/main.py8
-rw-r--r--addons/point_of_sale/models/pos_order.py2
-rw-r--r--addons/point_of_sale/static/src/js/screens.js122
-rw-r--r--addons/point_of_sale/static/src/xml/pos.xml3
-rw-r--r--addons/portal/controllers/mail.py2
-rw-r--r--addons/product/models/product_template.py3
-rw-r--r--addons/project/models/project.py4
-rw-r--r--addons/purchase_stock/models/account_invoice.py6
-rw-r--r--addons/purchase_stock/tests/test_stockvaluation.py242
-rw-r--r--addons/sale/models/sale.py2
-rw-r--r--addons/sale/views/sale_portal_templates.xml57
-rw-r--r--addons/sale/views/sale_views.xml2
-rw-r--r--addons/sale_management/controllers/portal.py9
-rw-r--r--addons/sale_management/static/src/js/sale_management.js26
-rw-r--r--addons/sale_timesheet/models/account.py8
-rw-r--r--addons/sale_timesheet/report/project_profitability_report_analysis.py53
-rw-r--r--addons/stock/models/stock_picking.py9
-rw-r--r--addons/stock/views/stock_move_views.xml32
-rw-r--r--addons/stock/views/stock_picking_views.xml13
-rw-r--r--addons/web/static/src/js/tools/debug_manager.js2
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_model.js3
-rw-r--r--addons/web/static/tests/views/calendar_tests.js61
-rw-r--r--addons/web_editor/static/src/js/wysiwyg_snippets/snippets.editor.js2
-rw-r--r--addons/web_unsplash/static/src/js/unsplash_image_widget.js3
-rw-r--r--addons/website/static/src/js/content/snippets.animation.js2
-rw-r--r--addons/website_blog/static/src/scss/primary_variables.scss2
-rw-r--r--addons/website_blog/static/src/scss/website_blog.scss29
-rw-r--r--addons/website_blog/views/website_blog_templates.xml8
-rw-r--r--addons/website_forum/models/forum.py3
-rw-r--r--addons/website_hr_recruitment/models/hr_recruitment.py2
-rw-r--r--addons/website_sale/controllers/main.py3
-rw-r--r--addons/website_sale/static/src/scss/website_sale.scss4
-rw-r--r--addons/website_sale/views/templates.xml2
-rw-r--r--odoo/addons/base/models/ir_qweb_fields.py4
-rw-r--r--odoo/addons/test_inherits/ir.model.access.csv2
-rw-r--r--odoo/addons/test_inherits/models.py26
-rw-r--r--odoo/addons/test_inherits/tests/test_inherits.py35
-rw-r--r--odoo/addons/test_new_api/tests/test_one2many.py19
-rw-r--r--odoo/fields.py4
-rw-r--r--odoo/models.py24
52 files changed, 792 insertions, 131 deletions
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py
index 9a8887ebeb0..c97b1d4d314 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -70,6 +70,12 @@ class AccountInvoice(models.Model):
self.amount_total_signed = self.amount_total * sign
self.amount_untaxed_signed = amount_untaxed_signed * sign
+ def _compute_sign_taxes(self):
+ for invoice in self:
+ sign = invoice.type in ['in_refund', 'out_refund'] and -1 or 1
+ invoice.amount_untaxed_invoice_signed = invoice.amount_untaxed * sign
+ invoice.amount_tax_signed = invoice.amount_tax * sign
+
@api.onchange('amount_total')
def _onchange_amount_total(self):
for inv in self:
@@ -347,8 +353,12 @@ class AccountInvoice(models.Model):
store=True, readonly=True, compute='_compute_amount', tracking=True)
amount_untaxed_signed = fields.Monetary(string='Untaxed Amount in Company Currency', currency_field='company_currency_id',
store=True, readonly=True, compute='_compute_amount')
+ amount_untaxed_invoice_signed = fields.Monetary(string='Untaxed Amount in Invoice Currency', currency_field='currency_id',
+ readonly=True, compute='_compute_sign_taxes')
amount_tax = fields.Monetary(string='Tax',
store=True, readonly=True, compute='_compute_amount')
+ amount_tax_signed = fields.Monetary(string='Tax in Invoice Currency', currency_field='currency_id',
+ readonly=True, compute='_compute_sign_taxes')
amount_total = fields.Monetary(string='Total',
store=True, readonly=True, compute='_compute_amount')
amount_total_signed = fields.Monetary(string='Total in Invoice Currency', currency_field='currency_id',
@@ -1790,14 +1800,14 @@ class AccountInvoiceLine(models.Model):
def _set_taxes(self):
""" Used in on_change to set taxes and price"""
self.ensure_one()
- if self.invoice_id.type in ('out_invoice', 'out_refund'):
- taxes = self.product_id.taxes_id or self.account_id.tax_ids or self.invoice_id.company_id.account_sale_tax_id
- else:
- taxes = self.product_id.supplier_taxes_id or self.account_id.tax_ids or self.invoice_id.company_id.account_purchase_tax_id
# Keep only taxes of the company
company_id = self.company_id or self.env.user.company_id
- taxes = taxes.filtered(lambda r: r.company_id == company_id)
+
+ if self.invoice_id.type in ('out_invoice', 'out_refund'):
+ taxes = self.product_id.taxes_id.filtered(lambda r: r.company_id == company_id) or self.account_id.tax_ids or self.invoice_id.company_id.account_sale_tax_id
+ else:
+ taxes = self.product_id.supplier_taxes_id.filtered(lambda r: r.company_id == company_id) or self.account_id.tax_ids or self.invoice_id.company_id.account_purchase_tax_id
self.invoice_line_tax_ids = fp_taxes = self.invoice_id.fiscal_position_id.map_tax(taxes, self.product_id, self.invoice_id.partner_id)
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index ad7eec8c651..f230e0ff777 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -1012,7 +1012,7 @@ class AccountMoveLine(models.Model):
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
line_to_reconcile = self.env['account.move.line']
# Iterate and create one writeoff by journal
writeoff_moves = self.env['account.move']
diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py
index 034cdab484d..cc9b43efb81 100644
--- a/addons/account/models/account_payment.py
+++ b/addons/account/models/account_payment.py
@@ -835,3 +835,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/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
index 8759a404473..9cd6bd772c0 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
@@ -277,6 +277,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
'field_changed': '_onFieldChanged',
}),
_avoidFieldUpdate: {},
+ MV_LINE_DEBOUNCE: 200,
/**
* create partner_id field in editable mode
@@ -289,6 +290,11 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
this.model = model;
this._initialState = state;
+ if (this.MV_LINE_DEBOUNCE) {
+ this._onSelectMoveLine = _.debounce(this._onSelectMoveLine, this.MV_LINE_DEBOUNCE, true);
+ } else {
+ this._onSelectMoveLine = this._onSelectMoveLine;
+ }
},
/**
* @override
diff --git a/addons/account/static/tests/reconciliation_tests.js b/addons/account/static/tests/reconciliation_tests.js
index 043cd09062c..829c5cbda02 100644
--- a/addons/account/static/tests/reconciliation_tests.js
+++ b/addons/account/static/tests/reconciliation_tests.js
@@ -585,6 +585,7 @@ odoo.define('account.reconciliation_tests', function (require) {
"use strict";
var ReconciliationClientAction = require('account.ReconciliationClientAction');
+var ReconciliationRenderer = require('account.ReconciliationRenderer');
var demoData = require('account.reconciliation_tests.data');
var testUtils = require('web.test_utils');
@@ -592,7 +593,13 @@ var testUtils = require('web.test_utils');
QUnit.module('account', {
beforeEach: function () {
this.params = demoData.getParams();
- }
+ testUtils.patch(ReconciliationRenderer.LineRenderer, {
+ MV_LINE_DEBOUNCE: 0,
+ });
+ },
+ afterEach: function () {
+ testUtils.unpatch(ReconciliationRenderer.LineRenderer);
+ },
}, function () {
QUnit.module('Reconciliation');
diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml
index 42138b29f8e..433defaffdb 100644
--- a/addons/account/views/account_invoice_view.xml
+++ b/addons/account/views/account_invoice_view.xml
@@ -165,8 +165,8 @@
<field name="user_id"/>
<field name="date_due"/>
<field name="origin"/>
- <field name="amount_untaxed" string="Tax Excluded" sum="Total"/>
- <field name="amount_tax" sum="Total"/>
+ <field name="amount_untaxed_invoice_signed" string="Tax Excluded" sum="Total"/>
+ <field name="amount_tax_signed" string="Tax" sum="Total"/>
<field name="amount_total_signed" string="Total" sum="Total"/>
<field name="residual_signed" string="Amount Due" sum="Amount Due"/>
<field name="currency_id" invisible="1"/>
diff --git a/addons/account/views/report_payment_receipt_templates.xml b/addons/account/views/report_payment_receipt_templates.xml
index 871ae39479f..c0e11a2ef7f 100644
--- a/addons/account/views/report_payment_receipt_templates.xml
+++ b/addons/account/views/report_payment_receipt_templates.xml
@@ -46,7 +46,7 @@
<td><span t-field="inv.date_invoice"/></td>
<td><span t-field="inv.number"/></td>
<td class="text-right"><span t-field="inv.amount_total"/></td>
- <td class="text-right"><span t-esc="inv.amount_total - inv.residual" t-options="{'widget': 'monetary', 'display_currency': inv.currency_id}"/></td>
+ <td class="text-right"><span t-esc="o._get_invoice_payment_amount(inv)" t-options="{'widget': 'monetary', 'display_currency': inv.currency_id}"/></td>
<td class="text-right"><span t-field="inv.residual"/></td>
</tr>
</tbody>
diff --git a/addons/account_cancel/views/account_views.xml b/addons/account_cancel/views/account_views.xml
index e3dd3821294..18f308bd9c5 100644
--- a/addons/account_cancel/views/account_views.xml
+++ b/addons/account_cancel/views/account_views.xml
@@ -17,7 +17,7 @@
<field name="inherit_id" ref="account.invoice_form"/>
<field name="arch" type="xml">
<xpath expr="//button[@name='action_invoice_draft']" position="before">
- <button name="action_invoice_cancel" type="object" states="draft,open,paid" string="Cancel" groups="account.group_account_invoice"/>
+ <button name="action_invoice_cancel" type="object" states="draft,open,in_payment,paid" string="Cancel" groups="account.group_account_invoice"/>
</xpath>
</field>
</record>
@@ -28,7 +28,7 @@
<field name="inherit_id" ref="account.invoice_supplier_form"/>
<field name="arch" type="xml">
<xpath expr="//button[@name='action_invoice_draft']" position="before">
- <button name="action_invoice_cancel" type="object" states="draft,open,paid" string="Cancel" groups="account.group_account_invoice"/>
+ <button name="action_invoice_cancel" type="object" states="draft,open,in_payment,paid" string="Cancel" groups="account.group_account_invoice"/>
</xpath>
</field>
</record>
diff --git a/addons/calendar/models/calendar.py b/addons/calendar/models/calendar.py
index 957bb1bdc61..2c0c2e32877 100644
--- a/addons/calendar/models/calendar.py
+++ b/addons/calendar/models/calendar.py
@@ -406,9 +406,8 @@ class AlarmManager(models.AbstractModel):
all_meetings = self.get_next_potential_limit_alarm('notification', partner_id=partner.id)
time_limit = 3600 * 24 # return alarms of the next 24 hours
- for event_id in all_meetings:
- max_delta = all_meetings[event_id]['max_duration']
- meeting = self.env['calendar.event'].browse(event_id)
+ for meeting in self.env['calendar.event'].search([('id', 'in', list(all_meetings))]): # cannot browse
+ max_delta = all_meetings[meeting.id]['max_duration']
if meeting.recurrency:
b_found = False
last_found = False
diff --git a/addons/l10n_ch/models/mail_template.py b/addons/l10n_ch/models/mail_template.py
index e3a1deca1dd..7ec29d4d508 100644
--- a/addons/l10n_ch/models/mail_template.py
+++ b/addons/l10n_ch/models/mail_template.py
@@ -36,6 +36,9 @@ class MailTemplate(models.Model):
pdf = self.env.ref('l10n_ch.l10n_ch_isr_report').render_qweb_pdf([res_id])[0]
pdf = base64.b64encode(pdf)
- attachments_list = multi_mode and rslt[res_id]['attachments'] or rslt['attachments']
- attachments_list.append((report_name, pdf))
+ attachments_list = multi_mode and rslt[res_id].get('attachments', False) or rslt.get('attachments', False)
+ if attachments_list:
+ attachments_list.append((report_name, pdf))
+ else:
+ rslt[res_id]['attachments'] = [(report_name, pdf)]
return rslt
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 @@
<record id="hr_contract_form_in_inherit" model="ir.ui.view">
<field name="name">hr.contract.form.in.inherit</field>
<field name="model">hr.contract</field>
- <field name="inherit_id" ref="hr_payroll.hr_contract_form_inherit"/>
+ <field name="inherit_id" ref="hr_contract.hr_contract_view_form"/>
<field name="arch" type="xml">
<data>
- <xpath expr="//field[@name='struct_id']" position="after">
+ <xpath expr="//group[@name='duration_group']" position="after">
<group col="2" colspan="2" name="right_column">
<separator colspan="2" string="Allowance"/>
<field name="driver_salay"/>
diff --git a/addons/mrp_byproduct/models/mrp_production.py b/addons/mrp_byproduct/models/mrp_production.py
index 1ecf0c3484a..fd7613f323d 100644
--- a/addons/mrp_byproduct/models/mrp_production.py
+++ b/addons/mrp_byproduct/models/mrp_production.py
@@ -61,13 +61,14 @@ class MrpProductProduce(models.TransientModel):
for by_product_move in by_product_moves:
rounding = by_product_move.product_uom.rounding
quantity = float_round(self.qty_producing * by_product_move.unit_factor, precision_rounding=rounding)
+ location_dest_id = by_product_move.location_dest_id.get_putaway_strategy(by_product_move.product_id).id or by_product_move.location_dest_id.id
values = {
'move_id': by_product_move.id,
'product_id': by_product_move.product_id.id,
'production_id': self.production_id.id,
'product_uom_id': by_product_move.product_uom.id,
'location_id': by_product_move.location_id.id,
- 'location_dest_id': by_product_move.location_dest_id.id,
+ 'location_dest_id': location_dest_id,
}
if by_product_move.product_id.tracking == 'lot':
values.update({
diff --git a/addons/payment_authorize/controllers/main.py b/addons/payment_authorize/controllers/main.py
index 70d641564a6..cf6877218db 100644
--- a/addons/payment_authorize/controllers/main.py
+++ b/addons/payment_authorize/controllers/main.py
@@ -55,14 +55,14 @@ class AuthorizeController(http.Controller):
except ValidationError as e:
message = e.args[0]
if isinstance(message, dict) and 'missing_fields' in message:
- msg = _("The transaction cannot be processed because some contact details are missing or invalid: ")
- message = msg + ', '.join(message['missing_fields']) + '. '
if request.env.user._is_public():
- message += _("Please sign in to complete your profile.")
+ message = _("Please sign in to complete the payment.")
# update message if portal mode = b2b
if request.env['ir.config_parameter'].sudo().get_param('auth_signup.allow_uninvited', 'False').lower() == 'false':
- message += _("If you don't have any account, please ask your salesperson to update your profile. ")
+ message += _(" If you don't have any account, ask your salesperson to grant you a portal access. ")
else:
+ msg = _("The transaction cannot be processed because some contact details are missing or invalid: ")
+ message = msg + ', '.join(message['missing_fields']) + '. '
message += _("Please complete your profile. ")
return {
diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py
index cdad087eba2..8518e351c47 100644
--- a/addons/point_of_sale/models/pos_order.py
+++ b/addons/point_of_sale/models/pos_order.py
@@ -756,7 +756,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 29404cbef6b..44bc8722717 100644
--- a/addons/point_of_sale/static/src/js/screens.js
+++ b/addons/point_of_sale/static/src/js/screens.js
@@ -151,6 +151,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"),
+ });
+ }
+ },
});
/*--------------------------------------*\
@@ -1550,7 +1605,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;
@@ -1654,9 +1711,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()));
@@ -2081,45 +2161,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 4827706756c..26d315631a8 100644
--- a/addons/point_of_sale/static/src/xml/pos.xml
+++ b/addons/point_of_sale/static/src/xml/pos.xml
@@ -719,6 +719,9 @@
</span>
</div>
<div class="centered-content touch-scrollable">
+ <div class="button print_invoice">
+ <i class='fa fa-print'></i> Print Invoice
+ </div>
<div class="button print">
<i class='fa fa-print'></i> Print Receipt
</div>
diff --git a/addons/portal/controllers/mail.py b/addons/portal/controllers/mail.py
index 411106dc188..8d1378218a1 100644
--- a/addons/portal/controllers/mail.py
+++ b/addons/portal/controllers/mail.py
@@ -137,7 +137,7 @@ class PortalChatter(http.Controller):
raise Forbidden()
# Non-employee see only messages with not internal subtype (aka, no internal logs)
if not request.env['res.users'].has_group('base.group_user'):
- domain = expression.AND([['&', ('subtype_id', '!=', False), ('subtype_id.internal', '=', False)], domain])
+ domain = expression.AND([['&', '&', ('subtype_id', '!=', False), ('subtype_id.internal', '=', False), ('website_published', '=', True)], domain])
Message = request.env['mail.message'].sudo()
return {
'messages': Message.search(domain, limit=limit, offset=offset).portal_message_format(),
diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py
index 5fc9bed772a..c6c8eb92325 100644
--- a/addons/product/models/product_template.py
+++ b/addons/product/models/product_template.py
@@ -362,6 +362,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)
diff --git a/addons/project/models/project.py b/addons/project/models/project.py
index e2b9510c1d8..8796058b71d 100644
--- a/addons/project/models/project.py
+++ b/addons/project/models/project.py
@@ -458,7 +458,6 @@ class Task(models.Model):
date_assign = fields.Datetime(string='Assigning Date', index=True, copy=False, readonly=True)
date_deadline = fields.Date(string='Deadline', index=True, copy=False, tracking=True)
date_last_stage_update = fields.Datetime(string='Last Stage Update',
- default=fields.Datetime.now,
index=True,
copy=False,
readonly=True)
@@ -671,9 +670,10 @@ class Task(models.Model):
# user_id change: update date_assign
if vals.get('user_id'):
vals['date_assign'] = fields.Datetime.now()
- # Stage change: Update date_end if folded stage
+ # Stage change: Update date_end if folded stage and date_last_stage_update
if vals.get('stage_id'):
vals.update(self.update_date_end(vals['stage_id']))
+ vals['date_last_stage_update'] = fields.Datetime.now()
task = super(Task, self.with_context(context)).create(vals)
return task
diff --git a/addons/purchase_stock/models/account_invoice.py b/addons/purchase_stock/models/account_invoice.py
index 048853d4c64..78cd9ba2707 100644
--- a/addons/purchase_stock/models/account_invoice.py
+++ b/addons/purchase_stock/models/account_invoice.py
@@ -48,7 +48,6 @@ class AccountInvoice(models.Model):
inv.currency_id,
company=inv.company_id, date=fields.Date.today(), round=False,
)
- line_quantity = line['quantity']
if i_line.product_id.cost_method != 'standard' and i_line.purchase_line_id:
po_currency = i_line.purchase_id.currency_id
@@ -86,7 +85,6 @@ class AccountInvoice(models.Model):
# in Stock Move, price unit is in company_currency
valuation_price_unit = valuation_price_unit_total / valuation_total_qty
valuation_price_unit = i_line.product_id.uom_id._compute_price(valuation_price_unit, i_line.uom_id)
- line_quantity = valuation_total_qty
elif i_line.product_id.cost_method == 'fifo':
# In this condition, we have a real price-valuated product which has not yet been received
@@ -95,7 +93,7 @@ class AccountInvoice(models.Model):
company=po_company, date=inv.date or inv.date_invoice, round=False,
)
- interim_account_price = valuation_price_unit * line_quantity
+ interim_account_price = valuation_price_unit * line['quantity']
invoice_cur_prec = inv.currency_id.decimal_places
if float_compare(valuation_price_unit, i_line.price_unit, precision_digits=invoice_cur_prec) != 0 and float_compare(line['price_unit'], i_line.price_unit, precision_digits=invoice_cur_prec) == 0:
@@ -126,7 +124,7 @@ class AccountInvoice(models.Model):
'type': 'src',
'name': i_line.name[:64],
'price_unit': price_unit_val_dif,
- 'quantity': line_quantity,
+ 'quantity': line['quantity'],
'price': inv.currency_id.round(price_val_dif),
'account_id': acc,
'product_id': line['product_id'],
diff --git a/addons/purchase_stock/tests/test_stockvaluation.py b/addons/purchase_stock/tests/test_stockvaluation.py
index 118f54b63a2..f79e6f04af3 100644
--- a/addons/purchase_stock/tests/test_stockvaluation.py
+++ b/addons/purchase_stock/tests/test_stockvaluation.py
@@ -922,3 +922,245 @@ class TestStockValuationWithCOA(AccountingTestCase):
exchange_stock_line = reconciled_lines.filtered(lambda l: l.journal_id == exhange_diff_journal)
self.assertEqual(exchange_stock_line.amount_currency, 0.00)
self.assertAlmostEqual(exchange_stock_line.balance, 27.86)
+
+ def test_average_realtime_with_two_delivery_anglo_saxon_valuation_multicurrency_different_dates(self):
+ """
+ The PO and invoice are in the same foreign currency.
+ The deliveries occur at different times and rates
+ The invoice is created at an even different date
+ This should create a price difference entry.
+ """
+ company = self.env.user.company_id
+ company.anglo_saxon_accounting = True
+
+ date_po = '2019-01-01'
+ date_delivery = '2019-01-08'
+ date_delivery1 = '2019-01-10'
+ date_invoice = '2019-01-16'
+ date_invoice1 = '2019-01-20'
+
+ self.product1.categ_id.property_valuation = 'real_time'
+ self.product1.categ_id.property_cost_method = 'average'
+ product_avg = self.product1.product_tmpl_id.copy({
+ 'purchase_method': 'purchase',
+ 'name': 'AVG',
+ 'standard_price': 60,
+ 'property_account_creditor_price_difference': self.price_diff_account.id
+ }).product_variant_id
+ product_avg.invoice_policy = 'order'
+
+ # SetUp currency and rates
+ self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", (self.usd_currency.id, company.id))
+ self.env['res.currency.rate'].search([]).unlink()
+ self.env['res.currency.rate'].create({
+ 'name': date_po,
+ 'rate': 1.0,
+ 'currency_id': self.usd_currency.id,
+ 'company_id': company.id,
+ })
+ self.env['res.currency.rate'].create({
+ 'name': date_po,
+ 'rate': 1.5,
+ 'currency_id': self.eur_currency.id,
+ 'company_id': company.id,
+ })
+
+ self.env['res.currency.rate'].create({
+ 'name': date_delivery,
+ 'rate': 0.7,
+ 'currency_id': self.eur_currency.id,
+ 'company_id': company.id,
+ })
+ self.env['res.currency.rate'].create({
+ 'name': date_delivery1,
+ 'rate': 0.8,
+ 'currency_id': self.eur_currency.id,
+ 'company_id': company.id,
+ })
+
+ self.env['res.currency.rate'].create({
+ 'name': date_invoice,
+ 'rate': 2,
+ 'currency_id': self.eur_currency.id,
+ 'company_id': company.id,
+ })
+ self.env['res.currency.rate'].create({
+ 'name': date_invoice1,
+ 'rate': 2.2,
+ 'currency_id': self.eur_currency.id,
+ 'company_id': company.id,
+ })
+
+ # To allow testing validation of PO
+ def _today(*args, **kwargs):
+ return date_po
+ # To allow testing validation of Delivery
+ delivery_now = date_delivery
+ def _now(*args, **kwargs):
+ return delivery_now + ' 01:00:00'
+
+ patchers = [
+ patch('odoo.fields.Date.context_today', _today),
+ patch('odoo.fields.Datetime.now', _now),
+ ]
+
+ for p in patchers:
+ p.start()
+
+ # Proceed
+ po = self.env['purchase.order'].create({
+ 'currency_id': self.eur_currency.id,
+ 'partner_id': self.partner_id.id,
+ 'date_order': date_po,
+ 'order_line': [
+ (0, 0, {
+ 'name': product_avg.name,
+ 'product_id': product_avg.id,
+ 'product_qty': 10.0,
+ 'product_uom': product_avg.uom_po_id.id,
+ 'price_unit': 30.0,
+ 'date_planned': date_po,
+ })
+ ],
+ })
+ po.button_confirm()
+
+ line_product_avg = po.order_line.filtered(lambda l: l.product_id == product_avg)
+
+ picking = po.picking_ids
+ (picking.move_lines
+ .filtered(lambda l: l.purchase_line_id == line_product_avg)
+ .write({'quantity_done': 5.0}))
+
+ picking.button_validate()
+ picking.action_done() # Create Backorder
+
+ inv = self.env['account.invoice'].create({
+ 'type': 'in_invoice',
+ 'date_invoice': date_invoice,
+ 'currency_id': self.eur_currency.id,
+ 'purchase_id': po.id,
+ 'partner_id': self.partner_id.id,
+ 'invoice_line_ids': [
+ (0, 0, {
+ 'name': product_avg.name,
+ 'price_subtotal': 100.0,
+ 'price_unit': 20.0,
+ 'product_id': product_avg.id,
+ 'purchase_id': po.id,
+ 'purchase_line_id': line_product_avg.id,
+ 'quantity': 5.0,
+ 'account_id': self.stock_input_account.id,
+ })
+ ]
+ })
+
+ inv.action_invoice_open()
+
+ backorder_picking = self.env['stock.picking'].search([('backorder_id', '=', picking.id)])
+ delivery_now = date_delivery1
+ (backorder_picking.move_lines
+ .filtered(lambda l: l.purchase_line_id == line_product_avg)
+ .write({'quantity_done': 5.0}))
+ backorder_picking.button_validate()
+
+ inv1 = self.env['account.invoice'].create({
+ 'type': 'in_invoice',
+ 'date_invoice': date_invoice1,
+ 'currency_id': self.eur_currency.id,
+ 'purchase_id': po.id,
+ 'partner_id': self.partner_id.id,
+ 'invoice_line_ids': [
+ (0, 0, {
+ 'name': product_avg.name,
+ 'price_subtotal': 200.0,
+ 'price_unit': 40.0,
+ 'product_id': product_avg.id,
+ 'purchase_id': po.id,
+ 'purchase_line_id': line_product_avg.id,
+ 'quantity': 5.0,
+ 'account_id': self.stock_input_account.id,
+ })
+ ]
+ })
+
+ inv1.action_invoice_open()
+
+ for p in patchers:
+ p.stop()
+
+ ##########################
+ # Invoice 0 #
+ ##########################
+ move_lines = inv.move_id.line_ids
+ self.assertEqual(len(move_lines), 3)
+
+ # PAYABLE CHECK
+ payable_line = move_lines.filtered(lambda l: l.account_id.internal_type == 'payable')
+ self.assertEqual(payable_line.amount_currency, -100.0)
+ self.assertAlmostEqual(payable_line.balance, -50.00)
+
+ # # PRODUCTS CHECKS
+
+ # DELIVERY DIFFERENCE (AVERAGE)
+ stock_line = move_lines.filtered(lambda l: l.account_id == self.stock_input_account)
+ self.assertEqual(stock_line.journal_id, inv.journal_id)
+ self.assertEqual(stock_line.amount_currency, 150.00)
+ self.assertAlmostEqual(stock_line.balance, 75.00)
+
+ price_diff_line = move_lines.filtered(lambda l: l.account_id == self.price_diff_account)
+ self.assertEqual(price_diff_line.amount_currency, -50.00)
+ self.assertAlmostEqual(price_diff_line.balance, -25.00)
+
+ full_reconcile = stock_line.full_reconcile_id
+ self.assertTrue(full_reconcile.exists())
+
+ reconciled_lines = full_reconcile.reconciled_line_ids - stock_line
+ self.assertEqual(len(reconciled_lines), 2)
+
+ stock_journal_line = reconciled_lines.filtered(lambda l: l.journal_id == self.stock_journal)
+ self.assertEqual(stock_journal_line.amount_currency, -150)
+ self.assertAlmostEqual(stock_journal_line.balance, -214.29)
+
+ exhange_diff_journal = company.currency_exchange_journal_id.exists()
+ exchange_stock_line = reconciled_lines.filtered(lambda l: l.journal_id == exhange_diff_journal)
+ self.assertEqual(exchange_stock_line.amount_currency, 0.00)
+ self.assertAlmostEqual(exchange_stock_line.balance, 139.29)
+
+ ##########################
+ # Invoice 1 #
+ ##########################
+ move_lines = inv1.move_id.line_ids
+ self.assertEqual(len(move_lines), 3)
+
+ # PAYABLE CHECK
+ payable_line = move_lines.filtered(lambda l: l.account_id.internal_type == 'payable')
+ self.assertEqual(payable_line.amount_currency, -200.0)
+ self.assertAlmostEqual(payable_line.balance, -90.91)
+
+ # # PRODUCTS CHECKS
+
+ # DELIVERY DIFFERENCE (AVERAGE)
+ stock_line = move_lines.filtered(lambda l: l.account_id == self.stock_input_account)
+ self.assertEqual(stock_line.journal_id, inv.journal_id)
+ self.assertEqual(stock_line.amount_currency, 150.00)
+ self.assertAlmostEqual(stock_line.balance, 68.18)
+
+ price_diff_line = move_lines.filtered(lambda l: l.account_id == self.price_diff_account)
+ self.assertEqual(price_diff_line.amount_currency, 50.00)
+ self.assertAlmostEqual(price_diff_line.balance, 22.73)
+
+ full_reconcile = stock_line.full_reconcile_id
+ self.assertTrue(full_reconcile.exists())
+
+ reconciled_lines = full_reconcile.reconciled_line_ids - stock_line
+ self.assertEqual(len(reconciled_lines), 2)
+
+ stock_journal_line = reconciled_lines.filtered(lambda l: l.journal_id == self.stock_journal)
+ self.assertEqual(stock_journal_line.amount_currency, -150)
+ self.assertAlmostEqual(stock_journal_line.balance, -187.5)
+
+ exhange_diff_journal = company.currency_exchange_journal_id.exists()
+ exchange_stock_line = reconciled_lines.filtered(lambda l: l.journal_id == exhange_diff_journal)
+ self.assertEqual(exchange_stock_line.amount_currency, 0.00)
+ self.assertAlmostEqual(exchange_stock_line.balance, 119.32)
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index a46f7ffebb8..ff947db684a 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -1101,7 +1101,7 @@ class SaleOrderLine(models.Model):
result = super(SaleOrderLine, self).write(values)
return result
- order_id = fields.Many2one('sale.order', string='Order Reference', required=True, ondelete='cascade', index=True, copy=False, readonly=True)
+ order_id = fields.Many2one('sale.order', string='Order Reference', required=True, ondelete='cascade', index=True, copy=False)
name = fields.Text(string='Description', required=True)
sequence = fields.Integer(string='Sequence', default=10)
diff --git a/addons/sale/views/sale_portal_templates.xml b/addons/sale/views/sale_portal_templates.xml
index 95e863c3a20..b6176f1d764 100644
--- a/addons/sale/views/sale_portal_templates.xml
+++ b/addons/sale/views/sale_portal_templates.xml
@@ -127,7 +127,7 @@
<t t-set="classes" t-value="'col-lg-auto d-print-none'"/>
<t t-set="title">
- <h2 class="mb-0"><b t-field="sale_order.amount_total"/> </h2>
+ <h2 class="mb-0"><b t-field="sale_order.amount_total" data-id="total_amount"/> </h2>
</t>
<t t-set="entries">
<ul class="list-group list-group-flush flex-wrap flex-row flex-lg-column">
@@ -167,14 +167,15 @@
<b t-field="sale_order.amount_undiscounted"
t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'
style="text-decoration: line-through"
- class="d-block mt-1"/>
+ class="d-block mt-1"
+ data-id="amount_undiscounted" />
</small>
<t t-if="sale_order.amount_untaxed == sale_order.amount_total">
- <h4 t-field="sale_order.amount_total" class="text-success"/>
+ <h4 t-field="sale_order.amount_total" class="text-success" data-id="total_amount"/>
</t>
<t t-else="">
- <h4 t-field="sale_order.amount_untaxed" class="text-success mb-0"/>
- <small>(<span t-field="sale_order.amount_total"/> Incl. tax)</small>
+ <h4 t-field="sale_order.amount_untaxed" class="text-success mb-0" data-id="total_untaxed"/>
+ <small>(<span t-field="sale_order.amount_total" data-id="total_amount"/> Incl. tax)</small>
</t>
</li>
</t>
@@ -374,7 +375,7 @@
</t>
</div>
<div class="mb-3">
- <strong>Date:</strong> <span t-field="sale_order.create_date" t-options='{"widget": "date"}'/>
+ <strong>Date:</strong> <span t-field="sale_order.date_order" t-options='{"widget": "date"}'/>
</div>
<div class="mb-3" t-if="sale_order.validity_date">
<strong>Expiration Date:</strong> <span t-field="sale_order.validity_date" t-options='{"widget": "date"}'/>
@@ -514,6 +515,7 @@
<div id="total" class="row" name="total" style="page-break-inside: avoid;">
<div t-attf-class="#{'col-4' if report_type != 'html' else 'col-sm-7 col-md-5'} ml-auto">
+ <!-- Should be replaced in master by t-call="sale.sale_order_portal_content_totals_table" -->
<table class="table table-sm">
<tr class="border-black" style="border-bottom:1px solid #dddddd;">
<td><strong>Subtotal</strong></td>
@@ -577,4 +579,47 @@
</div>
</template>
+ <template id="sale_order_portal_content_totals_table">
+ <table class="table table-sm">
+ <tr class="border-black">
+ <td><strong>Subtotal</strong></td>
+ <td class="text-right">
+ <span
+ data-id="total_untaxed"
+ t-field="sale_order.amount_untaxed"
+ t-options='{"widget": "monetary","display_currency": sale_order.pricelist_id.currency_id}'
+ />
+ </td>
+ </tr>
+ <t t-foreach="sale_order.amount_by_group" t-as="amount_by_group">
+ <tr>
+ <t t-if="amount_by_group[3] == 1 and sale_order.amount_untaxed == amount_by_group[2]">
+ <td>
+ <span t-esc="amount_by_group[0]"/>
+ <span>&amp;nbsp;<span>on</span>&amp;nbsp;<t t-esc="amount_by_group[2]" t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'/></span>
+ </td>
+ <td class="text-right">
+ <span t-esc="amount_by_group[1]"
+ t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'/>
+ </td>
+ </t>
+ <t t-else ="">
+ <td>
+ <span t-esc="amount_by_group[0]"/>
+ </td>
+ <td class="text-right">
+ <span t-esc="amount_by_group[1]"
+ t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'/>
+ </td>
+ </t>
+ </tr>
+ </t>
+ <tr class="border-black">
+ <td><strong>Total</strong></td>
+ <td class="text-right">
+ <span data-id="total_amount" t-field="sale_order.amount_total" t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'/>
+ </td>
+ </tr>
+ </table>
+ </template>
</odoo>
diff --git a/addons/sale/views/sale_views.xml b/addons/sale/views/sale_views.xml
index 18b06b56c9a..d6313076e6c 100644
--- a/addons/sale/views/sale_views.xml
+++ b/addons/sale/views/sale_views.xml
@@ -923,7 +923,7 @@
</div>
<group>
<group>
- <field name="order_id"/>
+ <field name="order_id" readonly="1"/>
<field name="product_id" readonly="1"/>
<field name="name" readonly="1"/>
<field name="product_uom_qty" readonly="1"/>
diff --git a/addons/sale_management/controllers/portal.py b/addons/sale_management/controllers/portal.py
index 1796beb7223..eb4e945b8d3 100644
--- a/addons/sale_management/controllers/portal.py
+++ b/addons/sale_management/controllers/portal.py
@@ -47,14 +47,21 @@ class CustomerPortal(CustomerPortal):
currency = order_sudo.currency_id
format_price = partial(formatLang, request.env, digits=currency.decimal_places)
- return {
+ results = {
'order_line_product_uom_qty': str(quantity),
'order_line_price_total': format_price(order_line.price_total),
'order_line_price_subtotal': format_price(order_line.price_subtotal),
'order_amount_total': format_price(order_sudo.amount_total),
'order_amount_untaxed': format_price(order_sudo.amount_untaxed),
'order_amount_tax': format_price(order_sudo.amount_tax),
+ 'order_amount_undiscounted': format_price(order_sudo.amount_undiscounted),
}
+ try:
+ results['order_totals_table'] = request.env['ir.ui.view'].render_template('sale.sale_order_portal_content_totals_table', {'sale_order': order_sudo})
+ except ValueError:
+ pass
+
+ return results
@http.route(["/my/orders/<int:order_id>/add_option/<int:option_id>"], type='http', auth="public", website=True)
def add(self, order_id, option_id, access_token=None, **post):
diff --git a/addons/sale_management/static/src/js/sale_management.js b/addons/sale_management/static/src/js/sale_management.js
index 88b956e0e6e..98484d40f03 100644
--- a/addons/sale_management/static/src/js/sale_management.js
+++ b/addons/sale_management/static/src/js/sale_management.js
@@ -39,7 +39,7 @@ if (!$('.o_portal_sale_sidebar').length) {
var order_id = href.match(/my\/orders\/([0-9]+)/);
var line_id = href.match(/update_line\/([0-9]+)/);
var params = {
- 'line_id': line_id[1],
+ 'line_id': parseInt(line_id[1]),
'remove': this.$el.is('[href*="remove"]'),
'unlink': this.$el.is('[href*="unlink"]'),
'input_quantity': quantity >= 0 ? quantity : false,
@@ -49,6 +49,7 @@ if (!$('.o_portal_sale_sidebar').length) {
params.access_token = token;
}
+ order_id = parseInt(order_id[1])
this._callUpdateLineRoute(order_id, params).then(this._updateOrderValues.bind(this));
return false;
},
@@ -66,19 +67,20 @@ if (!$('.o_portal_sale_sidebar').length) {
* when the quantity of a product has changed
*
* @private
+ * @param {integer} order_id
* @param {Object} params
* @return {Deferred}
*/
_callUpdateLineRoute: function (order_id, params) {
var def = new $.Deferred();
- var url = "/my/orders/" + parseInt(order_id[1]) + "/update_line_dict";
+ var url = "/my/orders/" + order_id + "/update_line_dict";
ajax.jsonRpc(url, 'call', params)
.then(def.resolve.bind(def))
.fail(function () {
// Compatibility: the server may not have been restarted
// So the real route may not exist
delete params.input_quantity;
- ajax.jsonRpc("/quote/update_line", 'call', params)
+ ajax.jsonRpc("/my/orders/" + order_id + "/update_line", 'call', params)
.fail(def.reject.bind(def))
.then(function (data) {
// Data is an array, convert it to a dict
@@ -108,6 +110,8 @@ if (!$('.o_portal_sale_sidebar').length) {
var orderAmountTotal = data.order_amount_total;
var orderAmountUntaxed = data.order_amount_untaxed;
var orderAmountTax = data.order_amount_tax;
+ var orderAmountUndiscounted = data.order_amount_undiscounted;
+ var orderTotalsTable = $(data.order_totals_table);
var lineProductUomQty = data.order_line_product_uom_qty;
var linePriceTotal = data.order_line_price_total;
@@ -129,6 +133,13 @@ if (!$('.o_portal_sale_sidebar').length) {
if (orderAmountTotal !== undefined) {
this.elems.$orderAmountTotal.text(orderAmountTotal);
}
+
+ if (orderAmountUndiscounted !== undefined) {
+ this.elems.$orderAmountUndiscounted.text(orderAmountUndiscounted);
+ }
+ if (orderTotalsTable) {
+ this.elems.$orderTotalsTable.find('table').replaceWith(orderTotalsTable);
+ }
},
/**
* Locate in the DOM the elements to update
@@ -147,12 +158,13 @@ if (!$('.o_portal_sale_sidebar').length) {
$linePriceTotal = $linePriceSubTotal = $parentTr.find('.oe_currency_value').last();
}
- var $orderAmountUntaxed = $('[data-id="total_untaxed"]>span');
- var $orderAmountTotal = $('[data-id="total_amount"]>span');
+ var $orderAmountUntaxed = $('[data-id="total_untaxed"]').find('span, b');
+ var $orderAmountTotal = $('[data-id="total_amount"]').find('span, b');
+ var $orderAmountUndiscounted = $('[data-id="amount_undiscounted"]').find('span, b');
if (!$orderAmountUntaxed.length) {
$orderAmountUntaxed = $orderAmountTotal.eq(1);
- $orderAmountTotal = $orderAmountTotal.eq(0).add($orderAmountTotal.eq(3));
+ $orderAmountTotal = $orderAmountTotal.eq(0).add($orderAmountTotal.eq(2));
}
return {
@@ -161,6 +173,8 @@ if (!$('.o_portal_sale_sidebar').length) {
$linePriceTotal: $linePriceTotal,
$orderAmountUntaxed: $orderAmountUntaxed,
$orderAmountTotal: $orderAmountTotal,
+ $orderTotalsTable: $('#total'),
+ $orderAmountUndiscounted: $orderAmountUndiscounted,
}
}
});
diff --git a/addons/sale_timesheet/models/account.py b/addons/sale_timesheet/models/account.py
index bfdc17f7d27..81895718a96 100644
--- a/addons/sale_timesheet/models/account.py
+++ b/addons/sale_timesheet/models/account.py
@@ -57,11 +57,15 @@ class AccountAnalyticLine(models.Model):
@api.multi
def write(self, values):
# prevent to update invoiced timesheets if one line is of type delivery
+ self._check_can_write(values)
+ result = super(AccountAnalyticLine, self).write(values)
+ return result
+
+ @api.multi
+ def _check_can_write(self, values):
if self.sudo().filtered(lambda aal: aal.so_line.product_id.invoice_policy == "delivery") and self.filtered(lambda timesheet: timesheet.timesheet_invoice_id):
if any([field_name in values for field_name in ['unit_amount', 'employee_id', 'project_id', 'task_id', 'so_line', 'amount', 'date']]):
raise UserError(_('You can not modify already invoiced timesheets (linked to a Sales order items invoiced on Time and material).'))
- result = super(AccountAnalyticLine, self).write(values)
- return result
@api.model
def _timesheet_preprocess(self, values):
diff --git a/addons/sale_timesheet/report/project_profitability_report_analysis.py b/addons/sale_timesheet/report/project_profitability_report_analysis.py
index 2ec12acea09..2dc397495aa 100644
--- a/addons/sale_timesheet/report/project_profitability_report_analysis.py
+++ b/addons/sale_timesheet/report/project_profitability_report_analysis.py
@@ -68,7 +68,7 @@ class ProfitabilityAnalysis(models.Model):
ELSE 0.0
END AS amount_untaxed_to_invoice,
CASE
- WHEN SOL.qty_delivered_method IN ('timesheet', 'manual') THEN (SOL.untaxed_amount_invoiced / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END)
+ WHEN SOL.qty_delivered_method IN ('timesheet', 'manual') THEN (COALESCE(SOL.untaxed_amount_invoiced, COST_SUMMARY.downpayment_invoiced) / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END)
ELSE 0.0
END AS amount_untaxed_invoiced,
COST_SUMMARY.timesheet_unit_amount AS timesheet_unit_amount,
@@ -83,7 +83,8 @@ class ProfitabilityAnalysis(models.Model):
sale_line_id,
SUM(timesheet_unit_amount) AS timesheet_unit_amount,
SUM(timesheet_cost) AS timesheet_cost,
- SUM(expense_cost) AS expense_cost
+ SUM(expense_cost) AS expense_cost,
+ SUM(downpayment_invoiced) AS downpayment_invoiced
FROM (
SELECT
P.id AS project_id,
@@ -91,7 +92,8 @@ class ProfitabilityAnalysis(models.Model):
TS.so_line AS sale_line_id,
SUM(TS.unit_amount) AS timesheet_unit_amount,
SUM(TS.amount) AS timesheet_cost,
- 0.0 AS expense_cost
+ 0.0 AS expense_cost,
+ 0.0 AS downpayment_invoiced
FROM account_analytic_line TS, project_project P
WHERE TS.project_id IS NOT NULL AND P.id = TS.project_id AND P.active = 't' AND P.allow_timesheets = 't'
GROUP BY P.id, TS.so_line
@@ -104,7 +106,8 @@ class ProfitabilityAnalysis(models.Model):
AAL.so_line AS sale_line_id,
0.0 AS timesheet_unit_amount,
0.0 AS timesheet_cost,
- SUM(AAL.amount) AS expense_cost
+ SUM(AAL.amount) AS expense_cost,
+ 0.0 AS downpayment_invoiced
FROM project_project P
LEFT JOIN account_analytic_account AA ON P.analytic_account_id = AA.id
LEFT JOIN account_analytic_line AAL ON AAL.account_id = AA.id
@@ -116,10 +119,47 @@ class ProfitabilityAnalysis(models.Model):
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
+ MY_SOLS.id AS sale_line_id,
+ 0.0 AS timesheet_unit_amount,
+ 0.0 AS timesheet_cost,
+ 0.0 AS expense_cost,
+ CASE WHEN MY_SOLS.invoice_status = 'invoiced' THEN MY_SOLS.price_reduce ELSE 0.0 END AS downpayment_invoiced
+ FROM project_project P
+ LEFT JOIN sale_order_line MY_SOL ON P.sale_line_id = MY_SOL.id
+ LEFT JOIN sale_order MY_S ON MY_SOL.order_id = MY_S.id
+ LEFT JOIN sale_order_line MY_SOLS ON MY_SOLS.order_id = MY_S.id
+ WHERE MY_SOLS.is_downpayment = 't'
+ GROUP BY P.id, MY_SOLS.id
+
+ UNION
+
+ SELECT
+ P.id AS project_id,
+ P.analytic_account_id AS analytic_account_id,
+ OLIS.id AS sale_line_id,
+ 0.0 AS timesheet_unit_amount,
+ 0.0 AS timesheet_cost,
+ OLIS.price_reduce AS expense_cost,
+ 0.0 AS downpayment_invoiced
+ FROM project_project P
+ LEFT JOIN account_analytic_account ANAC ON P.analytic_account_id = ANAC.id
+ LEFT JOIN account_analytic_line ANLI ON ANAC.id = ANLI.account_id
+ LEFT JOIN sale_order_line OLI ON P.sale_line_id = OLI.id
+ LEFT JOIN sale_order ORD ON OLI.order_id = ORD.id
+ LEFT JOIN sale_order_line OLIS ON ORD.id = OLIS.order_id
+ WHERE OLIS.product_id = ANLI.product_id AND OLIS.is_downpayment = 't' AND ANLI.amount < 0.0 AND ANLI.project_id IS NULL AND P.active = 't' AND P.allow_timesheets = 't'
+ GROUP BY P.id, OLIS.id
+
+ UNION
+
+ SELECT
+ P.id AS project_id,
+ P.analytic_account_id AS analytic_account_id,
SOL.id AS sale_line_id,
0.0 AS timesheet_unit_amount,
0.0 AS timesheet_cost,
- 0.0 AS expense_cost
+ 0.0 AS expense_cost,
+ 0.0 AS downpayment_invoiced
FROM sale_order_line SOL
INNER JOIN project_project P ON SOL.project_id = P.id
WHERE P.active = 't' AND P.allow_timesheets = 't'
@@ -132,7 +172,8 @@ class ProfitabilityAnalysis(models.Model):
SOL.id AS sale_line_id,
0.0 AS timesheet_unit_amount,
0.0 AS timesheet_cost,
- 0.0 AS expense_cost
+ 0.0 AS expense_cost,
+ 0.0 AS downpayment_invoiced
FROM sale_order_line SOL
INNER JOIN project_task T ON SOL.task_id = T.id
INNER JOIN project_project P ON P.id = T.project_id
diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py
index 23b75118216..81b161df058 100644
--- a/addons/stock/models/stock_picking.py
+++ b/addons/stock/models/stock_picking.py
@@ -1120,3 +1120,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 adb5c50923b..44b5802f497 100644
--- a/addons/stock/views/stock_move_views.xml
+++ b/addons/stock/views/stock_move_views.xml
@@ -66,6 +66,38 @@
</field>
</record>
+ <record id="view_picking_move_tree" model="ir.ui.view">
+ <field name="name">stock.picking.move.tree</field>
+ <field name="model">stock.move</field>
+ <field eval="50" name="priority"/>
+ <field name="arch" type="xml">
+ <tree decoration-danger="state != 'done' and quantity_done > reserved_availability and show_reserved_availability" decoration-muted="scrapped == True or state == 'cancel' or (state == 'done' and is_locked == True)" string="Stock Moves" editable="bottom">
+ <field name="name" invisible="1"/>
+ <field name="date_expected" invisible="1"/>
+ <field name="state" invisible="1" readonly="0"/>
+ <field name="picking_type_id" invisible="1"/>
+ <field name="location_id" invisible="1"/>
+ <field name="location_dest_id" invisible="1"/>
+ <field name="scrapped" invisible="1"/>
+ <field name="picking_code" invisible="1"/>
+ <field name="product_type" invisible="1"/>
+ <field name="show_details_visible" invisible="1"/>
+ <field name="show_reserved_availability" invisible="1"/>
+ <field name="show_operations" invisible="1" readonly="1"/>
+ <field name="additional" invisible="1"/>
+ <field name="has_move_lines" invisible="1"/>
+ <field name="is_locked" invisible="1"/>
+ <field name="product_id" required="1" attrs="{'readonly': ['|', '&amp;', ('state', '!=', 'draft'), ('additional', '=', False), ('has_move_lines', '=', True)]}"/>
+ <field name="is_initial_demand_editable" invisible="1"/>
+ <field name="is_quantity_done_editable" invisible="1"/>
+ <field name="product_uom_qty" string="Initial Demand" attrs="{'readonly': [('is_initial_demand_editable', '=', False)]}"/>
+ <field name="reserved_availability" string="Reserved"/>
+ <field name="quantity_done" string="Done" attrs="{'readonly': [('is_quantity_done_editable', '=', False)]}"/>
+ <field name="product_uom" attrs="{'readonly': [('state', '!=', 'draft'), ('additional', '=', False)]}" options="{'no_open': True, 'no_create': True}" string="Unit of Measure" groups="uom.group_uom"/>
+ </tree>
+ </field>
+ </record>
+
<record id="view_move_kandan" model="ir.ui.view">
<field name="name">stock.move.kanban</field>
<field name="model">stock.move</field>
diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml
index 171d4c1a842..01c289b95d0 100644
--- a/addons/stock/views/stock_picking_views.xml
+++ b/addons/stock/views/stock_picking_views.xml
@@ -243,6 +243,19 @@
class="oe_stat_button" icon="fa-cubes"
attrs="{'invisible': [('has_packages', '=', False)]}"/>
<button name="%(action_stock_report)d" icon="fa-arrow-up" class="oe_stat_button" string="Traceability Report" type="action" attrs="{'invisible': ['|', ('state', '!=', 'done'), ('has_tracking', '=', False)]}" groups="stock.group_production_lot"/>
+ <!-- Use the following button to avoid onchange on one2many -->
+ <button name="action_picking_move_tree"
+ class="oe_stat_button"
+ icon="fa-arrows-v"
+ type="object"
+ help="List view of operations"
+ groups="base.group_no_one"
+ attrs="{'invisible': ['|', '&amp;', ('show_operations', '=', True), '|', ('is_locked', '=', True), ('state', '=', 'done'), '&amp;', ('state', '=', 'done'), ('is_locked', '=', True)]}"
+ context="{'picking_type_code': picking_type_code, 'default_picking_id': id, 'form_view_ref':'stock.view_move_picking_form', 'address_in_id': partner_id, 'default_picking_type_id': picking_type_id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id}">
+ <div class="o_form_field o_stat_info">
+ <span class="o_stat_text">Operations</span>
+ </div>
+ </button>
</div>
<h1 class="d-none d-md-block">
<field name="name" attrs="{'invisible': [('name','=','/')]}" readonly="1"/>
diff --git a/addons/web/static/src/js/tools/debug_manager.js b/addons/web/static/src/js/tools/debug_manager.js
index f09d8745f04..7c5fd390871 100644
--- a/addons/web/static/src/js/tools/debug_manager.js
+++ b/addons/web/static/src/js/tools/debug_manager.js
@@ -433,6 +433,7 @@ DebugManager.include({
var fields = state.fields;
var fieldsInfo = state.fieldsInfo.form;
var fieldNamesInView = state.getFieldNames();
+ var fieldNamesOnlyOnView = ['message_attachment_count'];
var fieldsValues = state.data;
var modifierDatas = {};
_.each(fieldNamesInView, function (fieldName) {
@@ -441,6 +442,7 @@ DebugManager.include({
});
});
this.fields = _.chain(fieldNamesInView)
+ .difference(fieldNamesOnlyOnView)
.map(function (fieldName) {
var modifierData = modifierDatas[fieldName];
var invisibleOrReadOnly;
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 101588dcc93..64f0ebe3ce2 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 6d37751dc0a..79358530829 100644
--- a/addons/web/static/tests/views/calendar_tests.js
+++ b/addons/web/static/tests/views/calendar_tests.js
@@ -2334,6 +2334,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:
+ '<calendar date_start="start_date" mode="month">'+
+ '<field name="name"/>'+
+ '<field name="start_date"/>'+
+ '</calendar>',
+ 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/web_editor/static/src/js/wysiwyg_snippets/snippets.editor.js b/addons/web_editor/static/src/js/wysiwyg_snippets/snippets.editor.js
index b8c6057bb0e..7240d2db40d 100644
--- a/addons/web_editor/static/src/js/wysiwyg_snippets/snippets.editor.js
+++ b/addons/web_editor/static/src/js/wysiwyg_snippets/snippets.editor.js
@@ -920,7 +920,7 @@ var SnippetsMenu = Widget.extend({
&& (float_next === 'left' || float_next === 'right')) {
zone.remove();
} else if (disp_prev !== null && disp_next !== null
- && disp_prev !== 'block' && disp_next !== 'block') {
+ && disp_prev.indexOf('inline') >= 0 && disp_next.indexOf('inline') >= 0) {
zone.remove();
}
});
diff --git a/addons/web_unsplash/static/src/js/unsplash_image_widget.js b/addons/web_unsplash/static/src/js/unsplash_image_widget.js
index 61a698ed190..fe1b966e382 100644
--- a/addons/web_unsplash/static/src/js/unsplash_image_widget.js
+++ b/addons/web_unsplash/static/src/js/unsplash_image_widget.js
@@ -227,7 +227,8 @@ widgetsMedia.ImageWidget.include({
params: params,
}).then(function () {
self.unsplashAPI.clientId = key;
- self._renderImages();
+ self._unsplash.error = false;
+ self.search(self._unsplash.query);
});
}
}
diff --git a/addons/website/static/src/js/content/snippets.animation.js b/addons/website/static/src/js/content/snippets.animation.js
index 2c322012d1c..7c79b2382ca 100644
--- a/addons/website/static/src/js/content/snippets.animation.js
+++ b/addons/website/static/src/js/content/snippets.animation.js
@@ -1051,7 +1051,7 @@ registry.facebookPage = Animation.extend({
if (!params.href) {
return def;
}
- params.width = utils.confine(this.$el.width(), 180, 500);
+ params.width = utils.confine(Math.floor(this.$el.width()), 180, 500);
var src = $.param.querystring('https://www.facebook.com/plugins/page.php', params);
this.$iframe = $('<iframe/>', {
diff --git a/addons/website_blog/static/src/scss/primary_variables.scss b/addons/website_blog/static/src/scss/primary_variables.scss
index f5cda7678dc..c8b936b59f1 100644
--- a/addons/website_blog/static/src/scss/primary_variables.scss
+++ b/addons/website_blog/static/src/scss/primary_variables.scss
@@ -1,5 +1,3 @@
-$s-latest-posts-big-picture-figure-bg: null;
-
@mixin s-latest-posts-js-get-posts-hook {};
@mixin s-latest-posts-figure-hook {};
diff --git a/addons/website_blog/static/src/scss/website_blog.scss b/addons/website_blog/static/src/scss/website_blog.scss
index f16afd40bf2..f54dc52b832 100644
--- a/addons/website_blog/static/src/scss/website_blog.scss
+++ b/addons/website_blog/static/src/scss/website_blog.scss
@@ -262,13 +262,10 @@
}
}
-$s-latest-posts-big-picture-figure-bg: $white !default;
-
.s_latest_posts_big_picture {
.s_latest_posts_post {
position: relative;
height: 250px;
- background: $s-latest-posts-big-picture-figure-bg;
text-align: center;
cursor: pointer;
transition: all 300ms;
@@ -281,9 +278,11 @@ $s-latest-posts-big-picture-figure-bg: $white !default;
position: relative;
display: block;
width: 100%;
+ max-width: 100% !important;
height: 100%;
padding: 0;
opacity: 0.8;
+ overflow: hidden;
> .o_blog_cover_component {
@include o-position-absolute(0, 0, 0, 0);
@@ -293,15 +292,17 @@ $s-latest-posts-big-picture-figure-bg: $white !default;
background-position: center;
}
}
- > div, > div > a {
- @include o-position-absolute(0, 0, 0, 0);
- padding: 2em;
- color: $white;
- text-transform: uppercase;
- font-size: 1.25em;
- backface-visibility: hidden;
- &::after, &::before {
- pointer-events: none;
+ > div:not(.o_blog_cover_container):not(.js-loading) {
+ &, & > a {
+ @include o-position-absolute(0, $grid-gutter-width/2, 0, $grid-gutter-width/2);
+ padding: 2em;
+ color: $white;
+ text-transform: uppercase;
+ font-size: 1.25em;
+ backface-visibility: hidden;
+ &::after, &::before {
+ pointer-events: none;
+ }
}
> a {
@@ -328,7 +329,9 @@ $s-latest-posts-big-picture-figure-bg: $white !default;
@include s-latest-posts-figure-hook;
}
- @include s-latest-posts-js-get-posts-hook;
+ .js_get_posts {
+ @include s-latest-posts-js-get-posts-hook;
+ }
}
.s_latest_posts, .s_latest_posts_big_picture {
diff --git a/addons/website_blog/views/website_blog_templates.xml b/addons/website_blog/views/website_blog_templates.xml
index 817387db989..6623de92ebc 100644
--- a/addons/website_blog/views/website_blog_templates.xml
+++ b/addons/website_blog/views/website_blog_templates.xml
@@ -589,10 +589,10 @@
</template>
<template id="s_latest_posts" name="Latest Posts - List">
- <section class="s_latest_posts">
+ <section class="s_latest_posts pt16 pb16">
<div class="container">
<h2 class="text-center">Latest Posts</h2>
- <div class="row js_get_posts"
+ <div class="row s_col_no_bgcolor js_get_posts"
data-loading="true"
data-template="website_blog.s_latest_posts_list_template"/>
</div>
@@ -600,10 +600,10 @@
</template>
<template id="s_latest_posts_big_picture" name="Latest Posts - Big Images">
- <section class="s_latest_posts_big_picture">
+ <section class="s_latest_posts_big_picture pt16 pb16">
<div class="container-fluid">
<h2 class="text-center">Latest Posts</h2>
- <div class="row js_get_posts"
+ <div class="row s_col_no_bgcolor js_get_posts"
data-loading="true"
data-template="website_blog.s_latest_posts_big_picture_template"/>
</div>
diff --git a/addons/website_forum/models/forum.py b/addons/website_forum/models/forum.py
index 03b45d6bf17..3c29e8cf9fe 100644
--- a/addons/website_forum/models/forum.py
+++ b/addons/website_forum/models/forum.py
@@ -11,6 +11,7 @@ from odoo.addons.gamification.models.gamification_karma_rank import KarmaError
from odoo import api, fields, models, tools, SUPERUSER_ID, _
from odoo.exceptions import UserError, ValidationError
from odoo.tools import misc
+from odoo.tools.translate import html_translate
_logger = logging.getLogger(__name__)
@@ -28,7 +29,7 @@ class Forum(models.Model):
# description and use
name = fields.Char('Forum Name', required=True, translate=True)
active = fields.Boolean(default=True)
- faq = fields.Html('Guidelines', default=_get_default_faq, translate=True, sanitize=False)
+ faq = fields.Html('Guidelines', default=_get_default_faq, translate=html_translate, sanitize=False)
description = fields.Text(
'Description',
translate=True,
diff --git a/addons/website_hr_recruitment/models/hr_recruitment.py b/addons/website_hr_recruitment/models/hr_recruitment.py
index 8606b1826c1..8239fc1e355 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/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py
index 9d6275f1d73..ab13b403816 100644
--- a/addons/website_sale/controllers/main.py
+++ b/addons/website_sale/controllers/main.py
@@ -973,6 +973,9 @@ class WebsiteSale(ProductConfiguratorController):
if not order or (order.amount_total and not tx):
return request.redirect('/shop')
+ if order and not order.amount_total and not tx:
+ return request.redirect(order.get_portal_url())
+
# clean context and session, then redirect to the confirmation page
request.website.sale_reset()
if tx and tx.state == 'draft':
diff --git a/addons/website_sale/static/src/scss/website_sale.scss b/addons/website_sale/static/src/scss/website_sale.scss
index 3689314f28b..81e9653ba43 100644
--- a/addons/website_sale/static/src/scss/website_sale.scss
+++ b/addons/website_sale/static/src/scss/website_sale.scss
@@ -358,6 +358,10 @@
&.oe_list {
height: 200px;
+ .oe_subdescription {
+ max-height: 40px;
+ }
+
section {
background: rgba(100, 100, 100, 0.2);
}
diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml
index 3d4cd01eda4..91a57ccc35d 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -1334,7 +1334,7 @@
<div class="js_payment mt-3" t-if="not website_sale_order.amount_total" id="payment_method">
<form target="_self" action="/shop/payment/validate" method="post" class="float-right">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()" />
- <a role="button" class="btn btn-primary a-submit">
+ <a role="button" class="btn btn-primary a-submit" href="#">
<span t-if="order.amount_total &gt; 0">Pay Now <span class="fa fa-chevron-right"></span></span>
<span t-if="order.amount_total == 0">Confirm Order <span class="fa fa-chevron-right"></span></span>
</a>
diff --git a/odoo/addons/base/models/ir_qweb_fields.py b/odoo/addons/base/models/ir_qweb_fields.py
index 83d39e15891..8460325dc3a 100644
--- a/odoo/addons/base/models/ir_qweb_fields.py
+++ b/odoo/addons/base/models/ir_qweb_fields.py
@@ -479,6 +479,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_inherits/ir.model.access.csv b/odoo/addons/test_inherits/ir.model.access.csv
index 22ecf366e31..09aeec489d8 100644
--- a/odoo/addons/test_inherits/ir.model.access.csv
+++ b/odoo/addons/test_inherits/ir.model.access.csv
@@ -2,3 +2,5 @@
access_test_unit,access_test_unit,model_test_unit,,1,1,1,1
access_test_box,access_test_box,model_test_box,,1,1,1,1
access_test_pallet,access_test_pallet,model_test_pallet,,1,1,1,1
+access_test_another_unit,access_test_another_unit,model_test_another_unit,,1,1,1,1
+access_test_another_box,access_test_another_box,model_test_another_box,,1,1,1,1
diff --git a/odoo/addons/test_inherits/models.py b/odoo/addons/test_inherits/models.py
index e31ebcdfff0..efa965c2653 100644
--- a/odoo/addons/test_inherits/models.py
+++ b/odoo/addons/test_inherits/models.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api
+from odoo.exceptions import ValidationError
# We just create a new model
@@ -38,3 +39,28 @@ class Pallet(models.Model):
box_id = fields.Many2one('test.box', 'Box', required=True,
ondelete='cascade')
field_in_pallet = fields.Char('Field2')
+
+
+# Another model for another test suite
+class AnotherUnit(models.Model):
+ _name = 'test.another_unit'
+ _description = 'Another Test Unit'
+
+ val1 = fields.Integer('Value 1', required=True)
+
+
+# We want to _inherits from the parent model, add a field and check
+# the new field is always equals to the first one
+class AnotherBox(models.Model):
+ _name = 'test.another_box'
+ _inherits = {'test.another_unit': 'another_unit_id'}
+ _description = 'Another Test Box'
+
+ another_unit_id = fields.Many2one('test.another_unit', 'Another Unit',
+ required=True, ondelete='cascade')
+ val2 = fields.Integer('Value 2', required=True)
+
+ @api.constrains('val1', 'val2')
+ def _check(self):
+ if self.val1 != self.val2:
+ raise ValidationError("The two values must be equals")
diff --git a/odoo/addons/test_inherits/tests/test_inherits.py b/odoo/addons/test_inherits/tests/test_inherits.py
index 85396de5441..3328f23e2ed 100644
--- a/odoo/addons/test_inherits/tests/test_inherits.py
+++ b/odoo/addons/test_inherits/tests/test_inherits.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
from odoo.tests import common
+from odoo.exceptions import ValidationError
class test_inherits(common.TransactionCase):
@@ -37,3 +38,37 @@ class test_inherits(common.TransactionCase):
field = IrModelData.search([('name', '=', 'field_test_box__name')])
self.assertEqual(len(field), 1)
self.assertEqual(field.module, 'test_inherits')
+
+ def test_constraint_inherits(self):
+ """Validate constraints on inherits when the parent is not updated"""
+ Model = self.env['test.another_box']
+
+ with self.assertRaises(ValidationError):
+ another_box = Model.create({'val1': 1, 'val2': 2})
+ another_box = Model.create({'val1': 1, 'val2': 1})
+
+ with self.assertRaises(ValidationError):
+ another_box.write({'val2': 2})
+ another_box.write({'val1': 2, 'val2': 2})
+
+ def test_constraint_inherits_parent_change(self):
+ """Validate constraints on inherits when parent is updated too"""
+ UnitModel = self.env['test.another_unit']
+ BoxModel = self.env['test.another_box']
+
+ unit1 = UnitModel.create({'val1': 1})
+ box = BoxModel.create({'another_unit_id': unit1.id, 'val2': 1})
+
+ unit2 = UnitModel.create({'val1': 2})
+ box.write({'another_unit_id': unit2.id, 'val2': 2})
+
+ unit3 = UnitModel.create({'val1': 3})
+ box.write({'another_unit_id': unit3.id, 'val1': 4, 'val2': 4})
+
+ unit4 = UnitModel.create({'val1': 5})
+ with self.assertRaises(ValidationError):
+ box.write({'another_unit_id': unit4.id, 'val2': 6})
+
+ unit5 = UnitModel.create({'val1': 7})
+ with self.assertRaises(ValidationError):
+ box.write({'another_unit_id': unit5.id, 'val1': 8, 'val2': 7})
diff --git a/odoo/addons/test_new_api/tests/test_one2many.py b/odoo/addons/test_new_api/tests/test_one2many.py
index 8a1a9ef7850..30324f50288 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 0f729ba924b..0a80e52ead3 100644
--- a/odoo/fields.py
+++ b/odoo/fields.py
@@ -2265,8 +2265,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)):
diff --git a/odoo/models.py b/odoo/models.py
index b5f661e7c83..7ccd75a777f 100644
--- a/odoo/models.py
+++ b/odoo/models.py
@@ -2393,6 +2393,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
cr = self._cr
update_custom_fields = self._context.get('update_custom_fields', False)
must_create_table = not tools.table_exists(cr, self._table)
+ parent_path_compute = False
if self._auto:
if must_create_table:
@@ -2401,6 +2402,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
if self._parent_store:
if not tools.column_exists(cr, self._table, 'parent_path'):
self._create_parent_columns()
+ parent_path_compute = True
self._check_removed_columns(log=False)
@@ -2429,6 +2431,9 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
if must_create_table:
self._execute_sql()
+ if parent_path_compute:
+ self._parent_store_compute()
+
@api.model_cr
def init(self):
""" This method is called after :meth:`~._auto_init`, and may be
@@ -3332,14 +3337,12 @@ Fields:
self._name, ', '.join(sorted(unknown_names)))
with self.env.protecting(protected_fields, self):
- # write stored fields with (low-level) method _write
- if store_vals or inverse_vals or inherited_vals:
- # if log_access is enabled, this updates 'write_date' and
- # 'write_uid' and check access rules, even when old_vals is
- # empty
- self._write(store_vals)
+ # update references to parents
+ ref_store_vals = {k: store_vals[k] for k in self._inherits.values() if k in store_vals}
+ if ref_store_vals:
+ self._write(ref_store_vals)
- # update parent records (after possibly updating parent fields)
+ # update parent records
cr = self.env.cr
for model_name, parent_vals in inherited_vals.items():
parent_name = self._inherits[model_name]
@@ -3362,6 +3365,13 @@ Fields:
}
)
+ # write stored fields with (low-level) method _write
+ if store_vals or inverse_vals or inherited_vals:
+ # if log_access is enabled, this updates 'write_date' and
+ # 'write_uid' and check access rules, even when old_vals is
+ # empty
+ self._write(store_vals)
+
if inverse_vals:
self.check_field_access_rights('write', list(inverse_vals))