diff options
| author | Christophe Simonis <chs@odoo.com> | 2018-08-21 16:58:17 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2018-08-21 16:58:17 +0200 |
| commit | 70d0148d0da1ccf3727310d8e7c53e2c518a8d6a (patch) | |
| tree | 00c4884eca26b424989619b778d6ce9216e9493b /addons | |
| parent | c8762eaa03cc9dda4c7318251726af1b83a2c10f (diff) | |
| parent | fbfb91799eecc5297ab6c86b657bf213343f6cd8 (diff) | |
[MERGE] forward port branch 11.0 up to fbfb91799ee
Diffstat (limited to 'addons')
30 files changed, 335 insertions, 62 deletions
diff --git a/addons/account/views/report_invoice.xml b/addons/account/views/report_invoice.xml index 5acb2ffcb9f..018f95380ff 100644 --- a/addons/account/views/report_invoice.xml +++ b/addons/account/views/report_invoice.xml @@ -112,6 +112,12 @@ <t t-foreach="o._get_tax_amount_by_group()" t-as="amount_by_group"> <tr style="border-bottom:1px solid #dddddd;"> <t t-if="len(o.tax_line_ids) == 1 and o.amount_untaxed == amount_by_group[2]"> + <td><span t-esc="amount_by_group[0]"/></td> + <td class="text-right"> + <span t-esc="amount_by_group[3]" /> + </td> + </t> + <t t-else=""> <td> <span t-esc="amount_by_group[0]"/> <span>&nbsp;<span>on</span> @@ -122,12 +128,6 @@ <span t-esc="amount_by_group[3]"/> </td> </t> - <t t-else=""> - <td><span t-esc="amount_by_group[0]"/></td> - <td class="text-right"> - <span t-esc="amount_by_group[3]" /> - </td> - </t> </tr> </t> <tr class="border-black"> diff --git a/addons/base_sparse_field/models/models.py b/addons/base_sparse_field/models/models.py index 77ea36a6bd8..703c6466ee9 100644 --- a/addons/base_sparse_field/models/models.py +++ b/addons/base_sparse_field/models/models.py @@ -9,7 +9,7 @@ class IrModelFields(models.Model): ttype = fields.Selection(selection_add=[('serialized', 'serialized')]) serialization_field_id = fields.Many2one('ir.model.fields', string='Serialization Field', - ondelete='cascade', domain="[('ttype','=','serialized')]", + ondelete='cascade', domain="[('ttype','=','serialized'), ('model_id', '=', model_id)]", help="If set, this field will be stored in the sparse structure of the " "serialization field, instead of having its own database column. " "This cannot be changed after creation.", diff --git a/addons/base_sparse_field/views/views.xml b/addons/base_sparse_field/views/views.xml index 95c3d23634a..eaf684247af 100644 --- a/addons/base_sparse_field/views/views.xml +++ b/addons/base_sparse_field/views/views.xml @@ -22,7 +22,7 @@ <field name="arch" type="xml"> <field name="related" position="before"> <field name="serialization_field_id" - domain="[('ttype','=','serialized'), ('model_id','=',model_id)]" + context="{'default_model_id': model_id, 'default_ttype': 'serialized'}" attrs="{'readonly': [('state','=','base')]}"/> </field> </field> diff --git a/addons/calendar/models/calendar.py b/addons/calendar/models/calendar.py index c91f7033b67..8d95dada86c 100644 --- a/addons/calendar/models/calendar.py +++ b/addons/calendar/models/calendar.py @@ -947,11 +947,13 @@ class Meeting(models.Model): @api.onchange('start_date') def _onchange_start_date(self): - self.start = self.start_date + if self.start_date: + self.start = self.start_date @api.onchange('stop_date') def _onchange_stop_date(self): - self.stop = self.stop_date + if self.stop_date: + self.stop = self.stop_date #################################################### # Calendar Business, Reccurency, ... diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index c3cd70392c3..cf112b817c0 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -702,7 +702,7 @@ class Lead(models.Model): def _get_duplicated_leads_by_emails(self, partner_id, email, include_lost=False): """ Search for opportunities that have the same partner and that arent done or cancelled """ if not email: - return [] + return self.env['crm.lead'] partner_match_domain = [] for email in set(email_split(email) + [email]): partner_match_domain.append(('email_from', '=ilike', email)) diff --git a/addons/document/static/src/js/document.js b/addons/document/static/src/js/document.js index 0af85179b49..98bfe4e5ef3 100644 --- a/addons/document/static/src/js/document.js +++ b/addons/document/static/src/js/document.js @@ -15,12 +15,13 @@ Sidebar.include({ */ init : function (parent, options) { this._super.apply(this, arguments); - if (options.viewType === "form") { + this.hasAttachments = options.viewType === "form"; + if (this.hasAttachments) { this.sections.splice(1, 0, { 'name' : 'files', 'label' : _t('Attachment(s)'), }); this.items.files = []; + this.fileuploadId = _.uniqueId('oe_fileupload'); + $(window).on(this.fileuploadId, this._onFileUploaded.bind(this)); } - this.fileuploadId = _.uniqueId('oe_fileupload'); - $(window).on(this.fileuploadId, this._onFileUploaded.bind(this)); }, /** * Get the attachment linked to the record when the toolbar started @@ -29,13 +30,16 @@ Sidebar.include({ */ start: function () { var _super = this._super.bind(this); - this._updateAttachments().then(_super); + var def = this.hasAttachments ? this._updateAttachments() : $.when(); + return def.then(_super); }, /** * @override */ destroy: function () { - $(window).off(this.fileuploadId); + if (this.hasAttachments) { + $(window).off(this.fileuploadId); + } this._super.apply(this, arguments); }, @@ -46,8 +50,10 @@ Sidebar.include({ * @override */ updateEnv: function (env) { - this.env = env; - this._updateAttachments().then(this._redraw.bind(this)); + if (this.hasAttachments) { + this.env = env; + this._updateAttachments().then(this._redraw.bind(this)); + } }, //-------------------------------------------------------------------------- @@ -90,10 +96,12 @@ Sidebar.include({ */ _redraw: function () { this._super.apply(this, arguments); - this.$('.o_sidebar_add_attachment .o_form_binary_form') - .change(this._onAddAttachment.bind(this)); - this.$('.o_sidebar_delete_attachment') - .click(this._onDeleteAttachment.bind(this)); + if (this.hasAttachments) { + this.$('.o_sidebar_add_attachment .o_form_binary_form') + .change(this._onAddAttachment.bind(this)); + this.$('.o_sidebar_delete_attachment') + .click(this._onDeleteAttachment.bind(this)); + } }, /** * Update the attachments to be displayed in the attachment section @@ -102,6 +110,9 @@ Sidebar.include({ * @private */ _updateAttachments: function () { + if (this.items.files === undefined) { + return $.when(); + } var activeId = this.env.activeIds[0]; if (!activeId) { this.items.files = []; diff --git a/addons/document/static/tests/document_tests.js b/addons/document/static/tests/document_tests.js index 338f3e75581..96c699c041b 100644 --- a/addons/document/static/tests/document_tests.js +++ b/addons/document/static/tests/document_tests.js @@ -3,6 +3,7 @@ odoo.define('document.tests', function (require) { var testUtils = require('web.test_utils'); var FormView = require('web.FormView'); + var ListView = require('web.ListView'); var createView = testUtils.createView; @@ -16,6 +17,9 @@ odoo.define('document.tests', function (require) { records: [{ id: 1, display_name: "first record", + }, { + id: 2, + display_name: "second record", }] }, 'ir.attachment': { @@ -74,5 +78,34 @@ odoo.define('document.tests', function (require) { assert.strictEqual(form.sidebar.$('.o_sidebar_delete_attachment').length, 1, "there should be only one attachment"); form.destroy(); }); + + QUnit.test('no attachment on list view', function (assert) { + assert.expect(4); + + var list = createView({ + View: ListView, + model: 'partner', + data: this.data, + groupBy: ['display_name'], + viewOptions: {sidebar: true}, + arch: '<tree string="Partners">' + + '<field name="display_name"/>' + + '</tree>', + mockRPC: function (route, args) { + assert.step(args.model); + return this._super.apply(this, arguments); + } + }); + + // select record then trigger render + list.$('.o_group_header:last').click(); + list.$('.o_data_row input').click(); + list.$('.o_group_header:first').click(); + + assert.verifySteps(['partner', 'partner', 'partner'], + "ir.attachment not called when selecting record in list view"); + + list.destroy(); + }); }); }); diff --git a/addons/gamification/models/badge.py b/addons/gamification/models/badge.py index 3ca11577c33..0fd69631650 100644 --- a/addons/gamification/models/badge.py +++ b/addons/gamification/models/badge.py @@ -161,13 +161,13 @@ class GamificationBadge(models.Model): for badge in self: owners = badge.owner_ids - badge.stats_my = sum(o.user_id == self.env.user for o in owners) - badge.stats_this_month = sum(o.create_date >= first_month_day for o in owners) - badge.stats_my_this_month = sum( + badge.stat_my = sum(o.user_id == self.env.user for o in owners) + badge.stat_this_month = sum(o.create_date >= first_month_day for o in owners) + badge.stat_my_this_month = sum( o.user_id == self.env.user and o.create_date >= first_month_day for o in owners ) - badge.stats_my_monthly_sending = sum( + badge.stat_my_monthly_sending = sum( o.create_uid == self.env.user and o.create_date >= first_month_day for o in owners ) diff --git a/addons/hr_maintenance/models/equipment.py b/addons/hr_maintenance/models/equipment.py index ebfa6eb03fe..d66f29fdfc6 100644 --- a/addons/hr_maintenance/models/equipment.py +++ b/addons/hr_maintenance/models/equipment.py @@ -9,7 +9,7 @@ class MaintenanceEquipment(models.Model): employee_id = fields.Many2one('hr.employee', string='Assigned to Employee', track_visibility='onchange') department_id = fields.Many2one('hr.department', string='Assigned to Department', track_visibility='onchange') equipment_assign_to = fields.Selection( - [('department', 'Department'), ('employee', 'Employee') ,('other', 'Other')], + [('department', 'Department'), ('employee', 'Employee'), ('other', 'Other')], string='Used By', required=True, default='employee') @@ -82,10 +82,11 @@ class MaintenanceRequest(models.Model): @api.depends('employee_id', 'department_id') def _compute_owner(self): - if self.equipment_id.equipment_assign_to == 'employee': - self.owner_user_id = self.employee_id.user_id.id - elif self.equipment_id.equipment_assign_to == 'department': - self.owner_user_id = self.department_id.manager_id.user_id.id + for r in self: + if r.equipment_id.equipment_assign_to == 'employee': + r.owner_user_id = r.employee_id.user_id.id + elif r.equipment_id.equipment_assign_to == 'department': + r.owner_user_id = r.department_id.manager_id.user_id.id @api.onchange('employee_id', 'department_id') def onchange_department_or_employee_id(self): diff --git a/addons/hr_timesheet/views/project_views.xml b/addons/hr_timesheet/views/project_views.xml index 619878cf459..75f4f42ece8 100644 --- a/addons/hr_timesheet/views/project_views.xml +++ b/addons/hr_timesheet/views/project_views.xml @@ -111,7 +111,7 @@ <field eval="2" name="priority"/> <field name="arch" type="xml"> <field name="user_id" position="after"> - <field name="planned_hours"/> + <field name="planned_hours" widget="float_time" sum="Initially Planned Hours" /> <field name="remaining_hours" widget="float_time" sum="Remaining Hours" readonly="1"/> <field name="effective_hours" widget="float_time" sum="Spent Hours" invisible="1"/> </field> diff --git a/addons/l10n_be_invoice_bba/views/account_invoice_view.xml b/addons/l10n_be_invoice_bba/views/account_invoice_view.xml index 9ab348940a1..ee3e8470e93 100644 --- a/addons/l10n_be_invoice_bba/views/account_invoice_view.xml +++ b/addons/l10n_be_invoice_bba/views/account_invoice_view.xml @@ -12,4 +12,15 @@ </field> </field> </record> + + <record id="supplier_invoice_bbacomm_form" model="ir.ui.view"> + <field name="name">account.invoice.supplier.form</field> + <field name="model">account.invoice</field> + <field name="inherit_id" ref="account.invoice_supplier_form"/> + <field name="arch" type="xml"> + <field name="reference" position="before"> + <field name="reference_type" attrs="{'readonly':[('state','!=','draft')]}"/> + </field> + </field> + </record> </odoo> diff --git a/addons/l10n_ch/data/account_vat2011_data.xml b/addons/l10n_ch/data/account_vat2011_data.xml index 3dd350761ba..236ba2fb215 100644 --- a/addons/l10n_ch/data/account_vat2011_data.xml +++ b/addons/l10n_ch/data/account_vat2011_data.xml @@ -44,6 +44,10 @@ <field name="name">Switzerland VAT Form: grid 381 base</field> <field name="applicability">taxes</field> </record> + <record id="vat_tag_382_a" model="account.account.tag"> + <field name="name">Switzerland VAT Form: grid 382 base</field> + <field name="applicability">taxes</field> + </record> <record id="vat_tag_302_b" model="account.account.tag"> <field name="name">Switzerland VAT Form: grid 302 tax</field> <field name="applicability">taxes</field> @@ -60,6 +64,10 @@ <field name="name">Switzerland VAT Form: grid 381 tax</field> <field name="applicability">taxes</field> </record> + <record id="vat_tag_382_b" model="account.account.tag"> + <field name="name">Switzerland VAT Form: grid 382 tax</field> + <field name="applicability">taxes</field> + </record> <record id="vat_tag_400" model="account.account.tag"> <field name="name">Switzerland VAT Form: grid 400</field> <field name="applicability">taxes</field> @@ -82,7 +90,7 @@ </record> <record id="vat_tag_dedouanement" model="account.account.tag"> - <field name="name">Switzerland VAT Form: 100% dédouanement (base in grid 400)</field> + <field name="name">Switzerland VAT Form: 100% dédouanement (base in grid 400)</field> <field name="applicability">taxes</field> </record> @@ -320,6 +328,18 @@ <field name="tag_ids" eval="[(6,0,[ref('vat_tag_405')])]"/> <field name="tax_group_id" ref="tax_group_tva_77"/> </record> + <record model="account.tax.template" id="vat_77_purchase_return"> + <field name="name">TVA due a 7.7% (TN) (return)</field> + <field name="description">7.7% achat (return)</field> + <field name="amount" eval="-7.7"/> + <field name="amount_type">percent</field> + <field name="sequence" eval="0"/> + <field name="chart_template_id" ref="l10nch_chart_template"/> + <field name="type_tax_use">purchase</field> + <field name="refund_account_id" ref="ch_coa_1170"/> + <field name="account_id" ref="ch_coa_1170"/> + <field name="tag_ids" eval="[(6,0,[ref('vat_tag_382_a'),ref('vat_tag_382_b')])]"/> + </record> <record model="account.tax.template" id="vat_XO"> <field name="name">TVA due a 0% (Exportations)</field> <field name="amount" eval="0.00"/> @@ -368,4 +388,15 @@ <field name="tag_ids" eval="[(6,0,[ref('vat_tag_dedouanement')])]"/> <field name="tax_group_id" ref="tax_group_tva_0"/> </record> + <!--# for reverse charge or VAT on Acquisition (group of taxes)--> + <record model="account.tax.template" id="vat_77_purchase_reverse"> + <field name="description">7.7% achat</field> + <field name="name">TVA 7.7% sur achat service a l'etranger (reverse charge)</field> + <field name="amount" eval="7.7"/> + <field name="amount_type">group</field> + <field name="type_tax_use">purchase</field> + <field name="children_tax_ids" eval="[(6, 0, [ref('vat_77_purchase_return'), ref('vat_77_purchase')])]"/> + <field name="chart_template_id" ref="l10nch_chart_template"/> + <field name="tag_ids" eval="[(6,0,[ref('vat_tag_382_a'), ref('vat_tag_382_b')])]"/> + </record> </odoo> diff --git a/addons/l10n_co/data/account.account.template.csv b/addons/l10n_co/data/account.account.template.csv index e73728e96cf..b68f925a074 100644 --- a/addons/l10n_co/data/account.account.template.csv +++ b/addons/l10n_co/data/account.account.template.csv @@ -173,7 +173,6 @@ co_puc_413568,413568,Venta de equipo profesional y científico,FALSE,account.dat co_puc_413570,413570,"Venta de loterías, rifas, chance, apuestas y similares",FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic co_puc_413572,413572,Reparación de efectos personales y electrodomésticos,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic co_puc_413595,413595,Venta de Otros Productos,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic -co_puc_413595,413595,Venta de otros productos,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic co_puc_415505,415505,Arrendamientos de Bienes Inmuebles,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic co_puc_421005,421005,Intereses,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic co_puc_421020,421020,Diferencia en Cambio,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic diff --git a/addons/l10n_fr_hr_payroll/report/report_l10n_fr_fiche_paye.xml b/addons/l10n_fr_hr_payroll/report/report_l10n_fr_fiche_paye.xml index c9fcc8ae3fd..4f6e38bee9d 100644 --- a/addons/l10n_fr_hr_payroll/report/report_l10n_fr_fiche_paye.xml +++ b/addons/l10n_fr_hr_payroll/report/report_l10n_fr_fiche_paye.xml @@ -12,7 +12,7 @@ <h4>Paie du <span t-field="o.date_from"/> au <span t-field="o.date_to"/></h4> </div> <div class="col-xs-4 pull-right"> - <table class="table table-bordered" stype="border: 2px solid black;"> + <table class="table table-bordered" style="border: 2px solid black;"> <tr> <td> <div t-field="o.employee_id.company_id.partner_id" diff --git a/addons/membership/models/account_invoice.py b/addons/membership/models/account_invoice.py index 85b158e4959..90423ab571f 100644 --- a/addons/membership/models/account_invoice.py +++ b/addons/membership/models/account_invoice.py @@ -22,6 +22,15 @@ class Invoice(models.Model): ]).write({'date_cancel': fields.Date.today()}) return super(Invoice, self).action_cancel() + @api.multi + def write(self, vals): + '''Change the partner on related membership_line''' + if 'partner_id' in vals: + self.env['membership.membership_line'].search([ + ('account_invoice_line', 'in', self.mapped('invoice_line_ids').ids) + ]).write({'partner': vals['partner_id']}) + return super(Invoice, self).write(vals) + class AccountInvoiceLine(models.Model): _inherit = 'account.invoice.line' diff --git a/addons/membership/models/partner.py b/addons/membership/models/partner.py index 6b66bfb07b5..a5ec46f701f 100644 --- a/addons/membership/models/partner.py +++ b/addons/membership/models/partner.py @@ -39,6 +39,8 @@ class Partner(models.Model): @api.depends('member_lines.account_invoice_line.invoice_id.state', 'member_lines.account_invoice_line.invoice_id.invoice_line_ids', 'member_lines.account_invoice_line.invoice_id.payment_ids', + 'member_lines.account_invoice_line.invoice_id.payment_move_line_ids', + 'member_lines.account_invoice_line.invoice_id.partner_id', 'free_member', 'member_lines.date_to', 'member_lines.date_from', 'associate_member.membership_state') @@ -104,18 +106,22 @@ class Partner(models.Model): if partner.membership_stop and today > partner.membership_stop: res[partner.id] = 'free' if partner.free_member else 'old' continue + if partner.associate_member: + res_state = partner.associate_member._membership_state() + res[partner.id] = res_state[partner.associate_member.id] + continue s = 4 if partner.member_lines: for mline in partner.member_lines: if (mline.date_to or '0000-00-00') >= today and (mline.date_from or '0000-00-00') <= today: - if mline.account_invoice_line.invoice_id: + if mline.account_invoice_line.invoice_id.partner_id == partner: mstate = mline.account_invoice_line.invoice_id.state if mstate == 'paid': s = 0 inv = mline.account_invoice_line.invoice_id - for payment in inv.payment_ids: - if any(payment.invoice_ids.filtered(lambda inv: inv.type == 'out_refund')): + for ml in inv.payment_move_line_ids: + if any(ml.invoice_id.filtered(lambda inv: inv.type == 'out_refund')): s = 2 break elif mstate == 'open' and s != 0: @@ -144,9 +150,6 @@ class Partner(models.Model): res[partner.id] = 'none' if partner.free_member and s != 0: res[partner.id] = 'free' - if partner.associate_member: - res_state = partner.associate_member._membership_state() - res[partner.id] = res_state[partner.associate_member.id] return res @api.one diff --git a/addons/mrp/tests/test_order.py b/addons/mrp/tests/test_order.py index 6c56fa22275..d8f7f479d58 100644 --- a/addons/mrp/tests/test_order.py +++ b/addons/mrp/tests/test_order.py @@ -578,3 +578,42 @@ class TestMrpOrder(TestMrpCommon): mo.button_mark_done() self.assertEqual(mo.state, 'done', "Production order should be in done state.") + + def test_product_produce_4(self): + """ Possibility to produce with a given raw material in multiple locations. """ + self.stock_location = self.env.ref('stock.stock_location_stock') + self.stock_shelf_1 = self.env.ref('stock.stock_location_components') + self.stock_shelf_2 = self.env.ref('stock.stock_location_14') + mo, _, p_final, p1, p2 = self.generate_mo(qty_final=1, qty_base_1=5) + + self.env['stock.quant']._update_available_quantity(p1, self.stock_shelf_1, 2) + self.env['stock.quant']._update_available_quantity(p1, self.stock_shelf_2, 3) + self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 1) + + mo.action_assign() + ml_p1 = mo.move_raw_ids.filtered(lambda x: x.product_id == p1).mapped('move_line_ids') + ml_p2 = mo.move_raw_ids.filtered(lambda x: x.product_id == p2).mapped('move_line_ids') + self.assertEqual(len(ml_p1), 2) + self.assertEqual(len(ml_p2), 1) + + # Add some quantity already done to force an extra move line to be created + ml_p1[0].qty_done = 1.0 + + # Produce baby! + product_produce = self.env['mrp.product.produce'].with_context({ + 'active_id': mo.id, + 'active_ids': [mo.id], + }).create({ + 'product_qty': 1.0, + }) + product_produce.do_produce() + + ml_p1 = mo.move_raw_ids.filtered(lambda x: x.product_id == p1).mapped('move_line_ids') + self.assertEqual(len(ml_p1), 3) + for ml in ml_p1: + self.assertIn(ml.qty_done, [1.0, 2.0, 3.0], 'Quantity done should be 1.0, 2.0 or 3.0') + self.assertEqual(sum(ml_p1.mapped('qty_done')), 6.0, 'Total qty consumed should be 6.0') + self.assertEqual(sum(ml_p1.mapped('product_uom_qty')), 5.0, 'Total qty reserved should be 5.0') + + mo.button_mark_done() + self.assertEqual(mo.state, 'done', "Production order should be in done state.") diff --git a/addons/mrp/wizard/mrp_product_produce.py b/addons/mrp/wizard/mrp_product_produce.py index 57bffd5d2fe..37e85825cdd 100644 --- a/addons/mrp/wizard/mrp_product_produce.py +++ b/addons/mrp/wizard/mrp_product_produce.py @@ -100,8 +100,10 @@ class MrpProductProduce(models.TransientModel): if self.product_id.tracking != 'none': qty_to_add = float_round(quantity * move.unit_factor, precision_rounding=rounding) move._generate_consumed_move_line(qty_to_add, self.lot_id) - else: + elif len(move._get_move_lines()) < 2: move.quantity_done += float_round(quantity * move.unit_factor, precision_rounding=rounding) + else: + self._set_quantity_done(move, quantity * move.unit_factor) for move in self.production_id.move_finished_ids: if move.product_id.tracking == 'none' and move.state not in ('done', 'cancel'): rounding = move.product_uom.rounding @@ -118,6 +120,40 @@ class MrpProductProduce(models.TransientModel): }) return {'type': 'ir.actions.act_window_close'} + def _set_quantity_done(self, move, qty): + """ + Set the given quantity as quantity done on the move through the move lines. The method is + able to handle move lines with a different UoM than the move (but honestly, this would be + looking for trouble...). + @param move: stock.move + @param qty: quantity in the UoM of move.product_uom + """ + for ml in move.move_line_ids: + # Convert move line qty into move uom + ml_qty = ml.product_uom_qty - ml.qty_done + if ml.product_uom_id != move.product_uom: + ml_qty = ml.product_uom_id._compute_quantity(ml_qty, move.product_uom, round=False) + + taken_qty = min(qty, ml_qty) + # Convert taken qty into move line uom + if ml.product_uom_id != move.product_uom: + taken_qty = move.product_uom._compute_quantity(ml_qty, ml.product_uom_id, round=False) + + # Assign qty_done and explicitly round to make sure there is no inconsistency between + # ml.qty_done and qty. + taken_qty = float_round(taken_qty, precision_rounding=ml.product_uom_id.rounding) + ml.qty_done += taken_qty + if ml.product_uom_id != move.product_uom: + taken_qty = ml.product_uom_id._compute_quantity(ml_qty, move.product_uom, round=False) + qty -= taken_qty + + if float_compare(qty, 0.0, precision_rounding=move.product_uom.rounding) <= 0: + break + if float_compare(qty, 0.0, precision_rounding=move.product_uom.rounding) > 0: + vals = move._prepare_move_line_vals(quantity=0) + vals['qty_done'] = qty + ml = self.env['stock.move.line'].create(vals) + @api.multi def check_finished_move_lots(self): produce_move = self.production_id.move_finished_ids.filtered(lambda x: x.product_id == self.product_id and x.state not in ('done', 'cancel')) diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index ece3fc30a60..56d8500a3cd 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -180,7 +180,7 @@ class PosOrder(models.Model): } @api.model - def _get_account_move_line_group_data_type_key(self, data_type, values): + def _get_account_move_line_group_data_type_key(self, data_type, values, options={}): """ Return a tuple which will be used as a key for grouping account move lines in _create_account_move_line method. @@ -195,10 +195,16 @@ class PosOrder(models.Model): values['analytic_account_id'], values['debit'] > 0) elif data_type == 'tax': - return ('tax', - values['partner_id'], - values['tax_line_id'], - values['debit'] > 0) + order_id = values.pop('order_id', False) + tax_key = ('tax', + values['partner_id'], + values['tax_line_id'], + values['debit'] > 0) + if options.get('rounding_method') == 'round_globally': + tax_key = ('tax', + values['tax_line_id'], + order_id) + return tax_key elif data_type == 'counter_part': return ('counter_part', values['partner_id'], @@ -308,7 +314,7 @@ class PosOrder(models.Model): 'move_id': move.id, }) - key = self._get_account_move_line_group_data_type_key(data_type, values) + key = self._get_account_move_line_group_data_type_key(data_type, values, {'rounding_method': rounding_method}) if not key: return @@ -322,6 +328,14 @@ class PosOrder(models.Model): current_value['quantity'] = current_value.get('quantity', 0.0) + values.get('quantity', 0.0) current_value['credit'] = current_value.get('credit', 0.0) + values.get('credit', 0.0) current_value['debit'] = current_value.get('debit', 0.0) + values.get('debit', 0.0) + if key[0] == 'tax' and rounding_method == 'round_globally': + if current_value['debit'] - current_value['credit'] > 0: + current_value['debit'] = current_value['debit'] - current_value['credit'] + current_value['credit'] = 0 + else: + current_value['credit'] = current_value['credit'] - current_value['debit'] + current_value['debit'] = 0 + else: grouped_data[key].append(values) @@ -381,7 +395,8 @@ class PosOrder(models.Model): 'credit': ((tax['amount'] > 0) and tax['amount']) or 0.0, 'debit': ((tax['amount'] < 0) and -tax['amount']) or 0.0, 'tax_line_id': tax['id'], - 'partner_id': partner_id + 'partner_id': partner_id, + 'order_id': order.id }) # round tax lines per order diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index a23b478a61f..7984cd13c8f 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -480,7 +480,7 @@ class PurchaseOrder(models.Model): 'sequence': max(line.product_id.seller_ids.mapped('sequence')) + 1 if line.product_id.seller_ids else 1, 'product_uom': line.product_uom.id, 'min_qty': 0.0, - 'price': self.currency_id.compute(line.price_unit, currency), + 'price': self.currency_id.compute(line.price_unit, currency, round=False), 'currency_id': currency.id, 'delay': 0, } diff --git a/addons/sale_stock/models/stock.py b/addons/sale_stock/models/stock.py index eee0424021c..2a7f9e5560e 100644 --- a/addons/sale_stock/models/stock.py +++ b/addons/sale_stock/models/stock.py @@ -36,6 +36,15 @@ class StockMove(models.Model): #rslt += invoices.mapped('refund_invoice_ids') return rslt + def _assign_picking_post_process(self, new=False): + super(StockMove, self)._assign_picking_post_process(new=new) + if new and self.sale_line_id and self.sale_line_id.order_id: + self.picking_id.message_post_with_view( + 'mail.message_origin_link', + values={'self': self.picking_id, 'origin': self.sale_line_id.order_id}, + subtype_id=self.env.ref('mail.mt_note').id) + + class ProcurementGroup(models.Model): _inherit = 'procurement.group' diff --git a/addons/sales_team/models/crm_team.py b/addons/sales_team/models/crm_team.py index f3708fc5b10..180e2879cdd 100644 --- a/addons/sales_team/models/crm_team.py +++ b/addons/sales_team/models/crm_team.py @@ -8,7 +8,7 @@ from dateutil.relativedelta import relativedelta import json from odoo import api, fields, models, _ -from odoo.exceptions import UserError +from odoo.exceptions import AccessError, UserError from odoo.release import version from odoo.tools import DEFAULT_SERVER_DATE_FORMAT as DF @@ -33,8 +33,13 @@ class CrmTeam(models.Model): team_id = self.env['crm.team'].browse(self.env.context.get('default_team_id')) if not team_id: default_team_id = self.env.ref('sales_team.team_sales_department', raise_if_not_found=False) - if default_team_id and (self.env.context.get('default_type') != 'lead' or default_team_id.use_leads): - team_id = default_team_id + if default_team_id: + try: + default_team_id.check_access_rule('read') + except AccessError: + return self.env['crm.team'] + if self.env.context.get('default_type') != 'lead' or default_team_id.use_leads and default_team_id.active: + team_id = default_team_id return team_id def _get_default_favorite_user_ids(self): diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 88aede3ecf3..7f190e8d08a 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -716,7 +716,7 @@ class StockMove(models.Model): recompute = True picking = Picking.create(move._get_new_picking_values()) move.write({'picking_id': picking.id}) - + move._assign_picking_post_process(new=recompute) # If this method is called in batch by a write on a one2many and # at some point had to create a picking, some next iterations could # try to find back the created picking. As we look for it by searching @@ -726,6 +726,9 @@ class StockMove(models.Model): move.recompute() return True + def _assign_picking_post_process(self, new=False): + pass + def _get_new_picking_values(self): """ Prepares a new picking for this move as it could not be assigned to another picking. This method is designed to be inherited. """ diff --git a/addons/stock/models/stock_scrap.py b/addons/stock/models/stock_scrap.py index bb985547ede..80fc6e0eabb 100644 --- a/addons/stock/models/stock_scrap.py +++ b/addons/stock/models/stock_scrap.py @@ -121,6 +121,8 @@ class StockScrap(models.Model): def action_validate(self): self.ensure_one() + if self.product_id.type != 'product': + return self.do_scrap() precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') available_qty = sum(self.env['stock.quant']._gather(self.product_id, self.location_id, diff --git a/addons/stock/report/report_deliveryslip.xml b/addons/stock/report/report_deliveryslip.xml index 50289bee840..a5a9d7d0fe5 100644 --- a/addons/stock/report/report_deliveryslip.xml +++ b/addons/stock/report/report_deliveryslip.xml @@ -11,11 +11,11 @@ <div> <span><strong>Customer Address:</strong></span> </div> - <div t-if="o.move_lines and o.move_lines[0].partner_id and not o.partner_id" name="partner_header"> + <div t-if="o.move_lines and o.move_lines[0].partner_id" name="partner_header"> <div t-field="o.move_lines[0].partner_id" t-options='{"widget": "contact", "fields": ["address", "name", "phone"], "no_marker": True}'/> </div> - <div t-if="o.partner_id" name="partner_header"> + <div t-if="not (o.move_lines and o.move_lines[0].partner_id) and o.partner_id" name="partner_header"> <div t-field="o.partner_id" t-options='{"widget": "contact", "fields": ["address", "name", "phone"], "no_marker": True}'/> </div> diff --git a/addons/web/static/src/js/views/list/list_controller.js b/addons/web/static/src/js/views/list/list_controller.js index 3262196fa51..4629e57eb7f 100644 --- a/addons/web/static/src/js/views/list/list_controller.js +++ b/addons/web/static/src/js/views/list/list_controller.js @@ -42,7 +42,7 @@ var ListController = BasicController.extend({ this.toolbarActions = params.toolbarActions || {}; this.editable = params.editable; this.noLeaf = params.noLeaf; - this.selectedRecords = []; // there is no selected record by default + this.selectedRecords = params.selectedRecords || []; }, //-------------------------------------------------------------------------- @@ -171,6 +171,25 @@ var ListController = BasicController.extend({ this._toggleSidebar(); } }, + /** + * Overrides to update the list of selected records + * + * @override + */ + update: function (params, options) { + var self = this; + if (options && options.keepSelection) { + // filter out removed records from selection + var res_ids = this.model.get(this.handle).res_ids; + this.selectedRecords = _.filter(this.selectedRecords, function (id) { + return _.contains(res_ids, self.model.get(id).res_id); + }); + } else { + this.selectedRecords = []; + } + params.selectedRecords = this.selectedRecords; + return this._super.apply(this, arguments); + }, //-------------------------------------------------------------------------- // Private @@ -333,7 +352,6 @@ var ListController = BasicController.extend({ * @returns {Deferred} */ _update: function () { - this.selectedRecords = []; this._toggleSidebar(); return this._super.apply(this, arguments); }, @@ -538,7 +556,7 @@ var ListController = BasicController.extend({ _onToggleGroup: function (event) { this.model .toggleGroup(event.data.group.id) - .then(this.update.bind(this, {}, {reload: false})); + .then(this.update.bind(this, {}, {keepSelection: true, reload: false})); }, }); diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js index 31919962d56..a72cd7008eb 100644 --- a/addons/web/static/src/js/views/list/list_renderer.js +++ b/addons/web/static/src/js/views/list/list_renderer.js @@ -60,7 +60,7 @@ var ListRenderer = BasicRenderer.extend({ return py.parse(py.tokenize(value)); }).value(); this.hasSelectors = params.hasSelectors; - this.selection = []; + this.selection = params.selectedRecords || []; this.pagers = []; // instantiated pagers (only for grouped lists) this.editable = params.editable; }, @@ -81,7 +81,9 @@ var ListRenderer = BasicRenderer.extend({ */ updateState: function (state, params) { this._processColumns(params.columnInvisibleFields || {}); - this.selection = []; + if (params.selectedRecords) { + this.selection = params.selectedRecords; + } return this._super.apply(this, arguments); }, diff --git a/addons/web/static/src/js/views/list/list_view.js b/addons/web/static/src/js/views/list/list_view.js index 3082312e770..bc80017743b 100644 --- a/addons/web/static/src/js/views/list/list_view.js +++ b/addons/web/static/src/js/views/list/list_view.js @@ -35,6 +35,7 @@ var ListView = BasicView.extend({ */ init: function (viewInfo, params) { this._super.apply(this, arguments); + var selectedRecords = []; // there is no selected records by default var mode = this.arch.attrs.editable && !params.readonly ? "edit" : "readonly"; @@ -43,11 +44,13 @@ var ListView = BasicView.extend({ this.controllerParams.toolbarActions = viewInfo.toolbar; this.controllerParams.noLeaf = !!this.loadParams.context.group_by_no_leaf; this.controllerParams.mode = mode; + this.controllerParams.selectedRecords = selectedRecords; this.rendererParams.arch = this.arch; this.rendererParams.hasSelectors = 'hasSelectors' in params ? params.hasSelectors : true; this.rendererParams.editable = params.readonly ? false : this.arch.attrs.editable; + this.rendererParams.selectedRecords = selectedRecords; this.loadParams.limit = this.loadParams.limit || 80; this.loadParams.type = 'list'; diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index 31c89cb0a32..dd73814336f 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -618,6 +618,45 @@ QUnit.module('Views', { list.destroy(); }); + QUnit.test('selection is kept on render without reload', function (assert) { + assert.expect(5); + + var list = createView({ + View: ListView, + model: 'foo', + data: this.data, + groupBy: ['foo'], + viewOptions: {sidebar: true}, + arch: '<tree>' + + '<field name="foo"/>' + + '<field name="int_field" sum="Sum"/>' + + '</tree>', + }); + + // open blip grouping and check all lines + list.$('.o_group_header:contains("blip (2)")').click(); + list.$('.o_data_row input').click(); + assert.strictEqual(true, list.sidebar.$el.is(':visible'), + "element checked so sidebar") + + // open yop grouping and verify blip are still checked + list.$('.o_group_header:contains("yop (1)")').click() + assert.strictEqual(2, list.$('.o_data_row input:checked').length, + "opening a grouping does not uncheck others"); + assert.strictEqual(true, list.sidebar.$el.is(':visible'), + "element checked so sidebar") + + // close and open blip grouping and verify blip are unchecked + list.$('.o_group_header:contains("blip (2)")').click(); + list.$('.o_group_header:contains("blip (2)")').click(); + assert.strictEqual(0, list.$('.o_data_row input:checked').length, + "opening and closing a grouping uncheck its elements"); + assert.strictEqual(false, list.sidebar.$el.is(':visible'), + "no element checked so no sidebar") + + list.destroy(); + }); + QUnit.test('aggregates are computed correctly', function (assert) { assert.expect(4); diff --git a/addons/website_forum/models/res_users.py b/addons/website_forum/models/res_users.py index 054e3a4d56a..77cbcd7d9e5 100644 --- a/addons/website_forum/models/res_users.py +++ b/addons/website_forum/models/res_users.py @@ -88,7 +88,9 @@ class Users(models.Model): params['forum_id'] = forum_id base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') token_url = base_url + '/forum/validate_email?%s' % urls.url_encode(params) - activation_template.sudo().with_context(token_url=token_url).send_mail(self.id, force_send=True) + with self._cr.savepoint(): + activation_template.sudo().with_context(token_url=token_url).send_mail( + self.id, force_send=True, raise_exception=True) return True @api.one |
