summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2018-05-23 19:33:40 +0200
committerChristophe Simonis <chs@odoo.com>2018-05-23 19:33:40 +0200
commit2bc6ea1b378bc8a660219b0502c93118f0bb1b6a (patch)
tree806acef55473ca422ca6f1d5d46041e3c5e1e590 /addons
parent2a43614c3c00d57a4376eea44cd8cd1ed2d316b7 (diff)
parent02ee3fd88eff00ecc058c060aa9b3b39b7c08e15 (diff)
[MERGE] forward port branch 11.0 up to 02ee3fd88ef
Diffstat (limited to 'addons')
-rw-r--r--addons/account/models/account_payment.py5
-rw-r--r--addons/analytic/models/analytic_account.py33
-rw-r--r--addons/calendar/models/calendar.py8
-rw-r--r--addons/l10n_co/__manifest__.py7
-rw-r--r--addons/l10n_es/__manifest__.py1
-rw-r--r--addons/l10n_es/data/account_fiscal_position_template_data.xml32
-rw-r--r--addons/l10n_es/data/account_tax_data.xml13
-rw-r--r--addons/mail/models/mail_thread.py15
-rw-r--r--addons/mail/static/src/js/activity.js2
-rw-r--r--addons/mrp/models/mrp_production.py8
-rw-r--r--addons/mrp/models/mrp_workorder.py8
-rw-r--r--addons/pos_mercury/static/src/js/pos_mercury.js1
-rw-r--r--addons/purchase/models/purchase.py1
-rw-r--r--addons/stock/data/stock_data.xml5
-rw-r--r--addons/stock/models/res_company.py10
-rw-r--r--addons/stock/models/stock_quant.py43
-rw-r--r--addons/stock/models/stock_traceability.py35
-rw-r--r--addons/stock/models/stock_warehouse.py3
-rw-r--r--addons/stock/report/report_stockpicking_operations.xml2
-rw-r--r--addons/stock/tests/test_move2.py67
-rw-r--r--addons/stock/tests/test_quant.py47
-rw-r--r--addons/web/controllers/main.py8
-rw-r--r--addons/web/static/src/js/core/ajax.js20
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js48
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js174
-rw-r--r--addons/website_form/controllers/main.py1
-rw-r--r--addons/website_sale/static/src/js/website_sale_tour_buy.js2
-rw-r--r--addons/website_sale_delivery/controllers/main.py19
-rw-r--r--addons/website_sale_delivery/models/sale_order.py3
-rw-r--r--addons/website_sale_delivery/static/src/js/website_sale_delivery.js10
-rw-r--r--addons/website_sale_delivery/views/website_sale_delivery_templates.xml2
31 files changed, 528 insertions, 105 deletions
diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py
index 1dc107794f8..d2e5a5bfc5d 100644
--- a/addons/account/models/account_payment.py
+++ b/addons/account/models/account_payment.py
@@ -428,12 +428,17 @@ class account_payment(models.Model):
@api.multi
def button_invoices(self):
+ if self.partner_type == 'supplier':
+ views = [(self.env.ref('account.invoice_supplier_tree').id, 'tree'), (self.env.ref('account.invoice_supplier_form').id, 'form')]
+ else:
+ views = [(self.env.ref('account.invoice_tree').id, 'tree'), (self.env.ref('account.invoice_form').id, 'form')]
return {
'name': _('Paid Invoices'),
'view_type': 'form',
'view_mode': 'tree,form',
'res_model': 'account.invoice',
'view_id': False,
+ 'views': views,
'type': 'ir.actions.act_window',
'domain': [('id', 'in', [x.id for x in self.invoice_ids])],
}
diff --git a/addons/analytic/models/analytic_account.py b/addons/analytic/models/analytic_account.py
index fbf7951aa4e..9b3a4a1eec0 100644
--- a/addons/analytic/models/analytic_account.py
+++ b/addons/analytic/models/analytic_account.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from collections import defaultdict
from odoo import api, fields, models, _
from odoo.osv import expression
from odoo.exceptions import ValidationError
@@ -61,9 +62,9 @@ class AccountAnalyticAccount(models.Model):
@api.multi
def _compute_debit_credit_balance(self):
- res_currency_obj = self.env['res.currency']
+ Curr = self.env['res.currency']
analytic_line_obj = self.env['account.analytic.line']
- domain = [('account_id', 'in', self.mapped('id'))]
+ domain = [('account_id', 'in', self.ids)]
if self._context.get('from_date', False):
domain.append(('date', '>=', self._context['from_date']))
if self._context.get('to_date', False):
@@ -75,17 +76,23 @@ class AccountAnalyticAccount(models.Model):
domain.append(('company_id', 'in', self._context['company_ids']))
user_currency = self.env.user.company_id.currency_id
- account_amounts = analytic_line_obj.search_read(domain, ['account_id', 'amount', 'currency_id'])
- account_ids = set([line['account_id'][0] for line in account_amounts])
- data_debit = {account_id: 0.0 for account_id in account_ids}
- data_credit = {account_id: 0.0 for account_id in account_ids}
- for account_amount in account_amounts:
- currency_id = account_amount['currency_id'][0]
- amount = res_currency_obj.browse(currency_id).compute(account_amount['amount'], user_currency)
- if amount < 0.0:
- data_debit[account_amount['account_id'][0]] += amount
- else:
- data_credit[account_amount['account_id'][0]] += amount
+ credit_groups = analytic_line_obj.read_group(
+ domain=domain + [('amount', '>=', 0.0)],
+ fields=['account_id', 'currency_id', 'amount'],
+ groupby=['account_id', 'currency_id'],
+ )
+ data_credit = defaultdict(float)
+ for l in credit_groups:
+ data_credit[l['account_id'][0]] += Curr.browse(l['currency_id'][0]).compute(l['amount'], user_currency)
+
+ debit_groups = analytic_line_obj.read_group(
+ domain=domain + [('amount', '<', 0.0)],
+ fields=['account_id', 'currency_id', 'amount'],
+ groupby=['account_id', 'currency_id'],
+ )
+ data_debit = defaultdict(float)
+ for l in debit_groups:
+ data_debit[l['account_id'][0]] += Curr.browse(l['currency_id'][0]).compute(l['amount'], user_currency)
for account in self:
account.debit = abs(data_debit.get(account.id, 0.0))
diff --git a/addons/calendar/models/calendar.py b/addons/calendar/models/calendar.py
index 3b36e1872d7..fa890e27495 100644
--- a/addons/calendar/models/calendar.py
+++ b/addons/calendar/models/calendar.py
@@ -655,7 +655,13 @@ class Meeting(models.Model):
}[data['rrule_type']]
deadline = fields.Datetime.from_string(data['stop'])
- return deadline + relativedelta(**{delay: count * mult})
+ computed_final_date = False
+ while not computed_final_date and count > 0:
+ try: # may crash if year > 9999 (in case of recurring events)
+ computed_final_date = deadline + relativedelta(**{delay: count * mult})
+ except ValueError:
+ count -= data['interval']
+ return computed_final_date or deadline
return final_date
@api.multi
diff --git a/addons/l10n_co/__manifest__.py b/addons/l10n_co/__manifest__.py
index 72a39483284..88f5fb464f9 100644
--- a/addons/l10n_co/__manifest__.py
+++ b/addons/l10n_co/__manifest__.py
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-# Copyright (C) David Arnold (devCO).
-# Author David Arnold (devCO), dar@devco.co
+# Copyright (C) David Arnold (XOE Solutions).
+# Author David Arnold (XOE Solutions), dar@xoe.solutions
# Co-Authors Juan Pablo Aries (devCO), jpa@devco.co
# Hector Ivan Valencia Muñoz (TIX SAS)
# Nhomar Hernandez (Vauxoo)
@@ -13,7 +13,8 @@
'version': '0.8',
'category': 'Localization',
'description': 'Colombian Accounting and Tax Preconfiguration',
- 'author': 'David Arnold BA HSG (devCO)',
+ 'author': 'David Arnold (XOE Solutions)',
+ 'website': 'https://xoe.solutions',
'depends': [
'account',
],
diff --git a/addons/l10n_es/__manifest__.py b/addons/l10n_es/__manifest__.py
index c5d7599074e..44b23a5e9c4 100644
--- a/addons/l10n_es/__manifest__.py
+++ b/addons/l10n_es/__manifest__.py
@@ -3,6 +3,7 @@
# List of contributors:
# Jordi Esteve <jesteve@zikzakmedia.com>
+# Ignacio Ibeas <ignacio@acysos.com>
# Dpto. Consultoría Grupo Opentia <consultoria@opentia.es>
# Pedro M. Baeza <pedro.baeza@tecnativa.com>
# Carlos Liébana <carlos.liebana@factorlibre.com>
diff --git a/addons/l10n_es/data/account_fiscal_position_template_data.xml b/addons/l10n_es/data/account_fiscal_position_template_data.xml
index 49beed9d022..eef1bd1ac03 100644
--- a/addons/l10n_es/data/account_fiscal_position_template_data.xml
+++ b/addons/l10n_es/data/account_fiscal_position_template_data.xml
@@ -113,6 +113,16 @@
<field name="chart_template_id" ref="account_chart_template_common"/>
</record>
+ <record id="fp_reagyp_a" model="account.fiscal.position.template">
+ <field name="name">REAGYP - Agricultura</field>
+ <field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
+ </record>
+
+ <record id="fp_reagyp_gp" model="account.fiscal.position.template">
+ <field name="name">REAGYP - Ganadería y pesca</field>
+ <field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
+ </record>
+
<!-- ************************************************************* -->
<!-- Fiscal Position Tax Templates -->
<!-- ************************************************************* -->
@@ -2889,6 +2899,28 @@
<field name="tax_dest_id" ref="account_tax_template_s_iva0_isp"/>
</record>
+ <!-- Régimen especial de Agricultura, Ganaderia y Pesca -->
+ <record id="fptt_reagyp_a_4b_1" model="account.fiscal.position.tax.template">
+ <field name="position_id" ref="fp_reagyp_a"/>
+ <field name="tax_src_id" ref="l10n_es.account_tax_template_p_iva4_bc"/>
+ <field name="tax_dest_id" ref="l10n_es.account_tax_template_p_iva12_agr"/>
+ </record>
+ <record id="fptt_reagyp_a_4b_2" model="account.fiscal.position.tax.template">
+ <field name="position_id" ref="fp_reagyp_a"/>
+ <field name="tax_src_id" ref="l10n_es.account_tax_template_p_iva4_bc"/>
+ <field name="tax_dest_id" ref="l10n_es.account_tax_template_p_irpf2"/>
+ </record>
+ <record id="fptt_reagyp_gp_4b_1" model="account.fiscal.position.tax.template">
+ <field name="position_id" ref="fp_reagyp_gp"/>
+ <field name="tax_src_id" ref="l10n_es.account_tax_template_p_iva4_bc"/>
+ <field name="tax_dest_id" ref="account_tax_template_p_iva105_gan"/>
+ </record>
+ <record id="fptt_reagyp_gp_4b_2" model="account.fiscal.position.tax.template">
+ <field name="position_id" ref="fp_reagyp_gp"/>
+ <field name="tax_src_id" ref="l10n_es.account_tax_template_p_iva4_bc"/>
+ <field name="tax_dest_id" ref="l10n_es.account_tax_template_p_irpf2"/>
+ </record>
+
<!-- ************************************************************* -->
<!-- Fiscal Position Account Templates -->
<!-- ************************************************************* -->
diff --git a/addons/l10n_es/data/account_tax_data.xml b/addons/l10n_es/data/account_tax_data.xml
index d07503b0b88..327144f6fe1 100644
--- a/addons/l10n_es/data/account_tax_data.xml
+++ b/addons/l10n_es/data/account_tax_data.xml
@@ -1044,6 +1044,18 @@
<field name="tax_group_id" ref="tax_group_iva_12"/>
<field name="tag_ids" eval="[(6, False, [ref('mod_303_42')])]"/>
</record>
+ <record id="account_tax_template_p_iva105_gan" model="account.tax.template">
+ <field name="description">P_IVA105_GAN</field>
+ <field name="type_tax_use">purchase</field>
+ <field name="account_id" ref="l10n_es.account_common_472"/>
+ <field name="name">10,5% IVA Soportado régimen ganadero o pesca</field>
+ <field name="refund_account_id" ref="l10n_es.account_common_472"/>
+ <field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
+ <field name="amount" eval="10.5"/>
+ <field name="amount_type">percent</field>
+ <field name="include_base_amount" eval="1"/>
+ <field name="tag_ids" eval="[(6, False, [ref('mod_303_42')])]"/>
+ </record>
<record id="account_tax_template_s_iva0_e" model="account.tax.template">
<field name="description">S_IVA0_E</field>
<field name="type_tax_use">sale</field>
@@ -1330,6 +1342,7 @@
<field name="refund_account_id" ref="l10n_es.account_common_4751"/>
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
<field name="amount" eval="-2"/>
+ <field name="sequence" eval="2"/>
<field name="amount_type">percent</field>
<field name="tax_group_id" ref="tax_group_retenciones_2"/>
<field name="tag_ids" eval="[(6, False, [ref('mod_111_08'), ref('mod_111_09')])]"/>
diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py
index c3936238f2f..71dce759f8e 100644
--- a/addons/mail/models/mail_thread.py
+++ b/addons/mail/models/mail_thread.py
@@ -124,7 +124,8 @@ class MailThread(models.AbstractModel):
followers = self.env['mail.followers'].sudo().search([
('res_model', '=', self._name),
('partner_id', operator, operand)])
- return [('id', 'in', followers.mapped('res_id'))]
+ # using read() below is much faster than followers.mapped('res_id')
+ return [('id', 'in', [res['res_id'] for res in followers.read(['res_id'])])]
@api.model
def _search_follower_channels(self, operator, operand):
@@ -137,7 +138,8 @@ class MailThread(models.AbstractModel):
followers = self.env['mail.followers'].sudo().search([
('res_model', '=', self._name),
('channel_id', operator, operand)])
- return [('id', 'in', followers.mapped('res_id'))]
+ # using read() below is much faster than followers.mapped('res_id')
+ return [('id', 'in', [res['res_id'] for res in followers.read(['res_id'])])]
@api.multi
@api.depends('message_follower_ids')
@@ -147,7 +149,8 @@ class MailThread(models.AbstractModel):
('res_id', 'in', self.ids),
('partner_id', '=', self.env.user.partner_id.id),
])
- following_ids = followers.mapped('res_id')
+ # using read() below is much faster than followers.mapped('res_id')
+ following_ids = [res['res_id'] for res in followers.read(['res_id'])]
for record in self:
record.message_is_follower = record.id in following_ids
@@ -159,9 +162,11 @@ class MailThread(models.AbstractModel):
])
# Cases ('message_is_follower', '=', True) or ('message_is_follower', '!=', False)
if (operator == '=' and operand) or (operator == '!=' and not operand):
- return [('id', 'in', followers.mapped('res_id'))]
+ # using read() below is much faster than followers.mapped('res_id')
+ return [('id', 'in', [res['res_id'] for res in followers.read(['res_id'])])]
else:
- return [('id', 'not in', followers.mapped('res_id'))]
+ # using read() below is much faster than followers.mapped('res_id')
+ return [('id', 'not in', [res['res_id'] for res in followers.read(['res_id'])])]
@api.multi
def _get_message_unread(self):
diff --git a/addons/mail/static/src/js/activity.js b/addons/mail/static/src/js/activity.js
index 4bb81489789..fce7eb29f6f 100644
--- a/addons/mail/static/src/js/activity.js
+++ b/addons/mail/static/src/js/activity.js
@@ -32,6 +32,7 @@ function _readActivities(self, ids) {
model: 'mail.activity',
method: 'read',
args: [ids],
+ context: (self.record && self.record.getContext()) || self.getSession().user_context,
}).then(function (activities) {
// convert create_date and date_deadline to moments
_.each(activities, function (activity) {
@@ -108,6 +109,7 @@ var AbstractActivityField = AbstractField.extend({
method: 'action_feedback',
args: [[id]],
kwargs: {feedback: feedback},
+ context: this.record.getContext(),
});
},
_scheduleActivity: function (id, previous_activity_type_id, callback) {
diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py
index 8b39d73d3ae..80e2cfe56e4 100644
--- a/addons/mrp/models/mrp_production.py
+++ b/addons/mrp/models/mrp_production.py
@@ -474,10 +474,16 @@ class MrpProduction(models.Model):
@api.multi
def _generate_workorders(self, exploded_boms):
workorders = self.env['mrp.workorder']
+ original_one = False
for bom, bom_data in exploded_boms:
# If the routing of the parent BoM and phantom BoM are the same, don't recreate work orders, but use one master routing
if bom.routing_id.id and (not bom_data['parent_line'] or bom_data['parent_line'].bom_id.routing_id.id != bom.routing_id.id):
- workorders += self._workorders_create(bom, bom_data)
+ temp_workorders = self._workorders_create(bom, bom_data)
+ workorders += temp_workorders
+ if temp_workorders: # In order to avoid two "ending work orders"
+ if original_one:
+ temp_workorders[-1].next_work_order_id = original_one
+ original_one = temp_workorders[0]
return workorders
def _workorders_create(self, bom, bom_data):
diff --git a/addons/mrp/models/mrp_workorder.py b/addons/mrp/models/mrp_workorder.py
index eef3b9a7de9..945b2b7436b 100644
--- a/addons/mrp/models/mrp_workorder.py
+++ b/addons/mrp/models/mrp_workorder.py
@@ -225,6 +225,7 @@ class MrpWorkorder(models.Model):
'done_wo': False,
'location_id': move.location_id.id,
'location_dest_id': move.location_dest_id.id,
+ 'date': move.date,
})
qty_todo -= 1
elif float_compare(qty_todo, 0.0, precision_rounding=rounding) < 0:
@@ -416,7 +417,12 @@ class MrpWorkorder(models.Model):
@api.multi
def button_start(self):
- # TDE CLEANME
+ self.ensure_one()
+ # As button_start is automatically called in the new view
+ if self.state in ('done', 'cancel'):
+ return True
+
+ # Need a loss in case of the real time exceeding the expected
timeline = self.env['mrp.workcenter.productivity']
if self.duration < self.duration_expected:
loss_id = self.env['mrp.workcenter.productivity.loss'].search([('loss_type','=','productive')], limit=1)
diff --git a/addons/pos_mercury/static/src/js/pos_mercury.js b/addons/pos_mercury/static/src/js/pos_mercury.js
index e403b9bcf12..7f896e5661f 100644
--- a/addons/pos_mercury/static/src/js/pos_mercury.js
+++ b/addons/pos_mercury/static/src/js/pos_mercury.js
@@ -449,6 +449,7 @@ PaymentScreenWidget.include({
order.selected_paymentline.paid = true;
order.selected_paymentline.mercury_swipe_pending = false;
order.selected_paymentline.mercury_amount = response.authorize;
+ order.selected_paymentline.set_amount(response.authorize);
order.selected_paymentline.mercury_card_number = decodedMagtek['number'];
order.selected_paymentline.mercury_card_brand = response.card_type;
order.selected_paymentline.mercury_card_owner_name = decodedMagtek['name'];
diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py
index 1d3db0a3d97..35ff379f620 100644
--- a/addons/purchase/models/purchase.py
+++ b/addons/purchase/models/purchase.py
@@ -347,6 +347,7 @@ class PurchaseOrder(models.Model):
@api.multi
def print_quotation(self):
+ self.write({'state': "sent"})
return self.env.ref('purchase.report_purchase_quotation').report_action(self)
@api.multi
diff --git a/addons/stock/data/stock_data.xml b/addons/stock/data/stock_data.xml
index bc37a9bb572..53e4639be74 100644
--- a/addons/stock/data/stock_data.xml
+++ b/addons/stock/data/stock_data.xml
@@ -142,11 +142,6 @@
<field name="number_increment">1</field>
</record>
- <record id="base.main_partner" model="res.partner">
- <field name="property_stock_customer" model="stock.warehouse"
- eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
- </record>
-
<!-- create xml ids for demo data that are widely used in tests or in other codes, for more convenience -->
<function model="ir.model.data" name="_update">
<value>stock.location</value>
diff --git a/addons/stock/models/res_company.py b/addons/stock/models/res_company.py
index a310991cc17..58c61be714f 100644
--- a/addons/stock/models/res_company.py
+++ b/addons/stock/models/res_company.py
@@ -28,12 +28,18 @@ class Company(models.Model):
location.sudo().write({'company_id': company.id})
company.write({'internal_transit_location_id': location.id})
+ warehouses = self.env['stock.warehouse'].search([('partner_id', '=', self.partner_id.id)])
+ warehouses.mapped('partner_id').with_context(force_company=self.id).write({
+ 'property_stock_customer': location.id,
+ 'property_stock_supplier': location.id,
+ })
+
@api.model
def create(self, vals):
company = super(Company, self).create(vals)
- # multi-company rules prevents creating warehouse and sub-locations
+ company.create_transit_location()
+ # mutli-company rules prevents creating warehouse and sub-locations
self.env['stock.warehouse'].check_access_rights('create')
self.env['stock.warehouse'].sudo().create({'name': company.name, 'code': company.name[:5], 'company_id': company.id, 'partner_id': company.partner_id.id})
- company.create_transit_location()
return company
diff --git a/addons/stock/models/stock_quant.py b/addons/stock/models/stock_quant.py
index c1765c3f083..0b73b7815e6 100644
--- a/addons/stock/models/stock_quant.py
+++ b/addons/stock/models/stock_quant.py
@@ -79,12 +79,6 @@ class StockQuant(models.Model):
if float_compare(quant.quantity, 1, precision_rounding=quant.product_uom_id.rounding) > 0 and quant.lot_id and quant.product_id.tracking == 'serial':
raise ValidationError(_('A serial number should only be linked to a single product.'))
- @api.constrains('in_date', 'lot_id')
- def check_in_date(self):
- for quant in self:
- if quant.in_date and not quant.lot_id:
- raise ValidationError(_('An incoming date cannot be set to an untracked product.'))
-
@api.constrains('location_id')
def check_location_id(self):
for quant in self:
@@ -109,9 +103,9 @@ class StockQuant(models.Model):
@api.model
def _get_removal_strategy_order(self, removal_strategy):
if removal_strategy == 'fifo':
- return 'in_date, id'
+ return 'in_date ASC NULLS FIRST, id'
elif removal_strategy == 'lifo':
- return 'in_date desc, id desc'
+ return 'in_date DESC NULLS LAST, id desc'
raise UserError(_('Removal strategy %s not implemented.') % (removal_strategy,))
def _gather(self, product_id, location_id, lot_id=None, package_id=None, owner_id=None, strict=False):
@@ -134,7 +128,17 @@ class StockQuant(models.Model):
domain = expression.AND([[('owner_id', '=', owner_id and owner_id.id or False)], domain])
domain = expression.AND([[('location_id', '=', location_id.id)], domain])
- return self.search(domain, order=removal_strategy_order)
+ # Copy code of _search for special NULLS FIRST/LAST order
+ self.sudo(self._uid).check_access_rights('read')
+ query = self._where_calc(domain)
+ self._apply_ir_rules(query, 'read')
+ from_clause, where_clause, where_clause_params = query.get_sql()
+ where_str = where_clause and (" WHERE %s" % where_clause) or ''
+ query_str = 'SELECT "%s".id FROM ' % self._table + from_clause + where_str + " ORDER BY "+ removal_strategy_order
+ self._cr.execute(query_str, where_clause_params)
+ res = self._cr.fetchall()
+ # No uniquify list necessary as auto_join is not applied anyways...
+ return self.browse([x[0] for x in res])
@api.model
def _get_available_quantity(self, product_id, location_id, lot_id=None, package_id=None, owner_id=None, strict=False, allow_negative=False):
@@ -196,17 +200,16 @@ class StockQuant(models.Model):
quants = self._gather(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=True)
rounding = product_id.uom_id.rounding
- if lot_id:
- incoming_dates = quants.mapped('in_date') # `mapped` already filtered out falsy items
- incoming_dates = [fields.Datetime.from_string(incoming_date) for incoming_date in incoming_dates]
- if in_date:
- incoming_dates += [in_date]
- # If multiple incoming dates are available for a given lot_id/package_id/owner_id, we
- # consider only the oldest one as being relevant.
- if incoming_dates:
- in_date = fields.Datetime.to_string(min(incoming_dates))
- else:
- in_date = fields.Datetime.now()
+ incoming_dates = [d for d in quants.mapped('in_date') if d]
+ incoming_dates = [fields.Datetime.from_string(incoming_date) for incoming_date in incoming_dates]
+ if in_date:
+ incoming_dates += [in_date]
+ # If multiple incoming dates are available for a given lot_id/package_id/owner_id, we
+ # consider only the oldest one as being relevant.
+ if incoming_dates:
+ in_date = fields.Datetime.to_string(min(incoming_dates))
+ else:
+ in_date = fields.Datetime.now()
for quant in quants:
try:
diff --git a/addons/stock/models/stock_traceability.py b/addons/stock/models/stock_traceability.py
index 633fb1b785e..7869c732d3c 100644
--- a/addons/stock/models/stock_traceability.py
+++ b/addons/stock/models/stock_traceability.py
@@ -21,24 +21,31 @@ class MrpStockReport(models.TransientModel):
@api.model
def _get_move_lines(self, move_lines):
- res = self.env['stock.move.line']
- for move_line in move_lines:
+ lines_seen = move_lines
+ lines_todo = list(move_lines)
+ while lines_todo:
+ move_line = lines_todo.pop(0)
# if MTO
if move_line.move_id.move_orig_ids:
- res |= move_line.move_id.move_orig_ids.mapped('move_line_ids').filtered(
- lambda m: m.lot_id.id == move_line.lot_id.id)
+ lines = move_line.move_id.move_orig_ids.mapped('move_line_ids').filtered(
+ lambda m: m.lot_id == move_line.lot_id
+ ) - lines_seen
# if MTS
+ elif move_line:
+ lines = self.env['stock.move.line'].search([
+ ('product_id', '=', move_line.product_id.id),
+ ('lot_id', '=', move_line.lot_id.id),
+ ('id', 'not in', lines_seen.ids),
+ ('date', '<', move_line.date),
+ ])
else:
- if move_line:
- res |= self.env['stock.move.line'].search([
- ('product_id', '=', move_line.product_id.id),
- ('lot_id', '=', move_line.lot_id.id),
- ('id', '!=', move_line.id),
- ('date', '<', move_line.date),
- ])
- if res:
- res |= self._get_move_lines(res)
- return res
+ continue
+ lines_todo += list(lines)
+ lines_seen |= lines
+ return lines_seen - move_lines
+ lines_todo += list(lines)
+ lines_seen |= lines
+ return lines_seen - move_lines
@api.model
def get_lines(self, line_id=None, **kw):
diff --git a/addons/stock/models/stock_warehouse.py b/addons/stock/models/stock_warehouse.py
index dd8d2a09d07..661628b2132 100644
--- a/addons/stock/models/stock_warehouse.py
+++ b/addons/stock/models/stock_warehouse.py
@@ -178,9 +178,10 @@ class Warehouse(models.Model):
ResCompany = self.env['res.company']
if company_id:
transit_loc = ResCompany.browse(company_id).internal_transit_location_id.id
+ self.env['res.partner'].browse(partner_id).with_context(force_company=company_id).write({'property_stock_customer': transit_loc, 'property_stock_supplier': transit_loc})
else:
transit_loc = ResCompany._company_default_get('stock.warehouse').internal_transit_location_id.id
- self.env['res.partner'].browse(partner_id).write({'property_stock_customer': transit_loc, 'property_stock_supplier': transit_loc})
+ self.env['res.partner'].browse(partner_id).write({'property_stock_customer': transit_loc, 'property_stock_supplier': transit_loc})
def create_sequences_and_picking_types(self):
IrSequenceSudo = self.env['ir.sequence'].sudo()
diff --git a/addons/stock/report/report_stockpicking_operations.xml b/addons/stock/report/report_stockpicking_operations.xml
index cbb472a946f..836e0d0e843 100644
--- a/addons/stock/report/report_stockpicking_operations.xml
+++ b/addons/stock/report/report_stockpicking_operations.xml
@@ -98,7 +98,7 @@
<span t-field="move.product_id.description_picking"/>
</td>
<td>
- <span t-if="move.product_qty" t-esc="move.product_qty"/>
+ <span t-if="move.product_qty" t-field="move.product_qty"/>
<span t-if="not move.product_qty" t-esc="move.product_uom._compute_quantity(move.quantity_done, move.product_id.uom_id, rounding_method='HALF-UP')"/>
<span t-field="move.product_id.uom_id" groups="product.group_uom"/>
</td>
diff --git a/addons/stock/tests/test_move2.py b/addons/stock/tests/test_move2.py
index 4e22335cad2..dc385165b61 100644
--- a/addons/stock/tests/test_move2.py
+++ b/addons/stock/tests/test_move2.py
@@ -185,6 +185,73 @@ class TestPickShip(TestStockCommon):
# the client picking should not be assigned anymore, as we returned partially what we took
self.assertEqual(picking_client.state, 'confirmed')
+ def test_mto_moves_return_return(self):
+ picking_pick, picking_client = self.create_pick_ship()
+ stock_location = self.env['stock.location'].browse(self.stock_location)
+ lot = self.env['stock.production.lot'].create({
+ 'product_id': self.productA.id,
+ 'name': '123456789'
+ })
+ self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 10.0, lot_id=lot)
+
+ picking_pick.action_assign()
+ picking_pick.move_lines[0].move_line_ids[0].qty_done = 10.0
+ picking_pick.button_validate()
+ self.assertEqual(picking_pick.state, 'done')
+ self.assertEqual(picking_client.state, 'assigned')
+
+ # return this picking
+ stock_return_picking = self.env['stock.return.picking']\
+ .with_context(active_ids=picking_pick.ids, active_id=picking_pick.ids[0])\
+ .create({})
+ stock_return_picking.product_return_moves.quantity = 10.0
+ stock_return_picking_action = stock_return_picking.create_returns()
+ return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
+ return_pick.move_lines[0].move_line_ids[0].write({
+ 'qty_done': 10.0,
+ 'lot_id': lot.id,
+ })
+ return_pick.button_validate()
+ # return this return of this picking
+ stock_return_picking = self.env['stock.return.picking']\
+ .with_context(active_id=return_pick.id)\
+ .create({})
+ stock_return_picking.product_return_moves.quantity = 10.0
+ stock_return_picking_action = stock_return_picking.create_returns()
+ return_return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
+ return_return_pick.move_lines[0].move_line_ids[0].write({
+ 'qty_done': 10.0,
+ 'lot_id': lot.id,
+ })
+ return_return_pick.button_validate()
+ # test computation of traceability
+ vals = {
+ 'line_id': 1,
+ 'model_name': 'stock.move.line',
+ 'level': 11,
+ 'parent_quant': False,
+ }
+ lines = self.env['stock.traceability.report'].get_lines(
+ model_id=return_return_pick.move_line_ids[0].id,
+ stream='upstream',
+ **vals
+ )
+ self.assertEqual(
+ [l.get('res_id') for l in lines],
+ [return_return_pick.id, return_pick.id, picking_pick.id],
+ "Upstream computation from return of return worked"
+ )
+ lines = self.env['stock.traceability.report'].get_lines(
+ model_id=picking_pick.move_line_ids[0].id,
+ stream='downstream',
+ **vals
+ )
+ self.assertEqual(
+ [l.get('res_id') for l in lines],
+ [picking_pick.id, return_pick.id, return_return_pick.id],
+ "Downstream computation from original picking worked"
+ )
+
def test_mto_resupply_cancel_ship(self):
""" This test simulates a pick pack ship with a resupply route
set. Pick and pack are validated, ship is cancelled. This test
diff --git a/addons/stock/tests/test_quant.py b/addons/stock/tests/test_quant.py
index bff1f902ba5..4a41b7474a4 100644
--- a/addons/stock/tests/test_quant.py
+++ b/addons/stock/tests/test_quant.py
@@ -629,7 +629,24 @@ class StockQuant(TransactionCase):
})
quantity, in_date = self.env['stock.quant']._update_available_quantity(product1, stock_location, 1.0)
self.assertEqual(quantity, 1)
- self.assertEqual(in_date, None)
+ self.assertNotEqual(in_date, None)
+
+
+ def test_in_date_1b(self):
+ stock_location = self.env.ref('stock.stock_location_stock')
+ product1 = self.env['product.product'].create({
+ 'name': 'Product A',
+ 'type': 'product',
+ })
+ self.env['stock.quant'].create({
+ 'product_id': product1.id,
+ 'location_id': stock_location.id,
+ 'quantity': 1.0,
+ })
+ quantity, in_date = self.env['stock.quant']._update_available_quantity(product1, stock_location, 2.0)
+ self.assertEqual(quantity, 3)
+ self.assertNotEqual(in_date, None)
+
def test_in_date_2(self):
""" Check that an incoming date is correctly set when updating the quantity of a tracked
@@ -707,6 +724,34 @@ class StockQuant(TransactionCase):
# Removal strategy is LIFO, so lot1 should be received as it was received later.
self.assertEqual(quants[0][0].lot_id.id, lot1.id)
+ def test_in_date_4b(self):
+ """ Check for LIFO and max with/without in_date that it handles the LIFO NULLS LAST well
+ """
+ stock_location = self.env.ref('stock.stock_location_stock')
+ stock_location1 = self.env.ref('stock.stock_location_components')
+ stock_location2 = self.env.ref('stock.stock_location_14')
+ lifo_strategy = self.env['product.removal'].search([('method', '=', 'lifo')])
+ stock_location.removal_strategy_id = lifo_strategy
+ product1 = self.env['product.product'].create({
+ 'name': 'Product A',
+ 'type': 'product',
+ 'tracking': 'serial',
+ })
+
+ self.env['stock.quant'].create({
+ 'product_id': product1.id,
+ 'location_id': stock_location1.id,
+ 'quantity': 1.0,
+ })
+
+ in_date_location2 = datetime.now()
+ self.env['stock.quant']._update_available_quantity(product1, stock_location2, 1.0, in_date=in_date_location2)
+
+ quants = self.env['stock.quant']._update_reserved_quantity(product1, stock_location, 1)
+
+ # Removal strategy is LIFO, so the one with date is the most recent one and should be selected
+ self.assertEqual(quants[0][0].location_id.id, stock_location2.id)
+
def test_in_date_5(self):
""" Receive the same lot at different times, once they're in the same location, the quants
are merged and only the earliest incoming date is kept.
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index 77d7304df12..f05a3855ab8 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -1047,8 +1047,12 @@ class Binary(http.Controller):
elif status != 200 and download:
return request.not_found()
- height = int(height or 0)
- width = int(width or 0)
+ if headers and dict(headers).get('Content-Type', '') == 'image/svg+xml': # we shan't resize svg images
+ height = 0
+ width = 0
+ else:
+ height = int(height or 0)
+ width = int(width or 0)
if crop and (width or height):
content = crop_image(content, type='center', size=(width, height), ratio=(1, 1))
diff --git a/addons/web/static/src/js/core/ajax.js b/addons/web/static/src/js/core/ajax.js
index 2912b319df2..d6c67eacaec 100644
--- a/addons/web/static/src/js/core/ajax.js
+++ b/addons/web/static/src/js/core/ajax.js
@@ -260,12 +260,20 @@ function get_file(options) {
if (options.error) {
var body = this.contentDocument.body;
var nodes = body.children.length === 0 ? body.childNodes : body.children;
- options.error({
- message: nodes.length > 1 ? nodes[1].textContent : '',
- data: {
- title: nodes.length > 0 ? nodes[0].textContent : '',
- },
- });
+ var errorParams = {};
+
+ try { // Case of a serialized Odoo Exception: It is Json Parsable
+ var node = nodes[1] || nodes[0];
+ errorParams = JSON.parse(node.textContent);
+ } catch (e) { // Arbitrary uncaught python side exception
+ errorParams = {
+ message: nodes.length > 1 ? nodes[1].textContent : '',
+ data: {
+ title: nodes.length > 0 ? nodes[0].textContent : '',
+ }
+ }
+ }
+ options.error(errorParams);
}
} finally {
complete();
diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js
index 8b266d8ffef..e4d2248d7c3 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -1542,8 +1542,10 @@ var BasicModel = AbstractModel.extend({
rec = self._makeDataPoint(params);
list._cache[rec.res_id] = rec.id;
}
-
- rec._noAbandon = true;
+ // Do not abandon the record if it has been created
+ // from `default_get`. The list has a savepoint only
+ // after having fully executed `default_get`.
+ rec._noAbandon = !list._savePoint;
list._changes.push({operation: 'ADD', id: rec.id});
if (command[0] === 1) {
list._changes.push({operation: 'UPDATE', id: rec.id});
@@ -1962,6 +1964,9 @@ var BasicModel = AbstractModel.extend({
var records = [];
var ids = [];
list = this._applyX2ManyOperations(list);
+ if (_.isEmpty(list.data)) {
+ return $.when();
+ }
_.each(list.data, function (localId) {
var record = self.localData[localId];
var data = record._changes || record.data;
@@ -1975,13 +1980,16 @@ var BasicModel = AbstractModel.extend({
return this._rpc({
model: model,
method: 'name_get',
- args: [ids],
+ args: [_.uniq(ids)],
context: list.context,
})
.then(function (name_gets) {
- for (var i = 0; i < name_gets.length; i++) {
- records[i].data.display_name = name_gets[i][1];
- }
+ _.each(records, function (record) {
+ var nameGet = _.find(name_gets, function (nameGet) {
+ return nameGet[0] === record.data.id;
+ });
+ record.data.display_name = nameGet[1];
+ });
});
},
/**
@@ -2467,7 +2475,16 @@ var BasicModel = AbstractModel.extend({
var self = this;
var def;
if (list.static) {
- def = this._readUngroupedList(list);
+ def = this._readUngroupedList(list).then(function () {
+ if (list.parentID && self.isNew(list.parentID)) {
+ // list from a default_get, so fetch display_name for many2one fields
+ var many2ones = self._getMany2OneFieldNames(list);
+ var defs = _.map(many2ones, function (name) {
+ return self._fetchNameGets(list, name);
+ });
+ return $.when.apply($, defs);
+ }
+ });
} else {
def = this._searchReadUngroupedList(list);
}
@@ -3036,6 +3053,23 @@ var BasicModel = AbstractModel.extend({
return Object.keys(fieldsInfo && fieldsInfo[viewType] || {});
},
/**
+ * Get many2one fields names in a datapoint. This is useful in order to
+ * fetch their names in the case of a default_get.
+ *
+ * @private
+ * @param {Object} datapoint a valid resource object
+ * @returns {string[]} list of field names that are many2one
+ */
+ _getMany2OneFieldNames: function (datapoint) {
+ var many2ones = [];
+ _.each(datapoint.fields, function (field, name) {
+ if (field.type === 'many2one') {
+ many2ones.push(name);
+ }
+ });
+ return many2ones;
+ },
+ /**
* Evaluate the record evaluation context. This method is supposed to be
* called by _getEvalContext. It basically only generates a dictionary of
* current values for the record, with commands for x2manys fields.
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index 23e7aedb0eb..99de84c1654 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -1237,8 +1237,167 @@ QUnit.module('relational_fields', {
form.destroy();
});
- QUnit.test('list in form: discard newly added element with empty required field (onchange)', function (assert) {
- assert.expect(8);
+ QUnit.test('list in form: name_get with unique ids (default_get)', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].display_name = "MyTrululu";
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="trululu"/>' +
+ '</tree>' +
+ '</field>' +
+ '</sheet>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'default_get') {
+ return $.when({
+ p: [
+ [0, 0, { trululu: 1 }],
+ [0, 0, { trululu: 1 }]
+ ]
+ });
+ }
+ if (args.method === 'name_get') {
+ assert.deepEqual(args.args[0], _.uniq(args.args[0]),
+ "should not have duplicates in name_get rpc");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(form.$('td.o_data_cell').text(), "MyTrululuMyTrululu",
+ "both records should have the correct display_name for trululu field");
+
+ form.destroy();
+ });
+
+ QUnit.test('list in form: show name of many2one fields in multi-page (default_get)', function (assert) {
+ assert.expect(4);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<field name="p">' +
+ '<tree editable="bottom" limit="1">' +
+ '<field name="display_name"/>' +
+ '<field name="trululu"/>' +
+ '</tree>' +
+ '</field>' +
+ '</sheet>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'default_get') {
+ return $.when({
+ p: [
+ [0, 0, { display_name: 'record1', trululu: 1 }],
+ [0, 0, { display_name: 'record2', trululu: 2 }]
+ ]
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(form.$('td.o_data_cell').first().text(),
+ "record1", "should show display_name of 1st record");
+ assert.strictEqual(form.$('td.o_data_cell').first().next().text(),
+ "first record", "should show display_name of trululu of 1st record");
+
+ form.$('button.o_pager_next').click();
+
+ assert.strictEqual(form.$('td.o_data_cell').first().text(),
+ "record2", "should show display_name of 2nd record");
+ assert.strictEqual(form.$('td.o_data_cell').first().next().text(),
+ "second record", "should show display_name of trululu of 2nd record");
+
+ form.destroy();
+ });
+
+ QUnit.test('list in form: discard newly added element with empty required field (onchange in default_get)', function (assert) {
+ // variant of the test "list in form: discard newly added element with
+ // empty required field (default_get)", in which the `default_get`
+ // performs an `onchange` at the same time. This `onchange` may create
+ // some records, which should not be abandoned on discard, similarly
+ // to records created directly by `default_get`
+ assert.expect(7);
+
+ var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
+
+ this.data.partner.onchanges = {
+ product_id: function (obj) {
+ if (obj.product_id === 37) {
+ obj.p = [[0, 0, { display_name: "entry", trululu: false }]];
+ }
+ },
+ };
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="product_id"/>' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '<field name="trululu" required="1"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'default_get') {
+ return $.when({
+ product_id: 37,
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ // check that there is a record in the editable list with empty string as required field
+ assert.strictEqual(form.$('.o_data_row').length, 1,
+ "should have a row in the editable list");
+ assert.strictEqual($('td.o_data_cell').first().text(), "entry",
+ "should have the correct displayed name");
+ var requiredField = $('td.o_data_cell.o_required_modifier');
+ assert.strictEqual(requiredField.length, 1,
+ "should have a required field on this record");
+ assert.strictEqual(requiredField.text(), "",
+ "should have empty string in the required field on this record");
+
+ // click on empty required field in editable list record
+ requiredField.click();
+ // click off so that the required field still stay empty
+ $('body').click();
+
+ // record should not be dropped
+ assert.strictEqual(form.$('.o_data_row').length, 1,
+ "should not have dropped record in the editable list");
+ assert.strictEqual($('td.o_data_cell').first().text(), "entry",
+ "should still have the correct displayed name");
+ assert.strictEqual($('td.o_data_cell.o_required_modifier').text(), "",
+ "should still have empty string in the required field");
+
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
+ form.destroy();
+ });
+
+ QUnit.test('list in form: discard newly added element with empty required field (onchange after default_get)', function (assert) {
+ // discarding a record from an `onchange` in a `default_get` should not
+ // abandon the record. However, any `onchange` after `default_get`
+ // should be abandoned on discard
+ assert.expect(6);
var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
@@ -1249,7 +1408,6 @@ QUnit.module('relational_fields', {
obj.p = [[0, 0, { display_name: "entry", trululu: false }]];
}
},
- trululu: function () {},
};
var form = createView({
@@ -1291,13 +1449,9 @@ QUnit.module('relational_fields', {
// click off so that the required field still stay empty
$('body').click();
- // record should not be dropped
- assert.strictEqual(form.$('.o_data_row').length, 1,
- "should not have droppped record in the editable list");
- assert.strictEqual($('td.o_data_cell').first().text(), "entry",
- "should still have the correct displayed name");
- assert.strictEqual($('td.o_data_cell.o_required_modifier').text(), "",
- "should still have empty string in the required field");
+ // record should be dropped
+ assert.strictEqual(form.$('.o_data_row').length, 0,
+ "should have dropped record in the editable list");
relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
form.destroy();
diff --git a/addons/website_form/controllers/main.py b/addons/website_form/controllers/main.py
index b37ca97ea79..41a120008b4 100644
--- a/addons/website_form/controllers/main.py
+++ b/addons/website_form/controllers/main.py
@@ -112,6 +112,7 @@ class WebsiteForm(http.Controller):
'record': {}, # Values to create record
'attachments': [], # Attached files
'custom': '', # Custom fields values
+ 'meta': '', # Add metadata if enabled
}
authorized_fields = model.sudo()._get_form_writable_fields()
diff --git a/addons/website_sale/static/src/js/website_sale_tour_buy.js b/addons/website_sale/static/src/js/website_sale_tour_buy.js
index 47921e71547..a0659e43925 100644
--- a/addons/website_sale/static/src/js/website_sale_tour_buy.js
+++ b/addons/website_sale/static/src/js/website_sale_tour_buy.js
@@ -77,7 +77,7 @@ tour.register('shop_buy_product', {
content: "Pay Now",
//Either there are multiple payment methods, and one is checked, either there is only one, and therefore there are no radio inputs
extra_trigger: '#payment_method label:contains("Wire Transfer") input:checked,#payment_method:not(:has("input:radio:visible"))',
- trigger: 'button[id="o_payment_form_pay"]:visible',
+ trigger: 'button[id="o_payment_form_pay"]:visible:not(:disabled)',
},
{
content: "finish",
diff --git a/addons/website_sale_delivery/controllers/main.py b/addons/website_sale_delivery/controllers/main.py
index 56ef9c451f2..70b3b446303 100644
--- a/addons/website_sale_delivery/controllers/main.py
+++ b/addons/website_sale_delivery/controllers/main.py
@@ -4,7 +4,6 @@
from odoo import http, _
from odoo.http import request
from odoo.addons.website_sale.controllers.main import WebsiteSale
-from odoo.tools import float_repr
class WebsiteSaleDelivery(WebsiteSale):
@@ -46,9 +45,6 @@ class WebsiteSaleDelivery(WebsiteSale):
has_stockable_products = any(line.product_id.type in ['consu', 'product'] for line in order.order_line)
if has_stockable_products:
if order.carrier_id and not order.delivery_rating_success:
- values['errors'].append(
- (_("Ouch, you cannot choose this carrier!"),
- _("%s does not ship to your address, please choose another one.\n(Error: %s)" % (order.carrier_id.name, order.delivery_message))))
order._remove_delivery_line()
delivery_carriers = order._get_delivery_methods()
@@ -67,8 +63,15 @@ class WebsiteSaleDelivery(WebsiteSale):
return {'status': order.delivery_rating_success,
'error_message': order.delivery_message,
'carrier_id': carrier_id,
- 'new_amount_delivery': float_repr(currency.round(order.delivery_price), currency.decimal_places),
- 'new_amount_untaxed': order.amount_untaxed,
- 'new_amount_tax': order.amount_tax,
- 'new_amount_total': order.amount_total,
+ 'new_amount_delivery': self._format_amount(order.delivery_price, currency),
+ 'new_amount_untaxed': self._format_amount(order.amount_untaxed, currency),
+ 'new_amount_tax': self._format_amount(order.amount_tax, currency),
+ 'new_amount_total': self._format_amount(order.amount_total, currency),
}
+
+ def _format_amount(self, amount, currency):
+ fmt = "%.{0}f".format(currency.decimal_places)
+ lang = request.env['res.lang']._lang_get(request.env.context.get('lang') or 'en_US')
+
+ return lang.format(fmt, currency.round(amount), grouping=True, monetary=True)\
+ .replace(r' ', u'\N{NO-BREAK SPACE}').replace(r'-', u'\u2011')
diff --git a/addons/website_sale_delivery/models/sale_order.py b/addons/website_sale_delivery/models/sale_order.py
index 027d3db904d..461a70f5496 100644
--- a/addons/website_sale_delivery/models/sale_order.py
+++ b/addons/website_sale_delivery/models/sale_order.py
@@ -65,12 +65,11 @@ class SaleOrder(models.Model):
carrier = delivery
break
self.write({'carrier_id': carrier.id})
+ self._remove_delivery_line()
if carrier:
self.get_delivery_price()
if self.delivery_rating_success:
self.set_delivery_line()
- else:
- self._remove_delivery_line()
return bool(carrier)
diff --git a/addons/website_sale_delivery/static/src/js/website_sale_delivery.js b/addons/website_sale_delivery/static/src/js/website_sale_delivery.js
index 3cb0df152c1..971e604d1d1 100644
--- a/addons/website_sale_delivery/static/src/js/website_sale_delivery.js
+++ b/addons/website_sale_delivery/static/src/js/website_sale_delivery.js
@@ -27,6 +27,10 @@ odoo.define('website_sale_delivery.checkout', function (require) {
else {
console.error(result.error_message);
$compute_badge.text(result.error_message);
+ $amount_delivery.text(result.new_amount_delivery);
+ $amount_untaxed.text(result.new_amount_untaxed);
+ $amount_tax.text(result.new_amount_tax);
+ $amount_total.text(result.new_amount_total);
}
};
@@ -40,6 +44,12 @@ odoo.define('website_sale_delivery.checkout', function (require) {
var $carriers = $("#delivery_carrier input[name='delivery_type']");
$carriers.click(_onCarrierClick);
+ // Workaround to:
+ // - update the amount/error on the label at first rendering
+ // - prevent clicking on 'Pay Now' if the shipper rating fails
+ if ($carriers.length > 0) {
+ $carriers.filter(':checked').click();
+ }
/* Handle stuff */
$(".oe_website_sale select[name='shipping_id']").on('change', function () {
diff --git a/addons/website_sale_delivery/views/website_sale_delivery_templates.xml b/addons/website_sale_delivery/views/website_sale_delivery_templates.xml
index 3a39f64c27f..70279df3681 100644
--- a/addons/website_sale_delivery/views/website_sale_delivery_templates.xml
+++ b/addons/website_sale_delivery/views/website_sale_delivery_templates.xml
@@ -3,7 +3,7 @@
<template id="cart_delivery" name="Delivery Costs" inherit_id="website_sale.total">
<xpath expr="//div[@id='order_total_untaxed']" position="before">
- <div class="row" id="order_delivery" t-if="website_sale_order and website_sale_order.has_delivery">
+ <div class="row" id="order_delivery" t-if="website_sale_order and website_sale_order.carrier_id">
<span class="col-xs-6 text-right text-muted" title="Delivery will be updated after choosing a new delivery method"> Delivery:</span>
<span class="col-xs-6 text-right-not-xs text-left-xs text-muted">
<span t-field="website_sale_order.amount_delivery" style="white-space: nowrap;" t-options='{