[MERGE] forward port branch 11.0 up to 56ce29e71f
This commit is contained in:
@@ -240,9 +240,9 @@ class ResPartner(models.Model):
|
||||
LEFT JOIN account_move_line aml ON aml.partner_id = partner.id
|
||||
RIGHT JOIN account_account acc ON aml.account_id = acc.id
|
||||
WHERE acc.internal_type = %s
|
||||
AND NOT acc.deprecated
|
||||
AND NOT acc.deprecated AND acc.company_id = %s
|
||||
GROUP BY partner.id
|
||||
HAVING %s * COALESCE(SUM(aml.amount_residual), 0) ''' + operator + ''' %s''', (account_type, sign, operand))
|
||||
HAVING %s * COALESCE(SUM(aml.amount_residual), 0) ''' + operator + ''' %s''', (account_type, self.env.user.company_id.id, sign, operand))
|
||||
res = self._cr.fetchall()
|
||||
if not res:
|
||||
return [('id', '=', '0')]
|
||||
|
||||
@@ -308,7 +308,7 @@ class AccountFrFec(models.TransientModel):
|
||||
TO_CHAR(am.date, 'YYYYMMDD') AS PieceDate,
|
||||
CASE WHEN aml.name IS NULL OR aml.name = '' THEN '/'
|
||||
WHEN aml.name SIMILAR TO '[\t|\s|\n]*' THEN '/'
|
||||
ELSE replace(replace(replace(aml.name, '|', '/'), '\t', ''), '\n', '') END AS EcritureLib,
|
||||
ELSE replace(replace(replace(replace(aml.name, '|', '/'), '\t', ''), '\n', ''), '\r', '') END AS EcritureLib,
|
||||
replace(CASE WHEN aml.debit = 0 THEN '0,00' ELSE to_char(aml.debit, '000000000000000D99') END, '.', ',') AS Debit,
|
||||
replace(CASE WHEN aml.credit = 0 THEN '0,00' ELSE to_char(aml.credit, '000000000000000D99') END, '.', ',') AS Credit,
|
||||
CASE WHEN rec.name IS NULL THEN '' ELSE rec.name END AS EcritureLet,
|
||||
|
||||
@@ -1707,8 +1707,11 @@ exports.Orderline = Backbone.Model.extend({
|
||||
},
|
||||
_compute_all: function(tax, base_amount, quantity) {
|
||||
if (tax.amount_type === 'fixed') {
|
||||
var sign_base_amount = base_amount >= 0 ? 1 : -1;
|
||||
return (Math.abs(tax.amount) * sign_base_amount) * quantity;
|
||||
var sign_base_amount = Math.sign(base_amount) || 1;
|
||||
// Since base amount has been computed with quantity
|
||||
// we take the abs of quantity
|
||||
// Same logic as bb72dea98de4dae8f59e397f232a0636411d37ce
|
||||
return tax.amount * sign_base_amount * Math.abs(quantity);
|
||||
}
|
||||
if ((tax.amount_type === 'percent' && !tax.price_include) || (tax.amount_type === 'division' && tax.price_include)){
|
||||
return base_amount * tax.amount / 100;
|
||||
|
||||
@@ -42,10 +42,16 @@ var DiscountButton = screens.ActionButtonWidget.extend({
|
||||
}
|
||||
|
||||
// Add discount
|
||||
// We add the price as manually set to avoid recomputation when changing customer.
|
||||
var discount = - pc / 100.0 * order.get_total_with_tax();
|
||||
|
||||
if( discount < 0 ){
|
||||
order.add_product(product, { price: discount });
|
||||
order.add_product(product, {
|
||||
price: discount,
|
||||
extras: {
|
||||
price_manually_set: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -46,7 +46,12 @@ class StockMove(models.Model):
|
||||
if line.product_uom.id != line.product_id.uom_id.id:
|
||||
price_unit *= line.product_uom.factor / line.product_id.uom_id.factor
|
||||
if order.currency_id != order.company_id.currency_id:
|
||||
price_unit = order.currency_id.with_context(date=self.date or order.date_approve).compute(price_unit, order.company_id.currency_id, round=False)
|
||||
# DO NOT FORWARD-PORT! ONLY FOR V11!!!
|
||||
# The date must be today, and not the date of the move since the move move is still
|
||||
# in assigned state. However, the move date is the scheduled date until move is
|
||||
# done, then date of actual move processing. See:
|
||||
# https://github.com/odoo/odoo/blob/2f789b6863407e63f90b3a2d4cc3be09815f7002/addons/stock/models/stock_move.py#L36
|
||||
price_unit = order.currency_id.with_context(date=fields.Date.context_today(self)).compute(price_unit, order.company_id.currency_id, round=False)
|
||||
return price_unit
|
||||
return super(StockMove, self)._get_price_unit()
|
||||
|
||||
|
||||
@@ -771,12 +771,12 @@ class TestStockValuationWithCOA(AccountingTestCase):
|
||||
'company_id': company.id,
|
||||
})
|
||||
|
||||
# To allow testing validation of PO
|
||||
# To allow testing validation of PO and Delivery
|
||||
today = date_po
|
||||
def _today(*args, **kwargs):
|
||||
return date_po
|
||||
# To allow testing validation of Delivery
|
||||
return today
|
||||
def _now(*args, **kwargs):
|
||||
return date_delivery + ' 01:00:00'
|
||||
return today + ' 01:00:00'
|
||||
|
||||
patchers = [
|
||||
patch('odoo.fields.Date.context_today', _today),
|
||||
@@ -805,13 +805,17 @@ class TestStockValuationWithCOA(AccountingTestCase):
|
||||
|
||||
line_product_avg = po.order_line.filtered(lambda l: l.product_id == product_avg)
|
||||
|
||||
today = date_delivery
|
||||
picking = po.picking_ids
|
||||
(picking.move_lines
|
||||
.filtered(lambda l: l.purchase_line_id == line_product_avg)
|
||||
.write({'quantity_done': 1.0}))
|
||||
|
||||
picking.button_validate()
|
||||
# 1 Units received at rate 0.7 = 42.86
|
||||
self.assertAlmostEqual(product_avg.standard_price, 42.86)
|
||||
|
||||
today = date_invoice
|
||||
inv = self.env['account.invoice'].create({
|
||||
'type': 'in_invoice',
|
||||
'date_invoice': date_invoice,
|
||||
|
||||
@@ -2283,7 +2283,7 @@ $.summernote.pluginEvents.backColor = function (event, editor, layoutInfo, backC
|
||||
|
||||
options.onCreateLink = function (sLinkUrl) {
|
||||
if (sLinkUrl.indexOf('mailto:') === 0 || sLinkUrl.indexOf('tel:') === 0) {
|
||||
// pass
|
||||
sLinkUrl = sLinkUrl.replace(/^tel:([0-9]+)$/, 'tel://$1');
|
||||
} else if (sLinkUrl.indexOf('@') !== -1 && sLinkUrl.indexOf(':') === -1) {
|
||||
sLinkUrl = 'mailto:' + sLinkUrl;
|
||||
} else if (sLinkUrl.indexOf('://') === -1 && sLinkUrl[0] !== '/'
|
||||
|
||||
@@ -926,7 +926,7 @@ var VideoWidget = MediaWidget.extend({
|
||||
return {errorCode: 0};
|
||||
}
|
||||
|
||||
var autoplay = options.autoplay ? '?autoplay=1' : '?autoplay=0';
|
||||
var autoplay = options.autoplay ? '?autoplay=1&mute=1' : '?autoplay=0';
|
||||
|
||||
if (ytMatch && ytMatch[2].length === 11) {
|
||||
$video.attr('src', '//www.youtube' + (ytMatch[1] || '') + '.com/embed/' + ytMatch[2] + autoplay);
|
||||
@@ -955,7 +955,8 @@ var VideoWidget = MediaWidget.extend({
|
||||
$video.attr('src', $video.attr('src') + '&rel=0');
|
||||
}
|
||||
if (options.loop && (ytMatch || vimMatch)) {
|
||||
$video.attr('src', $video.attr('src') + '&loop=1');
|
||||
var videoSrc = _.str.sprintf('%s&loop=1', $video.attr('src'));
|
||||
$video.attr('src', ytMatch ? _.str.sprintf('%s&playlist=%s', videoSrc, ytMatch[2]) : videoSrc);
|
||||
}
|
||||
if (options.hide_controls && (ytMatch || dmMatch)) {
|
||||
$video.attr('src', $video.attr('src') + '&controls=0');
|
||||
|
||||
@@ -202,7 +202,11 @@
|
||||
<small class="text-muted">Options</small>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item o_yt_option o_vim_option o_dm_option">
|
||||
<label class="o_switch mb0"><input id="o_video_autoplay" type="checkbox"/><span/>Autoplay</label>
|
||||
<label class="o_switch mb0">
|
||||
<input id="o_video_autoplay" type="checkbox"/><span/>
|
||||
<span style="margin-right: 8px;">Autoplay</span>
|
||||
<span class="small text-muted" style="margin-left: auto;">Videos are muted when autoplay is enabled</span>
|
||||
</label>
|
||||
</li>
|
||||
<li class="list-group-item o_yt_option o_vim_option">
|
||||
<label class="o_switch mb0"><input id="o_video_loop" type="checkbox"/><span/>Loop</label>
|
||||
|
||||
@@ -45,3 +45,4 @@ Jose Manuel Robles josemanuel@vauxoo.com https://github.com/keylor2906
|
||||
Erick Birbe erick@vauxoo.com https://github.com/ebirbe
|
||||
Tomas Alvarez tomas@vauxoo.com https://github.com/tomeyro
|
||||
Carmen Liliana Miranda González carmen@vauxoo.com https://github.com/CarmenMiranda
|
||||
Deivis Laya deivis@vauxoo.com https://github.com/deivislaya
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import models
|
||||
@@ -0,0 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
"name": "Test RPC",
|
||||
"version": "1.0",
|
||||
"category": "Tests",
|
||||
"description": """A module to test the RPC requests.""",
|
||||
"depends": ["base", "web"],
|
||||
"installable": True,
|
||||
"auto_install": False,
|
||||
"data": ["ir.model.access.csv"],
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
|
||||
access_test_rpc_model_a,access_test_rpc_model_a,model_test_rpc_model_a,,1,1,1,1
|
||||
access_test_rpc_model_b,access_test_rpc_model_b,model_test_rpc_model_b,,1,1,1,1
|
||||
|
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ModelA(models.Model):
|
||||
_name = "test_rpc.model_a"
|
||||
_description = "Model A"
|
||||
|
||||
name = fields.Char(required=True)
|
||||
field_b1 = fields.Many2one("test_rpc.model_b", string="required field", required=True)
|
||||
field_b2 = fields.Many2one("test_rpc.model_b", string="restricted field", ondelete="restrict")
|
||||
|
||||
|
||||
class ModelB(models.Model):
|
||||
_name = "test_rpc.model_b"
|
||||
_description = "Model B"
|
||||
|
||||
name = fields.Char(required=True)
|
||||
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import test_error
|
||||
@@ -0,0 +1,67 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo.tests import common
|
||||
from odoo.tools.misc import mute_logger
|
||||
|
||||
|
||||
class TestError(common.HttpCase):
|
||||
def setUp(self):
|
||||
super(TestError, self).setUp()
|
||||
self.o = self.xmlrpc_object
|
||||
self.db_name = common.get_db_name()
|
||||
# Reset the admin's lang to avoid breaking tests due to admin not in English
|
||||
self.o.execute(self.db_name, 1, "admin", "res.users", "write", [1], {"lang": False})
|
||||
|
||||
def test_01_create(self):
|
||||
""" Create: mandatory field not provided """
|
||||
self.o.execute(self.db_name, 1, "admin", "test_rpc.model_b", "create", {"name": "B1"})
|
||||
try:
|
||||
with mute_logger("odoo.sql_db"):
|
||||
self.o.execute(self.db_name, 1, "admin", "test_rpc.model_b", "create", {})
|
||||
raise
|
||||
except Exception as e:
|
||||
self.assertIn("The operation cannot be completed:", e.faultString)
|
||||
self.assertIn("Create/update: a mandatory field is not set.", e.faultString)
|
||||
self.assertIn(
|
||||
"Delete: another model requires the record being deleted. If possible, archive it instead.",
|
||||
e.faultString,
|
||||
)
|
||||
self.assertIn("Model: Model B (test_rpc.model_b), Field: Name (name)", e.faultString)
|
||||
|
||||
def test_02_delete(self):
|
||||
""" Delete: NOT NULL and ON DELETE RESTRICT constraints """
|
||||
b1 = self.o.execute(self.db_name, 1, "admin", "test_rpc.model_b", "create", {"name": "B1"})
|
||||
b2 = self.o.execute(self.db_name, 1, "admin", "test_rpc.model_b", "create", {"name": "B2"})
|
||||
self.o.execute(
|
||||
self.db_name, 1, "admin", "test_rpc.model_a", "create", {"name": "A1", "field_b1": b1, "field_b2": b2}
|
||||
)
|
||||
|
||||
# Unlink b1 => NOT NULL constraint raises
|
||||
# FORWARD-PORT: in master, `required=True` implies `ondelete="restrict"`
|
||||
# => the message should be the same than 'Unlink b2' case.
|
||||
try:
|
||||
with mute_logger("odoo.sql_db"):
|
||||
self.o.execute(self.db_name, 1, "admin", "test_rpc.model_b", "unlink", b1)
|
||||
raise
|
||||
except Exception as e:
|
||||
self.assertIn("The operation cannot be completed:", e.faultString)
|
||||
self.assertIn("Create/update: a mandatory field is not set.", e.faultString)
|
||||
self.assertIn(
|
||||
"Delete: another model requires the record being deleted. If possible, archive it instead.",
|
||||
e.faultString,
|
||||
)
|
||||
self.assertIn("Model: Model A (test_rpc.model_a), Field: required field (field_b1)", e.faultString)
|
||||
|
||||
# Unlink b2 => ON DELETE RESTRICT constraint raises
|
||||
try:
|
||||
with mute_logger("odoo.sql_db"):
|
||||
self.o.execute(self.db_name, 1, "admin", "test_rpc.model_b", "unlink", b2)
|
||||
raise
|
||||
except Exception as e:
|
||||
self.assertIn("The operation cannot be completed:", e.faultString)
|
||||
self.assertIn(
|
||||
" another model requires the record being deleted. If possible, archive it instead.", e.faultString
|
||||
)
|
||||
self.assertIn(
|
||||
"Model: Model A (test_rpc.model_a), Constraint: test_rpc_model_a_field_b2_fkey", e.faultString
|
||||
)
|
||||
+32
-14
@@ -118,22 +118,40 @@ def check(f):
|
||||
if key in inst.pgerror:
|
||||
raise ValidationError(tr(registry._sql_error[key], 'sql_constraint') or inst.pgerror)
|
||||
if inst.pgcode in (errorcodes.NOT_NULL_VIOLATION, errorcodes.FOREIGN_KEY_VIOLATION, errorcodes.RESTRICT_VIOLATION):
|
||||
msg = _('The operation cannot be completed, probably due to the following:\n- deletion: you may be trying to delete a record while other records still reference it\n- creation/update: a mandatory field is not correctly set')
|
||||
msg = _('The operation cannot be completed:')
|
||||
_logger.debug("IntegrityError", exc_info=True)
|
||||
try:
|
||||
errortxt = inst.pgerror.replace('«','"').replace('»','"')
|
||||
if '"public".' in errortxt:
|
||||
context = errortxt.split('"public".')[1]
|
||||
model_name = table = context.split('"')[1]
|
||||
else:
|
||||
last_quote_end = errortxt.rfind('"')
|
||||
last_quote_begin = errortxt.rfind('"', 0, last_quote_end)
|
||||
model_name = table = errortxt[last_quote_begin+1:last_quote_end].strip()
|
||||
model = table.replace("_",".")
|
||||
if model in registry:
|
||||
model_class = registry[model]
|
||||
model_name = model_class._description or model_class._name
|
||||
msg += _('\n\n[object with reference: %s - %s]') % (model_name, model)
|
||||
# Get corresponding model and field
|
||||
model = field = None
|
||||
for name, rclass in registry.items():
|
||||
if inst.diag.table_name == rclass._table:
|
||||
model = rclass
|
||||
field = model._fields.get(inst.diag.column_name)
|
||||
break
|
||||
if inst.pgcode == errorcodes.NOT_NULL_VIOLATION:
|
||||
# This is raised when a field is set with `required=True`. 2 cases:
|
||||
# - Create/update: a mandatory field is not set.
|
||||
# - Delete: another model has a not nullable using the deleted record.
|
||||
msg += '\n'
|
||||
msg += _(
|
||||
'- Create/update: a mandatory field is not set.\n'
|
||||
'- Delete: another model requires the record being deleted. If possible, archive it instead.'
|
||||
)
|
||||
if model:
|
||||
msg += '\n\n{} {} ({}), {} {} ({})'.format(
|
||||
_('Model:'), model._description, model._name,
|
||||
_('Field:'), field.string if field else _('Unknown'), field.name if field else _('Unknown'),
|
||||
)
|
||||
elif inst.pgcode == errorcodes.FOREIGN_KEY_VIOLATION:
|
||||
# This is raised when a field is set with `ondelete='restrict'`, at
|
||||
# unlink only.
|
||||
msg += _(' another model requires the record being deleted. If possible, archive it instead.')
|
||||
constraint = inst.diag.constraint_name
|
||||
if model or constraint:
|
||||
msg += '\n\n{} {} ({}), {} {}'.format(
|
||||
_('Model:'), model._description if model else _('Unknown'), model._name if model else _('Unknown'),
|
||||
_('Constraint:'), constraint if constraint else _('Unknown'),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
raise ValidationError(msg)
|
||||
|
||||
Reference in New Issue
Block a user