[MERGE] forward port branch saas-17 up to d08b4407be
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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,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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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]],
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')):
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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"> </div>'+
|
||||
'<div class="media_iframe_video_size" contenteditable="false"> </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"> </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());
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user