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 @@