diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py
index c51e464ba9e..53115f099c5 100644
--- a/addons/account/models/account_bank_statement.py
+++ b/addons/account/models/account_bank_statement.py
@@ -520,6 +520,8 @@ class AccountBankStatementLine(models.Model):
:returns: The journal entries with which the transaction was matched. If there was at least an entry in counterpart_aml_dicts or new_aml_dicts, this list contains
the move created by the reconciliation, containing entries for the statement.line (1), the counterpart move lines (0..*) and the new move lines (0..*).
"""
+ payable_account_type = self.env.ref('account.data_account_type_payable')
+ receivable_account_type = self.env.ref('account.data_account_type_receivable')
counterpart_aml_dicts = counterpart_aml_dicts or []
payment_aml_rec = payment_aml_rec or self.env['account.move.line']
new_aml_dicts = new_aml_dicts or []
@@ -540,10 +542,16 @@ class AccountBankStatementLine(models.Model):
raise UserError(_('A selected move line was already reconciled.'))
if isinstance(aml_dict['move_line'], pycompat.integer_types):
aml_dict['move_line'] = aml_obj.browse(aml_dict['move_line'])
+
+ account_types = self.env['account.account.type']
for aml_dict in (counterpart_aml_dicts + new_aml_dicts):
if aml_dict.get('tax_ids') and isinstance(aml_dict['tax_ids'][0], pycompat.integer_types):
# Transform the value in the format required for One2many and Many2many fields
aml_dict['tax_ids'] = [(4, id, None) for id in aml_dict['tax_ids']]
+
+ user_type_id = self.env['account.account'].browse(aml_dict.get('account_id')).user_type_id
+ if user_type_id in [payable_account_type, receivable_account_type] and user_type_id not in account_types:
+ account_types |= user_type_id
if any(line.journal_entry_ids for line in self):
raise UserError(_('A selected statement line was already reconciled with an account move.'))
@@ -580,7 +588,9 @@ class AccountBankStatementLine(models.Model):
if abs(total)>0.00001:
partner_id = self.partner_id and self.partner_id.id or False
partner_type = False
- if partner_id:
+ if partner_id and len(account_types) == 1:
+ partner_type = 'customer' if account_types == receivable_account_type else 'supplier'
+ if partner_id and not partner_type:
if total < 0:
partner_type = 'supplier'
else:
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py
index 655de63597f..12c223e89d8 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -147,8 +147,13 @@ class AccountInvoice(models.Model):
amount_to_show = currency._convert(abs(line.amount_residual), self.currency_id, self.company_id, line.date or fields.Date.today())
if float_is_zero(amount_to_show, precision_rounding=self.currency_id.rounding):
continue
+ if line.ref :
+ title = '%s : %s' % (line.move_id.name, line.ref)
+ else:
+ title = line.move_id.name
info['content'].append({
'journal_name': line.ref or line.move_id.name,
+ 'title': title,
'amount': amount_to_show,
'currency': currency_id.symbol,
'id': line.id,
@@ -1378,6 +1383,20 @@ class AccountInvoice(models.Model):
return result
@api.model
+ def _refund_tax_lines_account_change(self, lines, taxes_to_change):
+ # Let's change the account on tax lines when
+ # @param {list} lines: a list of orm commands
+ # @param {dict} taxes_to_change
+ # key: tax ID, value: refund account
+
+ if not taxes_to_change:
+ return lines
+
+ for line in lines:
+ if isinstance(line[2], dict) and line[2]['tax_id'] in taxes_to_change:
+ line[2]['account_id'] = taxes_to_change[line[2]['tax_id']]
+ return lines
+
def _get_refund_common_fields(self):
return ['partner_id', 'payment_term_id', 'account_id', 'currency_id', 'journal_id']
@@ -1423,7 +1442,12 @@ class AccountInvoice(models.Model):
values['invoice_line_ids'] = self._refund_cleanup_lines(invoice.invoice_line_ids)
tax_lines = invoice.tax_line_ids
- values['tax_line_ids'] = self._refund_cleanup_lines(tax_lines)
+ taxes_to_change = {
+ line.tax_id.id: line.tax_id.refund_account_id.id
+ for line in tax_lines.filtered(lambda l: l.tax_id.refund_account_id != l.tax_id.account_id)
+ }
+ cleaned_tax_lines = self._refund_cleanup_lines(tax_lines)
+ values['tax_line_ids'] = self._refund_tax_lines_account_change(cleaned_tax_lines, taxes_to_change)
if journal_id:
journal = self.env['account.journal'].browse(journal_id)
diff --git a/addons/account/static/src/xml/account_payment.xml b/addons/account/static/src/xml/account_payment.xml
index 4e72bd67d06..55b327183fc 100644
--- a/addons/account/static/src/xml/account_payment.xml
+++ b/addons/account/static/src/xml/account_payment.xml
@@ -16,8 +16,8 @@
Add
-
-
+
+
diff --git a/addons/account/tests/test_account_customer_invoice.py b/addons/account/tests/test_account_customer_invoice.py
index 8e2e825ae2b..bda3a1360fb 100644
--- a/addons/account/tests/test_account_customer_invoice.py
+++ b/addons/account/tests/test_account_customer_invoice.py
@@ -168,6 +168,62 @@ class TestAccountCustomerInvoice(AccountTestUsers):
self.assertEquals(invoice.amount_untaxed, sum([x.base for x in invoice.tax_line_ids]))
+ def test_customer_invoice_tax_refund(self):
+ company = self.env.user.company_id
+ tax_account = self.env['account.account'].create({
+ 'name': 'TAX',
+ 'code': 'TAX',
+ 'user_type_id': self.env.ref('account.data_account_type_current_assets').id,
+ 'company_id': company.id,
+ })
+
+ tax_refund_account = self.env['account.account'].create({
+ 'name': 'TAX_REFUND',
+ 'code': 'TAX_R',
+ 'user_type_id': self.env.ref('account.data_account_type_current_assets').id,
+ 'company_id': company.id,
+ })
+
+ journalrec = self.env['account.journal'].search([('type', '=', 'sale')])[0]
+ partner3 = self.env.ref('base.res_partner_3')
+ account_id = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_revenue').id)], limit=1).id
+
+ tax = self.env['account.tax'].create({
+ 'name': 'Tax 15.0',
+ 'amount': 15.0,
+ 'amount_type': 'percent',
+ 'type_tax_use': 'sale',
+ 'account_id': tax_account.id,
+ 'refund_account_id': tax_refund_account.id
+ })
+
+ invoice_line_data = [
+ (0, 0,
+ {
+ 'product_id': self.env.ref('product.product_product_1').id,
+ 'quantity': 40.0,
+ 'account_id': account_id,
+ 'name': 'product test 1',
+ 'discount': 10.00,
+ 'price_unit': 2.27,
+ 'invoice_line_tax_ids': [(6, 0, [tax.id])],
+ }
+ )]
+
+ invoice = self.env['account.invoice'].create(dict(
+ name="Test Customer Invoice",
+ reference_type="none",
+ journal_id=journalrec.id,
+ partner_id=partner3.id,
+ invoice_line_ids=invoice_line_data
+ ))
+
+ invoice.action_invoice_open()
+
+ refund = invoice.refund()
+ self.assertEqual(invoice.tax_line_ids.mapped('account_id'), tax_account)
+ self.assertEqual(refund.tax_line_ids.mapped('account_id'), tax_refund_account)
+
def test_customer_invoice_dashboard(self):
def patched_today(*args, **kwargs):
return '2019-01-22'
diff --git a/addons/l10n_fr/data/account_chart_template_data.xml b/addons/l10n_fr/data/account_chart_template_data.xml
index d184a621db5..6aa50ef5cbf 100644
--- a/addons/l10n_fr/data/account_chart_template_data.xml
+++ b/addons/l10n_fr/data/account_chart_template_data.xml
@@ -239,7 +239,7 @@
Résultat de l'exercice (bénéfice)
120
-
+
diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py
index 0f8f290b8f4..750dc185add 100644
--- a/addons/mail/models/mail_thread.py
+++ b/addons/mail/models/mail_thread.py
@@ -1337,7 +1337,8 @@ class MailThread(models.AbstractModel):
subtype = 'mail.mt_note'
if message_dict.get('parent_id'):
parent_message = self.env['mail.message'].sudo().browse(message_dict['parent_id'])
- partner_ids = [(4, parent_message.author_id.id)]
+ if parent_message.author_id:
+ partner_ids = [(4, parent_message.author_id.id)]
else:
subtype = 'mail.mt_comment'
diff --git a/addons/mail/static/src/js/activity.js b/addons/mail/static/src/js/activity.js
index b1f43e495c7..d99abb11e7d 100644
--- a/addons/mail/static/src/js/activity.js
+++ b/addons/mail/static/src/js/activity.js
@@ -273,7 +273,7 @@ var BasicActivity = AbstractField.extend({
var self = this;
var $markDoneBtn = $(ev.currentTarget);
var activityID = $markDoneBtn.data('activity-id');
- var previousActivityTypeID = $markDoneBtn.data('previous-activity-type-id');
+ var previousActivityTypeID = $markDoneBtn.data('previous-activity-type-id') || false;
var forceNextActivity = $markDoneBtn.data('force-next-activity');
if ($markDoneBtn.data('toggle') == 'collapse') {
diff --git a/addons/mass_mailing/models/__init__.py b/addons/mass_mailing/models/__init__.py
index 6afa7a98d09..c542abccfba 100644
--- a/addons/mass_mailing/models/__init__.py
+++ b/addons/mass_mailing/models/__init__.py
@@ -4,6 +4,7 @@
from . import mass_mailing
from . import mass_mailing_stats
from . import mail_mail
+from . import mail_template
from . import mail_thread
from . import res_config_settings
from . import mass_mailing_report
diff --git a/addons/mass_mailing/models/mail_template.py b/addons/mass_mailing/models/mail_template.py
new file mode 100644
index 00000000000..68005ee0d10
--- /dev/null
+++ b/addons/mass_mailing/models/mail_template.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, models
+
+
+class MailTemplate(models.Model):
+ _inherit = "mail.template"
+
+ @api.model
+ def render_post_process(self, html):
+ # super will transform relative url to absolute
+ html = super(MailTemplate, self).render_post_process(html)
+
+ # apply shortener after
+ if self.env.context.get('post_convert_links'):
+ html = self.env['link.tracker'].convert_links(
+ html,
+ self.env.context['post_convert_links'],
+ blacklist=['/unsubscribe_from_list']
+ )
+ return html
diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py
index 32503b5eb61..d93aa998dc2 100644
--- a/addons/mass_mailing/models/mass_mailing.py
+++ b/addons/mass_mailing/models/mass_mailing.py
@@ -779,6 +779,21 @@ class MassMailing(models.Model):
_logger.info("Mass-mailing %s targets %s, no opt out list available", self, target._name)
return opt_out
+ def _get_convert_links(self):
+ self.ensure_one()
+ utm_mixin = self.mass_mailing_campaign_id if self.mass_mailing_campaign_id else self
+ vals = {'mass_mailing_id': self.id}
+
+ if self.mass_mailing_campaign_id:
+ vals['mass_mailing_campaign_id'] = self.mass_mailing_campaign_id.id
+ if utm_mixin.campaign_id:
+ vals['campaign_id'] = utm_mixin.campaign_id.id
+ if utm_mixin.source_id:
+ vals['source_id'] = utm_mixin.source_id.id
+ if utm_mixin.medium_id:
+ vals['medium_id'] = utm_mixin.medium_id.id
+ return vals
+
def _get_seen_list(self):
"""Returns a set of emails already targeted by current mailing/campaign (no duplicates)"""
self.ensure_one()
@@ -814,6 +829,7 @@ class MassMailing(models.Model):
return {
'mass_mailing_opt_out_list': self._get_opt_out_list(),
'mass_mailing_seen_list': self._get_seen_list(),
+ 'post_convert_links': self._get_convert_links(),
}
def get_recipients(self):
@@ -855,13 +871,10 @@ class MassMailing(models.Model):
if not res_ids:
raise UserError(_('There is no recipients selected.'))
- # Convert links in absolute URLs before the application of the shortener
- mailing.body_html = self.env['mail.thread']._replace_local_links(mailing.body_html)
-
composer_values = {
'author_id': author_id,
'attachment_ids': [(4, attachment.id) for attachment in mailing.attachment_ids],
- 'body': mailing.convert_links()[mailing.id],
+ 'body': mailing.body_html,
'subject': mailing.name,
'model': mailing.mailing_model_real,
'email_from': mailing.email_from,
diff --git a/addons/mass_mailing/tests/__init__.py b/addons/mass_mailing/tests/__init__.py
index b02648f85eb..36745d1525a 100644
--- a/addons/mass_mailing/tests/__init__.py
+++ b/addons/mass_mailing/tests/__init__.py
@@ -2,3 +2,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_mass_mailing_list_merge
+from . import test_mass_mailing_shortener
diff --git a/addons/mass_mailing/tests/test_mass_mailing_shortener.py b/addons/mass_mailing/tests/test_mass_mailing_shortener.py
new file mode 100644
index 00000000000..99e3de1e864
--- /dev/null
+++ b/addons/mass_mailing/tests/test_mass_mailing_shortener.py
@@ -0,0 +1,132 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.tests import common
+from lxml import etree
+
+try:
+ from unittest.mock import patch
+except ImportError:
+ from mock import patch
+
+
+class TestMassMailingShortener(common.TransactionCase):
+ def getHrefFor(self, html, id):
+ return html.xpath("*[@id='%s']" % id)[0].attrib.get('href')
+
+ def shorturl_to_link(self, short_url):
+ return self.env['link.tracker.code'].search([('code', '=', short_url.split('/r/')[-1])]).link_id
+
+ def setUp(self):
+ super(TestMassMailingShortener, self).setUp()
+
+ def _get_title_from_url(u):
+ return "Hello"
+
+ def _compute_favicon():
+ # 1px to avoid real request
+ return 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=='
+
+ patcher = patch('odoo.addons.link_tracker.models.link_tracker.link_tracker._compute_favicon', wraps=_compute_favicon)
+ patcher2 = patch('odoo.addons.link_tracker.models.link_tracker.link_tracker._get_title_from_url', wraps=_get_title_from_url)
+ patcher.start()
+ patcher2.start()
+ self.addCleanup(patcher.stop)
+ self.addCleanup(patcher2.stop)
+
+ def test_00_test_mass_mailing_shortener(self):
+ mailing_list_A = self.env['mail.mass_mailing.list'].create({
+ 'name': 'A',
+ })
+ self.env['mail.mass_mailing.contact'].create({
+ 'name': 'User 1', 'email': 'user1@example.com', 'list_ids': [(4, mailing_list_A.id)]
+ })
+ self.env['mail.mass_mailing.contact'].create({
+ 'name': 'User 2', 'email': 'user2@example.com', 'list_ids': [(4, mailing_list_A.id)]
+ })
+ self.env['mail.mass_mailing.contact'].create({
+ 'name': 'User 3', 'email': 'user3@example.com', 'list_ids': [(4, mailing_list_A.id)]
+ })
+
+ mass_mailing = self.env['mail.mass_mailing'].create({
+ "reply_to_mode": "email",
+ "reply_to": "Administrator ",
+ "mailing_model_id": self.env.ref('mass_mailing.model_mail_mass_mailing_list').id,
+ "mailing_domain": "[('list_ids', 'in', [%d])]" % mailing_list_A.id,
+ "contact_list_ids": [[6, False, [mailing_list_A.id]]],
+ "mass_mailing_campaign_id": False,
+ "name": "sdf",
+ "body_html": """
+Hi,
+% set url = "www.odoo.com"
+% set httpurl = "https://www.odoo.eu"
+Website0: https://www.odoo.tz/my/${object.name}
+Website1: https://www.odoo.be
+Website2: https://${url}
+Website3: ${httpurl}
+Email: test@odoo.com
+ """,
+ "schedule_date": False,
+ "state": "draft",
+ "keep_archives": True,
+ })
+
+ mass_mailing.put_in_queue()
+ mass_mailing._process_mass_mailing_queue()
+
+ sent_mails = self.env['mail.mail'].search([('mailing_id', '=', mass_mailing.id)])
+ sent_messages = sent_mails.mapped('mail_message_id')
+
+ self.assertEqual(mailing_list_A.contact_nbr, len(sent_messages),
+ 'Some message has not been sent')
+
+ xbody = etree.fromstring(sent_messages[0].body)
+ after_url0 = self.getHrefFor(xbody, 'url0')
+ after_url1 = self.getHrefFor(xbody, 'url1')
+ after_url2 = self.getHrefFor(xbody, 'url2')
+ after_url3 = self.getHrefFor(xbody, 'url3')
+ after_url4 = self.getHrefFor(xbody, 'url4')
+
+ self.assertTrue('/r/' in after_url0, 'URL0 should be shortened: %s' % after_url0)
+ self.assertTrue('/r/' in after_url1, 'URL1 should be shortened: %s' % after_url1)
+ self.assertTrue('/r/' in after_url2, 'URL2 should be shortened: %s' % after_url2)
+ self.assertTrue('/r/' in after_url3, 'URL3 should be shortened: %s' % after_url3)
+ self.assertEqual(after_url4, "mailto:test@odoo.com", 'mailto: has been converted')
+
+ short0 = self.shorturl_to_link(after_url0)
+ short1 = self.shorturl_to_link(after_url1)
+ short2 = self.shorturl_to_link(after_url2)
+ short3 = self.shorturl_to_link(after_url3)
+
+ self.assertTrue("https://www.odoo.tz/my/User" in short0.url, 'URL mismatch')
+ self.assertEqual(short1.url, "https://www.odoo.be", 'URL mismatch')
+ self.assertEqual(short2.url, "https://www.odoo.com", 'URL mismatch')
+ self.assertEqual(short3.url, "https://www.odoo.eu", 'URL mismatch')
+
+ _xbody = etree.fromstring(sent_messages[1].body)
+ _after_url0 = self.getHrefFor(_xbody, 'url0')
+ _after_url1 = self.getHrefFor(_xbody, 'url1')
+ _after_url2 = self.getHrefFor(_xbody, 'url2')
+ _after_url3 = self.getHrefFor(_xbody, 'url3')
+ _after_url4 = self.getHrefFor(_xbody, 'url4')
+
+ self.assertTrue('/r/' in _after_url0, 'URL0 should be shortened: %s' % _after_url0)
+ self.assertTrue('/r/' in _after_url1, 'URL1 should be shortened: %s' % _after_url1)
+ self.assertTrue('/r/' in _after_url2, 'URL2 should be shortened: %s' % _after_url2)
+ self.assertTrue('/r/' in _after_url3, 'URL3 should be shortened: %s' % _after_url3)
+ self.assertEqual(_after_url4, "mailto:test@odoo.com", 'mailto: has been converted')
+
+ _short0 = self.shorturl_to_link(_after_url0)
+ _short1 = self.shorturl_to_link(_after_url1)
+ _short2 = self.shorturl_to_link(_after_url2)
+ _short3 = self.shorturl_to_link(_after_url3)
+
+ self.assertTrue("https://www.odoo.tz/my/User" in _short0.url, 'URL mismatch')
+ self.assertEqual(_short1.url, "https://www.odoo.be", 'URL mismatch')
+ self.assertEqual(_short2.url, "https://www.odoo.com", 'URL mismatch')
+ self.assertEqual(_short3.url, "https://www.odoo.eu", 'URL mismatch')
+
+ self.assertNotEqual(short0.url, _short0.url)
+ self.assertEqual(short1.url, _short1.url)
+ self.assertEqual(short2.url, _short2.url)
+ self.assertEqual(short3.url, _short3.url)
diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py
index 77295665439..f9e7f2ccce4 100644
--- a/addons/mrp/models/mrp_production.py
+++ b/addons/mrp/models/mrp_production.py
@@ -5,7 +5,7 @@ from collections import defaultdict
from odoo import api, fields, models, _
from odoo.addons import decimal_precision as dp
-from odoo.exceptions import UserError
+from odoo.exceptions import AccessError, UserError
from odoo.tools import float_compare, float_round
class MrpProduction(models.Model):
@@ -30,6 +30,10 @@ class MrpProduction(models.Model):
location = self.env['stock.picking.type'].browse(self.env.context['default_picking_type_id']).default_location_src_id
if not location:
location = self.env.ref('stock.stock_location_stock', raise_if_not_found=False)
+ try:
+ location.check_access_rule('read')
+ except (AttributeError, AccessError):
+ location = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.company_id.id)], limit=1).lot_stock_id
return location and location.id or False
@api.model
@@ -39,6 +43,10 @@ class MrpProduction(models.Model):
location = self.env['stock.picking.type'].browse(self.env.context['default_picking_type_id']).default_location_dest_id
if not location:
location = self.env.ref('stock.stock_location_stock', raise_if_not_found=False)
+ try:
+ location.check_access_rule('read')
+ except (AttributeError, AccessError):
+ location = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.company_id.id)], limit=1).lot_stock_id
return location and location.id or False
name = fields.Char(
@@ -336,6 +344,10 @@ class MrpProduction(models.Model):
@api.onchange('picking_type_id')
def onchange_picking_type(self):
location = self.env.ref('stock.stock_location_stock')
+ try:
+ location.check_access_rule('read')
+ except (AttributeError, AccessError):
+ location = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.company_id.id)], limit=1).lot_stock_id
self.location_src_id = self.picking_type_id.default_location_src_id.id or location.id
self.location_dest_id = self.picking_type_id.default_location_dest_id.id or location.id
diff --git a/addons/mrp/models/mrp_unbuild.py b/addons/mrp/models/mrp_unbuild.py
index 7ee94a3af71..6da539e2256 100644
--- a/addons/mrp/models/mrp_unbuild.py
+++ b/addons/mrp/models/mrp_unbuild.py
@@ -2,7 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
-from odoo.exceptions import UserError
+from odoo.exceptions import AccessError, UserError
from odoo.tools import float_compare
@@ -13,10 +13,20 @@ class MrpUnbuild(models.Model):
_order = 'id desc'
def _get_default_location_id(self):
- return self.env.ref('stock.stock_location_stock', raise_if_not_found=False)
+ stock_location = self.env.ref('stock.stock_location_stock', raise_if_not_found=False)
+ try:
+ stock_location.check_access_rule('read')
+ return stock_location.id
+ except (AttributeError, AccessError):
+ return self.env['stock.warehouse'].search([('company_id', '=', self.env.user.company_id.id)], limit=1).lot_stock_id.id
def _get_default_location_dest_id(self):
- return self.env.ref('stock.stock_location_stock', raise_if_not_found=False)
+ stock_location = self.env.ref('stock.stock_location_stock', raise_if_not_found=False)
+ try:
+ stock_location.check_access_rule('read')
+ return stock_location.id
+ except (AttributeError, AccessError):
+ return self.env['stock.warehouse'].search([('company_id', '=', self.env.user.company_id.id)], limit=1).lot_stock_id.id
name = fields.Char('Reference', copy=False, readonly=True, default=lambda x: _('New'))
product_id = fields.Many2one(
diff --git a/addons/mrp/views/mrp_unbuild_views.xml b/addons/mrp/views/mrp_unbuild_views.xml
index e811e2a6140..00e96e8cbad 100644
--- a/addons/mrp/views/mrp_unbuild_views.xml
+++ b/addons/mrp/views/mrp_unbuild_views.xml
@@ -128,8 +128,8 @@
-
-
+
+
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index a9ec839625e..97befbfe2b4 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -893,12 +893,20 @@ exports.PosModel = Backbone.Model.extend({
transfer.pipe(function(order_server_id){
// generate the pdf and download it
- self.chrome.do_action('point_of_sale.pos_invoice_report',{additional_context:{
- active_ids:order_server_id,
- }}).done(function () {
- invoiced.resolve();
- done.resolve();
- });
+ if (order_server_id.length) {
+ self.chrome.do_action('point_of_sale.pos_invoice_report',{additional_context:{
+ active_ids:order_server_id,
+ }}).done(function () {
+ invoiced.resolve();
+ done.resolve();
+ });
+ } else {
+ // The order has been pushed separately in batch when
+ // the connection came back.
+ // The user has to go to the backend to print the invoice
+ invoiced.reject({code:401, message:'Backend Invoice', data:{order: order}});
+ done.reject();
+ }
});
return done;
diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js
index 6939aff72f2..aac23b1ebb8 100644
--- a/addons/point_of_sale/static/src/js/screens.js
+++ b/addons/point_of_sale/static/src/js/screens.js
@@ -2089,6 +2089,17 @@ var PaymentScreenWidget = ScreenWidget.extend({
self.gui.show_screen('clientlist');
},
});
+ } else if (error.message === 'Backend Invoice') {
+ self.gui.show_popup('confirm',{
+ 'title': _t('Please print the invoice from the backend'),
+ 'body': _t('The order has been synchronized earlier. Please make the invoice from the backend for the order: ') + error.data.order.name,
+ confirm: function () {
+ this.gui.show_screen('receipt');
+ },
+ cancel: function () {
+ this.gui.show_screen('receipt');
+ },
+ });
} else if (error.code < 0) { // XmlHttpRequest Errors
self.gui.show_popup('error',{
'title': _t('The order could not be sent'),
diff --git a/addons/stock/models/stock_scrap.py b/addons/stock/models/stock_scrap.py
index 16f6b505acb..7c8eeefb57a 100644
--- a/addons/stock/models/stock_scrap.py
+++ b/addons/stock/models/stock_scrap.py
@@ -131,7 +131,8 @@ class StockScrap(models.Model):
self.package_id,
self.owner_id,
strict=True).mapped('quantity'))
- if float_compare(available_qty, self.scrap_qty, precision_digits=precision) >= 0:
+ scrap_qty = self.product_uom_id._compute_quantity(self.scrap_qty, self.product_id.uom_id)
+ if float_compare(available_qty, scrap_qty, precision_digits=precision) >= 0:
return self.do_scrap()
else:
return {
diff --git a/addons/stock/tests/test_move.py b/addons/stock/tests/test_move.py
index 2037ec4f34e..0939389dda9 100644
--- a/addons/stock/tests/test_move.py
+++ b/addons/stock/tests/test_move.py
@@ -3569,6 +3569,24 @@ class StockMove(TransactionCase):
self.assertEqual(scrap.state, 'done')
self.assertEqual(move1.reserved_availability, 0.25)
+ def test_scrap_6(self):
+ """ Check that scrap correctly handle UoM. """
+ self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 1)
+ scrap = self.env['stock.scrap'].create({
+ 'product_id': self.product1.id,
+ 'product_uom_id': self.uom_dozen.id,
+ 'scrap_qty': 1,
+ })
+ warning_message = scrap.action_validate()
+ self.assertEqual(warning_message.get('res_model', 'Wrong Model'), 'stock.warn.insufficient.qty.scrap')
+ insufficient_qty_wizard = self.env['stock.warn.insufficient.qty.scrap'].create({
+ 'product_id': self.product1.id,
+ 'location_id': self.stock_location.id,
+ 'scrap_id': scrap.id
+ })
+ insufficient_qty_wizard.action_done()
+ self.assertEqual(self.env['stock.quant']._gather(self.product1, self.stock_location).quantity, -11)
+
def test_in_date_1(self):
""" Check that moving a tracked quant keeps the incoming date.
"""
diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml
index 78bc346495b..e46c57c6a21 100644
--- a/addons/stock/views/stock_picking_views.xml
+++ b/addons/stock/views/stock_picking_views.xml
@@ -219,7 +219,7 @@
-
+
diff --git a/addons/stock_account/models/product.py b/addons/stock_account/models/product.py
index b035a23ec2b..ea8c358ba91 100644
--- a/addons/stock_account/models/product.py
+++ b/addons/stock_account/models/product.py
@@ -189,23 +189,38 @@ class ProductProduct(models.Model):
StockMove = self.env['stock.move']
to_date = self.env.context.get('to_date')
- self.env['account.move.line'].check_access_rights('read')
- fifo_automated_values = {}
- query = """SELECT aml.product_id, aml.account_id, sum(aml.debit) - sum(aml.credit), sum(quantity), array_agg(aml.id)
- FROM account_move_line AS aml
- WHERE aml.product_id IS NOT NULL AND aml.company_id=%%s %s
- GROUP BY aml.product_id, aml.account_id"""
- params = (self.env.user.company_id.id,)
- if to_date:
- query = query % ('AND aml.date <= %s',)
- params = params + (to_date,)
- else:
- query = query % ('',)
- self.env.cr.execute(query, params=params)
+ real_time_product_ids = [product.id for product in self if product.product_tmpl_id.valuation == 'real_time']
+ if real_time_product_ids:
+ self.env['account.move.line'].check_access_rights('read')
+ fifo_automated_values = {}
+ query = """SELECT aml.product_id, aml.account_id, sum(aml.debit) - sum(aml.credit), sum(quantity), array_agg(aml.id)
+ FROM account_move_line AS aml
+ WHERE aml.product_id IN %%s AND aml.company_id=%%s %s
+ GROUP BY aml.product_id, aml.account_id"""
+ params = (tuple(real_time_product_ids), self.env.user.company_id.id)
+ if to_date:
+ query = query % ('AND aml.date <= %s',)
+ params = params + (to_date,)
+ else:
+ query = query % ('',)
+ self.env.cr.execute(query, params=params)
- res = self.env.cr.fetchall()
- for row in res:
- fifo_automated_values[(row[0], row[1])] = (row[2], row[3], list(row[4]))
+ res = self.env.cr.fetchall()
+ for row in res:
+ fifo_automated_values[(row[0], row[1])] = (row[2], row[3], list(row[4]))
+
+ product_values = {product: 0 for product in self}
+ product_move_ids = {product: [] for product in self}
+ if to_date:
+ domain = [('product_id', 'in', self.ids), ('date', '<=', to_date)] + StockMove._get_all_base_domain()
+ for move in StockMove.search(domain).with_context(prefetch_fields=False):
+ product_values[move.product_id] += move.value
+ product_move_ids[move.product_id].append(move.id)
+ else:
+ domain = [('product_id', 'in', self.ids)] + StockMove._get_all_base_domain()
+ for move in StockMove.search(domain).with_context(prefetch_fields=False):
+ product_values[move.product_id] += move.remaining_value
+ product_move_ids[move.product_id].append(move.id)
for product in self:
if product.cost_method in ['standard', 'average']:
@@ -221,11 +236,9 @@ class ProductProduct(models.Model):
elif product.cost_method == 'fifo':
if to_date:
if product.product_tmpl_id.valuation == 'manual_periodic':
- domain = [('product_id', '=', product.id), ('date', '<=', to_date)] + StockMove._get_all_base_domain()
- moves = StockMove.search(domain)
- product.stock_value = sum(moves.mapped('value'))
+ product.stock_value = product_values[product]
product.qty_at_date = product.with_context(company_owned=True, owner_id=False).qty_available
- product.stock_fifo_manual_move_ids = StockMove.browse(moves.ids)
+ product.stock_fifo_manual_move_ids = StockMove.browse(product_move_ids[product])
elif product.product_tmpl_id.valuation == 'real_time':
valuation_account_id = product.categ_id.property_stock_valuation_account_id.id
value, quantity, aml_ids = fifo_automated_values.get((product.id, valuation_account_id)) or (0, 0, [])
@@ -233,7 +246,7 @@ class ProductProduct(models.Model):
product.qty_at_date = quantity
product.stock_fifo_real_time_aml_ids = self.env['account.move.line'].browse(aml_ids)
else:
- product.stock_value, moves = product._sum_remaining_values()
+ product.stock_value, moves = product_values[product], StockMove.browse(product_move_ids[product])
product.qty_at_date = product.with_context(company_owned=True, owner_id=False).qty_available
if product.product_tmpl_id.valuation == 'manual_periodic':
product.stock_fifo_manual_move_ids = moves
diff --git a/addons/web/static/src/js/views/search/search_filters.js b/addons/web/static/src/js/views/search/search_filters.js
index 4455a7b9bdf..86fdff9f0cb 100644
--- a/addons/web/static/src/js/views/search/search_filters.js
+++ b/addons/web/static/src/js/views/search/search_filters.js
@@ -185,8 +185,10 @@ var DateTime = Field.extend({
operators: [
{value: "=", text: _lt("is equal to")},
{value: "!=", text: _lt("is not equal to")},
- {value: ">=", text: _lt("is after")},
- {value: "<=", text: _lt("is before")},
+ {value: ">", text: _lt("is after")},
+ {value: "<", text: _lt("is before")},
+ {value: ">=", text: _lt("is after or equal to")},
+ {value: "<=", text: _lt("is before or equal to")},
{value: "between", text: _lt("is between")},
{value: "∃", text: _lt("is set")},
{value: "∄", text: _lt("is not set")}
diff --git a/addons/web/static/src/scss/navbar_mobile.scss b/addons/web/static/src/scss/navbar_mobile.scss
index 6ef917b8517..f4593436163 100644
--- a/addons/web/static/src/scss/navbar_mobile.scss
+++ b/addons/web/static/src/scss/navbar_mobile.scss
@@ -23,7 +23,6 @@
position: relative;
height: $o-navbar-height;
- overflow: hidden;
> ul {
> li {
@@ -39,6 +38,7 @@
&.o_menu_sections {
width: 100%;
+ display: none;
.dropdown-menu.show {
position: static;
@@ -82,6 +82,9 @@
body.o_mobile_menu_opened > .o_main_navbar {
height: 100%;
overflow: auto;
+ .o_menu_sections {
+ display: block;
+ }
}
}
diff --git a/addons/web_editor/controllers/main.py b/addons/web_editor/controllers/main.py
index 7bb6f77a649..5c7b5316d44 100644
--- a/addons/web_editor/controllers/main.py
+++ b/addons/web_editor/controllers/main.py
@@ -51,11 +51,16 @@ class Web_Editor(http.Controller):
kwargs.update(trans)
- record = None
- if model and kwargs.get('res_id'):
- record = request.env[model].with_context(trans).browse(kwargs.get('res_id'))
+ content = None
+ if model:
+ Model = request.env[model].with_context(trans)
+ if kwargs.get('res_id'):
+ record = Model.browse(kwargs.get('res_id'))
+ content = record and getattr(record, field)
+ else:
+ content = Model.default_get([field]).get(field)
- kwargs.update(content=record and getattr(record, field) or "")
+ kwargs.update(content=content or '')
return request.render(kwargs.get("template") or "web_editor.FieldTextHtml", kwargs, uid=request.uid)
diff --git a/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py
index f04a61c4903..e4f3f18a040 100644
--- a/addons/website_slides/controllers/main.py
+++ b/addons/website_slides/controllers/main.py
@@ -190,7 +190,7 @@ class WebsiteSlides(http.Controller):
@http.route('''/slides/slide//download''', type='http', auth="public", website=True, sitemap=False)
def slide_download(self, slide, **kw):
slide = slide.sudo()
- if slide.download_security == 'public' or (slide.download_security == 'user' and request.session.uid):
+ if slide.download_security == 'public' or (slide.download_security == 'user' and request.env.user and request.env.user != request.website.user_id):
filecontent = base64.b64decode(slide.datas)
disposition = 'attachment; filename=%s.pdf' % werkzeug.urls.url_quote(slide.name)
return request.make_response(