[MERGE] forward port branch saas-16 up to 745d00362a

This commit is contained in:
Christophe Simonis
2017-11-16 12:40:36 +01:00
35 changed files with 1814 additions and 624 deletions
+39 -170
View File
@@ -4,7 +4,7 @@ import time
import math
from odoo.osv import expression
from odoo.tools.float_utils import float_round as round, float_is_zero as is_zero
from odoo.tools.float_utils import float_round as round
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
from odoo.exceptions import UserError, ValidationError
from odoo import api, fields, models, _
@@ -848,7 +848,6 @@ class AccountTax(models.Model):
price_unit * quantity eventually affected by previous taxes (if tax is include_base_amount XOR price_include)
"""
self.ensure_one()
price_include = self._context.get('force_price_include', self.price_include)
if self.amount_type == 'fixed':
# Use copysign to take into account the sign of the base amount which includes the sign
# of the quantity and the sign of the price_unit
@@ -862,11 +861,11 @@ class AccountTax(models.Model):
return math.copysign(quantity, base_amount) * self.amount
else:
return quantity * self.amount
if (self.amount_type == 'percent' and not price_include) or (self.amount_type == 'division' and self.price_include):
if (self.amount_type == 'percent' and not self.price_include) or (self.amount_type == 'division' and self.price_include):
return base_amount * self.amount / 100
if self.amount_type == 'percent' and price_include:
if self.amount_type == 'percent' and self.price_include:
return base_amount - (base_amount / (1 + self.amount / 100))
if self.amount_type == 'division' and not price_include:
if self.amount_type == 'division' and not self.price_include:
return base_amount / (1 - self.amount / 100) - base_amount
@api.multi
@@ -900,58 +899,13 @@ class AccountTax(models.Model):
'analytic': boolean,
}]
} """
# 1) Flatten the taxes.
def collect_taxes(self, all_taxes=None):
# Collect all the taxes recursively ordered by the sequence.
# Example:
# group | seq | sub-group |
# ------------|-----------|
# | 1 | |
# ------------|-----------|
# t | 2 | | seq | |
# | | | 4 | |
# | | | 5 | |
# | | | 6 | |
# | | |
# ------------|-----------|
# | 3 | |
# ------------|-----------|
# Result: 1-4-5-6-3
if not all_taxes:
all_taxes = self.env['account.tax']
for tax in self.sorted(key=lambda r: r.sequence):
if tax.amount_type == 'group':
all_taxes = collect_taxes(tax.children_tax_ids, all_taxes)
else:
all_taxes += tax
return all_taxes
taxes = collect_taxes(self)
# 2) Avoid dealing with taxes mixing price_include=False && include_base_amount=True
# with price_include=True
base_excluded_flag = False # price_include=False && include_base_amount=True
included_flag = False # price_include=True
for tax in taxes:
if tax.price_include:
included_flag = True
elif tax.include_base_amount:
base_excluded_flag = True
if base_excluded_flag and included_flag:
raise UserError(_('Unable to mix any taxes being price included with taxes affecting the base amount but not included in price.'))
# 3) Deal with the rounding methods
if len(self) == 0:
company_id = self.env.user.company_id
else:
company_id = self[0].company_id
if not currency:
currency = company_id.currency_id
taxes = []
# By default, for each tax, tax amount will first be computed
# and rounded at the 'Account' decimal precision for each
# PO/SO/invoice line and then these rounded amounts will be
@@ -976,135 +930,50 @@ class AccountTax(models.Model):
if not round_tax:
prec += 5
# 4) Iterate the taxes in the reversed sequence order to retrieve the initial base of the computation.
# tax | base | amount |
# /\ ----------------------------
# || tax_1 | XXXX | | <- we are looking for that, it's the total_excluded
# || tax_2 | | |
# || tax_3 | | |
# || ... | .. | .. |
# ----------------------------
base = round(price_unit * quantity, prec)
# Keep track of subsequent recomputed bases in order to avoid some rounding issues.
# For example, 399.99 computed with a tax 20% price_include leads to
# base = 399.99 / 1.2 = 333.32500000000005
# tax_amount = base * 0.2 = 66.665
# round(base) + round(tax_amount) = 333.33 + 66.67 = 400.0 (!= 399.99: WRONG)
#
# To fix such issues, base_gaps will contains amount between two bases.
# In our example, the gap between 333.32500000000005 and 399.99 is 66.66499999999996
#
# Then, when processing the tax and because 66.665 - 66.66499999999996 is close to zero,
# the real gap is returned and so:
# tax_amount = 66.66499999999996
# round(base) + round(tax_amount) = 333.33 + 66.66 = 399.99 (CORRECT)
base_gaps = []
def recompute_base(base_amount, fixed_amount, percent_amount):
# Recompute the new base amount based on included fixed/percent amount and the current base amount.
# Example:
# tax | amount |
# ------------------
# tax_1 | 10% |
# tax_2 | 15 |
# tax_3 | 20% |
# ------------------
# if base_amount = 145, the new base is computed as:
# (145 - 15) / (1.0 + ((10 + 20) / 100.0)) = 130 / 1.3 = 100
if fixed_amount == 0.0 and percent_amount == 0.0:
return base_amount
new_base = (base_amount - fixed_amount) / (1.0 + percent_amount / 100.0)
base_gaps.append(base_amount - new_base)
return new_base
# For the computation of move lines, we could have a negative base value.
# In this case, compute all with positive values and negative them at the end.
if base < 0:
base = -base
sign = -1
base_values = self.env.context.get('base_values')
if not base_values:
total_excluded = total_included = base = round(price_unit * quantity, prec)
else:
sign = 1
total_excluded, total_included, base = base_values
# Keep track of the accumulated included fixed/percent amount.
incl_fixed_amount = incl_percent_amount = 0
for tax in reversed(taxes):
if tax.include_base_amount:
base = recompute_base(base, incl_fixed_amount, incl_percent_amount)
incl_fixed_amount = incl_percent_amount = 0
if self._context.get('force_price_include', tax.price_include):
if tax.amount_type == 'fixed':
incl_fixed_amount += quantity * tax.amount
elif tax.amount_type == 'percent':
incl_percent_amount += tax.amount
# Start the computation of accumulated amounts at the total_excluded value.
total_excluded = total_included = base = recompute_base(base, incl_fixed_amount, incl_percent_amount)
# Sorting key is mandatory in this case. When no key is provided, sorted() will perform a
# search. However, the search method is overridden in account.tax in order to add a domain
# depending on the context. This domain might filter out some taxes from self, e.g. in the
# case of group taxes.
for tax in self.sorted(key=lambda r: r.sequence):
if tax.amount_type == 'group':
children = tax.children_tax_ids.with_context(base_values=(total_excluded, total_included, base))
ret = children.compute_all(price_unit, currency, quantity, product, partner)
total_excluded = ret['total_excluded']
base = ret['base'] if tax.include_base_amount else base
total_included = ret['total_included']
tax_amount = total_included - total_excluded
taxes += ret['taxes']
continue
# 5) Iterate the taxes in the sequence order to fill missing base/amount values.
# tax | base | amount |
# || ----------------------------
# || tax_1 | OK | XXXX |
# || tax_2 | XXXX | XXXX |
# || tax_3 | XXXX | XXXX |
# \/ ... | .. | .. |
# ----------------------------
def compute_amount(tax):
# Compute the amount of the tax but don't deal with the price_include because it's already
# took into account on the base amount except for 'division' tax:
# (tax.amount_type == 'percent' && not tax.price_include)
# == (tax.amount_type == 'division' && tax.price_include)
# N.B: don't use the with_context if force_price_include already False in context
if 'force_price_include' not in self._context or self._context['force_price_include']:
tax = tax.with_context(force_price_include=False)
# In case of price_included tax, subtract the amount to the corresponding
# gap between the current base and the next one.
amount = tax._compute_amount(base, price_unit, quantity, product, partner)
if not tax.price_include or not base_gaps:
return amount
# Compute the new gap after subtracting of the tax amount
new_gap = base_gaps[-1] - amount
# If the newly computed gap is very close of zero, return the current gap to avoid
# rounding issues (see comments above base_gaps).
if is_zero(new_gap, prec):
return base_gaps.pop()
# Update the current gap with the new one
base_gaps[-1] = new_gap
return amount
taxes_vals = []
for tax in taxes:
tax_amount = compute_amount(tax)
tax_amount = tax._compute_amount(base, price_unit, quantity, product, partner)
if not round_tax:
tax_amount = round(tax_amount, prec)
else:
tax_amount = currency.round(tax_amount)
# Suppose:
# seq | amount | incl | incl_base | base | amount
# -----------------------------------------------
# 1 | 10 % | t | t | 100.0 | 10.0
# -----------------------------------------------
# ... the next computation must be done using 100.0 + 10.0 = 110.0 as base but
# the tax base of this tax will be 100.0.
if tax.price_include:
total_excluded -= tax_amount
base -= tax_amount
else:
total_included += tax_amount
# Keep base amount used for the current tax
tax_base = base
if tax.include_base_amount:
base += tax_amount
# The total_included amount is computed as the sum of total_excluded with all tax_amount
total_included += tax_amount
taxes_vals.append({
taxes.append({
'id': tax.id,
'name': tax.with_context(**{'lang': partner.lang} if partner else {}).name,
'amount': sign * tax_amount,
'base': round(sign * tax_base, prec),
'amount': tax_amount,
'base': tax_base,
'sequence': tax.sequence,
'account_id': tax.account_id.id,
'refund_account_id': tax.refund_account_id.id,
@@ -1112,10 +981,10 @@ class AccountTax(models.Model):
})
return {
'taxes': taxes_vals,
'total_excluded': sign * (currency.round(total_excluded) if round_total else total_excluded),
'total_included': sign * (currency.round(total_included) if round_total else total_included),
'base': round(sign * base, prec),
'taxes': sorted(taxes, key=lambda k: k['sequence']),
'total_excluded': currency.round(total_excluded) if round_total else total_excluded,
'total_included': currency.round(total_included) if round_total else total_included,
'base': base,
}
@api.model
+8 -1
View File
@@ -3,6 +3,7 @@
import json
import re
import uuid
from functools import partial
from lxml import etree
from dateutil.relativedelta import relativedelta
@@ -10,6 +11,7 @@ from werkzeug.urls import url_encode
from odoo import api, exceptions, fields, models, _
from odoo.tools import float_is_zero, float_compare, pycompat
from odoo.tools.misc import formatLang
from odoo.exceptions import AccessError, UserError, RedirectWarning, ValidationError, Warning
@@ -1407,13 +1409,18 @@ class AccountInvoice(models.Model):
@api.multi
def _get_tax_amount_by_group(self):
self.ensure_one()
currency = self.currency_id or self.company_id.currency_id
fmt = partial(formatLang, self.with_context(lang=self.partner_id.lang).env, currency_obj=currency)
res = {}
for line in self.tax_line_ids:
res.setdefault(line.tax_id.tax_group_id, {'base': 0.0, 'amount': 0.0})
res[line.tax_id.tax_group_id]['amount'] += line.amount
res[line.tax_id.tax_group_id]['base'] += line.base
res = sorted(res.items(), key=lambda l: l[0].sequence)
res = [(l[0].name, l[1]['amount'], l[1]['base']) for l in res]
res = [(
r[0].name, r[1]['amount'], r[1]['base'],
fmt(r[1]['amount']), fmt(r[1]['base']),
) for r in res]
return res
+29 -256
View File
@@ -1,7 +1,4 @@
# -*- coding: utf-8 -*-
from odoo.addons.account.tests.account_test_users import AccountTestUsers
import time
@@ -28,12 +25,6 @@ class TestTax(AccountTestUsers):
'amount': 10,
'sequence': 3,
})
self.percent_tax_bis = self.tax_model.create({
'name': "Percent tax bis",
'amount_type': 'percent',
'amount': 21,
'sequence': 3,
})
self.division_tax = self.tax_model.create({
'name': "Division tax",
'amount_type': 'division',
@@ -74,125 +65,47 @@ class TestTax(AccountTestUsers):
self.bank_account = self.bank_journal.default_debit_account_id
self.expense_account = self.env['account.account'].search([('user_type_id.type', '=', 'payable')], limit=1) #Should be done by onchange later
def _check_compute_all_results(self, base, total_included, total_excluded, taxes, res):
self.assertAlmostEqual(res['base'], base)
self.assertAlmostEqual(res['total_included'], total_included)
self.assertAlmostEqual(res['total_excluded'], total_excluded)
for i in range(0, len(taxes)):
self.assertAlmostEqual(res['taxes'][i]['base'], taxes[i][0])
self.assertAlmostEqual(res['taxes'][i]['amount'], taxes[i][1])
def test_tax_group_of_group_tax(self):
self.fixed_tax.include_base_amount = True
self.group_tax.include_base_amount = True
self.group_of_group_tax.include_base_amount = True
res = self.group_of_group_tax.compute_all(200.0)
self._check_compute_all_results(
220, # 'base'
263, # 'total_included'
200, # 'total_excluded'
[
# base , amount | seq | amount | incl | incl_base
# ---------------------------------------------------
(200.0, 10.0), # | 1 | 10 | | t
(210.0, 21.0), # | 3 | 10% | |
(210.0, 10.0), # | 1 | 10 | | t
(220.0, 22.0), # | 3 | 10% | |
# ---------------------------------------------------
],
res
)
self.assertEquals(res['total_excluded'], 200.0)
# After calculation of first group
# base = 210
# total_included = 231
# Base of the first grouped is passed
# Base after the second group (220) is dropped.
# Base of the group of groups is passed out,
# so we obtain base as after first group
self.assertEquals(res['base'], 210.0)
self.assertEquals(res['total_included'], 263.0)
def test_tax_group(self):
res = self.group_tax.compute_all(200.0)
self._check_compute_all_results(
200, # 'base'
230, # 'total_included'
200, # 'total_excluded'
[
# base , amount | seq | amount | incl | incl_base
# ---------------------------------------------------
(200.0, 10.0), # | 1 | 10 | |
(200.0, 20.0), # | 3 | 10% | |
# ---------------------------------------------------
],
res
)
self.assertEquals(res['total_excluded'], 200.0)
self.assertEquals(res['total_included'], 230.0)
self.assertEquals(len(res['taxes']), 2)
self.assertEquals(res['taxes'][0]['amount'], 10.0)
self.assertEquals(res['taxes'][1]['amount'], 20.0)
def test_tax_percent_division(self):
self.division_tax.price_include = True
self.division_tax.include_base_amount = True
res_division = self.division_tax.compute_all(200.0)
self._check_compute_all_results(
220, # 'base'
220, # 'total_included'
200, # 'total_excluded'
[
# base , amount | seq | amount | incl | incl_base
# ---------------------------------------------------
(200.0, 20.0), # | 4 | 10/ | t | t
# ---------------------------------------------------
],
res_division
)
self.percent_tax.price_include = False
self.percent_tax.include_base_amount = False
res_division = self.division_tax.compute_all(200.0)
res_percent = self.percent_tax.compute_all(200.0)
self._check_compute_all_results(
200, # 'base'
220, # 'total_included'
200, # 'total_excluded'
[
# base , amount | seq | amount | incl | incl_base
# ---------------------------------------------------
(200.0, 20.0), # | 3 | 10% | |
# ---------------------------------------------------
],
res_percent
)
self.assertEquals(res_division['taxes'][0]['amount'], 20.0)
self.assertEquals(res_percent['taxes'][0]['amount'], 20.0)
self.division_tax.price_include = False
self.division_tax.include_base_amount = False
res_division = self.division_tax.compute_all(200.0)
self._check_compute_all_results(
200, # 'base'
222.22, # 'total_included'
200, # 'total_excluded'
[
# base , amount | seq | amount | incl | incl_base
# ---------------------------------------------------
(200.0, 22.22), # | 4 | 10/ | |
# ---------------------------------------------------
],
res_division
)
self.percent_tax.price_include = True
self.percent_tax.include_base_amount = True
res_division = self.division_tax.compute_all(200.0)
res_percent = self.percent_tax.compute_all(200.0)
self._check_compute_all_results(
200, # 'base'
200, # 'total_included'
181.82, # 'total_excluded'
[
# base , amount | seq | amount | incl | incl_base
# ---------------------------------------------------
(181.82, 18.18), # | 3 | 10% | t | t
# ---------------------------------------------------
],
res_percent
)
self.percent_tax_bis.price_include = True
self.percent_tax_bis.include_base_amount = True
res_percent = self.percent_tax_bis.compute_all(7.0)
self._check_compute_all_results(
7.0, # 'base'
7.0, # 'total_included'
5.79, # 'total_excluded'
[
# base , amount | seq | amount | incl | incl_base
# ---------------------------------------------------
(5.79, 1.21), # | 3 | 21% | t | t
# ---------------------------------------------------
],
res_percent
)
self.assertEquals(res_division['taxes'][0]['amount'], 22.22)
self.assertEquals(res_percent['taxes'][0]['amount'], 18.18)
def test_tax_sequence_normalized_set(self):
self.division_tax.sequence = 1
@@ -200,103 +113,19 @@ class TestTax(AccountTestUsers):
self.percent_tax.sequence = 3
taxes_set = (self.group_tax | self.division_tax)
res = taxes_set.compute_all(200.0)
self._check_compute_all_results(
200, # 'base'
252.22, # 'total_included'
200, # 'total_excluded'
[
# base , amount | seq | amount | incl | incl_base
# ---------------------------------------------------
(200.0, 22.22), # | 1 | 10/ | |
(200.0, 10.0), # | 2 | 10 | |
(200.0, 20.0), # | 3 | 10% | |
# ---------------------------------------------------
],
res
)
self.assertEquals(res['taxes'][0]['amount'], 22.22)
self.assertEquals(res['taxes'][1]['amount'], 10.0)
self.assertEquals(res['taxes'][2]['amount'], 20.0)
def test_tax_include_base_amount(self):
self.fixed_tax.include_base_amount = True
res = self.group_tax.compute_all(200.0)
self._check_compute_all_results(
210, # 'base'
231, # 'total_included'
200, # 'total_excluded'
[
# base , amount | seq | amount | incl | incl_base
# ---------------------------------------------------
(200.0, 10.0), # | 1 | 10 | | t
(210.0, 21.0), # | 3 | 10% | |
# ---------------------------------------------------
],
res
)
self.fixed_tax.price_include = True
self.fixed_tax.include_base_amount = False
res = self.fixed_tax.compute_all(100.0, quantity=2.0)
self._check_compute_all_results(
180, # 'base'
200, # 'total_included'
180, # 'total_excluded'
[
# base , amount | seq | amount | incl | incl_base
# ---------------------------------------------------
(180.0, 20.0), # | 1 | 20 | | t
# ---------------------------------------------------
],
res
)
def test_tax_include_base_amount_2(self):
self.percent_tax.price_include = True
self.percent_tax.amount = 21.0
res = self.percent_tax.compute_all(7.0)
self._check_compute_all_results(
5.79, # 'base'
7.0, # 'total_included'
5.79, # 'total_excluded'
[
# base , amount | seq | amount | incl | incl_base
# ---------------------------------------------------
(5.79, 1.21), # | 3 | 10% | t |
# ---------------------------------------------------
],
res
)
def test_tax_include_base_amount_3(self):
self.percent_tax.price_include = True
self.percent_tax.amount = 20.0
res = self.percent_tax.compute_all(399.99)
self._check_compute_all_results(
333.33, # 'base'
399.99, # 'total_included'
333.33, # 'total_excluded'
[
# base , amount | seq | amount | incl | incl_base
# ---------------------------------------------------
(333.33, 66.66), # | 3 | 10% | t |
# ---------------------------------------------------
],
res
)
self.assertEquals(res['total_included'], 231.0)
def test_tax_currency(self):
self.division_tax.amount = 15.0
res = self.division_tax.compute_all(200.0, currency=self.env.ref('base.VEF'))
self._check_compute_all_results(
200, # 'base'
235.2941, # 'total_included'
200, # 'total_excluded'
[
# base , amount | seq | amount | incl | incl_base
# ---------------------------------------------------
(200.0, 35.2941), # | 4 | 15/ | |
# ---------------------------------------------------
],
res
)
self.assertAlmostEqual(res['total_included'], 235.2941)
def test_tax_move_lines_creation(self):
""" Test that creating a move.line with tax_ids generates the tax move lines and adjust line amount when a tax is price_include """
@@ -327,7 +156,6 @@ class TestTax(AccountTestUsers):
}
move = self.env['account.move'].with_context(apply_taxes=True).create(vals)
aml_fixed_tax = move.line_ids.filtered(lambda l: l.tax_line_id.id == self.fixed_tax.id)
aml_percent_tax = move.line_ids.filtered(lambda l: l.tax_line_id.id == self.percent_tax.id)
aml_fixed_tax_bis = move.line_ids.filtered(lambda l: l.tax_line_id.id == self.fixed_tax_bis.id)
@@ -337,62 +165,7 @@ class TestTax(AccountTestUsers):
self.assertEquals(aml_percent_tax.credit, 20)
self.assertEquals(len(aml_fixed_tax_bis), 1)
self.assertEquals(aml_fixed_tax_bis.credit, 15)
aml_with_taxes = move.line_ids.filtered(lambda l: set(l.tax_ids.ids) == set([self.group_tax.id, self.fixed_tax_bis.id]))
self.assertEquals(len(aml_with_taxes), 1)
self.assertEquals(aml_with_taxes.credit, 190)
def test_advanced_taxes_computation_0(self):
'''Test more advanced taxes computation (see issue 34471).'''
tax_1 = self.env['account.tax'].create({
'name': 'test_advanced_taxes_computation_0_1',
'amount_type': 'percent',
'amount': 10,
'price_include': True,
'include_base_amount': True,
'sequence': 1,
})
tax_2 = self.env['account.tax'].create({
'name': 'test_advanced_taxes_computation_0_2',
'amount_type': 'percent',
'amount': 10,
'sequence': 2,
})
tax_3 = self.env['account.tax'].create({
'name': 'test_advanced_taxes_computation_0_3',
'amount_type': 'percent',
'amount': 10,
'price_include': True,
'sequence': 3,
})
tax_4 = self.env['account.tax'].create({
'name': 'test_advanced_taxes_computation_0_4',
'amount_type': 'percent',
'amount': 10,
'sequence': 4,
})
tax_5 = self.env['account.tax'].create({
'name': 'test_advanced_taxes_computation_0_5',
'amount_type': 'percent',
'amount': 10,
'price_include': True,
'sequence': 5,
})
taxes = tax_1 + tax_2 + tax_3 + tax_4 + tax_5
res = taxes.compute_all(132.0)
self._check_compute_all_results(
110, # 'base'
154, # 'total_included'
100, # 'total_excluded'
[
# base , amount | seq | amount | incl | incl_base
# ---------------------------------------------------
(100.0, 10.0), # | 1 | 10% | t | t
(110.0, 11.0), # | 2 | 10% | |
(110.0, 11.0), # | 3 | 10% | t |
(110.0, 11.0), # | 4 | 10% | |
(110.0, 11.0), # | 5 | 10% | t |
# ---------------------------------------------------
],
res
)
+3 -3
View File
@@ -115,18 +115,18 @@
<t t-if="len(o.tax_line_ids) == 1 and o.amount_untaxed == amount_by_group[2]">
<td><span t-esc="amount_by_group[0]"/></td>
<td class="text-right">
<span t-esc="amount_by_group[1]" t-options='{"widget": "monetary", "display_currency": o.currency_id}'/>
<span t-esc="amount_by_group[3]"/>
</td>
</t>
<t t-else="">
<td>
<span t-esc="amount_by_group[0]"/>
<span>&amp;nbsp;<span>on</span>
<t t-esc="amount_by_group[2]" t-options='{"widget": "monetary", "display_currency": o.currency_id}'/>
<t t-esc="amount_by_group[4]"/>
</span>
</td>
<td class="text-right">
<span t-esc="amount_by_group[1]" t-options='{"widget": "monetary", "display_currency": o.currency_id}'/>
<span t-esc="amount_by_group[3]" />
</td>
</t>
</tr>
@@ -36,7 +36,6 @@
<filter name="group_by_channel" string="Channel" domain="[]" context="{'group_by':'channel_id'}"/>
<filter name="group_by_operator" string="Operator" domain="[('partner_id','!=', False)]" context="{'group_by':'partner_id'}"/>
<separator orientation="vertical" />
<filter name="group_by_hour" string="Creation date (hour)" domain="[]" context="{'group_by':'start_date_hour'}"/>
<filter name="group_by_day" string="Creation date (day)" domain="[]" context="{'group_by':'start_date:day'}"/>
<filter name="group_by_week" string="Creation date (week)" domain="[]" context="{'group_by':'start_date:week'}"/>
<filter name="group_by_month" string="Creation date (month)" domain="[]" context="{'group_by':'start_date:month'}" />
+60 -1
View File
@@ -169,8 +169,9 @@ class AccountFrFec(models.TransientModel):
'''
sql_query += '''
GROUP BY aml.account_id
GROUP BY aml.account_id, aat.type
HAVING sum(aml.balance) != 0
AND aat.type not in ('receivable', 'payable')
'''
formatted_date_from = self.date_from.replace('-', '')
date_from = datetime.strptime(self.date_from, DEFAULT_SERVER_DATE_FORMAT)
@@ -208,6 +209,64 @@ class AccountFrFec(models.TransientModel):
unaffected_earnings_results[5] = unaffected_earnings_account.name
w.writerow(unaffected_earnings_results)
# INITIAL BALANCE - receivable/payable
sql_query = '''
SELECT
'OUV' AS JournalCode,
'Balance initiale' AS JournalLib,
'OUVERTURE/' || %s AS EcritureNum,
%s AS EcritureDate,
MIN(aa.code) AS CompteNum,
replace(MIN(aa.name), '|', '/') AS CompteLib,
CASE WHEN rp.ref IS null OR rp.ref = ''
THEN COALESCE('ID ' || rp.id, '')
ELSE rp.ref
END
AS CompAuxNum,
COALESCE(replace(rp.name, '|', '/'), '') AS CompAuxLib,
'-' AS PieceRef,
%s AS PieceDate,
'/' AS EcritureLib,
replace(CASE WHEN sum(aml.balance) <= 0 THEN '0,00' ELSE to_char(SUM(aml.balance), '999999999999999D99') END, '.', ',') AS Debit,
replace(CASE WHEN sum(aml.balance) >= 0 THEN '0,00' ELSE to_char(-SUM(aml.balance), '999999999999999D99') END, '.', ',') AS Credit,
'' AS EcritureLet,
'' AS DateLet,
%s AS ValidDate,
'' AS Montantdevise,
'' AS Idevise,
MIN(aa.id) AS CompteID
FROM
account_move_line aml
LEFT JOIN account_move am ON am.id=aml.move_id
LEFT JOIN res_partner rp ON rp.id=aml.partner_id
JOIN account_account aa ON aa.id = aml.account_id
LEFT JOIN account_account_type aat ON aa.user_type_id = aat.id
WHERE
am.date < %s
AND am.company_id = %s
AND aat.include_initial_balance = 't'
AND (aml.debit != 0 OR aml.credit != 0)
'''
# For official report: only use posted entries
if self.export_type == "official":
sql_query += '''
AND am.state = 'posted'
'''
sql_query += '''
GROUP BY aml.account_id, aat.type, rp.ref, rp.id
HAVING sum(aml.balance) != 0
AND aat.type in ('receivable', 'payable')
'''
self._cr.execute(
sql_query, (formatted_date_year, formatted_date_from, formatted_date_from, formatted_date_from, self.date_from, company.id))
for row in self._cr.fetchall():
listrow = list(row)
account_id = listrow.pop()
w.writerow([s.encode("utf-8") for s in listrow])
# LINES
sql_query = '''
SELECT
+1 -1
View File
@@ -42,7 +42,7 @@ class TestMailGroup(TestMail):
def mail_group_message_get_recipient_values(self, notif_message=None, recipient_ids=None):
return self.env['mail.thread'].message_get_recipient_values(notif_message=notif_message, recipient_ids=recipient_ids)
cls.env['mail.channel']._patch_method('message_get_recipient_values', mail_group_message_get_recipient_values)
super(TestMail, cls).tearDownClass()
super(TestMailGroup, cls).tearDownClass()
@mute_logger('odoo.addons.base.ir.ir_model', 'odoo.models')
def test_access_rights_public(self):
@@ -321,7 +321,6 @@ snippets_editor.Class.include({
switch_theme.last = theme_params;
$body.removeClass(all_classes).addClass(theme_params.className);
switch_images(theme_params, $editable_area);
var $old_layout = $editable_area.find(".o_layout");
// This wrapper structure is the only way to have a responsive and
@@ -351,8 +350,9 @@ snippets_editor.Class.include({
$contents = $editable_area.contents();
}
$editable_area.empty().append($new_layout);
$new_wrapper_content.append($contents);
switch_images(theme_params, $new_wrapper_content);
$editable_area.empty().append($new_layout);
$old_layout.remove();
if (first_choice) {
@@ -71,8 +71,7 @@
</tr>
<tr t-foreach="o.move_raw_ids" t-as="line">
<td>
<span t-field="line.product_id.code"/>
<span t-field="line.product_id.name"/>
<span t-field="line.product_id"/>
</td>
<td class="text-right">
<span t-field="line.product_uom_qty"/>
+1 -1
View File
@@ -66,5 +66,5 @@ class StripeController(http.Controller):
response = tx._create_stripe_charge(tokenid=post['tokenid'], email=post['email'])
_logger.info('Stripe: entering form_feedback with post data %s', pprint.pformat(response))
if response:
request.env['payment.transaction'].sudo().form_feedback(response, 'stripe')
request.env['payment.transaction'].sudo().with_context(lang=None).form_feedback(response, 'stripe')
return post.pop('return_url', '/')
+47 -133
View File
@@ -88,7 +88,7 @@ exports.PosModel = Backbone.Model.extend({
// We fetch the backend data on the server asynchronously. this is done only when the pos user interface is launched,
// Any change on this data made on the server is thus not reflected on the point of sale until it is relaunched.
// when all the data has loaded, we compute some stuff, and declare the Pos ready to be used.
// when all the data has loaded, we compute some stuff, and declare the Pos ready to be used.
this.ready = this.load_server_data().then(function(){
return self.after_load_server_data();
});
@@ -205,7 +205,7 @@ exports.PosModel = Backbone.Model.extend({
},
},{
model: 'account.tax',
fields: ['name','amount', 'price_include', 'include_base_amount', 'amount_type', 'children_tax_ids', 'sequence'],
fields: ['name','amount', 'price_include', 'include_base_amount', 'amount_type', 'children_tax_ids'],
domain: null,
loaded: function(self, taxes){
self.taxes = taxes;
@@ -1647,159 +1647,74 @@ exports.Orderline = Backbone.Model.extend({
return tax;
},
/**
* Mirror JS method of:
* _compute_amount in addons/account/models/account.py
*/
_compute_all: function(tax, base_amount, quantity, price_include) {
if(price_include === undefined)
price_include = tax.price_include;
_compute_all: function(tax, base_amount, quantity) {
if (tax.amount_type === 'fixed') {
var sign_base_amount = base_amount >= 0 ? 1 : -1;
return (Math.abs(tax.amount) * sign_base_amount) * quantity;
}
if ((tax.amount_type === 'percent' && !price_include) || (tax.amount_type === 'division' && tax.price_include)){
if ((tax.amount_type === 'percent' && !tax.price_include) || (tax.amount_type === 'division' && tax.price_include)){
return base_amount * tax.amount / 100;
}
if (tax.amount_type === 'percent' && price_include){
if (tax.amount_type === 'percent' && tax.price_include){
return base_amount - (base_amount / (1 + tax.amount / 100));
}
if (tax.amount_type === 'division' && !price_include) {
if (tax.amount_type === 'division' && !tax.price_include) {
return base_amount / (1 - tax.amount / 100) - base_amount;
}
return false;
},
/**
* Mirror JS method of:
* compute_all in addons/account/models/account.py
*
* Read comments in the python side method for more details about each sub-methods.
*/
compute_all: function(taxes, price_unit, quantity, currency_rounding, no_map_tax) {
var self = this;
// 1) Flatten the taxes.
var _collect_taxes = function(taxes, all_taxes){
taxes.sort(function (tax1, tax2) {
return tax1.sequence - tax2.sequence;
});
_(taxes).each(function(tax){
if(tax.amount_type === 'group')
all_taxes = _collect_taxes(tax.children_tax_ids, all_taxes);
else
all_taxes.push(tax);
});
return all_taxes;
}
var collect_taxes = function(taxes){
return _collect_taxes(taxes, []);
}
taxes = collect_taxes(taxes);
// 2) Avoid dealing with taxes mixing price_include=False && include_base_amount=True
// with price_include=True
var base_excluded_flag = false; // price_include=False && include_base_amount=True
var included_flag = false; // price_include=True
_(taxes).each(function(tax){
if(tax.price_include)
included_flag = true;
else if(tax.include_base_amount)
base_excluded_flag = true
if(base_excluded_flag && included_flag)
throw new Error('Unable to mix any taxes being price included with taxes affecting the base amount but not included in price.');
});
// 3) Deal with the rounding methods
var list_taxes = [];
var currency_rounding_bak = currency_rounding;
if (this.pos.company.tax_calculation_rounding_method == "round_globally"){
currency_rounding = currency_rounding * 0.00001;
}
// 4) Iterate the taxes in the reversed sequence order to retrieve the initial base of the computation.
var base = round_pr(price_unit * quantity, currency_rounding);
var base_gaps = [];
var recompute_base = function(base_amount, fixed_amount, percent_amount){
if(fixed_amount === 0.0 && percent_amount === 0.0)
return base_amount;
var new_base = (base_amount - fixed_amount) / (1.0 + percent_amount / 100.0);
base_gaps.push(base_amount - new_base);
return new_base;
}
var sign = 1;
if(base < 0){
base = -base;
sign = -1;
}
var incl_fixed_amount = 0.0;
var incl_percent_amount = 0.0;
_(taxes.reverse()).each(function(tax){
if(tax.include_base_amount){
base = recompute_base(base, incl_fixed_amount, incl_percent_amount);
incl_fixed_amount = 0.0;
incl_percent_amount = 0.0;
var total_excluded = round_pr(price_unit * quantity, currency_rounding);
var total_included = total_excluded;
var base = total_excluded;
_(taxes).each(function(tax) {
if (!no_map_tax){
tax = self._map_tax_fiscal_position(tax);
}
if(tax.price_include){
if(tax.amount_type === 'fixed')
incl_fixed_amount += quantity * tax.amount;
else if(tax.amount_type === 'percent')
incl_percent_amount += tax.amount;
if (!tax){
return;
}
});
if (tax.amount_type === 'group'){
var ret = self.compute_all(tax.children_tax_ids, price_unit, quantity, currency_rounding);
total_excluded = ret.total_excluded;
base = ret.total_excluded;
total_included = ret.total_included;
list_taxes = list_taxes.concat(ret.taxes);
}
else {
var tax_amount = self._compute_all(tax, base, quantity);
tax_amount = round_pr(tax_amount, currency_rounding);
base = recompute_base(base, incl_fixed_amount, incl_percent_amount);
var total_excluded = base;
var total_included = base;
// 5) Iterate the taxes in the sequence order to fill missing base/amount values.
var compute_amount = function(tax){
var amount = self._compute_all(tax, base, quantity, false);
if(!tax.price_include || base_gaps.length == 0)
return amount;
var new_gap = base_gaps[base_gaps.length - 1] - amount;
if(round_pr(new_gap, currency_rounding) === 0.0)
return base_gaps.pop();
base_gaps[base_gaps.length - 1] = new_gap;
return amount;
};
var taxes_vals = [];
_(taxes.reverse()).each(function(tax){
var tax_amount = compute_amount(tax);
tax_amount = round_pr(tax_amount, currency_rounding);
var tax_base = base;
if(tax.include_base_amount)
base += tax_amount;
total_included += tax_amount;
var tax_vals = {
id: tax.id,
amount: sign * tax_amount,
name: tax.name,
};
taxes_vals.push(tax_vals);
if (tax_amount){
if (tax.price_include) {
total_excluded -= tax_amount;
base -= tax_amount;
}
else {
total_included += tax_amount;
}
if (tax.include_base_amount) {
base += tax_amount;
}
var data = {
id: tax.id,
amount: tax_amount,
name: tax.name,
};
list_taxes.push(data);
}
}
});
return {
taxes: taxes_vals,
total_excluded: sign * round_pr(total_excluded, currency_rounding_bak),
total_included: sign * round_pr(total_included, currency_rounding_bak)
taxes: list_taxes,
total_excluded: round_pr(total_excluded, currency_rounding_bak),
total_included: round_pr(total_included, currency_rounding_bak)
};
},
get_all_prices: function(){
@@ -1817,7 +1732,6 @@ exports.Orderline = Backbone.Model.extend({
return t.id === el;
}));
});
product_taxes = _.map(product_taxes, this._map_tax_fiscal_position.bind(this));
var all_taxes = this.compute_all(product_taxes, price_unit, this.get_quantity(), this.pos.currency.rounding);
_(all_taxes.taxes).each(function(tax) {
+3 -3
View File
@@ -39,7 +39,7 @@
<t t-set="menu_categoryclasses" t-value="'fa-flag-checkered'"/>
<t t-call="web_planner.page">
<t t-set="page_title">Your Objectives</t>
<t t-set="page_menutitle" t-value="'Objectives'"/>
<t t-set="page_menutitle">Objectives</t>
<p class="text-muted">
It's essential to be clear about why you want to use Odoo Project and what your goals are.
Indeed, there are many ways to manage a project, to find the best one for you, you need to know exactly what you want to achieve. And later on, we will hopefully transform your objectives into real improvements for your company.
@@ -83,7 +83,7 @@
<t t-call="web_planner.page">
<t t-set="page_title">Your Activities</t>
<t t-set="page_menutitle" t-value="'Activities'"/>
<t t-set="page_menutitle">Activities</t>
<p class="text-muted">The Odoo Project app can be used to manage many activities, from the development of a new product to the daily operations of a customer support. With some creativity, it can even be used to manage your marketing communications or personal projects. But just because it can be done doesn't mean it's always a good idea: let's start by helping you understand what can be a good project.</p>
<div class="row">
@@ -116,7 +116,7 @@
<t t-call="web_planner.page">
<t t-set="page_title">Your Projects</t>
<t t-set="page_menutitle" t-value="'Projects'"/>
<t t-set="page_menutitle">Projects</t>
<p class="text-muted">Depending on what you need and how you want to operate, there are several ways to work with Odoo. First, decide if you want to think in terms of tasks. Then, activate the Timesheets app if you need it.</p>
<h4>1. Learn about Tasks and Timesheets.</h4>
<div class="row">
+1 -1
View File
@@ -363,7 +363,7 @@
<t t-set="menu_categoryclasses" t-value="'fa-recycle'"/>
<t t-call="web_planner.page">
<t t-set="page_title">Reordering Rules</t>
<t t-set="page_menutitle" t-value="'Reordering Rules'"/>
<t t-set="page_menutitle">Reordering Rules</t>
<p class="text-muted">
A good inventory management aims to optimize stock levels: not too low (or
you may find yourself out of stock) and not too high (your products occupy
+2
View File
@@ -500,6 +500,8 @@ class ProductTemplate(models.Model):
done_moves = self.env['stock.move'].search([('product_id', 'in', updated.mapped('product_variant_ids').ids)], limit=1)
if done_moves:
raise UserError(_("You can not change the unit of measure of a product that has already been used in a done stock move. If you need to change the unit of measure, you may deactivate this product."))
if 'type' in vals and vals['type'] != 'product' and sum(self.mapped('nbr_reordering_rules')) != 0:
raise UserError(_('You still have some active reordering rules on this product. Please archive or delete them first.'))
if any('type' in vals and vals['type'] != prod_tmpl.type for prod_tmpl in self):
existing_move_lines = self.env['stock.move.line'].search([
('product_id', 'in', self.mapped('product_variant_ids').ids),
+7 -3
View File
@@ -607,9 +607,13 @@ class StockMove(models.Model):
if not self.product_id or self.product_qty < 0.0:
self.product_qty = 0.0
if self.product_qty < self._origin.product_qty:
return {'warning': _("By changing this quantity here, you accept the "
"new quantity as complete: Odoo will not "
"automatically generate a back order.")}
warning_mess = {
'title': _('Quantity decreased!'),
'message' : _("By changing this quantity here, you accept the "
"new quantity as complete: Odoo will not "
"automatically generate a back order."),
}
return {'warning': warning_mess}
@api.onchange('product_id')
def onchange_product_id(self):
@@ -349,7 +349,7 @@ var DateField = Field.extend(/** @lends instance.web.search.DateField# */{
var t, v;
try {
t = (this.attrs && this.attrs.type === 'datetime') ? 'datetime' : 'date';
v = field_utils.parse[t](needle, {type: t});
v = field_utils.parse[t](needle, {type: t}, {timezone: true});
} catch (e) {
return $.when(null);
}
@@ -1257,9 +1257,13 @@ var FieldMany2Many = FieldX2Many.extend({
*
* @override
* @private
* @param {OdooEvent|MouseEvent} ev this event comes either from the 'Add
* record' link in the list editable renderer, or from the 'Create' button
* in the kanban view
*/
_onAddRecord: function () {
_onAddRecord: function (ev) {
var self = this;
ev.stopPropagation();
var domain = this.record.getDomain({fieldName: this.name});
@@ -54,7 +54,7 @@ var CrashManager = core.Class.extend({
new (handler)(this, error).display();
return;
}
if (error.data.name === "openerp.http.SessionExpiredException" || error.data.name === "werkzeug.exceptions.Forbidden") {
if (error.data.name === "odoo.http.SessionExpiredException" || error.data.name === "werkzeug.exceptions.Forbidden") {
this.show_warning({type: _t("Odoo Session Expired"), data: {message: _t("Your Odoo session expired. Please refresh the current web page.")}});
return;
}
@@ -3458,6 +3458,7 @@ var BasicModel = AbstractModel.extend({
domain: list.domain,
context: list.context,
groupBy: list.groupedBy,
orderBy: list.orderedBy,
lazy: true,
})
.then(function (groups) {
@@ -5521,6 +5521,65 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('many2many list in a one2many opened by a many2one', function (assert) {
assert.expect(1);
this.data.turtle.records[1].turtle_trululu = 2;
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="turtles">' +
'<tree editable="bottom">' +
'<field name="turtle_trululu"/>' +
'</tree>' +
'</field>' +
'</form>',
res_id: 1,
archs: {
"partner,false,form": '<form string="P">' +
'<field name="timmy"/>' +
'</form>',
"partner_type,false,list": '<tree editable="bottom">' +
'<field name="display_name"/>' +
'</tree>',
"partner_type,false,search": '<search>' +
'</search>',
},
viewOptions: {
mode: 'edit',
},
mockRPC: function (route, args) {
if (route === '/web/dataset/call_kw/partner/get_formview_id') {
return $.when(false);
}
if (args.method === 'write') {
assert.deepEqual(args.args[1].timmy, [[6, false, [12]]],
'should properly write ids')
}
return this._super.apply(this, arguments);
},
});
// edit the first partner in the one2many partner form view
form.$('.o_data_row:first td.o_data_cell').click();
// open form view for many2one
form.$('.o_external_button').click();
// click on add, to add a new partner in the m2m
$('.modal .o_field_x2many_list_row_add a').click();
// select the partner_type 'gold' (this closes the 2nd modal)
$('.modal td:contains(gold)').click();
// confirm the changes in the modal
$('.modal .modal-footer .btn-primary').click();
form.$buttons.find('.o_form_button_save').click();
form.destroy();
});
QUnit.test('one2many (who contains display_name) with tree view and without form view', function (assert) {
assert.expect(1);
@@ -707,6 +707,23 @@ var MockServer = Class.extend({
return res;
});
if (kwargs.orderby) {
// only consider first sorting level
kwargs.orderby = kwargs.orderby.split(',')[0];
var fieldName = kwargs.orderby.split(' ')[0];
var order = kwargs.orderby.split(' ')[1];
result.sort(function (g1, g2) {
if (g1[fieldName] < g2[fieldName]) {
return order === 'ASC' ? -1 : 1;
}
if (g1[fieldName] > g2[fieldName]) {
return order === 'ASC' ? 1 : -1;
}
return 0;
});
}
return result;
},
/**
+37 -1
View File
@@ -637,6 +637,42 @@ QUnit.module('Views', {
list.destroy();
});
QUnit.test('groups can be sorted on aggregates', function (assert) {
assert.expect(10);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
groupBy: ['foo'],
arch: '<tree editable="bottom"><field name="int_field" sum="Sum"/></tree>',
mockRPC: function (route, args) {
if (args.method === 'read_group') {
assert.step(args.kwargs.orderby || 'default order');
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual(list.$('tbody .o_list_number').text(), '10517',
"initial order should be 10, 5, 17");
assert.strictEqual(list.$('tfoot td:nth(2)').text(), '32', "total should be 32");
list.$('.o_column_sortable').click(); // sort (int_field ASC)
assert.strictEqual(list.$('tfoot td:nth(2)').text(), '32', "total should still be 32");
assert.strictEqual(list.$('tbody .o_list_number').text(), '51017',
"order should be 5, 10, 17");
list.$('.o_column_sortable').click(); // sort (int_field DESC)
assert.strictEqual(list.$('tbody .o_list_number').text(), '17105',
"initial order should be 17, 10, 5");
assert.strictEqual(list.$('tfoot td:nth(2)').text(), '32', "total should still be 32");
assert.verifySteps(['default order', 'int_field ASC', 'int_field DESC']);
list.destroy();
});
QUnit.test('properly apply onchange in simple case', function (assert) {
assert.expect(2);
@@ -1695,7 +1731,7 @@ QUnit.module('Views', {
// close first level group
nbRPCs = {readGroup: 0, searchRead: 0};
envIDs = []; // the group being closed, there is no more record in the environment
list.$('.o_group_header:first').click();
list.$('.o_group_header:nth(1)').click();
assert.strictEqual(nbRPCs.readGroup, 0, "should have done no read_group");
assert.strictEqual(nbRPCs.searchRead, 0, "should have done no search_read");
@@ -95,6 +95,7 @@ QUnit.module('Views', {
domain: [["display_name","like","a"], ["display_name","ilike","piou"], ["foo","ilike","piou"]],
fields:["display_name","foo","bar"],
groupby:["bar"],
orderby: '',
lazy: true
}, "should search with the complete domain (domain + search), and group by 'bar'");
}
@@ -1039,6 +1039,7 @@ var MediaDialog = Dialog.extend({
this.final_data = [media, self.old_media];
$(document.body).trigger("media-saved", this.final_data);
$(self.old_media).trigger("save", this.final_data);
$(this.final_data).trigger('input');
// Update editor bar after image edition (in case the image change to icon or other)
_.defer(function () {
+2 -2
View File
@@ -999,7 +999,7 @@
<t t-if="request.params.get('code_not_available')" name="code_not_available">
<p class="bg-warning">This promo code is not available</p>
</t>
<form t-if="website_sale_order and website_sale_order.website_order_line" t-att-action="'/shop/pricelist%s' % (redirect and '?r=' + redirect or '')"
<form t-att-action="'/shop/pricelist%s' % (redirect and '?r=' + redirect or '')"
method="post" class="mb32" name="coupon_code">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()" />
<div class="input-group">
@@ -1499,7 +1499,7 @@
</template>
<template id="total">
<div id="cart_total" t-att-class="extra_class or ''" t-if="website_sale_order">
<div id="cart_total" t-att-class="extra_class or ''" t-if="website_sale_order and website_sale_order.website_order_line">
<div class="row" id="order_total_untaxed">
<span class="col-xs-6 text-right text-muted">Subtotal:</span>
<span class="col-xs-6 text-right-not-xs text-left-xs text-muted">
+16
View File
@@ -0,0 +1,16 @@
Belgium, 2017-11-14
METRUM SA 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,
Jonathan Nemry jonathan.nemry@metrum.lu https://github.com/JonathanNEMRY
List of contributors:
Pascal Pelzer pascal.pelzer@metrum.lu https://github.com/PascalPelzer
Jonathan Nemry jonathan.nemry@metrum.lu https://github.com/JonathanNEMRY
+11
View File
@@ -0,0 +1,11 @@
Poland, 2017-09-17
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,
Grzegorz Krukar grzegorzgk1@gmail.com https://github.com/grzekru
+9
View File
@@ -0,0 +1,9 @@
France, 2017-11-10
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,
Mehdi GHEZAL mehdi.ghezal@gmail.com https://github.com/mehdi-ghezal
+11
View File
@@ -0,0 +1,11 @@
Spain, 2017-10-24
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,
Silvio Fernandez silviofernandezmarin@gmail.com https://github.com/silviofdez
File diff suppressed because it is too large Load Diff
+23 -31
View File
@@ -305,12 +305,13 @@ class test_m2o(CreatorCase):
[[False]])
def test_basic(self):
""" Exported value is empty
""" Exported value is the name_get of the related object
"""
record = self.env['export.integer'].create({'value': 42})
name = dict(record.name_get())[record.id]
self.assertEqual(
self.export(record.id),
[['']])
[[name]])
def test_path(self):
""" Can recursively export fields of m2o via path
@@ -352,13 +353,14 @@ class test_o2m(CreatorCase):
def test_single(self):
self.assertEqual(
self.export([(0, False, {'value': 42})]),
[['']])
# name_get result
[[u'export.one2many.child:42']])
def test_single_subfield(self):
self.assertEqual(
self.export([(0, False, {'value': 42})],
fields=['value', 'value/value']),
[['', u'42']])
[[u'export.one2many.child:42', u'42']])
def test_integrate_one_in_parent(self):
self.assertEqual(
@@ -380,13 +382,9 @@ class test_o2m(CreatorCase):
def test_multiple_records_name(self):
self.assertEqual(
self.export(self.commands, fields=['const', 'value']),
[
[u'4', ''],
['', ''],
['', ''],
['', ''],
['', ''],
])
[[
u'4', u','.join(self.names)
]])
def test_multiple_records_id(self):
export = self.export(self.commands, fields=['const', 'value/.id'])
@@ -404,18 +402,14 @@ class test_o2m(CreatorCase):
def test_multiple_records_with_name_before(self):
self.assertEqual(
self.export(self.commands, fields=['const', 'value', 'value/value']),
[
[u'4', '', u'4'],
['', '', u'42'],
['', '', u'36'],
['', '', u'4'],
['', '', u'13'],
])
[[ # exports sub-fields of very first o2m
u'4', u','.join(self.names), u'4'
]])
def test_multiple_records_with_name_after(self):
self.assertEqual(
self.export(self.commands, fields=['const', 'value/value', 'value']),
[
[ # completely ignores name_get request
[u'4', u'4', ''],
['', u'42', ''],
['', u'36', ''],
@@ -466,16 +460,16 @@ class test_o2m_multiple(CreatorCase):
def test_single_per_side(self):
self.assertEqual(
self.export(child1=False, child2=[(0, False, {'value': 42})]),
[[False, '']])
[[False, u'export.one2many.child.2:42']])
self.assertEqual(
self.export(child1=[(0, False, {'value': 43})], child2=False),
[['', False]])
[[u'export.one2many.child.1:43', False]])
self.assertEqual(
self.export(child1=[(0, False, {'value': 43})],
child2=[(0, False, {'value': 42})]),
[['', '']])
[[u'export.one2many.child.1:43', u'export.one2many.child.2:42']])
def test_single_integrate_subfield(self):
fields = ['const', 'child1/value', 'child2/value']
@@ -562,13 +556,14 @@ class test_m2m(CreatorCase):
def test_single(self):
self.assertEqual(
self.export([(0, False, {'value': 42})]),
[['']])
# name_get result
[[u'export.many2many.other:42']])
def test_single_subfield(self):
self.assertEqual(
self.export([(0, False, {'value': 42})],
fields=['value', 'value/value']),
[['', u'42']])
[[u'export.many2many.other:42', u'42']])
def test_integrate_one_in_parent(self):
self.assertEqual(
@@ -590,13 +585,10 @@ class test_m2m(CreatorCase):
def test_multiple_records_name(self):
self.assertEqual(
self.export(self.commands, fields=['const', 'value']),
[
[u'4', ''],
['', ''],
['', ''],
['', ''],
['', ''],
])
[[ # FIXME: hardcoded comma, import uses config.csv_internal_sep
# resolution: remove configurable csv_internal_sep
u'4', u','.join(self.names)
]])
# essentially same as o2m, so boring
+8 -2
View File
@@ -697,8 +697,14 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
for j, val in enumerate(lines2[0]):
if val or isinstance(val, bool):
current[j] = val
# append the other lines at the end
lines += lines2[1:]
# check value of current field
if not current[i] and not isinstance(current[i], bool):
# assign xml_ids, and forget about remaining lines
xml_ids = [item[1] for item in value.name_get()]
current[i] = ','.join(xml_ids)
else:
# append the other lines at the end
lines += lines2[1:]
else:
current[i] = False
+5 -5
View File
@@ -364,15 +364,15 @@ class GeventServer(CommonServer):
import gevent
from gevent.wsgi import WSGIServer
# Set process memory limit as an extra safeguard
_, hard = resource.getrlimit(resource.RLIMIT_AS)
resource.setrlimit(resource.RLIMIT_AS, (config['limit_memory_hard'], hard))
if os.name == 'posix':
# Set process memory limit as an extra safeguard
_, hard = resource.getrlimit(resource.RLIMIT_AS)
resource.setrlimit(resource.RLIMIT_AS, (config['limit_memory_hard'], hard))
signal.signal(signal.SIGQUIT, dumpstacks)
signal.signal(signal.SIGUSR1, log_ormcache_stats)
gevent.spawn(self.watchdog)
gevent.spawn(self.watchdog)
self.httpd = WSGIServer((self.interface, self.port), self.app)
_logger.info('Evented Service (longpolling) running on %s:%s', self.interface, self.port)
try:
+1 -1
View File
@@ -1077,7 +1077,7 @@ def formatLang(env, value, digits=None, grouping=True, monetary=False, dp=False,
if isinstance(value, pycompat.string_types) and not value:
return ''
lang = env.user.company_id.partner_id.lang or 'en_US'
lang = env.context.get('lang') or env.user.company_id.partner_id.lang or 'en_US'
lang_objs = env['res.lang'].search([('code', '=', lang)])
if not lang_objs:
lang_objs = env['res.lang'].search([], limit=1)
+1 -1
View File
@@ -16,7 +16,7 @@ ofxparse==0.16
passlib==1.6.5
Pillow==3.4.1
psutil==4.3.1
psycopg2==2.7.1
psycopg2==2.7.3
pydot==1.2.3
pyldap==2.4.28
pyparsing==2.1.10