[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', 'city',
'contact_name', 'contact_name',
'description', 'description',
'email',
'fax', 'fax',
'mobile', 'mobile',
'partner_name', 'partner_name',
@@ -468,7 +467,7 @@ class Lead(models.Model):
body = [title] body = [title]
fields = self.env['ir.model.fields'].search([('name', 'in', fields or []), ('model_id.model', '=', self._name)]) fields = self.env['ir.model.fields'].search([('name', 'in', fields or []), ('model_id.model', '=', self._name)])
for field in fields: for field in fields:
value = self[field.name] value = getattr(self, field.name, False)
if field.ttype == 'selection': if field.ttype == 'selection':
value = dict(field.get_values(self.env)).get(value, value) value = dict(field.get_values(self.env)).get(value, value)
elif field.ttype == 'many2one': elif field.ttype == 'many2one':
@@ -8,7 +8,7 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<pivot string="Pipeline Analysis" disable_linking="True"> <pivot string="Pipeline Analysis" disable_linking="True">
<field name="create_date" interval="month" type="col"/> <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"/> <field name="expected_revenue" type="measure"/>
</pivot> </pivot>
</field> </field>
@@ -31,7 +31,7 @@
<field name="model">crm.opportunity.report</field> <field name="model">crm.opportunity.report</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<graph string="Pipeline Analysis" stacked="True"> <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="date_deadline" type="row" interval="month"/>
<field name="expected_revenue" type="measure"/> <field name="expected_revenue" type="measure"/>
</graph> </graph>
@@ -9,6 +9,7 @@
} }
.kanban_html_preview { .kanban_html_preview {
pointer-events: none;
width: 600px; width: 600px;
-webkit-transform: scale(.50); -webkit-transform: scale(.50);
-ms-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" msgid "You cannot cancel a stock move having already consumed material"
msgstr "" 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 #. module: mrp
#: code:addons/mrp/wizard/change_production_qty.py:44 #: code:addons/mrp/wizard/change_production_qty.py:44
#, python-format #, 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 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') 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): def _compute_plus(self):
for movelot in self: for movelot in self:
if movelot.move_id.product_id.tracking == 'serial': 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 #Possibly the entire move is selected
remaining_qty = movelots.quantity - movelots.quantity_done remaining_qty = movelots.quantity - movelots.quantity_done
if remaining_qty > 0: if remaining_qty > 0:
new_move_lot = movelots.copy() default = {'quantity': movelots.quantity_done, 'lot_produced_id': self.lot_id.id}
new_move_lot.write({'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}) movelots.write({'quantity': remaining_qty, 'quantity_done': 0})
else: else:
movelots.write({'lot_produced_id': self.lot_id.id}) 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: if not self.to_invoice:
self.price_subtotal = 0.0 self.price_subtotal = 0.0
else: 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'] self.price_subtotal = taxes['total_excluded']
@api.onchange('type', 'repair_id') @api.onchange('type', 'repair_id')
@@ -536,7 +536,7 @@ class RepairFee(models.Model):
if not self.to_invoice: if not self.to_invoice:
self.price_subtotal = 0.0 self.price_subtotal = 0.0
else: 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'] self.price_subtotal = taxes['total_excluded']
@api.onchange('repair_id', 'product_id', 'product_uom_qty') @api.onchange('repair_id', 'product_id', 'product_uom_qty')
+2 -2
View File
@@ -250,8 +250,8 @@ class SaleOrder(models.Model):
@api.model @api.model
def create(self, vals): def create(self, vals):
if vals.get('name', 'New') == 'New': if vals.get('name', _('New')) == _('New'):
vals['name'] = self.env['ir.sequence'].next_by_code('sale.order') or '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 # 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']): 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 qty = 0.0
for move in self.procurement_ids.mapped('move_ids').filtered(lambda r: r.state == 'done' and not r.scrapped): 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": 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: 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) qty -= move.product_uom._compute_quantity(move.product_uom_qty, self.product_uom)
return qty return qty
+42 -16
View File
@@ -385,31 +385,57 @@ class InventoryLine(models.Model):
('owner_id', '=', self.partner_id.id), ('owner_id', '=', self.partner_id.id),
('package_id', '=', self.package_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): def _generate_moves(self):
moves = self.env['stock.move'] moves = self.env['stock.move']
Quant = self.env['stock.quant'] Quant = self.env['stock.quant']
for line in self: 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: if float_utils.float_compare(line.theoretical_qty, line.product_qty, precision_rounding=line.product_id.uom_id.rounding) == 0:
continue continue
diff = line.theoretical_qty - line.product_qty 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 if diff < 0: # found more than expected
vals['location_id'] = line.product_id.property_stock_inventory.id vals = self._get_move_values(abs(diff), line.product_id.property_stock_inventory.id, line.location_id.id)
vals['location_dest_id'] = line.location_id.id
vals['product_uom_qty'] = abs(diff)
else: else:
vals['location_id'] = line.location_id.id vals = self._get_move_values(abs(diff), line.location_id.id, line.product_id.property_stock_inventory.id)
vals['location_dest_id'] = line.product_id.property_stock_inventory.id
vals['product_uom_qty'] = diff
move = moves.create(vals) move = moves.create(vals)
if diff > 0: if diff > 0:
+4 -2
View File
@@ -245,8 +245,10 @@ class Picking(models.Model):
picking_type_code = fields.Selection([ picking_type_code = fields.Selection([
('incoming', 'Vendors'), ('incoming', 'Vendors'),
('outgoing', 'Customers'), ('outgoing', 'Customers'),
('internal', 'Internal')], related='picking_type_id.code') ('internal', 'Internal')], related='picking_type_id.code',
picking_type_entire_packs = fields.Boolean(related='picking_type_id.show_entire_packs') readonly=True)
picking_type_entire_packs = fields.Boolean(related='picking_type_id.show_entire_packs',
readonly=True)
quant_reserved_exist = fields.Boolean( quant_reserved_exist = fields.Boolean(
'Has quants already reserved', compute='_compute_quant_reserved_exist', '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)]) routes = self.env['stock.location.route'].search([('supplied_wh_id', 'in', self.ids)])
self.env['procurement.rule'].search([ self.env['procurement.rule'].search([
'&', ('route_id', 'in', routes.ids), '&', ('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 @api.multi
def _update_routes(self): def _update_routes(self):
+128
View File
@@ -103,6 +103,134 @@ class TestInventory(TestStockCommon):
self.assertEqual(product.qty_available, 8.0) 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) 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): class TestResupply(TestStockCommon):
def setUp(self): def setUp(self):
+1 -1
View File
@@ -179,7 +179,7 @@
.o_form_invalid { .o_form_invalid {
&.o_form_input, .o_form_input, &.o_form_textarea, .o_form_textarea { &.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(); date_stop = m_start.toDate();
} }
var r = { var r = {
'start': moment(date_start).format('YYYY-MM-DD HH:mm:ss'), 'start': moment(date_start).toISOString(),
'end': moment(date_stop).format('YYYY-MM-DD HH:mm:ss'), 'end': moment(date_stop).toISOString(),
'title': the_title, 'title': the_title,
'allDay': (this.fields[this.date_start].type == 'date' || (this.all_day && evt[this.all_day]) || false), 'allDay': (this.fields[this.date_start].type == 'date' || (this.all_day && evt[this.all_day]) || false),
'id': evt.id, '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) { eventHandler.modules.clipboard.attach = function(layoutInfo) {
var $editable = layoutInfo.editable(); var $editable = layoutInfo.editable();
$editable.on('paste', function(e) { $editable.on('paste', function(e) {
e.preventDefault(); var clipboardData = ((e.originalEvent || e).clipboardData || window.clipboardData);
$editable.data('NoteHistory').recordUndo($editable); // Change nothing if pasting html (copy from text editor / web / ...) or
var pastedText = ((e.originalEvent || e).clipboardData || window.clipboardData).getData('Text'); // if clipboardData is not available (IE / ...)
var formattedText = pastedText.replace(/([^.!?:;])\r?\n/g, "$1").trim(); // Remove linebreaks which are not at the end of a sentence if (clipboardData && clipboardData.types && clipboardData.types.length === 1 && clipboardData.types[0] === "text/plain") {
document.execCommand("insertText", false, formattedText); 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') { if (style.display === 'block') {
delete style.display; 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); _.each(['margin', 'padding'], function(p) {
delete style['margin-top']; if (style[p+'-top'] || style[p+'-right'] || style[p+'-bottom'] || style[p+'-left']) {
delete style['margin-right']; if (style[p+'-top'] === style[p+'-right'] && style[p+'-top'] === style[p+'-bottom'] && style[p+'-top'] === style[p+'-left']) {
delete style['margin-bottom']; // keep => property: [top/right/bottom/left value];
delete style['margin-left']; style[p] = style[p+'-top'];
} }
if (style['padding-top']) { else {
style.padding = (style['padding-top'] || 0) + ' ' + (style['padding-right'] || 0) + ' ' + (style['padding-bottom'] || 0) + ' ' + (style['padding-left'] || 0); // keep => property: [top value] [right value] [bottom value] [left value];
delete style['padding-top']; style[p] = (style[p+'-top'] || 0) + ' ' + (style[p+'-right'] || 0) + ' ' + (style[p+'-bottom'] || 0) + ' ' + (style[p+'-left'] || 0);
delete style['padding-right']; if (style[p].indexOf('inherit') !== -1 || style[p].indexOf('initial') !== -1) {
delete style['padding-bottom']; // keep => property-top: [top value]; property-right: [right value]; property-bottom: [bottom value]; property-left: [left value];
delete style['padding-left']; delete style[p];
} return;
}
}
delete style[p+'-top'];
delete style[p+'-right'];
delete style[p+'-bottom'];
delete style[p+'-left'];
}
});
return style; return style;
}; };
+4 -4
View File
@@ -38,11 +38,11 @@
-webkit-border-radius: 3px; -webkit-border-radius: 3px;
border-radius: 3px; border-radius: 3px;
background: white;"> 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"> <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> <option t-att-value="lang.code" t-att-selected="lang.code == widget.lang ? 'selected' : null"><t t-esc="lang.name"/></option>
</t> </t>
</select> </select>
@@ -63,7 +63,7 @@ var KanbanView = View.extend({
this.qweb = new QWeb(session.debug, {_s: session.origin}); this.qweb = new QWeb(session.debug, {_s: session.origin});
this.limit = this.options.limit || parseInt(this.fields_view.arch.attrs.limit, 10) || 40; 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.fields_keys = _.keys(this.fields_view.fields);
this.grouped = undefined; this.grouped = undefined;
this.group_by_field = undefined; this.group_by_field = undefined;
@@ -105,7 +105,7 @@ var KanbanView = View.extend({
this.qweb.add_template(utils.json_node_to_xml(child)); this.qweb.add_template(utils.json_node_to_xml(child));
break; break;
} else if (child.tag === 'field') { } 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) { if(ftype === "many2many" && "context" in child.attrs) {
this.m2m_context[child.attrs.name] = child.attrs.context; this.m2m_context[child.attrs.name] = child.attrs.context;
} }
@@ -122,7 +122,7 @@ var KanbanView = View.extend({
do_search: function(domain, context, group_by) { do_search: function(domain, context, group_by) {
var self = this; var self = this;
var group_by_field = group_by[0] || this.default_group_by; 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 options = {};
var fields_def; var fields_def;
if (field === undefined) { if (field === undefined) {
@@ -248,7 +248,7 @@ var KanbanView = View.extend({
_.each(groups, function (group) { _.each(groups, function (group) {
var value = group.attributes.value; var value = group.attributes.value;
group.id = value instanceof Array ? value[0] : 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") { if (field && field.type === "selection") {
value= _.find(field.selection, function (s) { return s[0] === group.id; }); value= _.find(field.selection, function (s) { return s[0] === group.id; });
} }
@@ -304,7 +304,7 @@ var KanbanView = View.extend({
return this._super(action); return this._super(action);
}, },
has_active_field: function() { has_active_field: function() {
return this.fields_view.fields.active; return this.fields.active;
}, },
_is_quick_create_enabled: function() { _is_quick_create_enabled: function() {
if (!this.quick_creatable || !this.is_action_enabled('create')) if (!this.quick_creatable || !this.is_action_enabled('create'))
@@ -465,7 +465,7 @@ var KanbanView = View.extend({
var self = this; var self = this;
// Drag'n'drop activation/deactivation // 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: // Deactivate the drag'n'drop if:
// - field is a date or datetime since we group by month // - 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 # prevent screwing up fields that depend on these models' fields
model.field_id._prepare_update() model.field_id._prepare_update()
imc = self.env['ir.model.constraint'].search([('model', 'in', self.ids)])
imc.unlink()
self._drop_table() self._drop_table()
res = super(IrModel, self).unlink() res = super(IrModel, self).unlink()
+3 -3
View File
@@ -122,7 +122,7 @@ class IntegerConverter(models.AbstractModel):
@api.model @api.model
def value_to_html(self, value, options): 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): class FloatConverter(models.AbstractModel):
@@ -142,7 +142,7 @@ class FloatConverter(models.AbstractModel):
value = float_utils.float_round(value, precision_digits=precision) value = float_utils.float_round(value, precision_digits=precision)
fmt = '%.{precision}f'.format(precision=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 # %f does not strip trailing zeroes. %g does but its precision causes
# it to switch to scientific notation starting at a million *and* to # it to switch to scientific notation starting at a million *and* to
@@ -322,7 +322,7 @@ class MonetaryConverter(models.AbstractModel):
lang = self.user_lang() lang = self.user_lang()
formatted_amount = lang.format(fmt, display_currency.round(value), 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'' pre = post = u''
if display_currency.position == 'before': 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) function += "if (arguments[1] === %s) { %s }" % (currency.id, return_str)
if (currency == company_currency): if (currency == company_currency):
company_currency_format = return_str 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 return function
def _select_companies_rates(self): def _select_companies_rates(self):
@@ -72,8 +72,8 @@ class TestFloatExport(TestBasicExport):
def test_float(self): def test_float(self):
converter = self.get_converter('float') converter = self.get_converter('float')
value = converter(42.0) value = converter(-42.0)
self.assertEqual(value, "42.0") self.assertEqual(value, u"\u201142.0")
value = converter(42.0100) value = converter(42.0100)
self.assertEqual(value, "42.01") self.assertEqual(value, "42.01")
@@ -115,12 +115,12 @@ class TestCurrencyExport(TestExport):
def test_currency_post(self): def test_currency_post(self):
currency = self.create(self.Currency, name="Test", symbol=u"test") 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) converted = self.convert(obj, dest=currency)
self.assertEqual( 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( u'\N{NO-BREAK SPACE}{symbol}'.format(
obj=obj, obj=obj,
symbol=currency.symbol.encode('utf-8') 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_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,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_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_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_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 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') 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): class MixedModel(models.Model):
_name = 'test_new_api.mixed' _name = 'test_new_api.mixed'
@@ -10,6 +10,36 @@ class One2manyCase(TransactionCase):
"name": "What is up?" "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): def operations(self):
"""Run operations on o2m fields to check all works fine.""" """Run operations on o2m fields to check all works fine."""
# Check the lines first # Check the lines first
@@ -63,3 +93,40 @@ class One2manyCase(TransactionCase):
"""Check lines created with RPC style and added in one step""" """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.multi.lines = [(0, 0, {'name': str(name)}) for name in range(10)]
self.operations() 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': elif field.type == 'one2many':
call_null = True 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 right is not False:
if isinstance(right, basestring): if isinstance(right, basestring):
op = {'!=': '=', 'not like': 'like', 'not ilike': 'ilike'}.get(operator, operator) 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)] ids2 = [x[0] for x in comodel.name_search(right, domain or [], op, limit=None)]
if ids2: if ids2:
operator = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in' operator = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in'
elif isinstance(right, collections.Iterable):
ids2 = right
else: 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 not ids2:
if operator in ['like', 'ilike', 'in', '=']: 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) ids1 = select_from_where(cr, field.inverse_name, comodel._table, 'id', ids2, operator)
else: else:
recs = comodel.browse(ids2).sudo().with_context(prefetch_fields=False) 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: if ids1:
call_null = False call_null = False
o2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in' o2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in'
@@ -958,12 +964,16 @@ class expression(object):
if call_null: if call_null:
o2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in' o2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in'
# determine ids from field.inverse_name # 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) ids1 = select_distinct_from_where_not_null(cr, field.inverse_name, comodel._table)
else: else:
domain = [(field.inverse_name, '!=', False)] comodel_domain = [(field.inverse_name, '!=', False)]
recs = comodel.search(domain).sudo().with_context(prefetch_fields=False) if is_integer_m2o and domain:
ids1 = recs.mapped(field.inverse_name).ids 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)) push(create_substitution_leaf(leaf, ('id', o2m_op, ids1), model))
elif field.type == 'many2many': elif field.type == 'many2many':