diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 94608c8872..bf6cbe97fc 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -198,6 +198,7 @@ class AccountMove(models.Model): help="It indicates that the invoice/payment has been sent.", ) partner_bank_id = fields.Many2one('res.partner.bank', string='Recipient Bank', + compute="_compute_partner_bank_id", store=True, readonly=False, help='Bank Account Number to which the invoice will be paid. A Company bank account if this is a Customer Invoice or Vendor Credit Note, otherwise a Partner bank account number.', check_company=True) payment_reference = fields.Char(string='Payment Reference', index=True, copy=False, @@ -473,8 +474,6 @@ class AccountMove(models.Model): line.account_id = new_term_account self._compute_bank_partner_id() - bank_ids = self.bank_partner_id.bank_ids.filtered(lambda bank: bank.company_id is False or bank.company_id == self.company_id) - self.partner_bank_id = bank_ids and bank_ids[0] # Find the new fiscal position. delivery_partner_id = self._get_invoice_delivery_partner_id() @@ -1347,7 +1346,13 @@ class AccountMove(models.Model): for move in self: move.commercial_partner_id = move.partner_id.commercial_partner_id - @api.depends('commercial_partner_id') + @api.depends('bank_partner_id') + def _compute_partner_bank_id(self): + for move in self: + bank_ids = move.bank_partner_id.bank_ids.filtered(lambda bank: bank.company_id is False or bank.company_id == move.company_id) + move.partner_bank_id = bank_ids and bank_ids[0] + + @api.depends('commercial_partner_id', 'move_type') def _compute_bank_partner_id(self): for move in self: if move.is_outbound(): @@ -2386,7 +2391,9 @@ class AccountMove(models.Model): return tags move_vals = self.with_context(include_business_fields=True).copy_data(default=default_values)[0] - + # partner_bank_id field will be set by _compute_partner_bank_id + if 'partner_bank_id' in move_vals: + del move_vals['partner_bank_id'] is_refund = False if move_vals['move_type'] in ('out_refund', 'in_refund'): is_refund = True @@ -2659,6 +2666,14 @@ class AccountMove(models.Model): move.date = move._get_accounting_date(move.invoice_date or move.date, True) move.with_context(check_move_validity=False)._onchange_currency() + + for move in to_post: + # Fix inconsistencies that may occure if the OCR has been editing the invoice at the same time of a user. We force the + # partner on the lines to be the same as the one on the move, because that's the only one the user can see/edit. + wrong_lines = move.is_invoice() and move.line_ids.filtered(lambda aml: aml.partner_id != move.commercial_partner_id and not aml.display_type) + if wrong_lines: + wrong_lines.partner_id = move.commercial_partner_id.id + # Create the analytic lines in batch is faster as it leads to less cache invalidation. to_post.mapped('line_ids').create_analytic_lines() to_post.write({ @@ -2931,7 +2946,6 @@ class AccountMove(models.Model): move.write({ 'move_type': move.move_type.replace('invoice', 'refund'), 'invoice_line_ids' : [(5, 0, 0)], - 'partner_bank_id': False, }) move.write({'invoice_line_ids' : new_invoice_line_ids}) diff --git a/addons/board/controllers/main.py b/addons/board/controllers/main.py index 09129d4c79..b63e7e7b9d 100644 --- a/addons/board/controllers/main.py +++ b/addons/board/controllers/main.py @@ -21,6 +21,10 @@ class Board(Controller): xml = ElementTree.fromstring(board['arch']) column = xml.find('./board/column') if column is not None: + # We don't want to save allowed_company_ids + # Otherwise on dashboard, the multi-company widget does not filter the records + if 'allowed_company_ids' in context_to_save: + context_to_save.pop('allowed_company_ids') new_action = ElementTree.Element('action', { 'name': str(action_id), 'string': name, diff --git a/addons/hr_skills_slides/models/slide_channel.py b/addons/hr_skills_slides/models/slide_channel.py index c115c627f6..049b3086a8 100644 --- a/addons/hr_skills_slides/models/slide_channel.py +++ b/addons/hr_skills_slides/models/slide_channel.py @@ -9,19 +9,36 @@ class SlideChannelPartner(models.Model): def _recompute_completion(self): res = super(SlideChannelPartner, self)._recompute_completion() - partner_has_completed = {channel_partner.partner_id.id: channel_partner.channel_id for channel_partner in self} - employees = self.env['hr.employee'].sudo().search([('user_id.partner_id', 'in', list(partner_has_completed.keys()))]) - for employee in employees: + partner_has_completed = { + channel_partner.partner_id.id: channel_partner.channel_id for channel_partner in self + if channel_partner.completed} + employees = self.env['hr.employee'].sudo().search( + [('user_id.partner_id', 'in', list(partner_has_completed.keys()))]) + + if employees: + HrResumeLine = self.env['hr.resume.line'].sudo() line_type = self.env.ref('hr_skills_slides.resume_type_training', raise_if_not_found=False) - channel = partner_has_completed[employee.user_id.partner_id.id] - self.env['hr.resume.line'].create({ - 'employee_id': employee.id, - 'name': channel.name, - 'date_start': fields.Date.today(), - 'date_end': fields.Date.today(), - 'description': channel.description, - 'line_type_id': line_type and line_type.id, - 'display_type': 'course', - 'channel_id': channel.id - }) + line_type_id = line_type and line_type.id + + for employee in employees: + channel = partner_has_completed[employee.user_id.partner_id.id] + + already_added = HrResumeLine.search([ + ("employee_id", "in", employees.ids), + ("channel_id", "=", channel.id), + ("line_type_id", "=", line_type_id), + ("display_type", "=", "course") + ]) + + if not already_added: + HrResumeLine.create({ + 'employee_id': employee.id, + 'name': channel.name, + 'date_start': fields.Date.today(), + 'date_end': fields.Date.today(), + 'description': channel.description, + 'line_type_id': line_type_id, + 'display_type': 'course', + 'channel_id': channel.id + }) return res diff --git a/addons/hr_timesheet/security/hr_timesheet_security.xml b/addons/hr_timesheet/security/hr_timesheet_security.xml index 19b93859da..94a1d6f8a7 100644 --- a/addons/hr_timesheet/security/hr_timesheet_security.xml +++ b/addons/hr_timesheet/security/hr_timesheet_security.xml @@ -60,7 +60,7 @@ account.analytic.line.timesheet.manager [('project_id', '!=', False)] - + diff --git a/addons/l10n_au/data/account_tax_template_data.xml b/addons/l10n_au/data/account_tax_template_data.xml index 247516ad8e..064d90c31c 100644 --- a/addons/l10n_au/data/account_tax_template_data.xml +++ b/addons/l10n_au/data/account_tax_template_data.xml @@ -58,7 +58,7 @@ 10.0 - - 10.0 - - 0 - - 0 - - 0 - - 10.0 - - 10.0 - - 10.0 - - 10.0 - - 10.0 - - 10.0 - - 10.0 - - 10.0 - - 10.0 - - 10.0 - - ,;]+)')) @@ -492,21 +492,21 @@ class MassMailing(models.Model): """ elif issubclass(type(target), self.pool['mail.thread.blacklist']): mail_field = 'email_normalized' - elif 'email_from' in target._fields: + elif 'email_from' in target._fields and target._fields['email_from'].store: mail_field = 'email_from' - elif 'partner_email' in target._fields: + elif 'partner_email' in target._fields and target._fields['partner_email'].store: mail_field = 'partner_email' - elif 'email' in target._fields: + elif 'email' in target._fields and target._fields['email'].store: mail_field = 'email' else: raise UserError(_("Unsupported mass mailing model %s", self.mailing_model_id.name)) if self.unique_ab_testing: - query +=""" + query += """ AND s.campaign_id = %%(mailing_campaign_id)s; """ else: - query +=""" + query += """ AND s.mass_mailing_id = %%(mailing_id)s AND s.model = %%(target_model)s; """ diff --git a/addons/mass_mailing/tests/common.py b/addons/mass_mailing/tests/common.py index 27b89b2d61..bc83d33413 100644 --- a/addons/mass_mailing/tests/common.py +++ b/addons/mass_mailing/tests/common.py @@ -189,26 +189,37 @@ class MassMailCase(MailCase, MockLinkTracker): raise AssertionError('url %s not found in mailing %s for record %s' % (click_label, mailing, record)) @classmethod - def _create_bounce_trace(cls, mailing, record, dt=None): - if 'email_normalized' in record: - trace_email = record.email_normalized - elif 'email_from' in record: - trace_email = record.email_from - else: - trace_email = record.email + def _create_bounce_trace(cls, mailing, records, dt=None): if dt is None: dt = datetime.datetime.now() - datetime.timedelta(days=1) + return cls._create_traces(mailing, records, bounced=dt) + + @classmethod + def _create_sent_traces(cls, mailing, records, dt=None): + if dt is None: + dt = datetime.datetime.now() - datetime.timedelta(days=1) + return cls._create_traces(mailing, records, sent=dt) + + @classmethod + def _create_traces(cls, mailing, records, **values): + if 'email_normalized' in records: + fname = 'email_normalized' + elif 'email_from' in records: + fname = 'email_from' + else: + fname = 'email' randomized = random.random() - trace = cls.env['mailing.trace'].create({ - 'mass_mailing_id': mailing.id, - 'model': record._name, - 'res_id': record.id, - 'bounced': dt, - # TDE FIXME: improve this with a mail-enabled heuristics - 'email': trace_email, - 'message_id': '<%5f@gilbert.boitempomils>' % randomized, - }) - return trace + traces = cls.env['mailing.trace'].create([ + dict({'mass_mailing_id': mailing.id, + 'model': record._name, + 'res_id': record.id, + # TDE FIXME: improve this with a mail-enabled heuristics + 'email': record[fname], + 'message_id': '<%5f@gilbert.boitempomils>' % randomized, + }, **values) + for record in records + ]) + return traces class MassMailCommon(MailCommon, MassMailCase): diff --git a/addons/mass_mailing_sms/models/mailing_mailing.py b/addons/mass_mailing_sms/models/mailing_mailing.py index 4f6c38e476..32952e25b1 100644 --- a/addons/mass_mailing_sms/models/mailing_mailing.py +++ b/addons/mass_mailing_sms/models/mailing_mailing.py @@ -184,16 +184,19 @@ class Mailing(models.Model): if issubclass(type(target), self.pool['mail.thread.phone']): phone_fields = ['phone_sanitized'] elif issubclass(type(target), self.pool['mail.thread']): - phone_fields = target._sms_get_number_fields() + phone_fields = [ + fname for fname in target._sms_get_number_fields() + if fname in target._fields and target._fields[fname].store + ] partner_fields = target._sms_get_partner_fields() else: phone_fields = [] - if 'mobile' in target._fields: + if 'mobile' in target._fields and target._fields['mobile'].store: phone_fields.append('mobile') - if 'phone' in target._fields: + if 'phone' in target._fields and target._fields['phone'].store: phone_fields.append('phone') partner_field = next( - (fname for fname in partner_fields if target._fields[fname].type == 'many2one'), + (fname for fname in partner_fields if target._fields[fname].store and target._fields[fname].type == 'many2one'), False ) if not phone_fields and not partner_field: diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index 74b7d3af94..d96013909e 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -282,15 +282,17 @@ class MrpProduction(models.Model): product_domain += [('id', 'in', production.bom_id.product_tmpl_id.product_variant_ids.ids)] production.allowed_product_ids = self.env['product.product'].search(product_domain) - @api.depends('procurement_group_id.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids') + @api.depends('procurement_group_id.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids', + 'procurement_group_id.stock_move_ids.move_orig_ids.created_production_id.procurement_group_id.mrp_production_ids') def _compute_mrp_production_child_count(self): for production in self: - production.mrp_production_child_count = len(production.procurement_group_id.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids - production) + production.mrp_production_child_count = len(production._get_children()) - @api.depends('move_dest_ids.group_id.mrp_production_ids') + @api.depends('procurement_group_id.mrp_production_ids.move_dest_ids.group_id.mrp_production_ids', + 'procurement_group_id.stock_move_ids.move_dest_ids.group_id.mrp_production_ids') def _compute_mrp_production_source_count(self): for production in self: - production.mrp_production_source_count = len(production.procurement_group_id.mrp_production_ids.move_dest_ids.group_id.mrp_production_ids - production) + production.mrp_production_source_count = len(production._get_sources()) @api.depends('procurement_group_id.mrp_production_ids') def _compute_mrp_production_backorder(self): @@ -1051,9 +1053,21 @@ class MrpProduction(models.Model): self.workorder_ids.filtered(lambda w: w.state not in ['done', 'cancel'])._action_confirm() + def _get_children(self): + self.ensure_one() + procurement_moves = self.procurement_group_id.stock_move_ids + child_moves = procurement_moves.move_orig_ids + return (procurement_moves | child_moves).created_production_id.procurement_group_id.mrp_production_ids - self + + def _get_sources(self): + self.ensure_one() + dest_moves = self.procurement_group_id.mrp_production_ids.move_dest_ids + parent_moves = self.procurement_group_id.stock_move_ids.move_dest_ids + return (dest_moves | parent_moves).group_id.mrp_production_ids - self + def action_view_mrp_production_childs(self): self.ensure_one() - mrp_production_ids = self.procurement_group_id.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids.ids + mrp_production_ids = self._get_children() action = { 'res_model': 'mrp.production', 'type': 'ir.actions.act_window', @@ -1073,7 +1087,7 @@ class MrpProduction(models.Model): def action_view_mrp_production_sources(self): self.ensure_one() - mrp_production_ids = self.procurement_group_id.mrp_production_ids.move_dest_ids.group_id.mrp_production_ids.ids + mrp_production_ids = self._get_sources() action = { 'res_model': 'mrp.production', 'type': 'ir.actions.act_window', diff --git a/addons/mrp/models/product.py b/addons/mrp/models/product.py index 13224336e8..e2930a40d2 100644 --- a/addons/mrp/models/product.py +++ b/addons/mrp/models/product.py @@ -136,18 +136,33 @@ class ProductProduct(models.Model): """ bom_kits = self.env['mrp.bom']._get_product2bom(self, bom_type='phantom') kits = self.filtered(lambda p: bom_kits.get(p)) - res = super(ProductProduct, self - kits)._compute_quantities_dict(lot_id, owner_id, package_id, from_date=from_date, to_date=to_date) + regular_products = self - kits + res = ( + super(ProductProduct, regular_products)._compute_quantities_dict(lot_id, owner_id, package_id, from_date=from_date, to_date=to_date) + if regular_products + else {} + ) qties = self.env.context.get("mrp_compute_quantities", {}) qties.update(res) + # pre-compute bom lines and identify missing kit components to prefetch + bom_sub_lines_per_kit = {} + prefetch_component_ids = set() for product in bom_kits: - boms, bom_sub_lines = bom_kits[product].explode(product, 1) + __, bom_sub_lines = bom_kits[product].explode(product, 1) + bom_sub_lines_per_kit[product] = bom_sub_lines + for bom_line, __ in bom_sub_lines: + if bom_line.product_id.id not in qties: + prefetch_component_ids.add(bom_line.product_id.id) + # compute kit quantities + for product in bom_kits: + bom_sub_lines = bom_sub_lines_per_kit[product] ratios_virtual_available = [] ratios_qty_available = [] ratios_incoming_qty = [] ratios_outgoing_qty = [] ratios_free_qty = [] for bom_line, bom_line_data in bom_sub_lines: - component = bom_line.product_id.with_context(mrp_compute_quantities=qties) + component = bom_line.product_id.with_context(mrp_compute_quantities=qties).with_prefetch(prefetch_component_ids) if component.type != 'product' or float_is_zero(bom_line_data['qty'], precision_rounding=bom_line.product_uom_id.rounding): # As BoMs allow components with 0 qty, a.k.a. optionnal components, we simply skip those # to avoid a division by zero. The same logic is applied to non-storable products as those diff --git a/addons/mrp/tests/test_warehouse_multistep_manufacturing.py b/addons/mrp/tests/test_warehouse_multistep_manufacturing.py index 4ebd325952..403a57cacb 100644 --- a/addons/mrp/tests/test_warehouse_multistep_manufacturing.py +++ b/addons/mrp/tests/test_warehouse_multistep_manufacturing.py @@ -421,6 +421,16 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon): self.assertEqual(len(picking.move_lines), 1) picking.product_id = self.complex_product + # MOs are correctly linked to each other + self.assertEqual(production.mrp_production_child_count, 1) + self.assertEqual(production.mrp_production_source_count, 0) + self.assertEqual(subproduction.mrp_production_child_count, 0) + self.assertEqual(subproduction.mrp_production_source_count, 1) + child_action = production.action_view_mrp_production_childs() + source_action = subproduction.action_view_mrp_production_sources() + self.assertEqual(child_action.get('res_id'), subproduction) + self.assertEqual(source_action.get('res_id'), production) + def test_3_steps_and_byproduct(self): """ Suppose a warehouse with Manufacture option set to '3 setps' and a product P01 with a reordering rule. Suppose P01 has a BoM and this BoM mentions that when some P01 are produced, some P02 are produced too. diff --git a/addons/mrp_subcontracting_dropshipping/tests/test_sale_dropshipping.py b/addons/mrp_subcontracting_dropshipping/tests/test_sale_dropshipping.py index fd3ddb96ae..f7ad339e7a 100644 --- a/addons/mrp_subcontracting_dropshipping/tests/test_sale_dropshipping.py +++ b/addons/mrp_subcontracting_dropshipping/tests/test_sale_dropshipping.py @@ -250,3 +250,44 @@ class TestSaleDropshippingFlows(TestMrpSubcontractingCommon): sale_order.picking_ids.action_cancel() self.assertEqual(sale_order.order_line.qty_delivered, 0.0) + + def test_sale_kit_with_dropshipped_component(self): + """ + The test checks the delivered quantity of a kit when one of the + components is dropshipped + """ + compo01, compo02, kit = self.env['product.product'].create([{ + 'name': n, + 'type': 'consu', + } for n in ['compo01', 'compo02', 'super kit']]) + + compo02.write({ + 'route_ids': [(6, 0, [self.dropship_route.id])], + 'seller_ids': [(0, 0, {'name': self.supplier.id})], + }) + + self.env['mrp.bom'].create({ + 'product_tmpl_id': kit.product_tmpl_id.id, + 'product_qty': 1, + 'type': 'phantom', + 'bom_line_ids': [ + (0, 0, {'product_id': compo01.id, 'product_qty': 1}), + (0, 0, {'product_id': compo02.id, 'product_qty': 1}), + ], + }) + + sale_order = self.env['sale.order'].create({ + 'partner_id': self.customer.id, + 'picking_policy': 'direct', + 'order_line': [ + (0, 0, {'name': kit.name, 'product_id': kit.id, 'product_uom_qty': 1}), + ], + }) + sale_order.action_confirm() + self.env['purchase.order'].search([], order='id desc', limit=1).button_confirm() + + sale_order.picking_ids.move_lines.quantity_done = 1 + sale_order.picking_ids[0].button_validate() + sale_order.picking_ids[1].button_validate() + + self.assertEqual(sale_order.order_line.qty_delivered, 1.0) diff --git a/addons/purchase_stock/tests/test_reordering_rule.py b/addons/purchase_stock/tests/test_reordering_rule.py index 6422f72bb8..cb2a8f4993 100644 --- a/addons/purchase_stock/tests/test_reordering_rule.py +++ b/addons/purchase_stock/tests/test_reordering_rule.py @@ -683,3 +683,34 @@ class TestReorderingRule(SavepointCase): {'product_id': self.product_01.id, 'qty_done': 1.0, 'state': 'done', 'location_dest_id': stock_location.id}, {'product_id': self.product_01.id, 'qty_done': 2.0, 'state': 'done', 'location_dest_id': sub_location.id}, ]) + + def test_change_of_scheduled_date(self): + """ + A user creates a delivery, an orderpoint is created. Its forecast + quantity becomes -1 and the quantity to order is 1. Then the user + postpones the scheduled date of the delivery. The quantities of the + orderpoint should be reset to zero. + """ + delivery_form = Form(self.env['stock.picking']) + delivery_form.partner_id = self.partner + delivery_form.picking_type_id = self.env.ref('stock.picking_type_out') + with delivery_form.move_ids_without_package.new() as move: + move.product_id = self.product_01 + move.product_uom_qty = 1 + delivery = delivery_form.save() + delivery.action_confirm() + + self.env['report.stock.quantity'].flush() + self.env['stock.warehouse.orderpoint']._get_orderpoint_action() + + orderpoint = self.env['stock.warehouse.orderpoint'].search([('product_id', '=', self.product_01.id)]) + self.assertRecordValues(orderpoint, [ + {'qty_forecast': -1, 'qty_to_order': 1}, + ]) + + delivery.scheduled_date += td(days=7) + orderpoint.invalidate_cache(fnames=['qty_forecast', 'qty_to_order'], ids=orderpoint.ids) + + self.assertRecordValues(orderpoint, [ + {'qty_forecast': 0, 'qty_to_order': 0}, + ]) diff --git a/addons/sale/models/product_template.py b/addons/sale/models/product_template.py index 027298dcf9..5d18546580 100644 --- a/addons/sale/models/product_template.py +++ b/addons/sale/models/product_template.py @@ -281,6 +281,12 @@ class ProductTemplate(models.Model): 'has_discounted_price': has_discounted_price, } + def _can_be_added_to_cart(self): + """ + Pre-check to `_is_add_to_cart_possible` to know if product can be sold. + """ + return self.sale_ok + def _is_add_to_cart_possible(self, parent_combination=None): """ It's possible to add to cart (potentially after configuration) if @@ -294,7 +300,7 @@ class ProductTemplate(models.Model): :rtype: bool """ self.ensure_one() - if not self.active or not self.sale_ok: + if not self.active or not self._can_be_added_to_cart(): # for performance: avoid calling `_get_possible_combinations` return False return next(self._get_possible_combinations(parent_combination), False) is not False diff --git a/addons/sale_mrp/models/sale.py b/addons/sale_mrp/models/sale.py index 862c286183..876f9f47a5 100644 --- a/addons/sale_mrp/models/sale.py +++ b/addons/sale_mrp/models/sale.py @@ -78,10 +78,9 @@ class SaleOrderLine(models.Model): for order_line in self: if order_line.qty_delivered_method == 'stock_move': boms = order_line.move_ids.filtered(lambda m: m.state != 'cancel').mapped('bom_line_id.bom_id') - dropship = False - if not boms and any(m._is_dropshipped() for m in order_line.move_ids): + dropship = any(m._is_dropshipped() for m in order_line.move_ids) + if not boms and dropship: boms = boms._bom_find(product=order_line.product_id, company_id=order_line.company_id.id, bom_type='phantom') - dropship = True # We fetch the BoMs of type kits linked to the order_line, # the we keep only the one related to the finished produst. # This bom shoud be the only one since bom_line_id was written on the moves diff --git a/addons/sale_timesheet/models/account.py b/addons/sale_timesheet/models/account.py index 6a3eff61b5..cbf754eb05 100644 --- a/addons/sale_timesheet/models/account.py +++ b/addons/sale_timesheet/models/account.py @@ -94,7 +94,7 @@ class AccountAnalyticLine(models.Model): task = self.env['project.task'].browse(values.get('task_id')) if task.analytic_account_id: values['account_id'] = task.analytic_account_id.id - values['company_id'] = task.analytic_account_id.company_id.id + values['company_id'] = task.analytic_account_id.company_id.id or task.company_id.id values = super(AccountAnalyticLine, self)._timesheet_preprocess(values) return values diff --git a/addons/snailmail/static/src/js/snailmail_external_layout.js b/addons/snailmail/static/src/js/snailmail_external_layout.js index b8d12a0d63..3f1c63977a 100644 --- a/addons/snailmail/static/src/js/snailmail_external_layout.js +++ b/addons/snailmail/static/src/js/snailmail_external_layout.js @@ -1,6 +1,6 @@ // Change address font-size if needed document.addEventListener('DOMContentLoaded', function (evt) { - var recipientAddress = document.querySelector(".address.row > div[name='address'] > div") + var recipientAddress = document.querySelector(".address.row > div[name='address'] > address"); var style = window.getComputedStyle(recipientAddress, null); var height = parseFloat(style.getPropertyValue('height')); var fontSize = parseFloat(style.getPropertyValue('font-size')); diff --git a/addons/snailmail/static/src/scss/snailmail_external_layout_asset.scss b/addons/snailmail/static/src/scss/snailmail_external_layout_asset.scss index a5c259fa71..612dc2c294 100644 --- a/addons/snailmail/static/src/scss/snailmail_external_layout_asset.scss +++ b/addons/snailmail/static/src/scss/snailmail_external_layout_asset.scss @@ -25,7 +25,7 @@ .pt-5 { padding-top: 48px !important; } -.article > div > .address.row > div[name="address"] { +.article > .address.row > div[name="address"] { height: 65mm; background-color: #ffffff; padding-top: 11mm; diff --git a/addons/stock/models/stock_orderpoint.py b/addons/stock/models/stock_orderpoint.py index b0890fed5e..09716ebe00 100644 --- a/addons/stock/models/stock_orderpoint.py +++ b/addons/stock/models/stock_orderpoint.py @@ -226,7 +226,8 @@ class StockWarehouseOrderpoint(models.Model): self.trigger = 'auto' return self.action_replenish() - @api.depends('product_id', 'location_id', 'product_id.stock_move_ids', 'product_id.stock_move_ids.state', 'product_id.stock_move_ids.product_uom_qty') + @api.depends('product_id', 'location_id', 'product_id.stock_move_ids', 'product_id.stock_move_ids.state', + 'product_id.stock_move_ids.date', 'product_id.stock_move_ids.product_uom_qty') def _compute_qty(self): orderpoints_contexts = defaultdict(lambda: self.env['stock.warehouse.orderpoint']) for orderpoint in self: diff --git a/addons/stock_picking_batch/models/stock_picking_batch.py b/addons/stock_picking_batch/models/stock_picking_batch.py index 893694788c..d3cd63077d 100644 --- a/addons/stock_picking_batch/models/stock_picking_batch.py +++ b/addons/stock_picking_batch/models/stock_picking_batch.py @@ -104,7 +104,8 @@ class StockPickingBatch(models.Model): @api.depends('picking_ids', 'picking_ids.scheduled_date') def _compute_scheduled_date(self): - self.scheduled_date = min(self.picking_ids.filtered('scheduled_date').mapped('scheduled_date'), default=False) + for rec in self: + rec.scheduled_date = min(rec.picking_ids.filtered('scheduled_date').mapped('scheduled_date'), default=False) @api.onchange('scheduled_date') def onchange_scheduled_date(self): diff --git a/addons/test_mail/models/test_mail_corner_case_models.py b/addons/test_mail/models/test_mail_corner_case_models.py index 0de5e3aa22..a6ef3b9a08 100644 --- a/addons/test_mail/models/test_mail_corner_case_models.py +++ b/addons/test_mail/models/test_mail_corner_case_models.py @@ -73,3 +73,13 @@ class MailTestMultiCompany(models.Model): name = fields.Char() company_id = fields.Many2one('res.company') + + +class MailTestSelectionTracking(models.Model): + """ Test tracking for selection fields """ + _description = 'Test Selection Tracking' + _name = 'mail.test.track.selection' + _inherit = ['mail.thread'] + + name = fields.Char() + type = fields.Selection([('first', 'First'), ('second', 'Second')], tracking=True) diff --git a/addons/test_mail/security/ir.model.access.csv b/addons/test_mail/security/ir.model.access.csv index 68c70193d1..974e7b5934 100644 --- a/addons/test_mail/security/ir.model.access.csv +++ b/addons/test_mail/security/ir.model.access.csv @@ -22,3 +22,5 @@ access_mail_test_cc_user,mail.test.cc.user,model_mail_test_cc,base.group_user,1, access_mail_test_multi_company_user,mail.test.multi.company.user,model_mail_test_multi_company,base.group_user,1,1,1,1 access_mail_test_multi_company_portal,mail.test.multi.company.portal,model_mail_test_multi_company,base.group_portal,1,0,0,0 access_mail_test_track_compute,mail.test.track.compute,model_mail_test_track_compute,base.group_user,1,1,1,1 +access_mail_test_track_selection_portal,mail.test.track.selection.portal,model_mail_test_track_selection,base.group_portal,0,0,0,0 +access_mail_test_track_selection_user,mail.test.track.selection.user,model_mail_test_track_selection,base.group_user,1,1,1,1 diff --git a/addons/test_mail/tests/test_message_track.py b/addons/test_mail/tests/test_message_track.py index c4706701ae..467259630a 100644 --- a/addons/test_mail/tests/test_message_track.py +++ b/addons/test_mail/tests/test_message_track.py @@ -165,6 +165,37 @@ class TestTracking(TestMailCommon): self.assertTrue(new_partner) self.assertEqual(new_partner.company_id, company1) + def test_track_invalid_selection(self): + # Test: Check that initial invalid selection values are allowed when tracking + # Create a record with an initially invalid selection value + invalid_value = 'I love writing tests!' + record = self.env['mail.test.track.selection'].create({ + 'name': 'Test Invalid Selection Values', + "type": "first", + }) + + self.flush_tracking() + self.env.cr.execute( + """ + UPDATE mail_test_track_selection + SET type = %s + WHERE id = %s + """, + [invalid_value, record.id] + ) + + record.invalidate_cache() + + self.assertEqual(record.type, invalid_value) + + # Write a valid selection value + record.type = "second" + + self.flush_tracking() + self.assertTracking(record.message_ids, [ + ('type', 'char', invalid_value, 'Second'), + ]) + def test_track_template(self): # Test: Check that default_* keys are not taken into account in _message_track_post_template magic_code = 'Up-Up-Down-Down-Left-Right-Left-Right-Square-Triangle' diff --git a/addons/test_mass_mailing/models/__init__.py b/addons/test_mass_mailing/models/__init__.py index dabc37c6c7..0fc1880c0c 100644 --- a/addons/test_mass_mailing/models/__init__.py +++ b/addons/test_mass_mailing/models/__init__.py @@ -3,3 +3,4 @@ from . import mailing_mailing from . import mailing_models +from . import mailing_models_cornercase diff --git a/addons/test_mass_mailing/models/mailing_models_cornercase.py b/addons/test_mass_mailing/models/mailing_models_cornercase.py new file mode 100644 index 0000000000..e1f176fac6 --- /dev/null +++ b/addons/test_mass_mailing/models/mailing_models_cornercase.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. + +from flectra import api, fields, models + + +class MailingTestPartnerUnstored(models.Model): + """ Check mailing with unstored fields """ + _description = 'Mailing Model without stored partner_id' + _name = 'mailing.test.partner.unstored' + _inherit = ['mail.thread.blacklist'] + _primary_email = 'email_from' + + name = fields.Char() + email_from = fields.Char() + partner_id = fields.Many2one( + 'res.partner', 'Customer', + compute='_compute_partner_id', + store=False) + + @api.depends('email_from') + def _compute_partner_id(self): + partners = self.env['res.partner'].search( + [('email_normalized', 'in', self.filtered('email_from').mapped('email_normalized'))] + ) + self.partner_id = False + for record in self.filtered('email_from'): + record.partner_id = next( + (partner.id for partner in partners + if partner.email_normalized == record.email_normalized), + False + ) diff --git a/addons/test_mass_mailing/security/ir.model.access.csv b/addons/test_mass_mailing/security/ir.model.access.csv index 5deee89f57..b21c7be49b 100644 --- a/addons/test_mass_mailing/security/ir.model.access.csv +++ b/addons/test_mass_mailing/security/ir.model.access.csv @@ -9,5 +9,7 @@ access_mailing_performance_all,access.mailing.performance.all,model_mailing_perf access_mailing_performance_user,access.mailing.performance.user,model_mailing_performance,base.group_user,1,1,1,1 access_mailing_performance_blacklist_all,access.mailing.performance.blacklist.all,model_mailing_performance_blacklist,,0,0,0,0 access_mailing_performance_blacklist_user,access.mailing.performance.blacklist.user,model_mailing_performance_blacklist,base.group_user,1,1,1,1 +access_mailing_test_partner_unstored_all,access.mailing.test.partner.unstored.all,model_mailing_test_partner_unstored,,0,0,0,0 +access_mailing_test_partner_unstored_user,access.mailing.test.partner.unstored.user,model_mailing_test_partner_unstored,base.group_user,1,1,1,1 access_mailing_test_utm_all,access.mailing.test.utm.all,model_mailing_test_utm,,0,0,0,0 access_mailing_test_utm_user,access.mailing.test.utm.user,model_mailing_test_utm,base.group_user,1,1,1,1 diff --git a/addons/test_mass_mailing/tests/test_mailing.py b/addons/test_mass_mailing/tests/test_mailing.py index a69a6b8f08..79100cc424 100644 --- a/addons/test_mass_mailing/tests/test_mailing.py +++ b/addons/test_mass_mailing/tests/test_mailing.py @@ -280,6 +280,46 @@ class TestMassMailing(TestMassMailCommon): ) self.assertEqual(mailing.ignored, 3) + @users('user_marketing') + def test_mailing_w_seenlist_unstored_partner(self): + """ Test seen list when partners are not stored. """ + test_customers = self.env['res.partner'].sudo().create([ + {'email': f'"Mailing Partner {idx}" Marketing stuff for ${object.name}

', + 'mailing_domain': [('id', 'in', test_records.ids)], + 'mailing_model_id': self.env['ir.model']._get_id('mailing.test.partner.unstored'), + 'name': 'test', + 'subject': 'Blacklisted', + }) + + # create existing traces to check the seen list + traces = self._create_sent_traces( + mailing, + test_records[:3] + ) + traces.flush() + + # check remaining recipients effectively check seen list + mailing.action_put_in_queue() + res_ids = mailing._get_remaining_recipients() + self.assertEqual(sorted(res_ids), sorted(test_records[3:].ids)) + + with self.mock_mail_gateway(mail_unlink_sent=False): + mailing.action_send_mail(res_ids=test_records.ids) + self.assertEqual(len(self._mails), 7, 'Mailing: seen list should contain 3 existing traces') + @users('user_marketing') @mute_logger('flectra.addons.mail.models.mail_mail') def test_mailing_mailing_list_optout(self): diff --git a/addons/web_editor/static/src/js/editor/rte.js b/addons/web_editor/static/src/js/editor/rte.js index da0bde41f8..06c1532b81 100644 --- a/addons/web_editor/static/src/js/editor/rte.js +++ b/addons/web_editor/static/src/js/editor/rte.js @@ -332,9 +332,23 @@ var RTEWidget = Widget.extend({ }); // start element observation + let pastingData = false; + $(document).on('paste', function (ev) { + pastingData = [...self.editable()]; + browser.setTimeout(function(){ pastingData = false; }, 0); + }); $(document).on('content_changed', function (ev) { self.trigger_up('rte_change', {target: ev.target}); + if (pastingData) { + const pastedDirtyEls = ev.target.querySelectorAll('[data-oe-id]'); + _.difference([...pastedDirtyEls], pastingData).forEach(el => { + const dirtyAttributes = el.getAttributeNames().filter(name => !name.indexOf("data-oe-")); + dirtyAttributes.forEach(name => el.removeAttribute(name)); + }) + pastingData = false; + } + // Add the dirty flag to the element that changed by either adding // it on the highest editable ancestor or, if there is no editable // ancestor, on the element itself (that element may not be editable diff --git a/addons/website/models/mixins.py b/addons/website/models/mixins.py index 70afd25c0f..0d6f5ae28c 100644 --- a/addons/website/models/mixins.py +++ b/addons/website/models/mixins.py @@ -45,21 +45,22 @@ class SeoMetadata(models.AbstractModel): title = (request.website or company).name if 'name' in self: title = '%s | %s' % (self.name, title) + img_field = 'social_default_image' if request.website.has_social_default_image else 'logo' - img = request.website.image_url(request.website, img_field) + # Default meta for OpenGraph default_opengraph = { 'og:type': 'website', 'og:title': title, 'og:site_name': company.name, 'og:url': url_join(request.httprequest.url_root, url_for(request.httprequest.path)), - 'og:image': img, + 'og:image': request.website.image_url(request.website, img_field), } # Default meta for Twitter default_twitter = { 'twitter:card': 'summary_large_image', 'twitter:title': title, - 'twitter:image': img + '/300x300', + 'twitter:image': request.website.image_url(request.website, img_field, size='300x300'), } if company.social_twitter: default_twitter['twitter:site'] = "@%s" % company.social_twitter.split('/')[-1] @@ -87,11 +88,8 @@ class SeoMetadata(models.AbstractModel): if self.website_meta_description: opengraph_meta['og:description'] = self.website_meta_description twitter_meta['twitter:description'] = self.website_meta_description - meta_image = self.website_meta_og_img or opengraph_meta['og:image'] - if meta_image.startswith('/'): - meta_image = "%s%s" % (root_url, meta_image) - opengraph_meta['og:image'] = meta_image - twitter_meta['twitter:image'] = meta_image + opengraph_meta['og:image'] = url_join(root_url, url_for(self.website_meta_og_img or opengraph_meta['og:image'])) + twitter_meta['twitter:image'] = url_join(root_url, url_for(self.website_meta_og_img or twitter_meta['twitter:image'])) return { 'opengraph_meta': opengraph_meta, 'twitter_meta': twitter_meta, diff --git a/addons/website_event_track/views/event_track_templates_page.xml b/addons/website_event_track/views/event_track_templates_page.xml index 5e929d9a7e..122b02bcef 100644 --- a/addons/website_event_track/views/event_track_templates_page.xml +++ b/addons/website_event_track/views/event_track_templates_page.xml @@ -30,7 +30,7 @@
-
+
Event