[MERGE] Forward-port 9.0 up to 74d8cbc190
This commit is contained in:
@@ -72,7 +72,7 @@
|
||||
<field name="type">other</field>
|
||||
</record>
|
||||
<record model="account.account.type" id="data_account_type_direct_costs">
|
||||
<field name="name">Direct Costs</field>
|
||||
<field name="name">Cost of Revenue</field>
|
||||
<field name="type">other</field>
|
||||
</record>
|
||||
</data>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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'):
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a type="object" name="open_action" context="{'use_domain': [('type','in',('out_refund','in_refund'))]}">
|
||||
<a type="object" name="open_action" context="{'use_domain': [('type','in',('out_refund','in_refund'))], 'invoice_type': 'refund'}">
|
||||
<span t-if="journal_type == 'sale'">Refunds</span>
|
||||
<span t-if="journal_type == 'purchase'">Refunds</span>
|
||||
</a>
|
||||
|
||||
@@ -36,8 +36,7 @@
|
||||
<div class="oe_account_help">
|
||||
<p class="oe_account_font_help text-center">Profit & Loss</p>
|
||||
<p class="oe_account_font_help oe_account_font_title">Income</p>
|
||||
<p class="oe_account_font_help oe_account_font_title">Less Cost of Sales</p>
|
||||
<p class="oe_account_font_help oe_account_font_content">Direct Costs</p>
|
||||
<p class="oe_account_font_help oe_account_font_title">Less Cost of Revenue</p>
|
||||
<p class="oe_account_font_help" style="color: grey;border-top: 2px solid;border-bottom: 2px solid;">GROSS PROFIT</p>
|
||||
<p class="oe_account_font_help oe_account_font_title">Plus Other Income</p>
|
||||
<p class="oe_account_font_help oe_account_font_title">Less Expenses</p>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="account_fr_tag_salaires" model="account.account.tag">
|
||||
<field name="name">Salaires</field>
|
||||
<field name="applicability">accounts</field>
|
||||
</record>
|
||||
|
||||
<record id="account_fr_tag_charges_sociales" model="account.account.tag">
|
||||
<field name="name">Charges Sociales</field>
|
||||
<field name="applicability">accounts</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="account_reports_fr_statements_menu" name="French Statements" parent="account.menu_finance_reports" sequence="3" groups="account.group_account_user"/>
|
||||
|
||||
<!--
|
||||
|
||||
@@ -125,11 +125,11 @@ class AccountFrFec(models.TransientModel):
|
||||
aa.code AS CompteNum,
|
||||
replace(aa.name, '|', '/') AS CompteLib,
|
||||
CASE WHEN rp.ref IS null OR rp.ref = ''
|
||||
THEN 'ID ' || rp.id
|
||||
THEN COALESCE('ID ' || rp.id, '')
|
||||
ELSE rp.ref
|
||||
END
|
||||
AS CompAuxNum,
|
||||
replace(rp.name, '|', '/') AS CompAuxLib,
|
||||
COALESCE(replace(rp.name, '|', '/'), '') AS CompAuxLib,
|
||||
CASE WHEN am.ref IS null OR am.ref = ''
|
||||
THEN '-'
|
||||
ELSE replace(am.ref, '|', '/')
|
||||
@@ -142,7 +142,10 @@ class AccountFrFec(models.TransientModel):
|
||||
CASE WHEN rec.name IS NULL THEN '' ELSE rec.name END AS EcritureLet,
|
||||
CASE WHEN aml.full_reconcile_id IS NULL THEN '' ELSE TO_CHAR(rec.create_date, 'YYYYMMDD') END AS DateLet,
|
||||
TO_CHAR(am.date, 'YYYYMMDD') AS ValidDate,
|
||||
replace(CASE WHEN aml.amount_currency = 0 THEN '' ELSE to_char(aml.amount_currency, '999999999999999D99') END, '.', ',') AS Montantdevise,
|
||||
CASE
|
||||
WHEN aml.amount_currency IS NULL OR aml.amount_currency = 0 THEN ''
|
||||
ELSE replace(to_char(aml.amount_currency, '999999999999999D99'), '.', ',')
|
||||
END AS Montantdevise,
|
||||
CASE WHEN aml.currency_id IS NULL THEN '' ELSE rc.name END AS Idevise
|
||||
FROM
|
||||
account_move_line aml
|
||||
|
||||
@@ -156,8 +156,9 @@
|
||||
|
||||
<record id="conf_cog" model="account.account.template">
|
||||
<field name="code">210</field>
|
||||
<field name="name">Cost of Goods Sold</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_expenses"/>
|
||||
<field name="name">Cost of Revenue</field>
|
||||
<!-- FORWARDPORT UP TO SAAS-10 -->
|
||||
<field name="user_type_id" ref="account.data_account_type_direct_costs"/>
|
||||
<field name="chart_template_id" ref="configurable_chart_template"/>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('account.account_tag_operating')])]"/>
|
||||
</record>
|
||||
|
||||
@@ -192,8 +192,8 @@
|
||||
|
||||
<record id="a_exp_cogs" model="account.account.template">
|
||||
<field name="code">5100</field>
|
||||
<field name="name">Cost of goods sold</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_expenses"/>
|
||||
<field name="name">Cost of Revenue</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_direct_costs"/>
|
||||
<field name="chart_template_id" ref="chart"/>
|
||||
</record>
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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': '<div><p>Hello,</p>'
|
||||
'<p>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.</p></div>'
|
||||
'<blockquote>%s</blockquote>' % (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',
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1070,7 +1070,7 @@
|
||||
</t>
|
||||
<t t-if="product.to_weight">
|
||||
<span class="price-tag">
|
||||
<t t-esc="widget.format_currency(product.price,'Product Price')+'/Kg'"/>
|
||||
<t t-esc="widget.format_currency(product.price,'Product Price')+'/'+widget.pos.units_by_id[product.uom_id[0]].name"/>
|
||||
</span>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = "<div>%s</div>" % 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})
|
||||
|
||||
@@ -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()
|
||||
],
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user