-
- 1 else None
router = request.httprequest.app.get_db_router(request.db).bind('')
@@ -174,6 +174,10 @@ def is_multilang_url(local_url, langs=None):
return (func.routing.get('website', False) and
func.routing.get('multilang', func.routing['type'] == 'http'))
except werkzeug.exceptions.NotFound:
+ # Consider /static/ files as non-multilang
+ static_index = path.find('/static/', 1)
+ if static_index != -1 and static_index == path.find('/', 1):
+ return False
return True
except Exception as e:
return False
diff --git a/addons/l10n_pl/data/account_tax_data.xml b/addons/l10n_pl/data/account_tax_data.xml
index f29294b36aa..973d05ff7b6 100644
--- a/addons/l10n_pl/data/account_tax_data.xml
+++ b/addons/l10n_pl/data/account_tax_data.xml
@@ -256,7 +256,7 @@
sale
-
+
diff --git a/addons/mail/models/ir_attachment.py b/addons/mail/models/ir_attachment.py
index 31e59d680c9..7273145096d 100644
--- a/addons/mail/models/ir_attachment.py
+++ b/addons/mail/models/ir_attachment.py
@@ -13,9 +13,9 @@ class IrAttachment(models.Model):
"""
super(IrAttachment, self)._post_add_create()
for record in self:
- record.register_as_main_attachment()
+ record.register_as_main_attachment(force=False)
- def register_as_main_attachment(self):
+ def register_as_main_attachment(self, force=True):
""" Registers this attachment as the main one of the model it is
attached to.
"""
@@ -24,4 +24,5 @@ class IrAttachment(models.Model):
# message_main_attachment_id field can be empty, that's why we compare to False;
# we are just checking that it exists on the model before writing it
if related_record and hasattr(related_record, 'message_main_attachment_id'):
- related_record.message_main_attachment_id = self
+ if force or not related_record.message_main_attachment_id:
+ related_record.message_main_attachment_id = self
diff --git a/addons/mail/models/mail_mail.py b/addons/mail/models/mail_mail.py
index f758456c77e..a2ba820656f 100644
--- a/addons/mail/models/mail_mail.py
+++ b/addons/mail/models/mail_mail.py
@@ -5,6 +5,7 @@ import base64
import datetime
import logging
import psycopg2
+import smtplib
import threading
import re
@@ -394,11 +395,12 @@ class MailMail(models.Model):
mail.id, mail.message_id)
# mail status will stay on ongoing since transaction will be rollback
raise
- except psycopg2.Error:
- # If an error with the database occurs, chances are that the cursor is unusable.
- # This will lead to an `psycopg2.InternalError` being raised when trying to write
- # `state`, shadowing the original exception and forbid a retry on concurrent
- # update. Let's bubble it.
+ except (psycopg2.Error, smtplib.SMTPServerDisconnected):
+ # If an error with the database or SMTP session occurs, chances are that the cursor
+ # or SMTP session are unusable, causing further errors when trying to save the state.
+ _logger.exception(
+ 'Exception while processing mail with ID %r and Msg-Id %r.',
+ mail.id, mail.message_id)
raise
except Exception as e:
failure_reason = tools.ustr(e)
diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py
index e228f7c4062..e2ebb16c69e 100644
--- a/addons/mail/models/mail_message.py
+++ b/addons/mail/models/mail_message.py
@@ -154,8 +154,8 @@ class Message(models.Model):
@api.multi
def _search_has_error(self, operator, operand):
if operator == '=' and operand:
- return ['&', ('notification_ids.email_status', 'in', ('bounce', 'exception')), ('author_id', '=', self.env.user.partner_id.id)]
- return ['!', '&', ('notification_ids.email_status', 'in', ('bounce', 'exception')), ('author_id', '=', self.env.user.partner_id.id)] # this wont work and will be equivalent to "not in" beacause of orm restrictions. Dont use "has_error = False"
+ return [('notification_ids.email_status', 'in', ('bounce', 'exception'))]
+ return ['!', ('notification_ids.email_status', 'in', ('bounce', 'exception'))] # this wont work and will be equivalent to "not in" beacause of orm restrictions. Dont use "has_error = False"
@api.depends('starred_partner_ids')
def _get_starred(self):
diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py
index 127b7f7d609..81f1dc0510b 100644
--- a/addons/mail/models/mail_thread.py
+++ b/addons/mail/models/mail_thread.py
@@ -237,7 +237,7 @@ class MailThread(models.AbstractModel):
@api.model
def _search_message_has_error(self, operator, operand):
- return [('message_ids.has_error', operator, operand)]
+ return ['&', ('message_ids.has_error', operator, operand), ('message_ids.author_id', '=', self.env.user.partner_id.id)]
@api.multi
def _compute_message_attachment_count(self):
diff --git a/addons/mail/static/src/js/chatter.js b/addons/mail/static/src/js/chatter.js
index bc5d18a4150..968242ffe6a 100644
--- a/addons/mail/static/src/js/chatter.js
+++ b/addons/mail/static/src/js/chatter.js
@@ -81,6 +81,7 @@ var Chatter = Widget.extend({
var nodeOptions = fieldsInfo[mailFields.mail_thread].options || {};
this.hasLogButton = options.display_log_button || nodeOptions.display_log_button;
this.postRefresh = nodeOptions.post_refresh || 'never';
+ this.reloadOnUploadAttachment = this.postRefresh === 'always';
}
},
/**
@@ -577,6 +578,9 @@ var Chatter = Widget.extend({
* @private
*/
_onReloadAttachmentBox: function () {
+ if (this.reloadOnUploadAttachment) {
+ this.trigger_up('reload');
+ }
this._reloadAttachmentBox();
},
/**
diff --git a/addons/mail/static/src/scss/chatter.scss b/addons/mail/static/src/scss/chatter.scss
index b670e5b92d0..5b503966706 100644
--- a/addons/mail/static/src/scss/chatter.scss
+++ b/addons/mail/static/src/scss/chatter.scss
@@ -21,11 +21,11 @@
position: relative;
float: right;
flex: 1 1 auto;
+ height: $o-statusbar-height;
.o_chatter_button_attachment {
- height: $o-statusbar-height - 2px;
+ height: $o-statusbar-height;
float: right;
- padding: 0px;
margin-right: 25px;
.fa-paperclip, .o_chatter_attachment_button_count {
@@ -33,15 +33,20 @@
}
}
.o_followers {
- height: $o-statusbar-height + 1;
+ height: $o-statusbar-height;
border-bottom: 1px solid transparent;
flex: 0 1 auto;
}
}
> .btn {
- height: $o-statusbar-height + 1;
+ height: $o-statusbar-height;
border: 1px solid transparent;
}
+ .btn {
+ @extend .text-nowrap;
+ padding-right: $btn-padding-x;
+ padding-left: $btn-padding-x;
+ }
}
@@ -60,3 +65,11 @@
}
}
}
+
+@include media-breakpoint-down(sm) {
+ .o_chatter .o_chatter_topbar > .o_topbar_right_area {
+ .o_chatter_button_attachment {
+ margin-right: initial;
+ }
+ }
+}
diff --git a/addons/mail/static/src/scss/composer.scss b/addons/mail/static/src/scss/composer.scss
index 9dbdf8fde89..802236afd73 100644
--- a/addons/mail/static/src/scss/composer.scss
+++ b/addons/mail/static/src/scss/composer.scss
@@ -142,6 +142,9 @@
&:not(.o_chat_inline_composer) {
flex-flow: row wrap;
+ .o_composer_container {
+ max-width: 100%;
+ }
.o_composer_container, .o_composer_button_send {
flex: 1 0 100%;
}
diff --git a/addons/mass_mailing/models/mass_mailing_stats.py b/addons/mass_mailing/models/mass_mailing_stats.py
index 662b6adbaaf..f0d8a763438 100644
--- a/addons/mass_mailing/models/mass_mailing_stats.py
+++ b/addons/mass_mailing/models/mass_mailing_stats.py
@@ -66,14 +66,14 @@ class MailMailStats(models.Model):
stat.state = 'ignored'
elif stat.exception:
stat.state = 'exception'
- elif stat.sent:
- stat.state = 'sent'
elif stat.opened or stat.clicked:
stat.state = 'opened'
elif stat.replied:
stat.state = 'replied'
elif stat.bounced:
stat.state = 'bounced'
+ elif stat.sent:
+ stat.state = 'sent'
else:
stat.state = 'outgoing'
diff --git a/addons/mass_mailing/wizard/mail_compose_message.py b/addons/mass_mailing/wizard/mail_compose_message.py
index d0804977301..3f88e62552b 100644
--- a/addons/mass_mailing/wizard/mail_compose_message.py
+++ b/addons/mass_mailing/wizard/mail_compose_message.py
@@ -64,7 +64,8 @@ class MailComposeMessage(models.TransientModel):
if mail_values.get('email_to'):
mail_to = tools.email_normalize(mail_values['email_to'])
else:
- mail_to = tools.email_normalize(partners_email.get(res_id))
+ partner_id = (mail_values.get('recipient_ids') or [(False, '')])[0][1]
+ mail_to = tools.email_normalize(partners_email.get(partner_id))
if (opt_out_list and mail_to in opt_out_list) or (seen_list and mail_to in seen_list) \
or (not mail_to or not email_re.findall(mail_to)):
# prevent sending to blocked addresses that were included by mistake
diff --git a/addons/point_of_sale/static/src/js/chrome.js b/addons/point_of_sale/static/src/js/chrome.js
index ffc5d5cc860..f1a79a22064 100644
--- a/addons/point_of_sale/static/src/js/chrome.js
+++ b/addons/point_of_sale/static/src/js/chrome.js
@@ -720,9 +720,6 @@ var Chrome = PosBaseWidget.extend(AbstractAction.prototype, {
if(err.message === 'XmlHttpRequestError '){
title = 'Network Failure (XmlHttpRequestError)';
body = 'The Point of Sale could not be loaded due to a network problem.\n Please check your internet connection.';
- }else if(err.message === 'TLSError'){
- title = 'Https connection to IoT Box failed';
- body = 'Make sure you are using IoT Box v18.12 or higher.\n\n Navigate to ' + err.url + ' to accept the certificate of your IoT Box.';
}else if(err.code === 200){
title = err.data.message;
body = err.data.debug;
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index 2840b6b2866..6797107a367 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -137,11 +137,16 @@ exports.PosModel = Backbone.Model.extend({
resolve();
},
function (statusText, url) {
- if (statusText == 'error' && window.location.protocol == 'https:') {
- var error = {message: 'TLSError', url: url};
- self.chrome.loading_error(error);
- } else {
- resolve();
+ var show_loading_error = (self.gui.current_screen === null);
+ resolve();
+ if (show_loading_error && statusText == 'error' && window.location.protocol == 'https:') {
+ self.gui.show_popup('alert', {
+ title: _t('HTTPS connection to IoT Box failed'),
+ body: _.str.sprintf(
+ _t('Make sure you are using IoT Box v18.12 or higher. Navigate to %s to accept the certificate of your IoT Box.'),
+ url
+ ),
+ });
}
}
);
diff --git a/addons/pos_hr/__manifest__.py b/addons/pos_hr/__manifest__.py
index a741ea0cd2c..170529048ad 100644
--- a/addons/pos_hr/__manifest__.py
+++ b/addons/pos_hr/__manifest__.py
@@ -20,6 +20,6 @@
'views/pos_order_report_view.xml',
],
'installable': True,
- 'auto-install': True,
+ 'auto_install': True,
'qweb': ['static/src/xml/pos.xml'],
}
diff --git a/addons/product/models/product.py b/addons/product/models/product.py
index 1979ccda68c..99a314662dd 100644
--- a/addons/product/models/product.py
+++ b/addons/product/models/product.py
@@ -15,6 +15,7 @@ from odoo.tools import float_compare
_logger = logging.getLogger(__name__)
+
class ProductCategory(models.Model):
_name = "product.category"
_description = "Product Category"
@@ -612,7 +613,10 @@ class ProductProduct(models.Model):
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
res = self.env['product.supplierinfo']
- for seller in self._prepare_sellers(params):
+ sellers = self._prepare_sellers(params)
+ if self.env.context.get('force_company'):
+ sellers = sellers.filtered(lambda s: not s.company_id or s.company_id.id == self.env.context['force_company'])
+ for seller in sellers:
# Set quantity in UoM of seller
quantity_uom_seller = quantity
if quantity_uom_seller and uom_id and uom_id != seller.product_uom:
diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py
index c9fb51039c1..56f9e10a840 100644
--- a/addons/product/models/product_template.py
+++ b/addons/product/models/product_template.py
@@ -523,6 +523,7 @@ class ProductTemplate(models.Model):
variants_to_create.append({
'product_tmpl_id': tmpl_id.id,
'attribute_value_ids': [(6, 0, list(value_ids))],
+ 'active': tmpl_id.active,
})
if len(variants_to_create) > 1000:
raise UserError(_(
@@ -991,8 +992,10 @@ class ProductTemplate(models.Model):
if not self.active:
return _("The product template is archived so no combination is possible.")
- ptal_stack = [self._get_valid_product_template_attribute_lines()]
- combination_stack = [self.env['product.template.attribute.value']]
+ necessary_values = necessary_values or self.env['product.template.attribute.value']
+ necessary_attributes = necessary_values.mapped('attribute_id')
+ ptal_stack = [self.valid_product_template_attribute_line_ids.filtered(lambda ptal: ptal.attribute_id not in necessary_attributes)]
+ combination_stack = [necessary_values]
# keep going while we have attribute lines to test
while len(ptal_stack):
@@ -1001,7 +1004,7 @@ class ProductTemplate(models.Model):
if not attribute_lines:
# full combination, if it's possible return it, otherwise skip it
- if self._is_combination_possible(combination, parent_combination) and all(v in combination for v in (necessary_values or [])):
+ if self._is_combination_possible(combination, parent_combination):
yield(combination)
else:
# we have remaining attribute lines to consider
diff --git a/addons/product/tests/test_product_attribute_value_config.py b/addons/product/tests/test_product_attribute_value_config.py
index aa1de67d16b..e966adbdbdc 100644
--- a/addons/product/tests/test_product_attribute_value_config.py
+++ b/addons/product/tests/test_product_attribute_value_config.py
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import time
+
from odoo.tests import tagged
from odoo.tests.common import TransactionCase
@@ -423,6 +425,47 @@ class TestProductAttributeValueConfig(TestProductAttributeValueSetup):
self.assertEqual(self.computer._get_closest_possible_combination(computer_ssd_256 + computer_ram_8 + computer_hdd_4 + computer_hdd_2),
computer_ssd_256 + computer_ram_8 + computer_hdd_4)
+ # Make sure this is not extremely slow:
+ product_template = self.env['product.template'].create({
+ 'name': 'many combinations',
+ })
+
+ for i in range(10):
+ # create the attributes
+ product_attribute = self.env['product.attribute'].create({
+ 'name': "att %s" % i,
+ 'create_variant': 'dynamic',
+ 'sequence': i,
+ })
+
+ for j in range(10):
+ # create the attribute values
+ self.env['product.attribute.value'].create([{
+ 'name': "val %s/%s" % (i, j),
+ 'attribute_id': product_attribute.id,
+ 'sequence': j,
+ }])
+
+ # set attribute and attribute values on the template
+ self.env['product.template.attribute.line'].create([{
+ 'attribute_id': product_attribute.id,
+ 'product_tmpl_id': product_template.id,
+ 'value_ids': [(6, 0, product_attribute.value_ids.ids)]
+ }])
+
+ # Get a value in the middle for each attribute to make sure it would
+ # take time to reach it (if looping one by one like before the fix).
+ combination = self.env['product.template.attribute.value']
+ for ptal in product_template.attribute_line_ids:
+ combination += ptal.product_template_value_ids[5]
+
+ started_at = time.time()
+ self.assertEqual(product_template._get_closest_possible_combination(combination), combination)
+ elapsed = time.time() - started_at
+ # It should take around 10ms, but to avoid false positives we check an
+ # higher value. Before the fix it would take hours.
+ self.assertLess(elapsed, 0.5)
+
def test_clear_caches(self):
"""The goal of this test is to make sure the cache is invalidated when
it should be."""
diff --git a/addons/product/views/product_views.xml b/addons/product/views/product_views.xml
index eead94d7c9a..cac587a3fa7 100644
--- a/addons/product/views/product_views.xml
+++ b/addons/product/views/product_views.xml
@@ -300,8 +300,9 @@
-
+
+
diff --git a/addons/purchase/models/account_invoice.py b/addons/purchase/models/account_invoice.py
index 9f63039afd7..6c7b1b9293d 100644
--- a/addons/purchase/models/account_invoice.py
+++ b/addons/purchase/models/account_invoice.py
@@ -25,6 +25,10 @@ class AccountInvoice(models.Model):
return {}
self.purchase_id = self.vendor_bill_purchase_id.purchase_order_id
self.vendor_bill_id = self.vendor_bill_purchase_id.vendor_bill_id
+ vendor_ref = self.vendor_bill_purchase_id.purchase_order_id.partner_ref
+ if vendor_ref:
+ self.reference = ", ".join([self.reference, vendor_ref]) if (
+ self.reference and vendor_ref not in self.reference) else vendor_ref
self.vendor_bill_purchase_id = False
return {}
diff --git a/addons/purchase_requisition/models/product.py b/addons/purchase_requisition/models/product.py
index ef94ad283b7..48380f1c3dc 100644
--- a/addons/purchase_requisition/models/product.py
+++ b/addons/purchase_requisition/models/product.py
@@ -16,10 +16,11 @@ class ProductProduct(models.Model):
_inherit = 'product.product'
def _prepare_sellers(self, params):
+ sellers = super(ProductProduct, self)._prepare_sellers(params)
if params and params.get('order_id'):
- return self.seller_ids.filtered(lambda s: not s.purchase_requisition_id or s.purchase_requisition_id == params['order_id'].requisition_id)
+ return sellers.filtered(lambda s: not s.purchase_requisition_id or s.purchase_requisition_id == params['order_id'].requisition_id)
else:
- return self.seller_ids
+ return sellers
class ProductTemplate(models.Model):
diff --git a/addons/purchase_stock/models/stock_rule.py b/addons/purchase_stock/models/stock_rule.py
index 64a72467b11..e8873375ba9 100644
--- a/addons/purchase_stock/models/stock_rule.py
+++ b/addons/purchase_stock/models/stock_rule.py
@@ -174,7 +174,7 @@ class StockRule(models.Model):
def _update_purchase_order_line(self, product_id, product_qty, product_uom, company_id, values, line):
partner = values['supplier'].name
procurement_uom_po_qty = product_uom._compute_quantity(product_qty, product_id.uom_po_id)
- seller = product_id._select_seller(
+ seller = product_id.with_context(force_company=company_id.id)._select_seller(
partner_id=partner,
quantity=line.product_qty + procurement_uom_po_qty,
date=line.order_id.date_order and line.order_id.date_order.date(),
@@ -197,7 +197,7 @@ class StockRule(models.Model):
procurement_uom_po_qty = product_uom._compute_quantity(product_qty, product_id.uom_po_id)
# _select_seller is used if the supplier have different price depending
# the quantities ordered.
- seller = product_id._select_seller(
+ seller = product_id.with_context(force_company=company_id.id)._select_seller(
partner_id=partner,
quantity=procurement_uom_po_qty,
date=po.date_order and po.date_order.date(),
diff --git a/addons/sale/models/res_company.py b/addons/sale/models/res_company.py
index 6d6cfccc87c..439061adafa 100644
--- a/addons/sale/models/res_company.py
+++ b/addons/sale/models/res_company.py
@@ -61,6 +61,7 @@ class ResCompany(models.Model):
'product_uom_qty': 10,
'price_unit': 123,
'order_id': sample_sales_order.id,
+ 'company_id': sample_sales_order.company_id.id,
})
return sample_sales_order
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index 99258e80233..427c9a7986e 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -1031,20 +1031,23 @@ class SaleOrderLine(models.Model):
res[field] = line._fields[field].convert_to_write(line[field], line)
return res
- @api.model
- def create(self, values):
- if values.get('display_type', self.default_get(['display_type'])['display_type']):
- values.update(product_id=False, price_unit=0, product_uom_qty=0, product_uom=False, customer_lead=0)
+ @api.model_create_multi
+ def create(self, vals_list):
+ for values in vals_list:
+ if values.get('display_type', self.default_get(['display_type'])['display_type']):
+ values.update(product_id=False, price_unit=0, product_uom_qty=0, product_uom=False, customer_lead=0)
- values.update(self._prepare_add_missing_fields(values))
- line = super(SaleOrderLine, self).create(values)
- if line.product_id and line.order_id.state == 'sale':
- msg = _("Extra line with %s ") % (line.product_id.display_name,)
- line.order_id.message_post(body=msg)
- # create an analytic account if at least an expense product
- if line.product_id.expense_policy not in [False, 'no'] and not self.order_id.analytic_account_id:
- self.order_id._create_analytic_account()
- return line
+ values.update(self._prepare_add_missing_fields(values))
+
+ lines = super().create(vals_list)
+ for line in lines:
+ if line.product_id and line.order_id.state == 'sale':
+ msg = _("Extra line with %s ") % (line.product_id.display_name,)
+ line.order_id.message_post(body=msg)
+ # create an analytic account if at least an expense product
+ if line.product_id.expense_policy not in [False, 'no'] and not line.order_id.analytic_account_id:
+ line.order_id._create_analytic_account()
+ return lines
_sql_constraints = [
('accountable_required_fields',
diff --git a/addons/sale/report/sale_report_templates.xml b/addons/sale/report/sale_report_templates.xml
index 380596eb181..c8c9612326a 100644
--- a/addons/sale/report/sale_report_templates.xml
+++ b/addons/sale/report/sale_report_templates.xml
@@ -70,11 +70,15 @@
diff --git a/addons/sale_management/models/sale_order.py b/addons/sale_management/models/sale_order.py
index 6ebed05a2bc..6a95dfeaeeb 100644
--- a/addons/sale_management/models/sale_order.py
+++ b/addons/sale_management/models/sale_order.py
@@ -31,7 +31,8 @@ class SaleOrder(models.Model):
@api.onchange('partner_id')
def onchange_partner_id(self):
super(SaleOrder, self).onchange_partner_id()
- self.note = self.sale_order_template_id.note or self.note
+ template = self.sale_order_template_id.with_context(lang=self.partner_id.lang)
+ self.note = template.note or self.note
def _compute_line_data_for_template_change(self, line):
return {
diff --git a/addons/sale_management/models/sale_order_template.py b/addons/sale_management/models/sale_order_template.py
index 8eef6efd405..493386f33c7 100644
--- a/addons/sale_management/models/sale_order_template.py
+++ b/addons/sale_management/models/sale_order_template.py
@@ -18,7 +18,7 @@ class SaleOrderTemplate(models.Model):
name = fields.Char('Quotation Template', required=True)
sale_order_template_line_ids = fields.One2many('sale.order.template.line', 'sale_order_template_id', 'Lines', copy=True)
- note = fields.Text('Terms and conditions')
+ note = fields.Text('Terms and conditions', translate=True)
sale_order_template_option_ids = fields.One2many('sale.order.template.option', 'sale_order_template_id', 'Optional Products', copy=True)
number_of_days = fields.Integer('Quotation Duration',
help='Number of days for the validity date computation of the quotation')
diff --git a/addons/sale_management/views/sale_portal_templates.xml b/addons/sale_management/views/sale_portal_templates.xml
index 90d2100ba4a..f29a538dd0e 100644
--- a/addons/sale_management/views/sale_portal_templates.xml
+++ b/addons/sale_management/views/sale_portal_templates.xml
@@ -15,6 +15,7 @@
+
diff --git a/addons/stock/models/stock_inventory.py b/addons/stock/models/stock_inventory.py
index d3111d2564f..4710ff88851 100644
--- a/addons/stock/models/stock_inventory.py
+++ b/addons/stock/models/stock_inventory.py
@@ -3,7 +3,7 @@
from odoo import api, fields, models, _
from odoo.addons import decimal_precision as dp
-from odoo.exceptions import UserError
+from odoo.exceptions import UserError, ValidationError
from odoo.tools import float_utils, float_compare
@@ -151,13 +151,13 @@ class Inventory(models.Model):
if self.filter == 'none' and self.product_id and self.location_id and self.lot_id:
return
if self.filter not in ('product', 'product_owner') and self.product_id:
- raise UserError(_('The selected product doesn\'t belong to that owner..'))
+ raise ValidationError(_('The selected product doesn\'t belong to that owner..'))
if self.filter != 'lot' and self.lot_id:
- raise UserError(_('The selected lot number doesn\'t exist.'))
+ raise ValidationError(_('The selected lot number doesn\'t exist.'))
if self.filter not in ('owner', 'product_owner') and self.partner_id:
- raise UserError(_('The selected owner doesn\'t have the proprietary of that product.'))
+ raise ValidationError(_('The selected owner doesn\'t have the proprietary of that product.'))
if self.filter != 'pack' and self.package_id:
- raise UserError(_('The selected inventory options are not coherent, the package doesn\'t exist.'))
+ raise ValidationError(_('The selected inventory options are not coherent, the package doesn\'t exist.'))
def action_reset_product_qty(self):
self.mapped('line_ids').write({'product_qty': 0})
@@ -425,7 +425,7 @@ class InventoryLine(models.Model):
"""
for line in self:
if line.product_id.type != 'product':
- raise UserError(_("You can only adjust storable products.") + '\n\n%s -> %s' % (line.product_id.display_name, line.product_id.type))
+ raise ValidationError(_("You can only adjust storable products.") + '\n\n%s -> %s' % (line.product_id.display_name, line.product_id.type))
def _get_move_values(self, qty, location_id, location_dest_id, out):
self.ensure_one()
diff --git a/addons/stock/models/stock_quant.py b/addons/stock/models/stock_quant.py
index 03c1890c51a..66a7aa90b8c 100644
--- a/addons/stock/models/stock_quant.py
+++ b/addons/stock/models/stock_quant.py
@@ -365,7 +365,7 @@ class QuantPackage(models.Model):
'res.partner', 'Owner', compute='_compute_package_info', search='_search_owner',
index=True, readonly=True)
- @api.depends('quant_ids.package_id', 'quant_ids.location_id', 'quant_ids.company_id', 'quant_ids.owner_id')
+ @api.depends('quant_ids.package_id', 'quant_ids.location_id', 'quant_ids.company_id', 'quant_ids.owner_id', 'quant_ids.quantity', 'quant_ids.reserved_quantity')
def _compute_package_info(self):
for package in self:
values = {'location_id': False, 'company_id': self.env.user.company_id.id, 'owner_id': False}
diff --git a/addons/stock/report/report_stockinventory.xml b/addons/stock/report/report_stockinventory.xml
index 90abd6b4109..19a988dfe59 100644
--- a/addons/stock/report/report_stockinventory.xml
+++ b/addons/stock/report/report_stockinventory.xml
@@ -6,6 +6,7 @@
+
Stock Inventory
diff --git a/addons/stock/wizard/stock_immediate_transfer.py b/addons/stock/wizard/stock_immediate_transfer.py
index 66a652ac795..9b46a91c585 100644
--- a/addons/stock/wizard/stock_immediate_transfer.py
+++ b/addons/stock/wizard/stock_immediate_transfer.py
@@ -22,7 +22,7 @@ class StockImmediateTransfer(models.TransientModel):
picking.action_assign()
if picking.state != 'assigned':
raise UserError(_("Could not reserve all requested products. Please use the \'Mark as Todo\' button to handle the reservation manually."))
- for move in picking.move_lines:
+ for move in picking.move_lines.filtered(lambda m: m.state not in ['done', 'cancel']):
for move_line in move.move_line_ids:
move_line.qty_done = move_line.product_uom_qty
if picking._check_backorder():
diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py
index 5a6500ebf9d..1abd666afc2 100644
--- a/addons/stock_account/models/stock.py
+++ b/addons/stock_account/models/stock.py
@@ -303,7 +303,7 @@ class StockMove(models.Model):
if qty_to_take_on_candidates == 0:
move.write({
'value': -tmp_value if not quantity else move.value or -tmp_value, # outgoing move are valued negatively
- 'price_unit': -tmp_value / move.product_qty,
+ 'price_unit': -tmp_value / (move.product_qty or quantity),
})
elif qty_to_take_on_candidates > 0:
last_fifo_price = new_standard_price or move.product_id.standard_price
diff --git a/addons/stock_account/views/product_views.xml b/addons/stock_account/views/product_views.xml
index 4919bf85924..b4fd4e46503 100644
--- a/addons/stock_account/views/product_views.xml
+++ b/addons/stock_account/views/product_views.xml
@@ -67,7 +67,7 @@