diff --git a/addons/account/tests/test_product_id_change.py b/addons/account/tests/test_product_id_change.py
new file mode 100644
index 00000000000..1bd5154f4ba
--- /dev/null
+++ b/addons/account/tests/test_product_id_change.py
@@ -0,0 +1,50 @@
+from openerp.tests.common import TransactionCase
+
+class TestProductIdChange(TransactionCase):
+ """Test that when an included tax is mapped by a fiscal position, the included tax must be
+ subtracted to the price of the product.
+ """
+
+ def setUp(self):
+ super(TestProductIdChange, self).setUp()
+ self.fiscal_position_model = self.registry('account.fiscal.position')
+ self.fiscal_position_tax_model = self.registry('account.fiscal.position.tax')
+ self.tax_model = self.registry('account.tax')
+ self.pricelist_model = self.registry('product.pricelist')
+ self.res_partner_model = self.registry('res.partner')
+ self.product_tmpl_model = self.registry('product.template')
+ self.product_model = self.registry('product.product')
+ self.invoice_line_model = self.registry('account.invoice.line')
+
+ def test_product_id_change(self):
+ cr, uid = self.cr, self.uid
+ partner_id = self.res_partner_model.create(cr, uid, dict(name="George"))
+ tax_include_id = self.tax_model.create(cr, uid, dict(name="Include tax",
+ type='percent',
+ amount='0.21',
+ price_include=True))
+ tax_exclude_id = self.tax_model.create(cr, uid, dict(name="Exclude tax",
+ type='percent',
+ amount='0.00'))
+ product_tmpl_id = self.product_tmpl_model.create(cr, uid, dict(name="Voiture",
+ list_price='121',
+ standard_price='121',
+ taxes_id=[(6, 0, [tax_include_id])],
+ supplier_taxes_id=[(6, 0, [tax_include_id])]))
+ product_id = self.product_model.create(cr, uid, dict(product_tmpl_id=product_tmpl_id))
+ product = self.product_model.browse(cr, uid, [product_id])
+ fp_id = self.fiscal_position_model.create(cr, uid, dict(name="fiscal position",
+ sequence=1))
+ fp_tax_id = self.fiscal_position_tax_model.create(cr, uid, dict(position_id=fp_id,
+ tax_src_id=tax_include_id,
+ tax_dest_id=tax_exclude_id))
+
+ res = self.invoice_line_model.product_id_change(cr, uid, [], product.id, product.uom_id.id,
+ qty=1, type='out_invoice', partner_id=partner_id,
+ fposition_id=fp_id)
+ self.assertEquals(100, res['value']['price_unit'], "The included tax must be subtracted to the price")
+
+ res = self.invoice_line_model.product_id_change(cr, uid, [], product.id, product.uom_id.id,
+ qty=1, type='in_invoice', partner_id=partner_id,
+ fposition_id=fp_id)
+ self.assertEquals(100, res['value']['price_unit'], "The included tax must be subtracted to the price")
diff --git a/addons/auth_signup/controllers/main.py b/addons/auth_signup/controllers/main.py
index 9035a650809..b508514617f 100644
--- a/addons/auth_signup/controllers/main.py
+++ b/addons/auth_signup/controllers/main.py
@@ -36,7 +36,11 @@ class AuthSignupHome(openerp.addons.web.controllers.main.Home):
self.do_signup(qcontext)
return super(AuthSignupHome, self).web_login(*args, **kw)
except (SignupError, AssertionError), e:
- qcontext['error'] = _(e.message)
+ if request.env["res.users"].sudo().search([("login", "=", qcontext.get("login"))]):
+ qcontext["error"] = _("Another user is already registered using this email address.")
+ else:
+ _logger.error(e.message)
+ qcontext['error'] = _("Could not create a new account.")
return request.render('auth_signup.signup', qcontext)
diff --git a/addons/calendar/calendar.py b/addons/calendar/calendar.py
index 596bdc5677f..c46bd5a16e9 100644
--- a/addons/calendar/calendar.py
+++ b/addons/calendar/calendar.py
@@ -1469,7 +1469,7 @@ class calendar_event(osv.Model):
for arg in args:
new_arg = arg
- if arg[0] in ('start_date', 'start_datetime', 'start',) and arg[1] == ">=":
+ if arg[0] in ('stop_date', 'stop_datetime', 'stop',) and arg[1] == ">=":
if context.get('virtual_id', True):
new_args += ['|', '&', ('recurrency', '=', 1), ('final_date', arg[1], arg[2])]
elif arg[0] == "id":
diff --git a/addons/crm/crm_lead_view.xml b/addons/crm/crm_lead_view.xml
index 7fef14bfcb1..371724682fb 100644
--- a/addons/crm/crm_lead_view.xml
+++ b/addons/crm/crm_lead_view.xml
@@ -131,6 +131,7 @@
diff --git a/addons/crm/wizard/crm_lead_to_opportunity.py b/addons/crm/wizard/crm_lead_to_opportunity.py
index e621910c71d..b08d8445b3d 100644
--- a/addons/crm/wizard/crm_lead_to_opportunity.py
+++ b/addons/crm/wizard/crm_lead_to_opportunity.py
@@ -121,6 +121,7 @@ class crm_lead2opportunity_partner(osv.osv_memory):
context = {}
lead_obj = self.pool['crm.lead']
+ partner_obj = self.pool['res.partner']
w = self.browse(cr, uid, ids, context=context)[0]
opp_ids = [o.id for o in w.opportunity_ids]
@@ -144,6 +145,9 @@ class crm_lead2opportunity_partner(osv.osv_memory):
lead_ids = context.get('active_ids', [])
vals.update({'lead_ids': lead_ids, 'user_ids': [w.user_id.id]})
self._convert_opportunity(cr, uid, ids, vals, context=context)
+ for lead in lead_obj.browse(cr, uid, lead_ids, context=context):
+ if lead.partner_id and lead.partner_id.user_id != lead.user_id:
+ partner_obj.write(cr, uid, [lead.partner_id.id], {'user_id': lead.user_id.id}, context=context)
return self.pool.get('crm.lead').redirect_opportunity_view(cr, uid, lead_ids[0], context=context)
diff --git a/addons/delivery/models/delivery_carrier.py b/addons/delivery/models/delivery_carrier.py
index 4aa8987b5dc..dbf93a92853 100644
--- a/addons/delivery/models/delivery_carrier.py
+++ b/addons/delivery/models/delivery_carrier.py
@@ -3,7 +3,7 @@
import logging
from openerp import api, fields, models, _
-from openerp.exceptions import UserError
+from openerp.exceptions import UserError, ValidationError
_logger = logging.getLogger(__name__)
@@ -52,6 +52,25 @@ class DeliveryCarrier(models.Model):
fixed_price = fields.Float(compute='_compute_fixed_price', inverse='_set_product_fixed_price', store=True, string='Fixed Price',help="Keep empty if the pricing depends on the advanced pricing per destination")
shipping_enabled = fields.Boolean(string="Shipping enabled", default=True, help="Uncheck this box to disable package shipping while validating Delivery Orders")
+ @api.multi
+ def name_get(self):
+ display_delivery = self.env.context.get('display_delivery', False)
+ order_id = self.env.context.get('order_id', False)
+ if display_delivery and order_id:
+ order = self.env['sale.order'].browse(order_id)
+ currency = order.pricelist_id.currency_id.name or ''
+ res = []
+ for carrier_id in self.ids:
+ try:
+ r = self.read([carrier_id], ['name', 'price'])[0]
+ res.append((r['id'], r['name'] + ' (' + (str(r['price'])) + ' ' + currency + ')'))
+ except ValidationError:
+ r = self.read([carrier_id], ['name'])[0]
+ res.append((r['id'], r['name']))
+ else:
+ res = super(DeliveryCarrier, self).name_get()
+ return res
+
@api.depends('product_id.list_price', 'product_id.product_tmpl_id.list_price')
def _compute_fixed_price(self):
for carrier in self:
@@ -234,6 +253,8 @@ class DeliveryCarrier(models.Model):
quantity += qty
total = (order.amount_total or 0.0) - total_delivery
+ total = order.currency_id.with_context(date=order.date_order).compute(total, order.company_id.currency_id)
+
return self.get_price_from_picking(total, weight, volume, quantity)
def get_price_from_picking(self, total, weight, volume, quantity):
diff --git a/addons/delivery/views/delivery_view.xml b/addons/delivery/views/delivery_view.xml
index a001ffd21a4..ab38c276ae9 100644
--- a/addons/delivery/views/delivery_view.xml
+++ b/addons/delivery/views/delivery_view.xml
@@ -171,7 +171,8 @@
-
+
diff --git a/addons/google_calendar/google_calendar.py b/addons/google_calendar/google_calendar.py
index e00b97a8336..81d1bb82f5f 100644
--- a/addons/google_calendar/google_calendar.py
+++ b/addons/google_calendar/google_calendar.py
@@ -232,12 +232,12 @@ class google_calendar(osv.AbstractModel):
"start": {
type: start_date,
vstype: None,
- 'timeZone': context.get('tz', 'UTC'),
+ 'timeZone': context.get('tz') or 'UTC',
},
"end": {
type: final_date,
vstype: None,
- 'timeZone': context.get('tz', 'UTC'),
+ 'timeZone': context.get('tz') or 'UTC',
},
"attendees": attendee_list,
"reminders": {
diff --git a/addons/hr_payroll/report/report_payslip_details.py b/addons/hr_payroll/report/report_payslip_details.py
index ba31bd8b100..f5858f8e7ef 100644
--- a/addons/hr_payroll/report/report_payslip_details.py
+++ b/addons/hr_payroll/report/report_payslip_details.py
@@ -22,7 +22,7 @@ class payslip_details_report(report_sxw.rml_parse):
if not rule_categories:
return []
if rule_categories[0].parent_id:
- rule_categories.insert(0, rule_categories[0].parent_id)
+ rule_categories = rule_categories[0].parent_id | rule_categories
get_recursive_parent(rule_categories)
return rule_categories
diff --git a/addons/hw_blackbox_be/__init__.py b/addons/hw_blackbox_be/__init__.py
new file mode 100644
index 00000000000..bf7fb7b45c1
--- /dev/null
+++ b/addons/hw_blackbox_be/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import controllers
diff --git a/addons/hw_blackbox_be/__openerp__.py b/addons/hw_blackbox_be/__openerp__.py
new file mode 100644
index 00000000000..d33930e2143
--- /dev/null
+++ b/addons/hw_blackbox_be/__openerp__.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+{
+ 'name': 'Blackbox Hardware Driver',
+ 'version': '1.0',
+ 'category': 'Hardware Drivers',
+ 'sequence': 6,
+ 'summary': 'Hardware Driver for Belgian Fiscal Data Modules',
+ 'website': 'https://www.odoo.com/page/point-of-sale',
+ 'description': """
+Fiscal Data Module Hardware Driver
+==================================
+
+This module allows a Point Of Sale client to communicate with a
+connected Belgian Fiscal Data Module.
+""",
+ 'author': 'OpenERP SA',
+ 'depends': ['hw_proxy'],
+ 'external_dependencies': {'python': ['serial']},
+ 'test': [
+ ],
+ 'installable': True,
+ 'auto_install': False,
+}
diff --git a/addons/hw_blackbox_be/controllers/__init__.py b/addons/hw_blackbox_be/controllers/__init__.py
new file mode 100644
index 00000000000..f48716d3a69
--- /dev/null
+++ b/addons/hw_blackbox_be/controllers/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import main
diff --git a/addons/hw_blackbox_be/controllers/main.py b/addons/hw_blackbox_be/controllers/main.py
new file mode 100644
index 00000000000..fbc7664cbc0
--- /dev/null
+++ b/addons/hw_blackbox_be/controllers/main.py
@@ -0,0 +1,162 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import logging
+import serial
+from os import listdir
+from threading import Thread, Lock
+
+from openerp import http
+
+import openerp.addons.hw_proxy.controllers.main as hw_proxy
+
+_logger = logging.getLogger(__name__)
+
+class Blackbox(Thread):
+ def __init__(self):
+ Thread.__init__(self)
+ self.blackbox_lock = Lock()
+ self.set_status('connecting')
+ self.device_path = self._find_device_path_by_probing()
+
+ def set_status(self, status, messages=[]):
+ self.status = {
+ 'status': status,
+ 'messages': messages
+ }
+
+ def get_status(self):
+ return self.status
+
+ # There is no real way to find a serial device, all you can really
+ # find is the name of the serial to usb interface, which in the
+ # case of the blackbox is not defined because it doesn't always
+ # come with it's own interface (eg. Retail Cleancash SC-B). So, in
+ # order to differentiate between other devices like this, what
+ # we'll do is probe every serial device with an FDM status
+ # request. The first device to give an answer that makes sense
+ # wins.
+ def _find_device_path_by_probing(self):
+ path = "/dev/serial/by-id/"
+ probe_message = self._wrap_low_level_message_around("S000")
+
+ try:
+ devices = listdir(path)
+ except OSError:
+ _logger.warning(path + " doesn't exist")
+ else:
+ for device in listdir(path):
+ path_to_device = path + device
+ _logger.debug("Probing " + device)
+
+ if self._send_to_blackbox(probe_message, 21, path_to_device, just_wait_for_ack=True):
+ _logger.info(device + " will be used as the blackbox")
+ self.set_status("connected", [device])
+ return path_to_device
+
+ _logger.warning("Blackbox could not be found")
+ self.set_status("error", ["Couldn't find the Fiscal Data Module"])
+ return ""
+
+ def _lrc(self, msg):
+ lrc = 0
+
+ for character in msg:
+ byte = ord(character)
+ lrc = (lrc + byte) & 0xFF
+
+ lrc = ((lrc ^ 0xFF) + 1) & 0xFF
+
+ return lrc
+
+ def _wrap_low_level_message_around(self, high_level_message):
+ bcc = self._lrc(high_level_message)
+ high_level_message_bytes = map(ord, high_level_message)
+
+ low_level_message = bytearray()
+ low_level_message.append(0x02)
+ low_level_message.extend(high_level_message_bytes)
+ low_level_message.append(0x03)
+ low_level_message.append(bcc)
+
+ return low_level_message
+
+ def _send_and_wait_for_ack(self, packet, serial):
+ ack = 0
+ MAX_RETRIES = 1
+
+ while ack != 0x06 and int(chr(packet[4])) < MAX_RETRIES:
+ serial.write(packet)
+ ack = serial.read(1)
+
+ # This violates the principle that we do high level
+ # client-side and low level posbox-side but the retry
+ # counter is always in a fixed position in the high level
+ # message so it's safe to do it. Also it would be a pain
+ # to have to throw this all the way back to js just so it
+ # can increment the retry counter and then try again.
+ packet = packet[:4] + str(int(packet[4]) + 1) + packet[5:]
+
+ if ack:
+ ack = ord(ack)
+ else:
+ _logger.warning("did not get ACK, retrying...")
+ ack = 0
+
+ if ack == 0x06:
+ return True
+ else:
+ _logger.error("retried " + str(MAX_RETRIES) + " times without receiving ACK, is blackbox properly connected?")
+ return False
+
+ def _send_to_blackbox(self, packet, response_size, device_path, just_wait_for_ack=False):
+ if not device_path:
+ return ""
+
+ ser = serial.Serial(port=device_path,
+ baudrate=19200,
+ timeout=3)
+ MAX_NACKS = 1
+ got_response = False
+ sent_nacks = 0
+
+ if self._send_and_wait_for_ack(packet, ser):
+ if just_wait_for_ack:
+ return True
+
+ while not got_response and sent_nacks < MAX_NACKS:
+ stx = ser.read(1)
+ response = ser.read(response_size)
+ etx = ser.read(1)
+ bcc = ser.read(1)
+
+ if stx == chr(0x02) and etx == chr(0x03) and bcc and self._lrc(response) == ord(bcc):
+ got_response = True
+ ser.write(chr(0x06))
+ else:
+ _logger.warning("received ACK but not a valid response, sending NACK...")
+ sent_nacks += 1
+ ser.write(chr(0x15))
+
+ if not got_response:
+ _logger.error("sent " + str(MAX_NACKS) + " NACKS without receiving response, giving up.")
+ return ""
+
+ ser.close()
+ return response
+ else:
+ ser.close()
+ return ""
+
+blackbox_thread = Blackbox()
+hw_proxy.drivers['fiscal_data_module'] = blackbox_thread
+
+class BlackboxDriver(hw_proxy.Proxy):
+ @http.route('/hw_proxy/request_blackbox/', type='json', auth='none', cors='*')
+ def request_blackbox(self, high_level_message, response_size):
+ to_send = blackbox_thread._wrap_low_level_message_around(high_level_message)
+
+ with blackbox_thread.blackbox_lock:
+ response = blackbox_thread._send_to_blackbox(to_send, response_size, blackbox_thread.device_path)
+
+ return response
diff --git a/addons/l10n_be_invoice_bba/invoice.py b/addons/l10n_be_invoice_bba/invoice.py
index d20dab8cfd1..8e6bd53d4d3 100644
--- a/addons/l10n_be_invoice_bba/invoice.py
+++ b/addons/l10n_be_invoice_bba/invoice.py
@@ -198,7 +198,7 @@ class account_invoice(osv.osv):
required=True, readonly=True),
}
_constraints = [
- (_check_communication, 'Invalid BBA Structured Communication !', ['Communication']),
+ (_check_communication, 'Invalid BBA Structured Communication !', ['reference', 'reference_type']),
]
account_invoice()
diff --git a/addons/mail/models/mail_mail.py b/addons/mail/models/mail_mail.py
index 2520718ce10..bdd2045acf7 100644
--- a/addons/mail/models/mail_mail.py
+++ b/addons/mail/models/mail_mail.py
@@ -154,7 +154,7 @@ class MailMail(models.Model):
if partner:
email_to = [formataddr((partner.name, partner.email))]
else:
- email_to = tools.email_split(self.email_to)
+ email_to = tools.email_split_and_format(self.email_to)
return email_to
@api.multi
diff --git a/addons/mail/models/mail_template.py b/addons/mail/models/mail_template.py
index 3117b571ed6..a20d7a7ecb4 100644
--- a/addons/mail/models/mail_template.py
+++ b/addons/mail/models/mail_template.py
@@ -38,7 +38,7 @@ def format_tz(pool, cr, uid, dt, tz=False, format=False, context=None):
format_date = lang_params.get("date_format", '%B-%d-%Y')
format_time = lang_params.get("time_format", '%I-%M %p')
- fdate = ts.strftime(format_date)
+ fdate = ts.strftime(format_date).decode('utf-8')
ftime = ts.strftime(format_time)
return "%s %s%s" % (fdate, ftime, (' (%s)' % tz) if tz else '')
diff --git a/addons/mail/tests/common.py b/addons/mail/tests/common.py
index 98e9c8ad3c1..f70a929464c 100644
--- a/addons/mail/tests/common.py
+++ b/addons/mail/tests/common.py
@@ -39,6 +39,7 @@ class TestMail(common.SavepointCase):
cls.env['ir.mail_server']._patch_method('build_email', build_email)
cls.env['ir.mail_server']._patch_method('send_email', send_email)
+
# User groups
user_group_employee = cls.env.ref('base.group_user')
user_group_portal = cls.env.ref('base.group_portal')
diff --git a/addons/mail/tests/test_mail_gateway.py b/addons/mail/tests/test_mail_gateway.py
index 41ae67ba369..f7656259ef8 100644
--- a/addons/mail/tests/test_mail_gateway.py
+++ b/addons/mail/tests/test_mail_gateway.py
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
+from email.utils import formataddr
+
from .common import TestMail
from openerp.tools import mute_logger
import socket
diff --git a/addons/mass_mailing/i18n/mass_mailing.pot b/addons/mass_mailing/i18n/mass_mailing.pot
index 41113b5ea12..08c325ce36e 100644
--- a/addons/mass_mailing/i18n/mass_mailing.pot
+++ b/addons/mass_mailing/i18n/mass_mailing.pot
@@ -1714,3 +1714,9 @@ msgstr ""
msgid "© 2014 All Rights Reserved"
msgstr ""
+#. module: mass_mailing
+#: code:addons/mass_mailing/models/mail_mail.py:72
+#, python-format
+msgid "Click to unsubscribe"
+msgstr ""
+
diff --git a/addons/mass_mailing/models/mail_mail.py b/addons/mass_mailing/models/mail_mail.py
index cc9928ca8ac..1256280c00b 100644
--- a/addons/mass_mailing/models/mail_mail.py
+++ b/addons/mass_mailing/models/mail_mail.py
@@ -5,6 +5,7 @@ import re
import urlparse
import re
import werkzeug.urls
+import re
from openerp import tools
from openerp import SUPERUSER_ID
@@ -86,6 +87,14 @@ class MailMail(osv.Model):
body = re.sub('(
]*\ssrc=")(/[^/][^"]+)', _sub_relative2absolute, body)
body = re.sub(r'(<[^>]+\bstyle="[^"]+\burl\(\'?)(/[^/\'][^\'")]+)', _sub_relative2absolute, body)
+ # resolve relative image url to absolute for outlook.com
+ def _sub_relative2absolute(match):
+ return match.group(1) + urlparse.urljoin(domain, match.group(2))
+ # Regex: https://regex101.com/r/aE8uG5/3
+ body = re.sub('(
]*\ssrc=["\'])(/[^/][^"\']+)', _sub_relative2absolute, body)
+ # Regex: https://regex101.com/r/kT3lD5/2
+ body = re.sub(r'(<[^>]+\bstyle=["\'][^"\']+\burl\([\'"]?)(/[^/\'"][^\'")]+)', _sub_relative2absolute, body)
+
# generate tracking URL
if mail.statistics_ids:
tracking_url = self._get_tracking_url(cr, uid, mail, partner, context=context)
diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py
index 4263f0c50db..e8b4c755508 100644
--- a/addons/mass_mailing/models/mass_mailing.py
+++ b/addons/mass_mailing/models/mass_mailing.py
@@ -685,6 +685,8 @@ class MassMailing(osv.Model):
value['mailing_domain'] = "[('list_id', 'in', %s), ('opt_out', '=', False)]" % list(mailing_list_ids)
else:
value['mailing_domain'] = "[('list_id', '=', False)]"
+ elif 'opt_out' in self.pool[mailing_model]._fields:
+ value['mailing_domain'] = "[('opt_out', '=', False)]"
else:
value['mailing_domain'] = []
value['body_html'] = "on_change_model_and_list"
diff --git a/addons/mass_mailing/views/mass_mailing.xml b/addons/mass_mailing/views/mass_mailing.xml
index 4cbfab36e71..f7543a50ad1 100644
--- a/addons/mass_mailing/views/mass_mailing.xml
+++ b/addons/mass_mailing/views/mass_mailing.xml
@@ -384,7 +384,8 @@