-
+
+
-
-
-
+
+
+
+
+
-
+
-
-
diff --git a/addons/hr_expense/__manifest__.py b/addons/hr_expense/__manifest__.py
index ac6f28bc91e..46af9f9b4fa 100644
--- a/addons/hr_expense/__manifest__.py
+++ b/addons/hr_expense/__manifest__.py
@@ -40,6 +40,7 @@ This module also uses analytic accounting and is compatible with the invoice on
'views/hr_department_views.xml',
'views/assets.xml',
'views/res_config_settings_views.xml',
+ 'views/account_journal_dashboard.xml',
],
'demo': ['data/hr_expense_demo.xml'],
'installable': True,
diff --git a/addons/hr_holidays/models/hr.py b/addons/hr_holidays/models/hr.py
index a30175abbca..3cbf20badf5 100644
--- a/addons/hr_holidays/models/hr.py
+++ b/addons/hr_holidays/models/hr.py
@@ -220,3 +220,10 @@ class Employee(models.Model):
('date_to', '>=', today_start)
])
return [('id', 'in', holidays.mapped('employee_id').ids)]
+
+ def write(self, values):
+ res = super(Employee, self).write(values)
+ if 'parent_id' in values:
+ holidays = self.env['hr.holidays'].search([('state', 'in', ['draft', 'confirm']), ('employee_id', 'in', self.ids)])
+ holidays.write({'manager_id': values['parent_id']})
+ return res
diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py
index 30ebc057333..69d578ff281 100644
--- a/addons/hr_holidays/models/hr_leave.py
+++ b/addons/hr_holidays/models/hr_leave.py
@@ -62,7 +62,7 @@ class HolidaysRequest(models.Model):
domain="[('valid', '=', True), ('employee_applicability', 'in', ['leave', 'both'])]")
employee_id = fields.Many2one('hr.employee', string='Employee', index=True, readonly=True,
states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, default=_default_employee, track_visibility='onchange')
- manager_id = fields.Many2one('hr.employee', related='employee_id.parent_id', string='Manager', readonly=True, store=True)
+ manager_id = fields.Many2one('hr.employee', string='Manager', readonly=True)
notes = fields.Text('Reasons', readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]})
number_of_days_temp = fields.Float(
'Allocation', copy=False, readonly=True,
@@ -97,6 +97,10 @@ class HolidaysRequest(models.Model):
('date_check', "CHECK ( number_of_days_temp >= 0 )", "The number of days must be greater than 0."),
]
+ @api.onchange('employee_id')
+ def _onchange_employee_id(self):
+ self.manager_id = self.employee_id and self.employee_id.parent_id
+
@api.multi
@api.depends('number_of_days_temp')
def _compute_number_of_days(self):
@@ -242,6 +246,8 @@ class HolidaysRequest(models.Model):
values.update({'department_id': self.env['hr.employee'].browse(employee_id).department_id.id})
holiday = super(HolidaysRequest, self.with_context(mail_create_nolog=True, mail_create_nosubscribe=True)).create(values)
holiday.add_follower(employee_id)
+ if 'employee_id' in values:
+ holiday._onchange_employee_id()
return holiday
@api.multi
@@ -249,6 +255,8 @@ class HolidaysRequest(models.Model):
employee_id = values.get('employee_id', False)
result = super(HolidaysRequest, self).write(values)
self.add_follower(employee_id)
+ if 'employee_id' in values:
+ self._onchange_employee_id()
return result
@api.multi
diff --git a/addons/im_livechat/static/src/js/im_livechat.js b/addons/im_livechat/static/src/js/im_livechat.js
index e9d23227202..d652325d757 100644
--- a/addons/im_livechat/static/src/js/im_livechat.js
+++ b/addons/im_livechat/static/src/js/im_livechat.js
@@ -1,9 +1,9 @@
odoo.define('im_livechat.im_livechat', function (require) {
"use strict";
+var bus = require('bus.bus').bus;
var config = require('web.config');
var core = require('web.core');
-var ServiceProviderMixin = require('web.ServiceProviderMixin');
var session = require('web.session');
var time = require('web.time');
var utils = require('web.utils');
@@ -33,11 +33,7 @@ if (!_.contains(url_history, page)) {
utils.set_cookie(LIVECHAT_COOKIE_HISTORY, JSON.stringify(url_history), 60*60*24); // 1 day cookie
}
-/**
- * In order to handle services like ajax in the external lib,
- * The parent of Livechat, LivechatButton, is set as a service provider
- */
-var LivechatButton = Widget.extend(ServiceProviderMixin, {
+var LivechatButton = Widget.extend({
className:"openerp o_livechat_button hidden-print",
events: {
@@ -56,7 +52,7 @@ var LivechatButton = Widget.extend(ServiceProviderMixin, {
this.chat_window = null;
this.messages = [];
this.server_url = server_url;
- this.busBus = this.call('bus_service', 'getBus');
+ this.busBus = bus;
},
willStart: function () {
diff --git a/addons/im_livechat/views/im_livechat_channel_templates.xml b/addons/im_livechat/views/im_livechat_channel_templates.xml
index ec25a417dca..3c13ab09fd8 100644
--- a/addons/im_livechat/views/im_livechat_channel_templates.xml
+++ b/addons/im_livechat/views/im_livechat_channel_templates.xml
@@ -142,7 +142,6 @@
-
diff --git a/addons/link_tracker/i18n/link_tracker.pot b/addons/link_tracker/i18n/link_tracker.pot
index efc1285b23a..d14c2e2ac58 100644
--- a/addons/link_tracker/i18n/link_tracker.pot
+++ b/addons/link_tracker/i18n/link_tracker.pot
@@ -162,11 +162,6 @@ msgstr ""
msgid "Title and URL"
msgstr ""
-#. module: link_tracker
-#: model:ir.ui.view,arch_db:link_tracker.view_link_tracker_filter
-msgid "Tracked Link"
-msgstr ""
-
#. module: link_tracker
#: model:ir.model.fields,field_description:link_tracker.field_link_tracker_short_url
msgid "Tracked URL"
diff --git a/addons/mail/models/res_config_settings.py b/addons/mail/models/res_config_settings.py
index 881f7c20184..6cae4f3b0d5 100644
--- a/addons/mail/models/res_config_settings.py
+++ b/addons/mail/models/res_config_settings.py
@@ -21,21 +21,12 @@ class ResConfigSettings(models.TransientModel):
def get_values(self):
res = super(ResConfigSettings, self).get_values()
- alias_domain = res.get('alias_domain')
- if not alias_domain:
- domain = self.env["ir.config_parameter"].get_param("web.base.url")
- try:
- alias_domain = urls.url_parse(domain).host
- except Exception:
- pass
-
previous_date = datetime.datetime.now() - datetime.timedelta(days=30)
res.update(
fail_counter=self.env['mail.mail'].sudo().search_count([
('date', '>=', previous_date.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)),
('state', '=', 'exception')]),
- alias_domain=alias_domain or False,
)
return res
diff --git a/addons/mass_mailing/static/src/js/mass_mailing_editor.js b/addons/mass_mailing/static/src/js/mass_mailing_editor.js
index 8ebec45da2b..c7070e61a31 100644
--- a/addons/mass_mailing/static/src/js/mass_mailing_editor.js
+++ b/addons/mass_mailing/static/src/js/mass_mailing_editor.js
@@ -89,11 +89,11 @@ options.registry.sizing_x = options.Class.extend({
});
options.registry.table_item = options.Class.extend({
- onClone: function () {
+ onClone: function (options) {
this._super.apply(this, arguments);
// If we cloned a td or th element...
- if (this.$target.is("td, th")) {
+ if (options.isCurrent && this.$target.is("td, th")) {
// ... and that the td or th element was alone on its row ...
if (this.$target.siblings().length === 1) {
var $tr = this.$target.parent();
diff --git a/addons/mass_mailing/static/src/less/themes/theme_default.less b/addons/mass_mailing/static/src/less/themes/theme_default.less
index 898174c8b37..aba67dbeb64 100644
--- a/addons/mass_mailing/static/src/less/themes/theme_default.less
+++ b/addons/mass_mailing/static/src/less/themes/theme_default.less
@@ -39,6 +39,11 @@
background-color: @o_mm_def_color_alpha;
}
+td {
+ // Default browser style but needed so that alignment works on some mail
+ // clients (see transcoder)
+ text-align: inherit;
+}
// ===== Layout =====
.o_layout {
diff --git a/addons/mass_mailing/views/link_tracker_views.xml b/addons/mass_mailing/views/link_tracker_views.xml
index 724f08cc8a4..f7ea013cefd 100644
--- a/addons/mass_mailing/views/link_tracker_views.xml
+++ b/addons/mass_mailing/views/link_tracker_views.xml
@@ -19,7 +19,7 @@
link.tracker
-
+
diff --git a/addons/mrp/models/mrp_workorder.py b/addons/mrp/models/mrp_workorder.py
index 9a3f093a87d..6a6f154594c 100644
--- a/addons/mrp/models/mrp_workorder.py
+++ b/addons/mrp/models/mrp_workorder.py
@@ -345,13 +345,14 @@ class MrpWorkorder(models.Model):
# If last work order, then post lots used
# TODO: should be same as checking if for every workorder something has been done?
if not self.next_work_order_id:
- production_move = self.production_id.move_finished_ids.filtered(lambda x: (x.product_id.id == self.production_id.product_id.id) and (x.state not in ('done', 'cancel')))
- if production_move.has_tracking != 'none':
- move_line = production_move.move_line_ids.filtered(lambda x: x.lot_id.id == self.final_lot_id.id)
- if move_line:
- move_line.product_uom_qty += self.qty_producing
- else:
- move_line.create({'move_id': production_move.id,
+ production_moves = self.production_id.move_finished_ids.filtered(lambda x: (x.state not in ('done', 'cancel')))
+ for production_move in production_moves:
+ if production_move.product_id.id == self.production_id.product_id.id and production_move.has_tracking != 'none':
+ move_line = production_move.move_line_ids.filtered(lambda x: x.lot_id.id == self.final_lot_id.id)
+ if move_line:
+ move_line.product_uom_qty += self.qty_producing
+ else:
+ move_line.create({'move_id': production_move.id,
'product_id': production_move.product_id.id,
'lot_id': self.final_lot_id.id,
'product_uom_qty': self.qty_producing,
@@ -360,9 +361,12 @@ class MrpWorkorder(models.Model):
'workorder_id': self.id,
'location_id': production_move.location_id.id,
'location_dest_id': production_move.location_dest_id.id,
- })
- else:
- production_move.quantity_done += self.qty_producing
+ })
+ elif production_move.unit_factor:
+ rounding = production_move.product_uom.rounding
+ production_move.quantity_done += float_round(self.qty_producing * production_move.unit_factor, precision_rounding=rounding)
+ else:
+ production_move.quantity_done += self.qty_producing
if not self.next_work_order_id:
for by_product_move in self.production_id.move_finished_ids.filtered(lambda x: (x.product_id.id != self.production_id.product_id.id) and (x.state not in ('done', 'cancel'))):
diff --git a/addons/payment_paypal/controllers/main.py b/addons/payment_paypal/controllers/main.py
index 951799917a4..e14b7d59ca6 100644
--- a/addons/payment_paypal/controllers/main.py
+++ b/addons/payment_paypal/controllers/main.py
@@ -63,7 +63,7 @@ class PaypalController(http.Controller):
Once data is validated, process it. """
res = False
- new_post = dict(post, cmd='_notify-validate', charset='utf-8')
+ new_post = dict(post, cmd='_notify-validate', charset='UTF-8')
reference = post.get('item_number')
tx = None
if reference:
diff --git a/addons/point_of_sale/__init__.py b/addons/point_of_sale/__init__.py
index ee87c280f2c..0c674f48dfc 100644
--- a/addons/point_of_sale/__init__.py
+++ b/addons/point_of_sale/__init__.py
@@ -5,3 +5,4 @@ from . import models
from . import controllers
from . import report
from . import wizard
+from . import tests
diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py
index 5ef49361c51..cad950c576e 100644
--- a/addons/point_of_sale/models/pos_order.py
+++ b/addons/point_of_sale/models/pos_order.py
@@ -245,6 +245,41 @@ class PosOrder(models.Model):
have_to_group_by = session and session.config_id.group_by or False
rounding_method = session and session.config_id.company_id.tax_calculation_rounding_method
+ def add_anglosaxon_lines(grouped_data):
+ Product = self.env['product.product']
+ Analytic = self.env['account.analytic.account']
+ for product_key in list(grouped_data.keys()):
+ if product_key[0] == "product":
+ line = grouped_data[product_key][0]
+ product = Product.browse(line['product_id'])
+ # In the SO part, the entries will be inverted by function compute_invoice_totals
+ price_unit = - product._get_anglo_saxon_price_unit()
+ account_analytic = Analytic.browse(line.get('analytic_account_id'))
+ res = Product._anglo_saxon_sale_move_lines(
+ line['name'], product, product.uom_id, line['quantity'], price_unit,
+ fiscal_position=order.fiscal_position_id,
+ account_analytic=account_analytic)
+ if res:
+ line1, line2 = res
+ line1 = Product._convert_prepared_anglosaxon_line(line1, order.partner_id)
+ insert_data('counter_part', {
+ 'name': line1['name'],
+ 'account_id': line1['account_id'],
+ 'credit': line1['credit'] or 0.0,
+ 'debit': line1['debit'] or 0.0,
+ 'partner_id': line1['partner_id']
+
+ })
+
+ line2 = Product._convert_prepared_anglosaxon_line(line2, order.partner_id)
+ insert_data('counter_part', {
+ 'name': line2['name'],
+ 'account_id': line2['account_id'],
+ 'credit': line2['credit'] or 0.0,
+ 'debit': line2['debit'] or 0.0,
+ 'partner_id': line2['partner_id']
+ })
+
for order in self.filtered(lambda o: not o.account_move or o.state == 'paid'):
current_company = order.sale_journal.company_id
account_def = IrProperty.get(
@@ -360,6 +395,9 @@ class PosOrder(models.Model):
order.write({'state': 'done', 'account_move': move.id})
+ if self and order.company_id.anglo_saxon_accounting:
+ add_anglosaxon_lines(grouped_data)
+
all_lines = []
for group_key, group_data in grouped_data.items():
for value in group_data:
diff --git a/addons/point_of_sale/tests/__init__.py b/addons/point_of_sale/tests/__init__.py
index 8a93c4c4477..24294101606 100644
--- a/addons/point_of_sale/tests/__init__.py
+++ b/addons/point_of_sale/tests/__init__.py
@@ -4,3 +4,4 @@
from . import test_point_of_sale_flow
from . import test_frontend
from . import test_point_of_sale_ui
+from . import test_anglo_saxon
diff --git a/addons/point_of_sale/tests/test_anglo_saxon.py b/addons/point_of_sale/tests/test_anglo_saxon.py
new file mode 100644
index 00000000000..b6d27af9505
--- /dev/null
+++ b/addons/point_of_sale/tests/test_anglo_saxon.py
@@ -0,0 +1,101 @@
+# -*- coding: utf-8 -*-
+
+import openerp
+import time
+from openerp import fields
+
+from openerp.tests import common
+
+
+class TestAngloSaxonCommon(common.TransactionCase):
+
+ def setUp(self):
+ super(TestAngloSaxonCommon, self).setUp()
+ self.PosMakePayment = self.env['pos.make.payment']
+ self.PosOrder = self.env['pos.order']
+ self.Statement = self.env['account.bank.statement']
+ self.company = self.env.ref('base.main_company')
+ self.product = self.env.ref('product.product_product_3')
+ self.partner = self.env.ref('base.res_partner_1')
+ self.category = self.env.ref('product.product_category_1')
+ self.category = self.category.copy({'name': 'New category','property_valuation': 'real_time'})
+ account_type_rcv = self.env['account.account.type'].create({'name': 'RCV type', 'type': 'receivable'})
+ account_type_oth = self.env['account.account.type'].create({'name': 'RCV type', 'type': 'other'})
+ self.account = self.env['account.account'].create({'name': 'Receivable', 'code': 'RCV00' , 'user_type_id': account_type_rcv.id, 'reconcile': True})
+ account_expense = self.env['account.account'].create({'name': 'Expense', 'code': 'EXP00' , 'user_type_id': account_type_oth.id, 'reconcile': True})
+ account_output = self.env['account.account'].create({'name': 'Output', 'code': 'OUT00' , 'user_type_id': account_type_oth.id, 'reconcile': True})
+ self.partner.property_account_receivable_id = self.account
+ self.category.property_account_income_categ_id = self.account
+ self.category.property_account_expense_categ_id = account_expense
+ self.category.property_stock_account_input_categ_id = self.account
+ self.category.property_stock_account_output_categ_id = account_output
+ self.category.property_stock_valuation_account_id = self.account
+ self.category.property_stock_journal = self.env['account.journal'].create({'name': 'Stock journal', 'type': 'sale', 'code': 'STK00'})
+ self.pos_config = self.env.ref('point_of_sale.pos_config_main')
+ self.pos_config = self.pos_config.copy({'name': 'New POS config'})
+ self.product = self.product.copy({'name': 'New product','standard_price': 100})
+ self.company.anglo_saxon_accounting = True
+ self.product.categ_id = self.category
+ sale_journal = self.env['account.journal'].create({'name': 'POS journal', 'type': 'sale', 'code': 'POS00'})
+ self.pos_config.journal_id = sale_journal
+ self.cash_journal = self.env['account.journal'].create({'name': 'CASH journal', 'type': 'cash', 'code': 'CSH00'})
+
+
+class TestAngloSaxonFlow(TestAngloSaxonCommon):
+
+ def test_create_account_move_line(self):
+ # This test will check that the correct journal entries are created when a product in real time valuation
+ # is sold in a company using anglo-saxon
+ self.pos_config.open_session_cb()
+ self.pos_config.current_session_id.write({'journal_ids': [(6, 0, [self.cash_journal.id])]})
+ self.cash_journal.loss_account_id = self.account
+ self.pos_statement = self.Statement.create({
+ 'balance_start': 0.0,
+ 'balance_end_real': 0.0,
+ 'date': time.strftime('%Y-%m-%d'),
+ 'journal_id': self.cash_journal.id,
+ 'company_id': self.company.id,
+ 'name': 'pos session test',
+ })
+ self.pos_config.current_session_id.write({'statement_ids': [(6, 0, [self.pos_statement.id])]})
+
+ # I create a PoS order with 1 unit of New product at 450 EUR
+ self.pos_order_pos0 = self.PosOrder.create({
+ 'company_id': self.company.id,
+ 'partner_id': self.partner.id,
+ 'pricelist_id': self.company.partner_id.property_product_pricelist.id,
+ 'session_id': self.pos_config.current_session_id.id,
+ 'lines': [(0, 0, {
+ 'name': "OL/0001",
+ 'product_id': self.product.id,
+ 'price_unit': 450,
+ 'discount': 0.0,
+ 'qty': 1.0
+ })]
+ })
+
+ # I make a payment to fully pay the order
+ context_make_payment = {"active_ids": [self.pos_order_pos0.id], "active_id": self.pos_order_pos0.id}
+ self.pos_make_payment_0 = self.PosMakePayment.with_context(context_make_payment).create({
+ 'amount': 450.0,
+ 'journal_id': self.cash_journal.id,
+ })
+
+ # I click on the validate button to register the payment.
+ context_payment = {'active_id': self.pos_order_pos0.id}
+ self.pos_make_payment_0.with_context(context_payment).check()
+
+ # I check that the order is marked as paid
+ self.assertEqual(self.pos_order_pos0.state, 'paid', 'Order should be in paid state.')
+
+ # I close the current session to generate the journal entries
+ self.pos_config.current_session_id.action_pos_session_close()
+
+ # I test that the generated journal entries are correct.
+ account_output = self.category.property_stock_account_output_categ_id
+ expense_account = self.category.property_account_expense_categ_id
+ aml = self.pos_order_pos0.account_move.line_ids
+ aml_output = aml.filtered(lambda l: l.account_id.id == account_output.id)
+ aml_expense = aml.filtered(lambda l: l.account_id.id == expense_account.id)
+ self.assertEqual(aml_output.credit, self.product.standard_price, "Cost of Good Sold entry missing or mismatching")
+ self.assertEqual(aml_expense.debit, self.product.standard_price, "Cost of Good Sold entry missing or mismatching")
\ No newline at end of file
diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py
index c00265e711e..4f263d17ebf 100644
--- a/addons/product/models/product_template.py
+++ b/addons/product/models/product_template.py
@@ -416,7 +416,7 @@ class ProductTemplate(models.Model):
for tmpl_id in self.with_context(active_test=False):
# adding an attribute with only one value should not recreate product
# write this attribute on every product to make sure we don't lose them
- variant_alone = tmpl_id.attribute_line_ids.filtered(lambda line: len(line.value_ids) == 1).mapped('value_ids')
+ variant_alone = tmpl_id.attribute_line_ids.filtered(lambda line: line.attribute_id.create_variant and len(line.value_ids) == 1).mapped('value_ids')
for value_id in variant_alone:
updated_products = tmpl_id.product_variant_ids.filtered(lambda product: value_id.attribute_id not in product.mapped('attribute_value_ids.attribute_id'))
updated_products.write({'attribute_value_ids': [(4, value_id.id)]})
diff --git a/addons/product/tests/test_variants.py b/addons/product/tests/test_variants.py
index 813f5f9e28d..1a621d689cb 100644
--- a/addons/product/tests/test_variants.py
+++ b/addons/product/tests/test_variants.py
@@ -4,10 +4,10 @@
from . import common
from odoo.tests.common import TransactionCase
-class TestVariants(TransactionCase):
+class TestVariantsSearch(TransactionCase):
def setUp(self):
- res = super(TestVariants, self).setUp()
+ res = super(TestVariantsSearch, self).setUp()
self.size_attr = self.env['product.attribute'].create({'name': 'Size'})
self.size_attr_value_s = self.env['product.attribute.value'].create({'name': 'S', 'attribute_id': self.size_attr.id})
self.size_attr_value_m = self.env['product.attribute.value'].create({'name': 'M', 'attribute_id': self.size_attr.id})
@@ -152,3 +152,181 @@ class TestVariants(common.TestProductCommon):
'value_ids': [(4, self.size_attr_value_l.id)],
})]
})
+
+
+class TestVariantsNoCreate(common.TestProductCommon):
+
+ def setUp(self):
+ super(TestVariantsNoCreate, self).setUp()
+ self.size = self.env['product.attribute'].create({
+ 'name': 'Size',
+ 'create_variant': False,
+ 'value_ids': [(0, 0, {'name': 'S'}), (0, 0, {'name': 'M'}), (0, 0, {'name': 'L'})],
+ })
+ self.size_S = self.size.value_ids[0]
+ self.size_M = self.size.value_ids[1]
+ self.size_L = self.size.value_ids[2]
+
+ def test_create_mono(self):
+ """ create a product with a 'nocreate' attribute with a single value """
+ template = self.env['product.template'].create({
+ 'name': 'Sofa',
+ 'uom_id': self.uom_unit.id,
+ 'uom_po_id': self.uom_unit.id,
+ 'attribute_line_ids': [(0, 0, {
+ 'attribute_id': self.size.id,
+ 'value_ids': [(4, self.size_S.id)],
+ })],
+ })
+ self.assertEqual(len(template.product_variant_ids), 1)
+ self.assertFalse(template.product_variant_ids.attribute_value_ids)
+
+ def test_update_mono(self):
+ """ modify a product with a 'nocreate' attribute with a single value """
+ template = self.env['product.template'].create({
+ 'name': 'Sofa',
+ 'uom_id': self.uom_unit.id,
+ 'uom_po_id': self.uom_unit.id,
+ })
+ self.assertEqual(len(template.product_variant_ids), 1)
+
+ template.write({
+ 'attribute_line_ids': [(0, 0, {
+ 'attribute_id': self.size.id,
+ 'value_ids': [(4, self.size_S.id)],
+ })],
+ })
+ self.assertEqual(len(template.product_variant_ids), 1)
+ self.assertFalse(template.product_variant_ids.attribute_value_ids)
+
+ def test_create_multi(self):
+ """ create a product with a 'nocreate' attribute with several values """
+ template = self.env['product.template'].create({
+ 'name': 'Sofa',
+ 'uom_id': self.uom_unit.id,
+ 'uom_po_id': self.uom_unit.id,
+ 'attribute_line_ids': [(0, 0, {
+ 'attribute_id': self.size.id,
+ 'value_ids': [(6, 0, self.size.value_ids.ids)],
+ })],
+ })
+ self.assertEqual(len(template.product_variant_ids), 1)
+ self.assertFalse(template.product_variant_ids.attribute_value_ids)
+
+ def test_update_multi(self):
+ """ modify a product with a 'nocreate' attribute with several values """
+ template = self.env['product.template'].create({
+ 'name': 'Sofa',
+ 'uom_id': self.uom_unit.id,
+ 'uom_po_id': self.uom_unit.id,
+ })
+ self.assertEqual(len(template.product_variant_ids), 1)
+
+ template.write({
+ 'attribute_line_ids': [(0, 0, {
+ 'attribute_id': self.size.id,
+ 'value_ids': [(6, 0, self.size.value_ids.ids)],
+ })],
+ })
+ self.assertEqual(len(template.product_variant_ids), 1)
+ self.assertFalse(template.product_variant_ids.attribute_value_ids)
+
+ def test_create_mixed_mono(self):
+ """ create a product with regular and 'nocreate' attributes """
+ template = self.env['product.template'].create({
+ 'name': 'Sofa',
+ 'uom_id': self.uom_unit.id,
+ 'uom_po_id': self.uom_unit.id,
+ 'attribute_line_ids': [
+ (0, 0, { # no variants for this one
+ 'attribute_id': self.size.id,
+ 'value_ids': [(4, self.size_S.id)],
+ }),
+ (0, 0, { # two variants for this one
+ 'attribute_id': self.prod_att_1.id,
+ 'value_ids': [(4, self.prod_attr1_v1.id), (4, self.prod_attr1_v2.id)],
+ }),
+ ],
+ })
+ self.assertEqual(len(template.product_variant_ids), 2)
+ self.assertEqual(
+ {variant.attribute_value_ids for variant in template.product_variant_ids},
+ {self.prod_attr1_v1, self.prod_attr1_v2},
+ )
+
+ def test_update_mixed_mono(self):
+ """ modify a product with regular and 'nocreate' attributes """
+ template = self.env['product.template'].create({
+ 'name': 'Sofa',
+ 'uom_id': self.uom_unit.id,
+ 'uom_po_id': self.uom_unit.id,
+ })
+ self.assertEqual(len(template.product_variant_ids), 1)
+
+ template.write({
+ 'attribute_line_ids': [
+ (0, 0, { # no variants for this one
+ 'attribute_id': self.size.id,
+ 'value_ids': [(4, self.size_S.id)],
+ }),
+ (0, 0, { # two variants for this one
+ 'attribute_id': self.prod_att_1.id,
+ 'value_ids': [(4, self.prod_attr1_v1.id), (4, self.prod_attr1_v2.id)],
+ }),
+ ],
+ })
+ self.assertEqual(len(template.product_variant_ids), 2)
+ self.assertEqual(
+ {variant.attribute_value_ids for variant in template.product_variant_ids},
+ {self.prod_attr1_v1, self.prod_attr1_v2},
+ )
+
+ def test_create_mixed_multi(self):
+ """ create a product with regular and 'nocreate' attributes """
+ template = self.env['product.template'].create({
+ 'name': 'Sofa',
+ 'uom_id': self.uom_unit.id,
+ 'uom_po_id': self.uom_unit.id,
+ 'attribute_line_ids': [
+ (0, 0, { # no variants for this one
+ 'attribute_id': self.size.id,
+ 'value_ids': [(6, 0, self.size.value_ids.ids)],
+ }),
+ (0, 0, { # two variants for this one
+ 'attribute_id': self.prod_att_1.id,
+ 'value_ids': [(4, self.prod_attr1_v1.id), (4, self.prod_attr1_v2.id)],
+ }),
+ ],
+ })
+ self.assertEqual(len(template.product_variant_ids), 2)
+ self.assertEqual(
+ {variant.attribute_value_ids for variant in template.product_variant_ids},
+ {self.prod_attr1_v1, self.prod_attr1_v2},
+ )
+
+ def test_update_mixed_multi(self):
+ """ modify a product with regular and 'nocreate' attributes """
+ template = self.env['product.template'].create({
+ 'name': 'Sofa',
+ 'uom_id': self.uom_unit.id,
+ 'uom_po_id': self.uom_unit.id,
+ })
+ self.assertEqual(len(template.product_variant_ids), 1)
+
+ template.write({
+ 'attribute_line_ids': [
+ (0, 0, { # no variants for this one
+ 'attribute_id': self.size.id,
+ 'value_ids': [(6, 0, self.size.value_ids.ids)],
+ }),
+ (0, 0, { # two variants for this one
+ 'attribute_id': self.prod_att_1.id,
+ 'value_ids': [(4, self.prod_attr1_v1.id), (4, self.prod_attr1_v2.id)],
+ }),
+ ],
+ })
+ self.assertEqual(len(template.product_variant_ids), 2)
+ self.assertEqual(
+ {variant.attribute_value_ids for variant in template.product_variant_ids},
+ {self.prod_attr1_v1, self.prod_attr1_v2},
+ )
diff --git a/addons/project/models/project.py b/addons/project/models/project.py
index 8de21782852..4b824ded373 100644
--- a/addons/project/models/project.py
+++ b/addons/project/models/project.py
@@ -318,7 +318,11 @@ class Project(models.Model):
@api.multi
def write(self, vals):
- res = super(Project, self).write(vals)
+ # directly compute is_favorite to dodge allow write access right
+ if 'is_favorite' in vals:
+ vals.pop('is_favorite')
+ self._fields['is_favorite'].determine_inverse(self)
+ res = super(Project, self).write(vals) if vals else True
if 'active' in vals:
# archiving/unarchiving a project does it on its tasks, too
self.with_context(active_test=False).mapped('tasks').write({'active': vals['active']})
diff --git a/addons/project/tests/test_access_rights.py b/addons/project/tests/test_access_rights.py
index 21278b287d4..0016a71f71f 100644
--- a/addons/project/tests/test_access_rights.py
+++ b/addons/project/tests/test_access_rights.py
@@ -53,6 +53,15 @@ class TestPortalProject(TestPortalProjectBase):
tmp_task.sudo(self.user_projectuser).unlink()
@mute_logger('odoo.addons.base.models.ir_model')
+ def test_favorite_project_access_rights(self):
+ pigs = self.project_pigs.sudo(self.user_projectuser)
+
+ # we can't write on project name
+ self.assertRaises(AccessError, pigs.write, {'name': 'False Pigs'})
+ # we can write on is_favorite
+ pigs.write({'is_favorite': True})
+
+ @mute_logger('odoo.addons.base.ir.ir_model')
def test_followers_project_access_rights(self):
pigs = self.project_pigs
pigs.write({'privacy_visibility': 'followers'})
diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml
index 37b2880bb59..20e16200a9e 100644
--- a/addons/project/views/project_views.xml
+++ b/addons/project/views/project_views.xml
@@ -341,7 +341,7 @@
-
+
diff --git a/addons/resource/models/resource.py b/addons/resource/models/resource.py
index 2182e4f5adb..c5c010374c5 100644
--- a/addons/resource/models/resource.py
+++ b/addons/resource/models/resource.py
@@ -311,7 +311,7 @@ class ResourceCalendar(models.Model):
domain += [('date_to', '>', fields.Datetime.to_string(start_datetime + timedelta(days=-1)))]
if end_datetime:
# domain += [('date_from', '<', fields.Datetime.to_string(to_naive_utc(end_datetime, self.env.user)))]
- domain += [('date_from', '<', fields.Datetime.to_string(start_datetime + timedelta(days=1)))]
+ domain += [('date_from', '<', fields.Datetime.to_string(end_datetime + timedelta(days=1)))]
leaves = self.env['resource.calendar.leaves'].search(domain + [('calendar_id', '=', self.id)])
filtered_leaves = self.env['resource.calendar.leaves']
diff --git a/addons/stock_account/models/account_invoice.py b/addons/stock_account/models/account_invoice.py
index db13282be1e..9547c0625e7 100644
--- a/addons/stock_account/models/account_invoice.py
+++ b/addons/stock_account/models/account_invoice.py
@@ -28,54 +28,15 @@ class AccountInvoice(models.Model):
"""
inv = i_line.invoice_id
company_currency = inv.company_id.currency_id
+ price_unit = i_line._get_anglo_saxon_price_unit()
+ if inv.currency_id != company_currency:
+ currency_id = inv.currency_id.id
+ amount_currency = i_line._get_price(company_currency, price_unit)
+ else:
+ currency_id = False
+ amount_currency = False
- if i_line.product_id.type == 'product' and i_line.product_id.valuation == 'real_time':
- fpos = i_line.invoice_id.fiscal_position_id
- accounts = i_line.product_id.product_tmpl_id.get_product_accounts(fiscal_pos=fpos)
- # debit account dacc will be the output account
- dacc = accounts['stock_output'].id
- # credit account cacc will be the expense account
- cacc = accounts['expense'].id
- if dacc and cacc:
- price_unit = i_line._get_anglo_saxon_price_unit()
- if inv.currency_id != company_currency:
- currency_id = inv.currency_id.id
- amount_currency = i_line._get_price(company_currency, price_unit)
- else:
- currency_id = False
- amount_currency = False
- return [
- {
- 'type': 'src',
- 'name': i_line.name[:64],
- 'price_unit': price_unit,
- 'quantity': i_line.quantity,
- 'price': price_unit * i_line.quantity,
- 'currency_id': currency_id,
- 'amount_currency': amount_currency,
- 'account_id':dacc,
- 'product_id':i_line.product_id.id,
- 'uom_id':i_line.uom_id.id,
- 'account_analytic_id': i_line.account_analytic_id.id,
- 'analytic_tag_ids': i_line.analytic_tag_ids.ids and [(6, 0, i_line.analytic_tag_ids.ids)] or False,
- },
-
- {
- 'type': 'src',
- 'name': i_line.name[:64],
- 'price_unit': price_unit,
- 'quantity': i_line.quantity,
- 'price': -1 * price_unit * i_line.quantity,
- 'currency_id': currency_id,
- 'amount_currency': -1 * amount_currency,
- 'account_id':cacc,
- 'product_id':i_line.product_id.id,
- 'uom_id':i_line.uom_id.id,
- 'account_analytic_id': i_line.account_analytic_id.id,
- 'analytic_tag_ids': i_line.analytic_tag_ids.ids and [(6, 0, i_line.analytic_tag_ids.ids)] or False,
- },
- ]
- return []
+ return self.env['product.product']._anglo_saxon_sale_move_lines(i_line.name, i_line.product_id, i_line.uom_id, i_line.quantity, price_unit, currency=currency_id, amount_currency=amount_currency, fiscal_position=inv.fiscal_position_id, account_analytic=i_line.account_analytic_id, analytic_tags=i_line.analytic_tag_ids)
class AccountInvoiceLine(models.Model):
@@ -83,11 +44,7 @@ class AccountInvoiceLine(models.Model):
def _get_anglo_saxon_price_unit(self):
self.ensure_one()
- price = self.product_id.standard_price
- if not self.uom_id or self.product_id.uom_id == self.uom_id:
- return price
- else:
- return self.product_id.uom_id._compute_price(price, self.uom_id)
+ return self.product_id._get_anglo_saxon_price_unit(uom=self.uom_id)
def _get_price(self, company_currency, price_unit):
if self.invoice_id.currency_id.id != company_currency.id:
diff --git a/addons/stock_account/models/product.py b/addons/stock_account/models/product.py
index d60f889b1af..93bc4c980ae 100644
--- a/addons/stock_account/models/product.py
+++ b/addons/stock_account/models/product.py
@@ -197,6 +197,70 @@ class ProductProduct(models.Model):
}
return action
+ @api.model
+ def _anglo_saxon_sale_move_lines(self, name, product, uom, qty, price_unit, currency=False, amount_currency=False, fiscal_position=False, account_analytic=False, analytic_tags=False):
+ """Prepare dicts describing new journal COGS journal items for a product sale.
+
+ Returns a dict that should be passed to `_convert_prepared_anglosaxon_line()` to
+ obtain the creation value for the new journal items.
+
+ :param Model product: a product.product record of the product being sold
+ :param Model uom: a product.uom record of the UoM of the sale line
+ :param Integer qty: quantity of the product being sold
+ :param Integer price_unit: unit price of the product being sold
+ :param Model currency: a res.currency record from the order of the product being sold
+ :param Interger amount_currency: unit price in the currency from the order of the product being sold
+ :param Model fiscal_position: a account.fiscal.position record from the order of the product being sold
+ :param Model account_analytic: a account.account.analytic record from the line of the product being sold
+ """
+
+ if product.type == 'product' and product.valuation == 'real_time':
+ accounts = product.product_tmpl_id.get_product_accounts(fiscal_pos=fiscal_position)
+ # debit account dacc will be the output account
+ dacc = accounts['stock_output'].id
+ # credit account cacc will be the expense account
+ cacc = accounts['expense'].id
+ if dacc and cacc:
+ return [
+ {
+ 'type': 'src',
+ 'name': name[:64],
+ 'price_unit': price_unit,
+ 'quantity': qty,
+ 'price': price_unit * qty,
+ 'currency_id': currency and currency.id,
+ 'amount_currency': amount_currency,
+ 'account_id': dacc,
+ 'product_id': product.id,
+ 'uom_id': uom.id,
+ 'account_analytic_id': account_analytic and account_analytic.id,
+ 'analytic_tag_ids': analytic_tags and analytic_tag.ids and [(6, 0, analytic_tag.ids)] or False,
+ },
+
+ {
+ 'type': 'src',
+ 'name': name[:64],
+ 'price_unit': price_unit,
+ 'quantity': qty,
+ 'price': -1 * price_unit * qty,
+ 'currency_id': currency and currency.id,
+ 'amount_currency': -1 * amount_currency,
+ 'account_id': cacc,
+ 'product_id': product.id,
+ 'uom_id': uom.id,
+ 'account_analytic_id': account_analytic and account_analytic.id,
+ 'analytic_tag_ids': analytic_tags and analytic_tags.ids and [(6, 0, analytic_tags.ids)] or False,
+ },
+ ]
+ return []
+
+ @api.model
+ def _get_anglo_saxon_price_unit(self, uom=False):
+ price = self.standard_price
+ if not uom or self.uom_id.id == uom.id:
+ return price
+ return self.uom_id._compute_price(self.uom_id.id, price, to_uom_id=uom.id)
+
class ProductCategory(models.Model):
_inherit = 'product.category'
diff --git a/addons/web/static/src/js/chrome/action_manager.js b/addons/web/static/src/js/chrome/action_manager.js
index cea75c56726..237ce1f1c96 100644
--- a/addons/web/static/src/js/chrome/action_manager.js
+++ b/addons/web/static/src/js/chrome/action_manager.js
@@ -172,20 +172,12 @@ var ActionManager = Widget.extend({
}
return this.dp.add($.when(def)).then(function () {
- action.jsID = _.uniqueId('action_');
- action.pushState = options.pushState;
-
// action.target 'main' is equivalent to 'current' except that it
// also clears the breadcrumbs
options.clear_breadcrumbs = action.target === 'main' ||
options.clear_breadcrumbs;
- // ensure that the context and domain are evaluated
- var context = new Context(self.userContext, options.additional_context, action.context);
- action.context = pyeval.eval('context', context);
- if (action.domain) {
- action.domain = pyeval.eval('domain', action.domain, action.context);
- }
+ self._preprocessAction(action, options);
return self._handleAction(action, options);
});
@@ -757,6 +749,24 @@ var ActionManager = Widget.extend({
});
return def;
},
+ /**
+ * Preprocesses the action before it is handled by the ActionManager
+ * (assigns a JS id, evaluates its context and domains, etc.).
+ *
+ * @param {Object} action
+ * @param {Object} options see @doAction options
+ */
+ _preprocessAction: function (action, options) {
+ action.jsID = _.uniqueId('action_');
+ action.pushState = options.pushState;
+
+ // ensure that the context and domain are evaluated
+ var context = new Context(this.userContext, options.additional_context, action.context);
+ action.context = pyeval.eval('context', context);
+ if (action.domain) {
+ action.domain = pyeval.eval('domain', action.domain, action.context);
+ }
+ },
/**
* Unlinks the given action and its controller from the internal structures
* and destroys its controllers.
diff --git a/addons/web_editor/static/src/js/editor/transcoder.js b/addons/web_editor/static/src/js/editor/transcoder.js
index 6fd57c0330b..1a77b3d19e7 100644
--- a/addons/web_editor/static/src/js/editor/transcoder.js
+++ b/addons/web_editor/static/src/js/editor/transcoder.js
@@ -18,18 +18,12 @@ function getMatchedCSSRules(a) {
var sheets = document.styleSheets;
for (i = sheets.length-1 ; i >= 0 ; i--) {
var rules;
- if (sheets[i].hasOwnProperty('rules')) {
- rules = sheets[i].rules;
- } else {
- // try...catch because Firefox not able to enumerate
- // document.styleSheets[].cssRules[] for cross-domain sheets.
- try {
- rules = sheets[i].cssRules;
- } catch (e) {
- console.warn("Can't read the css rules of: " + sheets[i].href, e);
- continue;
- }
- rules = sheets[i].cssRules;
+ // try...catch because browser may not able to enumerate rules for cross-domain sheets
+ try {
+ rules = sheets[i].rules || sheets[i].cssRules;
+ } catch (e) {
+ console.warn("Can't read the css rules of: " + sheets[i].href, e);
+ continue;
}
if (rules) {
for (r = rules.length-1; r >= 0; r--) {
@@ -143,6 +137,20 @@ function getMatchedCSSRules(a) {
delete style['text-decoration-style'];
}
+ // text-align inheritance does not seem to get past
elements on some
+ // mail clients
+ if (style['text-align'] === 'inherit') {
+ var $el = $(a).parent();
+ do {
+ var align = $el.css('text-align');
+ if (_.indexOf(['left', 'right', 'center', 'justify'], align) >= 0) {
+ style['text-align'] = align;
+ break;
+ }
+ $el = $el.parent();
+ } while (!$el.is('html'));
+ }
+
return style;
}
diff --git a/addons/website_event/models/event.py b/addons/website_event/models/event.py
index 8f31c1505e1..d8d3f03f802 100644
--- a/addons/website_event/models/event.py
+++ b/addons/website_event/models/event.py
@@ -21,7 +21,7 @@ class Event(models.Model):
website_menu = fields.Boolean('Dedicated Menu',
help="Creates menus Introduction, Location and Register on the page "
- " of the event on the website.")
+ " of the event on the website.", copy=False)
menu_id = fields.Many2one('website.menu', 'Event Menu')
def _compute_is_participating(self):
diff --git a/addons/website_livechat/views/website_livechat.xml b/addons/website_livechat/views/website_livechat.xml
index 3a994c4e4ac..3a0caa340ba 100644
--- a/addons/website_livechat/views/website_livechat.xml
+++ b/addons/website_livechat/views/website_livechat.xml
@@ -9,7 +9,6 @@
-
diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml
index ab59f8ffed9..07c5c79e634 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -846,7 +846,7 @@
-
+
@@ -932,7 +932,7 @@
- Qty:
+ Qty:
@@ -1298,7 +1298,7 @@
-
+
@@ -1416,7 +1416,7 @@
-
+
diff --git a/odoo/addons/base/models/ir_mail_server.py b/odoo/addons/base/models/ir_mail_server.py
index affbaacc5c8..2ebeec8d300 100644
--- a/odoo/addons/base/models/ir_mail_server.py
+++ b/odoo/addons/base/models/ir_mail_server.py
@@ -443,7 +443,7 @@ class IrMailServer(models.Model):
message['To'] = x_forge_to
# Do not actually send emails in testing mode!
- if getattr(threading.currentThread(), 'testing', False):
+ if getattr(threading.currentThread(), 'testing', False) or self.env.registry.in_test_mode():
_test_logger.info("skip sending email in test mode")
return message['Message-Id']
diff --git a/odoo/addons/base/models/ir_ui_view.py b/odoo/addons/base/models/ir_ui_view.py
index 67cd56e6d27..02199375f87 100644
--- a/odoo/addons/base/models/ir_ui_view.py
+++ b/odoo/addons/base/models/ir_ui_view.py
@@ -71,7 +71,7 @@ def keep_query(*keep_params, **additional_params):
if not keep_params and not additional_params:
keep_params = ('*',)
params = additional_params.copy()
- qs_keys = list(request.httprequest.args)
+ qs_keys = list(request.httprequest.args) if request else []
for keep_param in keep_params:
for param in fnmatch.filter(qs_keys, keep_param):
if param not in additional_params and param in qs_keys:
diff --git a/odoo/api.py b/odoo/api.py
index e0ba72a7e51..0e9975f25c9 100644
--- a/odoo/api.py
+++ b/odoo/api.py
@@ -1014,6 +1014,17 @@ class Cache(object):
]
return model.browse(ids)
+ def copy(self, records, env):
+ """ Copy the cache of ``records`` to ``env``. """
+ src = records
+ dst = records.with_env(env)
+ for field, field_cache in self._data.items():
+ src_key = field.cache_key(src)
+ dst_key = field.cache_key(dst)
+ for record_id, record_cache in field_cache.items():
+ if src_key in record_cache:
+ record_cache[dst_key] = record_cache[src_key]
+
def invalidate(self, spec=None):
""" Invalidate the cache, partially or totally depending on ``spec``. """
if spec is None:
diff --git a/odoo/fields.py b/odoo/fields.py
index 70a9e1af80e..e0ea484eb89 100644
--- a/odoo/fields.py
+++ b/odoo/fields.py
@@ -39,18 +39,7 @@ Default = object() # default value for __init__() methods
def copy_cache(records, env):
""" Recursively copy the cache of ``records`` to the environment ``env``. """
- src, dst = records.env.cache, env.cache
- todo, done = set(records), set()
- while todo:
- record = todo.pop()
- if record not in done:
- done.add(record)
- target = record.with_env(env)
- for field in src.get_fields(record):
- value = src.get(record, field)
- dst.set(target, field, value)
- if value and field.type in ('many2one', 'one2many', 'many2many', 'reference'):
- todo.update(field.convert_to_record(value, record))
+ env.cache.copy(records, env)
def first(records):
""" Return the first record in ``records``, with the same prefetching. """
diff --git a/odoo/modules/migration.py b/odoo/modules/migration.py
index 130b4ab1bb4..e512d331caa 100644
--- a/odoo/modules/migration.py
+++ b/odoo/modules/migration.py
@@ -145,7 +145,8 @@ class MigrationManager(object):
lst.sort()
return lst
- parsed_installed_version = parse_version(getattr(pkg, 'load_version', pkg.installed_version) or '')
+ installed_version = getattr(pkg, 'load_version', pkg.installed_version) or ''
+ parsed_installed_version = parse_version(installed_version)
current_version = parse_version(convert_version(pkg.data['version']))
versions = _get_migration_versions(pkg)
@@ -173,7 +174,7 @@ class MigrationManager(object):
except AttributeError:
_logger.error('module %(addon)s: Each %(stage)s-migration file must have a "migrate(cr, installed_version)" function' % strfmt)
else:
- migrate(self.cr, pkg.installed_version)
+ migrate(self.cr, installed_version)
finally:
if mod:
del mod