[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/ir.model.access.csv',
|
||||||
'security/account_analytic_default_security.xml',
|
'security/account_analytic_default_security.xml',
|
||||||
'views/account_analytic_default_view.xml',
|
'views/account_analytic_default_view.xml',
|
||||||
'views/product_views.xml',
|
|
||||||
'views/res_config_settings_views.xml',
|
'views/res_config_settings_views.xml',
|
||||||
],
|
],
|
||||||
'installable': True,
|
'installable': True,
|
||||||
|
|||||||
@@ -3,4 +3,3 @@
|
|||||||
|
|
||||||
from . import account_analytic_default
|
from . import account_analytic_default
|
||||||
from . import sale_order_line
|
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 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# 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 import api, fields, models
|
||||||
|
|
||||||
|
|
||||||
@@ -22,7 +24,7 @@ class ResConfigSettings(models.TransientModel):
|
|||||||
res.update(
|
res.update(
|
||||||
auth_signup_reset_password=get_param('auth_signup.reset_password', 'False').lower() == 'true',
|
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_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
|
return res
|
||||||
|
|
||||||
@@ -38,6 +40,6 @@ class ResConfigSettings(models.TransientModel):
|
|||||||
@api.multi
|
@api.multi
|
||||||
def open_template_user(self):
|
def open_template_user(self):
|
||||||
action = self.env.ref('base.action_res_users').read()[0]
|
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']]
|
action['views'] = [[self.env.ref('base.view_users_form').id, 'form']]
|
||||||
return action
|
return action
|
||||||
|
|||||||
@@ -19,7 +19,11 @@
|
|||||||
<field name="auth_signup_uninvited" widget="radio" required="True"/>
|
<field name="auth_signup_uninvited" widget="radio" required="True"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="content-group" attrs="{'invisible': [('auth_signup_uninvited','=','b2b')]}">
|
<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"/>
|
<button type="object" name="open_template_user" string="Default Access Rights" icon="fa-arrow-right" class="btn-link"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -297,6 +297,8 @@ class Import(models.TransientModel):
|
|||||||
if all(val.lower() in ('true', 'false', 't', 'f', '') for val in preview_values):
|
if all(val.lower() in ('true', 'false', 't', 'f', '') for val in preview_values):
|
||||||
return ['boolean']
|
return ['boolean']
|
||||||
# If all values can be cast to float, type is either float or monetary
|
# 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:
|
try:
|
||||||
thousand_separator = decimal_separator = False
|
thousand_separator = decimal_separator = False
|
||||||
for val in preview_values:
|
for val in preview_values:
|
||||||
@@ -328,7 +330,7 @@ class Import(models.TransientModel):
|
|||||||
if thousand_separator and not options.get('float_decimal_separator'):
|
if thousand_separator and not options.get('float_decimal_separator'):
|
||||||
options['float_thousand_separator'] = thousand_separator
|
options['float_thousand_separator'] = thousand_separator
|
||||||
options['float_decimal_separator'] = decimal_separator
|
options['float_decimal_separator'] = decimal_separator
|
||||||
return ['float', 'monetary']
|
results = ['float', 'monetary']
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
# Try to see if all values are a date or datetime
|
# 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)
|
current_date_pattern = check_patterns(date_patterns, preview_values)
|
||||||
if current_date_pattern:
|
if current_date_pattern:
|
||||||
options['date_format'] = current_date_pattern
|
options['date_format'] = current_date_pattern
|
||||||
return ['date']
|
results += ['date']
|
||||||
|
|
||||||
current_datetime_pattern = check_patterns(datetime_patterns, preview_values)
|
current_datetime_pattern = check_patterns(datetime_patterns, preview_values)
|
||||||
if current_datetime_pattern:
|
if current_datetime_pattern:
|
||||||
options['datetime_format'] = 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']
|
return ['id', 'text', 'char', 'datetime', 'selection', 'many2one', 'one2many', 'many2many', 'html']
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
@@ -492,7 +496,7 @@ class Import(models.TransientModel):
|
|||||||
'headers_type': header_types or False,
|
'headers_type': header_types or False,
|
||||||
'preview': preview,
|
'preview': preview,
|
||||||
'options': options,
|
'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'),
|
'debug': self.user_has_groups('base.group_no_one'),
|
||||||
}
|
}
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# 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
|
from odoo.http import Controller, route, request
|
||||||
|
|
||||||
|
|||||||
@@ -87,9 +87,9 @@ class DeliveryCarrier(models.Model):
|
|||||||
return False
|
return False
|
||||||
if self.state_ids and partner.state_id not in self.state_ids:
|
if self.state_ids and partner.state_id not in self.state_ids:
|
||||||
return False
|
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
|
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 False
|
||||||
return True
|
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 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:
|
if request.session.uid:
|
||||||
anonymous_name = request.env.user.name
|
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')
|
@http.route('/im_livechat/feedback', type='json', auth='public')
|
||||||
def feedback(self, uuid, rate, reason=None, **kwargs):
|
def feedback(self, uuid, rate, reason=None, **kwargs):
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ var ActivityMenu = Widget.extend({
|
|||||||
template:'mail.chat.ActivityMenu',
|
template:'mail.chat.ActivityMenu',
|
||||||
events: {
|
events: {
|
||||||
"click": "_onActivityMenuClick",
|
"click": "_onActivityMenuClick",
|
||||||
"click .o_activity_filter_button, .o_mail_channel_preview": "_onActivityFilterClick",
|
"click .o_mail_channel_preview": "_onActivityFilterClick",
|
||||||
},
|
},
|
||||||
start: function () {
|
start: function () {
|
||||||
this.$activities_preview = this.$('.o_mail_navbar_dropdown_channels');
|
this.$activities_preview = this.$('.o_mail_navbar_dropdown_channels');
|
||||||
@@ -252,19 +252,19 @@ var ActivityMenu = Widget.extend({
|
|||||||
* @param {MouseEvent} event
|
* @param {MouseEvent} event
|
||||||
*/
|
*/
|
||||||
_onActivityFilterClick: function (event) {
|
_onActivityFilterClick: function (event) {
|
||||||
event.stopPropagation();
|
// fetch the data from the button otherwise fetch the ones from the parent (.o_mail_channel_preview).
|
||||||
var $target = $(event.currentTarget);
|
var data = _.extend({}, $(event.currentTarget).data(), $(event.target).data());
|
||||||
var context = {};
|
var context = {};
|
||||||
if ($target.data('filter')=='my') {
|
if (data.filter === 'my') {
|
||||||
context['search_default_activities_overdue'] = 1;
|
context['search_default_activities_overdue'] = 1;
|
||||||
context['search_default_activities_today'] = 1;
|
context['search_default_activities_today'] = 1;
|
||||||
} else {
|
} else {
|
||||||
context['search_default_activities_' + $target.data('filter')] = 1;
|
context['search_default_activities_' + data.filter] = 1;
|
||||||
}
|
}
|
||||||
this.do_action({
|
this.do_action({
|
||||||
type: 'ir.actions.act_window',
|
type: 'ir.actions.act_window',
|
||||||
name: $target.data('model_name'),
|
name: data.model_name,
|
||||||
res_model: $target.data('res_model'),
|
res_model: data.res_model,
|
||||||
views: [[false, 'kanban'], [false, 'form']],
|
views: [[false, 'kanban'], [false, 'form']],
|
||||||
search_view_id: [false],
|
search_view_id: [false],
|
||||||
domain: [['activity_user_id', '=', session.uid]],
|
domain: [['activity_user_id', '=', session.uid]],
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ QUnit.module('ActivityMenu', {
|
|||||||
res_model: "project.issue",
|
res_model: "project.issue",
|
||||||
planned_count: 1,
|
planned_count: 1,
|
||||||
today_count: 1,
|
today_count: 1,
|
||||||
overdue_count: 0,
|
overdue_count: 1,
|
||||||
total_count: 2,
|
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) {
|
QUnit.test('activity menu widget: activity menu with 3 records', function (assert) {
|
||||||
assert.expect(4);
|
assert.expect(10);
|
||||||
var self = this;
|
var self = this;
|
||||||
var activityMenu = new systray.ActivityMenu();
|
var activityMenu = new systray.ActivityMenu();
|
||||||
|
|
||||||
testUtils.addMockEnvironment(activityMenu, {
|
testUtils.addMockEnvironment(activityMenu, {
|
||||||
mockRPC: function (route, args) {
|
mockRPC: function (route, args) {
|
||||||
if (args.method === 'activity_user_count') {
|
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.$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_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.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();
|
activityMenu.destroy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
wiz_obj = self.env['mrp.product.produce']
|
wiz_obj = self.env['mrp.product.produce']
|
||||||
ctx = dict(self.env.context, active_id=self.id)
|
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 = 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 Product,Post Inventory and Set MO Done.
|
||||||
produce_wiz.do_produce()
|
produce_wiz.do_produce()
|
||||||
self.post_inventory()
|
self.post_inventory()
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ class MrpWorkorder(models.Model):
|
|||||||
'stock.move.line', 'workorder_id',
|
'stock.move.line', 'workorder_id',
|
||||||
domain=[('done_wo', '=', False)])
|
domain=[('done_wo', '=', False)])
|
||||||
final_lot_id = fields.Many2one(
|
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)]})
|
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
|
||||||
time_ids = fields.One2many(
|
time_ids = fields.One2many(
|
||||||
'mrp.workcenter.productivity', 'workorder_id')
|
'mrp.workcenter.productivity', 'workorder_id')
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ class TestUnbuild(TestMrpCommon):
|
|||||||
}).action_unbuild()
|
}).action_unbuild()
|
||||||
|
|
||||||
# Check quantity in stock after last 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(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')
|
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,
|
'product_uom_id': self.uom_unit.id,
|
||||||
}).action_unbuild()
|
}).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(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')
|
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,
|
'product_uom_id': self.uom_unit.id,
|
||||||
}).action_unbuild()
|
}).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(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')
|
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,
|
'product_uom_id': self.uom_unit.id,
|
||||||
}).action_unbuild()
|
}).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(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')
|
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)
|
# 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}
|
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})
|
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})
|
laptop_lot_001 = self.env['stock.production.lot'].create({'product_id': custom_laptop.id})
|
||||||
product_consume.lot_id = laptop_lot_001.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()
|
product_consume.do_produce()
|
||||||
|
|
||||||
# Check consumed move after produce 6 quantity of customized laptop.
|
# 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})
|
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})
|
laptop_lot_002 = self.env['stock.production.lot'].create({'product_id': custom_laptop.id})
|
||||||
product_consume.lot_id = laptop_lot_002.id
|
product_consume.lot_id = laptop_lot_002.id
|
||||||
self.assertEquals(len(product_consume.consume_line_ids), 2)
|
self.assertEquals(len(product_consume.produce_line_ids), 2)
|
||||||
product_consume.consume_line_ids.write({'qty_done': 8})
|
product_consume.produce_line_ids.write({'qty_done': 8})
|
||||||
product_consume.do_produce()
|
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')
|
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')
|
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})
|
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})
|
# laptop_lot_002 = self.env['stock.production.lot'].create({'product_id': custom_laptop.id})
|
||||||
product_consume.lot_id = lot_a.id
|
product_consume.lot_id = lot_a.id
|
||||||
self.assertEquals(len(product_consume.consume_line_ids), 2)
|
self.assertEquals(len(product_consume.produce_line_ids), 2)
|
||||||
product_consume.consume_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_C).write({'qty_done': 3000})
|
||||||
product_consume.consume_line_ids.filtered(lambda x : x.product_id == product_B).write({'qty_done': 20})
|
product_consume.produce_line_ids.filtered(lambda x : x.product_id == product_B).write({'qty_done': 20})
|
||||||
product_consume.do_produce()
|
product_consume.do_produce()
|
||||||
mo_custom_product.post_inventory()
|
mo_custom_product.post_inventory()
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
<span class="o_stat_text">Work Orders</span>
|
<span class="o_stat_text">Work Orders</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</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)]}">
|
<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">
|
<div class="o_field_widget o_stat_info">
|
||||||
<span class="o_stat_value"><field name="scrap_count"/></span>
|
<span class="o_stat_value"><field name="scrap_count"/></span>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<odoo>
|
<odoo>
|
||||||
<record model="ir.actions.act_window" id="action_mrp_production_moves">
|
<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="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_type">form</field>
|
||||||
<field name="view_mode">tree,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>
|
||||||
|
|
||||||
<record id="view_stock_move_lots" model="ir.ui.view">
|
<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)
|
res = super(MrpProductProduce, self).default_get(fields)
|
||||||
if self._context and self._context.get('active_id'):
|
if self._context and self._context.get('active_id'):
|
||||||
production = self.env['mrp.production'].browse(self._context['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_finished = (production.product_id.tracking == 'serial')
|
||||||
serial = bool(serial_finished)
|
|
||||||
if serial_finished:
|
if serial_finished:
|
||||||
quantity = 1.0
|
todo_quantity = 1.0
|
||||||
else:
|
else:
|
||||||
quantity = production.product_qty - sum(main_product_moves.mapped('quantity_done'))
|
main_product_moves = production.move_finished_ids.filtered(lambda x: x.product_id.id == production.product_id.id)
|
||||||
quantity = quantity if (quantity > 0) else 0
|
todo_quantity = production.product_qty - sum(main_product_moves.mapped('quantity_done'))
|
||||||
lines = []
|
todo_quantity = todo_quantity if (todo_quantity > 0) else 0
|
||||||
existing_lines = []
|
if 'production_id' in fields:
|
||||||
for move in production.move_raw_ids.filtered(lambda x: (x.product_id.tracking != 'none') and x.state not in ('done', 'cancel')):
|
res['production_id'] = production.id
|
||||||
if not move.move_line_ids.filtered(lambda x: not x.lot_produced_id):
|
if 'product_id' in fields:
|
||||||
qty = quantity / move.bom_line_id.bom_id.product_qty * move.bom_line_id.product_qty
|
res['product_id'] = production.product_id.id
|
||||||
if move.product_id.tracking == 'serial':
|
if 'product_uom_id' in fields:
|
||||||
while float_compare(qty, 0.0, precision_rounding=move.product_uom.rounding) > 0:
|
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({
|
lines.append({
|
||||||
'move_id': move.id,
|
'move_id': move.id,
|
||||||
'product_qty': min(1,qty),
|
'qty_to_consume': qty_to_consume,
|
||||||
'qty_done': 0.0,
|
'qty_done': 0.0,
|
||||||
'product_uom_id': move.product_uom.id,
|
'product_uom_id': move.product_uom.id,
|
||||||
'product_id': move.product_id.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['produce_line_ids'] = [(0, 0, x) for x in lines]
|
||||||
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]
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
serial = fields.Boolean('Requires Serial')
|
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_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')
|
product_uom_id = fields.Many2one('product.uom', 'Unit of Measure')
|
||||||
lot_id = fields.Many2one('stock.production.lot', string='Lot')
|
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")
|
product_tracking = fields.Selection(related="product_id.tracking")
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
@@ -104,14 +112,13 @@ class MrpProductProduce(models.TransientModel):
|
|||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def check_finished_move_lots(self):
|
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'))
|
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 produce_move and produce_move.product_id.tracking != 'none':
|
||||||
if not self.lot_id:
|
if not self.lot_id:
|
||||||
raise UserError(_('You need to provide a lot for the finished product'))
|
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)
|
existing_move_line = produce_move.move_line_ids.filtered(lambda x: x.lot_id == self.lot_id)
|
||||||
if existing_move_line:
|
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
|
existing_move_line.qty_done += self.product_qty
|
||||||
else:
|
else:
|
||||||
vals = {
|
vals = {
|
||||||
@@ -125,18 +132,48 @@ class MrpProductProduce(models.TransientModel):
|
|||||||
'location_id': produce_move.location_id.id,
|
'location_id': produce_move.location_id.id,
|
||||||
'location_dest_id': produce_move.location_dest_id.id,
|
'location_dest_id': produce_move.location_dest_id.id,
|
||||||
}
|
}
|
||||||
packs.create(vals)
|
self.env['stock.move.line'].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):
|
for pl in self.produce_line_ids:
|
||||||
if moveline.qty_done and self.lot_id:
|
if pl.qty_done and pl.lot_id:
|
||||||
#Possibly the entire move is selected
|
ml = pl.move_id.move_line_ids.filtered(lambda ml: ml.lot_id == pl.lot_id and not ml.lot_produced_id)
|
||||||
remaining_qty = moveline.product_uom_qty - moveline.qty_done
|
if ml:
|
||||||
if remaining_qty > 0:
|
if (ml.qty_done + pl.qty_done) >= ml.product_uom_qty:
|
||||||
default = {'product_uom_qty': moveline.qty_done,
|
ml.write({'qty_done': ml.qty_done + pl.qty_done, 'lot_produced_id': self.lot_id.id})
|
||||||
'qty_done': moveline.qty_done,
|
else:
|
||||||
'lot_produced_id': self.lot_id.id}
|
new_qty_todo = ml.product_uom_qty - (ml.qty_done + pl.qty_done)
|
||||||
new_move_line = moveline.copy(default=default)
|
default = {'product_uom_qty': ml.qty_done + pl.qty_done,
|
||||||
moveline.with_context(bypass_reservation_update=True).write({'product_uom_qty': remaining_qty, 'qty_done': 0})
|
'qty_done': ml.qty_done + pl.qty_done,
|
||||||
else:
|
'lot_produced_id': self.lot_id.id}
|
||||||
moveline.write({'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
|
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"/>
|
<label for="product_qty"/>
|
||||||
<div class="o_row">
|
<div class="o_row">
|
||||||
<field name="product_qty" attrs="{'readonly': [('serial', '=', True)]}"/>
|
<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>
|
</div>
|
||||||
<field name="product_tracking" invisible="1"/>
|
<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}"/>
|
<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>
|
</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">
|
<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="lot_id" context="{'default_product_id': product_id}" domain="[('product_id', '=', product_id)]"/>
|
||||||
<field name="product_uom_qty" string="To Consume"/>
|
<field name="qty_to_consume" readonly="1"/>
|
||||||
<field name="product_qty" readonly="1" string="Reserved"/>
|
|
||||||
<field name="qty_done"/>
|
<field name="qty_done"/>
|
||||||
|
<field name="product_uom_id" readonly="1" force_save="1" groups="product.group_uom"/>
|
||||||
<field name="move_id" invisible="1"/>
|
<field name="move_id" invisible="1"/>
|
||||||
</tree>
|
</tree>
|
||||||
</field>
|
</field>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class Repair(models.Model):
|
|||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _default_stock_location(self):
|
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:
|
if warehouse:
|
||||||
return warehouse.lot_stock_id.id
|
return warehouse.lot_stock_id.id
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -497,7 +497,7 @@ class SaleOrder(models.Model):
|
|||||||
})
|
})
|
||||||
if self.env.context.get('send_email'):
|
if self.env.context.get('send_email'):
|
||||||
self.force_quotation_send()
|
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()
|
self.action_done()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.o_kanban_grouped {
|
&.o_kanban_grouped {
|
||||||
.o-flex-flow(row, wrap);
|
|
||||||
.o_kanban_record {
|
.o_kanban_record {
|
||||||
min-width: inherit;
|
min-width: inherit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,15 +121,15 @@ class Product(models.Model):
|
|||||||
Quant = self.env['stock.quant']
|
Quant = self.env['stock.quant']
|
||||||
domain_move_in_todo = [('state', 'not in', ('done', 'cancel', 'draft'))] + domain_move_in
|
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
|
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_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']))
|
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']))
|
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:
|
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)
|
# 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_in_done = [('state', '=', 'done'), ('date', '>', to_date)] + domain_move_in_done
|
||||||
domain_move_out_done = [('state', '=', 'done'), ('date', '>', to_date)] + domain_move_out_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_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']))
|
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()
|
res = dict()
|
||||||
for product in self.with_context(prefetch_fields=False):
|
for product in self.with_context(prefetch_fields=False):
|
||||||
@@ -272,7 +272,7 @@ class Product(models.Model):
|
|||||||
domain_quant.append(('owner_id', '=', owner_id))
|
domain_quant.append(('owner_id', '=', owner_id))
|
||||||
if package_id:
|
if package_id:
|
||||||
domain_quant.append(('package_id', '=', 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:
|
for quant in quants_groupby:
|
||||||
if OPERATORS[operator](quant['quantity'], value):
|
if OPERATORS[operator](quant['quantity'], value):
|
||||||
product_ids.add(quant['product_id'][0])
|
product_ids.add(quant['product_id'][0])
|
||||||
@@ -359,6 +359,14 @@ class Product(models.Model):
|
|||||||
action['domain'] = [('product_id', '=', self.id)]
|
action['domain'] = [('product_id', '=', self.id)]
|
||||||
return action
|
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
|
@api.multi
|
||||||
def write(self, values):
|
def write(self, values):
|
||||||
res = super(Product, self).write(values)
|
res = super(Product, self).write(values)
|
||||||
@@ -512,7 +520,7 @@ class ProductTemplate(models.Model):
|
|||||||
products = self.mapped('product_variant_ids')
|
products = self.mapped('product_variant_ids')
|
||||||
action = self.env.ref('stock.product_open_quants').read()[0]
|
action = self.env.ref('stock.product_open_quants').read()[0]
|
||||||
action['domain'] = [('product_id', 'in', products.ids)]
|
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
|
return action
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
@@ -533,6 +541,13 @@ class ProductTemplate(models.Model):
|
|||||||
action['domain'] = [('product_id.product_tmpl_id', 'in', self.ids)]
|
action['domain'] = [('product_id.product_tmpl_id', 'in', self.ids)]
|
||||||
return action
|
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):
|
class ProductCategory(models.Model):
|
||||||
_inherit = 'product.category'
|
_inherit = 'product.category'
|
||||||
|
|||||||
@@ -17,15 +17,11 @@ class PutAwayStrategy(models.Model):
|
|||||||
_description = 'Put Away Strategy'
|
_description = 'Put Away Strategy'
|
||||||
|
|
||||||
name = fields.Char('Name', required=True)
|
name = fields.Char('Name', required=True)
|
||||||
method = fields.Selection('_get_putaway_options', "Method", default='fixed', required=True)
|
|
||||||
fixed_location_ids = fields.One2many(
|
fixed_location_ids = fields.One2many(
|
||||||
'stock.fixed.putaway.strat', 'putaway_id', 'Fixed Locations Per Product Category', copy=True,
|
'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")
|
help="When the method is fixed, this location will be used to store the products")
|
||||||
|
|
||||||
def _get_putaway_options(self):
|
def putaway_apply(self, product):
|
||||||
return [('fixed', 'Fixed Location')]
|
|
||||||
|
|
||||||
def _putaway_apply_fixed(self, product):
|
|
||||||
for strat in self.fixed_location_ids:
|
for strat in self.fixed_location_ids:
|
||||||
categ = product.categ_id
|
categ = product.categ_id
|
||||||
while categ:
|
while categ:
|
||||||
@@ -34,11 +30,6 @@ class PutAwayStrategy(models.Model):
|
|||||||
categ = categ.parent_id
|
categ = categ.parent_id
|
||||||
return self.env['stock.location']
|
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):
|
class FixedPutAwayStrategy(models.Model):
|
||||||
_name = 'stock.fixed.putaway.strat'
|
_name = 'stock.fixed.putaway.strat'
|
||||||
|
|||||||
@@ -713,7 +713,7 @@ class StockMove(models.Model):
|
|||||||
if move.location_id.usage in ('supplier', 'inventory', 'production', 'customer')\
|
if move.location_id.usage in ('supplier', 'inventory', 'production', 'customer')\
|
||||||
or move.product_id.type == 'consu':
|
or move.product_id.type == 'consu':
|
||||||
# create the move line(s) but do not impact quants
|
# 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)):
|
for i in range(0, int(move.product_qty)):
|
||||||
self.env['stock.move.line'].create(move._prepare_move_line_vals(quantity=1))
|
self.env['stock.move.line'].create(move._prepare_move_line_vals(quantity=1))
|
||||||
else:
|
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)
|
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)
|
new_move = move.split(qty_split)
|
||||||
for move_line in move.move_line_ids:
|
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
|
# FIXME: there will be an issue if the move was partially available
|
||||||
# By decreasing `product_qty`, we free the reservation.
|
# By decreasing `product_qty`, we free the reservation.
|
||||||
# FIXME: if qty_done > product_qty, this could raise if nothing is in stock
|
# 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,
|
'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.")
|
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 = 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
|
# Statistics for the kanban view
|
||||||
last_done_picking = fields.Char('Last 10 Done Pickings', compute='_compute_last_done_picking')
|
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)
|
return self.search(domain, order=removal_strategy_order)
|
||||||
|
|
||||||
@api.model
|
@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
|
""" 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,
|
`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*
|
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()
|
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 = 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
|
@api.model
|
||||||
def _update_available_quantity(self, product_id, location_id, quantity, lot_id=None, package_id=None, owner_id=None, in_date=None):
|
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,
|
'owner_id': owner_id and owner_id.id,
|
||||||
'in_date': in_date,
|
'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
|
@api.model
|
||||||
def _update_reserved_quantity(self, product_id, location_id, quantity, lot_id=None, package_id=None, owner_id=None, strict=False):
|
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()
|
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 = self._gather(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=strict)
|
||||||
|
available_quantity = self._get_available_quantity(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'))
|
|
||||||
if quantity > 0 and quantity > available_quantity:
|
if quantity > 0 and quantity > available_quantity:
|
||||||
raise UserError(_('It is not possible to reserve more products than you have in stock.'))
|
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')):
|
elif quantity < 0 and abs(quantity) > sum(quants.mapped('reserved_quantity')):
|
||||||
|
|||||||
@@ -156,8 +156,7 @@ class MrpStockReport(models.TransientModel):
|
|||||||
'model':'stock.move.line',
|
'model':'stock.move.line',
|
||||||
'product_id': move_line.product_id.display_name,
|
'product_id': move_line.product_id.display_name,
|
||||||
'product_qty_uom': str(move_line.qty_done) + ' ' + move_line.product_id.uom_id.name,
|
'product_qty_uom': str(move_line.qty_done) + ' ' + move_line.product_id.uom_id.name,
|
||||||
'location_source': move_line.location_id.name,
|
'location': move_line.location_id.name + ' -> ' + move_line.location_dest_id.name,
|
||||||
'location_destination': move_line.location_dest_id.name,
|
|
||||||
'reference_id': ref,
|
'reference_id': ref,
|
||||||
'res_id': res_id,
|
'res_id': res_id,
|
||||||
'stream': stream,
|
'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):
|
def make_dict_head(self, level, parent_id, model=False, stream=False, move_line=False):
|
||||||
data = []
|
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':
|
if model == 'stock.move.line':
|
||||||
data = [{
|
data = [{
|
||||||
'level': level,
|
'level': level,
|
||||||
@@ -177,9 +173,10 @@ class MrpStockReport(models.TransientModel):
|
|||||||
'model_id': move_line.id,
|
'model_id': move_line.id,
|
||||||
'parent_id': parent_id,
|
'parent_id': parent_id,
|
||||||
'model': model or 'stock.move.line',
|
'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,
|
'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,
|
'stream': stream,
|
||||||
'reference_id': False}]
|
'reference_id': False}]
|
||||||
elif model == 'stock.quant':
|
elif model == 'stock.quant':
|
||||||
@@ -190,9 +187,10 @@ class MrpStockReport(models.TransientModel):
|
|||||||
'model_id': move_line.id,
|
'model_id': move_line.id,
|
||||||
'parent_id': parent_id,
|
'parent_id': parent_id,
|
||||||
'model': model or 'stock.quant',
|
'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,
|
'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,
|
'stream': stream,
|
||||||
'reference_id': False}]
|
'reference_id': False}]
|
||||||
return data
|
return data
|
||||||
@@ -254,10 +252,10 @@ class MrpStockReport(models.TransientModel):
|
|||||||
'res_model': data.get('res_model', False),
|
'res_model': data.get('res_model', False),
|
||||||
'name': _(data.get('lot_id', False)),
|
'name': _(data.get('lot_id', False)),
|
||||||
'columns': [data.get('reference_id', False) or data.get('product_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('date', False),
|
||||||
data.get('product_qty_uom', 0),
|
data.get('product_qty_uom', 0),
|
||||||
data.get('location_source', False),
|
data.get('location', False)],
|
||||||
data.get('location_destination', False)],
|
|
||||||
'level': level,
|
'level': level,
|
||||||
'unfoldable': data['unfoldable'],
|
'unfoldable': data['unfoldable'],
|
||||||
})
|
})
|
||||||
@@ -315,10 +313,10 @@ class MrpStockReport(models.TransientModel):
|
|||||||
'type': 'line',
|
'type': 'line',
|
||||||
'name': _(data.get('lot_id')),
|
'name': _(data.get('lot_id')),
|
||||||
'columns': [data.get('reference_id') or data.get('product_id'),
|
'columns': [data.get('reference_id') or data.get('product_id'),
|
||||||
|
data.get('lot_id'),
|
||||||
data.get('date'),
|
data.get('date'),
|
||||||
data.get('product_qty_uom', 0),
|
data.get('product_qty_uom', 0),
|
||||||
data.get('location_source'),
|
data.get('location')],
|
||||||
data.get('location_destination')],
|
|
||||||
'level': data['level'],
|
'level': data['level'],
|
||||||
'unfoldable': data['unfoldable'],
|
'unfoldable': data['unfoldable'],
|
||||||
})
|
})
|
||||||
|
|||||||
+119
-10
@@ -74,7 +74,8 @@ class StockMove(TransactionCase):
|
|||||||
move1.action_done()
|
move1.action_done()
|
||||||
self.assertEqual(move1.state, 'done')
|
self.assertEqual(move1.state, 'done')
|
||||||
# no quants are created in the supplier location
|
# 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(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.supplier_location)), 1.0)
|
||||||
self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_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(move_line.product_qty, 0) # change reservation to 0 for done move
|
||||||
self.assertEqual(move1.state, 'done')
|
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), 0.0)
|
||||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product3, self.supplier_location), -5.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(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)
|
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)
|
quants = self.env['stock.quant']._gather(self.product3, self.stock_location)
|
||||||
@@ -165,7 +168,9 @@ class StockMove(TransactionCase):
|
|||||||
self.assertEqual(move1.state, 'done')
|
self.assertEqual(move1.state, 'done')
|
||||||
|
|
||||||
# Quant balance should result with 5 quant in supplier and stock
|
# 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(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)
|
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)
|
self.assertEqual(len(move1.move_line_ids), 2)
|
||||||
|
|
||||||
def test_mixed_tracking_reservation_2(self):
|
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
|
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
|
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.
|
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, 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=lot1)
|
||||||
self.env['stock.quant']._update_available_quantity(self.product2, self.stock_location, 1, lot_id=lot2)
|
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)
|
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.stock_location), 4.0)
|
||||||
# creation
|
# creation
|
||||||
move1 = self.env['stock.move'].create({
|
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(len(self.env['stock.quant']._gather(self.product1, self.stock_location)), 1.0)
|
||||||
self.assertEqual(move1.availability, 50.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):
|
def test_unreserve_1(self):
|
||||||
""" Check that unreserving a stock move sets the products reserved as available and
|
""" Check that unreserving a stock move sets the products reserved as available and
|
||||||
set the state back to confirmed.
|
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, 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, 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):
|
def test_edit_done_move_line_7(self):
|
||||||
""" Test that editing a done stock move line linked to an untracked product correctly and
|
""" 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
|
# edit once done, we actually moved 2 products
|
||||||
move1.move_line_ids.qty_done = 2
|
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, shelf1_location), 0.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, 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)
|
self.assertEqual(move1.product_uom_qty, 2.0)
|
||||||
|
|
||||||
def test_edit_done_move_line_9(self):
|
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.qty_done, 10.0)
|
||||||
self.assertEqual(picking.move_lines.move_line_ids.product_qty, 0.0)
|
self.assertEqual(picking.move_lines.move_line_ids.product_qty, 0.0)
|
||||||
# Check quants data
|
# 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)
|
self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location)), 1.0)
|
||||||
|
|
||||||
def test_immediate_validate_4(self):
|
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(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)
|
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):
|
def test_immediate_validate_5(self):
|
||||||
""" Create a picking and simulates validate button effect.
|
""" Create a picking and simulates validate button effect.
|
||||||
This tests two cases:
|
This tests two cases:
|
||||||
@@ -2727,3 +2803,36 @@ class StockMove(TransactionCase):
|
|||||||
|
|
||||||
self.assertEqual(self.product1.qty_available, 5.0)
|
self.assertEqual(self.product1.qty_available, 5.0)
|
||||||
self.assertEqual(self.product1.with_context(company_owned=True).qty_available, 10.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
|
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)
|
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 0.0)
|
||||||
delivery_order.with_context(debug=True).do_transfer()
|
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 = delivery_order.move_lines - move1
|
||||||
extra_move_line = extra_move.move_line_ids[0]
|
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
|
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)
|
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, pack_location), 0.0)
|
||||||
delivery_order.do_transfer()
|
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 = delivery_order.move_lines - move1
|
||||||
extra_move_line = extra_move.move_line_ids[0]
|
extra_move_line = extra_move.move_line_ids[0]
|
||||||
|
|||||||
@@ -131,7 +131,8 @@ class StockQuant(TransactionCase):
|
|||||||
'quantity': 5.0,
|
'quantity': 5.0,
|
||||||
'reserved_quantity': 0.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):
|
def test_get_available_quantity_7(self):
|
||||||
""" Quantity availability with only one tracked quant in a location.
|
""" Quantity availability with only one tracked quant in a location.
|
||||||
@@ -153,7 +154,8 @@ class StockQuant(TransactionCase):
|
|||||||
'reserved_quantity': 20.0,
|
'reserved_quantity': 20.0,
|
||||||
'lot_id': lot1.id,
|
'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):
|
def test_get_available_quantity_8(self):
|
||||||
""" Quantity availability with a consumable product.
|
""" Quantity availability with a consumable product.
|
||||||
@@ -282,7 +284,8 @@ class StockQuant(TransactionCase):
|
|||||||
'type': 'product',
|
'type': 'product',
|
||||||
})
|
})
|
||||||
self.env['stock.quant']._update_available_quantity(product1, stock_location, -1.0)
|
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):
|
def test_decrease_available_quantity_2(self):
|
||||||
""" Decrease the available quantity when multiple quants are already in a location.
|
""" Decrease the available quantity when multiple quants are already in a location.
|
||||||
|
|||||||
@@ -7,18 +7,15 @@
|
|||||||
<form string="Putaway">
|
<form string="Putaway">
|
||||||
<group colspan="4">
|
<group colspan="4">
|
||||||
<field name="name"/>
|
<field name="name"/>
|
||||||
<field name="method"/>
|
|
||||||
</group>
|
</group>
|
||||||
<div attrs="{'invisible': [('method', '!=', 'fixed')]}">
|
<separator string="Fixed Locations Per Categories"/>
|
||||||
<separator string="Fixed Locations Per Categories"/>
|
<field name="fixed_location_ids" colspan="4" nolabel="1">
|
||||||
<field name="fixed_location_ids" colspan="4" nolabel="1">
|
<tree editable="top">
|
||||||
<tree editable="top">
|
<field name="sequence" widget='handle'/>
|
||||||
<field name="sequence" widget='handle'/>
|
<field name="category_id"/>
|
||||||
<field name="category_id"/>
|
<field name="fixed_location_id"/>
|
||||||
<field name="fixed_location_id"/>
|
</tree>
|
||||||
</tree>
|
</field>
|
||||||
</field>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|||||||
@@ -205,7 +205,7 @@
|
|||||||
<span class="o_stat_text">Forecasted</span>
|
<span class="o_stat_text">Forecasted</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button string="Inventory Moves"
|
<button string="Product Moves"
|
||||||
type="object"
|
type="object"
|
||||||
name= "action_view_stock_move_lines"
|
name= "action_view_stock_move_lines"
|
||||||
attrs="{'invisible':[('type', 'not in', ['product', 'consu'])]}"
|
attrs="{'invisible':[('type', 'not in', ['product', 'consu'])]}"
|
||||||
@@ -233,6 +233,10 @@
|
|||||||
name="action_view_routes"
|
name="action_view_routes"
|
||||||
attrs="{'invisible':[('type', '!=', 'product')]}"
|
attrs="{'invisible':[('type', '!=', 'product')]}"
|
||||||
class="oe_stat_button" icon="fa-cogs" groups="base.group_no_one"/>
|
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>
|
</button>
|
||||||
</data>
|
</data>
|
||||||
</field>
|
</field>
|
||||||
@@ -268,7 +272,7 @@
|
|||||||
<span class="o_stat_text">Forecasted</span>
|
<span class="o_stat_text">Forecasted</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button string="Inventory Moves" type="object"
|
<button string="Product Moves" type="object"
|
||||||
name= "action_view_stock_move_lines"
|
name= "action_view_stock_move_lines"
|
||||||
attrs="{'invisible':[('type', 'not in', ['product', 'consu'])]}"
|
attrs="{'invisible':[('type', 'not in', ['product', 'consu'])]}"
|
||||||
class="oe_stat_button" icon="fa-arrows-v"
|
class="oe_stat_button" icon="fa-arrows-v"
|
||||||
@@ -298,6 +302,10 @@
|
|||||||
attrs="{'invisible':[('type', '!=', 'product')]}"
|
attrs="{'invisible':[('type', '!=', 'product')]}"
|
||||||
class="oe_stat_button" icon="fa-cogs"
|
class="oe_stat_button" icon="fa-cogs"
|
||||||
groups="base.group_no_one"/>
|
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>
|
</button>
|
||||||
|
|
||||||
<!-- change attrs of fields added in view_template_property_form
|
<!-- change attrs of fields added in view_template_property_form
|
||||||
|
|||||||
@@ -48,10 +48,10 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr class="o_report_header">
|
<tr class="o_report_header">
|
||||||
<th class="o_report_line_header">PRODUCT</th>
|
<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">QUANTITY</th>
|
||||||
<th class="o_report_line_header">LOCATION SOURCE</th>
|
<th class="o_report_line_header">LOCATION</th>
|
||||||
<th class="o_report_line_header">LOCATION DESTINATION</th>
|
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -86,10 +86,10 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr class="o_report_header">
|
<tr class="o_report_header">
|
||||||
<th class="o_report_line_header">PRODUCT</th>
|
<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">QUANTITY</th>
|
||||||
<th class="o_report_line_header">LOCATION SOURCE</th>
|
<th class="o_report_line_header">LOCATION</th>
|
||||||
<th class="o_report_line_header">LOCATION DESTINATION</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -355,7 +355,7 @@
|
|||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="stock_move_action" model="ir.actions.act_window">
|
<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="res_model">stock.move</field>
|
||||||
<field name="type">ir.actions.act_window</field>
|
<field name="type">ir.actions.act_window</field>
|
||||||
<field name="view_type">form</field>
|
<field name="view_type">form</field>
|
||||||
|
|||||||
@@ -185,19 +185,24 @@
|
|||||||
<field name="location_id"/>
|
<field name="location_id"/>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
<notebook>
|
<separator string="Content" />
|
||||||
<page string="Content">
|
<field name="current_picking_id" invisible="1"/>
|
||||||
<separator string="Products" />
|
<field name="current_picking_move_line_ids" readonly="1" attrs="{'invisible': [('current_picking_id', '=', False)]}">
|
||||||
<field name="current_picking_id" invisible="1"/>
|
<tree>
|
||||||
<field name="current_picking_move_line_ids" readonly="1" attrs="{'invisible': [('current_picking_id', '=', False)]}">
|
<field name="product_id"/>
|
||||||
<tree>
|
<field name="lot_id" groups="stock.group_production_lot"/>
|
||||||
<field name="product_id"/>
|
<field name="qty_done" string="Quantity"/>
|
||||||
<field name="qty_done" string="Quantity"/>
|
<field name="product_uom_id" groups="product.group_uom"/>
|
||||||
</tree>
|
</tree>
|
||||||
</field>
|
</field>
|
||||||
<field name="quant_ids" attrs="{'invisible': [('current_picking_id', '=', True)]}"/>
|
<field name="quant_ids" attrs="{'invisible': [('current_picking_id', '=', True)]}">
|
||||||
</page>
|
<tree>
|
||||||
</notebook>
|
<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>
|
</sheet>
|
||||||
</form>
|
</form>
|
||||||
</field>
|
</field>
|
||||||
|
|||||||
@@ -74,6 +74,19 @@ class ProductTemplate(models.Model):
|
|||||||
})
|
})
|
||||||
return accounts
|
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
|
@api.multi
|
||||||
def get_product_accounts(self, fiscal_pos=None):
|
def get_product_accounts(self, fiscal_pos=None):
|
||||||
""" Add the stock journal related to product to the result of super()
|
""" Add the stock journal related to product to the result of super()
|
||||||
@@ -156,6 +169,19 @@ class ProductProduct(models.Model):
|
|||||||
moves = StockMove.search(domain)
|
moves = StockMove.search(domain)
|
||||||
product.stock_value = sum(moves.mapped('value'))
|
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):
|
class ProductCategory(models.Model):
|
||||||
_inherit = 'product.category'
|
_inherit = 'product.category'
|
||||||
|
|||||||
@@ -92,9 +92,11 @@
|
|||||||
<field name="model">product.product</field>
|
<field name="model">product.product</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<tree>
|
<tree>
|
||||||
<field name="name"/>
|
<field name="name" string="Product"/>
|
||||||
<field name="stock_value" sum="Stock Valuation"/>
|
<field name="qty_available" context="{'company_owned': True}" string="Quantity on Hand"/>
|
||||||
<field name="qty_available" context="{'company_owned': True}"/>
|
<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>
|
</tree>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
@@ -113,5 +115,27 @@
|
|||||||
</p>
|
</p>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</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>
|
</data>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
@@ -68,13 +68,12 @@
|
|||||||
<field name="product_uom_qty" string="Qty"/>
|
<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="product_uom" options="{'no_open': True, 'no_create': True}" string="Unit of Measure" groups="product.group_uom"/>
|
||||||
<field name="price_unit"/>
|
<field name="price_unit"/>
|
||||||
<field name="remaining_qty"/>
|
|
||||||
<field name="value" sum="Stock Valuation"/>
|
<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>
|
</tree>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="stock_move_valuation_action" model="ir.actions.act_window">
|
<record id="stock_move_valuation_action" model="ir.actions.act_window">
|
||||||
<field name="name">Stock Moves</field>
|
<field name="name">Stock Moves</field>
|
||||||
<field name="res_model">stock.move</field>
|
<field name="res_model">stock.move</field>
|
||||||
|
|||||||
@@ -877,7 +877,8 @@ var FieldX2Many = AbstractField.extend({
|
|||||||
*/
|
*/
|
||||||
_onEditLine: function (ev) {
|
_onEditLine: function (ev) {
|
||||||
ev.stopPropagation();
|
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);
|
.done(ev.data.onSuccess);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -336,20 +336,30 @@ return core.Class.extend({
|
|||||||
attrs.mode = mode;
|
attrs.mode = mode;
|
||||||
if (mode in attrs.views) {
|
if (mode in attrs.views) {
|
||||||
var view = attrs.views[mode];
|
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;
|
var defaultOrder = view.arch.attrs.default_order;
|
||||||
if (defaultOrder) {
|
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) {
|
attrs.orderedBy = _.map(defaultOrder.split(','), function (order) {
|
||||||
order = order.trim().split(' ');
|
order = order.trim().split(' ');
|
||||||
return {name: order[0], asc: order[1] !== 'desc'};
|
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
|
// detect editables list has they behave differently with respect
|
||||||
// to the sorting (changes are not sorted directly)
|
// to the sorting (changes are not sorted directly)
|
||||||
if (mode === 'list' && view.arch.attrs.editable) {
|
if (mode === 'list' && view.arch.attrs.editable) {
|
||||||
attrs.keepChangesUnsorted = true;
|
attrs.keepChangesUnsorted = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (attrs.Widget.prototype.fieldsToFetch) {
|
if (attrs.Widget.prototype.fieldsToFetch) {
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ var AbstractView = Class.extend({
|
|||||||
}
|
}
|
||||||
defs.push(parent.loadViews(
|
defs.push(parent.loadViews(
|
||||||
field.relation,
|
field.relation,
|
||||||
new Context(context, self.userContext),
|
new Context(context, self.userContext, self.loadParams.context),
|
||||||
[[null, attrs.mode === 'tree' ? 'list' : attrs.mode]])
|
[[null, attrs.mode === 'tree' ? 'list' : attrs.mode]])
|
||||||
.then(function (views) {
|
.then(function (views) {
|
||||||
for (var viewName in views) {
|
for (var viewName in views) {
|
||||||
|
|||||||
@@ -780,6 +780,55 @@ var BasicModel = AbstractModel.extend({
|
|||||||
parent.data = _.without(parent.data, elementID);
|
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,
|
* Save a local resource, if needed. This is a complicated operation,
|
||||||
* - it needs to check all changes,
|
* - it needs to check all changes,
|
||||||
@@ -1186,6 +1235,7 @@ var BasicModel = AbstractModel.extend({
|
|||||||
record._rawChanges[name] = val;
|
record._rawChanges[name] = val;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
var oldValue = record._changes[name] || record.data[name];
|
||||||
var id;
|
var id;
|
||||||
if (field.type === 'many2one') {
|
if (field.type === 'many2one') {
|
||||||
id = false;
|
id = false;
|
||||||
@@ -1198,29 +1248,42 @@ var BasicModel = AbstractModel.extend({
|
|||||||
var data = _.isArray(val) ?
|
var data = _.isArray(val) ?
|
||||||
{id: val[0], display_name: val[1]} :
|
{id: val[0], display_name: val[1]} :
|
||||||
{id: val};
|
{id: val};
|
||||||
rec = self._makeDataPoint({
|
if (!oldValue || (self.localData[oldValue].res_id !== data.id)) {
|
||||||
context: record.context,
|
// only register a change if the value has changed
|
||||||
data: data,
|
rec = self._makeDataPoint({
|
||||||
modelName: field.relation,
|
context: record.context,
|
||||||
parentID: record.id,
|
data: data,
|
||||||
});
|
modelName: field.relation,
|
||||||
id = rec.id;
|
parentID: record.id,
|
||||||
|
});
|
||||||
|
id = rec.id;
|
||||||
|
record._changes[name] = id;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
record._changes[name] = false;
|
||||||
}
|
}
|
||||||
record._changes[name] = id;
|
|
||||||
} else if (field.type === 'reference') {
|
} else if (field.type === 'reference') {
|
||||||
id = false;
|
id = false;
|
||||||
if (val) {
|
if (val) {
|
||||||
var ref = val.split(',');
|
var ref = val.split(',');
|
||||||
rec = self._makeDataPoint({
|
var modelName = ref[0];
|
||||||
context: record.context,
|
var resID = parseInt(ref[1]);
|
||||||
data: {id: parseInt(ref[1])},
|
if (!oldValue || self.localData[oldValue].res_id !== resID ||
|
||||||
modelName: ref[0],
|
self.localData[oldValue].model !== modelName) {
|
||||||
parentID: record.id,
|
// only register a change if the value has changed
|
||||||
});
|
rec = self._makeDataPoint({
|
||||||
defs.push(self._fetchNameGet(rec));
|
context: record.context,
|
||||||
id = rec.id;
|
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') {
|
} else if (field.type === 'one2many' || field.type === 'many2many') {
|
||||||
var listId = record._changes[name] || record.data[name];
|
var listId = record._changes[name] || record.data[name];
|
||||||
var list;
|
var list;
|
||||||
@@ -1240,26 +1303,40 @@ var BasicModel = AbstractModel.extend({
|
|||||||
viewType: fieldInfo.viewType,
|
viewType: fieldInfo.viewType,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// TODO: before registering the changes, verify that the x2many
|
||||||
|
// value has changed
|
||||||
record._changes[name] = list.id;
|
record._changes[name] = list.id;
|
||||||
var shouldLoad = false;
|
var shouldLoad = false;
|
||||||
list._changes = list._changes || [];
|
list._changes = list._changes || [];
|
||||||
_.each(val, function (command) {
|
_.each(val, function (command) {
|
||||||
|
var rec;
|
||||||
if (command[0] === 0 || command[0] === 1) {
|
if (command[0] === 0 || command[0] === 1) {
|
||||||
// CREATE or UPDATE
|
// CREATE or UPDATE
|
||||||
var params = {
|
if (command[0] === 1 && command[1]) {
|
||||||
context: list.context,
|
// updating an existing record
|
||||||
fields: list.fields,
|
var recID = _.find(list.data, function (childID) {
|
||||||
fieldsInfo: list.fieldsInfo,
|
var child = self.localData[childID];
|
||||||
modelName: list.model,
|
return child.res_id === command[1];
|
||||||
parentID: list.id,
|
});
|
||||||
viewType: list.viewType,
|
rec = self.localData[recID];
|
||||||
ref: command[1],
|
|
||||||
};
|
|
||||||
if (command[0] === 1) {
|
|
||||||
params.res_id = command[1];
|
|
||||||
}
|
}
|
||||||
rec = self._makeDataPoint(params);
|
if (!rec) {
|
||||||
list._cache[rec.res_id] = rec.id;
|
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});
|
list._changes.push({operation: 'ADD', id: rec.id});
|
||||||
if (command[0] === 1) {
|
if (command[0] === 1) {
|
||||||
list._changes.push({operation: 'UPDATE', id: rec.id});
|
list._changes.push({operation: 'UPDATE', id: rec.id});
|
||||||
@@ -1280,7 +1357,10 @@ var BasicModel = AbstractModel.extend({
|
|||||||
defs.push(self._readUngroupedList(list));
|
defs.push(self._readUngroupedList(list));
|
||||||
}
|
}
|
||||||
} else {
|
} 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);
|
return $.when.apply($, defs);
|
||||||
|
|||||||
@@ -614,7 +614,8 @@ return AbstractModel.extend({
|
|||||||
if (this.mapping.all_day && evt[this.mapping.all_day]) {
|
if (this.mapping.all_day && evt[this.mapping.all_day]) {
|
||||||
date_stop.add(1, 'days');
|
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 = {
|
var r = {
|
||||||
'record': evt,
|
'record': evt,
|
||||||
'start': date_start,
|
'start': date_start,
|
||||||
@@ -622,7 +623,7 @@ return AbstractModel.extend({
|
|||||||
'r_start': date_start,
|
'r_start': date_start,
|
||||||
'r_end': date_stop,
|
'r_end': date_stop,
|
||||||
'title': the_title,
|
'title': the_title,
|
||||||
'allDay': this.mapping.all_day && evt[this.mapping.all_day] || false,
|
'allDay': isAllDay,
|
||||||
'id': evt.id,
|
'id': evt.id,
|
||||||
'attendees':attendees,
|
'attendees':attendees,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -110,7 +110,6 @@ var KanbanColumn = Widget.extend({
|
|||||||
var record = ui.item.data('record');
|
var record = ui.item.data('record');
|
||||||
var index = self.records.indexOf(record);
|
var index = self.records.indexOf(record);
|
||||||
record.$el.removeAttr('style'); // jqueryui sortable add display:block inline
|
record.$el.removeAttr('style'); // jqueryui sortable add display:block inline
|
||||||
ui.item.addClass('o_updating');
|
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
if ($.contains(self.$el[0], record.$el[0])) {
|
if ($.contains(self.$el[0], record.$el[0])) {
|
||||||
// resequencing records
|
// resequencing records
|
||||||
@@ -118,6 +117,7 @@ var KanbanColumn = Widget.extend({
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// adding record to this column
|
// adding record to this column
|
||||||
|
ui.item.addClass('o_updating');
|
||||||
self.trigger_up('kanban_column_add_record', {record: record, ids: self._getIDs()});
|
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);
|
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
|
// Private
|
||||||
|
|||||||
@@ -372,9 +372,16 @@ var ListController = BasicController.extend({
|
|||||||
* @param {OdooEvent} ev
|
* @param {OdooEvent} ev
|
||||||
*/
|
*/
|
||||||
_onEditLine: function (ev) {
|
_onEditLine: function (ev) {
|
||||||
|
var self = this;
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
this._setMode('edit', ev.data.recordID)
|
this.trigger_up('mutexify', {
|
||||||
.done(ev.data.onSuccess);
|
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
|
* Opens the Export Dialog
|
||||||
@@ -404,23 +411,24 @@ var ListController = BasicController.extend({
|
|||||||
* @param {OdooEvent} event
|
* @param {OdooEvent} event
|
||||||
*/
|
*/
|
||||||
_onResequence: function (event) {
|
_onResequence: function (event) {
|
||||||
var data = this.model.get(this.handle);
|
var self = this;
|
||||||
var resIDs = _.map(event.data.rowIDs, function(rowID) {
|
|
||||||
return _.findWhere(data.data, {id: rowID}).res_id;
|
this.trigger_up('mutexify', {
|
||||||
})
|
action: function () {
|
||||||
return this._rpc({
|
var state = self.model.get(self.handle);
|
||||||
route: '/web/dataset/resequence',
|
var resIDs = _.map(event.data.rowIDs, function(rowID) {
|
||||||
params: {
|
return _.findWhere(state.data, {id: rowID}).res_id;
|
||||||
model: this.modelName,
|
});
|
||||||
ids: resIDs,
|
var options = {
|
||||||
offset: event.data.offset,
|
offset: event.data.offset,
|
||||||
field: event.data.handleField,
|
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 self = this;
|
||||||
var movedRecordID = ui.item.data('id');
|
var movedRecordID = ui.item.data('id');
|
||||||
var rowIDs = _.pluck(this.state.data, 'id');
|
var rowIDs = _.pluck(this.state.data, 'id');
|
||||||
rowIDs = _.without(rowIDs, movedRecordID)
|
rowIDs = _.without(rowIDs, movedRecordID);
|
||||||
rowIDs.splice(ui.item.index(), 0, movedRecordID);
|
rowIDs.splice(ui.item.index(), 0, movedRecordID);
|
||||||
var sequences = _.map(this.state.data, function(record) {
|
var sequences = _.map(this.state.data, function(record) {
|
||||||
return record.data[self.handleField];
|
return record.data[self.handleField];
|
||||||
})
|
});
|
||||||
this.trigger_up('resequence', {
|
this.trigger_up('resequence', {
|
||||||
rowIDs: rowIDs,
|
rowIDs: rowIDs,
|
||||||
offset: _.min(sequences),
|
offset: _.min(sequences),
|
||||||
@@ -516,10 +516,9 @@ ListRenderer.include({
|
|||||||
var self = this;
|
var self = this;
|
||||||
return this.unselectRow().then(function () {
|
return this.unselectRow().then(function () {
|
||||||
// Notify the controller we want to make a record editable
|
// Notify the controller we want to make a record editable
|
||||||
var record = self.state.data[rowIndex];
|
|
||||||
var def = $.Deferred();
|
var def = $.Deferred();
|
||||||
self.trigger_up('edit_line', {
|
self.trigger_up('edit_line', {
|
||||||
recordID: record.id,
|
index: rowIndex,
|
||||||
onSuccess: def.resolve.bind(def),
|
onSuccess: def.resolve.bind(def),
|
||||||
});
|
});
|
||||||
return def;
|
return def;
|
||||||
|
|||||||
@@ -390,7 +390,7 @@ DebugManager.include({
|
|||||||
// or m2m
|
// or m2m
|
||||||
if (!value || invisibleOrReadOnly || fieldInfo.type === 'one2many' ||
|
if (!value || invisibleOrReadOnly || fieldInfo.type === 'one2many' ||
|
||||||
fieldInfo.type === 'many2many' || fieldInfo.type === 'binary' ||
|
fieldInfo.type === 'many2many' || fieldInfo.type === 'binary' ||
|
||||||
fieldsInfo[fieldName].options.isPassword) {
|
fieldsInfo[fieldName].options.isPassword || !_.isEmpty(fieldInfo.depends)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -28,6 +28,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.o_kanban_view.o_kanban_dashboard {
|
.o_kanban_view.o_kanban_dashboard {
|
||||||
|
// correctly display the no_content_helper in dashboards
|
||||||
|
.o-flex-flow(row, wrap);
|
||||||
|
|
||||||
.o_kanban_record {
|
.o_kanban_record {
|
||||||
position: relative;
|
position: relative;
|
||||||
.o-flex-display();
|
.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_qux: {string: "Qux", type: "float", digits: [16,1], required: true, default: 1.5},
|
||||||
turtle_description: {string: "Description", type: "text"},
|
turtle_description: {string: "Description", type: "text"},
|
||||||
turtle_trululu: {string: "Trululu", type: "many2one", relation: 'partner'},
|
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},
|
product_id: {string: "Product", type: "many2one", relation: 'product', required: true},
|
||||||
partner_ids: {string: "Partner", type: "many2many", relation: 'partner'},
|
partner_ids: {string: "Partner", type: "many2many", relation: 'partner'},
|
||||||
},
|
},
|
||||||
@@ -126,11 +128,13 @@ QUnit.module('relational_fields', {
|
|||||||
}, {
|
}, {
|
||||||
id: 3,
|
id: 3,
|
||||||
display_name: "raphael",
|
display_name: "raphael",
|
||||||
|
product_id: 37,
|
||||||
turtle_bar: false,
|
turtle_bar: false,
|
||||||
turtle_foo: "kawa",
|
turtle_foo: "kawa",
|
||||||
turtle_int: 21,
|
turtle_int: 21,
|
||||||
turtle_qux: 9.8,
|
turtle_qux: 9.8,
|
||||||
partner_ids: [],
|
partner_ids: [],
|
||||||
|
turtle_ref: 'product,37',
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
@@ -1950,6 +1954,67 @@ QUnit.module('relational_fields', {
|
|||||||
form.destroy();
|
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) {
|
QUnit.test('one2many field when using the pager', function (assert) {
|
||||||
assert.expect(13);
|
assert.expect(13);
|
||||||
|
|
||||||
@@ -3356,6 +3421,72 @@ QUnit.module('relational_fields', {
|
|||||||
form.destroy();
|
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) {
|
QUnit.test('one2many list (editable): readonly domain is evaluated', function (assert) {
|
||||||
assert.expect(2);
|
assert.expect(2);
|
||||||
|
|
||||||
|
|||||||
@@ -57,16 +57,22 @@ var MockServer = Class.extend({
|
|||||||
* 'field_view_get' call to the server. Calls processViews() of data_manager
|
* 'field_view_get' call to the server. Calls processViews() of data_manager
|
||||||
* to mimick the real behavior of a call to loadViews().
|
* to mimick the real behavior of a call to loadViews().
|
||||||
*
|
*
|
||||||
* @param {string|Object} arch a string OR a parsed xml document
|
* @param {Object} params
|
||||||
* @param {string} model a model name (that should be in this.data)
|
* @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
|
* @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)) {
|
if (!(model in this.data)) {
|
||||||
throw new Error('Model ' + model + ' was not defined in mock server data');
|
throw new Error('Model ' + model + ' was not defined in mock server data');
|
||||||
}
|
}
|
||||||
var fields = $.extend(true, {}, this.data[model].fields);
|
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 = {};
|
var fields_views = {};
|
||||||
fields_views[fvg.type] = fvg;
|
fields_views[fvg.type] = fvg;
|
||||||
data_manager.processViews(fields_views, fields);
|
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|Object} arch a string OR a parsed xml document
|
||||||
* @param {string} model a model name (that should be in this.data)
|
* @param {string} model a model name (that should be in this.data)
|
||||||
* @param {Object} fields
|
* @param {Object} fields
|
||||||
|
* @param {Object} context
|
||||||
* @returns {Object} an object with 2 keys: arch and fields (the fields
|
* @returns {Object} an object with 2 keys: arch and fields (the fields
|
||||||
* appearing in the views)
|
* appearing in the views)
|
||||||
*/
|
*/
|
||||||
_fieldsViewGet: function (arch, model, fields) {
|
_fieldsViewGet: function (arch, model, fields, context) {
|
||||||
var self = this;
|
var self = this;
|
||||||
var modifiersNames = ['invisible', 'readonly', 'required'];
|
var modifiersNames = ['invisible', 'readonly', 'required'];
|
||||||
var onchanges = this.data[model].onchanges || {};
|
var onchanges = this.data[model].onchanges || {};
|
||||||
@@ -224,7 +231,8 @@ var MockServer = Class.extend({
|
|||||||
}
|
}
|
||||||
_.each(modifiersNames, function (a) {
|
_.each(modifiersNames, function (a) {
|
||||||
if (node.attrs[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') {
|
if (inTreeView && a === 'invisible') {
|
||||||
modifiers['tree_invisible'] = v;
|
modifiers['tree_invisible'] = v;
|
||||||
} else if (v || !(a in modifiers) || !_.isArray(modifiers[a])) {
|
} else if (v || !(a in modifiers) || !_.isArray(modifiers[a])) {
|
||||||
@@ -252,7 +260,8 @@ var MockServer = Class.extend({
|
|||||||
_.each(node.children, function (children) {
|
_.each(node.children, function (children) {
|
||||||
relModel = field.relation;
|
relModel = field.relation;
|
||||||
relFields = $.extend(true, {}, self.data[relModel].fields);
|
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, ...
|
// add mock environment: mock server, session, fieldviewget, ...
|
||||||
var mockServer = addMockEnvironment(widget, params);
|
var mockServer = addMockEnvironment(widget, params);
|
||||||
var viewInfo = mockServer.fieldsViewGet(params.arch, params.model, params.toolbar);
|
var viewInfo = mockServer.fieldsViewGet(params);
|
||||||
|
|
||||||
// create the view
|
// create the view
|
||||||
var viewOptions = {
|
var viewOptions = {
|
||||||
@@ -329,10 +329,17 @@ function addMockEnvironment(widget, params) {
|
|||||||
var view_type = view_descr[1];
|
var view_type = view_descr[1];
|
||||||
var key = [model, view_id, view_type].join(',');
|
var key = [model, view_id, view_type].join(',');
|
||||||
var arch = params.archs[key];
|
var arch = params.archs[key];
|
||||||
|
var viewParams = {
|
||||||
|
arch: arch,
|
||||||
|
model: model,
|
||||||
|
viewOptions: {
|
||||||
|
context: event.data.context.eval(),
|
||||||
|
},
|
||||||
|
};
|
||||||
if (!arch) {
|
if (!arch) {
|
||||||
throw new Error('No arch found for key ' + key);
|
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);
|
event.data.on_success(views);
|
||||||
|
|||||||
@@ -1257,6 +1257,37 @@ QUnit.module('Views', {
|
|||||||
calendar.destroy();
|
calendar.destroy();
|
||||||
$view.remove();
|
$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) {
|
QUnit.test('*_view_ref in context are passed correctly', function (assert) {
|
||||||
var done = assert.async();
|
var done = assert.async();
|
||||||
assert.expect(4);
|
assert.expect(3);
|
||||||
|
|
||||||
createAsyncView({
|
createAsyncView({
|
||||||
View: FormView,
|
View: FormView,
|
||||||
@@ -3938,8 +3938,6 @@ QUnit.module('Views', {
|
|||||||
var context = event.data.context.eval();
|
var context = event.data.context.eval();
|
||||||
assert.strictEqual(context.tree_view_ref, 'module.tree_view_ref',
|
assert.strictEqual(context.tree_view_ref, 'module.tree_view_ref',
|
||||||
"context should contain 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();
|
event.data.on_success();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -5790,6 +5788,61 @@ QUnit.module('Views', {
|
|||||||
form.destroy();
|
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) {
|
QUnit.test('rainbowman attributes correctly passed on button click', function (assert) {
|
||||||
assert.expect(1);
|
assert.expect(1);
|
||||||
|
|
||||||
|
|||||||
@@ -1801,6 +1801,75 @@ QUnit.module('Views', {
|
|||||||
kanban.destroy();
|
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) {
|
QUnit.test('don\'t fold column quick create after creation', function (assert) {
|
||||||
assert.expect(2);
|
assert.expect(2);
|
||||||
|
|
||||||
|
|||||||
@@ -2586,7 +2586,7 @@ QUnit.module('Views', {
|
|||||||
assert.strictEqual(list.$('tbody tr:eq(2) td:last').text(), '300',
|
assert.strictEqual(list.$('tbody tr:eq(2) td:last').text(), '300',
|
||||||
"default third record should have amount 300");
|
"default third record should have amount 300");
|
||||||
assert.strictEqual(list.$('tbody tr:eq(3) td:last').text(), '0',
|
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
|
// Drag and drop the fourth line in second position
|
||||||
testUtils.dragAndDrop(
|
testUtils.dragAndDrop(
|
||||||
@@ -2602,7 +2602,143 @@ QUnit.module('Views', {
|
|||||||
assert.strictEqual(list.$('tbody tr:eq(2) td:last').text(), '500',
|
assert.strictEqual(list.$('tbody tr:eq(2) td:last').text(), '500',
|
||||||
"new third record should have amount 500");
|
"new third record should have amount 500");
|
||||||
assert.strictEqual(list.$('tbody tr:eq(3) td:last').text(), '300',
|
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();
|
list.destroy();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
import re
|
||||||
import odoo.tests
|
import odoo.tests
|
||||||
|
|
||||||
|
|
||||||
@@ -11,3 +12,19 @@ class WebSuite(odoo.tests.HttpCase):
|
|||||||
|
|
||||||
def test_01_js(self):
|
def test_01_js(self):
|
||||||
self.phantom_js('/web/tests?mod=web', "", "", login='admin', timeout=300)
|
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++) {
|
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]);
|
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
|
// 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'));
|
$('<p></p>').append(br).insertAfter($(r.sc).closest('blockquote, pre'));
|
||||||
node = br;
|
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) {
|
} else if (last === r.sc) {
|
||||||
if (dom.isBR(last)) {
|
if (dom.isBR(last)) {
|
||||||
last = last.parentNode;
|
last = last.parentNode;
|
||||||
|
|||||||
@@ -681,7 +681,7 @@ var VideoDialog = Widget.extend({
|
|||||||
|
|
||||||
if (this.$('.o_video_dialog_iframe').is('iframe')) {
|
if (this.$('.o_video_dialog_iframe').is('iframe')) {
|
||||||
var $content = $(
|
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="css_editable_mode_display"> </div>'+
|
||||||
'<div class="media_iframe_video_size" contenteditable="false"> </div>'+
|
'<div class="media_iframe_video_size" contenteditable="false"> </div>'+
|
||||||
'<iframe src="' + this.$content.attr('src') + '" frameborder="0" contenteditable="false"></iframe>'+
|
'<iframe src="' + this.$content.attr('src') + '" frameborder="0" contenteditable="false"></iframe>'+
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# 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 import api, fields, models
|
||||||
from odoo.exceptions import AccessDenied
|
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_analytics_dashboard', self.has_google_analytics_dashboard)
|
||||||
set_param('website.has_google_maps', self.has_google_maps)
|
set_param('website.has_google_maps', self.has_google_maps)
|
||||||
set_param('google_maps_api_key', (self.google_maps_api_key or '').strip())
|
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>';
|
var size = '<div class="media_iframe_video_size"> </div>';
|
||||||
this.$target.html(editor+size);
|
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);
|
return this._super.apply(this, arguments);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -148,6 +148,11 @@
|
|||||||
<div class="mt8">
|
<div class="mt8">
|
||||||
<field name="auth_signup_uninvited" widget="radio" required="True"/>
|
<field name="auth_signup_uninvited" widget="radio" required="True"/>
|
||||||
</div>
|
</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>
|
</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">
|
<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'");
|
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();
|
$("[data-toggle='popover']").popover();
|
||||||
$('.karma_required').on('click', function (ev) {
|
$('.karma_required').on('click', function (ev) {
|
||||||
var karma = $(ev.currentTarget).data('karma');
|
var karma = $(ev.currentTarget).data('karma');
|
||||||
@@ -410,6 +413,9 @@ odoo.define('website_forum.website_forum', function (require) {
|
|||||||
toolbar: toolbar,
|
toolbar: toolbar,
|
||||||
styleWithSpan: false
|
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 () {
|
$form.on('click', 'button, .a-submit', function () {
|
||||||
$textarea.html($form.find('.note-editable').code());
|
$textarea.html($form.find('.note-editable').code());
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
'website': 'https://www.odoo.com/page/e-commerce',
|
'website': 'https://www.odoo.com/page/e-commerce',
|
||||||
'version': '1.0',
|
'version': '1.0',
|
||||||
'description': "",
|
'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': [
|
'data': [
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
'security/website_sale.xml',
|
'security/website_sale.xml',
|
||||||
|
|||||||
@@ -686,11 +686,11 @@ class WebsiteSale(http.Controller):
|
|||||||
values = {
|
values = {
|
||||||
'website_sale_order': order,
|
'website_sale_order': order,
|
||||||
'post': post,
|
'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)
|
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 . import tools
|
||||||
from .tools.func import frame_codeinfo
|
from .tools.func import frame_codeinfo
|
||||||
|
from .tools import pycompat
|
||||||
|
|
||||||
from .tools import parse_version as pv
|
from .tools import parse_version as pv
|
||||||
if pv(psycopg2.__version__) < pv('2.7'):
|
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.")
|
raise ValueError("A string literal cannot contain NUL (0x00) characters.")
|
||||||
return QuotedString(adapted)
|
return QuotedString(adapted)
|
||||||
|
|
||||||
psycopg2.extensions.register_adapter(str, adapt_string)
|
for type_ in pycompat.string_types:
|
||||||
psycopg2.extensions.register_adapter(unicode, adapt_string)
|
psycopg2.extensions.register_adapter(type_, adapt_string)
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import threading
|
import threading
|
||||||
|
|||||||
@@ -23,9 +23,13 @@ def _convert_nn_fr(val):
|
|||||||
if val < 20:
|
if val < 20:
|
||||||
return to_19_fr[val]
|
return to_19_fr[val]
|
||||||
for (dcap, dval) in ((k, 20 + (10 * v)) for (v, k) in enumerate(tens_fr)):
|
for (dcap, dval) in ((k, 20 + (10 * v)) for (v, k) in enumerate(tens_fr)):
|
||||||
if dval + 10 > val:
|
base = 10
|
||||||
if val % 10:
|
if dval in [60, 80]:
|
||||||
return dcap + '-' + to_19_fr[val % 10]
|
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
|
return dcap
|
||||||
|
|
||||||
def _convert_nnn_fr(val):
|
def _convert_nnn_fr(val):
|
||||||
@@ -38,7 +42,7 @@ def _convert_nnn_fr(val):
|
|||||||
word = ''
|
word = ''
|
||||||
(mod, rem) = (val % 100, val // 100)
|
(mod, rem) = (val % 100, val // 100)
|
||||||
if rem > 0:
|
if rem > 0:
|
||||||
word = to_19_fr[rem] + ' Cent'
|
word = (rem > 1 and to_19_fr[rem] + ' cents') or 'Cent'
|
||||||
if mod > 0:
|
if mod > 0:
|
||||||
word += ' '
|
word += ' '
|
||||||
if mod > 0:
|
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)):
|
for (dcap, dval) in ((k, 20 + (10 * v)) for (v, k) in enumerate(tens_nl)):
|
||||||
if dval + 10 > val:
|
if dval + 10 > val:
|
||||||
if val % 10:
|
if val % 10:
|
||||||
return dcap + '-' + to_19_nl[val % 10]
|
return to_19_nl[val % 10] + '-en-' + dcap
|
||||||
return dcap
|
return dcap
|
||||||
|
|
||||||
def _convert_nnn_nl(val):
|
def _convert_nnn_nl(val):
|
||||||
@@ -106,7 +110,7 @@ def _convert_nnn_nl(val):
|
|||||||
word = ''
|
word = ''
|
||||||
(mod, rem) = (val % 100, val // 100)
|
(mod, rem) = (val % 100, val // 100)
|
||||||
if rem > 0:
|
if rem > 0:
|
||||||
word = to_19_nl[rem] + ' Honderd'
|
word = (rem > 1 and to_19_nl[rem] + 'honderd') or 'Honderd'
|
||||||
if mod > 0:
|
if mod > 0:
|
||||||
word += ' '
|
word += ' '
|
||||||
if mod > 0:
|
if mod > 0:
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ from PIL import ImageEnhance
|
|||||||
from random import randrange
|
from random import randrange
|
||||||
|
|
||||||
# Preload PIL with the minimal subset of image formats we need
|
# Preload PIL with the minimal subset of image formats we need
|
||||||
|
from odoo.tools import pycompat
|
||||||
|
|
||||||
Image.preinit()
|
Image.preinit()
|
||||||
Image._initialized = 2
|
Image._initialized = 2
|
||||||
|
|
||||||
@@ -253,6 +255,8 @@ def image_get_resized_images(base64_source, return_big=False, return_medium=True
|
|||||||
previous parameters.
|
previous parameters.
|
||||||
"""
|
"""
|
||||||
return_dict = dict()
|
return_dict = dict()
|
||||||
|
if isinstance(base64_source, pycompat.text_type):
|
||||||
|
base64_source = base64_source.encode('ascii')
|
||||||
if return_big:
|
if return_big:
|
||||||
return_dict[big_name] = image_resize_image_big(base64_source, avoid_if_small=avoid_resize_big)
|
return_dict[big_name] = image_resize_image_big(base64_source, avoid_if_small=avoid_resize_big)
|
||||||
if return_medium:
|
if return_medium:
|
||||||
|
|||||||
+7
-7
@@ -24,19 +24,19 @@ _CONFDELTYPES = {
|
|||||||
def existing_tables(cr, tablenames):
|
def existing_tables(cr, tablenames):
|
||||||
""" Return the names of existing tables among ``tablenames``. """
|
""" Return the names of existing tables among ``tablenames``. """
|
||||||
query = """
|
query = """
|
||||||
SELECT relname
|
SELECT c.relname
|
||||||
FROM pg_class
|
FROM pg_class c
|
||||||
WHERE relname IN %s
|
JOIN pg_namespace n ON (n.oid = c.relnamespace)
|
||||||
AND relkind IN ('r', 'v', 'm')
|
WHERE c.relname IN %s
|
||||||
|
AND c.relkind IN ('r', 'v', 'm')
|
||||||
|
AND n.nspname = 'public'
|
||||||
"""
|
"""
|
||||||
cr.execute(query, [tuple(tablenames)])
|
cr.execute(query, [tuple(tablenames)])
|
||||||
return [row[0] for row in cr.fetchall()]
|
return [row[0] for row in cr.fetchall()]
|
||||||
|
|
||||||
def table_exists(cr, tablename):
|
def table_exists(cr, tablename):
|
||||||
""" Return whether the given table exists. """
|
""" Return whether the given table exists. """
|
||||||
query = "SELECT 1 FROM pg_class WHERE relkind IN ('r', 'v', 'm') AND relname=%s"
|
return len(existing_tables(cr, {tablename})) == 1
|
||||||
cr.execute(query, (tablename,))
|
|
||||||
return cr.rowcount
|
|
||||||
|
|
||||||
def table_kind(cr, tablename):
|
def table_kind(cr, tablename):
|
||||||
""" Return the kind of a table: ``'r'`` (regular table), ``'v'`` (view),
|
""" 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:
|
elif len(sys.argv) == 2 and sys.argv[1] in cmds:
|
||||||
cmds[sys.argv[1]]()
|
cmds[sys.argv[1]]()
|
||||||
else:
|
else:
|
||||||
sys.exit('Unknow command. Command available: %r' % (list(cmds,))
|
sys.exit('Unknow command. Command available: %r' % (list(cmds,)))
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user