[MERGE] forward port branch 10.0 up to 31ebb07773

This commit is contained in:
Christophe Simonis
2017-03-17 17:57:52 +01:00
27 changed files with 380 additions and 81 deletions
+1 -2
View File
@@ -30,7 +30,6 @@ CRM_LEAD_FIELDS_TO_MERGE = [
'city',
'contact_name',
'description',
'email',
'fax',
'mobile',
'partner_name',
@@ -468,7 +467,7 @@ class Lead(models.Model):
body = [title]
fields = self.env['ir.model.fields'].search([('name', 'in', fields or []), ('model_id.model', '=', self._name)])
for field in fields:
value = self[field.name]
value = getattr(self, field.name, False)
if field.ttype == 'selection':
value = dict(field.get_values(self.env)).get(value, value)
elif field.ttype == 'many2one':
@@ -8,7 +8,7 @@
<field name="arch" type="xml">
<pivot string="Pipeline Analysis" disable_linking="True">
<field name="create_date" interval="month" type="col"/>
<field name="stage_name" type="row"/>
<field name="stage_id" type="row"/>
<field name="expected_revenue" type="measure"/>
</pivot>
</field>
@@ -31,7 +31,7 @@
<field name="model">crm.opportunity.report</field>
<field name="arch" type="xml">
<graph string="Pipeline Analysis" stacked="True">
<field name="stage_name" type="row"/>
<field name="stage_id" type="row"/>
<field name="date_deadline" type="row" interval="month"/>
<field name="expected_revenue" type="measure"/>
</graph>
@@ -9,6 +9,7 @@
}
.kanban_html_preview {
pointer-events: none;
width: 600px;
-webkit-transform: scale(.50);
-ms-transform: scale(.50);
+6
View File
@@ -3168,6 +3168,12 @@ msgstr ""
msgid "You cannot cancel a stock move having already consumed material"
msgstr ""
#. module: mrp
#: code:addons/mrp/models/stock_move.py:40
#, python-format
msgid "You cannot use the same serial number in two different lines."
msgstr ""
#. module: mrp
#: code:addons/mrp/wizard/change_production_qty.py:44
#, python-format
+10
View File
@@ -28,6 +28,16 @@ class StockMoveLots(models.Model):
done_move = fields.Boolean('Move Done', related='move_id.is_done', store=True) # TDE FIXME: naming
plus_visible = fields.Boolean("Plus Visible", compute='_compute_plus')
@api.one
@api.constrains('lot_id')
def _check_lot_id(self):
if self.move_id.product_id.tracking == 'serial':
lots = set([])
for move_lot in self.move_id.move_lot_ids.filtered(lambda r: not r.lot_produced_id):
if move_lot.lot_id in lots:
raise exceptions.UserError(_('You cannot use the same serial number in two different lines.'))
lots.add(move_lot.lot_id)
def _compute_plus(self):
for movelot in self:
if movelot.move_id.product_id.tracking == 'serial':
+2 -2
View File
@@ -125,8 +125,8 @@ class MrpProductProduce(models.TransientModel):
#Possibly the entire move is selected
remaining_qty = movelots.quantity - movelots.quantity_done
if remaining_qty > 0:
new_move_lot = movelots.copy()
new_move_lot.write({'quantity':movelots.quantity_done, 'lot_produced_id': self.lot_id.id})
default = {'quantity': movelots.quantity_done, 'lot_produced_id': self.lot_id.id}
new_move_lot = movelots.copy(default=default)
movelots.write({'quantity': remaining_qty, 'quantity_done': 0})
else:
movelots.write({'lot_produced_id': self.lot_id.id})
+2 -2
View File
@@ -453,7 +453,7 @@ class RepairLine(models.Model):
if not self.to_invoice:
self.price_subtotal = 0.0
else:
taxes = self.env['account.tax'].compute_all(self.price_unit, self.repair_id.pricelist_id.currency_id, self.product_uom_qty, self.product_id, self.repair_id.partner_id)
taxes = self.tax_id.compute_all(self.price_unit, self.repair_id.pricelist_id.currency_id, self.product_uom_qty, self.product_id, self.repair_id.partner_id)
self.price_subtotal = taxes['total_excluded']
@api.onchange('type', 'repair_id')
@@ -536,7 +536,7 @@ class RepairFee(models.Model):
if not self.to_invoice:
self.price_subtotal = 0.0
else:
taxes = self.env['account.tax'].compute_all(self.price_unit, self.repair_id.pricelist_id.currency_id, self.product_uom_qty, self.product_id, self.repair_id.partner_id)
taxes = self.tax_id.compute_all(self.price_unit, self.repair_id.pricelist_id.currency_id, self.product_uom_qty, self.product_id, self.repair_id.partner_id)
self.price_subtotal = taxes['total_excluded']
@api.onchange('repair_id', 'product_id', 'product_uom_qty')
+2 -2
View File
@@ -250,8 +250,8 @@ class SaleOrder(models.Model):
@api.model
def create(self, vals):
if vals.get('name', 'New') == 'New':
vals['name'] = self.env['ir.sequence'].next_by_code('sale.order') or 'New'
if vals.get('name', _('New')) == _('New'):
vals['name'] = self.env['ir.sequence'].next_by_code('sale.order') or _('New')
# Makes sure partner_invoice_id', 'partner_shipping_id' and 'pricelist_id' are defined
if any(f not in vals for f in ['partner_invoice_id', 'partner_shipping_id', 'pricelist_id']):
+2 -1
View File
@@ -183,7 +183,8 @@ class SaleOrderLine(models.Model):
qty = 0.0
for move in self.procurement_ids.mapped('move_ids').filtered(lambda r: r.state == 'done' and not r.scrapped):
if move.location_dest_id.usage == "customer":
qty += move.product_uom._compute_quantity(move.product_uom_qty, self.product_uom)
if not move.origin_returned_move_id:
qty += move.product_uom._compute_quantity(move.product_uom_qty, self.product_uom)
elif move.location_dest_id.usage == "internal" and move.to_refund_so:
qty -= move.product_uom._compute_quantity(move.product_uom_qty, self.product_uom)
return qty
+42 -16
View File
@@ -385,31 +385,57 @@ class InventoryLine(models.Model):
('owner_id', '=', self.partner_id.id),
('package_id', '=', self.package_id.id)])
def _get_move_values(self, qty, location_id, location_dest_id):
self.ensure_one()
return {
'name': _('INV:') + (self.inventory_id.name or ''),
'product_id': self.product_id.id,
'product_uom': self.product_uom_id.id,
'product_uom_qty': qty,
'date': self.inventory_id.date,
'company_id': self.inventory_id.company_id.id,
'inventory_id': self.inventory_id.id,
'state': 'confirmed',
'restrict_lot_id': self.prod_lot_id.id,
'restrict_partner_id': self.partner_id.id,
'location_id': location_id,
'location_dest_id': location_dest_id,
}
def _fixup_negative_quants(self):
""" This will handle the irreconciable quants created by a force availability followed by a
return. When generating the moves of an inventory line, we look for quants of this line's
product created to compensate a force availability. If there are some and if the quant
which it is propagated from is still in the same location, we move it to the inventory
adjustment location before getting it back. Getting the quantity from the inventory
location will allow the negative quant to be compensated.
"""
self.ensure_one()
for quant in self._get_quants().filtered(lambda q: q.propagated_from_id.location_id.id == self.location_id.id):
# send the quantity to the inventory adjustment location
move_out_vals = self._get_move_values(quant.qty, self.location_id.id, self.product_id.property_stock_inventory.id)
move_out = self.env['stock.move'].create(move_out_vals)
self.env['stock.quant'].quants_reserve([(quant, quant.qty)], move_out)
move_out.action_done()
# get back the quantity from the inventory adjustment location
move_in_vals = self._get_move_values(quant.qty, self.product_id.property_stock_inventory.id, self.location_id.id)
move_in = self.env['stock.move'].create(move_in_vals)
move_in.action_done()
def _generate_moves(self):
moves = self.env['stock.move']
Quant = self.env['stock.quant']
for line in self:
line._fixup_negative_quants()
if float_utils.float_compare(line.theoretical_qty, line.product_qty, precision_rounding=line.product_id.uom_id.rounding) == 0:
continue
diff = line.theoretical_qty - line.product_qty
vals = {
'name': _('INV:') + (line.inventory_id.name or ''),
'product_id': line.product_id.id,
'product_uom': line.product_uom_id.id,
'date': line.inventory_id.date,
'company_id': line.inventory_id.company_id.id,
'inventory_id': line.inventory_id.id,
'state': 'confirmed',
'restrict_lot_id': line.prod_lot_id.id,
'restrict_partner_id': line.partner_id.id}
if diff < 0: # found more than expected
vals['location_id'] = line.product_id.property_stock_inventory.id
vals['location_dest_id'] = line.location_id.id
vals['product_uom_qty'] = abs(diff)
vals = self._get_move_values(abs(diff), line.product_id.property_stock_inventory.id, line.location_id.id)
else:
vals['location_id'] = line.location_id.id
vals['location_dest_id'] = line.product_id.property_stock_inventory.id
vals['product_uom_qty'] = diff
vals = self._get_move_values(abs(diff), line.location_id.id, line.product_id.property_stock_inventory.id)
move = moves.create(vals)
if diff > 0:
+4 -2
View File
@@ -245,8 +245,10 @@ class Picking(models.Model):
picking_type_code = fields.Selection([
('incoming', 'Vendors'),
('outgoing', 'Customers'),
('internal', 'Internal')], related='picking_type_id.code')
picking_type_entire_packs = fields.Boolean(related='picking_type_id.show_entire_packs')
('internal', 'Internal')], related='picking_type_id.code',
readonly=True)
picking_type_entire_packs = fields.Boolean(related='picking_type_id.show_entire_packs',
readonly=True)
quant_reserved_exist = fields.Boolean(
'Has quants already reserved', compute='_compute_quant_reserved_exist',
+1 -1
View File
@@ -548,7 +548,7 @@ class Warehouse(models.Model):
routes = self.env['stock.location.route'].search([('supplied_wh_id', 'in', self.ids)])
self.env['procurement.rule'].search([
'&', ('route_id', 'in', routes.ids),
('location_src_id.usage', '=', 'transit')]).write({'location_id': new_location})
('location_src_id.usage', '=', 'transit')]).write({'location_id': new_location.id})
@api.multi
def _update_routes(self):
+128
View File
@@ -103,6 +103,134 @@ class TestInventory(TestStockCommon):
self.assertEqual(product.qty_available, 8.0)
self.assertEqual(product.with_context(location=self.env.ref('stock.stock_location_customers').id).qty_available, 7.0)
def test_inventory_adjustment_and_negative_quants_1(self):
"""Make sure negative quants from returns get wiped out with an inventory adjustment"""
productA = self.env['product.product'].create({'name': 'Product A', 'type': 'product'})
stock_location = self.env.ref('stock.stock_location_stock')
customer_location = self.env.ref('stock.stock_location_customers')
location_loss = self.env.ref('stock.location_inventory')
# Create a picking out and force availability
picking_out = self.env['stock.picking'].create({
'partner_id': self.env.ref('base.res_partner_2').id,
'picking_type_id': self.env.ref('stock.picking_type_out').id,
'location_id': stock_location.id,
'location_dest_id': customer_location.id,
})
self.env['stock.move'].create({
'name': productA.name,
'product_id': productA.id,
'product_uom_qty': 1,
'product_uom': productA.uom_id.id,
'picking_id': picking_out.id,
'location_id': stock_location.id,
'location_dest_id': customer_location.id,
})
picking_out.action_confirm()
picking_out.force_assign()
picking_out.do_transfer()
# Create return picking for all goods
default_data = self.env['stock.return.picking']\
.with_context(active_ids=picking_out.ids, active_id=picking_out.ids[0])\
.default_get([
'move_dest_exists',
'original_location_id',
'product_return_moves',
'parent_location_id',
'location_id',
])
return_wiz = self.env['stock.return.picking']\
.with_context(active_ids=picking_out.ids, active_id=picking_out.ids[0])\
.create(default_data)
res = return_wiz.create_returns()
return_pick = self.env['stock.picking'].browse(res['res_id'])
return_pick.action_assign()
return_pick.do_transfer()
# Make an inventory adjustment to set the quantity to 0
inventory = self.env['stock.inventory'].create({
'name': 'Starting for product_1',
'filter': 'product',
'location_id': stock_location.id,
'product_id': productA.id,
})
inventory.prepare_inventory()
self.assertEqual(len(inventory.line_ids), 1, "Wrong inventory lines generated.")
self.assertEqual(inventory.line_ids.theoretical_qty, 0, "Theoretical quantity should be zero.")
inventory.action_done()
# The inventory adjustment should have created two moves
self.assertEqual(len(inventory.move_ids), 2)
quantity = inventory.move_ids.mapped('product_qty')
self.assertEqual(quantity, [1, 1], "Moves created with wrong quantity.")
location_ids = inventory.move_ids.mapped('location_id').ids
self.assertEqual(set(location_ids), {stock_location.id, location_loss.id})
# There should be no quant in the stock location
quants = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', stock_location.id)])
self.assertEqual(len(quants), 0)
# There should be one quant in the inventory loss location
quant = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', location_loss.id)])
self.assertEqual(len(quant), 1)
self.assertEqual(quant.qty, 1)
def test_inventory_adjustment_and_negative_quants_2(self):
"""Make sure negative quants get wiped out with an inventory adjustment"""
productA = self.env['product.product'].create({'name': 'Product A', 'type': 'product'})
stock_location = self.env.ref('stock.stock_location_stock')
customer_location = self.env.ref('stock.stock_location_customers')
location_loss = self.env.ref('stock.location_inventory')
# Create a picking out and force availability
picking_out = self.env['stock.picking'].create({
'partner_id': self.env.ref('base.res_partner_2').id,
'picking_type_id': self.env.ref('stock.picking_type_out').id,
'location_id': stock_location.id,
'location_dest_id': customer_location.id,
})
self.env['stock.move'].create({
'name': productA.name,
'product_id': productA.id,
'product_uom_qty': 1,
'product_uom': productA.uom_id.id,
'picking_id': picking_out.id,
'location_id': stock_location.id,
'location_dest_id': customer_location.id,
})
picking_out.action_confirm()
picking_out.force_assign()
picking_out.do_transfer()
# Make an inventory adjustment to set the quantity to 0
inventory = self.env['stock.inventory'].create({
'name': 'Starting for product_1',
'filter': 'product',
'location_id': stock_location.id,
'product_id': productA.id,
})
inventory.prepare_inventory()
self.assertEqual(len(inventory.line_ids), 1, "Wrong inventory lines generated.")
self.assertEqual(inventory.line_ids.theoretical_qty, -1, "Theoretical quantity should be -1.")
inventory.line_ids.product_qty = 0 # Put the quantity back to 0
inventory.action_done()
# The inventory adjustment should have created one
self.assertEqual(len(inventory.move_ids), 1)
quantity = inventory.move_ids.mapped('product_qty')
self.assertEqual(quantity, [1], "Moves created with wrong quantity.")
location_ids = inventory.move_ids.mapped('location_id').ids
self.assertEqual(set(location_ids), {location_loss.id})
# There should be no quant in the stock location
quants = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', stock_location.id)])
self.assertEqual(len(quants), 0)
# There should be no quant in the inventory loss location
quant = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', location_loss.id)])
self.assertEqual(len(quant), 0)
class TestResupply(TestStockCommon):
def setUp(self):
+1 -1
View File
@@ -179,7 +179,7 @@
.o_form_invalid {
&.o_form_input, .o_form_input, &.o_form_textarea, .o_form_textarea {
border-color: @brand-danger;
border-color: @brand-danger!important; // enterprise o_form_required rule overrides this without !important
}
}
@@ -642,8 +642,8 @@ var CalendarView = View.extend({
date_stop = m_start.toDate();
}
var r = {
'start': moment(date_start).format('YYYY-MM-DD HH:mm:ss'),
'end': moment(date_stop).format('YYYY-MM-DD HH:mm:ss'),
'start': moment(date_start).toISOString(),
'end': moment(date_stop).toISOString(),
'title': the_title,
'allDay': (this.fields[this.date_start].type == 'date' || (this.all_day && evt[this.all_day]) || false),
'id': evt.id,
+13 -5
View File
@@ -2313,11 +2313,19 @@ eventHandler.modules.popover.update = function ($popover, oStyle, isAirMode) {
eventHandler.modules.clipboard.attach = function(layoutInfo) {
var $editable = layoutInfo.editable();
$editable.on('paste', function(e) {
e.preventDefault();
$editable.data('NoteHistory').recordUndo($editable);
var pastedText = ((e.originalEvent || e).clipboardData || window.clipboardData).getData('Text');
var formattedText = pastedText.replace(/([^.!?:;])\r?\n/g, "$1").trim(); // Remove linebreaks which are not at the end of a sentence
document.execCommand("insertText", false, formattedText);
var clipboardData = ((e.originalEvent || e).clipboardData || window.clipboardData);
// Change nothing if pasting html (copy from text editor / web / ...) or
// if clipboardData is not available (IE / ...)
if (clipboardData && clipboardData.types && clipboardData.types.length === 1 && clipboardData.types[0] === "text/plain") {
e.preventDefault();
$editable.data('NoteHistory').recordUndo($editable); // FIXME
var pastedText = clipboardData.getData("text/plain");
// Try removing linebreaks which are not really linebreaks (in a PDF,
// when a sentence goes over the next line, copying it considers it
// a linebreak for example).
var formattedText = pastedText.replace(/([\w-])\r?\n([\w-])/g, "$1 $2").trim();
document.execCommand("insertText", false, formattedText);
}
});
};
+22 -14
View File
@@ -98,20 +98,28 @@ var getMatchedCSSRules = function (a) {
if (style.display === 'block') {
delete style.display;
}
if (style['margin-top']) {
style.margin = (style['margin-top'] || 0) + ' ' + (style['margin-right'] || 0) + ' ' + (style['margin-bottom'] || 0) + ' ' + (style['margin-left'] || 0);
delete style['margin-top'];
delete style['margin-right'];
delete style['margin-bottom'];
delete style['margin-left'];
}
if (style['padding-top']) {
style.padding = (style['padding-top'] || 0) + ' ' + (style['padding-right'] || 0) + ' ' + (style['padding-bottom'] || 0) + ' ' + (style['padding-left'] || 0);
delete style['padding-top'];
delete style['padding-right'];
delete style['padding-bottom'];
delete style['padding-left'];
}
_.each(['margin', 'padding'], function(p) {
if (style[p+'-top'] || style[p+'-right'] || style[p+'-bottom'] || style[p+'-left']) {
if (style[p+'-top'] === style[p+'-right'] && style[p+'-top'] === style[p+'-bottom'] && style[p+'-top'] === style[p+'-left']) {
// keep => property: [top/right/bottom/left value];
style[p] = style[p+'-top'];
}
else {
// keep => property: [top value] [right value] [bottom value] [left value];
style[p] = (style[p+'-top'] || 0) + ' ' + (style[p+'-right'] || 0) + ' ' + (style[p+'-bottom'] || 0) + ' ' + (style[p+'-left'] || 0);
if (style[p].indexOf('inherit') !== -1 || style[p].indexOf('initial') !== -1) {
// keep => property-top: [top value]; property-right: [right value]; property-bottom: [bottom value]; property-left: [left value];
delete style[p];
return;
}
}
delete style[p+'-top'];
delete style[p+'-right'];
delete style[p+'-bottom'];
delete style[p+'-left'];
}
});
return style;
};
+4 -4
View File
@@ -38,11 +38,11 @@
-webkit-border-radius: 3px;
border-radius: 3px;
background: white;">
<option value="en_US" t-att-selected="'en_US' == widget.lang ? 'selected' : null">
<t t-if="!widget.get('effective_readonly')">English (edit mode)</t>
<t t-if="widget.get('effective_readonly')">English</t>
</option>
<t t-foreach="widget.languages" t-as="lang">
<option value="en_US" t-att-selected="'en_US' == widget.lang ? 'selected' : null">
<t t-if="!widget.get('effective_readonly')">English (edit mode)</t>
<t t-if="widget.get('effective_readonly')">English</t>
</option>
<option t-att-value="lang.code" t-att-selected="lang.code == widget.lang ? 'selected' : null"><t t-esc="lang.name"/></option>
</t>
</select>
@@ -63,7 +63,7 @@ var KanbanView = View.extend({
this.qweb = new QWeb(session.debug, {_s: session.origin});
this.limit = this.options.limit || parseInt(this.fields_view.arch.attrs.limit, 10) || 40;
this.fields = {};
this.fields = this.fields_view.fields;
this.fields_keys = _.keys(this.fields_view.fields);
this.grouped = undefined;
this.group_by_field = undefined;
@@ -105,7 +105,7 @@ var KanbanView = View.extend({
this.qweb.add_template(utils.json_node_to_xml(child));
break;
} else if (child.tag === 'field') {
var ftype = child.attrs.widget || this.fields_view.fields[child.attrs.name].type;
var ftype = child.attrs.widget || this.fields[child.attrs.name].type;
if(ftype === "many2many" && "context" in child.attrs) {
this.m2m_context[child.attrs.name] = child.attrs.context;
}
@@ -122,7 +122,7 @@ var KanbanView = View.extend({
do_search: function(domain, context, group_by) {
var self = this;
var group_by_field = group_by[0] || this.default_group_by;
var field = this.fields_view.fields[group_by_field];
var field = this.fields[group_by_field];
var options = {};
var fields_def;
if (field === undefined) {
@@ -248,7 +248,7 @@ var KanbanView = View.extend({
_.each(groups, function (group) {
var value = group.attributes.value;
group.id = value instanceof Array ? value[0] : value;
var field = self.fields_view.fields[options.group_by_field];
var field = self.fields[options.group_by_field];
if (field && field.type === "selection") {
value= _.find(field.selection, function (s) { return s[0] === group.id; });
}
@@ -304,7 +304,7 @@ var KanbanView = View.extend({
return this._super(action);
},
has_active_field: function() {
return this.fields_view.fields.active;
return this.fields.active;
},
_is_quick_create_enabled: function() {
if (!this.quick_creatable || !this.is_action_enabled('create'))
@@ -465,7 +465,7 @@ var KanbanView = View.extend({
var self = this;
// Drag'n'drop activation/deactivation
var group_by_field_attrs = this.fields_view.fields[this.group_by_field] || this.fields[this.group_by_field];
var group_by_field_attrs = this.fields[this.group_by_field];
// Deactivate the drag'n'drop if:
// - field is a date or datetime since we group by month
+3
View File
@@ -139,6 +139,9 @@ class IrModel(models.Model):
# prevent screwing up fields that depend on these models' fields
model.field_id._prepare_update()
imc = self.env['ir.model.constraint'].search([('model', 'in', self.ids)])
imc.unlink()
self._drop_table()
res = super(IrModel, self).unlink()
+3 -3
View File
@@ -122,7 +122,7 @@ class IntegerConverter(models.AbstractModel):
@api.model
def value_to_html(self, value, options):
return unicodifier(self.user_lang().format('%d', value, grouping=True))
return unicodifier(self.user_lang().format('%d', value, grouping=True).replace(r'-', u'\u2011'))
class FloatConverter(models.AbstractModel):
@@ -142,7 +142,7 @@ class FloatConverter(models.AbstractModel):
value = float_utils.float_round(value, precision_digits=precision)
fmt = '%.{precision}f'.format(precision=precision)
formatted = self.user_lang().format(fmt, value, grouping=True)
formatted = self.user_lang().format(fmt, value, grouping=True).replace(r'-', u'\u2011')
# %f does not strip trailing zeroes. %g does but its precision causes
# it to switch to scientific notation starting at a million *and* to
@@ -322,7 +322,7 @@ class MonetaryConverter(models.AbstractModel):
lang = self.user_lang()
formatted_amount = lang.format(fmt, display_currency.round(value),
grouping=True, monetary=True).replace(r' ', u'\N{NO-BREAK SPACE}')
grouping=True, monetary=True).replace(r' ', u'\N{NO-BREAK SPACE}').replace(r'-', u'\u2011')
pre = post = u''
if display_currency.position == 'before':
+1 -1
View File
@@ -177,7 +177,7 @@ class Currency(models.Model):
function += "if (arguments[1] === %s) { %s }" % (currency.id, return_str)
if (currency == company_currency):
company_currency_format = return_str
function = "if (arguments[1] === false || arguments[1] === undefined) {" + company_currency_format + " }" + function
function = "if (arguments[1] === false || arguments[1] === undefined) {" + company_currency_format + " }" + function
return function
def _select_companies_rates(self):
@@ -72,8 +72,8 @@ class TestFloatExport(TestBasicExport):
def test_float(self):
converter = self.get_converter('float')
value = converter(42.0)
self.assertEqual(value, "42.0")
value = converter(-42.0)
self.assertEqual(value, u"\u201142.0")
value = converter(42.0100)
self.assertEqual(value, "42.01")
@@ -115,12 +115,12 @@ class TestCurrencyExport(TestExport):
def test_currency_post(self):
currency = self.create(self.Currency, name="Test", symbol=u"test")
obj = self.create(self.Model, value=0.12)
obj = self.create(self.Model, value=-0.12)
converted = self.convert(obj, dest=currency)
self.assertEqual(
converted, '<span class="oe_currency_value">0.12</span>'
converted, '<span class="oe_currency_value">\u20110.12</span>'
u'\N{NO-BREAK SPACE}{symbol}'.format(
obj=obj,
symbol=currency.symbol.encode('utf-8')
@@ -5,6 +5,9 @@ access_message,test_new_api_message,test_new_api.model_test_new_api_message,,1,1
access_emailmessage,test_new_api_emailmessage,test_new_api.model_test_new_api_emailmessage,,1,1,1,1
access_multi,test_new_api_multi,test_new_api.model_test_new_api_multi,,1,1,1,1
access_multi_line,test_new_api_multi_line,test_new_api.model_test_new_api_multi_line,,1,1,1,1
access_creativework_edition,test_new_api_creativework_edition,model_test_new_api_creativework_edition,,1,1,1,1
access_creativework_book,test_new_api_creativework_book,model_test_new_api_creativework_book,,1,1,1,1
access_creativework_movie,test_new_api_creativework_movie,model_test_new_api_creativework_movie,,1,1,1,1
access_mixed,test_new_api_mixed,test_new_api.model_test_new_api_mixed,,1,1,1,1
access_domain_bool,access_domain_bool,model_domain_bool,,1,1,1,1
access_test_new_api_foo,access_test_new_api_foo,model_test_new_api_foo,,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
5 access_emailmessage test_new_api_emailmessage test_new_api.model_test_new_api_emailmessage 1 1 1 1
6 access_multi test_new_api_multi test_new_api.model_test_new_api_multi 1 1 1 1
7 access_multi_line test_new_api_multi_line test_new_api.model_test_new_api_multi_line 1 1 1 1
8 access_creativework_edition test_new_api_creativework_edition model_test_new_api_creativework_edition 1 1 1 1
9 access_creativework_book test_new_api_creativework_book model_test_new_api_creativework_book 1 1 1 1
10 access_creativework_movie test_new_api_creativework_movie model_test_new_api_creativework_movie 1 1 1 1
11 access_mixed test_new_api_mixed test_new_api.model_test_new_api_mixed 1 1 1 1
12 access_domain_bool access_domain_bool model_domain_bool 1 1 1 1
13 access_test_new_api_foo access_test_new_api_foo model_test_new_api_foo 1 1 1 1
+27
View File
@@ -199,6 +199,33 @@ class MultiLine(models.Model):
partner = fields.Many2one('res.partner')
class Edition(models.Model):
_name = 'test_new_api.creativework.edition'
name = fields.Char()
res_id = fields.Integer(required=True)
res_model_id = fields.Many2one('ir.model', required=True)
res_model = fields.Char(related='res_model_id.model', store=True)
class Book(models.Model):
_name = 'test_new_api.creativework.book'
name = fields.Char()
editions = fields.One2many(
'test_new_api.creativework.edition', 'res_id', domain=[('res_model', '=', _name)]
)
class Movie(models.Model):
_name = 'test_new_api.creativework.movie'
name = fields.Char()
editions = fields.One2many(
'test_new_api.creativework.edition', 'res_id', domain=[('res_model', '=', _name)]
)
class MixedModel(models.Model):
_name = 'test_new_api.mixed'
@@ -10,6 +10,36 @@ class One2manyCase(TransactionCase):
"name": "What is up?"
})
# data for One2many with inverse field Integer
self.Edition = self.env["test_new_api.creativework.edition"]
self.Book = self.env["test_new_api.creativework.book"]
self.Movie = self.env["test_new_api.creativework.movie"]
book_model_id = self.env['ir.model'].search([('model', '=', self.Book._name)]).id
movie_model_id = self.env['ir.model'].search([('model', '=', self.Movie._name)]).id
books_data = (
('Imaginary book', ()),
('Another imaginary book', ()),
('Nineteen Eighty Four', ('First edition', 'Fourth Edition'))
)
movies_data = (
('The Gold Rush', ('1925 (silent)', '1942')),
('Imaginary movie', ()),
('Another imaginary movie', ())
)
for name, editions in books_data:
book_id = self.Book.create({'name': name}).id
for edition in editions:
self.Edition.create({'res_model_id': book_model_id, 'name': edition, 'res_id': book_id})
for name, editions in movies_data:
movie_id = self.Movie.create({'name': name}).id
for edition in editions:
self.Edition.create({'res_model_id': movie_model_id, 'name': edition, 'res_id': movie_id})
def operations(self):
"""Run operations on o2m fields to check all works fine."""
# Check the lines first
@@ -63,3 +93,40 @@ class One2manyCase(TransactionCase):
"""Check lines created with RPC style and added in one step"""
self.multi.lines = [(0, 0, {'name': str(name)}) for name in range(10)]
self.operations()
def test_many2one_integer(self):
"""Test several models one2many with same inverse Integer field"""
# utility function to convert records to tuples with id,name
t = lambda records: records.mapped(lambda r: (r.id, r.name))
books = self.Book.search([])
books_with_edition = books.filtered(lambda r: r.editions)
movies = self.Movie.search([])
movies_without_edition = movies.filtered(lambda r: not r.editions)
movies_with_edition = movies.filtered(lambda r: r.editions)
movie_editions = movies_with_edition.mapped('editions')
one_movie_edition = movie_editions[0]
res_movies_without_edition = self.Movie.search([('editions', '=', False)])
self.assertItemsEqual(t(res_movies_without_edition), t(movies_without_edition))
res_movies_with_edition = self.Movie.search([('editions', '!=', False)])
self.assertItemsEqual(t(res_movies_with_edition), t(movies_with_edition))
res_books_with_movie_edition = self.Book.search([('editions', 'in', movie_editions.ids)])
self.assertFalse(t(res_books_with_movie_edition))
res_books_without_movie_edition = self.Book.search([('editions', 'not in', movie_editions.ids)])
self.assertItemsEqual(t(res_books_without_movie_edition), t(books_with_edition))
res_books_without_one_movie_edition = self.Book.search([('editions', 'not in', movie_editions[:1].ids)])
self.assertItemsEqual(t(res_books_without_one_movie_edition), t(books_with_edition))
res_books_with_one_movie_edition_name = self.Book.search([('editions', '=', movie_editions[:1].name)])
self.assertFalse(t(res_books_with_one_movie_edition_name))
res_books_without_one_movie_edition_name = self.Book.search([('editions', '!=', movie_editions[:1].name)])
self.assertItemsEqual(t(res_books_without_one_movie_edition_name), t(books_with_edition))
res_movies_not_of_edition_name = self.Movie.search([('editions', '!=', one_movie_edition.name)])
self.assertItemsEqual(t(res_movies_not_of_edition_name), t(movies.filtered(lambda r: one_movie_edition not in r.editions)))
+21 -11
View File
@@ -920,19 +920,23 @@ class expression(object):
elif field.type == 'one2many':
call_null = True
domain = field.domain
if callable(domain):
domain = domain(model)
is_integer_m2o = comodel._fields[field.inverse_name].type == 'integer'
if right is not False:
if isinstance(right, basestring):
op = {'!=': '=', 'not like': 'like', 'not ilike': 'ilike'}.get(operator, operator)
domain = field.domain
if callable(domain):
domain = domain(model)
ids2 = [x[0] for x in comodel.name_search(right, domain or [], op, limit=None)]
if ids2:
operator = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in'
elif isinstance(right, collections.Iterable):
ids2 = right
else:
ids2 = [right]
if isinstance(right, collections.Iterable):
ids2 = right
else:
ids2 = [right]
if ids2 and is_integer_m2o and domain:
ids2 = comodel.search([('id', 'in', ids2)] + domain).ids
if not ids2:
if operator in ['like', 'ilike', 'in', '=']:
@@ -945,7 +949,9 @@ class expression(object):
ids1 = select_from_where(cr, field.inverse_name, comodel._table, 'id', ids2, operator)
else:
recs = comodel.browse(ids2).sudo().with_context(prefetch_fields=False)
ids1 = recs.mapped(field.inverse_name).ids
ids1 = recs.mapped(field.inverse_name)
if not is_integer_m2o:
ids1 = ids1.ids
if ids1:
call_null = False
o2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in'
@@ -958,12 +964,16 @@ class expression(object):
if call_null:
o2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in'
# determine ids from field.inverse_name
if comodel._fields[field.inverse_name].store:
if comodel._fields[field.inverse_name].store and not (is_integer_m2o and domain):
ids1 = select_distinct_from_where_not_null(cr, field.inverse_name, comodel._table)
else:
domain = [(field.inverse_name, '!=', False)]
recs = comodel.search(domain).sudo().with_context(prefetch_fields=False)
ids1 = recs.mapped(field.inverse_name).ids
comodel_domain = [(field.inverse_name, '!=', False)]
if is_integer_m2o and domain:
comodel_domain += domain
recs = comodel.search(comodel_domain).sudo().with_context(prefetch_fields=False)
ids1 = recs.mapped(field.inverse_name)
if not is_integer_m2o:
ids1 = ids1.ids
push(create_substitution_leaf(leaf, ('id', o2m_op, ids1), model))
elif field.type == 'many2many':