[MERGE] forward port branch 11.0 up to 33981790de
This commit is contained in:
@@ -4,7 +4,7 @@ from odoo import api, fields, models, _
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import float_is_zero, pycompat
|
||||
from odoo.tools import float_compare, float_round, float_repr
|
||||
from odoo.tools.misc import formatLang
|
||||
from odoo.tools.misc import formatLang, format_date
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
import time
|
||||
|
||||
@@ -5,7 +5,7 @@ from collections import OrderedDict
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.osv import expression
|
||||
from odoo.exceptions import RedirectWarning, UserError, ValidationError
|
||||
from odoo.tools.misc import formatLang
|
||||
from odoo.tools.misc import formatLang, format_date
|
||||
from odoo.tools import float_is_zero, float_compare
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
from odoo.addons import decimal_precision as dp
|
||||
|
||||
@@ -447,6 +447,7 @@ var StatementModel = BasicModel.extend({
|
||||
model: 'account.reconciliation.widget',
|
||||
method: 'get_bank_statement_line_data',
|
||||
args: [ids, excluded_ids],
|
||||
context: self.context,
|
||||
})
|
||||
.then(self._formatLine.bind(self));
|
||||
},
|
||||
@@ -1102,6 +1103,7 @@ var StatementModel = BasicModel.extend({
|
||||
model: 'account.reconciliation.widget',
|
||||
method: 'get_move_lines_for_bank_statement_line',
|
||||
args: [line.id, line.st_line.partner_id, excluded_ids, filter, offset, limit],
|
||||
context: this.context,
|
||||
})
|
||||
.then(this._formatMoveLine.bind(this, handle));
|
||||
},
|
||||
@@ -1171,6 +1173,7 @@ var ManualModel = StatementModel.extend({
|
||||
*/
|
||||
load: function (context) {
|
||||
var self = this;
|
||||
this.context = context;
|
||||
|
||||
var domain_account_id = [];
|
||||
if (context && context.company_ids) {
|
||||
@@ -1461,6 +1464,7 @@ var ManualModel = StatementModel.extend({
|
||||
model: 'account.reconciliation.widget',
|
||||
method: 'get_move_lines_for_manual_reconciliation',
|
||||
args: args,
|
||||
context: this.context,
|
||||
})
|
||||
.then(this._formatMoveLine.bind(this, handle));
|
||||
},
|
||||
|
||||
@@ -167,7 +167,7 @@ class Lead(models.Model):
|
||||
@api.depends('date_open')
|
||||
def _compute_day_open(self):
|
||||
""" Compute difference between create date and open date """
|
||||
for lead in self.filtered(lambda l: l.date_open):
|
||||
for lead in self.filtered(lambda l: l.date_open and l.create_date):
|
||||
date_create = fields.Datetime.from_string(lead.create_date)
|
||||
date_open = fields.Datetime.from_string(lead.date_open)
|
||||
lead.day_open = abs((date_open - date_create).days)
|
||||
@@ -175,7 +175,7 @@ class Lead(models.Model):
|
||||
@api.depends('date_closed')
|
||||
def _compute_day_close(self):
|
||||
""" Compute difference between current date and log date """
|
||||
for lead in self.filtered(lambda l: l.date_closed):
|
||||
for lead in self.filtered(lambda l: l.date_closed and l.create_date):
|
||||
date_create = fields.Datetime.from_string(lead.create_date)
|
||||
date_close = fields.Datetime.from_string(lead.date_closed)
|
||||
lead.day_close = abs((date_close - date_create).days)
|
||||
@@ -710,7 +710,7 @@ class Lead(models.Model):
|
||||
partner_match_domain.append(('partner_id', '=', partner_id))
|
||||
partner_match_domain = ['|'] * (len(partner_match_domain) - 1) + partner_match_domain
|
||||
if not partner_match_domain:
|
||||
return []
|
||||
return self.env['crm.lead']
|
||||
domain = partner_match_domain
|
||||
if not include_lost:
|
||||
domain += ['&', ('active', '=', True), ('probability', '<', 100)]
|
||||
|
||||
@@ -411,8 +411,8 @@ class Channel(models.Model):
|
||||
AND P.partner_id IN %s
|
||||
AND channel_type LIKE 'chat'
|
||||
GROUP BY P.channel_id
|
||||
HAVING COUNT(P.partner_id) = %s
|
||||
""", (tuple(partners_to), len(partners_to),))
|
||||
HAVING array_agg(P.partner_id ORDER BY P.partner_id) = %s
|
||||
""", (tuple(partners_to), sorted(list(partners_to)),))
|
||||
result = self.env.cr.dictfetchall()
|
||||
if result:
|
||||
# get the existing channel between the given partners
|
||||
|
||||
@@ -1774,6 +1774,7 @@ class MailThread(models.AbstractModel):
|
||||
if not attachment:
|
||||
attachment = fname_mapping.get(node.get('data-filename'), '')
|
||||
if attachment:
|
||||
attachment.generate_access_token()
|
||||
node.set('src', '/web/image/%s?access_token=%s' % (attachment.id, attachment.access_token))
|
||||
postprocessed = True
|
||||
if postprocessed:
|
||||
|
||||
@@ -144,7 +144,7 @@ class PosOrder(models.Model):
|
||||
cash_journal_id = cash_journal[0].id
|
||||
order.add_payment({
|
||||
'amount': -pos_order['amount_return'],
|
||||
'payment_date': fields.Datetime.now(),
|
||||
'payment_date': fields.Date.context_today(self),
|
||||
'payment_name': _('return'),
|
||||
'journal': cash_journal_id,
|
||||
})
|
||||
|
||||
@@ -435,7 +435,8 @@
|
||||
<div class="content-group mt16" attrs="{'invisible': [('iface_invoicing', '=', False)]}">
|
||||
<div class="row mt16">
|
||||
<label string="Invoice Journal" for="invoice_journal_id" class="col-md-3 o_light_label"/>
|
||||
<field name="invoice_journal_id"/>
|
||||
<field name="invoice_journal_id"
|
||||
attrs="{'required': [('iface_invoicing', '=', True)]}"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -909,11 +909,13 @@ class StockMove(models.Model):
|
||||
assigned_moves = self.env['stock.move']
|
||||
partially_available_moves = self.env['stock.move']
|
||||
for move in self.filtered(lambda m: m.state in ['confirmed', 'waiting', 'partially_available']):
|
||||
missing_reserved_uom_quantity = move.product_uom_qty - move.reserved_availability
|
||||
missing_reserved_quantity = move.product_uom._compute_quantity(missing_reserved_uom_quantity, move.product_id.uom_id, rounding_method='HALF-UP')
|
||||
if move.location_id.should_bypass_reservation()\
|
||||
or move.product_id.type == 'consu':
|
||||
# create the move line(s) but do not impact quants
|
||||
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 - move.reserved_availability)):
|
||||
for i in range(0, int(missing_reserved_quantity)):
|
||||
self.env['stock.move.line'].create(move._prepare_move_line_vals(quantity=1))
|
||||
else:
|
||||
to_update = move.move_line_ids.filtered(lambda ml: ml.product_uom_id == move.product_uom and
|
||||
@@ -924,16 +926,16 @@ class StockMove(models.Model):
|
||||
not ml.package_id and
|
||||
not ml.owner_id)
|
||||
if to_update:
|
||||
to_update[0].product_uom_qty += move.product_qty - move.reserved_availability
|
||||
to_update[0].product_uom_qty += missing_reserved_uom_quantity
|
||||
else:
|
||||
self.env['stock.move.line'].create(move._prepare_move_line_vals(quantity=move.product_qty - move.reserved_availability))
|
||||
self.env['stock.move.line'].create(move._prepare_move_line_vals(quantity=missing_reserved_quantity))
|
||||
assigned_moves |= move
|
||||
else:
|
||||
if not move.move_orig_ids:
|
||||
if move.procure_method == 'make_to_order':
|
||||
continue
|
||||
# If we don't need any quantity, consider the move assigned.
|
||||
need = move.product_qty - move.reserved_availability
|
||||
need = missing_reserved_quantity
|
||||
if float_is_zero(need, precision_rounding=move.product_id.uom_id.rounding):
|
||||
assigned_moves |= move
|
||||
continue
|
||||
|
||||
@@ -1099,6 +1099,27 @@ class StockMove(TransactionCase):
|
||||
move_partial._action_assign()
|
||||
self.assertEqual(move_partial.state, 'assigned')
|
||||
|
||||
def test_availability_9(self):
|
||||
""" Test the assignment mechanism when the product quantity is increase
|
||||
on a receipt move.
|
||||
"""
|
||||
move_receipt = self.env['stock.move'].create({
|
||||
'name': 'test_receipt_edit',
|
||||
'location_id': self.supplier_location.id,
|
||||
'location_dest_id': self.stock_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_dozen.id,
|
||||
'product_uom_qty': 1.0,
|
||||
})
|
||||
|
||||
move_receipt._action_confirm()
|
||||
move_receipt._action_assign()
|
||||
self.assertEqual(move_receipt.state, 'assigned')
|
||||
move_receipt.product_uom_qty = 3.0
|
||||
move_receipt._action_assign()
|
||||
self.assertEqual(move_receipt.state, 'assigned')
|
||||
self.assertEqual(move_receipt.move_line_ids.product_uom_qty, 3)
|
||||
|
||||
def test_unreserve_1(self):
|
||||
""" Check that unreserving a stock move sets the products reserved as available and
|
||||
set the state back to confirmed.
|
||||
|
||||
@@ -905,8 +905,14 @@ var BasicModel = AbstractModel.extend({
|
||||
params: params,
|
||||
})
|
||||
.then(function () {
|
||||
var offset = options.offset ? options.offset : 0;
|
||||
var old_data = data.data.slice();
|
||||
data.data = _.sortBy(data.data, function (d) {
|
||||
return _.indexOf(resIDs, self.localData[d].res_id);
|
||||
if (_.contains(resIDs, self.localData[d].res_id)) {
|
||||
return _.indexOf(resIDs, self.localData[d].res_id) + offset;
|
||||
} else {
|
||||
return _.indexOf(old_data, d);
|
||||
}
|
||||
});
|
||||
data.res_ids = [];
|
||||
_.each(data.data, function (d) {
|
||||
|
||||
@@ -857,6 +857,23 @@ var MockServer = Class.extend({
|
||||
|
||||
return data;
|
||||
},
|
||||
/**
|
||||
* Simulates a 'resequence' operation
|
||||
*
|
||||
* @private
|
||||
* @param {string} model
|
||||
* @param {string} field
|
||||
* @param {Array} ids
|
||||
*/
|
||||
_mockResequence: function (args) {
|
||||
var offset = args.offset ? Number(args.offset) : 0;
|
||||
var field = args.field ? args.field : 'sequence';
|
||||
var records = this.data[args.model].records;
|
||||
for (var i in args.ids) {
|
||||
var record = _.findWhere(this.data[args.model].records, {id: args.ids[i]});
|
||||
record[field] = Number(i) + offset;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Simulate a 'search_count' operation
|
||||
*
|
||||
@@ -1016,7 +1033,7 @@ var MockServer = Class.extend({
|
||||
return $.when(this._mockSearchReadController(args));
|
||||
|
||||
case '/web/dataset/resequence':
|
||||
return $.when();
|
||||
return $.when(this._mockResequence(args));
|
||||
}
|
||||
if (route.indexOf('/web/image') >= 0 || _.contains(['.png', '.jpg'], route.substr(route.length - 4))) {
|
||||
return $.when();
|
||||
|
||||
@@ -1655,7 +1655,7 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
QUnit.test('hide and display help message (ESC) in kanban quick create', function (assert) {
|
||||
assert.expect(2);
|
||||
assert.expect(4);
|
||||
|
||||
var kanban = createView({
|
||||
View: KanbanView,
|
||||
@@ -1680,6 +1680,13 @@ QUnit.module('Views', {
|
||||
assert.notOk(kanban.$('.o_discard_msg').is(':visible'),
|
||||
'the ESC to discard message is no longer visible');
|
||||
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_quick_create:visible button.o_kanban_add').length, 1,
|
||||
"the quick create column should still be opened for mass creation");
|
||||
kanban.$('.o_column_quick_create input').trigger($.Event('keydown', { keyCode: $.ui.keyCode.ESCAPE }));
|
||||
assert.strictEqual(kanban.$('.o_kanban_quick_create:visible button.o_kanban_add').length, 0,
|
||||
"the quick create column should now be closed");
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
|
||||
@@ -3022,20 +3022,118 @@ QUnit.module('Views', {
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('result of consecutive resequences is correctly sorted', function (assert) {
|
||||
assert.expect(9);
|
||||
this.data = { // we want the data to be minimal to have a minimal test
|
||||
foo: {
|
||||
fields: {int_field: {string: "int_field", type: "integer", sortable: true}},
|
||||
records: [
|
||||
{id: 1, int_field: 0},
|
||||
{id: 2, int_field: 1},
|
||||
{id: 3, int_field: 2},
|
||||
{id: 4, int_field: 3},
|
||||
]
|
||||
}
|
||||
};
|
||||
var moves = 0;
|
||||
var list = createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree>' +
|
||||
'<field name="int_field" widget="handle"/>' +
|
||||
'<field name="id"/>' +
|
||||
'</tree>',
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/web/dataset/resequence') {
|
||||
if (moves === 0) {
|
||||
assert.deepEqual(args, {
|
||||
model: "foo",
|
||||
ids: [4, 3],
|
||||
offset: 2,
|
||||
field: "int_field",
|
||||
});
|
||||
}
|
||||
if (moves === 1) {
|
||||
assert.deepEqual(args, {
|
||||
model: "foo",
|
||||
ids: [1, 4, 2, 3],
|
||||
field: "int_field",
|
||||
});
|
||||
}
|
||||
if (moves === 2) {
|
||||
assert.deepEqual(args, {
|
||||
model: "foo",
|
||||
ids: [2, 4],
|
||||
offset: 1,
|
||||
field: "int_field",
|
||||
});
|
||||
}
|
||||
if (moves === 3) {
|
||||
assert.deepEqual(args, {
|
||||
model: "foo",
|
||||
ids: [1, 4, 2, 3],
|
||||
field: "int_field",
|
||||
});
|
||||
}
|
||||
moves += 1;
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
assert.strictEqual(list.$('tbody tr td').text(), '1234',
|
||||
"default should be sorted by id");
|
||||
testUtils.dragAndDrop(
|
||||
list.$('.ui-sortable-handle').eq(3),
|
||||
list.$('tbody tr').eq(2),
|
||||
{position: 'top'}
|
||||
);
|
||||
assert.strictEqual(list.$('tbody tr td').text(), '1243',
|
||||
"the int_field (sequence) should have been correctly updated");
|
||||
testUtils.dragAndDrop(
|
||||
list.$('.ui-sortable-handle').eq(2),
|
||||
list.$('tbody tr').eq(1),
|
||||
{position: 'top'}
|
||||
);
|
||||
assert.deepEqual(list.$('tbody tr td').text(), '1423',
|
||||
"the int_field (sequence) should have been correctly updated");
|
||||
testUtils.dragAndDrop(
|
||||
list.$('.ui-sortable-handle').eq(1),
|
||||
list.$('tbody tr').eq(3),
|
||||
{position: 'top'}
|
||||
);
|
||||
assert.deepEqual(list.$('tbody tr td').text(), '1243',
|
||||
"the int_field (sequence) should have been correctly updated");
|
||||
testUtils.dragAndDrop(
|
||||
list.$('.ui-sortable-handle').eq(2),
|
||||
list.$('tbody tr').eq(1),
|
||||
{position: 'top'}
|
||||
);
|
||||
assert.deepEqual(list.$('tbody tr td').text(), '1423',
|
||||
"the int_field (sequence) should have been correctly updated");
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('editable list with handle widget', function (assert) {
|
||||
assert.expect(12);
|
||||
|
||||
// resequence makes sense on a sequence field, not on arbitrary fields
|
||||
this.data.foo.records[0].int_field = 0;
|
||||
this.data.foo.records[1].int_field = 1;
|
||||
this.data.foo.records[2].int_field = 2;
|
||||
this.data.foo.records[3].int_field = 3;
|
||||
|
||||
var list = createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree editable="top">' +
|
||||
arch: '<tree editable="top" default_order="int_field">' +
|
||||
'<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,
|
||||
assert.strictEqual(args.offset, 1,
|
||||
"should write the sequence starting from the lowest current one");
|
||||
assert.strictEqual(args.field, 'int_field',
|
||||
"should write the right field as sequence");
|
||||
@@ -3083,6 +3181,12 @@ QUnit.module('Views', {
|
||||
QUnit.test('editable list with handle widget with slow network', function (assert) {
|
||||
assert.expect(15);
|
||||
|
||||
// resequence makes sense on a sequence field, not on arbitrary fields
|
||||
this.data.foo.records[0].int_field = 0;
|
||||
this.data.foo.records[1].int_field = 1;
|
||||
this.data.foo.records[2].int_field = 2;
|
||||
this.data.foo.records[3].int_field = 3;
|
||||
|
||||
var def = $.Deferred();
|
||||
|
||||
var list = createView({
|
||||
@@ -3095,7 +3199,7 @@ QUnit.module('Views', {
|
||||
'</tree>',
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/web/dataset/resequence') {
|
||||
assert.strictEqual(args.offset, -4,
|
||||
assert.strictEqual(args.offset, 1,
|
||||
"should write the sequence starting from the lowest current one");
|
||||
assert.strictEqual(args.field, 'int_field',
|
||||
"should write the right field as sequence");
|
||||
|
||||
Reference in New Issue
Block a user