[MERGE] forward port branch saas-11.3 up to 83cc046e9a

This commit is contained in:
Christophe Simonis
2019-01-16 17:02:34 +01:00
21 changed files with 223 additions and 26 deletions
@@ -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">&#8230;</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 />
+4 -1
View File
@@ -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:
+18
View File
@@ -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,
+46
View File
@@ -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):
+29 -6
View File
@@ -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):
+2
View File
@@ -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>
+2
View File
@@ -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) {
+1 -1
View File
@@ -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')
+1 -1
View File
@@ -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">
+1
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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:
+3 -3
View File
@@ -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)