diff --git a/addons/event/models/event.py b/addons/event/models/event.py
index d55a17de099..f11105d5e03 100644
--- a/addons/event/models/event.py
+++ b/addons/event/models/event.py
@@ -386,9 +386,14 @@ class EventRegistration(models.Model):
@api.multi
def message_get_suggested_recipients(self):
recipients = super(EventRegistration, self).message_get_suggested_recipients()
+ public_users = self.env['res.users'].sudo()
+ public_groups = self.env.ref("base.group_public", raise_if_not_found=False)
+ if public_groups:
+ public_users = public_groups.sudo().with_context(active_test=False).mapped("users")
try:
for attendee in self:
- if attendee.partner_id:
+ is_public = attendee.sudo().with_context(active_test=False).partner_id.user_ids in public_users if public_users else False
+ if attendee.partner_id and not is_public:
attendee._message_add_suggested_recipient(recipients, partner=attendee.partner_id, reason=_('Customer'))
elif attendee.email:
attendee._message_add_suggested_recipient(recipients, email=attendee.email, reason=_('Customer Email'))
diff --git a/addons/l10n_nl/data/account_tax_template.xml b/addons/l10n_nl/data/account_tax_template.xml
index 07558101a49..b534e9b7369 100644
--- a/addons/l10n_nl/data/account_tax_template.xml
+++ b/addons/l10n_nl/data/account_tax_template.xml
@@ -549,7 +549,7 @@
none
-
+
20
@@ -680,7 +680,7 @@
none
-
+
20
diff --git a/addons/link_tracker/models/link_tracker.py b/addons/link_tracker/models/link_tracker.py
index bdc97778543..c1152d4b43c 100644
--- a/addons/link_tracker/models/link_tracker.py
+++ b/addons/link_tracker/models/link_tracker.py
@@ -248,19 +248,20 @@ class link_tracker_click(models.Model):
vals = {
'link_id': code_rec.link_id.id,
- 'create_date': datetime.date.today(),
'ip': ip,
'country_id': country_record.id,
- 'mail_stat_id': stat_id
}
if stat_id:
mail_stat = self.env['mail.mail.statistics'].search([('id', '=', stat_id)])
+ # It could happen that the related ID is no longer available, but we still want the link to work
+ if mail_stat.exists():
+ vals['mail_stat_id'] = stat_id
- if mail_stat.mass_mailing_campaign_id:
- vals['mass_mailing_campaign_id'] = mail_stat.mass_mailing_campaign_id.id
+ if mail_stat.mass_mailing_campaign_id:
+ vals['mass_mailing_campaign_id'] = mail_stat.mass_mailing_campaign_id.id
- if mail_stat.mass_mailing_id:
- vals['mass_mailing_id'] = mail_stat.mass_mailing_id.id
+ if mail_stat.mass_mailing_id:
+ vals['mass_mailing_id'] = mail_stat.mass_mailing_id.id
self.create(vals)
diff --git a/addons/payment_ogone/controllers/main.py b/addons/payment_ogone/controllers/main.py
index af2e9e05732..a89ff2c63b3 100644
--- a/addons/payment_ogone/controllers/main.py
+++ b/addons/payment_ogone/controllers/main.py
@@ -48,7 +48,7 @@ class OgoneController(http.Controller):
def feedback(self, **kwargs):
try:
tx = request.env['payment.transaction'].sudo()._ogone_form_get_tx_from_data(kwargs)
- tx._ogone_s2s_validate()
+ tx._ogone_s2s_validate_tree(kwargs)
except ValidationError:
return 'ko'
return 'ok'
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index 28f0eb9e1ed..8d13d682f5d 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -663,7 +663,7 @@ class SaleOrderLine(models.Model):
if line.state != 'sale' or not line.product_id._need_procurement():
continue
qty = 0.0
- for proc in line.procurement_ids:
+ for proc in line.procurement_ids.filtered(lambda r: r.state != 'cancel'):
qty += proc.product_qty
if float_compare(qty, line.product_uom_qty, precision_digits=precision) >= 0:
continue
diff --git a/addons/stock/models/stock_scrap.py b/addons/stock/models/stock_scrap.py
index 8453a49b4e3..f921d1eaba6 100644
--- a/addons/stock/models/stock_scrap.py
+++ b/addons/stock/models/stock_scrap.py
@@ -80,16 +80,17 @@ class StockScrap(models.Model):
for scrap in self:
moves = scrap._get_origin_moves() or self.env['stock.move']
move = self.env['stock.move'].create(scrap._prepare_move_values())
- quants = self.env['stock.quant'].quants_get_preferred_domain(
- move.product_qty, move,
- domain=[
- ('qty', '>', 0),
- ('lot_id', '=', self.lot_id.id),
- ('package_id', '=', self.package_id.id)],
- preferred_domain_list=scrap._get_preferred_domain())
- if any([not x[0] for x in quants]):
- raise UserError(_('You cannot scrap a move without having available stock for %s. You can correct it with an inventory adjustment.') % move.product_id.name)
- self.env['stock.quant'].quants_reserve(quants, move)
+ if move.product_id.type == 'product':
+ quants = self.env['stock.quant'].quants_get_preferred_domain(
+ move.product_qty, move,
+ domain=[
+ ('qty', '>', 0),
+ ('lot_id', '=', self.lot_id.id),
+ ('package_id', '=', self.package_id.id)],
+ preferred_domain_list=scrap._get_preferred_domain())
+ if any([not x[0] for x in quants]):
+ raise UserError(_('You cannot scrap a move without having available stock for %s. You can correct it with an inventory adjustment.') % move.product_id.name)
+ self.env['stock.quant'].quants_reserve(quants, move)
move.action_done()
scrap.write({'move_id': move.id, 'state': 'done'})
moves.recalculate_move_state()
diff --git a/addons/stock/tests/__init__.py b/addons/stock/tests/__init__.py
index bbc9cde189e..06e4428afda 100644
--- a/addons/stock/tests/__init__.py
+++ b/addons/stock/tests/__init__.py
@@ -4,3 +4,4 @@ from . import test_stock_flow
import test_product
import test_warehouse
import test_shipment
+import test_stock_scrap
diff --git a/addons/stock/tests/test_stock_scrap.py b/addons/stock/tests/test_stock_scrap.py
new file mode 100644
index 00000000000..b5dc521f4b7
--- /dev/null
+++ b/addons/stock/tests/test_stock_scrap.py
@@ -0,0 +1,107 @@
+# -*- coding: utf-8 -*-
+
+from openerp.tests.common import TransactionCase
+from openerp.exceptions import UserError
+
+
+class TestStockScrap(TransactionCase):
+
+ def setUp(self):
+ super(TestStockScrap, self).setUp()
+ self.user_demo = self.env.ref('base.user_demo')
+ self.scrap_obj = self.env['stock.scrap']
+ self.picking_obj = self.env['stock.picking']
+ self.stock_loc = self.browse_ref('stock.stock_location_stock')
+ self.customer_loc = self.browse_ref('stock.stock_location_customers')
+
+ # Stockable product
+ self.product_stockable = self.env['product.product'].create({
+ 'type': 'product',
+ 'name': 'Stockable Product',
+ 'uom_id': self.env.ref('product.product_uom_unit').id,
+ 'uom_po_id': self.env.ref('product.product_uom_unit').id,
+ })
+
+ # Consumable product
+ self.product_consumable = self.env['product.product'].create({
+ 'type': 'consu',
+ 'name': 'Consumable Product',
+ 'uom_id': self.env.ref('product.product_uom_unit').id,
+ 'uom_po_id': self.env.ref('product.product_uom_unit').id,
+ })
+
+ self._update_product_qty(self.product_stockable)
+
+ def _update_product_qty(self, product):
+ product_qty = self.env['stock.change.product.qty'].create({
+ 'location_id': self.stock_loc.id,
+ 'product_id': product.id,
+ 'new_quantity': 100.0,
+ })
+ product_qty.change_product_qty()
+ return product_qty
+
+ def _prepare_picking(self, product):
+ picking = self.picking_obj.create({
+ 'name': 'picking - test',
+ 'location_id': self.stock_loc.id,
+ 'location_dest_id': self.customer_loc.id,
+ 'picking_type_id': self.ref('stock.picking_type_out'),
+ 'move_lines': [(0, 0, {
+ 'name': product.name,
+ 'product_id': product.id,
+ 'product_uom_qty': 20.0,
+ 'product_uom': product.uom_id.id,
+ })]
+ })
+ return picking
+
+ def test_stock_scrap_01(self):
+ """ Standalone scrap a stockable product """
+ with self.assertRaises(UserError):
+ self.scrap_obj.create({
+ 'name': 'scrap - grand test',
+ 'product_id': self.product_stockable.id,
+ 'product_uom_id': self.ref('product.product_uom_unit'),
+ 'scrap_qty': 120,
+ })
+ self.assertEquals(self.product_stockable.qty_available, 100.0)
+
+ def test_stock_scrap_02(self):
+ """ Scrap a stockable product from a picking """
+ picking = self._prepare_picking(self.product_stockable)
+ scrap = self.scrap_obj.create({
+ 'name': 'scrap - grand test',
+ 'product_id': self.product_stockable.id,
+ 'product_uom_id': self.ref('product.product_uom_unit'),
+ 'picking_id': picking.id,
+ 'scrap_qty': 80,
+ })
+ self.assertEquals(scrap.state, 'done')
+ self.assertEquals(self.product_stockable.qty_available, 20.0)
+
+ def test_stock_scrap_03(self):
+ """ Standalone scrap a consumable product """
+ scrap = self.scrap_obj.create({
+ 'name': 'scrap - grand test',
+ 'product_id': self.product_consumable.id,
+ 'product_uom_id': self.ref('product.product_uom_unit'),
+ 'scrap_qty': 100,
+ })
+ self.assertEquals(scrap.state, 'done')
+ self.assertEquals(self.product_consumable.qty_available, -100.0)
+ with self.assertRaises(UserError):
+ scrap.sudo(self.user_demo).unlink()
+
+ def test_stock_scrap_04(self):
+ """ Scrap a consumable product from a picking """
+ picking = self._prepare_picking(self.product_consumable)
+ scrap = self.scrap_obj.create({
+ 'name': 'scrap - grand test',
+ 'product_id': self.product_consumable.id,
+ 'product_uom_id': self.ref('product.product_uom_unit'),
+ 'picking_id': picking.id,
+ 'scrap_qty': 100,
+ })
+ self.assertEquals(scrap.state, 'done')
+ self.assertEquals(self.product_consumable.qty_available, -100.0)
diff --git a/addons/web/static/lib/qweb/qweb-test-widgets.xml b/addons/web/static/lib/qweb/qweb-test-widgets.xml
new file mode 100644
index 00000000000..503880bcc34
--- /dev/null
+++ b/addons/web/static/lib/qweb/qweb-test-widgets.xml
@@ -0,0 +1,26 @@
+
+
+ {"value": "1988-09-16"}
+ 1988-09-16
+
+
+ {"value": "1988-09-16 14:00:00"}
+ 1988-09-16 14:00:00
+
+
+ {"value": "1988-09-16 14:00:00"}
+ 09/16/1988 16:00:00
+
+
+ {"value": "1988-09-16 14:00:00"}
+ 09/16/1988
+
+
+ {"value": "1988-09-16 01:00:00"}
+ 09/16/1988
+
+
+ {"value": "1988-09-16 23:00:00"}
+ 09/17/1988
+
+
diff --git a/doc/cla/corporate/eficent.md b/doc/cla/corporate/eficent.md
index 3723fbf0740..2b5b8c830c4 100644
--- a/doc/cla/corporate/eficent.md
+++ b/doc/cla/corporate/eficent.md
@@ -14,6 +14,7 @@ List of contributors:
Aarón Henríquez ahenriquez@eficent.com https://github.com/aheficent
Lois Rilo lois.rilo@eficent.com https://github.com/lreficent
-Gisela Mora gisela.mora@eficent.com https://github.com/gmeficent
Miquel Raich miquel.raich@eficent.com https://github.com/mreficent
Jordi Ballester jordi.ballester@eficent.com https://github.com/jbeficent
+Hector Villarreal hector.villarreal@eficent.com https://github.com/hveficent
+Roser Garcia roser.garcia@eficent.com https://github.com/rgarnau (up to 2018-05-01)
diff --git a/doc/cla/individual/subodhdahal.md b/doc/cla/individual/subodhdahal.md
new file mode 100644
index 00000000000..4e4c389bc99
--- /dev/null
+++ b/doc/cla/individual/subodhdahal.md
@@ -0,0 +1,11 @@
+Nepal, 2018-02-09
+
+I hereby agree to the terms of the Odoo Individual Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Subodh Dahal contact.subodhdahal@gmail.com https://github.com/SubodhDahal
diff --git a/doc/howtos/themes.rst b/doc/howtos/themes.rst
index 531f5e2c8f0..e2dc46f647b 100644
--- a/doc/howtos/themes.rst
+++ b/doc/howtos/themes.rst
@@ -614,7 +614,7 @@ The previous code will create the snippet’s content, but we still need to plac
.. code-block:: xml
-
+
diff --git a/odoo/addons/base/ir/ir_model.py b/odoo/addons/base/ir/ir_model.py
index c8f4c4122d0..4395de6cc9b 100644
--- a/odoo/addons/base/ir/ir_model.py
+++ b/odoo/addons/base/ir/ir_model.py
@@ -32,7 +32,7 @@ SAFE_EVAL_BASE = {
def make_compute(text, deps):
""" Return a compute function from its code body and dependencies. """
func = lambda self: safe_eval(text, SAFE_EVAL_BASE, {'self': self}, mode="exec")
- deps = [arg.strip() for arg in (deps or "").split(",")]
+ deps = [arg.strip() for arg in deps.split(",")] if deps else []
return api.depends(*deps)(func)
diff --git a/odoo/addons/base/ir/ir_qweb/fields.py b/odoo/addons/base/ir/ir_qweb/fields.py
index 4beec41db7c..28d60940978 100644
--- a/odoo/addons/base/ir/ir_qweb/fields.py
+++ b/odoo/addons/base/ir/ir_qweb/fields.py
@@ -173,7 +173,11 @@ class DateConverter(models.AbstractModel):
locale = babel.Locale.parse(lang.code)
if isinstance(value, basestring):
- value = fields.Datetime.from_string(value[:10])
+ if len(value) > 10: # datetime to be displayed as date
+ value = fields.Datetime.from_string(value)
+ value = fields.Datetime.context_timestamp(self, value)
+ else:
+ value = fields.Datetime.from_string(value)
if options and 'format' in options:
pattern = options['format']
diff --git a/odoo/addons/test_new_api/tests/test_new_fields.py b/odoo/addons/test_new_api/tests/test_new_fields.py
index b2ff0c1eb58..fd2ce1426c3 100644
--- a/odoo/addons/test_new_api/tests/test_new_fields.py
+++ b/odoo/addons/test_new_api/tests/test_new_fields.py
@@ -61,6 +61,19 @@ class TestFields(common.TransactionCase):
self.assertTrue(field.store)
self.assertTrue(field.readonly)
+ def test_10_computed_custom(self):
+ """ check definition of custom computed fields """
+ self.env['ir.model.fields'].create({
+ 'name': 'x_bool_false_computed',
+ 'model_id': self.env.ref('test_new_api.model_test_new_api_message').id,
+ 'field_description': 'A boolean computed to false',
+ 'compute': "for r in self: r['x_bool_false_computed'] = False",
+ 'store': False,
+ 'ttype': 'boolean'
+ })
+ field = self.env['test_new_api.message']._fields['x_bool_false_computed']
+ self.assertFalse(field.depends)
+
def test_10_non_stored(self):
""" test non-stored fields """
# a field declared with store=False should not have a column
diff --git a/odoo/http.py b/odoo/http.py
index 84298c20a63..0075b1bed34 100644
--- a/odoo/http.py
+++ b/odoo/http.py
@@ -1047,7 +1047,7 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
self.db = db
self.uid = uid
self.login = login
- self.session_token = uid and security.compute_session_token(self)
+ self.session_token = uid and security.compute_session_token(self, request.env)
request.uid = uid
request.disable_db = False
@@ -1062,15 +1062,17 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
"""
if not self.db or not self.uid:
raise SessionExpiredException("Session expired")
-
+ # We create our own environment instead of the request's one.
+ # to avoid creating it without the uid since request.uid isn't set yet
+ env = odoo.api.Environment(request.cr, self.uid, self.context)
# == BACKWARD COMPATIBILITY TO CONVERT OLD SESSION TYPE TO THE NEW ONES ! REMOVE ME AFTER 11.0 ==
if self.get('password'):
security.check(self.db, self.uid, self.password)
- self.session_token = security.compute_session_token(self)
+ self.session_token = security.compute_session_token(self, env)
self.pop('password')
# =================================================================================================
# here we check if the session is still valid
- if not security.check_session(self):
+ if not security.check_session(self, env):
raise SessionExpiredException("Session expired")
def logout(self, keep_db=False):
diff --git a/odoo/service/security.py b/odoo/service/security.py
index 229e4b84077..54d108fb4b1 100644
--- a/odoo/service/security.py
+++ b/odoo/service/security.py
@@ -12,15 +12,13 @@ def check(db, uid, passwd):
res_users = odoo.registry(db)['res.users']
return res_users.check(db, uid, passwd)
-def compute_session_token(session):
- with odoo.registry(session.db).cursor() as cr:
- self = odoo.api.Environment(cr, session.uid, {})['res.users'].browse(session.uid)
- return self._compute_session_token(session.sid)
+def compute_session_token(session, env):
+ self = env['res.users'].browse(session.uid)
+ return self._compute_session_token(session.sid)
-def check_session(session):
- with odoo.registry(session.db).cursor() as cr:
- self = odoo.api.Environment(cr, session.uid, {})['res.users'].browse(session.uid)
- if odoo.tools.misc.consteq(self._compute_session_token(session.sid), session.session_token):
- return True
- self._invalidate_session_cache()
- return False
+def check_session(session, env):
+ self = env['res.users'].browse(session.uid)
+ if odoo.tools.misc.consteq(self._compute_session_token(session.sid), session.session_token):
+ return True
+ self._invalidate_session_cache()
+ return False
diff --git a/odoo/tests/common.py b/odoo/tests/common.py
index 37000441306..5c88da20e57 100644
--- a/odoo/tests/common.py
+++ b/odoo/tests/common.py
@@ -288,7 +288,7 @@ class HttpCase(TransactionCase):
session.db = db
session.uid = uid
session.login = user
- session.session_token = uid and security.compute_session_token(session)
+ session.session_token = uid and security.compute_session_token(session, env)
session.context = env['res.users'].context_get() or {}
session.context['uid'] = uid
session._fix_lang(session.context)