diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index 3a52a0c8392..b3f719f1449 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -371,7 +371,7 @@ class AccountMoveLine(models.Model):
else:
date = partial_line.credit_move_id.date if partial_line.debit_move_id == line else partial_line.debit_move_id.date
rate = line.currency_id.with_context(date=date).rate
- amount_residual_currency += sign_partial_line * line.currency_id.round(partial_line.amount * rate)
+ amount_residual_currency += sign_partial_line * partial_line.amount * rate
#computing the `reconciled` field.
reconciled = False
diff --git a/addons/hw_escpos/escpos/escpos.py b/addons/hw_escpos/escpos/escpos.py
index 3573efb1d98..bc1c5e1842f 100644
--- a/addons/hw_escpos/escpos/escpos.py
+++ b/addons/hw_escpos/escpos/escpos.py
@@ -888,14 +888,23 @@ class Escpos:
self._raw(PAPER_FULL_CUT)
- def cashdraw(self, pin):
- """ Send pulse to kick the cash drawer """
- if pin == 2:
- self._raw(CD_KICK_2)
- elif pin == 5:
- self._raw(CD_KICK_5)
- else:
- raise CashDrawerError()
+ def cashdraw(self, pin, tries=5):
+ """ Send pulse to kick the cash drawer
+
+ With some printers the drawer will not open after one pulse, for this reason we will check
+ the drawer status up to 'tries' times and send a new pulse if the drawer is still closed. If the
+ drawer status is open, we will stop sending pulses.
+ """
+ for i in range(tries):
+ if pin == 2:
+ self._raw(CD_KICK_2)
+ elif pin == 5:
+ self._raw(CD_KICK_5)
+ else:
+ raise CashDrawerError()
+
+ if i != tries - 1 and not self.get_printer_status()['printer']['drawer_pin_high']:
+ break
def hw(self, hw):
diff --git a/addons/l10n_be_hr_payroll_fleet/__manifest__.py b/addons/l10n_be_hr_payroll_fleet/__manifest__.py
index 5577d12c671..f05e58bff6e 100644
--- a/addons/l10n_be_hr_payroll_fleet/__manifest__.py
+++ b/addons/l10n_be_hr_payroll_fleet/__manifest__.py
@@ -11,6 +11,7 @@
'views/fleet_views.xml',
'views/res_config_settings_views.xml',
'views/hr_contract_views.xml',
+ 'security/security.xml',
],
'auto_install': True,
}
diff --git a/addons/l10n_be_hr_payroll_fleet/security/security.xml b/addons/l10n_be_hr_payroll_fleet/security/security.xml
new file mode 100644
index 00000000000..3fc21715a4c
--- /dev/null
+++ b/addons/l10n_be_hr_payroll_fleet/security/security.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/addons/l10n_mx/data/account_tag_data.xml b/addons/l10n_mx/data/account_tag_data.xml
index 244c9b65ff3..1e8b23dacda 100644
--- a/addons/l10n_mx/data/account_tag_data.xml
+++ b/addons/l10n_mx/data/account_tag_data.xml
@@ -479,25 +479,25 @@
119.01 IVA pendiente de pago4accounts
- A
+ D119.02 IVA de importación pendiente de pago4accounts
- A
+ D119.03 IEPS pendiente de pago4accounts
- A
+ D119.04 IEPS pendiente de pago en importación4accounts
- A
+ D120.01 Anticipo a proveedores nacional
@@ -917,7 +917,7 @@
180.01 Crédito mercantil4accounts
- A
+ D181.01 Gastos de instalación
@@ -2423,7 +2423,7 @@
503 Devoluciones, descuentos o bonificaciones sobre compras1accounts
- D
+ A503.01 Devoluciones, descuentos o bonificaciones sobre compras
@@ -2585,13 +2585,13 @@
505.01 Costo por venta de activo fijo4accounts
- A
+ D505.02 Costo por baja de activo fijo4accounts
- A
+ D601.01 Sueldos y salarios
@@ -4781,7 +4781,7 @@
607.01 Participación de los trabajadores en las utilidades4accounts
- A
+ D608.01 Participación en resultados de subsidiarias
@@ -4799,7 +4799,7 @@
610.01 Participación de los trabajadores en las utilidades diferida4accounts
- A
+ D611.01 Impuesto Sobre la renta
@@ -4817,7 +4817,7 @@
612.01 Gastos no deducibles para CUFIN4accounts
- A
+ D613.01 Depreciación de edificios
diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py
index 36ced25ed75..5b1306dc11d 100644
--- a/addons/mail/models/mail_activity.py
+++ b/addons/mail/models/mail_activity.py
@@ -103,7 +103,7 @@ class MailActivity(models.Model):
('today', 'Today'),
('planned', 'Planned')], 'State',
compute='_compute_state')
- recommended_activity_type_id = fields.Many2one('mail.activity.type', string="Recommended Activity Type", readonly=True)
+ recommended_activity_type_id = fields.Many2one('mail.activity.type', string="Recommended Activity Type")
previous_activity_type_id = fields.Many2one('mail.activity.type', string='Previous Activity Type', readonly=True)
has_recommended_activities = fields.Boolean(
'Next activities available',
diff --git a/addons/mail/models/mail_mail.py b/addons/mail/models/mail_mail.py
index 1d53888c9f3..27e3cdd746f 100644
--- a/addons/mail/models/mail_mail.py
+++ b/addons/mail/models/mail_mail.py
@@ -5,6 +5,7 @@ import base64
import datetime
import logging
import psycopg2
+import smtplib
import threading
from collections import defaultdict
@@ -150,6 +151,7 @@ class MailMail(models.Model):
if notif_emails:
notifications = self.env['mail.notification'].search([
('mail_message_id', 'in', notif_emails.mapped('mail_message_id').ids),
+ ('res_partner_id', 'in', notif_emails.mapped('recipient_ids').ids),
('is_email', '=', True)])
if mail_sent:
notifications.write({
@@ -301,6 +303,20 @@ class MailMail(models.Model):
})
mail_sent = False
+ # Update notification in a transient exception state to avoid concurrent
+ # update in case an email bounces while sending all emails related to current
+ # mail record.
+ notifs = self.env['mail.notification'].search([
+ ('is_email', '=', True),
+ ('mail_message_id', 'in', mail.mapped('mail_message_id').ids),
+ ('res_partner_id', 'in', mail.mapped('recipient_ids').ids),
+ ('email_status', 'not in', ('sent', 'canceled'))
+ ])
+ if notifs:
+ notifs.sudo().write({
+ 'email_status': 'exception',
+ })
+
# build an RFC2822 email.message.Message object and send it without queuing
res = None
for email in email_list:
@@ -348,11 +364,12 @@ class MailMail(models.Model):
'MemoryError while processing mail with ID %r and Msg-Id %r. Consider raising the --limit-memory-hard startup option',
mail.id, mail.message_id)
raise
- except psycopg2.Error:
- # If an error with the database occurs, chances are that the cursor is unusable.
- # This will lead to an `psycopg2.InternalError` being raised when trying to write
- # `state`, shadowing the original exception and forbid a retry on concurrent
- # update. Let's bubble it.
+ except (psycopg2.Error, smtplib.SMTPServerDisconnected):
+ # If an error with the database or SMTP session occurs, chances are that the cursor
+ # or SMTP session are unusable, causing further errors when trying to save the state.
+ _logger.exception(
+ 'Exception while processing mail with ID %r and Msg-Id %r.',
+ mail.id, mail.message_id)
raise
except Exception as e:
failure_reason = tools.ustr(e)
diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py
index d60ddb5e3a5..c22a8a10cc9 100644
--- a/addons/mail/wizard/mail_compose_message.py
+++ b/addons/mail/wizard/mail_compose_message.py
@@ -353,6 +353,7 @@ class MailComposer(models.TransientModel):
- normal mode: return rendered values
/!\ for x2many field, this onchange return command instead of ids
"""
+ attachment_ids = []
if template_id and composition_mode == 'mass_mail':
template = self.env['mail.template'].browse(template_id)
fields = ['subject', 'body_html', 'email_from', 'reply_to', 'mail_server_id']
@@ -378,7 +379,7 @@ class MailComposer(models.TransientModel):
'res_id': 0,
'type': 'binary', # override default_type from context, possibly meant for another model!
}
- values.setdefault('attachment_ids', list()).append(Attachment.create(data_attach).id)
+ attachment_ids.append(Attachment.create(data_attach).id)
else:
default_values = self.with_context(default_composition_mode=composition_mode, default_model=model, default_res_id=res_id).default_get(['composition_mode', 'model', 'res_id', 'parent_id', 'partner_ids', 'subject', 'body', 'email_from', 'reply_to', 'attachment_ids', 'mail_server_id'])
values = dict((key, default_values[key]) for key in ['subject', 'body', 'partner_ids', 'email_from', 'reply_to', 'attachment_ids', 'mail_server_id'] if key in default_values)
@@ -391,6 +392,8 @@ class MailComposer(models.TransientModel):
# this force the complete replacement of x2many field with
# command and is compatible with onchange api.v7
values = self._convert_to_write(values)
+ if attachment_ids:
+ values.update(attachment_ids=[(6, 0, attachment_ids)])
return {'value': values}
diff --git a/addons/point_of_sale/static/src/js/gui.js b/addons/point_of_sale/static/src/js/gui.js
index b5cff2f233a..0d8c67bc3d9 100644
--- a/addons/point_of_sale/static/src/js/gui.js
+++ b/addons/point_of_sale/static/src/js/gui.js
@@ -458,7 +458,7 @@ var Gui = core.Class.extend({
}
} else if (input === '-') {
if (options.firstinput) {
- newbuf = '-';
+ newbuf = '-0';
} else if ( newbuf[0] === '-' ) {
newbuf = newbuf.substring(1,newbuf.length);
} else {
@@ -473,6 +473,9 @@ var Gui = core.Class.extend({
newbuf += input;
}
}
+ if (newbuf === "-") {
+ newbuf = "";
+ }
// End of input buffer at 12 characters.
if (newbuf.length > buffer.length && newbuf.length > 12) {
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index fd49ac36aed..b591f497745 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -1040,7 +1040,7 @@ class SaleOrderLine(models.Model):
result.setdefault(so_line_id, 0.0)
uom = product_uom_map.get(item['product_uom_id'][0])
if so_line.product_uom.category_id == uom.category_id:
- qty = uom._compute_quantity(item['unit_amount'], so_line.product_uom)
+ qty = uom._compute_quantity(item['unit_amount'], so_line.product_uom, rounding_method='HALF-UP')
else:
qty = item['unit_amount']
result[so_line_id] += qty
diff --git a/addons/website/static/src/js/content/snippets.animation.js b/addons/website/static/src/js/content/snippets.animation.js
index e28e1e0d14f..c9e28c31d6a 100644
--- a/addons/website/static/src/js/content/snippets.animation.js
+++ b/addons/website/static/src/js/content/snippets.animation.js
@@ -825,6 +825,10 @@ registry.gallerySlider = Animation.extend({
destroy: function () {
this._super.apply(this, arguments);
+ if (!this.$indicator) {
+ return;
+ }
+
this.$prev.prependTo(this.$indicator);
this.$next.appendTo(this.$indicator);
this.$carousel.off('.gallery_slider');
diff --git a/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml b/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml
index f441eb74dcb..1948413b47a 100644
--- a/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml
+++ b/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml
@@ -170,18 +170,12 @@