diff options
| author | Christophe Simonis <chs@odoo.com> | 2019-01-25 16:49:07 +0000 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2019-01-25 17:29:34 +0000 |
| commit | c8ace043e50b202c88df81b9c02cc58256982d77 (patch) | |
| tree | e0c3872843d7b77d5544ca526a8bd8037713fad0 /addons | |
| parent | e8803e91e22ace82f2ff8712e5c81c8c68ad3930 (diff) | |
| parent | 387d5cff90bdea935f6cd5b2b90f1125c5ea47a1 (diff) | |
[MERGE] forward port branch saas-11.3 up to 387d5cff90b
closes odoo/odoo#30566
Diffstat (limited to 'addons')
24 files changed, 313 insertions, 60 deletions
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 4c6853b4221..09283dae105 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -419,9 +419,6 @@ class AccountMove(models.Model): journal_id=journal_id, auto=auto) reversed_moves |= reversed_move - #unreconcile all lines reversed - aml = ac_move.line_ids.filtered(lambda x: x.account_id.reconcile or x.account_id.internal_type == 'liquidity') - aml.remove_move_reconcile() #reconcile together the reconcilable (or the liquidity aml) and their newly created counterpart for account in set([x.account_id for x in aml]): to_rec = aml.filtered(lambda y: y.account_id == account) @@ -438,6 +435,18 @@ class AccountMove(models.Model): def open_reconcile_view(self): return self.line_ids.open_reconcile_view() + # FIXME: Clarify me and change me in master + @api.multi + def action_duplicate(self): + self.ensure_one() + action = self.env.ref('account.action_move_journal_line').read()[0] + action['target'] = 'inline' + action['context'] = dict(self.env.context) + action['context']['view_no_maturity'] = False + action['views'] = [(self.env.ref('account.view_move_form').id, 'form')] + action['res_id'] = self.copy().id + return action + @api.model def _run_reverses_entries(self): ''' This method is called from a cron job. ''' @@ -627,7 +636,7 @@ class AccountMoveLine(models.Model): index=True, store=True, copy=False) # related is required blocked = fields.Boolean(string='No Follow-up', default=False, help="You can check this box to mark this journal item as a litigation with the associated partner") - date_maturity = fields.Date(string='Due date', index=True, required=True, + date_maturity = fields.Date(string='Due date', index=True, required=True, copy=False, help="This field is used for payable and receivable journal entries. You can put the limit date for the payment of this line.") date = fields.Date(related='move_id.date', string='Date', index=True, store=True, copy=False, readonly=False) # related is required analytic_line_ids = fields.One2many('account.analytic.line', 'move_id', string='Analytic lines', oldname="analytic_lines") diff --git a/addons/account/models/partner.py b/addons/account/models/partner.py index f2485b379e6..85ec5f9bb66 100644 --- a/addons/account/models/partner.py +++ b/addons/account/models/partner.py @@ -204,7 +204,7 @@ class ResPartner(models.Model): @api.multi def _credit_debit_get(self): - tables, where_clause, where_params = self.env['account.move.line']._query_get() + tables, where_clause, where_params = self.env['account.move.line'].with_context(company_id=self.env.user.company_id.id)._query_get() where_params = [tuple(self.ids)] + where_params if where_clause: where_clause = 'AND ' + where_clause diff --git a/addons/account/static/src/js/account_payment_field.js b/addons/account/static/src/js/account_payment_field.js index a20a681b0e6..c4dec36dda0 100644 --- a/addons/account/static/src/js/account_payment_field.js +++ b/addons/account/static/src/js/account_payment_field.js @@ -162,4 +162,8 @@ var ShowPaymentLineWidget = AbstractField.extend({ field_registry.add('payment', ShowPaymentLineWidget); +return { + ShowPaymentLineWidget: ShowPaymentLineWidget +}; + }); diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml index 92436671b63..7e78b448a85 100644 --- a/addons/account/views/account_view.xml +++ b/addons/account/views/account_view.xml @@ -1556,10 +1556,11 @@ <field name="name">account.move.form</field> <field name="model">account.move</field> <field name="arch" type="xml"> - <form string="Account Entry"> + <form string="Account Entry" duplicate="false"> <header> <button name="action_post" states="draft" string="Post" type="object" class="oe_highlight" groups="account.group_account_invoice"/> <button name="%(action_view_account_move_reversal)d" states="posted" string="Reverse Entry" type="action" groups="account.group_account_invoice"/> + <button name="action_duplicate" string="Duplicate" type="object" groups="account.group_account_invoice"/> <field name="state" widget="statusbar"/> </header> <sheet> diff --git a/addons/bus/models/res_partner.py b/addons/bus/models/res_partner.py index b0b4c23d74f..a476e0c08dc 100644 --- a/addons/bus/models/res_partner.py +++ b/addons/bus/models/res_partner.py @@ -15,13 +15,14 @@ class ResPartner(models.Model): self.env.cr.execute(""" SELECT U.partner_id as id, - CASE WHEN age(now() AT TIME ZONE 'UTC', B.last_poll) > interval %s THEN 'offline' - WHEN age(now() AT TIME ZONE 'UTC', B.last_presence) > interval %s THEN 'away' + CASE WHEN age(now() AT TIME ZONE 'UTC', max(B.last_poll)) > interval %s THEN 'offline' + WHEN age(now() AT TIME ZONE 'UTC', max(B.last_presence)) > interval %s THEN 'away' ELSE 'online' END as status FROM bus_presence B JOIN res_users U ON B.user_id = U.id WHERE U.partner_id IN %s AND U.active = 't' + GROUP BY U.partner_id """, ("%s seconds" % DISCONNECTION_TIMER, "%s seconds" % AWAY_TIMER, tuple(self.ids))) res = dict(((status['id'], status['status']) for status in self.env.cr.dictfetchall())) for partner in self: diff --git a/addons/delivery/models/delivery_carrier.py b/addons/delivery/models/delivery_carrier.py index b5b0fcb0644..ad0d4f3cb30 100644 --- a/addons/delivery/models/delivery_carrier.py +++ b/addons/delivery/models/delivery_carrier.py @@ -70,7 +70,7 @@ class DeliveryCarrier(models.Model): def install_more_provider(self): return { 'name': 'New Providers', - 'view_mode': 'kanban', + 'view_mode': 'kanban,form', 'res_model': 'ir.module.module', 'domain': [['name', 'ilike', 'delivery_']], 'type': 'ir.actions.act_window', diff --git a/addons/fleet/models/fleet_vehicle.py b/addons/fleet/models/fleet_vehicle.py index 091595bc937..6be09a1da8a 100644 --- a/addons/fleet/models/fleet_vehicle.py +++ b/addons/fleet/models/fleet_vehicle.py @@ -59,6 +59,7 @@ class FleetVehicle(models.Model): fuel_type = fields.Selection([ ('gasoline', 'Gasoline'), ('diesel', 'Diesel'), + ('lpg', 'LPG'), ('electric', 'Electric'), ('hybrid', 'Hybrid') ], 'Fuel Type', help='Fuel Used by the vehicle') diff --git a/addons/l10n_be_hr_payroll_fleet/models/fleet.py b/addons/l10n_be_hr_payroll_fleet/models/fleet.py index c72f10117ba..2a89a67de88 100644 --- a/addons/l10n_be_hr_payroll_fleet/models/fleet.py +++ b/addons/l10n_be_hr_payroll_fleet/models/fleet.py @@ -45,15 +45,21 @@ class FleetVehicle(models.Model): elif contract.cost_frequency == "yearly": car.total_cost += contract.cost_generated / 12.0 - def _get_co2_fee(self, co2): - return max((((co2 * 9.0) - 600.0) * 1.2488) / 12.0, 0.0) - - @api.depends('co2') + def _get_co2_fee(self, co2, fuel_type): + fuel_coefficient = {'diesel': 600, 'gasoline': 768, 'lpg': 990, 'electric': 0, 'hybrid': 600} + co2_fee = 0 + if fuel_type and fuel_type != 'electric': + if not co2: + co2 = 165 if fuel_type in ['diesel', 'hybrid'] else 182 + co2_fee = (((co2 * 9.0) - fuel_coefficient.get(fuel_type, 0)) * 144.97 / 114.08) / 12.0 + return max(co2_fee, 26.47) + + @api.depends('co2', 'fuel_type') def _compute_co2_fee(self): for car in self: - car.co2_fee = self._get_co2_fee(car.co2) + car.co2_fee = self._get_co2_fee(car.co2, car.fuel_type) - @api.depends('fuel_type', 'car_value', 'acquisition_date') + @api.depends('fuel_type', 'car_value', 'acquisition_date', 'co2') def _compute_car_atn(self): for car in self: car.atn = car._get_car_atn(car.acquisition_date, car.car_value, car.fuel_type, car.co2) @@ -111,15 +117,18 @@ class FleetVehicle(models.Model): atn = 0.0 else: if fuel_type in ['diesel', 'hybrid']: - reference = 87.0 + reference = 88.0 else: - reference = 105.0 + reference = 107.0 + + if not co2: + co2 = 195 if fuel_type in ['diesel', 'hybrid'] else 205 if co2 <= reference: atn = car_value * max(0.04, (0.055 - 0.001 * (reference - co2))) * magic_coeff else: atn = car_value * min(0.18, (0.055 + 0.001 * (co2 - reference))) * magic_coeff - return max(1280, atn) / 12.0 + return max(1310, atn) / 12.0 @api.onchange('model_id') def _onchange_model_id(self): @@ -140,7 +149,7 @@ class FleetVehicleModel(models.Model): default_recurring_cost_amount_depreciated = fields.Float(string="Cost (Depreciated)", help="Default recurring cost amount that should be applied to a new car from this model") default_co2 = fields.Float(string="CO2 emissions") - default_fuel_type = fields.Selection([('gasoline', 'Gasoline'), ('diesel', 'Diesel'), ('electric', 'Electric'), ('hybrid', 'Hybrid')], 'Fuel Type', help='Fuel Used by the vehicle') + default_fuel_type = fields.Selection([('gasoline', 'Gasoline'), ('diesel', 'Diesel'), ('lpg', 'LPG'), ('electric', 'Electric'), ('hybrid', 'Hybrid')], 'Fuel Type', help='Fuel Used by the vehicle') default_car_value = fields.Float(string="Catalog Value (VAT Incl.)") can_be_requested = fields.Boolean(string="Can be requested", help="Can be requested on a contract as a new car") default_atn = fields.Float(compute='_compute_atn', string="ATN") @@ -158,7 +167,7 @@ class FleetVehicleModel(models.Model): for model in self: model.default_total_depreciated_cost = model.co2_fee + model.default_recurring_cost_amount_depreciated - @api.depends('default_co2') + @api.depends('default_co2', 'default_fuel_type') def _compute_co2_fee(self): for model in self: - model.co2_fee = self.env['fleet.vehicle']._get_co2_fee(model.default_co2) + model.co2_fee = self.env['fleet.vehicle']._get_co2_fee(model.default_co2, model.default_fuel_type) diff --git a/addons/mail/static/src/js/chatter.js b/addons/mail/static/src/js/chatter.js index d32a9327068..7f3ca1cb202 100644 --- a/addons/mail/static/src/js/chatter.js +++ b/addons/mail/static/src/js/chatter.js @@ -266,6 +266,7 @@ var Chatter = Widget.extend({ recordName: this.recordName, defaultBody: oldComposer && oldComposer.$input && oldComposer.$input.val(), defaultMentionSelections: oldComposer && oldComposer.getMentionListenerSelections(), + attachmentIds: (oldComposer && oldComposer.get('attachment_ids')) || [], }); this._composer.on('input_focused', this, function () { this._composer.mentionSetPrefetchedPartners(this._mentionSuggestions || []); diff --git a/addons/mail/static/src/js/composers/basic_composer.js b/addons/mail/static/src/js/composers/basic_composer.js index 8a533cd4515..0e25b3cdccf 100644 --- a/addons/mail/static/src/js/composers/basic_composer.js +++ b/addons/mail/static/src/js/composers/basic_composer.js @@ -56,7 +56,7 @@ var BasicComposer = Widget.extend({ // Attachments this._attachmentDataSet = new data.DataSetSearch(this, 'ir.attachment', this.context); this.fileuploadID = _.uniqueId('o_chat_fileupload'); - this.set('attachment_ids', []); + this.set('attachment_ids', options.attachmentIds || []); // Mention this._mentionManager = new MentionManager(this); @@ -118,6 +118,7 @@ var BasicComposer = Widget.extend({ }); // Attachments + this._renderAttachments(); $(window).on(this.fileuploadID, this._onAttachmentLoaded.bind(this)); this.on('change:attachment_ids', this, this._renderAttachments); diff --git a/addons/rating/models/mail_message.py b/addons/rating/models/mail_message.py index f6b3553b7f4..aade3b9ade7 100644 --- a/addons/rating/models/mail_message.py +++ b/addons/rating/models/mail_message.py @@ -8,7 +8,7 @@ class MailMessage(models.Model): _inherit = 'mail.message' rating_ids = fields.One2many('rating.rating', 'message_id', string='Related ratings') - rating_value = fields.Float("Rating Value", compute='_compute_rating_value', store=False) + rating_value = fields.Float("Rating Value", compute='_compute_rating_value', store=False, search='_search_rating_value') @api.multi @api.depends('rating_ids', 'rating_ids.rating') @@ -17,3 +17,7 @@ class MailMessage(models.Model): mapping = dict((r.message_id.id, r.rating) for r in ratings) for message in self: message.rating_value = mapping.get(message.id, 0.0) + + @api.multi + def _search_rating_value(self, operator, operand): + return [('rating_ids.rating', operator, operand)] diff --git a/addons/sale/models/analytic.py b/addons/sale/models/analytic.py index 8fb938582a0..a60dd11b023 100644 --- a/addons/sale/models/analytic.py +++ b/addons/sale/models/analytic.py @@ -19,7 +19,7 @@ class AccountAnalyticLine(models.Model): @api.model def create(self, values): result = super(AccountAnalyticLine, self).create(values) - if 'so_line' not in values and not result.so_line and result.product_id and result.product_id.expense_policy != 'no' and result.amount <= 0: # allow to force a False value for so_line + if 'so_line' not in values and not result.so_line and result.product_id and result.product_id.expense_policy not in [False, 'no'] and result.amount <= 0: # allow to force a False value for so_line result.sudo()._sale_determine_order_line() return result @@ -28,7 +28,8 @@ class AccountAnalyticLine(models.Model): result = super(AccountAnalyticLine, self).write(values) if 'so_line' not in values: # allow to force a False value for so_line # only take the AAL from expense or vendor bill, meaning having a negative amount - self.sudo().filtered(lambda aal: not aal.so_line and aal.product_id and aal.product_id.expense_policy != 'no' and aal.amount <= 0)._sale_determine_order_line() + self.sudo().filtered(lambda aal: not aal.so_line and aal.product_id and aal.product_id.expense_policy not in [False, 'no'] and aal.amount <= 0)._sale_determine_order_line() + return result # ---------------------------------------------------------- # Vendor Bill / Expense : determine the Sale Order to reinvoice diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index 7e3e917b5a4..dd4bd9cf57e 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -47,8 +47,8 @@ class SaleOrder(models.Model): amount_untaxed += line.price_subtotal amount_tax += line.price_tax order.update({ - 'amount_untaxed': order.pricelist_id.currency_id.round(amount_untaxed), - 'amount_tax': order.pricelist_id.currency_id.round(amount_tax), + 'amount_untaxed': amount_untaxed, + 'amount_tax': amount_tax, 'amount_total': amount_untaxed + amount_tax, }) @@ -66,22 +66,31 @@ class SaleOrder(models.Model): for possible refunds created directly from existing invoices. This is necessary since such a refund is not directly linked to the SO. """ + # Ignore the status of the deposit product + deposit_product_id = self.env['sale.advance.payment.inv']._default_product_id() + line_invoice_status_all = [(d['order_id'][0], d['invoice_status']) for d in self.env['sale.order.line'].read_group([('order_id', 'in', self.ids), ('product_id', '!=', deposit_product_id.id)], ['order_id', 'invoice_status'], ['order_id', 'invoice_status'], lazy=False)] for order in self: invoice_ids = order.order_line.mapped('invoice_lines').mapped('invoice_id').filtered(lambda r: r.type in ['out_invoice', 'out_refund']) # Search for invoices which have been 'cancelled' (filter_refund = 'modify' in # 'account.invoice.refund') # use like as origin may contains multiple references (e.g. 'SO01, SO02') - refunds = invoice_ids.search([('origin', 'like', order.name), ('company_id', '=', order.company_id.id)]).filtered(lambda r: r.type in ['out_invoice', 'out_refund']) + refunds = invoice_ids.search([('origin', 'like', order.name), ('company_id', '=', order.company_id.id), ('type', 'in', ('out_invoice', 'out_refund'))]) invoice_ids |= refunds.filtered(lambda r: order.name in [origin.strip() for origin in r.origin.split(',')]) + # Search for refunds as well - refund_ids = self.env['account.invoice'].browse() - if invoice_ids: - for inv in invoice_ids: - refund_ids += refund_ids.search([('type', '=', 'out_refund'), ('origin', '=', inv.number), ('origin', '!=', False), ('journal_id', '=', inv.journal_id.id)]) + domain_inv = expression.OR([ + ['&', ('origin', '=', inv.number), ('journal_id', '=', inv.journal_id.id)] + for inv in invoice_ids if inv.number + ]) + if domain_inv: + refund_ids = self.env['account.invoice'].search(expression.AND([ + ['&', ('type', '=', 'out_refund'), ('origin', '!=', False)], + domain_inv + ])) + else: + refund_ids = self.env['account.invoice'].browse() - # Ignore the status of the deposit product - deposit_product_id = self.env['sale.advance.payment.inv']._default_product_id() - line_invoice_status = [line.invoice_status for line in order.order_line if line.product_id != deposit_product_id] + line_invoice_status = [d[1] for d in line_invoice_status_all if d[0] == order.id] if order.state not in ('sale', 'done'): invoice_status = 'no' @@ -662,7 +671,7 @@ class SaleOrder(models.Model): # create an analytic account if at least an expense product for order in self: - if any([expense_policy != 'no' for expense_policy in order.order_line.mapped('product_id.expense_policy')]): + if any([expense_policy not in [False, 'no'] for expense_policy in order.order_line.mapped('product_id.expense_policy')]): if not order.analytic_account_id: order._create_analytic_account() @@ -1026,7 +1035,7 @@ class SaleOrderLine(models.Model): msg = _("Extra line with %s ") % (line.product_id.display_name,) line.order_id.message_post(body=msg) # create an analytic account if at least an expense product - if line.product_id.expense_policy != 'no' and not self.order_id.analytic_account_id: + if line.product_id.expense_policy not in [False, 'no'] and not self.order_id.analytic_account_id: self.order_id._create_analytic_account() return line diff --git a/addons/sale/views/sale_views.xml b/addons/sale/views/sale_views.xml index 7e101fe63b1..72ede8b2e55 100644 --- a/addons/sale/views/sale_views.xml +++ b/addons/sale/views/sale_views.xml @@ -314,6 +314,7 @@ <field name="qty_delivered_manual" invisible="1"/> <field name="qty_delivered_method" invisible="1"/> <field name="price_total" invisible="1"/> + <field name="price_tax" invisible="1"/> <field name="price_subtotal" invisible="1"/> <label for="product_uom_qty" string="Ordered Quantity"/> <div> @@ -322,6 +323,7 @@ name="product_uom_qty" class="oe_inline"/> <field name="product_uom" + force_save="1" groups="uom.group_uom" class="oe_inline oe_no_button" attrs="{ @@ -442,6 +444,7 @@ <field name="qty_to_invoice" invisible="1"/> <field name="product_uom" + force_save="1" attrs="{ 'readonly': [('state', 'in', ('sale','done', 'cancel'))], 'required': [('display_type', '=', False)], @@ -473,6 +476,7 @@ <field name="invoice_status" invisible="1"/> <field name="customer_lead" invisible="1"/> <field name="currency_id" invisible="1"/> + <field name="price_tax" invisible="1"/> </tree> <kanban class="o_kanban_mobile"> <field name="name"/> @@ -480,6 +484,7 @@ <field name="product_uom_qty"/> <field name="product_uom" groups="uom.group_uom"/> <field name="price_subtotal"/> + <field name="price_tax" invisible="1"/> <field name="price_total" invisible="1"/> <field name="price_unit"/> <field name="display_type"/> diff --git a/addons/sale_expense/models/analytic.py b/addons/sale_expense/models/analytic.py index 9f037bfcae1..d387091adc1 100644 --- a/addons/sale_expense/models/analytic.py +++ b/addons/sale_expense/models/analytic.py @@ -10,7 +10,7 @@ class AccountAnalyticLine(models.Model): @api.multi def _sale_determine_order(self): mapping = super(AccountAnalyticLine, self)._sale_determine_order() - for analytic_line in self.sudo().filtered(lambda aal: not aal.so_line and aal.product_id and aal.product_id.expense_policy != 'no'): + for analytic_line in self.sudo().filtered(lambda aal: not aal.so_line and aal.product_id and aal.product_id.expense_policy not in [False, 'no']): if analytic_line.move_id.expense_id.sale_order_id: mapping[analytic_line.id] = analytic_line.move_id.expense_id.sale_order_id return mapping diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py index 50444266362..78666bc2669 100644 --- a/addons/stock_account/models/stock.py +++ b/addons/stock_account/models/stock.py @@ -387,7 +387,7 @@ class StockMove(models.Model): product_tot_qty_available = move.product_id.qty_available + tmpl_dict[move.product_id.id] rounding = move.product_id.uom_id.rounding - qty_done = 0.0 + qty_done = move.product_uom._compute_quantity(move.quantity_done, move.product_id.uom_id) if float_is_zero(product_tot_qty_available, precision_rounding=rounding): new_std_price = move._get_price_unit() elif float_is_zero(product_tot_qty_available + move.product_qty, precision_rounding=rounding) or \ @@ -396,7 +396,6 @@ class StockMove(models.Model): else: # Get the standard price amount_unit = std_price_update.get((move.company_id.id, move.product_id.id)) or move.product_id.standard_price - qty_done = move.product_uom._compute_quantity(move.quantity_done, move.product_id.uom_id) qty = forced_qty or qty_done new_std_price = ((amount_unit * product_tot_qty_available) + (move._get_price_unit() * qty)) / (product_tot_qty_available + qty_done) diff --git a/addons/stock_account/tests/test_stockvaluation.py b/addons/stock_account/tests/test_stockvaluation.py index 39ced676a1a..4afd3706d5e 100644 --- a/addons/stock_account/tests/test_stockvaluation.py +++ b/addons/stock_account/tests/test_stockvaluation.py @@ -2587,6 +2587,40 @@ class TestStockValuation(TransactionCase): self.assertAlmostEqual(self.product1.qty_at_date, 0.0) self.assertAlmostEqual(self.product1.stock_value, 0.0) + def test_average_perpetual_6(self): + self.product1.product_tmpl_id.cost_method = 'average' + + move1 = self.env['stock.move'].create({ + 'name': 'Receive 1 unit at 10', + 'location_id': self.supplier_location.id, + 'location_dest_id': self.stock_location.id, + 'product_id': self.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 1.0, + 'price_unit': 10, + }) + move1._action_confirm() + move1._action_assign() + move1.move_line_ids.qty_done = 1.0 + + move2 = self.env['stock.move'].create({ + 'name': 'Receive 1 units at 5', + 'location_id': self.supplier_location.id, + 'location_dest_id': self.stock_location.id, + 'product_id': self.product1.id, + 'product_uom': self.uom_unit.id, + 'product_uom_qty': 1.0, + 'price_unit': 5, + }) + move2._action_confirm() + move2._action_assign() + move2.move_line_ids.qty_done = 1.0 + + # Receive both at the same time + (move1 | move2)._action_done() + + self.assertAlmostEqual(self.product1.standard_price, 7.5) + def test_average_negative_1(self): """ Test edit in the past. Receive 10, send 20, edit the second move to only send 10. """ diff --git a/addons/stock_landed_costs/models/stock_landed_cost.py b/addons/stock_landed_costs/models/stock_landed_cost.py index 07aa7be9a51..ca1b544f868 100644 --- a/addons/stock_landed_costs/models/stock_landed_cost.py +++ b/addons/stock_landed_costs/models/stock_landed_cost.py @@ -307,6 +307,9 @@ class AdjustmentLines(models.Model): return False accounts = self.product_id.product_tmpl_id.get_product_accounts() debit_account_id = accounts.get('stock_valuation') and accounts['stock_valuation'].id or False + # If the stock move is dropshipped move we need to get the cost account instead the stock valuation account + if self.move_id._is_dropshipped(): + debit_account_id = accounts.get('expense') and accounts['expense'].id or False already_out_account_id = accounts['stock_output'].id credit_account_id = self.cost_line_id.account_id.id or cost_product.property_account_expense_id.id or cost_product.categ_id.property_account_expense_categ_id.id diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index 60b0275c8c8..ff9d6359dd1 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -240,6 +240,12 @@ var FieldMany2One = AbstractField.extend({ */ _bindAutoComplete: function () { var self = this; + // avoid ignoring autocomplete="off" by obfuscating placeholder, see #30439 + if (this.$input.attr('placeholder')) { + this.$input.attr('placeholder', function (index, val) { + return val.split('').join('\ufeff'); + }); + } this.$input.autocomplete({ source: function (req, resp) { _.each(self._autocompleteSources, function (source) { diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js index b7c8e8a6214..45b68ce6bea 100644 --- a/addons/web/static/src/js/views/form/form_controller.js +++ b/addons/web/static/src/js/views/form/form_controller.js @@ -228,17 +228,16 @@ var FormController = BasicController.extend({ // are displayed with an alert var fields = self.renderer.state.fields; var data = self.renderer.state.data; - var alertFields = []; + var alertFields = {}; for (var k = 0; k < changedFields.length; k++) { var field = fields[changedFields[k]]; var fieldData = data[changedFields[k]]; if (field.translate && fieldData) { - alertFields.push(field); + alertFields[changedFields[k]] = field; } } - if (alertFields.length) { - self.renderer.alertFields = alertFields; - self.renderer.displayTranslationAlert(); + if (!_.isEmpty(alertFields)) { + self.renderer.updateAlertFields(alertFields); } } return changedFields; diff --git a/addons/web/static/src/js/views/form/form_renderer.js b/addons/web/static/src/js/views/form/form_renderer.js index c7a32b72ba9..91146ccd504 100644 --- a/addons/web/static/src/js/views/form/form_renderer.js +++ b/addons/web/static/src/js/views/form/form_renderer.js @@ -13,6 +13,7 @@ var FormRenderer = BasicRenderer.extend({ className: "o_form_view", events: _.extend({}, BasicRenderer.prototype.events, { 'click .o_notification_box .oe_field_translate': '_onTranslate', + 'click .o_notification_box .close': '_onTranslateNotificationClose', 'click .oe_title, .o_inner_group': '_onClick', }), custom_events: _.extend({}, BasicRenderer.prototype.custom_events, { @@ -30,6 +31,7 @@ var FormRenderer = BasicRenderer.extend({ this._super.apply(this, arguments); this.idsForLabels = {}; this.lastActivatedFieldIndex = -1; + this.alertFields = {}; }, /** * @override @@ -103,23 +105,32 @@ var FormRenderer = BasicRenderer.extend({ }); return fieldNames; }, + /* + * Updates translation alert fields for the current state and display updated fields + * + * @param {Object} alertFields + */ + updateAlertFields: function (alertFields) { + this.alertFields[this.state.res_id] = _.extend(this.alertFields[this.state.res_id] || {}, alertFields); + this.displayTranslationAlert(); + }, /** * Show a warning message if the user modified a translated field. For each * field, the notification provides a link to edit the field's translations. - * - * @param {Object[]} alertFields field list */ displayTranslationAlert: function () { this.$('.o_notification_box').remove(); - var $notification = $(qweb.render('notification-box', {type: 'info'})) - .append(qweb.render('translation-alert', { - fields: this.alertFields, - lang: _t.database.parameters.name - })); - if (this.$('.o_form_statusbar').length) { - this.$('.o_form_statusbar').after($notification); - } else { - this.$el.prepend($notification); + if (this.alertFields[this.state.res_id]) { + var $notification = $(qweb.render('notification-box', {type: 'info'})) + .append(qweb.render('translation-alert', { + fields: this.alertFields[this.state.res_id], + lang: _t.database.parameters.name + })); + if (this.$('.o_form_statusbar').length) { + this.$('.o_form_statusbar').after($notification); + } else { + this.$el.prepend($notification); + } } }, /** @@ -905,6 +916,9 @@ var FormRenderer = BasicRenderer.extend({ return $.when.apply($, defs).then(function () { self._updateView($form.contents()); + if (self.state.res_id in self.alertFields) { + self.displayTranslationAlert(); + } }, function () { $form.remove(); }).then(function(){ @@ -1015,6 +1029,15 @@ var FormRenderer = BasicRenderer.extend({ event.preventDefault(); this.trigger_up('translate', {fieldName: event.target.name, id: this.state.id}); }, + /** + * remove alert fields of record from alertFields object + * + * @private + * @param {MouseEvent} ev + */ + _onTranslateNotificationClose: function(ev) { + delete this.alertFields[this.state.res_id]; + }, }); return FormRenderer; diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 30f160460cd..5a416938362 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -1525,7 +1525,7 @@ <div role="alertdialog"> Please update translations of : <t t-foreach="fields" t-as="field"> - <strong><a class="oe_field_translate" t-att-name="field.name" href="#"><t t-esc="field.string"/><t t-if="!field_last">, </t></a></strong> + <strong><a class="oe_field_translate" t-att-name="field" href="#"><t t-esc="field_value.string"/><t t-if="field_index < _.size(fields)-1">, </t></a></strong> </t> </div> </t> diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index e5b5f0cf36f..47db6dbe2f4 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -16,6 +16,7 @@ var Widget = require('web.Widget'); var _t = core._t; var createView = testUtils.createView; +var createActionManager = testUtils.createActionManager; var createAsyncView = testUtils.createAsyncView; var createActionManager = testUtils.createActionManager; @@ -5647,9 +5648,10 @@ QUnit.module('Views', { }); QUnit.test('display translation alert', function (assert) { - assert.expect(1); + assert.expect(2); this.data.partner.fields.foo.translate = true; + this.data.partner.fields.display_name.translate = true; var multi_lang = _t.database.multi_lang; _t.database.multi_lang = true; @@ -5662,6 +5664,7 @@ QUnit.module('Views', { '<sheet>' + '<group>' + '<field name="foo"/>' + + '<field name="display_name"/>' + '</group>' + '</sheet>' + '</form>', @@ -5671,14 +5674,154 @@ QUnit.module('Views', { form.$buttons.find('.o_form_button_edit').click(); form.$('input[name="foo"]').val("test").trigger("input"); form.$buttons.find('.o_form_button_save').click(); + assert.strictEqual(form.$('.o_form_view > .alert > div .oe_field_translate').length, 1, + "should have single translation alert"); - assert.strictEqual(form.$('.o_form_view > .alert > div').length, 1,"should have a translation alert"); + form.$buttons.find('.o_form_button_edit').click(); + form.$('input[name="display_name"]').val("test2").trigger("input"); + form.$buttons.find('.o_form_button_save').click(); + assert.strictEqual(form.$('.o_form_view > .alert > div .oe_field_translate').length, 2, + "should have two translate fields in translation alert"); form.destroy(); _t.database.multi_lang = multi_lang; }); + QUnit.test('translation alerts are preserved on pager change', function (assert) { + assert.expect(5); + + this.data.partner.fields.foo.translate = true; + + var multi_lang = _t.database.multi_lang; + _t.database.multi_lang = true; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<field name="foo"/>' + + '</sheet>' + + '</form>', + viewOptions: { + ids: [1, 2], + index: 0, + }, + res_id: 1, + }); + + form.$buttons.find('.o_form_button_edit').click(); + form.$('input[name="foo"]').val("test").trigger("input"); + form.$buttons.find('.o_form_button_save').click(); + + assert.strictEqual(form.$('.o_form_view > .alert > div').length, 1, + "should have a translation alert"); + + // click on the pager to switch to the next record + form.pager.$('.o_pager_next').click(); + assert.strictEqual(form.$('.o_form_view > .alert > div').length, 0, + "should not have a translation alert"); + + // click on the pager to switch back to the previous record + form.pager.$('.o_pager_previous').click(); + assert.strictEqual(form.$('.o_form_view > .alert > div').length, 1, + "should have a translation alert"); + + // remove translation alert by click X and check alert even after form reload + form.$('.o_form_view > .alert > .close').click(); + assert.strictEqual(form.$('.o_form_view > .alert > div').length, 0, + "should not have a translation alert"); + form.reload(); + assert.strictEqual(form.$('.o_form_view > .alert > div').length, 0, + "should not have a translation alert after reload"); + + form.destroy(); + _t.database.multi_lang = multi_lang; + }); + + QUnit.test('translation alerts preseved on reverse breadcrumb', function (assert) { + assert.expect(2); + + this.data['ir.translation'] = { + fields: { + name: { string: "name", type: "char" }, + source: {string: "Source", type: "char"}, + value: {string: "Value", type: "char"}, + }, + records: [], + }; + + this.data.partner.fields.foo.translate = true; + + var multi_lang = _t.database.multi_lang; + _t.database.multi_lang = true; + + var archs = { + 'partner,false,form': '<form string="Partners">' + + '<sheet>' + + '<field name="foo"/>' + + '</sheet>' + + '</form>', + 'partner,false,search': '<search></search>', + 'ir.translation,false,list': '<tree>' + + '<field name="name"/>' + + '<field name="source"/>' + + '<field name="value"/>' + + '</tree>', + 'ir.translation,false,search': '<search></search>', + }; + + var actions = [{ + id: 1, + name: 'Partner', + res_model: 'partner', + type: 'ir.actions.act_window', + views: [[false, 'form']], + }, { + id: 2, + name: 'Translate', + res_model: 'ir.translation', + type: 'ir.actions.act_window', + views: [[false, 'list']], + target: 'current', + flags: {'search_view': true, 'action_buttons': true}, + }]; + + var actionManager = createActionManager({ + actions: actions, + archs: archs, + data: this.data, + }); + + actionManager.doAction(1); + + actionManager.controlPanel.$el.find('.o_form_button_edit').click(); + actionManager.$('input[name="foo"]').val("test").trigger("input"); + actionManager.controlPanel.$el.find('.o_form_button_save').click(); + + assert.strictEqual(actionManager.$('.o_form_view > .alert > div').length, 1, + "should have a translation alert"); + + var currentController = actionManager.getCurrentController().widget; + actionManager.doAction(2, { + on_reverse_breadcrumb: function () { + if (!_.isEmpty(currentController.renderer.alertFields)) { + currentController.renderer.displayTranslationAlert(); + } + return false; + }, + }); + + $('.o_control_panel .breadcrumb a:first').click(); + assert.strictEqual(actionManager.$('.o_form_view > .alert > div').length, 1, + "should have a translation alert"); + + actionManager.destroy(); + _t.database.multi_lang = multi_lang; + }); + QUnit.test('translate event correctly handled with multiple controllers', function (assert) { assert.expect(3); diff --git a/addons/web_editor/controllers/main.py b/addons/web_editor/controllers/main.py index 27e4419afe9..7bb6f77a649 100644 --- a/addons/web_editor/controllers/main.py +++ b/addons/web_editor/controllers/main.py @@ -424,7 +424,7 @@ class Web_Editor(http.Controller): (note: searching for the URL string is not enough as it could appear in a comment or an xpath expression.) """ - return bool(etree.XML(view.arch).xpath("link[@href='{}']".format(url))) + return bool(etree.XML(view.arch).xpath("//link[@href='{}']".format(url))) view_to_xpath = IrUiView.get_related_views(bundle_xmlid, bundles=True).filtered(views_linking_url) |
