diff options
| author | Christophe Simonis <chs@odoo.com> | 2019-01-16 17:02:34 +0100 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2019-01-16 17:02:34 +0100 |
| commit | 378b283c02da5e6d3425f339668edcd63ee62fe5 (patch) | |
| tree | bbaecb2d1241e51e8ea5b0a10ab35ff73aff63a6 | |
| parent | e79c35dcbc1525196263cca6aa9da14ca30d2c0a (diff) | |
| parent | 83cc046e9a90e8592d0d47c7162ab7bd923621ba (diff) | |
[MERGE] forward port branch saas-11.3 up to 83cc046e9a9
21 files changed, 223 insertions, 26 deletions
diff --git a/addons/account/static/src/scss/account_reconciliation.scss b/addons/account/static/src/scss/account_reconciliation.scss index 6c21d14e8f2..6e218b9f11b 100644 --- a/addons/account/static/src/scss/account_reconciliation.scss +++ b/addons/account/static/src/scss/account_reconciliation.scss @@ -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 @@ } } } -}
\ No newline at end of file +} diff --git a/addons/event_sale/wizard/event_edit_registration.xml b/addons/event_sale/wizard/event_edit_registration.xml index 92a69107ba7..25422c2947a 100644 --- a/addons/event_sale/wizard/event_edit_registration.xml +++ b/addons/event_sale/wizard/event_edit_registration.xml @@ -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"/> diff --git a/addons/hr_org_chart/static/src/xml/hr_org_chart.xml b/addons/hr_org_chart/static/src/xml/hr_org_chart.xml index e4e7f9fa231..1dfa693fa40 100644 --- a/addons/hr_org_chart/static/src/xml/hr_org_chart.xml +++ b/addons/hr_org_chart/static/src/xml/hr_org_chart.xml @@ -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> diff --git a/addons/l10n_be_invoice_bba/data/mail_template_data.xml b/addons/l10n_be_invoice_bba/data/mail_template_data.xml index e12f7745b91..5e6bfecdfb7 100644 --- a/addons/l10n_be_invoice_bba/data/mail_template_data.xml +++ b/addons/l10n_be_invoice_bba/data/mail_template_data.xml @@ -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 /> diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index f14542398be..77295665439 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -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: diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py index 83444b708a7..f5e931a71d5 100644 --- a/addons/mrp/models/stock_move.py +++ b/addons/mrp/models/stock_move.py @@ -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, diff --git a/addons/mrp/tests/test_order.py b/addons/mrp/tests/test_order.py index e3bd7c3a72e..6dfe4c08e4b 100644 --- a/addons/mrp/tests/test_order.py +++ b/addons/mrp/tests/test_order.py @@ -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') diff --git a/addons/mrp/wizard/change_production_qty.py b/addons/mrp/wizard/change_production_qty.py index 6cda5d64f9b..b92616c9a50 100644 --- a/addons/mrp/wizard/change_production_qty.py +++ b/addons/mrp/wizard/change_production_qty.py @@ -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): diff --git a/addons/purchase/models/res_partner.py b/addons/purchase/models/res_partner.py index dd120858885..ec6fb75d5f8 100644 --- a/addons/purchase/models/res_partner.py +++ b/addons/purchase/models/res_partner.py @@ -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): diff --git a/addons/purchase/views/purchase_views.xml b/addons/purchase/views/purchase_views.xml index 5679d565c9c..65729354471 100644 --- a/addons/purchase/views/purchase_views.xml +++ b/addons/purchase/views/purchase_views.xml @@ -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> diff --git a/addons/sale/views/sale_views.xml b/addons/sale/views/sale_views.xml index e980ad2e61d..258beba4b91 100644 --- a/addons/sale/views/sale_views.xml +++ b/addons/sale/views/sale_views.xml @@ -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> diff --git a/addons/web/static/src/js/views/field_manager_mixin.js b/addons/web/static/src/js/views/field_manager_mixin.js index 343730cc7a4..bc61e92a89a 100644 --- a/addons/web/static/src/js/views/field_manager_mixin.js +++ b/addons/web/static/src/js/views/field_manager_mixin.js @@ -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)); + }); }); }, /** diff --git a/addons/web/static/src/js/views/view_dialogs.js b/addons/web/static/src/js/views/view_dialogs.js index ee11afbc3bd..3df17bbf155 100644 --- a/addons/web/static/src/js/views/view_dialogs.js +++ b/addons/web/static/src/js/views/view_dialogs.js @@ -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', }), diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index 17fe5788663..e0ab5ae7a5f 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -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) { diff --git a/addons/website/models/website.py b/addons/website/models/website.py index ee2d4322782..252ec58ee14 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -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') diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index 0b5ed81a6e5..4f0f84cebf6 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -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 diff --git a/addons/website_slides/views/website_slides_embed.xml b/addons/website_slides/views/website_slides_embed.xml index 35731f1f4cc..7470c27dbd0 100644 --- a/addons/website_slides/views/website_slides_embed.xml +++ b/addons/website_slides/views/website_slides_embed.xml @@ -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"> diff --git a/doc/cla/corporate/merchise.md b/doc/cla/corporate/merchise.md index e4cbd3c3f95..64d5b277ccd 100644 --- a/doc/cla/corporate/merchise.md +++ b/doc/cla/corporate/merchise.md @@ -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 diff --git a/odoo/addons/base/models/ir_actions.py b/odoo/addons/base/models/ir_actions.py index 428fabd6dcf..aa64b207407 100644 --- a/odoo/addons/base/models/ir_actions.py +++ b/odoo/addons/base/models/ir_actions.py @@ -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.") diff --git a/odoo/models.py b/odoo/models.py index d426789c757..96c6d5c3d76 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -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: diff --git a/odoo/modules/module.py b/odoo/modules/module.py index 93be467b0f3..e732104f3f2 100644 --- a/odoo/modules/module.py +++ b/odoo/modules/module.py @@ -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) |
