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 @@
- - + 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