diff --git a/addons/account/data/data_account_type.xml b/addons/account/data/data_account_type.xml
index 495b648e1d9..d55114a1033 100644
--- a/addons/account/data/data_account_type.xml
+++ b/addons/account/data/data_account_type.xml
@@ -72,7 +72,7 @@
other
- Direct Costs
+ Cost of Revenue
other
diff --git a/addons/account/i18n/account.pot b/addons/account/i18n/account.pot
index 6b54e806185..fb42728c1c4 100644
--- a/addons/account/i18n/account.pot
+++ b/addons/account/i18n/account.pot
@@ -2571,6 +2571,12 @@ msgstr ""
msgid "Control-Access"
msgstr ""
+#. module: account
+#: model:account.account.type,name:account.data_account_type_direct_costs
+#: model:ir.ui.view,arch_db:account.view_account_form
+msgid "Cost of Revenue"
+msgstr ""
+
#. module: account
#: model:ir.model.fields,field_description:account.field_account_move_line_counterpart
msgid "Counterpart"
@@ -5016,6 +5022,11 @@ msgstr ""
msgid "Less Cost of Sales"
msgstr ""
+#. module: account
+#: model:ir.ui.view,arch_db:account.view_account_form
+msgid "Less Cost of Revenue"
+msgstr ""
+
#. module: account
#: model:ir.ui.view,arch_db:account.view_account_form
msgid "Less Current Liabilities"
diff --git a/addons/account/models/account_journal_dashboard.py b/addons/account/models/account_journal_dashboard.py
index 1ebc3f77b0c..56298bebbba 100644
--- a/addons/account/models/account_journal_dashboard.py
+++ b/addons/account/models/account_journal_dashboard.py
@@ -281,13 +281,15 @@ class account_journal(models.Model):
action_name = 'action_move_journal_line'
_journal_invoice_type_map = {
- 'sale': 'out_invoice',
- 'purchase': 'in_invoice',
- 'bank': 'bank',
- 'cash': 'cash',
- 'general': 'general',
+ ('sale', None): 'out_invoice',
+ ('purchase', None): 'in_invoice',
+ ('sale', 'refund'): 'out_refund',
+ ('purchase', 'refund'): 'in_refund',
+ ('bank', None): 'bank',
+ ('cash', None): 'cash',
+ ('general', None): 'general',
}
- invoice_type = _journal_invoice_type_map[self.type]
+ invoice_type = _journal_invoice_type_map[(self.type, self._context.get('invoice_type'))]
ctx = self._context.copy()
ctx.update({
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index 8c61c4d3a0a..0ae0661710a 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -986,8 +986,9 @@ class AccountMoveLine(models.Model):
# Since create() receives ids instead of recordset, let's just use the old-api bridge
taxes = self.env['account.tax'].browse(tax_ids)
currency = self.env['res.currency'].browse(vals.get('currency_id'))
+ partner = self.env['res.partner'].browse(vals.get('partner_id'))
res = taxes.compute_all(amount,
- currency, 1, vals.get('product_id'), vals.get('partner_id'))
+ currency, 1, vals.get('product_id'), partner)
# Adjust line amount if any tax is price_include
if abs(res['total_excluded']) < abs(amount):
if vals['debit'] != 0.0: vals['debit'] = res['total_excluded']
@@ -1147,7 +1148,7 @@ class AccountMoveLine(models.Model):
@api.model
def _query_get(self, domain=None):
context = dict(self._context or {})
- domain = domain and safe_eval(domain) or []
+ domain = domain and safe_eval(str(domain)) or []
date_field = 'date'
if context.get('aged_balance'):
diff --git a/addons/account/views/account_journal_dashboard_view.xml b/addons/account/views/account_journal_dashboard_view.xml
index 468a5b2c759..b41b378c46d 100644
--- a/addons/account/views/account_journal_dashboard_view.xml
+++ b/addons/account/views/account_journal_dashboard_view.xml
@@ -117,7 +117,7 @@
-
+
Refunds
Refunds
diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml
index 5e464c34422..a6ccf7d5d84 100644
--- a/addons/account/views/account_view.xml
+++ b/addons/account/views/account_view.xml
@@ -36,8 +36,7 @@
Profit & Loss
Income
-
Less Cost of Sales
-
Direct Costs
+
Less Cost of Revenue
GROSS PROFIT
Plus Other Income
Less Expenses
diff --git a/addons/account_asset/account_asset_invoice.py b/addons/account_asset/account_asset_invoice.py
index db020599202..88027ca8125 100644
--- a/addons/account_asset/account_asset_invoice.py
+++ b/addons/account_asset/account_asset_invoice.py
@@ -3,6 +3,8 @@
from datetime import datetime
from dateutil.relativedelta import relativedelta
from openerp import api, fields, models
+from openerp.exceptions import Warning
+from openerp.tools.translate import _
import openerp.addons.decimal_precision as dp
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DF
@@ -14,6 +16,9 @@ class AccountInvoice(models.Model):
def action_move_create(self):
result = super(AccountInvoice, self).action_move_create()
for inv in self:
+ if inv.number:
+ if self.env['account.asset.asset'].sudo().search([('code', '=', inv.number)]):
+ raise Warning(_('You already have assets with the reference %s.\nPlease delete these assets before creating new ones for this invoice.') % (inv.number,))
inv.invoice_line_ids.asset_create()
return result
@@ -85,3 +90,13 @@ class ProductTemplate(models.Model):
_inherit = 'product.template'
asset_category_id = fields.Many2one('account.asset.category', string='Asset Type', ondelete="restrict")
deferred_revenue_category_id = fields.Many2one('account.asset.category', string='Deferred Revenue Type', ondelete="restrict")
+
+ @api.onchange('deferred_revenue_category_id')
+ def onchange_deferred_revenue(self):
+ if self.deferred_revenue_category_id:
+ self.property_account_income_id = self.deferred_revenue_category_id.account_asset_id
+
+ @api.onchange('asset_category_id')
+ def onchange_asset(self):
+ if self.asset_category_id:
+ self.property_account_expense_id = self.asset_category_id.account_asset_id
diff --git a/addons/account_asset/i18n/account_asset.pot b/addons/account_asset/i18n/account_asset.pot
index 786fecbc578..84520d28c65 100644
--- a/addons/account_asset/i18n/account_asset.pot
+++ b/addons/account_asset/i18n/account_asset.pot
@@ -1023,6 +1023,12 @@ msgstr ""
msgid "Year"
msgstr ""
+#. module: account_asset
+#: code:addons/account_asset/account_asset_invoice.py:53
+#, python-format
+msgid "You already have assets with the reference %s.\nPlease delete these assets before creating new ones for this invoice."
+msgstr ""
+
#. module: account_asset
#: code:addons/account_asset/account_asset.py:100
#, python-format
diff --git a/addons/calendar/calendar.py b/addons/calendar/calendar.py
index 72cdb2b0348..e92bb9354c8 100644
--- a/addons/calendar/calendar.py
+++ b/addons/calendar/calendar.py
@@ -961,7 +961,9 @@ class calendar_event(osv.Model):
def _check_closing_date(self, cr, uid, ids, context=None):
for event in self.browse(cr, uid, ids, context=context):
- if event.stop < event.start:
+ if event.start_datetime and event.stop_datetime < event.start_datetime:
+ return False
+ if event.start_date and event.stop_date < event.start_date:
return False
return True
diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py
index 24bb04bdbbd..d9d751ed219 100644
--- a/addons/hr_expense/models/hr_expense.py
+++ b/addons/hr_expense/models/hr_expense.py
@@ -216,9 +216,9 @@ class HrExpense(models.Model):
for expense in self:
if expense.date > maxdate:
maxdate = expense.date
- if expense.journal_id not in journal_dict:
- journal_dict[expense.journal_id] = []
- journal_dict[expense.journal_id].append(expense)
+ jrn = expense.bank_journal_id if expense.payment_mode == 'company_account' else expense.journal_id
+ journal_dict.setdefault(jrn, [])
+ journal_dict[jrn].append(expense)
for journal, expense_list in journal_dict.items():
#create the move that will contain the accounting entries
diff --git a/addons/hw_escpos/escpos/escpos.py b/addons/hw_escpos/escpos/escpos.py
index 5087b392922..4051e65066c 100644
--- a/addons/hw_escpos/escpos/escpos.py
+++ b/addons/hw_escpos/escpos/escpos.py
@@ -445,7 +445,7 @@ class Escpos:
img_rgba = Image.open(f)
img = Image.new('RGB', img_rgba.size, (255,255,255))
channels = img_rgba.split()
- if len(channels) > 1:
+ if len(channels) > 3:
# use alpha channel as mask
img.paste(img_rgba, mask=channels[3])
else:
diff --git a/addons/l10n_fr/plan_comptable_general.xml b/addons/l10n_fr/plan_comptable_general.xml
index d4f29e5d626..d16acb998d9 100644
--- a/addons/l10n_fr/plan_comptable_general.xml
+++ b/addons/l10n_fr/plan_comptable_general.xml
@@ -1,6 +1,16 @@
+
+ Salaires
+ accounts
+
+
+
+ Charges Sociales
+ accounts
+
+
+
diff --git a/addons/l10n_th/account_data.xml b/addons/l10n_th/account_data.xml
index a4571518a36..6462d346d0e 100644
--- a/addons/l10n_th/account_data.xml
+++ b/addons/l10n_th/account_data.xml
@@ -192,8 +192,8 @@
5100
- Cost of goods sold
-
+ Cost of Revenue
+
diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py
index f2c647be573..6cc7327fb36 100644
--- a/addons/mail/models/mail_message.py
+++ b/addons/mail/models/mail_message.py
@@ -762,7 +762,7 @@ class Message(models.Model):
# TDE: probably clean me
parent_ids = [message.get('parent_id') for mid, message in message_values.iteritems()
if message.get('parent_id')]
- self._cr.execute("""SELECT DISTINCT m.id FROM "%s" m
+ self._cr.execute("""SELECT DISTINCT m.id, partner_rel.res_partner_id, channel_partner.partner_id FROM "%s" m
LEFT JOIN "mail_message_res_partner_rel" partner_rel
ON partner_rel.mail_message_id = m.id AND partner_rel.res_partner_id = (%%s)
LEFT JOIN "mail_message_mail_channel_rel" channel_rel
@@ -772,7 +772,7 @@ class Message(models.Model):
LEFT JOIN "mail_channel_partner" channel_partner
ON channel_partner.channel_id = channel.id AND channel_partner.partner_id = (%%s)
WHERE m.id = ANY (%%s)""" % self._table, (self.env.user.partner_id.id, self.env.user.partner_id.id, parent_ids,))
- not_parent_ids = [mid[0] for mid in self._cr.fetchall()]
+ not_parent_ids = [mid[0] for mid in self._cr.fetchall() if any([mid[1], mid[2]])]
notified_ids += [mid for mid, message in message_values.iteritems()
if message.get('parent_id') in not_parent_ids]
diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py
index 175cdcc15d7..15cd22f2350 100644
--- a/addons/mail/models/mail_thread.py
+++ b/addons/mail/models/mail_thread.py
@@ -801,15 +801,20 @@ class MailThread(models.AbstractModel):
record_set = None
def _create_bounce_email():
- self.env['mail.mail'].create({
+ bounce_to = decode_header(message, 'Return-Path') or email_from
+ bounce_mail_values = {
'body_html': 'Hello,
'
'
The following email sent to %s cannot be accepted because this is '
'a private email address. Only allowed people can contact us at this address.
'
'%s
' % (message.get('to'), message_dict.get('body')),
'subject': 'Re: %s' % message.get('subject'),
- 'email_to': message.get('from'),
+ 'email_to': bounce_to,
'auto_delete': True,
- }).send()
+ }
+ bounce_from = self.env['ir.mail_server']._get_default_bounce_address()
+ if bounce_from:
+ bounce_mail_values['email_from'] = 'MAILER-DAEMON <%s>' % bounce_from
+ self.env['mail.mail'].create(bounce_mail_values).send()
def _warn(message):
_logger.info('Routing mail with Message-Id %s: route %s: %s',
diff --git a/addons/mail/static/src/js/chatter.js b/addons/mail/static/src/js/chatter.js
index ccf7a04b0ad..19074d1eb45 100644
--- a/addons/mail/static/src/js/chatter.js
+++ b/addons/mail/static/src/js/chatter.js
@@ -919,7 +919,7 @@ var Chatter = form_common.AbstractField.extend({
this.mute_new_message_button(true);
},
close_composer: function (force) {
- if (this.composer.is_empty() || force) {
+ if (this.composer && (this.composer.is_empty() || force)) {
this.composer.do_hide();
this.composer.$input.val('');
this.mute_new_message_button(false);
diff --git a/addons/mail/tests/test_mail_gateway.py b/addons/mail/tests/test_mail_gateway.py
index 11b9bb3b9a7..9c1bc2216da 100644
--- a/addons/mail/tests/test_mail_gateway.py
+++ b/addons/mail/tests/test_mail_gateway.py
@@ -313,8 +313,8 @@ class TestMailgateway(TestMail):
'message_process: incoming email on Partners alias should send a bounce email')
self.assertIn('New Frogs', self._mails[0].get('subject'),
'message_process: bounce email on Partners alias should contain the original subject')
- self.assertIn(formataddr(('Sylvie Lelitre', 'test.sylvie.lelitre@agrolait.com')), self._mails[0].get('email_to'),
- 'message_process: bounce email on Partners alias should have original email sender as recipient')
+ self.assertIn('whatever-2a840@postmaster.twitter.com', self._mails[0].get('email_to'),
+ 'message_process: bounce email on Partners alias should go to Return-Path address')
@mute_logger('openerp.addons.mail.models.mail_thread', 'openerp.models', 'openerp.addons.mail.models.mail_mail')
def test_message_process_alias_followers_bounce(self):
diff --git a/addons/mrp/mrp.py b/addons/mrp/mrp.py
index fdf1b92c468..b94428f91fe 100644
--- a/addons/mrp/mrp.py
+++ b/addons/mrp/mrp.py
@@ -1124,6 +1124,7 @@ class mrp_production(osv.osv):
data = {
'name': production.name,
'date': production.date_planned,
+ 'date_expected': production.date_planned,
'product_id': production.product_id.id,
'product_uom': production.product_uom.id,
'product_uom_qty': production.product_qty,
diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml
index 1f53f2b3ac0..381eee46fdd 100644
--- a/addons/point_of_sale/static/src/xml/pos.xml
+++ b/addons/point_of_sale/static/src/xml/pos.xml
@@ -1070,7 +1070,7 @@
-
+
diff --git a/addons/sale/sale.py b/addons/sale/sale.py
index 7a27973e91e..c7fbd39d880 100644
--- a/addons/sale/sale.py
+++ b/addons/sale/sale.py
@@ -600,6 +600,13 @@ class SaleOrderLine(models.Model):
@api.model
def create(self, values):
+ onchange_fields = ['name', 'price_unit', 'product_uom', 'tax_id']
+ if values.get('order_id') and values.get('product_id') and any(f not in values for f in onchange_fields):
+ line = self.new(values)
+ line.product_id_change()
+ for field in onchange_fields:
+ if field not in values:
+ values[field] = line._fields[field].convert_to_write(line[field])
line = super(SaleOrderLine, self).create(values)
if line.state == 'sale':
if (not line.order_id.project_id and
diff --git a/addons/stock/stock.py b/addons/stock/stock.py
index 2a774586124..9eb3d1909de 100644
--- a/addons/stock/stock.py
+++ b/addons/stock/stock.py
@@ -61,7 +61,7 @@ class stock_location(osv.osv):
res[m.id] = m.name
parent = m.location_id
while parent:
- res[m.id] = parent.name + ' / ' + res[m.id]
+ res[m.id] = parent.name + '/' + res[m.id]
parent = parent.location_id
return res
diff --git a/addons/stock_account/stock_account.py b/addons/stock_account/stock_account.py
index eb22d2c9a36..7b4eb4f9c91 100644
--- a/addons/stock_account/stock_account.py
+++ b/addons/stock_account/stock_account.py
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp.osv import fields, osv
+from openerp.tools import float_compare, float_round
from openerp.tools.translate import _
from openerp import SUPERUSER_ID, api, models
from openerp.exceptions import UserError
@@ -214,8 +215,32 @@ class stock_quant(osv.osv):
self._create_account_move_line(cr, uid, quants, move, acc_valuation, acc_dest, journal_id, context=ctx)
def _quant_create(self, cr, uid, qty, move, lot_id=False, owner_id=False, src_package_id=False, dest_package_id=False, force_location_from=False, force_location_to=False, context=None):
+ quant_obj = self.pool.get('stock.quant')
quant = super(stock_quant, self)._quant_create(cr, uid, qty, move, lot_id=lot_id, owner_id=owner_id, src_package_id=src_package_id, dest_package_id=dest_package_id, force_location_from=force_location_from, force_location_to=force_location_to, context=context)
self._account_entry_move(cr, uid, [quant], move, context)
+ if move.product_id.valuation == 'real_time':
+ # If the precision required for the variable quant cost is larger than the accounting
+ # precision, inconsistencies between the stock valuation and the accounting entries
+ # may arise.
+ # For example, a box of 13 units is bought 15.00. If the products leave the
+ # stock one unit at a time, the amount related to the cost will correspond to
+ # round(15/13, 2)*13 = 14.95. To avoid this case, we split the quant in 12 + 1, then
+ # record the difference on the new quant.
+ # We need to make sure to able to extract at least one unit of the product. There is
+ # an arbitrary minimum quantity set to 2.0 from which we consider we can extract a
+ # unit and adapt the cost.
+ curr_rounding = move.company_id.currency_id.rounding
+ cost_rounded = float_round(quant.cost, precision_rounding=curr_rounding)
+ cost_correct = cost_rounded
+ if float_compare(quant.product_id.uom_id.rounding, 1.0, precision_digits=1) == 0\
+ and float_compare(quant.qty * quant.cost, quant.qty * cost_rounded, precision_rounding=curr_rounding) != 0\
+ and float_compare(quant.qty, 2.0, precision_rounding=quant.product_id.uom_id.rounding) >= 0:
+ qty = quant.qty
+ cost = quant.cost
+ quant_correct = quant_obj._quant_split(cr, uid, quant, quant.qty - 1.0, context=context)
+ cost_correct += (qty * cost) - (qty * cost_rounded)
+ quant_obj.write(cr, SUPERUSER_ID, [quant.id], {'cost': cost_rounded}, context=context)
+ quant_obj.write(cr, SUPERUSER_ID, [quant_correct.id], {'cost': cost_correct}, context=context)
return quant
def move_quants_write(self, cr, uid, quants, move, location_dest_id, dest_package_id, lot_id=False, entire_pack=False, context=None):
diff --git a/addons/stock_landed_costs/__openerp__.py b/addons/stock_landed_costs/__openerp__.py
index 7e1e74572a4..1251e080358 100644
--- a/addons/stock_landed_costs/__openerp__.py
+++ b/addons/stock_landed_costs/__openerp__.py
@@ -27,6 +27,7 @@ This module allows you to easily add extra costs on pickings and decide the spli
'../account/test/account_minimal_test.xml',
'../stock_account/test/stock_valuation_account.xml',
'test/stock_landed_costs.yml',
+ 'test/stock_landed_costs_rounding.yml',
],
'installable': True,
'auto_install': False,
diff --git a/addons/stock_landed_costs/stock_landed_costs.py b/addons/stock_landed_costs/stock_landed_costs.py
index 947f6c6ec72..6c0423d7e38 100644
--- a/addons/stock_landed_costs/stock_landed_costs.py
+++ b/addons/stock_landed_costs/stock_landed_costs.py
@@ -209,13 +209,41 @@ class stock_landed_cost(osv.osv):
continue
per_unit = line.final_cost / line.quantity
diff = per_unit - line.former_cost_per_unit
- quants = [quant for quant in line.move_id.quant_ids]
+
+ # If the precision required for the variable diff is larger than the accounting
+ # precision, inconsistencies between the stock valuation and the accounting entries
+ # may arise.
+ # For example, a landed cost of 15 divided in 13 units. If the products leave the
+ # stock one unit at a time, the amount related to the landed cost will correspond to
+ # round(15/13, 2)*13 = 14.95. To avoid this case, we split the quant in 12 + 1, then
+ # record the difference on the new quant.
+ # We need to make sure to able to extract at least one unit of the product. There is
+ # an arbitrary minimum quantity set to 2.0 from which we consider we can extract a
+ # unit and adapt the cost.
+ curr_rounding = line.move_id.company_id.currency_id.rounding
+ diff_rounded = float_round(diff, precision_rounding=curr_rounding)
+ diff_correct = diff_rounded
+ quants = line.move_id.quant_ids.sorted(key=lambda r: r.qty, reverse=True)
+ quant_correct = False
+ if quants\
+ and float_compare(quants[0].product_id.uom_id.rounding, 1.0, precision_digits=1) == 0\
+ and float_compare(line.quantity * diff, line.quantity * diff_rounded, precision_rounding=curr_rounding) != 0\
+ and float_compare(quants[0].qty, 2.0, precision_rounding=quants[0].product_id.uom_id.rounding) >= 0:
+ # Search for existing quant of quantity = 1.0 to avoid creating a new one
+ quant_correct = quants.filtered(lambda r: float_compare(r.qty, 1.0, precision_rounding=quants[0].product_id.uom_id.rounding) == 0)
+ if not quant_correct:
+ quant_correct = quant_obj._quant_split(cr, uid, quants[0], quants[0].qty - 1.0, context=context)
+ else:
+ quant_correct = quant_correct[0]
+ quants = quants - quant_correct
+ diff_correct += (line.quantity * diff) - (line.quantity * diff_rounded)
+ diff = diff_rounded
+
quant_dict = {}
for quant in quants:
- if quant.id not in quant_dict:
- quant_dict[quant.id] = quant.cost + diff
- else:
- quant_dict[quant.id] += diff
+ quant_dict[quant.id] = quant.cost + diff
+ if quant_correct:
+ quant_dict[quant_correct.id] = quant_correct.cost + diff_correct
for key, value in quant_dict.items():
quant_obj.write(cr, SUPERUSER_ID, key, {'cost': value}, context=context)
qty_out = 0
diff --git a/addons/stock_landed_costs/test/stock_landed_costs_rounding.yml b/addons/stock_landed_costs/test/stock_landed_costs_rounding.yml
new file mode 100644
index 00000000000..2f237e6751a
--- /dev/null
+++ b/addons/stock_landed_costs/test/stock_landed_costs_rounding.yml
@@ -0,0 +1,175 @@
+-
+ In order to test the rounding in landed costs feature of stock, I create 2 landed cost
+-
+ Define undivisible units
+-
+ !record {model: product.uom, id: product_uom_unit_round_1}:
+ category_id: product.product_uom_categ_unit
+ name: Undivisible Unit(s)
+ factor: 1.0
+ rounding: 1.0
+-
+ I create 2 products with different cost prices and configure them for real_time valuation and real price costing method
+-
+ !record {model: product.product, id: product_landed_cost_3}:
+ name: "LC product 3"
+ cost_method: real
+ uom_id: product_uom_unit_round_1
+ valuation: real_time
+ property_stock_account_input: o_expense
+ property_stock_account_output: o_income
+-
+ !record {model: product.product, id: product_landed_cost_4}:
+ name: "LC product 4"
+ cost_method: real
+ uom_id: product_uom_unit_round_1
+ valuation: real_time
+ property_stock_account_input: o_expense
+ property_stock_account_output: o_income
+-
+ I create 2 pickings moving those products
+-
+ !record {model: stock.picking, id: picking_landed_cost_3}:
+ name: 'LC_pick_3'
+ picking_type_id: stock.picking_type_in
+ move_lines:
+ - name: move 3
+ product_id: product_landed_cost_3
+ product_uom_qty: 13
+ product_uom: product_uom_unit_round_1
+ location_id: stock.stock_location_customers
+ location_dest_id: stock.stock_location_stock
+-
+ !record {model: stock.picking, id: picking_landed_cost_4}:
+ name: 'LC_pick_4'
+ picking_type_id: stock.picking_type_in
+ move_lines:
+ - name: move 4
+ product_id: product_landed_cost_4
+ product_uom_qty: 1
+ product_uom: product.product_uom_dozen
+ location_id: stock.stock_location_customers
+ location_dest_id: stock.stock_location_stock
+ price_unit: !eval 17.00/12.00
+-
+ We perform all the tests for LC_pick_3
+-
+ I receive picking LC_pick_3, and check how many quants are created
+-
+ !python {model: stock.picking}: |
+ pick = self.browse(cr, uid, [ref("picking_landed_cost_3")], context=context)
+ pick.move_lines.price_unit = 1.0
+
+ self.action_confirm(cr, uid, [ref("picking_landed_cost_3")], context=context)
+ self.action_assign(cr, uid, [ref("picking_landed_cost_3")], context=context)
+ self.action_done(cr, uid, [ref("picking_landed_cost_3")], context=context)
+
+ quants = pick.move_lines.quant_ids
+ assert len(quants) == 1
+ assert quants.qty == 13
+ assert quants.cost == 1.0
+-
+ I create a landed cost for picking 3
+-
+ !record {model: stock.landed.cost, id: stock_landed_cost_2}:
+ picking_ids: [picking_landed_cost_3]
+ account_journal_id: expenses_journal
+ cost_lines:
+ - name: 'equal split'
+ split_method: 'equal'
+ price_unit: 15
+ product_id: product.product_product_1
+ valuation_adjustment_lines: []
+-
+ I compute the landed cost using Compute button
+-
+ !python {model: stock.landed.cost}: |
+ self.compute_landed_cost(cr, uid, [ref("stock_landed_cost_2")])
+-
+ I check the valuation adjustment lines
+-
+ !python {model: stock.landed.cost}: |
+ landed_cost = self.browse(cr, uid, ref("stock_landed_cost_2"))
+ for valuation in landed_cost.valuation_adjustment_lines:
+ assert valuation.additional_landed_cost == 15
+-
+ I confirm the landed cost
+-
+ !python {model: stock.landed.cost}: |
+ self.button_validate(cr, uid, [ref("stock_landed_cost_2")])
+-
+ I check that the landed cost is now "Closed" and that it has an accounting entry
+-
+ !assert {model: stock.landed.cost, id: stock_landed_cost_2}:
+ - state == 'done'
+ - account_move_id
+-
+ I check the quants quantity and cost
+-
+ !python {model: stock.landed.cost}: |
+ landed_cost = self.browse(cr, uid, ref("stock_landed_cost_2"))
+ for valuation in landed_cost.valuation_adjustment_lines:
+ quants = valuation.move_id.quant_ids
+ assert quants.mapped('qty') == [12.0, 1.0]
+ assert quants.mapped('cost') == [2.15, 2.2]
+-
+ We perform all the tests for LC_pick_4
+-
+ I receive picking LC_pick_4, and check how many quants are created
+-
+ !python {model: stock.picking}: |
+ pick = self.browse(cr, uid, [ref("picking_landed_cost_4")], context=context)
+ pick.move_lines.price_unit = 17.0/12.0
+
+ self.action_confirm(cr, uid, [ref("picking_landed_cost_4")], context=context)
+ self.action_assign(cr, uid, [ref("picking_landed_cost_4")], context=context)
+ self.action_done(cr, uid, [ref("picking_landed_cost_4")], context=context)
+
+ quants = pick.move_lines.quant_ids
+ assert len(quants) == 2
+ assert quants.mapped('qty') == [11.0, 1.0]
+ assert [round(c, 2) for c in quants.mapped('cost')] == [1.42, 1.38]
+-
+ I create a landed cost for picking 4
+-
+ !record {model: stock.landed.cost, id: stock_landed_cost_3}:
+ picking_ids: [picking_landed_cost_4]
+ account_journal_id: expenses_journal
+ cost_lines:
+ - name: 'equal split'
+ split_method: 'equal'
+ price_unit: 11
+ product_id: product.product_product_1
+ valuation_adjustment_lines: []
+-
+ I compute the landed cost using Compute button
+-
+ !python {model: stock.landed.cost}: |
+ self.compute_landed_cost(cr, uid, [ref("stock_landed_cost_3")])
+-
+ I check the valuation adjustment lines
+-
+ !python {model: stock.landed.cost}: |
+ landed_cost = self.browse(cr, uid, ref("stock_landed_cost_3"))
+ for valuation in landed_cost.valuation_adjustment_lines:
+ assert valuation.additional_landed_cost == 11
+-
+ I confirm the landed cost
+-
+ !python {model: stock.landed.cost}: |
+ self.button_validate(cr, uid, [ref("stock_landed_cost_3")])
+-
+ I check that the landed cost is now "Closed" and that it has an accounting entry
+-
+ !assert {model: stock.landed.cost, id: stock_landed_cost_3}:
+ - state == 'done'
+ - account_move_id
+-
+ I check the quants quantity and cost
+-
+ !python {model: stock.landed.cost}: |
+ landed_cost = self.browse(cr, uid, ref("stock_landed_cost_3"))
+ for valuation in landed_cost.valuation_adjustment_lines:
+ quants = valuation.move_id.quant_ids
+ assert quants.mapped('qty') == [11.0, 1.0]
+ assert [round(c, 2) for c in quants.mapped('cost')] == [2.34, 2.26]
diff --git a/addons/web/static/src/js/framework/ajax.js b/addons/web/static/src/js/framework/ajax.js
index 869e0214ad9..87fd08c7786 100644
--- a/addons/web/static/src/js/framework/ajax.js
+++ b/addons/web/static/src/js/framework/ajax.js
@@ -203,7 +203,9 @@ function get_file(options) {
// opening a new window seems the best way to workaround
if (navigator.userAgent.match(/(iPod|iPhone|iPad)/)) {
var params = _.extend({}, options.data || {}, {token: token});
- var url = this.url(options.url, params);
+ var url = options.session.url(options.url, params);
+ if (options.complete) { options.complete(); }
+
return window.open(url);
}
diff --git a/addons/web/static/src/js/framework/data.js b/addons/web/static/src/js/framework/data.js
index a1b8e1602e9..3e9396a4e96 100644
--- a/addons/web/static/src/js/framework/data.js
+++ b/addons/web/static/src/js/framework/data.js
@@ -320,6 +320,7 @@ var DataSet = Class.extend(mixins.PropertiesMixin, {
this.index = null;
this._sort = [];
this._model = new Model(model, context);
+ this.orderer = new utils.DropMisordered();
},
previous: function () {
this.index -= 1;
@@ -408,10 +409,11 @@ var DataSet = Class.extend(mixins.PropertiesMixin, {
read_slice: function (fields, options) {
var self = this;
options = options || {};
- return this._model.query(fields)
+ var query = this._model.query(fields)
.limit(options.limit || false)
.offset(options.offset || 0)
- .all().done(function (records) {
+ .all();
+ return this.orderer.add(query).done(function (records) {
self.ids = _(records).pluck('id');
});
},
diff --git a/addons/web/static/src/js/framework/session.js b/addons/web/static/src/js/framework/session.js
index 7eae8b20a65..e16b3767e64 100644
--- a/addons/web/static/src/js/framework/session.js
+++ b/addons/web/static/src/js/framework/session.js
@@ -271,6 +271,7 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, {
if (this.override_session){
options.data.session_id = this.session_id;
}
+ options.session = this;
ajax.get_file(options);
},
synchronized_mode: function(to_execute) {
diff --git a/addons/web/static/src/js/views/list_view.js b/addons/web/static/src/js/views/list_view.js
index cb6daf093ce..229ecb86d83 100644
--- a/addons/web/static/src/js/views/list_view.js
+++ b/addons/web/static/src/js/views/list_view.js
@@ -1901,6 +1901,7 @@ var Column = Class.extend({
row_data[this.id].value, this, options.value_if_empty));
}
});
+ListView.Column = Column;
var MetaColumn = Column.extend({
meta: true,
diff --git a/addons/web_editor/models/ir_translation.py b/addons/web_editor/models/ir_translation.py
index 1dedb26d1bf..19627b58c57 100644
--- a/addons/web_editor/models/ir_translation.py
+++ b/addons/web_editor/models/ir_translation.py
@@ -1,7 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from lxml import etree
+
from openerp import models, api
+from openerp.tools.translate import encode, xml_translate
def edit_translation_mapping(data):
data = dict(data, model=data['name'].partition(',')[0])
@@ -16,3 +19,20 @@ class ir_translation(models.Model):
self.insert_missing(field, records)
return edit_translation_mapping
return super(ir_translation, self)._get_terms_mapping(field, records)
+
+ @api.multi
+ def save_html(self, value):
+ """ Convert the HTML fragment ``value`` to XML if necessary, and write
+ it as the value of translation ``self``.
+ """
+ assert len(self) == 1 and self.type == 'model'
+ mname, fname = self.name.split(',')
+ field = self.env[mname]._fields[fname]
+ if field.translate == xml_translate:
+ # wrap value inside a div and parse it as HTML
+ div = "
%s
" % encode(value)
+ root = etree.fromstring(div, etree.HTMLParser(encoding='utf-8'))
+ # root is html > body > div
+ # serialize div as XML and discard surrounding tags
+ value = etree.tostring(root[0][0], encoding='utf-8')[5:-6]
+ return self.write({'value': value})
diff --git a/addons/web_editor/static/src/js/translator.js b/addons/web_editor/static/src/js/translator.js
index 39efbe1db2f..f243a878d2c 100644
--- a/addons/web_editor/static/src/js/translator.js
+++ b/addons/web_editor/static/src/js/translator.js
@@ -47,10 +47,10 @@ var RTE_Translate = rte.Class.extend({
return ajax.jsonRpc('/web/dataset/call', 'call', {
model: 'ir.translation',
- method: 'write',
+ method: 'save_html',
args: [
[+$el.data('oe-translation-id')],
- {'value': translation_content, 'state': 'translated'},
+ translation_content,
context || base.get_context()
],
});
diff --git a/addons/web_kanban/static/src/js/kanban_relational.js b/addons/web_kanban/static/src/js/kanban_relational.js
index 16b076d7af1..d717cbf9660 100644
--- a/addons/web_kanban/static/src/js/kanban_relational.js
+++ b/addons/web_kanban/static/src/js/kanban_relational.js
@@ -15,22 +15,6 @@ var X2ManyKanbanView = KanbanView.extend({
});
this._super($node, options);
},
- load_records: function (offset, dataset) {
- var options = {
- 'limit': this.limit,
- 'offset': offset,
- };
- dataset = dataset || this.dataset;
- return dataset
- .read_slice(this.fields_keys.concat(['__last_update']), options)
- .then(function(records) {
- return {
- records: records,
- is_empty: !records.length,
- grouped: false,
- };
- });
- },
});
var One2ManyKanbanView = X2ManyKanbanView.extend({
@@ -102,3 +86,4 @@ core.view_registry.add('one2many_kanban', One2ManyKanbanView);
core.view_registry.add('many2many_kanban', Many2ManyKanbanView);
});
+
diff --git a/addons/web_kanban/static/src/js/kanban_view.js b/addons/web_kanban/static/src/js/kanban_view.js
index e8c6d806fe4..e35976a1fa6 100644
--- a/addons/web_kanban/static/src/js/kanban_view.js
+++ b/addons/web_kanban/static/src/js/kanban_view.js
@@ -130,17 +130,13 @@ var KanbanView = View.extend({
return this.search_orderer
.add(options.grouped ? this.load_groups(options) : this.load_records())
.then(function (data) {
- var new_ids;
_.extend(self, options);
if (options.grouped) {
- new_ids = _.union.apply(null, _.map(data.groups, function (group) {
+ var new_ids = _.union.apply(null, _.map(data.groups, function (group) {
return group.dataset.ids;
}));
- } else {
- new_ids = _.pluck(data.records, 'id');
- self.dataset._length = data.count;
+ self.dataset.alter_ids(new_ids);
}
- self.dataset.alter_ids(new_ids);
self.data = data;
})
.then(this.proxy('render'))
@@ -157,20 +153,19 @@ var KanbanView = View.extend({
},
load_records: function (offset, dataset) {
- dataset = dataset || this.dataset;
- var query = dataset._model.query(this.fields_keys.concat(['__last_update']))
- .limit(this.limit || false)
- .offset(offset || 0);
- return query.all()
- .then(function (records) {
- return query.count().then(function(count) {
- return {
- records: records,
- is_empty: !records.length,
- grouped: false,
- count: count,
- };
- })
+ var options = {
+ 'limit': this.limit,
+ 'offset': offset,
+ };
+ dataset = dataset || this.dataset;
+ return dataset
+ .read_slice(this.fields_keys.concat(['__last_update']), options)
+ .then(function(records) {
+ return {
+ records: records,
+ is_empty: !records.length,
+ grouped: false,
+ };
});
},
diff --git a/addons/web_kanban/static/src/less/kanban_view.less b/addons/web_kanban/static/src/less/kanban_view.less
index ddd7f6374d1..e5d0a79d6e5 100644
--- a/addons/web_kanban/static/src/less/kanban_view.less
+++ b/addons/web_kanban/static/src/less/kanban_view.less
@@ -384,7 +384,7 @@
font-size: 18px;
font-weight: 300;
.o-flex(1, 1, auto);
- white-space: nowrap;
+ .o-text-overflow();
padding-top: 5px;
}
.o_kanban_quick_add i {
diff --git a/addons/website_form/controllers/main.py b/addons/website_form/controllers/main.py
index f2f0cc806ef..be1bf5aa53a 100644
--- a/addons/website_form/controllers/main.py
+++ b/addons/website_form/controllers/main.py
@@ -2,9 +2,12 @@
import base64
import json
+import pytz
+from datetime import datetime
from psycopg2 import IntegrityError
from openerp import http, SUPERUSER_ID
from openerp.http import request
+from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
from openerp.tools.translate import _
from openerp.exceptions import ValidationError
from openerp.addons.base.ir.ir_qweb import nl2br
@@ -60,6 +63,17 @@ class WebsiteForm(http.Controller):
def boolean(self, field_label, field_input):
return bool(field_input)
+ def date(self, field_label, field_input):
+ lang = request.env['ir.qweb.field'].user_lang()
+ return datetime.strptime(field_input, lang.date_format).strftime(DEFAULT_SERVER_DATE_FORMAT)
+
+ def datetime(self, field_label, field_input):
+ lang = request.env['ir.qweb.field'].user_lang()
+ strftime_format = (u"%s %s" % (lang.date_format, lang.time_format))
+ user_tz = pytz.timezone(request.context.get('tz') or request.env.user.tz or 'UTC')
+ dt = user_tz.localize(datetime.strptime(field_input, strftime_format)).astimezone(pytz.utc)
+ return dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
+
def binary(self, field_label, field_input):
return base64.b64encode(field_input.read())
@@ -73,7 +87,8 @@ class WebsiteForm(http.Controller):
'char': identity,
'text': identity,
'html': identity,
- 'datetime': identity,
+ 'date': date,
+ 'datetime': datetime,
'many2one': integer,
'one2many': one2many,
'many2many':many2many,
diff --git a/doc/cla/corporate/VanRoey.md b/doc/cla/corporate/VanRoey.md
new file mode 100644
index 00000000000..956d6bb43a4
--- /dev/null
+++ b/doc/cla/corporate/VanRoey.md
@@ -0,0 +1,18 @@
+Belgium 2016-04-11
+
+Van Roey agrees to the terms of the Odoo Corporate
+Contributor License Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Yenthe Van Ginneken yenthe.vanginneken@vanroey.be https://github.com/Yenthe666
+
+List of contributors:
+
+Yenthe Van Ginneken yenthe.vanginneken@vanroey.be https://github.com/Yenthe666
+Maarten Wijnants maarten.wijnants@vanroey.be https://github.com/weilant
+Vincent Adriaensen vincent.adriaensen@vanroey.be https://github.com/vincentadriaensen
+Pieter Paulussen pieter.paulussen@vanroey.be https://github.com/PieterPaulussen
diff --git a/doc/cla/corporate/bisneSmart.md b/doc/cla/corporate/bisneSmart.md
new file mode 100644
index 00000000000..229c626f5c5
--- /dev/null
+++ b/doc/cla/corporate/bisneSmart.md
@@ -0,0 +1,16 @@
+Spain, March 11, 2016
+
+bisneSmart S.C. agrees to the terms of the Odoo Corporate Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+bisneSmart S.C. hola@bisnesmart.com https://github.com/bisnesmart
+
+List of contributors:
+
+Gonzalo Borrás Beta gborras@bisnesmart.com https://github.com/gonxi
+Rubén Cabrera Martínez rcabrera@bisnesmart.com https://github.com/rubencabrera
diff --git a/doc/cla/corporate/camptocamp.md b/doc/cla/corporate/camptocamp.md
index bb68262d95f..6e6d131fc2c 100644
--- a/doc/cla/corporate/camptocamp.md
+++ b/doc/cla/corporate/camptocamp.md
@@ -27,4 +27,5 @@ Romain Deheele romain.deheele@camptocamp.com https://github.com/rdeheele
Maxime Wiot maxime.wiot@camptocamp.com https://github.com/maxime-c2c
Ferdinand Gassauer ferdinand.gassauer@camptocamp.com https://github.com/ferdiga
Jean-Baptiste Aubort jean-baptiste.aubort@camptocamp.com
+Cyril Gaudin cyril.gaudin@camptocamp.com https://github.com/cyrilgdn
diff --git a/doc/cla/individual/erocre.md b/doc/cla/individual/erocre.md
new file mode 100644
index 00000000000..a7a2aca53b0
--- /dev/null
+++ b/doc/cla/individual/erocre.md
@@ -0,0 +1,9 @@
+Spain, 2016-04-08
+
+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,
+
+Eduardo Rodriguez erocre@gmail.com https://github.com/erocre
diff --git a/doc/cla/individual/javcasas.md b/doc/cla/individual/javcasas.md
new file mode 100644
index 00000000000..0458f7754b1
--- /dev/null
+++ b/doc/cla/individual/javcasas.md
@@ -0,0 +1,11 @@
+Ecuador, 14th of March, 2016
+
+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,
+
+Javier Casas javcasas@gmail.com https://github.com/javcasas
diff --git a/doc/cla/individual/stephen144.md b/doc/cla/individual/stephen144.md
index 1caf0b28b4f..282bd5f0a50 100644
--- a/doc/cla/individual/stephen144.md
+++ b/doc/cla/individual/stephen144.md
@@ -1,4 +1,4 @@
-United States of America, 2015-10-22
+United States of America, 2016-04-07
I hereby agree to the terms of the Odoo Individual Contributor License
Agreement v1.0.
@@ -9,3 +9,4 @@ declaration.
Signed,
Stephen Medina stephen@lilmail.xyz https://github.com/stephen144
+Stephen Medina stephenm@lsm.org https://github.com/stephen144
diff --git a/doc/cla/individual/tesserb.md b/doc/cla/individual/tesserb.md
new file mode 100644
index 00000000000..ba4e41e2e42
--- /dev/null
+++ b/doc/cla/individual/tesserb.md
@@ -0,0 +1,11 @@
+United States, 2016-04-19
+
+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,
+
+Cody Kitterman ckitterm@gmail.com https://github.com/tesserb
\ No newline at end of file
diff --git a/openerp/service/server.py b/openerp/service/server.py
index f21699ddcf1..92fda76aa7a 100644
--- a/openerp/service/server.py
+++ b/openerp/service/server.py
@@ -55,7 +55,8 @@ SLEEP_INTERVAL = 60 # 1 min
def memory_info(process):
""" psutil < 2.0 does not have memory_info, >= 3.0 does not have
get_memory_info """
- return (getattr(process, 'memory_info', None) or process.get_memory_info)()
+ pmem = (getattr(process, 'memory_info', None) or process.get_memory_info)()
+ return (pmem.rss, pmem.vms)
#----------------------------------------------------------
# Werkzeug WSGI servers patched