[MERGE] forward port branch saas-17 up to d08b4407be

This commit is contained in:
Christophe Simonis
2017-09-12 12:09:11 +02:00
72 changed files with 1151 additions and 394 deletions
@@ -23,7 +23,6 @@ Allows to automatically select analytic accounts based on criterions:
'security/ir.model.access.csv',
'security/account_analytic_default_security.xml',
'views/account_analytic_default_view.xml',
'views/product_views.xml',
'views/res_config_settings_views.xml',
],
'installable': True,
@@ -3,4 +3,3 @@
from . import account_analytic_default
from . import sale_order_line
from . import product
@@ -1,33 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class ProductProduct(models.Model):
_inherit = 'product.product'
rules_count = fields.Integer(compute='_compute_rules_count', string='# Analytic Rules')
def _compute_rules_count(self):
Analytic = self.env['account.analytic.default']
for product in self:
product.rules_count = Analytic.search_count([('product_id', '=', product.id)])
class ProductTemplate(models.Model):
_inherit = 'product.template'
rules_count = fields.Integer(compute='_compute_rules_count', string='# Analytic Rules')
def _compute_rules_count(self):
for template in self:
template.rules_count = sum([p.rules_count for p in template.product_variant_ids])
@api.multi
def action_view_rules(self):
result = self.env.ref('account_analytic_default.action_product_default_list').read()[0]
result['domain'] = [('product_id', 'in', self.mapped('product_variant_ids').ids)]
# Remove context so it is not going to filter on product_id with active_id of template
result['context'] = {'default_product_id': self.mapped('product_variant_ids').id}
return result
@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="product_form_view_default_analytic_button" model="ir.ui.view">
<field name="name">product.product.inherit.rules_count</field>
<field name="model">product.product</field>
<field name="inherit_id" ref="product.product_normal_form_view"/>
<field name="arch" type="xml">
<button name="toggle_active" position="before">
<button name= "%(action_product_default_list)d" class="oe_stat_button" type="action"
groups="analytic.group_analytic_accounting" icon="fa-bookmark-o">
<field name="rules_count" string="Analytic Rules" widget="statinfo" />
</button>
</button>
</field>
</record>
<record id="product_template_view_default_analytic_button" model="ir.ui.view">
<field name="name">product.template.inherit.rules_count</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_only_form_view"/>
<field name="arch" type="xml">
<button name="toggle_active" position="before">
<button name= "action_view_rules" class="oe_stat_button" type="object"
groups="analytic.group_analytic_accounting" icon="fa-bookmark-o">
<field name="rules_count" string="Analytic Rules" widget="statinfo" />
</button>
</button>
</field>
</record>
</odoo>
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from ast import literal_eval
from odoo import api, fields, models
@@ -22,7 +24,7 @@ class ResConfigSettings(models.TransientModel):
res.update(
auth_signup_reset_password=get_param('auth_signup.reset_password', 'False').lower() == 'true',
auth_signup_uninvited='b2c' if get_param('auth_signup.allow_uninvited', 'False').lower() == 'true' else 'b2b',
auth_signup_template_user_id=get_param('auth_signup.template_user_id', 'False').lower() == 'true',
auth_signup_template_user_id=literal_eval(get_param('auth_signup.template_user_id', 'False')),
)
return res
@@ -38,6 +40,6 @@ class ResConfigSettings(models.TransientModel):
@api.multi
def open_template_user(self):
action = self.env.ref('base.action_res_users').read()[0]
action['res_id'] = self.env.ref('auth_signup.default_template_user').id
action['res_id'] = literal_eval(self.env['ir.config_parameter'].sudo().get_param('auth_signup.template_user_id', 'False'))
action['views'] = [[self.env.ref('base.view_users_form').id, 'form']]
return action
@@ -19,7 +19,11 @@
<field name="auth_signup_uninvited" widget="radio" required="True"/>
</div>
<div class="content-group" attrs="{'invisible': [('auth_signup_uninvited','=','b2b')]}">
<div class="mt16">
<div class="mt16" groups="base.group_no_one"
attrs="{'invisible': [('auth_signup_template_user_id', '!=', False)]}">
Copy access rights from <field name="auth_signup_template_user_id"/>
</div>
<div class="mt16" attrs="{'invisible': [('auth_signup_template_user_id', '=', False)]}">
<button type="object" name="open_template_user" string="Default Access Rights" icon="fa-arrow-right" class="btn-link"/>
</div>
</div>
+8 -4
View File
@@ -297,6 +297,8 @@ class Import(models.TransientModel):
if all(val.lower() in ('true', 'false', 't', 'f', '') for val in preview_values):
return ['boolean']
# If all values can be cast to float, type is either float or monetary
# Or a date/datetime if it matches the pattern
results = []
try:
thousand_separator = decimal_separator = False
for val in preview_values:
@@ -328,7 +330,7 @@ class Import(models.TransientModel):
if thousand_separator and not options.get('float_decimal_separator'):
options['float_thousand_separator'] = thousand_separator
options['float_decimal_separator'] = decimal_separator
return ['float', 'monetary']
results = ['float', 'monetary']
except ValueError:
pass
# Try to see if all values are a date or datetime
@@ -364,13 +366,15 @@ class Import(models.TransientModel):
current_date_pattern = check_patterns(date_patterns, preview_values)
if current_date_pattern:
options['date_format'] = current_date_pattern
return ['date']
results += ['date']
current_datetime_pattern = check_patterns(datetime_patterns, preview_values)
if current_datetime_pattern:
options['datetime_format'] = current_datetime_pattern
return ['datetime']
results += ['datetime']
if results:
return results
return ['id', 'text', 'char', 'datetime', 'selection', 'many2one', 'one2many', 'many2many', 'html']
@api.model
@@ -492,7 +496,7 @@ class Import(models.TransientModel):
'headers_type': header_types or False,
'preview': preview,
'options': options,
'advanced_mode': any([len(models.fix_import_export_id_paths(col)) > 1 for col in headers]),
'advanced_mode': any([len(models.fix_import_export_id_paths(col)) > 1 for col in headers or []]),
'debug': self.user_has_groups('base.group_no_one'),
}
except Exception as error:
+1 -1
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from xml.etree import ElementTree
from lxml import etree as ElementTree
from odoo.http import Controller, route, request
+2 -2
View File
@@ -87,9 +87,9 @@ class DeliveryCarrier(models.Model):
return False
if self.state_ids and partner.state_id not in self.state_ids:
return False
if self.zip_from and (partner.zip or '') < self.zip_from:
if self.zip_from and (partner.zip or '').upper() < self.zip_from.upper():
return False
if self.zip_to and (partner.zip or '') > self.zip_to:
if self.zip_to and (partner.zip or '').upper() > self.zip_to.upper():
return False
return True
+1 -1
View File
@@ -75,7 +75,7 @@ class LivechatController(http.Controller):
# if the user is identifiy (eg: portal user on the frontend), don't use the anonymous name. The user will be added to session.
if request.session.uid:
anonymous_name = request.env.user.name
return request.env["im_livechat.channel"].get_mail_channel(channel_id, anonymous_name)
return request.env["im_livechat.channel"].with_context(lang=False).get_mail_channel(channel_id, anonymous_name)
@http.route('/im_livechat/feedback', type='json', auth='public')
def feedback(self, uuid, rate, reason=None, **kwargs):
+7 -7
View File
@@ -162,7 +162,7 @@ var ActivityMenu = Widget.extend({
template:'mail.chat.ActivityMenu',
events: {
"click": "_onActivityMenuClick",
"click .o_activity_filter_button, .o_mail_channel_preview": "_onActivityFilterClick",
"click .o_mail_channel_preview": "_onActivityFilterClick",
},
start: function () {
this.$activities_preview = this.$('.o_mail_navbar_dropdown_channels');
@@ -252,19 +252,19 @@ var ActivityMenu = Widget.extend({
* @param {MouseEvent} event
*/
_onActivityFilterClick: function (event) {
event.stopPropagation();
var $target = $(event.currentTarget);
// fetch the data from the button otherwise fetch the ones from the parent (.o_mail_channel_preview).
var data = _.extend({}, $(event.currentTarget).data(), $(event.target).data());
var context = {};
if ($target.data('filter')=='my') {
if (data.filter === 'my') {
context['search_default_activities_overdue'] = 1;
context['search_default_activities_today'] = 1;
} else {
context['search_default_activities_' + $target.data('filter')] = 1;
context['search_default_activities_' + data.filter] = 1;
}
this.do_action({
type: 'ir.actions.act_window',
name: $target.data('model_name'),
res_model: $target.data('res_model'),
name: data.model_name,
res_model: data.res_model,
views: [[false, 'kanban'], [false, 'form']],
search_view_id: [false],
domain: [['activity_user_id', '=', session.uid]],
+38 -5
View File
@@ -41,8 +41,8 @@ QUnit.module('ActivityMenu', {
res_model: "project.issue",
planned_count: 1,
today_count: 1,
overdue_count: 0,
total_count: 2,
overdue_count: 1,
total_count: 3,
}],
},
};
@@ -67,10 +67,9 @@ QUnit.test('activity menu widget: menu with no records', function (assert) {
});
QUnit.test('activity menu widget: activity menu with 3 records', function (assert) {
assert.expect(4);
assert.expect(10);
var self = this;
var activityMenu = new systray.ActivityMenu();
testUtils.addMockEnvironment(activityMenu, {
mockRPC: function (route, args) {
if (args.method === 'activity_user_count') {
@@ -83,7 +82,41 @@ QUnit.test('activity menu widget: activity menu with 3 records', function (asser
assert.ok(activityMenu.$el.hasClass('o_mail_navbar_item'), 'should be the instance of widget');
assert.ok(activityMenu.$('.o_mail_channel_preview').hasClass('o_mail_channel_preview'), "should instance of widget");
assert.ok(activityMenu.$('.o_notification_counter').hasClass('o_notification_counter'), "widget should have notification counter");
assert.strictEqual(parseInt(activityMenu.el.innerText), 4, "widget should have 4 notification counter");
assert.strictEqual(parseInt(activityMenu.el.innerText), 5, "widget should have 5 notification counter");
var context = {};
testUtils.intercept(activityMenu, 'do_action', function(event) {
assert.deepEqual(event.data.action.context, context, "wrong context value");
}, true);
// case 1: click on "late"
context = {
search_default_activities_overdue: 1,
};
activityMenu.$('.dropdown-toggle').click();
assert.strictEqual(activityMenu.$el.hasClass("open"), true, 'ActivityMenu should be open');
activityMenu.$(".o_activity_filter_button[data-model_name='Issue'][data-filter='overdue']").click();
assert.strictEqual(activityMenu.$el.hasClass("open"), false, 'ActivityMenu should be closed');
// case 2: click on "today"
context = {
search_default_activities_today: 1,
};
activityMenu.$('.dropdown-toggle').click();
activityMenu.$(".o_activity_filter_button[data-model_name='Issue'][data-filter='today']").click();
// case 3: click on "future"
context = {
search_default_activities_upcoming_all: 1,
};
activityMenu.$('.dropdown-toggle').click();
activityMenu.$(".o_activity_filter_button[data-model_name='Issue'][data-filter='upcoming_all']").click();
// case 4: click anywere else
context = {
search_default_activities_overdue: 1,
search_default_activities_today: 1,
};
activityMenu.$('.dropdown-toggle').click();
activityMenu.$(".o_mail_navbar_dropdown_channels > div[data-model_name='Issue']").click();
activityMenu.destroy();
});
});
+1 -1
View File
@@ -5,7 +5,7 @@
wiz_obj = self.env['mrp.product.produce']
ctx = dict(self.env.context, active_id=self.id)
produce_wiz = wiz_obj.with_context(ctx).create({'product_qty': self.product_qty, 'lot_id': ref('lot_product_27_0')})
produce_wiz.consume_line_ids.write({'qty_done': produce_wiz.product_qty})
produce_wiz.produce_line_ids.write({'qty_done': produce_wiz.product_qty})
#Produce Product,Post Inventory and Set MO Done.
produce_wiz.do_produce()
self.post_inventory()
+1 -1
View File
@@ -109,7 +109,7 @@ class MrpWorkorder(models.Model):
'stock.move.line', 'workorder_id',
domain=[('done_wo', '=', False)])
final_lot_id = fields.Many2one(
'stock.production.lot', 'Current Lot', domain="[('product_id', '=', product_id)]",
'stock.production.lot', 'Lot/Serial Number', domain="[('product_id', '=', product_id)]",
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
time_ids = fields.One2many(
'mrp.workcenter.productivity', 'workorder_id')
+4 -4
View File
@@ -113,7 +113,7 @@ class TestUnbuild(TestMrpCommon):
}).action_unbuild()
# Check quantity in stock after last unbuild.
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), -5, 'You should have negative quantity for final product in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, allow_negative=True), -5, 'You should have negative quantity for final product in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 120, 'You should have 80 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 10, 'You should have consumed all the 5 product in stock')
@@ -194,7 +194,7 @@ class TestUnbuild(TestMrpCommon):
'product_uom_id': self.uom_unit.id,
}).action_unbuild()
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot), -5, 'You should have negative quantity for final product in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot, allow_negative=True), -5, 'You should have negative quantity for final product in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location), 120, 'You should have 80 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 10, 'You should have consumed all the 5 product in stock')
@@ -279,7 +279,7 @@ class TestUnbuild(TestMrpCommon):
'product_uom_id': self.uom_unit.id,
}).action_unbuild()
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), -5, 'You should have negative quantity for final product in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, allow_negative=True), -5, 'You should have negative quantity for final product in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot), 120, 'You should have 80 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location), 10, 'You should have consumed all the 5 product in stock')
@@ -378,7 +378,7 @@ class TestUnbuild(TestMrpCommon):
'product_uom_id': self.uom_unit.id,
}).action_unbuild()
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), -5, 'You should have negative quantity for final product in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final, allow_negative=True), -5, 'You should have negative quantity for final product in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot_1), 120, 'You should have 80 products in stock')
self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 10, 'You should have consumed all the 5 product in stock')
+6 -7
View File
@@ -229,12 +229,11 @@ class TestWorkOrderProcess(common.TransactionCase):
# -----------------
# Produce 6 Unit of custom laptop will consume ( 12 Unit of keybord and 12 Unit of charger)
context = {"active_ids": [mo_custom_laptop.id], "active_id": mo_custom_laptop.id}
product_consume = self.env['mrp.product.produce'].with_context(context).create({'product_qty': 6.00})
laptop_lot_001 = self.env['stock.production.lot'].create({'product_id': custom_laptop.id})
product_consume.lot_id = laptop_lot_001.id
product_consume.consume_line_ids.write({'qty_done': 12})
product_consume.produce_line_ids.write({'qty_done': 12})
product_consume.do_produce()
# Check consumed move after produce 6 quantity of customized laptop.
@@ -259,8 +258,8 @@ class TestWorkOrderProcess(common.TransactionCase):
product_consume = self.env['mrp.product.produce'].with_context(context).create({'product_qty': 4.00})
laptop_lot_002 = self.env['stock.production.lot'].create({'product_id': custom_laptop.id})
product_consume.lot_id = laptop_lot_002.id
self.assertEquals(len(product_consume.consume_line_ids), 2)
product_consume.consume_line_ids.write({'qty_done': 8})
self.assertEquals(len(product_consume.produce_line_ids), 2)
product_consume.produce_line_ids.write({'qty_done': 8})
product_consume.do_produce()
charger_move = mo_custom_laptop.move_raw_ids.filtered(lambda x: x.product_id.id == product_charger.id and x.state != 'done')
keybord_move = mo_custom_laptop.move_raw_ids.filtered(lambda x: x.product_id.id == product_keybord.id and x.state !='done')
@@ -394,9 +393,9 @@ class TestWorkOrderProcess(common.TransactionCase):
product_consume = self.env['mrp.product.produce'].with_context(context).create({'product_qty': 10})
# laptop_lot_002 = self.env['stock.production.lot'].create({'product_id': custom_laptop.id})
product_consume.lot_id = lot_a.id
self.assertEquals(len(product_consume.consume_line_ids), 2)
product_consume.consume_line_ids.filtered(lambda x : x.product_id == product_C).write({'qty_done': 3000})
product_consume.consume_line_ids.filtered(lambda x : x.product_id == product_B).write({'qty_done': 20})
self.assertEquals(len(product_consume.produce_line_ids), 2)
product_consume.produce_line_ids.filtered(lambda x : x.product_id == product_C).write({'qty_done': 3000})
product_consume.produce_line_ids.filtered(lambda x : x.product_id == product_B).write({'qty_done': 20})
product_consume.do_produce()
mo_custom_product.post_inventory()
+1 -1
View File
@@ -59,7 +59,7 @@
<span class="o_stat_text">Work Orders</span>
</div>
</button>
<button name="%(action_mrp_production_moves)d" type="action" string="Inventory Moves" class="oe_stat_button" icon="fa-arrows-v" attrs="{'invisible': [('state', 'not in', ('progress', 'cancel', 'done'))]}"/>
<button name="%(action_mrp_production_moves)d" type="action" string="Inventory Moves" class="oe_stat_button" icon="fa-arrows-v" attrs="{'invisible': [('state', 'not in', ('progress', 'done'))]}"/>
<button class="oe_stat_button" name="action_see_move_scrap" type="object" icon="fa-arrows-v" attrs="{'invisible': [('scrap_count', '=', 0)]}">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value"><field name="scrap_count"/></span>
+3 -3
View File
@@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record model="ir.actions.act_window" id="action_mrp_production_moves">
<field name="name">Stock Moves</field>
<field name="name">Inventory Moves</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">stock.move</field>
<field name="res_model">stock.move.line</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="domain">['|', ('raw_material_production_id', '=', active_id), ('production_id', '=', active_id)]</field>
<field name="domain">['|', ('move_id.raw_material_production_id', '=', active_id), ('move_id.production_id', '=', active_id)]</field>
</record>
<record id="view_stock_move_lots" model="ir.ui.view">
+91 -54
View File
@@ -17,53 +17,61 @@ class MrpProductProduce(models.TransientModel):
res = super(MrpProductProduce, self).default_get(fields)
if self._context and self._context.get('active_id'):
production = self.env['mrp.production'].browse(self._context['active_id'])
#serial_raw = production.move_raw_ids.filtered(lambda x: x.product_id.tracking == 'serial')
main_product_moves = production.move_finished_ids.filtered(lambda x: x.product_id.id == production.product_id.id)
serial_finished = (production.product_id.tracking == 'serial')
serial = bool(serial_finished)
if serial_finished:
quantity = 1.0
todo_quantity = 1.0
else:
quantity = production.product_qty - sum(main_product_moves.mapped('quantity_done'))
quantity = quantity if (quantity > 0) else 0
lines = []
existing_lines = []
for move in production.move_raw_ids.filtered(lambda x: (x.product_id.tracking != 'none') and x.state not in ('done', 'cancel')):
if not move.move_line_ids.filtered(lambda x: not x.lot_produced_id):
qty = quantity / move.bom_line_id.bom_id.product_qty * move.bom_line_id.product_qty
if move.product_id.tracking == 'serial':
while float_compare(qty, 0.0, precision_rounding=move.product_uom.rounding) > 0:
main_product_moves = production.move_finished_ids.filtered(lambda x: x.product_id.id == production.product_id.id)
todo_quantity = production.product_qty - sum(main_product_moves.mapped('quantity_done'))
todo_quantity = todo_quantity if (todo_quantity > 0) else 0
if 'production_id' in fields:
res['production_id'] = production.id
if 'product_id' in fields:
res['product_id'] = production.product_id.id
if 'product_uom_id' in fields:
res['product_uom_id'] = production.product_uom_id.id
if 'serial' in fields:
res['serial'] = bool(serial_finished)
if 'product_qty' in fields:
res['product_qty'] = todo_quantity
if 'produce_line_ids' in fields:
lines = []
for move in production.move_raw_ids.filtered(lambda x: (x.product_id.tracking != 'none') and x.state not in ('done', 'cancel')):
qty_to_consume = todo_quantity / move.bom_line_id.bom_id.product_qty * move.bom_line_id.product_qty
for move_line in move.move_line_ids:
if float_compare(qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) < 0:
break
to_consume_in_line = min(qty_to_consume, move_line.product_uom_qty)
lines.append({
'move_id': move.id,
'qty_to_consume': to_consume_in_line,
'qty_done': 0.0,
'lot_id': move_line.lot_id.id,
'product_uom_id': move.product_uom.id,
'product_id': move.product_id.id,
})
qty_to_consume -= to_consume_in_line
if float_compare(qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) > 0:
if move.product_id.tracking == 'serial':
while float_compare(qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) > 0:
lines.append({
'move_id': move.id,
'qty_to_consume': 1,
'qty_done': 0.0,
'product_uom_id': move.product_uom.id,
'product_id': move.product_id.id,
})
qty_to_consume -= 1
else:
lines.append({
'move_id': move.id,
'product_qty': min(1,qty),
'qty_to_consume': qty_to_consume,
'qty_done': 0.0,
'product_uom_id': move.product_uom.id,
'product_id': move.product_id.id,
'production_id': production.id,
'location_id': move.location_id.id,
'location_dest_id': move.location_dest_id.id,
})
qty -= 1
else:
lines.append({
'move_id': move.id,
'product_qty': qty,
'qty_done': 0.0,
'product_uom_id': move.product_uom.id,
'product_id': move.product_id.id,
'production_id': production.id,
'location_id': move.location_id.id,
'location_dest_id': move.location_dest_id.id,
})
else:
existing_lines += move.move_line_ids.filtered(lambda x: not x.lot_produced_id).ids
res['serial'] = serial
res['production_id'] = production.id
res['product_qty'] = quantity
res['product_id'] = production.product_id.id
res['product_uom_id'] = production.product_uom_id.id
res['consume_line_ids'] = (existing_lines and [(6, 0, [x for x in existing_lines])] or []) + [(0, 0, x) for x in lines]
res['produce_line_ids'] = [(0, 0, x) for x in lines]
return res
serial = fields.Boolean('Requires Serial')
@@ -72,7 +80,7 @@ class MrpProductProduce(models.TransientModel):
product_qty = fields.Float(string='Quantity', digits=dp.get_precision('Product Unit of Measure'), required=True)
product_uom_id = fields.Many2one('product.uom', 'Unit of Measure')
lot_id = fields.Many2one('stock.production.lot', string='Lot')
consume_line_ids = fields.Many2many('stock.move.line', 'mrp_produce_stock_move_line', string='Product to Track')
produce_line_ids = fields.One2many('mrp.product.produce.line', 'product_produce_id', string='Product to Track')
product_tracking = fields.Selection(related="product_id.tracking")
@api.multi
@@ -104,14 +112,13 @@ class MrpProductProduce(models.TransientModel):
@api.multi
def check_finished_move_lots(self):
packs = self.env['stock.move.line']
produce_move = self.production_id.move_finished_ids.filtered(lambda x: x.product_id == self.product_id and x.state not in ('done', 'cancel'))
if produce_move and produce_move.product_id.tracking != 'none':
if not self.lot_id:
raise UserError(_('You need to provide a lot for the finished product'))
existing_move_line = produce_move.move_line_ids.filtered(lambda x: x.lot_id == self.lot_id)
if existing_move_line:
existing_move_line.product_qty += self.product_qty
existing_move_line.product_uom_qty += self.product_qty
existing_move_line.qty_done += self.product_qty
else:
vals = {
@@ -125,18 +132,48 @@ class MrpProductProduce(models.TransientModel):
'location_id': produce_move.location_id.id,
'location_dest_id': produce_move.location_dest_id.id,
}
packs.create(vals)
for move in self.production_id.move_raw_ids:
for moveline in move.move_line_ids.filtered(lambda x: not x.lot_produced_id):
if moveline.qty_done and self.lot_id:
#Possibly the entire move is selected
remaining_qty = moveline.product_uom_qty - moveline.qty_done
if remaining_qty > 0:
default = {'product_uom_qty': moveline.qty_done,
'qty_done': moveline.qty_done,
'lot_produced_id': self.lot_id.id}
new_move_line = moveline.copy(default=default)
moveline.with_context(bypass_reservation_update=True).write({'product_uom_qty': remaining_qty, 'qty_done': 0})
else:
moveline.write({'lot_produced_id': self.lot_id.id})
self.env['stock.move.line'].create(vals)
for pl in self.produce_line_ids:
if pl.qty_done and pl.lot_id:
ml = pl.move_id.move_line_ids.filtered(lambda ml: ml.lot_id == pl.lot_id and not ml.lot_produced_id)
if ml:
if (ml.qty_done + pl.qty_done) >= ml.product_uom_qty:
ml.write({'qty_done': ml.qty_done + pl.qty_done, 'lot_produced_id': self.lot_id.id})
else:
new_qty_todo = ml.product_uom_qty - (ml.qty_done + pl.qty_done)
default = {'product_uom_qty': ml.qty_done + pl.qty_done,
'qty_done': ml.qty_done + pl.qty_done,
'lot_produced_id': self.lot_id.id}
ml.copy(default=default)
ml.with_context(bypass_reservation_update=True).write({'product_uom_qty': new_qty_todo, 'qty_done': 0})
else:
self.env['stock.move.line'].create({
'move_id': pl.move_id.id,
'product_id': pl.product_id.id,
'location_id': pl.move_id.location_id.id,
'location_dest_id': pl.move_id.location_dest_id.id,
'product_uom_qty': 0,
'product_uom_id': pl.product_uom_id.id,
'qty_done': pl.qty_done,
'lot_id': pl.lot_id.id,
'lot_produced_id': self.lot_id.id,
})
return True
class MrpProductProduceLine(models.TransientModel):
_name = "mrp.product.produce.line"
_description = "Record Production Line"
product_produce_id = fields.Many2one('mrp.product.produce')
product_id = fields.Many2one('product.product', 'Product')
lot_id = fields.Many2one('stock.production.lot', 'Lot')
qty_to_consume = fields.Float('To Consume')
product_uom_id = fields.Many2one('product.uom', 'Unit of Measure')
qty_done = fields.Float('Done')
move_id = fields.Many2one('stock.move')
@api.onchange('lot_id')
def _onchange_lot_id(self):
if self.product_id.tracking == 'serial':
self.qty_done = 1
@@ -14,20 +14,20 @@
<label for="product_qty"/>
<div class="o_row">
<field name="product_qty" attrs="{'readonly': [('serial', '=', True)]}"/>
<field name="product_uom_id" readonly="1"/>
<field name="product_uom_id" readonly="1" groups="product.group_uom"/>
</div>
<field name="product_tracking" invisible="1"/>
<field name="lot_id" attrs="{'invisible': [('product_tracking', '=', 'none')], 'required': [('product_tracking', '!=', 'none')]}" domain="[('product_id','=',product_id)]" context="{'default_product_id': product_id}"/>
</group>
</group>
<group>
<field name="consume_line_ids" attrs="{'invisible': [('consume_line_ids', '=', [])]}" nolabel="1" context="{'w_production': True, 'active_id': production_id, 'default_lot_id': lot_id}">
<field name="produce_line_ids" attrs="{'invisible': [('produce_line_ids', '=', [])]}" nolabel="1" context="{'w_production': True, 'active_id': production_id, 'default_lot_id': lot_id}">
<tree editable="top" delete="0" create="0">
<field name="product_id" readonly="1"/>
<field name="product_id" readonly="1" force_save="1"/>
<field name="lot_id" context="{'default_product_id': product_id}" domain="[('product_id', '=', product_id)]"/>
<field name="product_uom_qty" string="To Consume"/>
<field name="product_qty" readonly="1" string="Reserved"/>
<field name="qty_to_consume" readonly="1"/>
<field name="qty_done"/>
<field name="product_uom_id" readonly="1" force_save="1" groups="product.group_uom"/>
<field name="move_id" invisible="1"/>
</tree>
</field>
+1 -1
View File
@@ -22,7 +22,7 @@ class Repair(models.Model):
@api.model
def _default_stock_location(self):
warehouse = self.env.ref('stock.warehouse0', raise_if_not_found=False)
warehouse = self.env['stock.warehouse'].search([], limit=1)
if warehouse:
return warehouse.lot_stock_id.id
return False
+1 -1
View File
@@ -497,7 +497,7 @@ class SaleOrder(models.Model):
})
if self.env.context.get('send_email'):
self.force_quotation_send()
if self.env['ir.config_parameter'].get_param('sale.auto_done_setting'):
if self.env['ir.config_parameter'].sudo().get_param('sale.auto_done_setting'):
self.action_done()
return True
@@ -67,7 +67,6 @@
}
&.o_kanban_grouped {
.o-flex-flow(row, wrap);
.o_kanban_record {
min-width: inherit;
}
+22 -7
View File
@@ -121,15 +121,15 @@ class Product(models.Model):
Quant = self.env['stock.quant']
domain_move_in_todo = [('state', 'not in', ('done', 'cancel', 'draft'))] + domain_move_in
domain_move_out_todo = [('state', 'not in', ('done', 'cancel', 'draft'))] + domain_move_out
moves_in_res = dict((item['product_id'][0], item['product_qty']) for item in Move.read_group(domain_move_in_todo, ['product_id', 'product_qty'], ['product_id']))
moves_out_res = dict((item['product_id'][0], item['product_qty']) for item in Move.read_group(domain_move_out_todo, ['product_id', 'product_qty'], ['product_id']))
quants_res = dict((item['product_id'][0], item['quantity']) for item in Quant.read_group(domain_quant, ['product_id', 'quantity'], ['product_id']))
moves_in_res = dict((item['product_id'][0], item['product_qty']) for item in Move.read_group(domain_move_in_todo, ['product_id', 'product_qty'], ['product_id'], orderby='id'))
moves_out_res = dict((item['product_id'][0], item['product_qty']) for item in Move.read_group(domain_move_out_todo, ['product_id', 'product_qty'], ['product_id'], orderby='id'))
quants_res = dict((item['product_id'][0], item['quantity']) for item in Quant.read_group(domain_quant, ['product_id', 'quantity'], ['product_id'], orderby='id'))
if dates_in_the_past:
# Calculate the moves that were done before now to calculate back in time (as most questions will be recent ones)
domain_move_in_done = [('state', '=', 'done'), ('date', '>', to_date)] + domain_move_in_done
domain_move_out_done = [('state', '=', 'done'), ('date', '>', to_date)] + domain_move_out_done
moves_in_res_past = dict((item['product_id'][0], item['product_qty']) for item in Move.read_group(domain_move_in_done, ['product_id', 'product_qty'], ['product_id']))
moves_out_res_past = dict((item['product_id'][0], item['product_qty']) for item in Move.read_group(domain_move_out_done, ['product_id', 'product_qty'], ['product_id']))
moves_in_res_past = dict((item['product_id'][0], item['product_qty']) for item in Move.read_group(domain_move_in_done, ['product_id', 'product_qty'], ['product_id'], orderby='id'))
moves_out_res_past = dict((item['product_id'][0], item['product_qty']) for item in Move.read_group(domain_move_out_done, ['product_id', 'product_qty'], ['product_id'], orderby='id'))
res = dict()
for product in self.with_context(prefetch_fields=False):
@@ -272,7 +272,7 @@ class Product(models.Model):
domain_quant.append(('owner_id', '=', owner_id))
if package_id:
domain_quant.append(('package_id', '=', package_id))
quants_groupby = self.env['stock.quant'].read_group(domain_quant, ['product_id', 'quantity'], ['product_id'])
quants_groupby = self.env['stock.quant'].read_group(domain_quant, ['product_id', 'quantity'], ['product_id'], orderby='id')
for quant in quants_groupby:
if OPERATORS[operator](quant['quantity'], value):
product_ids.add(quant['product_id'][0])
@@ -359,6 +359,14 @@ class Product(models.Model):
action['domain'] = [('product_id', '=', self.id)]
return action
@api.multi
def action_open_product_lot(self):
self.ensure_one()
action = self.env.ref('stock.action_production_lot_form').read()[0]
action['domain'] = [('product_id', '=', self.id)]
action['context'] = {'default_product_id': self.id}
return action
@api.multi
def write(self, values):
res = super(Product, self).write(values)
@@ -512,7 +520,7 @@ class ProductTemplate(models.Model):
products = self.mapped('product_variant_ids')
action = self.env.ref('stock.product_open_quants').read()[0]
action['domain'] = [('product_id', 'in', products.ids)]
action['context'] = {'search_default_locationgroup': 1, 'search_default_internal_loc': 1}
action['context'] = {'search_default_internal_loc': 1}
return action
@api.multi
@@ -533,6 +541,13 @@ class ProductTemplate(models.Model):
action['domain'] = [('product_id.product_tmpl_id', 'in', self.ids)]
return action
@api.multi
def action_open_product_lot(self):
self.ensure_one()
action = self.env.ref('stock.action_production_lot_form').read()[0]
action['domain'] = [('product_id.product_tmpl_id', '=', self.id)]
action['context'] = {}
return action
class ProductCategory(models.Model):
_inherit = 'product.category'
+1 -10
View File
@@ -17,15 +17,11 @@ class PutAwayStrategy(models.Model):
_description = 'Put Away Strategy'
name = fields.Char('Name', required=True)
method = fields.Selection('_get_putaway_options', "Method", default='fixed', required=True)
fixed_location_ids = fields.One2many(
'stock.fixed.putaway.strat', 'putaway_id', 'Fixed Locations Per Product Category', copy=True,
help="When the method is fixed, this location will be used to store the products")
def _get_putaway_options(self):
return [('fixed', 'Fixed Location')]
def _putaway_apply_fixed(self, product):
def putaway_apply(self, product):
for strat in self.fixed_location_ids:
categ = product.categ_id
while categ:
@@ -34,11 +30,6 @@ class PutAwayStrategy(models.Model):
categ = categ.parent_id
return self.env['stock.location']
def putaway_apply(self, product):
if hasattr(self, '_putaway_apply_%s' % (self.method)):
return getattr(self, '_putaway_apply_%s' % (self.method))(product)
return self.env['stock.location']
class FixedPutAwayStrategy(models.Model):
_name = 'stock.fixed.putaway.strat'
+2 -2
View File
@@ -713,7 +713,7 @@ class StockMove(models.Model):
if move.location_id.usage in ('supplier', 'inventory', 'production', 'customer')\
or move.product_id.type == 'consu':
# create the move line(s) but do not impact quants
if move.product_id.tracking == 'serial':
if move.product_id.tracking == 'serial' and (move.picking_type_id.use_create_lots or move.picking_type_id.use_existing_lots):
for i in range(0, int(move.product_qty)):
self.env['stock.move.line'].create(move._prepare_move_line_vals(quantity=1))
else:
@@ -886,7 +886,7 @@ class StockMove(models.Model):
qty_split = move.product_uom._compute_quantity(move.product_uom_qty - move.quantity_done, move.product_id.uom_id)
new_move = move.split(qty_split)
for move_line in move.move_line_ids:
if move_line.product_qty:
if move_line.product_qty and move_line.qty_done:
# FIXME: there will be an issue if the move was partially available
# By decreasing `product_qty`, we free the reservation.
# FIXME: if qty_done > product_qty, this could raise if nothing is in stock
+1 -1
View File
@@ -46,7 +46,7 @@ class PickingType(models.Model):
'Show Detailed Operations', default=False,
help="If this checkbox is ticked, the pickings lines will represent detailed stock operations. If not, the picking lines will represent an aggregate of detailed stock operations.")
show_reserved = fields.Boolean(
'Show Reserved', default=True)
'Show Reserved', default=True, help="If this checkbox is ticked, Odoo will show which products are reserved (lot/serial number, source location, source package).")
# Statistics for the kanban view
last_done_picking = fields.Char('Last 10 Done Pickings', compute='_compute_last_done_picking')
+20 -6
View File
@@ -126,7 +126,7 @@ class StockQuant(models.Model):
return self.search(domain, order=removal_strategy_order)
@api.model
def _get_available_quantity(self, product_id, location_id, lot_id=None, package_id=None, owner_id=None, strict=False):
def _get_available_quantity(self, product_id, location_id, lot_id=None, package_id=None, owner_id=None, strict=False, allow_negative=False):
""" Return the available quantity, i.e. the sum of `quantity` minus the sum of
`reserved_quantity`, for the set of quants sharing the combination of `product_id,
location_id` if `strict` is set to False or sharing the *exact same characteristics*
@@ -146,7 +146,23 @@ class StockQuant(models.Model):
"""
self = self.sudo()
quants = self._gather(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=strict)
return sum(quants.mapped('quantity')) - sum(quants.mapped('reserved_quantity'))
if product_id.tracking == 'none':
available_quantity = sum(quants.mapped('quantity')) - sum(quants.mapped('reserved_quantity'))
if allow_negative:
return available_quantity
else:
return available_quantity if available_quantity >= 0.0 else 0.0
else:
availaible_quantities = {lot_id: 0.0 for lot_id in list(set(quants.mapped('lot_id'))) + ['untracked']}
for quant in quants:
if not quant.lot_id:
availaible_quantities['untracked'] += quant.quantity - quant.reserved_quantity
else:
availaible_quantities[quant.lot_id] += quant.quantity - quant.reserved_quantity
if allow_negative:
return sum(availaible_quantities.values())
else:
return sum([available_quantity for available_quantity in availaible_quantities.values() if available_quantity > 0])
@api.model
def _update_available_quantity(self, product_id, location_id, quantity, lot_id=None, package_id=None, owner_id=None, in_date=None):
@@ -206,7 +222,7 @@ class StockQuant(models.Model):
'owner_id': owner_id and owner_id.id,
'in_date': in_date,
})
return self._get_available_quantity(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=False), fields.Datetime.from_string(in_date)
return self._get_available_quantity(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=False, allow_negative=True), fields.Datetime.from_string(in_date)
@api.model
def _update_reserved_quantity(self, product_id, location_id, quantity, lot_id=None, package_id=None, owner_id=None, strict=False):
@@ -223,9 +239,7 @@ class StockQuant(models.Model):
"""
self = self.sudo()
quants = self._gather(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=strict)
quants_quantity = sum(quants.mapped('quantity'))
available_quantity = quants_quantity - sum(quants.mapped('reserved_quantity'))
available_quantity = self._get_available_quantity(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=strict)
if quantity > 0 and quantity > available_quantity:
raise UserError(_('It is not possible to reserve more products than you have in stock.'))
elif quantity < 0 and abs(quantity) > sum(quants.mapped('reserved_quantity')):
+11 -13
View File
@@ -156,8 +156,7 @@ class MrpStockReport(models.TransientModel):
'model':'stock.move.line',
'product_id': move_line.product_id.display_name,
'product_qty_uom': str(move_line.qty_done) + ' ' + move_line.product_id.uom_id.name,
'location_source': move_line.location_id.name,
'location_destination': move_line.location_dest_id.name,
'location': move_line.location_id.name + ' -> ' + move_line.location_dest_id.name,
'reference_id': ref,
'res_id': res_id,
'stream': stream,
@@ -166,9 +165,6 @@ class MrpStockReport(models.TransientModel):
def make_dict_head(self, level, parent_id, model=False, stream=False, move_line=False):
data = []
product_id_name = move_line.product_id.display_name
if move_line.lot_id:
product_id_name += ' ('+move_line.lot_id.name+')'
if model == 'stock.move.line':
data = [{
'level': level,
@@ -177,9 +173,10 @@ class MrpStockReport(models.TransientModel):
'model_id': move_line.id,
'parent_id': parent_id,
'model': model or 'stock.move.line',
'product_id': product_id_name,
'product_id': move_line.product_id.display_name,
'lot_id': move_line.lot_id.name,
'product_qty_uom': str(move_line.qty_done) + ' ' + move_line.product_id.uom_id.name,
'location_source': move_line.location_dest_id.name,
'location': move_line.location_dest_id.name,
'stream': stream,
'reference_id': False}]
elif model == 'stock.quant':
@@ -190,9 +187,10 @@ class MrpStockReport(models.TransientModel):
'model_id': move_line.id,
'parent_id': parent_id,
'model': model or 'stock.quant',
'product_id': product_id_name,
'product_id': move_line.product_id.display_name,
'lot_id': move_line.lot_id.name,
'product_qty_uom': str(move_line.quantity) + ' ' + move_line.product_id.uom_id.name,
'location_source': move_line.location_id.name,
'location': move_line.location_id.name,
'stream': stream,
'reference_id': False}]
return data
@@ -254,10 +252,10 @@ class MrpStockReport(models.TransientModel):
'res_model': data.get('res_model', False),
'name': _(data.get('lot_id', False)),
'columns': [data.get('reference_id', False) or data.get('product_id', False),
data.get('lot_id', False),
data.get('date', False),
data.get('product_qty_uom', 0),
data.get('location_source', False),
data.get('location_destination', False)],
data.get('location', False)],
'level': level,
'unfoldable': data['unfoldable'],
})
@@ -315,10 +313,10 @@ class MrpStockReport(models.TransientModel):
'type': 'line',
'name': _(data.get('lot_id')),
'columns': [data.get('reference_id') or data.get('product_id'),
data.get('lot_id'),
data.get('date'),
data.get('product_qty_uom', 0),
data.get('location_source'),
data.get('location_destination')],
data.get('location')],
'level': data['level'],
'unfoldable': data['unfoldable'],
})
+119 -10
View File
@@ -74,7 +74,8 @@ class StockMove(TransactionCase):
move1.action_done()
self.assertEqual(move1.state, 'done')
# no quants are created in the supplier location
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.supplier_location), -100.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.supplier_location), 0.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.supplier_location, allow_negative=True), -100.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 100.0)
self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.supplier_location)), 1.0)
self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location)), 1.0)
@@ -114,8 +115,10 @@ class StockMove(TransactionCase):
self.assertEqual(move_line.product_qty, 0) # change reservation to 0 for done move
self.assertEqual(move1.state, 'done')
# no quants are created in the supplier location
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product3, self.supplier_location), -5.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product3, self.supplier_location), 0.0)
supplier_quants = self.env['stock.quant']._gather(self.product3, self.supplier_location)
self.assertEqual(sum(supplier_quants.mapped('quantity')), -5.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product3, self.stock_location), 5.0)
self.assertEqual(len(self.env['stock.quant']._gather(self.product3, self.supplier_location)), 1.0)
quants = self.env['stock.quant']._gather(self.product3, self.stock_location)
@@ -165,7 +168,9 @@ class StockMove(TransactionCase):
self.assertEqual(move1.state, 'done')
# Quant balance should result with 5 quant in supplier and stock
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.supplier_location), -5.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.supplier_location), 0.0)
supplier_quants = self.env['stock.quant']._gather(self.product2, self.supplier_location)
self.assertEqual(sum(supplier_quants.mapped('quantity')), -5.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.stock_location), 5.0)
self.assertEqual(len(self.env['stock.quant']._gather(self.product2, self.supplier_location)), 5.0)
@@ -299,7 +304,7 @@ class StockMove(TransactionCase):
self.assertEqual(len(move1.move_line_ids), 2)
def test_mixed_tracking_reservation_2(self):
""" Send products tracked by lot to a customer. In your stock, there two tracked and
""" Send products tracked by lot to a customer. In your stock, there are two tracked and
mulitple untracked quants. There should be as many move lines as there are quants
reserved. Edit the reserve move lines to set them to new serial numbers, the reservation
should stay. Validate and the final quantity in stock should be 0, not negative.
@@ -315,7 +320,6 @@ class StockMove(TransactionCase):
self.env['stock.quant']._update_available_quantity(self.product2, self.stock_location, 2)
self.env['stock.quant']._update_available_quantity(self.product2, self.stock_location, 1, lot_id=lot1)
self.env['stock.quant']._update_available_quantity(self.product2, self.stock_location, 1, lot_id=lot2)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.stock_location), 4.0)
# creation
move1 = self.env['stock.move'].create({
@@ -685,6 +689,30 @@ class StockMove(TransactionCase):
self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location)), 1.0)
self.assertEqual(move1.availability, 50.0)
def test_availability_3(self):
lot1 = self.env['stock.production.lot'].create({
'name': 'lot1',
'product_id': self.product2.id,
})
lot2 = self.env['stock.production.lot'].create({
'name': 'lot2',
'product_id': self.product2.id,
})
self.env['stock.quant']._update_available_quantity(self.product2, self.stock_location, -1.0, lot_id=lot1)
self.env['stock.quant']._update_available_quantity(self.product2, self.stock_location, 1.0, lot_id=lot2)
move1 = self.env['stock.move'].create({
'name': 'test_availability_3',
'location_id': self.stock_location.id,
'location_dest_id': self.customer_location.id,
'product_id': self.product2.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
move1.action_confirm()
move1.action_assign()
self.assertEqual(move1.state, 'assigned')
self.assertEqual(move1.reserved_availability, 1.0)
def test_unreserve_1(self):
""" Check that unreserving a stock move sets the products reserved as available and
set the state back to confirmed.
@@ -1932,7 +1960,8 @@ class StockMove(TransactionCase):
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 0.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf1_location), 1.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf2_location), -1.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf2_location), 0.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf2_location, allow_negative=True), -1.0)
def test_edit_done_move_line_7(self):
""" Test that editing a done stock move line linked to an untracked product correctly and
@@ -2027,8 +2056,10 @@ class StockMove(TransactionCase):
# edit once done, we actually moved 2 products
move1.move_line_ids.qty_done = 2
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf1_location), -1.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), -1.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf1_location), 0.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, shelf1_location, allow_negative=True), -1.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 0.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, allow_negative=True), -1.0)
self.assertEqual(move1.product_uom_qty, 2.0)
def test_edit_done_move_line_9(self):
@@ -2243,7 +2274,8 @@ class StockMove(TransactionCase):
self.assertEqual(picking.move_lines.move_line_ids.qty_done, 10.0)
self.assertEqual(picking.move_lines.move_line_ids.product_qty, 0.0)
# Check quants data
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), -10.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 0.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, allow_negative=True), -10.0)
self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location)), 1.0)
def test_immediate_validate_4(self):
@@ -2291,6 +2323,50 @@ class StockMove(TransactionCase):
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 0.0)
self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location)), 0.0)
def _create_picking_test_immediate_validate_5(self, picking_type_id, product_id):
picking = self.env['stock.picking'].create({
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'picking_type_id': picking_type_id.id,
})
self.env['stock.move'].create({
'name': 'move1',
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'picking_id': picking.id,
'picking_type_id': picking_type_id.id,
'product_id': product_id.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 5.0,
})
picking.action_confirm()
for line in picking.move_line_ids:
line.qty_done = line.product_uom_qty
return picking
def test_immediate_validate_575487534895(self):
""" Create a picking and simulates validate button effect.
Test that tracked products can be received without specifying a serial
number when the picking type is configured that way.
"""
picking_type_id = self.env.ref('stock.picking_type_in')
product_id = self.product2
self.assertTrue(picking_type_id.use_create_lots or picking_type_id.use_existing_lots)
self.assertEqual(product_id.tracking, 'serial')
picking = self._create_picking_test_immediate_validate_5(picking_type_id, product_id)
# should raise because no serial numbers were specified
self.assertRaises(UserError, picking.do_new_transfer)
picking_type_id.use_create_lots = False
picking_type_id.use_existing_lots = False
picking = self._create_picking_test_immediate_validate_5(picking_type_id, product_id)
picking.do_new_transfer()
self.assertEqual(picking.state, 'done')
def test_immediate_validate_5(self):
""" Create a picking and simulates validate button effect.
This tests two cases:
@@ -2727,3 +2803,36 @@ class StockMove(TransactionCase):
self.assertEqual(self.product1.qty_available, 5.0)
self.assertEqual(self.product1.with_context(company_owned=True).qty_available, 10.0)
def test_split_1(self):
""" When we split a move line and having one without quantity done, we want to keep reservation
on the new one as it has not been unreserved during the copy.
"""
move1 = self.env['stock.move'].create({
'name': 'test_split_1',
'location_id': self.stock_location.id,
'location_dest_id': self.customer_location.id,
'product_id': self.product1.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 10.0,
'picking_type_id': self.env.ref('stock.picking_type_in').id,
})
self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 10)
move1.action_confirm()
move1.action_assign()
move_line = move1.move_line_ids
default = {'product_uom_qty': 3,
'qty_done': 3}
move_line.copy(default=default)
move_line.with_context(bypass_reservation_update=True).write({'product_uom_qty': 7, 'qty_done': 0})
move1.action_done()
new_move = self.env['stock.move'].search([('name', '=', 'test_split_1'), ('state', '=', 'confirmed')])
self.assertEqual(move1.move_line_ids.product_uom_qty, 0.0)
self.assertEqual(move1.move_line_ids.qty_done, 3.0)
self.assertEqual(new_move.move_line_ids.product_uom_qty, 7.0)
self.assertEqual(new_move.move_line_ids.qty_done, 0.0)
+4 -2
View File
@@ -622,7 +622,8 @@ class TestSinglePicking(TestStockCommon):
delivery_order.move_lines[0].move_line_ids[0].qty_done = 2
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 0.0)
delivery_order.with_context(debug=True).do_transfer()
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), -1.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 0.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location, allow_negative=True), -1.0)
extra_move = delivery_order.move_lines - move1
extra_move_line = extra_move.move_line_ids[0]
@@ -677,7 +678,8 @@ class TestSinglePicking(TestStockCommon):
delivery_order.move_lines[0].move_line_ids[0].qty_done = 3
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 0.0)
delivery_order.do_transfer()
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), -2.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 0.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location, allow_negative=True), -2.0)
extra_move = delivery_order.move_lines - move1
extra_move_line = extra_move.move_line_ids[0]
+6 -3
View File
@@ -131,7 +131,8 @@ class StockQuant(TransactionCase):
'quantity': 5.0,
'reserved_quantity': 0.0,
})
self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), -5.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), 0.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location, allow_negative=True), -5.0)
def test_get_available_quantity_7(self):
""" Quantity availability with only one tracked quant in a location.
@@ -153,7 +154,8 @@ class StockQuant(TransactionCase):
'reserved_quantity': 20.0,
'lot_id': lot1.id,
})
self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location, lot_id=lot1), -10.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location, lot_id=lot1), 0.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location, lot_id=lot1, allow_negative=True), -10.0)
def test_get_available_quantity_8(self):
""" Quantity availability with a consumable product.
@@ -282,7 +284,8 @@ class StockQuant(TransactionCase):
'type': 'product',
})
self.env['stock.quant']._update_available_quantity(product1, stock_location, -1.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), -1.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location), 0.0)
self.assertEqual(self.env['stock.quant']._get_available_quantity(product1, stock_location, allow_negative=True), -1.0)
def test_decrease_available_quantity_2(self):
""" Decrease the available quantity when multiple quants are already in a location.
+8 -11
View File
@@ -7,18 +7,15 @@
<form string="Putaway">
<group colspan="4">
<field name="name"/>
<field name="method"/>
</group>
<div attrs="{'invisible': [('method', '!=', 'fixed')]}">
<separator string="Fixed Locations Per Categories"/>
<field name="fixed_location_ids" colspan="4" nolabel="1">
<tree editable="top">
<field name="sequence" widget='handle'/>
<field name="category_id"/>
<field name="fixed_location_id"/>
</tree>
</field>
</div>
<separator string="Fixed Locations Per Categories"/>
<field name="fixed_location_ids" colspan="4" nolabel="1">
<tree editable="top">
<field name="sequence" widget='handle'/>
<field name="category_id"/>
<field name="fixed_location_id"/>
</tree>
</field>
</form>
</field>
</record>
+10 -2
View File
@@ -205,7 +205,7 @@
<span class="o_stat_text">Forecasted</span>
</div>
</button>
<button string="Inventory Moves"
<button string="Product Moves"
type="object"
name= "action_view_stock_move_lines"
attrs="{'invisible':[('type', 'not in', ['product', 'consu'])]}"
@@ -233,6 +233,10 @@
name="action_view_routes"
attrs="{'invisible':[('type', '!=', 'product')]}"
class="oe_stat_button" icon="fa-cogs" groups="base.group_no_one"/>
<button string="Lot/Serial Number" type="object"
name="action_open_product_lot"
attrs="{'invisible': [('tracking', '=', 'none')]}"
class="oe_stat_button" icon="fa-bars" groups="stock.group_production_lot"/>
</button>
</data>
</field>
@@ -268,7 +272,7 @@
<span class="o_stat_text">Forecasted</span>
</div>
</button>
<button string="Inventory Moves" type="object"
<button string="Product Moves" type="object"
name= "action_view_stock_move_lines"
attrs="{'invisible':[('type', 'not in', ['product', 'consu'])]}"
class="oe_stat_button" icon="fa-arrows-v"
@@ -298,6 +302,10 @@
attrs="{'invisible':[('type', '!=', 'product')]}"
class="oe_stat_button" icon="fa-cogs"
groups="base.group_no_one"/>
<button string="Lot/Serial Number" type="object"
name="action_open_product_lot"
attrs="{'invisible': [('tracking', '=', 'none')]}"
class="oe_stat_button" icon="fa-bars" groups="stock.group_production_lot"/>
</button>
<!-- change attrs of fields added in view_template_property_form
@@ -48,10 +48,10 @@
<thead>
<tr class="o_report_header">
<th class="o_report_line_header">PRODUCT</th>
<th class="o_report_line_header"></th>
<th class="o_report_line_header">LOT/SERIAL NUMBER</th>
<th class="o_report_line_header">DATE</th>
<th class="o_report_line_header">QUANTITY</th>
<th class="o_report_line_header">LOCATION SOURCE</th>
<th class="o_report_line_header">LOCATION DESTINATION</th>
<th class="o_report_line_header">LOCATION</th>
<th></th>
</tr>
</thead>
@@ -86,10 +86,10 @@
<thead>
<tr class="o_report_header">
<th class="o_report_line_header">PRODUCT</th>
<th class="o_report_line_header"></th>
<th class="o_report_line_header">LOT/SERIAL NUMBER</th>
<th class="o_report_line_header">DATE</th>
<th class="o_report_line_header">QUANTITY</th>
<th class="o_report_line_header">LOCATION SOURCE</th>
<th class="o_report_line_header">LOCATION DESTINATION</th>
<th class="o_report_line_header">LOCATION</th>
</tr>
</thead>
<tbody>
+1 -1
View File
@@ -355,7 +355,7 @@
</record>
<record id="stock_move_action" model="ir.actions.act_window">
<field name="name">Picking Moves</field>
<field name="name">Stock Moves</field>
<field name="res_model">stock.move</field>
<field name="type">ir.actions.act_window</field>
<field name="view_type">form</field>
+18 -13
View File
@@ -185,19 +185,24 @@
<field name="location_id"/>
</group>
</group>
<notebook>
<page string="Content">
<separator string="Products" />
<field name="current_picking_id" invisible="1"/>
<field name="current_picking_move_line_ids" readonly="1" attrs="{'invisible': [('current_picking_id', '=', False)]}">
<tree>
<field name="product_id"/>
<field name="qty_done" string="Quantity"/>
</tree>
</field>
<field name="quant_ids" attrs="{'invisible': [('current_picking_id', '=', True)]}"/>
</page>
</notebook>
<separator string="Content" />
<field name="current_picking_id" invisible="1"/>
<field name="current_picking_move_line_ids" readonly="1" attrs="{'invisible': [('current_picking_id', '=', False)]}">
<tree>
<field name="product_id"/>
<field name="lot_id" groups="stock.group_production_lot"/>
<field name="qty_done" string="Quantity"/>
<field name="product_uom_id" groups="product.group_uom"/>
</tree>
</field>
<field name="quant_ids" attrs="{'invisible': [('current_picking_id', '=', True)]}">
<tree>
<field name="product_id"/>
<field name="lot_id" groups="stock.group_production_lot"/>
<field name="quantity"/>
<field name="product_uom_id" groups="product.group_uom"/>
</tree>
</field>
</sheet>
</form>
</field>
+26
View File
@@ -74,6 +74,19 @@ class ProductTemplate(models.Model):
})
return accounts
@api.multi
def action_open_product_moves(self):
self.ensure_one()
action = self.env.ref('stock_account.stock_move_valuation_action').read()[0]
action['domain'] = [('product_id.product_tmpl_id', '=', self.id)]
action['context'] = {
'search_default_outgoing': True,
'search_default_incoming': True,
'search_default_done': True,
'is_avg': self.cost_method == 'average',
}
return action
@api.multi
def get_product_accounts(self, fiscal_pos=None):
""" Add the stock journal related to product to the result of super()
@@ -156,6 +169,19 @@ class ProductProduct(models.Model):
moves = StockMove.search(domain)
product.stock_value = sum(moves.mapped('value'))
@api.multi
def action_open_product_moves(self):
self.ensure_one()
action = self.env.ref('stock_account.stock_move_valuation_action').read()[0]
action['domain'] = [('product_id', '=', self.id)]
action['context'] = {
'search_default_outgoing': True,
'search_default_incoming': True,
'search_default_done': True,
'is_avg': self.cost_method == 'average',
}
return action
class ProductCategory(models.Model):
_inherit = 'product.category'
+27 -3
View File
@@ -92,9 +92,11 @@
<field name="model">product.product</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="stock_value" sum="Stock Valuation"/>
<field name="qty_available" context="{'company_owned': True}"/>
<field name="name" string="Product"/>
<field name="qty_available" context="{'company_owned': True}" string="Quantity on Hand"/>
<field name="uom_id" groups="product.group_uom"/>
<field name="currency_id" invisible="1"/>
<field name="stock_value" sum="Stock Valuation" widget="monetary" string="Total Value"/>
</tree>
</field>
</record>
@@ -113,5 +115,27 @@
</p>
</field>
</record>
<record id="product_valuation_form_view" model="ir.ui.view">
<field name="name">product.product</field>
<field name="model">product.product</field>
<field name="inherit_id" ref="stock.product_form_view_procurement_button"/>
<field name="arch" type="xml">
<xpath expr="//button[@name='action_view_stock_move_lines']" position="after">
<button string="Inventory Valuation" type="object" name="action_open_product_moves" class="oe_stat_button" icon="fa-bars" attrs="{'invisible': [('cost_method', '=', 'standard')]}"/>
</xpath>
</field>
</record>
<record id="product_template_valuation_form_view" model="ir.ui.view">
<field name="name">product.template</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="stock.product_template_form_view_procurement_button"/>
<field name="arch" type="xml">
<xpath expr="//button[@name='action_view_stock_move_lines']" position="after">
<button string="Inventory Valuation" type="object" name="action_open_product_moves" class="oe_stat_button" icon="fa-bars" attrs="{'invisible': [('cost_method', '=', 'standard')]}"/>
</xpath>
</field>
</record>
</data>
</odoo>
@@ -68,13 +68,12 @@
<field name="product_uom_qty" string="Qty"/>
<field name="product_uom" options="{'no_open': True, 'no_create': True}" string="Unit of Measure" groups="product.group_uom"/>
<field name="price_unit"/>
<field name="remaining_qty"/>
<field name="value" sum="Stock Valuation"/>
<field name="remaining_value" sum="Stock Valuation"/>
<field name="remaining_qty" invisible="context.get('is_avg')"/>
<field name="remaining_value" sum="Stock Valuation" invisible="context.get('is_avg')"/>
</tree>
</field>
</record>
<record id="stock_move_valuation_action" model="ir.actions.act_window">
<field name="name">Stock Moves</field>
<field name="res_model">stock.move</field>
@@ -877,7 +877,8 @@ var FieldX2Many = AbstractField.extend({
*/
_onEditLine: function (ev) {
ev.stopPropagation();
this.renderer.setRowMode(ev.data.recordID, 'edit')
var editedRecord = this.value.data[ev.data.index];
this.renderer.setRowMode(editedRecord.id, 'edit')
.done(ev.data.onSuccess);
},
/**
@@ -336,20 +336,30 @@ return core.Class.extend({
attrs.mode = mode;
if (mode in attrs.views) {
var view = attrs.views[mode];
// process the default_order, which is like 'name,id desc'
// but we need it like [{name: 'name', asc: true}, {name: 'id', asc: false}]
var defaultOrder = view.arch.attrs.default_order;
if (defaultOrder) {
// process the default_order, which is like 'name,id desc'
// but we need it like [{name: 'name', asc: true}, {name: 'id', asc: false}]
attrs.orderedBy = _.map(defaultOrder.split(','), function (order) {
order = order.trim().split(' ');
return {name: order[0], asc: order[1] !== 'desc'};
});
} else {
// if there is a field with widget `handle`, the x2many
// needs to be ordered by this field to correctly display
// the records
var handleField = _.find(view.arch.children, function (child) {
return child.attrs && child.attrs.widget === 'handle';
});
if (handleField) {
attrs.orderedBy = [{name: handleField.attrs.name, asc: true}];
}
}
// detect editables list has they behave differently with respect
// to the sorting (changes are not sorted directly)
if (mode === 'list' && view.arch.attrs.editable) {
attrs.keepChangesUnsorted = true;
}
}
}
}
if (attrs.Widget.prototype.fieldsToFetch) {
@@ -237,7 +237,7 @@ var AbstractView = Class.extend({
}
defs.push(parent.loadViews(
field.relation,
new Context(context, self.userContext),
new Context(context, self.userContext, self.loadParams.context),
[[null, attrs.mode === 'tree' ? 'list' : attrs.mode]])
.then(function (views) {
for (var viewName in views) {
@@ -780,6 +780,55 @@ var BasicModel = AbstractModel.extend({
parent.data = _.without(parent.data, elementID);
}
},
/**
* Resequences records.
*
* @param {string} modelName the resIDs model
* @param {Array[integer]} resIDs the new sequence of ids
* @param {string} parentID the localID of the parent
* @param {object} [options]
* @param {integer} [options.offset]
* @param {string} [options.field] the field name used as sequence
* @returns {Deferred<string>} resolves to the local id of the parent
*/
resequence: function (modelName, resIDs, parentID, options) {
options = options || {};
if ((resIDs.length <= 1)) {
return $.when(parentID); // there is nothing to sort
}
var self = this;
var data = this.localData[parentID];
var params = {
model: modelName,
ids: resIDs,
};
if (options.offset) {
params.offset = options.offset;
}
if (options.field) {
params.field = options.field;
}
return this._rpc({
route: '/web/dataset/resequence',
params: params,
})
.then(function () {
data.data = _.sortBy(data.data, function (d) {
return _.indexOf(resIDs, self.localData[d].res_id);
});
data.res_ids = [];
_.each(data.data, function (d) {
var dataPoint = self.localData[d];
if (dataPoint.type === 'record') {
data.res_ids.push(dataPoint.res_id);
} else {
data.res_ids = data.res_ids.concat(dataPoint.res_ids);
}
});
self._updateParentResIDs(data);
return parentID;
});
},
/**
* Save a local resource, if needed. This is a complicated operation,
* - it needs to check all changes,
@@ -1186,6 +1235,7 @@ var BasicModel = AbstractModel.extend({
record._rawChanges[name] = val;
return;
}
var oldValue = record._changes[name] || record.data[name];
var id;
if (field.type === 'many2one') {
id = false;
@@ -1198,29 +1248,42 @@ var BasicModel = AbstractModel.extend({
var data = _.isArray(val) ?
{id: val[0], display_name: val[1]} :
{id: val};
rec = self._makeDataPoint({
context: record.context,
data: data,
modelName: field.relation,
parentID: record.id,
});
id = rec.id;
if (!oldValue || (self.localData[oldValue].res_id !== data.id)) {
// only register a change if the value has changed
rec = self._makeDataPoint({
context: record.context,
data: data,
modelName: field.relation,
parentID: record.id,
});
id = rec.id;
record._changes[name] = id;
}
} else {
record._changes[name] = false;
}
record._changes[name] = id;
} else if (field.type === 'reference') {
id = false;
if (val) {
var ref = val.split(',');
rec = self._makeDataPoint({
context: record.context,
data: {id: parseInt(ref[1])},
modelName: ref[0],
parentID: record.id,
});
defs.push(self._fetchNameGet(rec));
id = rec.id;
var modelName = ref[0];
var resID = parseInt(ref[1]);
if (!oldValue || self.localData[oldValue].res_id !== resID ||
self.localData[oldValue].model !== modelName) {
// only register a change if the value has changed
rec = self._makeDataPoint({
context: record.context,
data: {id: parseInt(ref[1])},
modelName: modelName,
parentID: record.id,
});
defs.push(self._fetchNameGet(rec));
id = rec.id;
record._changes[name] = id;
}
} else {
record._changes[name] = id;
}
record._changes[name] = id;
} else if (field.type === 'one2many' || field.type === 'many2many') {
var listId = record._changes[name] || record.data[name];
var list;
@@ -1240,26 +1303,40 @@ var BasicModel = AbstractModel.extend({
viewType: fieldInfo.viewType,
});
}
// TODO: before registering the changes, verify that the x2many
// value has changed
record._changes[name] = list.id;
var shouldLoad = false;
list._changes = list._changes || [];
_.each(val, function (command) {
var rec;
if (command[0] === 0 || command[0] === 1) {
// CREATE or UPDATE
var params = {
context: list.context,
fields: list.fields,
fieldsInfo: list.fieldsInfo,
modelName: list.model,
parentID: list.id,
viewType: list.viewType,
ref: command[1],
};
if (command[0] === 1) {
params.res_id = command[1];
if (command[0] === 1 && command[1]) {
// updating an existing record
var recID = _.find(list.data, function (childID) {
var child = self.localData[childID];
return child.res_id === command[1];
});
rec = self.localData[recID];
}
rec = self._makeDataPoint(params);
list._cache[rec.res_id] = rec.id;
if (!rec) {
var params = {
context: list.context,
fields: list.fields,
fieldsInfo: list.fieldsInfo,
modelName: list.model,
parentID: list.id,
viewType: list.viewType,
ref: command[1],
};
if (command[0] === 1) {
params.res_id = command[1];
}
rec = self._makeDataPoint(params);
list._cache[rec.res_id] = rec.id;
}
list._changes.push({operation: 'ADD', id: rec.id});
if (command[0] === 1) {
list._changes.push({operation: 'UPDATE', id: rec.id});
@@ -1280,7 +1357,10 @@ var BasicModel = AbstractModel.extend({
defs.push(self._readUngroupedList(list));
}
} else {
record._changes[name] = self._parseServerValue(field, val);
var newValue = self._parseServerValue(field, val);
if (newValue !== oldValue) {
record._changes[name] = newValue;
}
}
});
return $.when.apply($, defs);
@@ -614,7 +614,8 @@ return AbstractModel.extend({
if (this.mapping.all_day && evt[this.mapping.all_day]) {
date_stop.add(1, 'days');
}
var isAllDay = this.fields[this.mapping.date_start].type === 'date' ||
this.mapping.all_day && evt[this.mapping.all_day] || false;
var r = {
'record': evt,
'start': date_start,
@@ -622,7 +623,7 @@ return AbstractModel.extend({
'r_start': date_start,
'r_end': date_stop,
'title': the_title,
'allDay': this.mapping.all_day && evt[this.mapping.all_day] || false,
'allDay': isAllDay,
'id': evt.id,
'attendees':attendees,
};
@@ -110,7 +110,6 @@ var KanbanColumn = Widget.extend({
var record = ui.item.data('record');
var index = self.records.indexOf(record);
record.$el.removeAttr('style'); // jqueryui sortable add display:block inline
ui.item.addClass('o_updating');
if (index >= 0) {
if ($.contains(self.$el[0], record.$el[0])) {
// resequencing records
@@ -118,6 +117,7 @@ var KanbanColumn = Widget.extend({
}
} else {
// adding record to this column
ui.item.addClass('o_updating');
self.trigger_up('kanban_column_add_record', {record: record, ids: self._getIDs()});
}
}
@@ -179,41 +179,6 @@ var KanbanModel = BasicModel.extend({
}
return this._super(id, options);
},
/**
* Resequences records.
*
* @param {string} modelName
* @param {Array[integer]} resIDs the new sequence of ids
* @param {string]} parentID the localID of the parent
* @returns {Deferred<string>} resolves to the local id of the parent
*/
resequence: function (modelName, resIDs, parentID) {
if ((resIDs.length <= 1)) {
return $.when(parentID); // there is nothing to sort
}
var self = this;
var data = this.localData[parentID];
return this._rpc({
route: '/web/dataset/resequence',
params: {model: modelName, ids: resIDs},
})
.then(function () {
data.data = _.sortBy(data.data, function (d) {
return _.indexOf(resIDs, self.localData[d].res_id);
});
data.res_ids = [];
_.each(data.data, function (d) {
var dataPoint = self.localData[d];
if (dataPoint.type === 'record') {
data.res_ids.push(dataPoint.res_id);
} else {
data.res_ids = data.res_ids.concat(dataPoint.res_ids);
}
});
self._updateParentResIDs(data);
return parentID;
});
},
//--------------------------------------------------------------------------
// Private
@@ -372,9 +372,16 @@ var ListController = BasicController.extend({
* @param {OdooEvent} ev
*/
_onEditLine: function (ev) {
var self = this;
ev.stopPropagation();
this._setMode('edit', ev.data.recordID)
.done(ev.data.onSuccess);
this.trigger_up('mutexify', {
action: function () {
var record = self.model.get(self.handle);
var editedRecord = record.data[ev.data.index];
self._setMode('edit', editedRecord.id)
.done(ev.data.onSuccess);
},
});
},
/**
* Opens the Export Dialog
@@ -404,23 +411,24 @@ var ListController = BasicController.extend({
* @param {OdooEvent} event
*/
_onResequence: function (event) {
var data = this.model.get(this.handle);
var resIDs = _.map(event.data.rowIDs, function(rowID) {
return _.findWhere(data.data, {id: rowID}).res_id;
})
return this._rpc({
route: '/web/dataset/resequence',
params: {
model: this.modelName,
ids: resIDs,
offset: event.data.offset,
field: event.data.handleField,
var self = this;
this.trigger_up('mutexify', {
action: function () {
var state = self.model.get(self.handle);
var resIDs = _.map(event.data.rowIDs, function(rowID) {
return _.findWhere(state.data, {id: rowID}).res_id;
});
var options = {
offset: event.data.offset,
field: event.data.handleField,
};
return self.model.resequence(self.modelName, resIDs, self.handle, options).then(function () {
self._updateEnv();
state = self.model.get(self.handle);
return self.renderer.updateState(state, {noRender: true});
});
},
}).then(function () {
data.data = _.sortBy(data.data, function (d) {
return _.indexOf(resIDs, d.res_id);
});
return this.handle;
});
},
/**
@@ -437,11 +437,11 @@ ListRenderer.include({
var self = this;
var movedRecordID = ui.item.data('id');
var rowIDs = _.pluck(this.state.data, 'id');
rowIDs = _.without(rowIDs, movedRecordID)
rowIDs = _.without(rowIDs, movedRecordID);
rowIDs.splice(ui.item.index(), 0, movedRecordID);
var sequences = _.map(this.state.data, function(record) {
return record.data[self.handleField];
})
});
this.trigger_up('resequence', {
rowIDs: rowIDs,
offset: _.min(sequences),
@@ -516,10 +516,9 @@ ListRenderer.include({
var self = this;
return this.unselectRow().then(function () {
// Notify the controller we want to make a record editable
var record = self.state.data[rowIndex];
var def = $.Deferred();
self.trigger_up('edit_line', {
recordID: record.id,
index: rowIndex,
onSuccess: def.resolve.bind(def),
});
return def;
@@ -390,7 +390,7 @@ DebugManager.include({
// or m2m
if (!value || invisibleOrReadOnly || fieldInfo.type === 'one2many' ||
fieldInfo.type === 'many2many' || fieldInfo.type === 'binary' ||
fieldsInfo[fieldName].options.isPassword) {
fieldsInfo[fieldName].options.isPassword || !_.isEmpty(fieldInfo.depends)) {
return false;
}
return {
@@ -28,6 +28,9 @@
}
.o_kanban_view.o_kanban_dashboard {
// correctly display the no_content_helper in dashboards
.o-flex-flow(row, wrap);
.o_kanban_record {
position: relative;
.o-flex-display();
@@ -107,6 +107,8 @@ QUnit.module('relational_fields', {
turtle_qux: {string: "Qux", type: "float", digits: [16,1], required: true, default: 1.5},
turtle_description: {string: "Description", type: "text"},
turtle_trululu: {string: "Trululu", type: "many2one", relation: 'partner'},
turtle_ref: {string: "Reference", type: 'reference', selection: [
["product", "Product"], ["partner", "Partner"]]},
product_id: {string: "Product", type: "many2one", relation: 'product', required: true},
partner_ids: {string: "Partner", type: "many2many", relation: 'partner'},
},
@@ -126,11 +128,13 @@ QUnit.module('relational_fields', {
}, {
id: 3,
display_name: "raphael",
product_id: 37,
turtle_bar: false,
turtle_foo: "kawa",
turtle_int: 21,
turtle_qux: 9.8,
partner_ids: [],
turtle_ref: 'product,37',
}],
},
user: {
@@ -1950,6 +1954,67 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('embedded one2many (editable list) with handle widget', function (assert) {
assert.expect(9);
this.data.partner.records[0].p = [1, 2, 4];
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<sheet>' +
'<notebook>' +
'<page string="P page">' +
'<field name="p">' +
'<tree editable="top">' +
'<field name="int_field" widget="handle"/>' +
'<field name="foo"/>' +
'</tree>' +
'</field>' +
'</page>' +
'</notebook>' +
'</sheet>' +
'</form>',
res_id: 1,
});
testUtils.intercept(form, "field_changed", function (event) {
assert.step(event.data.changes.p.data.int_field.toString());
}, true);
assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "My little Foo Valueblipyop",
"should have the 3 rows in the correct order");
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "My little Foo Valueblipyop",
"should still have the 3 rows in the correct order");
// Drag and drop the second line in first position
testUtils.dragAndDrop(
form.$('.ui-sortable-handle').eq(1),
form.$('tbody tr').first(),
{position: 'top'}
);
assert.verifySteps(["0", "1", "2"],
"sequences values should be incremental starting from the previous minimum one");
assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipMy little Foo Valueyop",
"should have the 3 rows in the new order");
form.$('tbody tr:first td:first').click();
assert.strictEqual(form.$('tbody tr:first td.o_data_cell:not(.o_handle_cell) input').val(), "blip",
"should edit the correct row");
form.$buttons.find('.o_form_button_save').click();
assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipMy little Foo Valueyop",
"should still have the 3 rows in the new order");
form.destroy();
});
QUnit.test('one2many field when using the pager', function (assert) {
assert.expect(13);
@@ -3356,6 +3421,72 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('one2many and onchange only write modified field', function (assert) {
assert.expect(2);
this.data.partner.onchanges = {
turtles: function (obj) {
obj.turtles = [
[5], // delete all
[1, 3, { // the server returns all fields
display_name: "coucou",
product_id: [37, "xphone"],
turtle_bar: false,
turtle_foo: "has changed",
turtle_int: 42,
turtle_qux: 9.8,
partner_ids: [],
turtle_ref: 'product,37',
}],
];
},
};
this.data.partner.records[0].turtles = [3];
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<field name="foo"/>' +
'<field name="turtles">' +
'<tree editable="bottom">' +
'<field name="display_name"/>' +
'<field name="product_id"/>' +
'<field name="turtle_bar"/>' +
'<field name="turtle_foo"/>' +
'<field name="turtle_int"/>' +
'<field name="turtle_qux"/>' +
'<field name="turtle_ref"/>' +
'</tree>' +
'</field>' +
'</form>',
mockRPC: function (method, args) {
if (args.method === 'write') {
// debugger;
assert.deepEqual(args.args[1].turtles, [
[1, 3, {display_name: 'coucou', turtle_foo: 'has changed', turtle_int: 42}],
], "correct commands should be sent (only send changed values)");
}
return this._super.apply(this, arguments);
},
res_id: 1,
viewOptions: {
mode: 'edit',
},
});
assert.strictEqual(form.$('.o_data_row').length, 1,
"o2m should contain one row");
form.$('.o_field_one2many .o_list_view tbody tr:first td:first').click();
form.$('.o_field_one2many .o_list_view tbody tr:first input:first').val('blurp').trigger('input');
form.$buttons.find('.o_form_button_save').click();
form.destroy();
});
QUnit.test('one2many list (editable): readonly domain is evaluated', function (assert) {
assert.expect(2);
+16 -7
View File
@@ -57,16 +57,22 @@ var MockServer = Class.extend({
* 'field_view_get' call to the server. Calls processViews() of data_manager
* to mimick the real behavior of a call to loadViews().
*
* @param {string|Object} arch a string OR a parsed xml document
* @param {string} model a model name (that should be in this.data)
* @param {Object} params
* @param {string|Object} params.arch a string OR a parsed xml document
* @param {string} params.model a model name (that should be in this.data)
* @param {Object} params.toolbar the actions possible in the toolbar
* @param {Object} [params.viewOptions] the view options set in the test (optional)
* @returns {Object} an object with 2 keys: arch and fields
*/
fieldsViewGet: function (arch, model, toolbar) {
fieldsViewGet: function (params) {
var model = params.model;
var toolbar = params.toolbar;
var viewOptions = params.viewOptions || {};
if (!(model in this.data)) {
throw new Error('Model ' + model + ' was not defined in mock server data');
}
var fields = $.extend(true, {}, this.data[model].fields);
var fvg = this._fieldsViewGet(arch, model, fields);
var fvg = this._fieldsViewGet(params.arch, model, fields, viewOptions.context);
var fields_views = {};
fields_views[fvg.type] = fvg;
data_manager.processViews(fields_views, fields);
@@ -155,10 +161,11 @@ var MockServer = Class.extend({
* @param {string|Object} arch a string OR a parsed xml document
* @param {string} model a model name (that should be in this.data)
* @param {Object} fields
* @param {Object} context
* @returns {Object} an object with 2 keys: arch and fields (the fields
* appearing in the views)
*/
_fieldsViewGet: function (arch, model, fields) {
_fieldsViewGet: function (arch, model, fields, context) {
var self = this;
var modifiersNames = ['invisible', 'readonly', 'required'];
var onchanges = this.data[model].onchanges || {};
@@ -224,7 +231,8 @@ var MockServer = Class.extend({
}
_.each(modifiersNames, function (a) {
if (node.attrs[a]) {
var v = pyeval.py_eval(node.attrs[a]) ? true: false;
var pyevalContext = window.py.dict.fromJSON(context || {});
var v = pyeval.py_eval(node.attrs[a], {context: pyevalContext}) ? true: false;
if (inTreeView && a === 'invisible') {
modifiers['tree_invisible'] = v;
} else if (v || !(a in modifiers) || !_.isArray(modifiers[a])) {
@@ -252,7 +260,8 @@ var MockServer = Class.extend({
_.each(node.children, function (children) {
relModel = field.relation;
relFields = $.extend(true, {}, self.data[relModel].fields);
field.views[children.tag] = self._fieldsViewGet(children, relModel, relFields);
field.views[children.tag] = self._fieldsViewGet(children, relModel,
relFields, context);
});
}
@@ -131,7 +131,7 @@ function createAsyncView(params) {
// add mock environment: mock server, session, fieldviewget, ...
var mockServer = addMockEnvironment(widget, params);
var viewInfo = mockServer.fieldsViewGet(params.arch, params.model, params.toolbar);
var viewInfo = mockServer.fieldsViewGet(params);
// create the view
var viewOptions = {
@@ -329,10 +329,17 @@ function addMockEnvironment(widget, params) {
var view_type = view_descr[1];
var key = [model, view_id, view_type].join(',');
var arch = params.archs[key];
var viewParams = {
arch: arch,
model: model,
viewOptions: {
context: event.data.context.eval(),
},
};
if (!arch) {
throw new Error('No arch found for key ' + key);
}
views[view_type] = mockServer.fieldsViewGet(arch, model);
views[view_type] = mockServer.fieldsViewGet(viewParams);
});
event.data.on_success(views);
@@ -1257,6 +1257,37 @@ QUnit.module('Views', {
calendar.destroy();
$view.remove();
});
QUnit.test('set event as all day when field is date', function (assert) {
assert.expect(1);
this.data.event.records[0].start_date = "2016-12-14";
var calendar = createView({
View: CalendarView,
model: 'event',
data: this.data,
arch:
'<calendar class="o_calendar_test" '+
'event_open_popup="true" '+
'date_start="start_date" '+
'all_day="allday" '+
'mode="week" '+
'attendee="partner_ids" '+
'color="partner_id">'+
'<field name="name"/>'+
'<filter name="user_id" avatar_field="image"/>'+
'<field name="partner_ids" write_model="filter_partner" write_field="partner_id"/>'+
'</calendar>',
archs: archs,
viewOptions: {
initialDate: initialDate,
},
});
assert.strictEqual(calendar.$('.fc-day-grid .fc-event-container').length, 1,
"should be one event in the all day row");
calendar.destroy();
});
});
});
+56 -3
View File
@@ -3921,7 +3921,7 @@ QUnit.module('Views', {
QUnit.test('*_view_ref in context are passed correctly', function (assert) {
var done = assert.async();
assert.expect(4);
assert.expect(3);
createAsyncView({
View: FormView,
@@ -3938,8 +3938,6 @@ QUnit.module('Views', {
var context = event.data.context.eval();
assert.strictEqual(context.tree_view_ref, 'module.tree_view_ref',
"context should contain tree_view_ref");
assert.notOk('some_context' in context,
"should not send record's context to load p's fields_view");
event.data.on_success();
}
},
@@ -5790,6 +5788,61 @@ QUnit.module('Views', {
form.destroy();
});
QUnit.test('process the context for inline subview', function (assert) {
assert.expect(1);
this.data.partner.records[0].p = [2];
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="p">' +
'<tree>' +
'<field name="foo"/>' +
'<field name="bar" invisible="context.get(\'hide_bar\', False)"/>' +
'</tree>' +
'</field>' +
'</form>',
res_id: 1,
viewOptions: {
context: {hide_bar: true},
},
});
assert.strictEqual(form.$('.o_list_view thead tr th').length, 1,
"there should be only one column");
form.destroy();
});
QUnit.test('process the context for subview not inline', function (assert) {
assert.expect(1);
this.data.partner.records[0].p = [2];
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="p"/>' +
'</form>',
archs: {
"partner,false,list": '<tree>' +
'<field name="foo"/>' +
'<field name="bar" invisible="context.get(\'hide_bar\', False)"/>' +
'</tree>',
},
res_id: 1,
viewOptions: {
context: {hide_bar: true},
},
});
assert.strictEqual(form.$('.o_list_view thead tr th').length, 1,
"there should be only one column");
form.destroy();
});
QUnit.test('rainbowman attributes correctly passed on button click', function (assert) {
assert.expect(1);
@@ -1801,6 +1801,75 @@ QUnit.module('Views', {
kanban.destroy();
});
QUnit.test('resequence a record twice', function (assert) {
assert.expect(10);
this.data.partner.records = [];
var nbResequence = 0;
var kanban = createView({
View: KanbanView,
model: 'partner',
data: this.data,
arch: '<kanban>' +
'<field name="product_id"/>' +
'<templates><t t-name="kanban-box">' +
'<div><field name="display_name"/></div>' +
'</t></templates></kanban>',
groupBy: ['product_id'],
mockRPC: function (route) {
if (route === '/web/dataset/resequence') {
nbResequence++;
return $.when();
}
return this._super.apply(this, arguments);
},
});
kanban.$('.o_column_quick_create').click();
kanban.$('.o_column_quick_create input').val('column1');
kanban.$('.o_column_quick_create button.o_kanban_add').click();
kanban.$('.o_kanban_group:eq(0) .o_kanban_quick_add i').click();
var $quickCreate = kanban.$('.o_kanban_group:eq(0) .o_kanban_quick_create');
$quickCreate.find('input').val('record1');
$quickCreate.find('button.o_kanban_add').click();
kanban.$('.o_kanban_group:eq(0) .o_kanban_quick_add i').click();
$quickCreate = kanban.$('.o_kanban_group:eq(0) .o_kanban_quick_create');
$quickCreate.find('input').val('record2');
$quickCreate.find('button.o_kanban_add').click();
assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_record').length, 2,
"column should contain 2 records");
assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_record:eq(0)').text(), "record2",
"records should be correctly ordered");
assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_record:eq(1)').text(), "record1",
"records should be correctly ordered");
var $record1 = kanban.$('.o_kanban_group:eq(0) .o_kanban_record:eq(1)');
var $record2 = kanban.$('.o_kanban_group:eq(0) .o_kanban_record:eq(0)');
testUtils.dragAndDrop($record1, $record2, {position: 'top'});
assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_record').length, 2,
"column should contain 2 records");
assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_record:eq(0)').text(), "record1",
"records should be correctly ordered");
assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_record:eq(1)').text(), "record2",
"records should be correctly ordered");
testUtils.dragAndDrop($record2, $record1, {position: 'top'});
assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_record').length, 2,
"column should contain 2 records");
assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_record:eq(0)').text(), "record2",
"records should be correctly ordered");
assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_record:eq(1)').text(), "record1",
"records should be correctly ordered");
assert.strictEqual(nbResequence, 2, "should have resequenced twice");
kanban.destroy();
});
QUnit.test('don\'t fold column quick create after creation', function (assert) {
assert.expect(2);
+138 -2
View File
@@ -2586,7 +2586,7 @@ QUnit.module('Views', {
assert.strictEqual(list.$('tbody tr:eq(2) td:last').text(), '300',
"default third record should have amount 300");
assert.strictEqual(list.$('tbody tr:eq(3) td:last').text(), '0',
"default third record should have amount 0");
"default fourth record should have amount 0");
// Drag and drop the fourth line in second position
testUtils.dragAndDrop(
@@ -2602,7 +2602,143 @@ QUnit.module('Views', {
assert.strictEqual(list.$('tbody tr:eq(2) td:last').text(), '500',
"new third record should have amount 500");
assert.strictEqual(list.$('tbody tr:eq(3) td:last').text(), '300',
"new third record should have amount 300");
"new fourth record should have amount 300");
list.destroy();
});
QUnit.test('editable list with handle widget', function (assert) {
assert.expect(12);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="top">' +
'<field name="int_field" widget="handle"/>' +
'<field name="amount" widget="float" digits="[5,0]"/>' +
'</tree>',
mockRPC: function (route, args) {
if (route === '/web/dataset/resequence') {
assert.strictEqual(args.offset, -4,
"should write the sequence starting from the lowest current one");
assert.strictEqual(args.field, 'int_field',
"should write the right field as sequence");
assert.deepEqual(args.ids, [1, 4, 2, 3],
"should write the sequence in correct order");
return $.when();
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual(list.$('tbody tr:eq(0) td:last').text(), '1200',
"default first record should have amount 1200");
assert.strictEqual(list.$('tbody tr:eq(1) td:last').text(), '500',
"default second record should have amount 500");
assert.strictEqual(list.$('tbody tr:eq(2) td:last').text(), '300',
"default third record should have amount 300");
assert.strictEqual(list.$('tbody tr:eq(3) td:last').text(), '0',
"default fourth record should have amount 0");
// Drag and drop the fourth line in second position
testUtils.dragAndDrop(
list.$('.ui-sortable-handle').eq(3),
list.$('tbody tr').first(),
{position: 'bottom'}
);
assert.strictEqual(list.$('tbody tr:eq(0) td:last').text(), '1200',
"new first record should have amount 1200");
assert.strictEqual(list.$('tbody tr:eq(1) td:last').text(), '0',
"new second record should have amount 0");
assert.strictEqual(list.$('tbody tr:eq(2) td:last').text(), '500',
"new third record should have amount 500");
assert.strictEqual(list.$('tbody tr:eq(3) td:last').text(), '300',
"new fourth record should have amount 300");
list.$('tbody tr:eq(1) td:last').click();
assert.strictEqual(list.$('tbody tr:eq(1) td:last input').val(), '0',
"the edited record should be the good one");
list.destroy();
});
QUnit.test('editable list with handle widget with slow network', function (assert) {
assert.expect(15);
var def = $.Deferred();
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="top">' +
'<field name="int_field" widget="handle"/>' +
'<field name="amount" widget="float" digits="[5,0]"/>' +
'</tree>',
mockRPC: function (route, args) {
if (route === '/web/dataset/resequence') {
assert.strictEqual(args.offset, -4,
"should write the sequence starting from the lowest current one");
assert.strictEqual(args.field, 'int_field',
"should write the right field as sequence");
assert.deepEqual(args.ids, [1, 4, 2, 3],
"should write the sequence in correct order");
return $.when(def);
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual(list.$('tbody tr:eq(0) td:last').text(), '1200',
"default first record should have amount 1200");
assert.strictEqual(list.$('tbody tr:eq(1) td:last').text(), '500',
"default second record should have amount 500");
assert.strictEqual(list.$('tbody tr:eq(2) td:last').text(), '300',
"default third record should have amount 300");
assert.strictEqual(list.$('tbody tr:eq(3) td:last').text(), '0',
"default fourth record should have amount 0");
// drag and drop the fourth line in second position
testUtils.dragAndDrop(
list.$('.ui-sortable-handle').eq(3),
list.$('tbody tr').first(),
{position: 'bottom'}
);
// edit moved row before the end of resequence
list.$('tbody tr:eq(3) td:last').click();
assert.strictEqual(list.$('tbody tr:eq(3) td:last input').length, 0,
"shouldn't edit the line before resequence");
def.resolve();
assert.strictEqual(list.$('tbody tr:eq(3) td:last input').length, 1,
"should edit the line after resequence");
assert.strictEqual(list.$('tbody tr:eq(3) td:last input').val(), '300',
"fourth record should have amount 300");
list.$('tbody tr:eq(3) td:last input').val(301).trigger('input');
list.$('tbody tr:eq(0) td:last').click();
list.$buttons.find('.o_list_button_save').click();
assert.strictEqual(list.$('tbody tr:eq(0) td:last').text(), '1200',
"first record should have amount 1200");
assert.strictEqual(list.$('tbody tr:eq(1) td:last').text(), '0',
"second record should have amount 1");
assert.strictEqual(list.$('tbody tr:eq(2) td:last').text(), '500',
"third record should have amount 500");
assert.strictEqual(list.$('tbody tr:eq(3) td:last').text(), '301',
"fourth record should have amount 301");
list.$('tbody tr:eq(3) td:last').click();
assert.strictEqual(list.$('tbody tr:eq(3) td:last input').val(), '301',
"fourth record should have amount 301");
list.destroy();
});
+17
View File
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import re
import odoo.tests
@@ -11,3 +12,19 @@ class WebSuite(odoo.tests.HttpCase):
def test_01_js(self):
self.phantom_js('/web/tests?mod=web', "", "", login='admin', timeout=300)
def test_check_suite(self):
# verify no js test is using `QUnit.only` as it forbid any other test to be executed
re_only = re.compile('QUnit\.only\(')
# As we currently aren't in a request context, we can't render `web.layout`.
# redefinied it as a minimal proxy template.
self.env.ref('web.layout').write({'arch_db': '<t t-name="web.layout"><t t-raw="head"/></t>'})
for asset in self.env['ir.qweb']._get_asset_content('web.qunit_suite', options={})[0]:
filename = asset['filename']
if not filename or asset['atype'] != 'text/javascript':
continue
with open(filename, 'r') as fp:
if re_only.search(fp.read()):
self.fail("`QUnit.only()` used in file %r" % asset['url'])
@@ -496,7 +496,7 @@ dom.removeBetween = function (sc, so, ec, eo, towrite) {
};
for (var i=0; i<nodes.length; i++) {
if (!dom.ancestor(nodes[i], ancestor_first_last) && !$.contains(nodes[i], before) && !$.contains(nodes[i], after)) {
if (!dom.ancestor(nodes[i], ancestor_first_last) && !$.contains(nodes[i], before) && !$.contains(nodes[i], after) && !dom.isEditable(nodes[i])) {
nodes[i].parentNode.removeChild(nodes[i]);
}
}
@@ -1082,6 +1082,9 @@ $.summernote.pluginEvents.enter = function (event, editor, layoutInfo) {
// double enter on the end of a blockquote & pre = new line out of the list
$('<p></p>').append(br).insertAfter($(r.sc).closest('blockquote, pre'));
node = br;
} else if (dom.isEditable(dom.node(r.sc))) {
// if we are directly in an editable, only SHIFT + ENTER should add a newline
node = null;
} else if (last === r.sc) {
if (dom.isBR(last)) {
last = last.parentNode;
@@ -681,7 +681,7 @@ var VideoDialog = Widget.extend({
if (this.$('.o_video_dialog_iframe').is('iframe')) {
var $content = $(
'<div class="media_iframe_video" data-src="' + this.$content.attr('src') + '">'+
'<div class="media_iframe_video" data-oe-expression="' + this.$content.attr('src') + '">'+
'<div class="css_editable_mode_display">&nbsp;</div>'+
'<div class="media_iframe_video_size" contenteditable="false">&nbsp;</div>'+
'<iframe src="' + this.$content.attr('src') + '" frameborder="0" contenteditable="false"></iframe>'+
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from ast import literal_eval
from odoo import api, fields, models
from odoo.exceptions import AccessDenied
@@ -79,3 +81,10 @@ class ResConfigSettings(models.TransientModel):
set_param('website.has_google_analytics_dashboard', self.has_google_analytics_dashboard)
set_param('website.has_google_maps', self.has_google_maps)
set_param('google_maps_api_key', (self.google_maps_api_key or '').strip())
@api.multi
def open_template_user(self):
action = self.env.ref('base.action_res_users').read()[0]
action['res_id'] = literal_eval(self.env['ir.config_parameter'].sudo().get_param('auth_signup.template_user_id', 'False'))
action['views'] = [[self.env.ref('base.view_users_form').id, 'form']]
return action
@@ -588,7 +588,8 @@ registry.mediaVideo = Animation.extend({
var size = '<div class="media_iframe_video_size">&nbsp;</div>';
this.$target.html(editor+size);
}
this.$target.html(this.$target.html()+'<iframe src="'+_.escape(this.$target.data("src"))+'" frameborder="0" allowfullscreen="allowfullscreen"></iframe>');
// rebuilding the iframe, from https://www.html5rocks.com/en/tutorials/security/sandboxed-iframes/
this.$target.html(this.$target.html()+'<iframe sandbox="allow-scripts allow-same-origin" src="'+_.escape(this.$target.data("oe-expression"))+'" frameborder="0" allowfullscreen="allowfullscreen"></iframe>');
return this._super.apply(this, arguments);
},
});
@@ -148,6 +148,11 @@
<div class="mt8">
<field name="auth_signup_uninvited" widget="radio" required="True"/>
</div>
<div class="content-group">
<div class="mt16">
<button type="object" name="open_template_user" string="Default Access Rights" icon="fa-arrow-right" class="btn-link"/>
</div>
</div>
</div>
</div>
<div class="col-xs-12 col-md-6 o_setting_box" id="cdn_setting" title="A CDN helps you serve your website’s content with high availability and high performance to any visitor wherever they are located." groups="base.group_no_one">
@@ -13,6 +13,9 @@ odoo.define('website_forum.website_forum', function (require) {
return $.Deferred().reject("DOM doesn't contain '.website_forum'");
}
// pull-left class messes up the post layout OPW 769721
$('span[data-oe-model="forum.post"][data-oe-field="content"]').find('img.pull-left').removeClass('pull-left');
$("[data-toggle='popover']").popover();
$('.karma_required').on('click', function (ev) {
var karma = $(ev.currentTarget).data('karma');
@@ -410,6 +413,9 @@ odoo.define('website_forum.website_forum', function (require) {
toolbar: toolbar,
styleWithSpan: false
});
// pull-left class messes up the post layout OPW 769721
$form.find('.note-editable').find('img.pull-left').removeClass('pull-left');
$form.on('click', 'button, .a-submit', function () {
$textarea.html($form.find('.note-editable').code());
});
+1 -1
View File
@@ -6,7 +6,7 @@
'website': 'https://www.odoo.com/page/e-commerce',
'version': '1.0',
'description': "",
'depends': ['website', 'sale', 'sale_payment', 'payment', 'website_payment', 'website_mail', 'website_form', 'website_rating'],
'depends': ['website', 'sale_payment', 'website_payment', 'website_mail', 'website_form', 'website_rating'],
'data': [
'security/ir.model.access.csv',
'security/website_sale.xml',
+3 -3
View File
@@ -686,11 +686,11 @@ class WebsiteSale(http.Controller):
values = {
'website_sale_order': order,
'post': post,
'escape': lambda x: x.replace("'", r"\'")
'escape': lambda x: x.replace("'", r"\'"),
'partner': order.partner_id.id,
'order': order,
}
values.update(request.env['sale.order']._get_website_data(order))
return request.render("website_sale.extra_info", values)
# ------------------------------------------------------
+3 -2
View File
@@ -48,6 +48,7 @@ psycopg2.extensions.register_type(psycopg2.extensions.new_type((700, 701, 1700,)
from . import tools
from .tools.func import frame_codeinfo
from .tools import pycompat
from .tools import parse_version as pv
if pv(psycopg2.__version__) < pv('2.7'):
@@ -58,8 +59,8 @@ if pv(psycopg2.__version__) < pv('2.7'):
raise ValueError("A string literal cannot contain NUL (0x00) characters.")
return QuotedString(adapted)
psycopg2.extensions.register_adapter(str, adapt_string)
psycopg2.extensions.register_adapter(unicode, adapt_string)
for type_ in pycompat.string_types:
psycopg2.extensions.register_adapter(type_, adapt_string)
from datetime import timedelta
import threading
+10 -6
View File
@@ -23,9 +23,13 @@ def _convert_nn_fr(val):
if val < 20:
return to_19_fr[val]
for (dcap, dval) in ((k, 20 + (10 * v)) for (v, k) in enumerate(tens_fr)):
if dval + 10 > val:
if val % 10:
return dcap + '-' + to_19_fr[val % 10]
base = 10
if dval in [60, 80]:
base = 20
if dval + base > val:
if val % base:
bond = (val % base in [1, 11] and dval != 80) and '-et-' or '-'
return dcap + bond + to_19_fr[val % base]
return dcap
def _convert_nnn_fr(val):
@@ -38,7 +42,7 @@ def _convert_nnn_fr(val):
word = ''
(mod, rem) = (val % 100, val // 100)
if rem > 0:
word = to_19_fr[rem] + ' Cent'
word = (rem > 1 and to_19_fr[rem] + ' cents') or 'Cent'
if mod > 0:
word += ' '
if mod > 0:
@@ -93,7 +97,7 @@ def _convert_nn_nl(val):
for (dcap, dval) in ((k, 20 + (10 * v)) for (v, k) in enumerate(tens_nl)):
if dval + 10 > val:
if val % 10:
return dcap + '-' + to_19_nl[val % 10]
return to_19_nl[val % 10] + '-en-' + dcap
return dcap
def _convert_nnn_nl(val):
@@ -106,7 +110,7 @@ def _convert_nnn_nl(val):
word = ''
(mod, rem) = (val % 100, val // 100)
if rem > 0:
word = to_19_nl[rem] + ' Honderd'
word = (rem > 1 and to_19_nl[rem] + 'honderd') or 'Honderd'
if mod > 0:
word += ' '
if mod > 0:
+4
View File
@@ -9,6 +9,8 @@ from PIL import ImageEnhance
from random import randrange
# Preload PIL with the minimal subset of image formats we need
from odoo.tools import pycompat
Image.preinit()
Image._initialized = 2
@@ -253,6 +255,8 @@ def image_get_resized_images(base64_source, return_big=False, return_medium=True
previous parameters.
"""
return_dict = dict()
if isinstance(base64_source, pycompat.text_type):
base64_source = base64_source.encode('ascii')
if return_big:
return_dict[big_name] = image_resize_image_big(base64_source, avoid_if_small=avoid_resize_big)
if return_medium:
+7 -7
View File
@@ -24,19 +24,19 @@ _CONFDELTYPES = {
def existing_tables(cr, tablenames):
""" Return the names of existing tables among ``tablenames``. """
query = """
SELECT relname
FROM pg_class
WHERE relname IN %s
AND relkind IN ('r', 'v', 'm')
SELECT c.relname
FROM pg_class c
JOIN pg_namespace n ON (n.oid = c.relnamespace)
WHERE c.relname IN %s
AND c.relkind IN ('r', 'v', 'm')
AND n.nspname = 'public'
"""
cr.execute(query, [tuple(tablenames)])
return [row[0] for row in cr.fetchall()]
def table_exists(cr, tablename):
""" Return whether the given table exists. """
query = "SELECT 1 FROM pg_class WHERE relkind IN ('r', 'v', 'm') AND relname=%s"
cr.execute(query, (tablename,))
return cr.rowcount
return len(existing_tables(cr, {tablename})) == 1
def table_kind(cr, tablename):
""" Return the kind of a table: ``'r'`` (regular table), ``'v'`` (view),
+1 -1
View File
@@ -153,7 +153,7 @@ def main():
elif len(sys.argv) == 2 and sys.argv[1] in cmds:
cmds[sys.argv[1]]()
else:
sys.exit('Unknow command. Command available: %r' % (list(cmds,))
sys.exit('Unknow command. Command available: %r' % (list(cmds,)))
if __name__ == "__main__":
main()