[MERGE] forward port branch saas-11.3 up to 83cc046e9a
This commit is contained in:
@@ -289,6 +289,10 @@
|
||||
overflow: auto;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 0 0 5px 5px;
|
||||
> div.quick_add > .o_reconcile_models {
|
||||
max-height: 54px;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
.quick_add {
|
||||
margin-bottom: 7px;
|
||||
@@ -357,4 +361,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<tree string="Registration" editable="top" create="false" delete="false">
|
||||
<field name="event_id" readonly='1' force_save="1"/>
|
||||
<field name="registration_id" readonly='1' force_save="1"/>
|
||||
<field name="event_ticket_id" domain="[('event_id', '=', event_id)]" readonly='1'/>
|
||||
<field name="event_ticket_id" domain="[('event_id', '=', event_id)]" readonly='1' force_save="1"/>
|
||||
<field name="name"/>
|
||||
<field name="email"/>
|
||||
<field name="phone"/>
|
||||
|
||||
@@ -105,6 +105,7 @@
|
||||
<div class="o_media_left">
|
||||
<a href="#"
|
||||
t-att-data-employee-id="self.id"
|
||||
t-att-data-employee-name="self.name"
|
||||
class="o_org_chart_show_more text-center o_employee_sub_redirect">…</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
This invoice is already paid.
|
||||
% else:
|
||||
Please remit payment at your earliest convenience.
|
||||
% if object.reference_type == 'bba' and object.reference:
|
||||
% if object.invoice_reference_type=='structured' and object.reference:
|
||||
Please use the following communication for your payment: <strong>${object.reference}</strong>.
|
||||
% endif
|
||||
<br /><br />
|
||||
|
||||
@@ -486,7 +486,10 @@ class MrpProduction(models.Model):
|
||||
if move:
|
||||
old_qty = move[0].product_uom_qty
|
||||
if quantity > 0:
|
||||
move[0].write({'product_uom_qty': quantity})
|
||||
move[0]._decrease_reserved_quanity(quantity)
|
||||
move[0].with_context(do_not_unreserve=True).write({'product_uom_qty': quantity})
|
||||
move[0]._recompute_state()
|
||||
move[0]._action_assign()
|
||||
move.unit_factor = quantity / move.raw_material_production_id.product_qty
|
||||
elif quantity < 0: # Do not remove 0 lines
|
||||
if move[0].quantity_done > 0:
|
||||
|
||||
@@ -182,6 +182,24 @@ class StockMove(models.Model):
|
||||
self.sudo().unlink()
|
||||
return processed_moves
|
||||
|
||||
def _decrease_reserved_quanity(self, quantity):
|
||||
""" Decrease the reservation on move lines but keeps the
|
||||
all other data.
|
||||
"""
|
||||
move_line_to_unlink = self.env['stock.move.line']
|
||||
for move in self:
|
||||
reserved_quantity = quantity
|
||||
for move_line in self.move_line_ids:
|
||||
if move_line.product_uom_qty > reserved_quantity:
|
||||
move_line.product_uom_qty = reserved_quantity
|
||||
else:
|
||||
move_line.product_uom_qty = 0
|
||||
reserved_quantity -= move_line.product_uom_qty
|
||||
if not move_line.product_uom_qty and not move_line.qty_done:
|
||||
move_line_to_unlink |= move_line
|
||||
move_line_to_unlink.unlink()
|
||||
return True
|
||||
|
||||
def _prepare_phantom_move_values(self, bom_line, quantity):
|
||||
return {
|
||||
'picking_id': self.picking_id.id if self.picking_id else False,
|
||||
|
||||
@@ -629,6 +629,52 @@ class TestMrpOrder(TestMrpCommon):
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
|
||||
|
||||
def test_product_produce_5(self):
|
||||
""" Build 5 final products with different consumed lots,
|
||||
then edit the finished quantity and update the Manufacturing
|
||||
order quantity. Then check if the produced quantity do not
|
||||
change and it is possible to close the MO.
|
||||
"""
|
||||
self.stock_location = self.env.ref('stock.stock_location_stock')
|
||||
mo, bom, p_final, p1, p2 = self.generate_mo(tracking_base_1='lot')
|
||||
self.assertEqual(len(mo), 1, 'MO should have been created')
|
||||
|
||||
lot_1 = self.env['stock.production.lot'].create({
|
||||
'name': 'lot1',
|
||||
'product_id': p1.id,
|
||||
})
|
||||
lot_2 = self.env['stock.production.lot'].create({
|
||||
'name': 'lot2',
|
||||
'product_id': p1.id,
|
||||
})
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 10, lot_id=lot_1)
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 10, lot_id=lot_2)
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
|
||||
mo.action_assign()
|
||||
|
||||
produce_wizard = self.env['mrp.product.produce'].with_context({
|
||||
'active_id': mo.id,
|
||||
'active_ids': [mo.id],
|
||||
}).create({
|
||||
'product_qty': 5.0,
|
||||
})
|
||||
|
||||
for produce_line in produce_wizard.produce_line_ids:
|
||||
produce_line.qty_done = produce_line.qty_to_consume
|
||||
produce_wizard.do_produce()
|
||||
|
||||
mo.move_finished_ids.move_line_ids.qty_done -= 1
|
||||
update_quantity_wizard = self.env['change.production.qty'].create({
|
||||
'mo_id': mo.id,
|
||||
'product_qty': 4,
|
||||
})
|
||||
update_quantity_wizard.change_prod_qty()
|
||||
|
||||
self.assertEqual(mo.move_raw_ids.filtered(lambda m: m.product_id == p1).quantity_done, 20, 'Update the produce quantity should not impact already produced quantity.')
|
||||
mo.button_mark_done()
|
||||
|
||||
def test_product_produce_uom(self):
|
||||
plastic_laminate = self.env.ref('mrp.product_product_plastic_laminate')
|
||||
bom = self.env.ref('mrp.mrp_bom_plastic_laminate')
|
||||
|
||||
@@ -53,7 +53,6 @@ class ChangeProductionQty(models.TransientModel):
|
||||
factor = production.product_uom_id._compute_quantity(production.product_qty - qty_produced, production.bom_id.product_uom_id) / production.bom_id.product_qty
|
||||
boms, lines = production.bom_id.explode(production.product_id, factor, picking_type=production.bom_id.picking_type_id)
|
||||
documents = {}
|
||||
done_quantities = {move: move.quantity_done for move in production.move_raw_ids}
|
||||
for line, line_data in lines:
|
||||
move, old_qty, new_qty = production._update_raw_move(line, line_data)
|
||||
iterate_key = production._get_document_iterate_key(move)
|
||||
@@ -73,9 +72,6 @@ class ChangeProductionQty(models.TransientModel):
|
||||
production._log_downside_manufactured_quantity(finished_moves_modification)
|
||||
moves = production.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
|
||||
moves._action_assign()
|
||||
for move in production.move_raw_ids:
|
||||
if move.quantity_done != done_quantities[move]:
|
||||
move._set_quantity_done(done_quantities[move])
|
||||
for wo in production.workorder_ids:
|
||||
operation = wo.operation_id
|
||||
if operation_bom_qty.get(operation.id):
|
||||
|
||||
@@ -11,15 +11,38 @@ class res_partner(models.Model):
|
||||
|
||||
@api.multi
|
||||
def _compute_purchase_order_count(self):
|
||||
PurchaseOrder = self.env['purchase.order']
|
||||
for partner in self:
|
||||
partner.purchase_order_count = PurchaseOrder.search_count([('partner_id', 'child_of', partner.id)])
|
||||
# retrieve all children partners and prefetch 'parent_id' on them
|
||||
all_partners = self.search([('id', 'child_of', self.ids)])
|
||||
all_partners.read(['parent_id'])
|
||||
|
||||
purchase_order_groups = self.env['purchase.order'].read_group(
|
||||
domain=[('partner_id', 'in', all_partners.ids)],
|
||||
fields=['partner_id'], groupby=['partner_id']
|
||||
)
|
||||
for group in purchase_order_groups:
|
||||
partner = self.browse(group['partner_id'][0])
|
||||
while partner:
|
||||
if partner in self:
|
||||
partner.purchase_order_count += group['partner_id_count']
|
||||
partner = partner.parent_id
|
||||
|
||||
@api.multi
|
||||
def _compute_supplier_invoice_count(self):
|
||||
Invoice = self.env['account.invoice']
|
||||
for partner in self:
|
||||
partner.supplier_invoice_count = Invoice.search_count([('partner_id', 'child_of', partner.id), ('type', '=', 'in_invoice')])
|
||||
# retrieve all children partners and prefetch 'parent_id' on them
|
||||
all_partners = self.search([('id', 'child_of', self.ids)])
|
||||
all_partners.read(['parent_id'])
|
||||
|
||||
supplier_invoice_groups = self.env['account.invoice'].read_group(
|
||||
domain=[('partner_id', 'in', all_partners.ids),
|
||||
('type', '=', 'in_invoice')],
|
||||
fields=['partner_id'], groupby=['partner_id']
|
||||
)
|
||||
for group in supplier_invoice_groups:
|
||||
partner = self.browse(group['partner_id'][0])
|
||||
while partner:
|
||||
if partner in self:
|
||||
partner.supplier_invoice_count += group['partner_id_count']
|
||||
partner = partner.parent_id
|
||||
|
||||
@api.model
|
||||
def _commercial_fields(self):
|
||||
|
||||
@@ -218,6 +218,7 @@
|
||||
<field name="taxes_id" widget="many2many_tags" domain="[('type_tax_use', '=', 'purchase')]" options="{'no_create': True}"/>
|
||||
<field name="date_planned" widget="date"/>
|
||||
<field name="account_analytic_id" colspan="2" groups="analytic.group_analytic_accounting"/>
|
||||
<field name="analytic_tag_ids" groups="purchase.group_analytic_accounting" widget="many2many_tags" options="{'color_field': 'color'}"/>
|
||||
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
|
||||
</group>
|
||||
</group>
|
||||
@@ -227,6 +228,7 @@
|
||||
</page>
|
||||
<page string="Invoices and Incoming Shipments">
|
||||
<field name="invoice_lines"/>
|
||||
<field name="move_dest_ids" invisible="1"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
|
||||
@@ -313,6 +313,7 @@
|
||||
<field name="qty_to_invoice" invisible="1"/>
|
||||
<field name="qty_delivered_manual" invisible="1"/>
|
||||
<field name="qty_delivered_method" invisible="1"/>
|
||||
<field name="price_total" invisible="1"/>
|
||||
<field name="price_subtotal" invisible="1"/>
|
||||
<label for="product_uom_qty" string="Ordered Quantity"/>
|
||||
<div>
|
||||
@@ -479,6 +480,7 @@
|
||||
<field name="product_uom_qty"/>
|
||||
<field name="product_uom" groups="uom.group_uom"/>
|
||||
<field name="price_subtotal"/>
|
||||
<field name="price_total" invisible="1"/>
|
||||
<field name="price_unit"/>
|
||||
<field name="display_type"/>
|
||||
<templates>
|
||||
|
||||
@@ -130,8 +130,10 @@ var FieldManagerMixin = {
|
||||
if ('offset' in data) {
|
||||
params.offset = data.offset;
|
||||
}
|
||||
this.model.reload(data.id, params).then(function (db_id) {
|
||||
data.on_success(self.model.get(db_id));
|
||||
this.mutex.exec(function () {
|
||||
return self.model.reload(data.id, params).then(function (db_id) {
|
||||
data.on_success(self.model.get(db_id));
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -303,7 +303,7 @@ var SelectCreateDialog = ViewDialog.extend({
|
||||
event.stopPropagation(); // prevent this event from bubbling up to the action manager
|
||||
var d = event.data;
|
||||
var searchData = this._process_search_data(d.domains, d.contexts, d.groupbys);
|
||||
this.list_controller.reload(searchData);
|
||||
this.list_controller.reload(_.extend({offset: 0}, searchData));
|
||||
},
|
||||
get_controller_context: '_onGetControllerContext',
|
||||
}),
|
||||
|
||||
@@ -524,6 +524,61 @@ QUnit.module('relational_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('search more pager is reset when doing a new search', function (assert) {
|
||||
assert.expect(6);
|
||||
for(var i = 10; i < 180; i++) {
|
||||
this.data.partner.records.push({
|
||||
id: i,
|
||||
display_name: "Partner " + i,
|
||||
});
|
||||
}
|
||||
|
||||
function stringToEvent ($element, string) {
|
||||
for (var i = 0; i < string.length; i++) {
|
||||
var keyAscii = string.charCodeAt(i);
|
||||
$element.val($element.val()+string[i]);
|
||||
$element.trigger($.Event('keyup', {which: keyAscii, keyCode:keyAscii}));
|
||||
}
|
||||
$element.trigger($.Event('keyup', {which: $.ui.keyCode.ENTER, keyCode:$.ui.keyCode.ENTER}));
|
||||
}
|
||||
|
||||
this.data.partner.fields.datetime.searchable = true;
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<group>' +
|
||||
'<field name="trululu"/>' +
|
||||
'</group>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
archs: {
|
||||
'partner,false,list': '<tree><field name="display_name"/></tree>',
|
||||
'partner,false,search': '<search><field name="datetime"/><field name="display_name"/></search>',
|
||||
},
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
var $dropdown = form.$('.o_field_many2one input').autocomplete('widget');
|
||||
form.$('.o_field_many2one input').click();
|
||||
$dropdown.find('.o_m2o_dropdown_option:contains(Search)').mouseenter().click();
|
||||
$('.modal .o_pager_next').click();
|
||||
|
||||
assert.strictEqual($('.o_pager_limit').text(), "1173", "there should be 173 records");
|
||||
assert.strictEqual($('.o_pager_value').text(), "181-160", "should display the second page");
|
||||
assert.strictEqual($('tr.o_data_row').length, 80, "should display 80 record");
|
||||
|
||||
stringToEvent($('.modal .o_searchview_input'), 'first');
|
||||
|
||||
assert.strictEqual($('.o_pager_limit').text(), "11", "there should be 1 record");
|
||||
assert.strictEqual($('.o_pager_value').text(), "11-1", "should display the first page");
|
||||
assert.strictEqual($('tr.o_data_row').length, 1, "should display 1 record");
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('onchanges on many2ones trigger when editing record in form view', function (assert) {
|
||||
assert.expect(10);
|
||||
|
||||
@@ -10906,6 +10961,48 @@ QUnit.module('relational_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('quickly switch between pages in one2many list', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.data.partner.records[0].turtles = [1, 2, 3];
|
||||
|
||||
var readDefs = [$.when(), $.Deferred(), $.Deferred()];
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<field name="turtles">' +
|
||||
'<tree limit="1">' +
|
||||
'<field name="display_name"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
mockRPC: function (route, args) {
|
||||
var result = this._super.apply(this, arguments);
|
||||
if (args.method === 'read') {
|
||||
var recordID = args.args[0][0];
|
||||
return $.when(readDefs[recordID - 1]).then(_.constant(result));
|
||||
}
|
||||
return result;
|
||||
},
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
form.$('.o_field_widget[name=turtles] .o_pager_next').click();
|
||||
form.$('.o_field_widget[name=turtles] .o_pager_next').click();
|
||||
|
||||
readDefs[1].resolve();
|
||||
|
||||
assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_data_cell').text(), 'donatello');
|
||||
|
||||
readDefs[2].resolve();
|
||||
|
||||
assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_data_cell').text(), 'raphael');
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.module('FieldMany2Many');
|
||||
|
||||
QUnit.test('many2many kanban: edition', function (assert) {
|
||||
|
||||
@@ -1254,7 +1254,7 @@ class WebsiteRedirect(models.Model):
|
||||
_order = "sequence, id"
|
||||
_rec_name = 'url_from'
|
||||
|
||||
type = fields.Selection([('301', 'Moved permanently (301)'), ('302', 'Moved temporarily (302)')], string='Redirection Type', required=True)
|
||||
type = fields.Selection([('301', 'Moved permanently (301)'), ('302', 'Moved temporarily (302)')], string='Redirection Type', required=True, default='301')
|
||||
url_from = fields.Char('Redirect From', required=True)
|
||||
url_to = fields.Char('Redirect To', required=True)
|
||||
website_id = fields.Many2one('website', 'Website')
|
||||
|
||||
@@ -568,7 +568,7 @@ class WebsiteSale(ProductConfiguratorController):
|
||||
except ValidationError:
|
||||
error["vat"] = 'error'
|
||||
|
||||
if [err for err in error.items() if err == 'missing']:
|
||||
if [err for err in error.values() if err == 'missing']:
|
||||
error_message.append(_('Some required fields are empty.'))
|
||||
|
||||
return error, error_message
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-auto text-right ml-auto mt-2 mb-2">
|
||||
<a t-if="slide.download_security and not is_embedded" target="_top" t-attf-href="/slides/slide/#{slug(slide)}/download" title="Download" id="download">
|
||||
<a t-if="slide.download_security != 'none' and not is_embedded" target="_top" t-attf-href="/slides/slide/#{slug(slide)}/download" title="Download" id="download">
|
||||
<i class="fa fa-download fa-2x" role="img" aria-label="Download"></i>
|
||||
</a>
|
||||
<span id="fullscreen" title="View fullscreen" role="img" aria-label="Fullscreen">
|
||||
|
||||
@@ -21,3 +21,4 @@ Medardo Rodríguez med@merchise.org https://github.com/med-merchise
|
||||
Abel Firvida firvida@merchise.org
|
||||
Abel Firvida abel@merchise.org
|
||||
Mónica Díaz Pena monicadp@merchise.org https://github.com/mdpena
|
||||
Larisa González larisa@merchise.org
|
||||
|
||||
@@ -394,7 +394,7 @@ class IrActionsServer(models.Model):
|
||||
# Create
|
||||
crud_model_id = fields.Many2one('ir.model', string='Create/Write Target Model',
|
||||
oldname='srcmodel_id', help="Model for record creation / update. Set this field only to specify a different model than the base model.")
|
||||
crud_model_name = fields.Char(related='crud_model_id.name', readonly=True)
|
||||
crud_model_name = fields.Char(related='crud_model_id.model', string='Target Model', readonly=True)
|
||||
link_field_id = fields.Many2one('ir.model.fields', string='Link using field',
|
||||
help="Provide the field used to link the newly created record "
|
||||
"on the record on used by the server action.")
|
||||
|
||||
+4
-2
@@ -2766,14 +2766,16 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
|
||||
|
||||
# retrieve results from records; this takes values from the cache and
|
||||
# computes remaining fields
|
||||
self = self.with_prefetch(self._prefetch.copy())
|
||||
data = {record: {'id': record.id} for record in self}
|
||||
missing = set()
|
||||
use_name_get = (load == '_classic_read')
|
||||
for name in (stored + inherited + computed):
|
||||
convert = self._fields[name].convert_to_read
|
||||
# read every field with prefetching limited to self; this avoids
|
||||
# restrict the prefetching of self's model to self; this avoids
|
||||
# computing fields on a larger recordset than self
|
||||
for record in self.with_prefetch():
|
||||
self._prefetch[self._name] = set(self._ids)
|
||||
for record in self:
|
||||
try:
|
||||
data[record][name] = convert(record[name], record, use_name_get)
|
||||
except MissingError:
|
||||
|
||||
@@ -128,17 +128,17 @@ def initialize_sys_path():
|
||||
global ad_paths
|
||||
global hooked
|
||||
|
||||
dd = tools.config.addons_data_dir
|
||||
dd = os.path.normcase(tools.config.addons_data_dir)
|
||||
if os.access(dd, os.R_OK) and dd not in ad_paths:
|
||||
ad_paths.append(dd)
|
||||
|
||||
for ad in tools.config['addons_path'].split(','):
|
||||
ad = os.path.abspath(tools.ustr(ad.strip()))
|
||||
ad = os.path.normcase(os.path.abspath(tools.ustr(ad.strip())))
|
||||
if ad not in ad_paths:
|
||||
ad_paths.append(ad)
|
||||
|
||||
# add base module path
|
||||
base_path = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'addons'))
|
||||
base_path = os.path.normcase(os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'addons')))
|
||||
if base_path not in ad_paths and os.path.isdir(base_path):
|
||||
ad_paths.append(base_path)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user