[MERGE] forward port branch saas-14 up to fbdda5437a
This commit is contained in:
@@ -648,6 +648,7 @@ 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
|
||||
@@ -661,11 +662,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 self.price_include) or (self.amount_type == 'division' and self.price_include):
|
||||
if (self.amount_type == 'percent' and not price_include) or (self.amount_type == 'division' and self.price_include):
|
||||
return base_amount * self.amount / 100
|
||||
if self.amount_type == 'percent' and self.price_include:
|
||||
if self.amount_type == 'percent' and price_include:
|
||||
return base_amount - (base_amount / (1 + self.amount / 100))
|
||||
if self.amount_type == 'division' and not self.price_include:
|
||||
if self.amount_type == 'division' and not price_include:
|
||||
return base_amount / (1 - self.amount / 100) - base_amount
|
||||
|
||||
@api.multi
|
||||
@@ -699,13 +700,58 @@ 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
|
||||
@@ -730,50 +776,93 @@ class AccountTax(models.Model):
|
||||
if not round_tax:
|
||||
prec += 5
|
||||
|
||||
base_values = self.env.context.get('base_values')
|
||||
if not base_values:
|
||||
total_excluded = total_included = base = round(price_unit * quantity, prec)
|
||||
# 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 | | |
|
||||
# || ... | .. | .. |
|
||||
# ----------------------------
|
||||
|
||||
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
|
||||
return round((base_amount - fixed_amount) / (1.0 + percent_amount / 100.0), prec)
|
||||
|
||||
base = round(price_unit * quantity, prec)
|
||||
|
||||
# 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
|
||||
else:
|
||||
total_excluded, total_included, base = base_values
|
||||
sign = 1
|
||||
|
||||
# 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
|
||||
# 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 tax.price_include:
|
||||
if tax.amount_type == 'fixed':
|
||||
incl_fixed_amount += 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)
|
||||
|
||||
tax_amount = tax._compute_amount(base, price_unit, quantity, product, partner)
|
||||
# 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 |
|
||||
# \/ ... | .. | .. |
|
||||
# ----------------------------
|
||||
taxes_vals = []
|
||||
for tax in taxes:
|
||||
# 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)
|
||||
tax_amount = tax.with_context(force_price_include=False)._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)
|
||||
|
||||
if tax.price_include:
|
||||
total_excluded -= tax_amount
|
||||
base -= tax_amount
|
||||
else:
|
||||
total_included += tax_amount
|
||||
|
||||
# Keep base amount used for the current tax
|
||||
# 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.
|
||||
tax_base = base
|
||||
|
||||
if tax.include_base_amount:
|
||||
base += tax_amount
|
||||
|
||||
taxes.append({
|
||||
# The total_included amount is computed as the sum of total_excluded with all tax_amount
|
||||
total_included += tax_amount
|
||||
|
||||
taxes_vals.append({
|
||||
'id': tax.id,
|
||||
'name': tax.with_context(**{'lang': partner.lang} if partner else {}).name,
|
||||
'amount': tax_amount,
|
||||
'base': tax_base,
|
||||
'amount': sign * tax_amount,
|
||||
'base': sign * tax_base,
|
||||
'sequence': tax.sequence,
|
||||
'account_id': tax.account_id.id,
|
||||
'refund_account_id': tax.refund_account_id.id,
|
||||
@@ -781,10 +870,10 @@ class AccountTax(models.Model):
|
||||
})
|
||||
|
||||
return {
|
||||
'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,
|
||||
'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': sign * base,
|
||||
}
|
||||
|
||||
@api.model
|
||||
|
||||
@@ -1325,6 +1325,9 @@ class AccountInvoiceLine(models.Model):
|
||||
self.price_unit = 0.0
|
||||
domain['uom_id'] = []
|
||||
else:
|
||||
# Use the purchase uom by default
|
||||
self.uom_id = self.product_id.uom_po_id
|
||||
|
||||
if part.lang:
|
||||
product = self.product_id.with_context(lang=part.lang)
|
||||
else:
|
||||
|
||||
@@ -154,7 +154,7 @@ class account_journal(models.Model):
|
||||
# optimization to read sum of balance from account_move_line
|
||||
account_ids = tuple(filter(None, [self.default_debit_account_id.id, self.default_credit_account_id.id]))
|
||||
if account_ids:
|
||||
amount_field = 'balance' if not self.currency_id else 'amount_currency'
|
||||
amount_field = 'balance' if (not self.currency_id or self.currency_id == self.company_id.currency_id) else 'amount_currency'
|
||||
query = """SELECT sum(%s) FROM account_move_line WHERE account_id in %%s AND date <= %%s;""" % (amount_field,)
|
||||
self.env.cr.execute(query, (account_ids, fields.Date.today(),))
|
||||
query_results = self.env.cr.dictfetchall()
|
||||
|
||||
@@ -398,7 +398,7 @@ class AccountMoveLine(models.Model):
|
||||
help="The move of this entry line.", index=True, required=True, auto_join=True)
|
||||
narration = fields.Text(related='move_id.narration', string='Narration')
|
||||
ref = fields.Char(related='move_id.ref', string='Reference', store=True, copy=False, index=True)
|
||||
payment_id = fields.Many2one('account.payment', string="Originator Payment", help="Payment that created this entry")
|
||||
payment_id = fields.Many2one('account.payment', string="Originator Payment", help="Payment that created this entry", copy=False)
|
||||
statement_line_id = fields.Many2one('account.bank.statement.line', index=True, string='Bank statement line reconciled with this entry', copy=False, readonly=True)
|
||||
statement_id = fields.Many2one('account.bank.statement', related='statement_line_id.statement_id', string='Statement', store=True,
|
||||
help="The bank statement used for bank reconciliation", index=True, copy=False)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo.addons.account.tests.account_test_users import AccountTestUsers
|
||||
from odoo.tests.common import TransactionCase
|
||||
from odoo.tools import float_compare
|
||||
|
||||
import time
|
||||
|
||||
|
||||
@@ -67,47 +68,110 @@ 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.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)
|
||||
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
|
||||
)
|
||||
|
||||
def test_tax_group(self):
|
||||
res = self.group_tax.compute_all(200.0)
|
||||
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)
|
||||
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
|
||||
)
|
||||
|
||||
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.assertEquals(res_division['taxes'][0]['amount'], 20.0)
|
||||
self.assertEquals(res_percent['taxes'][0]['amount'], 20.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.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.assertEquals(res_division['taxes'][0]['amount'], 22.22)
|
||||
self.assertEquals(res_percent['taxes'][0]['amount'], 18.18)
|
||||
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
|
||||
)
|
||||
|
||||
def test_tax_sequence_normalized_set(self):
|
||||
self.division_tax.sequence = 1
|
||||
@@ -115,19 +179,53 @@ class TestTax(AccountTestUsers):
|
||||
self.percent_tax.sequence = 3
|
||||
taxes_set = (self.group_tax | self.division_tax)
|
||||
res = taxes_set.compute_all(200.0)
|
||||
self.assertEquals(res['taxes'][0]['amount'], 22.22)
|
||||
self.assertEquals(res['taxes'][1]['amount'], 10.0)
|
||||
self.assertEquals(res['taxes'][2]['amount'], 20.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
|
||||
)
|
||||
|
||||
def test_tax_include_base_amount(self):
|
||||
self.fixed_tax.include_base_amount = True
|
||||
res = self.group_tax.compute_all(200.0)
|
||||
self.assertEquals(res['total_included'], 231.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
|
||||
)
|
||||
|
||||
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.assertAlmostEqual(res['total_included'], 235.2941)
|
||||
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
|
||||
)
|
||||
|
||||
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 """
|
||||
@@ -172,3 +270,58 @@ class TestTax(AccountTestUsers):
|
||||
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), # | 3 | 10% | |
|
||||
(110.0, 11.0), # | 3 | 10% | t |
|
||||
(110.0, 11.0), # | 3 | 10% | |
|
||||
(110.0, 11.0), # | 3 | 10% | t |
|
||||
# ---------------------------------------------------
|
||||
],
|
||||
res
|
||||
)
|
||||
|
||||
@@ -605,9 +605,18 @@ class Import(models.TransientModel):
|
||||
|
||||
@api.multi
|
||||
def _parse_import_data(self, data, import_fields, options):
|
||||
""" Lauch first call to _parse_import_data_recursive with an
|
||||
empty prefix. _parse_import_data_recursive will be run
|
||||
recursively for each relational field.
|
||||
"""
|
||||
return self._parse_import_data_recursive(self.res_model, '', data, import_fields, options)
|
||||
|
||||
@api.multi
|
||||
def _parse_import_data_recursive(self, model, prefix, data, import_fields, options):
|
||||
# Get fields of type date/datetime
|
||||
all_fields = self.env[self.res_model].fields_get()
|
||||
all_fields = self.env[model].fields_get()
|
||||
for name, field in all_fields.iteritems():
|
||||
name = prefix + name
|
||||
if field['type'] in ('date', 'datetime') and name in import_fields:
|
||||
# Parse date
|
||||
index = import_fields.index(name)
|
||||
@@ -624,7 +633,11 @@ class Import(models.TransientModel):
|
||||
raise ValueError(_("Column %s contains incorrect values. Error in line %d: %s") % (name, num + 1, ustr(e.message)))
|
||||
except Exception, e:
|
||||
raise ValueError(_("Error Parsing Date [%s:L%d]: %s") % (name, num + 1, ustr(e.message)))
|
||||
|
||||
# Check if the field is in import_field and is a relational (followed by /)
|
||||
# Also verify that the field name exactly match the import_field at the correct level.
|
||||
elif any(name + '/' in import_field and name == import_field.split('/')[prefix.count('/')] for import_field in import_fields):
|
||||
# Recursive call with the relational as new model and add the field name to the prefix
|
||||
self._parse_import_data_recursive(field['relation'], name + '/', data, import_fields, options)
|
||||
elif field['type'] in ('float', 'monetary') and name in import_fields:
|
||||
# Parse float, sometimes float values from file have currency symbol or () to denote a negative value
|
||||
# We should be able to manage both case
|
||||
|
||||
@@ -402,7 +402,33 @@ class test_convert_import_data(TransactionCase):
|
||||
# if results empty, no errors
|
||||
self.assertItemsEqual(results, [])
|
||||
|
||||
def test_parse_relational_fields(self):
|
||||
""" Ensure that relational fields float and date are correctly
|
||||
parsed during the import call.
|
||||
"""
|
||||
import_wizard = self.env['base_import.import'].create({
|
||||
'res_model': 'res.partner',
|
||||
'file': 'name,parent_id/id,parent_id/date,parent_id/credit_limit\n'
|
||||
'"foo","__export__.res_partner_1","2017年10月12日","5,69"\n',
|
||||
'file_type': 'text/csv'
|
||||
|
||||
})
|
||||
options = {
|
||||
'date_format': '%Y年%m月%d日',
|
||||
'quoting': '"',
|
||||
'separator': ',',
|
||||
'float_decimal_separator': ',',
|
||||
'float_thousand_separator': '.',
|
||||
'headers': True
|
||||
}
|
||||
data, import_fields = import_wizard._convert_import_data(
|
||||
['name', 'parent_id/.id', 'parent_id/date', 'parent_id/credit_limit'],
|
||||
options
|
||||
)
|
||||
result = import_wizard._parse_import_data(data, import_fields, options)
|
||||
# Check if the data 5,69 as been correctly parsed.
|
||||
self.assertEqual(float(result[0][-1]), 5.69)
|
||||
self.assertEqual(str(result[0][-2]), '2017-10-12')
|
||||
|
||||
def test_filtered(self):
|
||||
""" If ``False`` is provided as field mapping for a column,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import logging
|
||||
|
||||
from odoo.addons.mail.controllers.main import MailController
|
||||
from odoo import http
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CrmController(http.Controller):
|
||||
|
||||
@@ -12,8 +15,9 @@ class CrmController(http.Controller):
|
||||
comparison, record, redirect = MailController._check_token_and_record_or_redirect('crm.lead', int(res_id), token)
|
||||
if comparison and record:
|
||||
try:
|
||||
record.case_mark_won()
|
||||
record.action_set_won()
|
||||
except Exception:
|
||||
_logger.exception("Could not mark crm.lead as won")
|
||||
return MailController._redirect_to_messaging()
|
||||
return redirect
|
||||
|
||||
@@ -22,8 +26,9 @@ class CrmController(http.Controller):
|
||||
comparison, record, redirect = MailController._check_token_and_record_or_redirect('crm.lead', int(res_id), token)
|
||||
if comparison and record:
|
||||
try:
|
||||
record.case_mark_lost()
|
||||
record.action_set_lost()
|
||||
except Exception:
|
||||
_logger.exception("Could not mark crm.lead as lost")
|
||||
return MailController._redirect_to_messaging()
|
||||
return redirect
|
||||
|
||||
@@ -34,5 +39,6 @@ class CrmController(http.Controller):
|
||||
try:
|
||||
record.convert_opportunity(record.partner_id.id)
|
||||
except Exception:
|
||||
_logger.exception("Could not convert crm.lead to opportunity")
|
||||
return MailController._redirect_to_messaging()
|
||||
return redirect
|
||||
|
||||
@@ -116,17 +116,10 @@
|
||||
<filter string="Archived" name="archived" domain="[('active', '=', False)]"/>
|
||||
<group expand="0" string="Extended Filters">
|
||||
<field name="partner_id" filter_domain="[('partner_id','child_of',self)]"/>
|
||||
<field name="stage_id" domain="['|', ('team_id', '=', False), ('team_id', '=', 'team_id')]"/>
|
||||
<field name="campaign_id"/>
|
||||
<field name="medium_id"/>
|
||||
<field name="source_id"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
<separator orientation="vertical"/>
|
||||
<field name="stage_id" widget="selection" domain="['|', ('team_id', '=', False), ('team_id', '=', 'team_id')]"/>
|
||||
<field name="campaign_id" widget="selection"/>
|
||||
<field name="medium_id" widget="selection"/>
|
||||
<field name="source_id" widget="selection"/>
|
||||
<separator orientation="vertical"/>
|
||||
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
|
||||
<newline/>
|
||||
<field name="create_date"/>
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<p class="oe_mt32 oe_mb32 text-justify">
|
||||
Automate activities based on your sales scripts: calls, meetings, mails and quotations. Get all information you need, right on the opportunity: page viewed, mails received, etc.
|
||||
</p>
|
||||
<img src="crm_sc_02b.png">
|
||||
<img class='img-responsive' src="crm_sc_02b.png">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -42,6 +42,7 @@ class TimesheetAttendance(models.Model):
|
||||
ts.unit_amount AS timesheet,
|
||||
date_trunc('day', ts.date) AS date
|
||||
FROM account_analytic_line AS ts
|
||||
WHERE ts.project_id IS NOT NULL
|
||||
) AS t
|
||||
GROUP BY t.user_id, t.date
|
||||
ORDER BY t.date
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
<field name="view_ids" eval="[(5, 0, 0),
|
||||
(0, 0, {'view_mode': 'tree', 'view_id': ref('hr_timesheet.hr_timesheet_line_tree')}),
|
||||
(0, 0, {'view_mode': 'form', 'view_id': ref('hr_timesheet.hr_timesheet_line_form')})]"/>
|
||||
<field name="domain">[('project_id','!=',False)]</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_timesheet_sheet_form" model="ir.ui.view">
|
||||
|
||||
@@ -16,10 +16,10 @@ class MassMailController(http.Controller):
|
||||
if mailing.exists():
|
||||
res_id = res_id and int(res_id)
|
||||
res_ids = []
|
||||
if mailing.mailing_model == 'mail.mass_mailing.contact':
|
||||
if mailing.mailing_model == 'mail.mass_mailing.list':
|
||||
contacts = request.env['mail.mass_mailing.contact'].sudo().search([
|
||||
('email', '=', email),
|
||||
('list_id', 'in', [mailing_list.id for mailing_list in mailing.contact_list_ids])
|
||||
('list_ids', 'in', [mailing_list.id for mailing_list in mailing.contact_list_ids])
|
||||
])
|
||||
res_ids = contacts.ids
|
||||
else:
|
||||
|
||||
@@ -410,4 +410,15 @@ odoo_top[callback+"_updown"] = function (value, fields_values, field_name) {
|
||||
if ($editable_area.html().indexOf('on_change_model_and_list') !== -1) {
|
||||
$editable_area.empty();
|
||||
}
|
||||
// Adding compatibility for the outlook compliance of mailings.
|
||||
// Commit of such compatibility : a14f89c8663c9cafecb1cc26918055e023ecbe42
|
||||
options.registry.background.include({
|
||||
start: function() {
|
||||
this._super();
|
||||
var $table_target = this.$target.find('table:first');
|
||||
if ($table_target) {
|
||||
this.$target = $table_target;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -121,14 +121,20 @@ class MrpUnbuild(models.Model):
|
||||
for produce_move in produce_moves:
|
||||
if produce_move.has_tracking != 'none':
|
||||
original = original_quants.filtered(lambda quant: quant.product_id == produce_move.product_id)
|
||||
if original and not original[0].lot_id:
|
||||
if not original:
|
||||
raise UserError(_("You don't have in the stock the required lot/serial number for %s .") % (produce_move.product_id.name,))
|
||||
self.env['stock.move.lots'].create({
|
||||
'move_id': produce_move.id,
|
||||
'lot_id': original[0].lot_id.id,
|
||||
'quantity_done': produce_move.product_uom_qty,
|
||||
'quantity': produce_move.product_uom_qty
|
||||
})
|
||||
quantity_todo = produce_move.product_qty
|
||||
for quant in original:
|
||||
if quantity_todo <= 0:
|
||||
break
|
||||
move_quantity = min(quantity_todo, quant.qty)
|
||||
self.env['stock.move.lots'].create({
|
||||
'move_id': produce_move.id,
|
||||
'lot_id': quant.lot_id.id,
|
||||
'quantity_done': produce_move.product_id.uom_id._compute_quantity(move_quantity, produce_move.product_uom),
|
||||
'quantity': produce_move.product_id.uom_id._compute_quantity(move_quantity, produce_move.product_uom),
|
||||
})
|
||||
quantity_todo -= move_quantity
|
||||
else:
|
||||
produce_move.quantity_done = produce_move.product_uom_qty
|
||||
produce_moves.move_validate()
|
||||
|
||||
@@ -138,6 +138,14 @@ class StockMove(models.Model):
|
||||
self.check_move_lots()
|
||||
return res
|
||||
|
||||
def _propagate_cancel(self):
|
||||
self.ensure_one()
|
||||
if not self.move_dest_id.raw_material_production_id:
|
||||
super(StockMove, self)._propagate_cancel()
|
||||
elif self.move_dest_id.state == 'waiting':
|
||||
# If waiting, the chain will be broken and we are not sure if we can still wait for it (=> could take from stock instead)
|
||||
self.move_dest_id.write({'state': 'confirmed'})
|
||||
|
||||
@api.multi
|
||||
def action_cancel(self):
|
||||
if any(move.quantity_done for move in self):
|
||||
@@ -373,6 +381,10 @@ class StockMove(models.Model):
|
||||
self.sudo().unlink()
|
||||
return processed_moves
|
||||
|
||||
def _propagate_split(self, new_move, qty):
|
||||
if not self.move_dest_id.raw_material_production_id:
|
||||
super(StockMove, self)._propagate_split(new_move, qty)
|
||||
|
||||
def _generate_move_phantom(self, bom_line, quantity):
|
||||
if bom_line.product_id.type in ['product', 'consu']:
|
||||
return self.copy(default={
|
||||
|
||||
@@ -625,7 +625,7 @@ class PosOrder(models.Model):
|
||||
return_picking = Picking.create(return_vals)
|
||||
return_picking.message_post(body=message)
|
||||
|
||||
for line in order.lines.filtered(lambda l: l.product_id.type in ['product', 'consu'] and not float_is_zero(l.qty, precision_digits=l.product_id.uom_id.rounding)):
|
||||
for line in order.lines.filtered(lambda l: l.product_id.type in ['product', 'consu'] and not float_is_zero(l.qty, precision_rounding=l.product_id.uom_id.rounding)):
|
||||
moves |= Move.create({
|
||||
'name': line.name,
|
||||
'product_uom': line.product_id.uom_id.id,
|
||||
|
||||
@@ -202,7 +202,7 @@ exports.PosModel = Backbone.Model.extend({
|
||||
},
|
||||
},{
|
||||
model: 'account.tax',
|
||||
fields: ['name','amount', 'price_include', 'include_base_amount', 'amount_type', 'children_tax_ids'],
|
||||
fields: ['name','amount', 'price_include', 'include_base_amount', 'amount_type', 'children_tax_ids', 'sequence'],
|
||||
domain: null,
|
||||
loaded: function(self, taxes){
|
||||
self.taxes = taxes;
|
||||
@@ -1400,72 +1400,138 @@ exports.Orderline = Backbone.Model.extend({
|
||||
|
||||
return tax;
|
||||
},
|
||||
_compute_all: function(tax, base_amount, quantity) {
|
||||
/**
|
||||
* 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;
|
||||
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' && !tax.price_include) || (tax.amount_type === 'division' && tax.price_include)){
|
||||
if ((tax.amount_type === 'percent' && !price_include) || (tax.amount_type === 'division' && tax.price_include)){
|
||||
return base_amount * tax.amount / 100;
|
||||
}
|
||||
if (tax.amount_type === 'percent' && tax.price_include){
|
||||
if (tax.amount_type === 'percent' && price_include){
|
||||
return base_amount - (base_amount / (1 + tax.amount / 100));
|
||||
}
|
||||
if (tax.amount_type === 'division' && !tax.price_include) {
|
||||
if (tax.amount_type === 'division' && !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;
|
||||
var list_taxes = [];
|
||||
|
||||
// 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 currency_rounding_bak = currency_rounding;
|
||||
if (this.pos.company.tax_calculation_rounding_method == "round_globally"){
|
||||
currency_rounding = currency_rounding * 0.00001;
|
||||
}
|
||||
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){
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
var recompute_base = function(base_amount, fixed_amount, percent_amount){
|
||||
if(fixed_amount === 0.0 && percent_amount === 0.0)
|
||||
return base_amount;
|
||||
return round_pr((base_amount - fixed_amount) / (1.0 + percent_amount / 100.0), currency_rounding);
|
||||
}
|
||||
|
||||
// 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 sign = 1;
|
||||
if(base < 0){
|
||||
base = -base;
|
||||
var 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;
|
||||
}
|
||||
if(tax.price_include){
|
||||
if(tax.amount_type === 'fixed')
|
||||
incl_fixed_amount += tax.amount;
|
||||
else if(tax.amount_type === 'percent')
|
||||
incl_percent_amount += tax.amount;
|
||||
}
|
||||
});
|
||||
|
||||
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 taxes_vals = [];
|
||||
_(taxes.reverse()).each(function(tax){
|
||||
var tax_amount = self._compute_all(tax, base, quantity, false);
|
||||
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);
|
||||
});
|
||||
|
||||
return {
|
||||
taxes: list_taxes,
|
||||
taxes: taxes_vals,
|
||||
total_excluded: round_pr(total_excluded, currency_rounding_bak),
|
||||
total_included: round_pr(total_included, currency_rounding_bak)
|
||||
};
|
||||
|
||||
@@ -868,7 +868,7 @@ class SaleOrderLine(models.Model):
|
||||
return price
|
||||
else:
|
||||
from_currency = self.order_id.company_id.currency_id
|
||||
return from_currency.compute(product.lst_price, self.order_id.pricelist_id.currency_id)
|
||||
return from_currency.compute(product[pricelist_item.base], self.order_id.pricelist_id.currency_id)
|
||||
|
||||
@api.multi
|
||||
@api.onchange('product_id')
|
||||
|
||||
@@ -655,6 +655,15 @@ class StockMove(models.Model):
|
||||
if not no_prepare:
|
||||
self.check_recompute_pack_op()
|
||||
|
||||
def _propagate_cancel(self):
|
||||
self.ensure_one()
|
||||
if self.move_dest_id:
|
||||
if self.propagate:
|
||||
self.move_dest_id.action_cancel()
|
||||
elif self.move_dest_id.state == 'waiting':
|
||||
# If waiting, the chain will be broken and we are not sure if we can still wait for it (=> could take from stock instead)
|
||||
self.move_dest_id.write({'state': 'confirmed'})
|
||||
|
||||
@api.multi
|
||||
def action_cancel(self):
|
||||
""" Cancels the moves and if all moves are cancelled it cancels the picking. """
|
||||
@@ -671,12 +680,7 @@ class StockMove(models.Model):
|
||||
pass
|
||||
# procurements.search([('move_dest_id', '=', move.id)]).cancel()
|
||||
else:
|
||||
if move.move_dest_id:
|
||||
if move.propagate:
|
||||
move.move_dest_id.action_cancel()
|
||||
elif move.move_dest_id.state == 'waiting':
|
||||
# If waiting, the chain will be broken and we are not sure if we can still wait for it (=> could take from stock instead)
|
||||
move.move_dest_id.write({'state': 'confirmed'})
|
||||
move._propagate_cancel()
|
||||
if move.procurement_id:
|
||||
procurements |= move.procurement_id
|
||||
|
||||
@@ -916,6 +920,11 @@ class StockMove(models.Model):
|
||||
raise UserError(_('You can only delete draft moves.'))
|
||||
return super(StockMove, self).unlink()
|
||||
|
||||
def _propagate_split(self, new_move, qty):
|
||||
if self.move_dest_id and self.propagate and self.move_dest_id.state not in ('done', 'cancel'):
|
||||
new_move_prop = self.move_dest_id.split(qty)
|
||||
new_move.write({'move_dest_id': new_move_prop})
|
||||
|
||||
@api.multi
|
||||
def split(self, qty, restrict_lot_id=False, restrict_partner_id=False):
|
||||
""" Splits qty from move move into a new move
|
||||
@@ -956,10 +965,7 @@ class StockMove(models.Model):
|
||||
# TDE CLEANME: used only in write in this file, to clean
|
||||
# ctx['do_not_propagate'] = True
|
||||
self.with_context(do_not_propagate=True, rounding_method='HALF-UP').write({'product_uom_qty': self.product_uom_qty - uom_qty})
|
||||
|
||||
if self.move_dest_id and self.propagate and self.move_dest_id.state not in ('done', 'cancel'):
|
||||
new_move_prop = self.move_dest_id.split(qty)
|
||||
new_move.write({'move_dest_id': new_move_prop})
|
||||
self._propagate_split(new_move, qty)
|
||||
# returning the first element of list returned by action_confirm is ok because we checked it wouldn't be exploded (and
|
||||
# thus the result of action_confirm should always be a list of 1 element length)
|
||||
new_move.action_confirm()
|
||||
|
||||
@@ -8,7 +8,7 @@ from dateutil import relativedelta
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.addons import decimal_precision as dp
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
|
||||
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
|
||||
|
||||
import logging
|
||||
|
||||
@@ -837,7 +837,7 @@ class Orderpoint(models.Model):
|
||||
date=fields.Date.to_string(start_date),
|
||||
uom_id=self.product_uom).delay or 0.0
|
||||
date_planned = start_date + relativedelta.relativedelta(days=days)
|
||||
return date_planned.strftime(DEFAULT_SERVER_DATE_FORMAT)
|
||||
return date_planned.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
|
||||
@api.multi
|
||||
def _prepare_procurement_values(self, product_qty, date=False, group=False):
|
||||
|
||||
@@ -206,6 +206,18 @@ define([
|
||||
r.select();
|
||||
}
|
||||
|
||||
// If only pasting a <p/> element in an unique <p/> element, only paste
|
||||
// the <p/> element text
|
||||
var $p = $arch.children('p');
|
||||
var onlyAP = ($p.length === 1 && $arch.children().length === 1);
|
||||
if (onlyAP) {
|
||||
var $p1 = $(r.sc).closest('p');
|
||||
var $p2 = $(r.ec).closest('p');
|
||||
if ($p1.length && $p2.length && $p1[0] === $p2[0]) {
|
||||
$arch.html($p.text());
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
insert content
|
||||
*/
|
||||
|
||||
@@ -429,7 +429,7 @@ var RTE = Widget.extend({
|
||||
saveElement: function ($el, context) {
|
||||
// remove multi edition
|
||||
if ($el.data('oe-model')) {
|
||||
var key = $el.data('oe-model')+":"+$el.data('oe-id')+":"+$el.data('oe-field')+":"+$el.data('oe-type')+":"+$el.data('oe-expression');
|
||||
var key = $el.data('oe-model')+":"+$el.data('oe-id')+":"+$el.data('oe-field')+":"+$el.data('oe-type')+":"+$el.data('oe-expression')+":"+$el.data('oe-xpath');
|
||||
if (this.__saved[key]) return $.when();
|
||||
this.__saved[key] = true;
|
||||
}
|
||||
|
||||
@@ -454,7 +454,7 @@ $.summernote.pluginEvents.visible = function (event, editor, layoutInfo) {
|
||||
if (($node.is('[data-oe-type="html"]') || $node.is('[data-oe-field="arch"]')) &&
|
||||
$node.hasClass("o_editable") &&
|
||||
!$node[0].children.length &&
|
||||
"h1 h2 h3 h4 h5 h6 p b bold i u code sup strong small pre th td".toUpperCase().indexOf($node[0].nodeName) === -1) {
|
||||
"h1 h2 h3 h4 h5 h6 p b bold i u code sup strong small pre th td label".toUpperCase().indexOf($node[0].nodeName) === -1) {
|
||||
var p = $('<p><br/></p>')[0];
|
||||
$node.append( p );
|
||||
range.createFromNode(p.firstChild).select();
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
<group>
|
||||
<group>
|
||||
<field name="karma_ask"/>
|
||||
<field name="karma_answer"/>
|
||||
<field name="karma_upvote"/>
|
||||
<field name="karma_downvote"/>
|
||||
<field name="karma_edit_own"/>
|
||||
|
||||
@@ -150,6 +150,8 @@ var SlideDialog = Widget.extend({
|
||||
_.each(data.items, function (obj) {
|
||||
page_content = page_content + obj.str + " ";
|
||||
});
|
||||
// page_content may contain null characters
|
||||
page_content = page_content.replace(/\0/g, "");
|
||||
self.index_content = self.index_content + page_number + ". " + page_content + '\n';
|
||||
if (maxPages === page_number) {
|
||||
if (loaded) {
|
||||
|
||||
@@ -14576,7 +14576,7 @@ msgstr "Mois"
|
||||
#. module: base
|
||||
#: model:ir.ui.view,arch_db:base.sequence_view
|
||||
msgid "Month: %(month)s"
|
||||
msgstr "Mois : %(mois)"
|
||||
msgstr "Mois : %(month)s"
|
||||
|
||||
#. module: base
|
||||
#: selection:ir.cron,interval_type:0
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
@charset "utf-8";
|
||||
/*
|
||||
/**
|
||||
* This CSS is for the html description of modules
|
||||
* TODO clean
|
||||
*/
|
||||
@@ -78,11 +77,11 @@
|
||||
/* ------ BASE GRID CONSTRUCTS ----- */
|
||||
.oe_page {
|
||||
margin: 0px auto 64px auto;
|
||||
max-width: 992px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.oe_row {
|
||||
width: 928px;
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
margin-left: auto;
|
||||
@@ -111,35 +110,35 @@
|
||||
}
|
||||
|
||||
.oe_span12 {
|
||||
width: 928px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.oe_span10 {
|
||||
width: 773px;
|
||||
width: 83.33333%;
|
||||
}
|
||||
|
||||
.oe_span9 {
|
||||
width: 696px;
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
.oe_span8 {
|
||||
width: 618px;
|
||||
width: 66.66667%;
|
||||
}
|
||||
|
||||
.oe_span6 {
|
||||
width: 464px;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.oe_span4 {
|
||||
width: 309px;
|
||||
width: 33.33333%;
|
||||
}
|
||||
|
||||
.oe_span3 {
|
||||
width: 232px;
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.oe_span2 {
|
||||
width: 154px;
|
||||
width: 16.66667%;
|
||||
}
|
||||
|
||||
[class*='oe_span'].oe_fit {
|
||||
@@ -162,28 +161,28 @@
|
||||
width: auto;
|
||||
}
|
||||
.oe_row.oe_flex .oe_span12 {
|
||||
max-width: 928px;
|
||||
max-width: 100%;
|
||||
}
|
||||
.oe_row.oe_flex .oe_span10 {
|
||||
max-width: 769px;
|
||||
max-width: 83.33333%;
|
||||
}
|
||||
.oe_row.oe_flex .oe_span9 {
|
||||
max-width: 692px;
|
||||
max-width: 75%;
|
||||
}
|
||||
.oe_row.oe_flex .oe_span8 {
|
||||
max-width: 614px;
|
||||
max-width: 66.66667%;
|
||||
}
|
||||
.oe_row.oe_flex .oe_span6 {
|
||||
max-width: 460px;
|
||||
max-width: 50%;
|
||||
}
|
||||
.oe_row.oe_flex .oe_span4 {
|
||||
max-width: 305px;
|
||||
max-width: 33.33333%;
|
||||
}
|
||||
.oe_row.oe_flex .oe_span3 {
|
||||
max-width: 228px;
|
||||
max-width: 25%;
|
||||
}
|
||||
.oe_row.oe_flex .oe_span2 {
|
||||
max-width: 150px;
|
||||
max-width: 16.66667%;
|
||||
}
|
||||
|
||||
.oe_mb0 {
|
||||
|
||||
@@ -86,11 +86,11 @@ $v8_anchor_visited_color: rgb(91, 40, 79)
|
||||
|
||||
.oe_page
|
||||
margin: 0px auto 64px auto
|
||||
max-width: 992px
|
||||
max-width: 100%
|
||||
|
||||
|
||||
.oe_row
|
||||
width: 928px
|
||||
width: 100%
|
||||
margin-top: 16px
|
||||
margin-bottom: 16px
|
||||
margin-left: auto
|
||||
@@ -107,14 +107,14 @@ $v8_anchor_visited_color: rgb(91, 40, 79)
|
||||
line-height: 0
|
||||
height: 0
|
||||
|
||||
$oe_span12_width: 928px
|
||||
$oe_span10_width: 773px
|
||||
$oe_span9_width: 696px
|
||||
$oe_span8_width: 618px
|
||||
$oe_span6_width: 464px
|
||||
$oe_span4_width: 309px
|
||||
$oe_span3_width: 232px
|
||||
$oe_span2_width: 154px
|
||||
$oe_span12_width: 100%
|
||||
$oe_span10_width: 83.33333333%
|
||||
$oe_span9_width: 75%
|
||||
$oe_span8_width: 66.66666667%
|
||||
$oe_span6_width: 50%
|
||||
$oe_span4_width: 33.33333333%
|
||||
$oe_span3_width: 25%
|
||||
$oe_span2_width: 16.66666667%
|
||||
|
||||
[class*='oe_span']
|
||||
float: left
|
||||
@@ -155,19 +155,19 @@ $oe_span2_width: 154px
|
||||
.oe_span12
|
||||
max-width: $oe_span12_width
|
||||
.oe_span10
|
||||
max-width: ($oe_span10_width +-4px)
|
||||
max-width: $oe_span10_width
|
||||
.oe_span9
|
||||
max-width: ($oe_span9_width +-4px)
|
||||
.oe_span8
|
||||
max-width: ($oe_span8_width +-4px)
|
||||
max-width: $oe_span9_width
|
||||
.oe_span8
|
||||
max-width: $oe_span8_width
|
||||
.oe_span6
|
||||
max-width: ($oe_span6_width +-4px)
|
||||
max-width: $oe_span6_width
|
||||
.oe_span4
|
||||
max-width: ($oe_span4_width +-4px)
|
||||
max-width: $oe_span4_width
|
||||
.oe_span3
|
||||
max-width: ($oe_span3_width +-4px)
|
||||
max-width: $oe_span3_width
|
||||
.oe_span2
|
||||
max-width: ($oe_span2_width +-4px)
|
||||
max-width: $oe_span2_width
|
||||
|
||||
.oe_mb0
|
||||
margin-bottom: 0px !important
|
||||
|
||||
+1
-1
@@ -94,7 +94,7 @@ def ustr(value, hint_encoding='utf-8', errors='strict'):
|
||||
def exception_to_unicode(e):
|
||||
if (sys.version_info[:2] < (2,6)) and hasattr(e, 'message'):
|
||||
return ustr(e.message)
|
||||
if hasattr(e, 'args'):
|
||||
if getattr(e, 'args', ()):
|
||||
return "\n".join((ustr(a) for a in e.args))
|
||||
try:
|
||||
return unicode(e)
|
||||
|
||||
@@ -949,7 +949,8 @@ def trans_generate(lang, modules, cr):
|
||||
def get_module_from_path(path):
|
||||
for (mp, rec) in path_list:
|
||||
mp = os.path.join(mp, '')
|
||||
if rec and path.startswith(mp) and os.path.dirname(path) != mp:
|
||||
dirname = os.path.join(os.path.dirname(path), '')
|
||||
if rec and path.startswith(mp) and dirname != mp:
|
||||
path = path[len(mp):]
|
||||
return path.split(os.path.sep)[0]
|
||||
return 'base' # files that are not in a module are considered as being in 'base' module
|
||||
|
||||
Reference in New Issue
Block a user