[MERGE] forward port branch saas-12.4 up to fe59754c52
closes odoo/odoo#36721 Signed-off-by: Christophe Simonis <chs@odoo.com>
This commit is contained in:
@@ -1441,20 +1441,27 @@ class AccountTax(models.Model):
|
||||
cached_tax_amounts = {}
|
||||
if handle_price_include:
|
||||
for tax in reversed(taxes):
|
||||
tax_repartition_lines = (
|
||||
is_refund
|
||||
and tax.refund_repartition_line_ids
|
||||
or tax.invoice_repartition_line_ids
|
||||
).filtered(lambda x: x.repartition_type == "tax")
|
||||
sum_repartition_factor = sum(tax_repartition_lines.mapped("factor"))
|
||||
|
||||
if tax.include_base_amount:
|
||||
base = recompute_base(base, incl_fixed_amount, incl_percent_amount, incl_division_amount)
|
||||
incl_fixed_amount = incl_percent_amount = incl_division_amount = 0
|
||||
store_included_tax_total = True
|
||||
if tax.price_include or self._context.get('force_price_include'):
|
||||
if tax.amount_type == 'percent':
|
||||
incl_percent_amount += tax.amount
|
||||
incl_percent_amount += tax.amount * sum_repartition_factor
|
||||
elif tax.amount_type == 'division':
|
||||
incl_division_amount += tax.amount
|
||||
incl_division_amount += tax.amount * sum_repartition_factor
|
||||
elif tax.amount_type == 'fixed':
|
||||
incl_fixed_amount += quantity * tax.amount
|
||||
incl_fixed_amount += quantity * tax.amount * sum_repartition_factor
|
||||
else:
|
||||
# tax.amount_type == other (python)
|
||||
tax_amount = tax._compute_amount(base, price_unit, quantity, product, partner)
|
||||
tax_amount = tax._compute_amount(base, price_unit, quantity, product, partner) * sum_repartition_factor
|
||||
incl_fixed_amount += tax_amount
|
||||
# Avoid unecessary re-computation
|
||||
cached_tax_amounts[i] = tax_amount
|
||||
@@ -1473,6 +1480,9 @@ class AccountTax(models.Model):
|
||||
i = 0
|
||||
cumulated_tax_included_amount = 0
|
||||
for tax in taxes:
|
||||
tax_repartition_lines = (is_refund and tax.refund_repartition_line_ids or tax.invoice_repartition_line_ids).filtered(lambda x: x.repartition_type == 'tax')
|
||||
sum_repartition_factor = sum(tax_repartition_lines.mapped('factor'))
|
||||
|
||||
#compute the tax_amount
|
||||
if (self._context.get('force_price_include') or tax.price_include) and total_included_checkpoints.get(i):
|
||||
# We know the total to reach for that tax, so we make a substraction to avoid any rounding issues
|
||||
@@ -1480,13 +1490,14 @@ class AccountTax(models.Model):
|
||||
cumulated_tax_included_amount = 0
|
||||
else:
|
||||
tax_amount = tax.with_context(force_price_include=False)._compute_amount(
|
||||
base, price_unit, quantity, product, partner)
|
||||
base, sign * price_unit, quantity, product, partner)
|
||||
|
||||
# Round the tax_amount
|
||||
# Round the tax_amount multiplied by the computed repartition lines factor.
|
||||
tax_amount = round(tax_amount, prec)
|
||||
factorized_tax_amount = round(tax_amount * sum_repartition_factor, prec)
|
||||
|
||||
if tax.price_include and not total_included_checkpoints.get(i):
|
||||
cumulated_tax_included_amount += tax_amount
|
||||
cumulated_tax_included_amount += factorized_tax_amount
|
||||
|
||||
# If the tax affects the base of subsequent taxes, its tax move lines must
|
||||
# receive the base tags and tag_ids of these taxes, so that the tax report computes
|
||||
@@ -1497,18 +1508,24 @@ class AccountTax(models.Model):
|
||||
subsequent_taxes = taxes[i+1:]
|
||||
subsequent_tags = subsequent_taxes.get_tax_tags(is_refund, 'base')
|
||||
|
||||
# Compute the tax lines
|
||||
tax_repartition_lines = (is_refund and tax.refund_repartition_line_ids or tax.invoice_repartition_line_ids).filtered(lambda x: x.repartition_type == 'tax')
|
||||
sum_factor = sum(tax_repartition_lines.mapped('factor'))
|
||||
repartition_lines_to_treat = len(tax_repartition_lines)
|
||||
total_amount = 0
|
||||
for repartition_line in tax_repartition_lines:
|
||||
if repartition_lines_to_treat != 1 or sum_factor != 1.0:
|
||||
line_amount = round(tax_amount * repartition_line.factor, prec)
|
||||
else:
|
||||
# When the sum of the repartition lines factor is 100%, we need to ensure the whole tax amount has
|
||||
# been spread into the repartition lines.
|
||||
line_amount = round(tax_amount - total_amount, prec)
|
||||
# Compute the tax line amounts by multiplying each factor with the tax amount.
|
||||
# Then, spread the tax rounding to ensure the consistency of each line independently with the factorized
|
||||
# amount. E.g:
|
||||
#
|
||||
# Suppose a tax having 4 x 50% repartition line applied on a tax amount of 0.03 with 2 decimal places.
|
||||
# The factorized_tax_amount will be 0.06 (200% x 0.03). However, each line taken independently will compute
|
||||
# 50% * 0.03 = 0.01 with rounding. It means there is 0.06 - 0.04 = 0.02 as total_rounding_error to dispatch
|
||||
# in lines as 2 x 0.01.
|
||||
repartition_line_amounts = [round(tax_amount * line.factor, prec) for line in tax_repartition_lines]
|
||||
total_rounding_error = round(factorized_tax_amount - sum(repartition_line_amounts), prec)
|
||||
nber_rounding_steps = int(abs(total_rounding_error / currency.rounding))
|
||||
rounding_error = round(nber_rounding_steps and total_rounding_error / nber_rounding_steps or 0.0, prec)
|
||||
|
||||
for repartition_line, line_amount in zip(tax_repartition_lines, repartition_line_amounts):
|
||||
|
||||
if nber_rounding_steps:
|
||||
line_amount += rounding_error
|
||||
nber_rounding_steps -= 1
|
||||
|
||||
taxes_vals.append({
|
||||
'id': tax.id,
|
||||
@@ -1525,16 +1542,14 @@ class AccountTax(models.Model):
|
||||
'tax_ids': subsequent_taxes.ids,
|
||||
})
|
||||
|
||||
total_amount += line_amount
|
||||
if not repartition_line.account_id:
|
||||
total_void += line_amount
|
||||
repartition_lines_to_treat -= 1
|
||||
|
||||
# Affect subsequent taxes
|
||||
if tax.include_base_amount:
|
||||
base += tax_amount
|
||||
base += factorized_tax_amount
|
||||
|
||||
total_included += tax_amount
|
||||
total_included += factorized_tax_amount
|
||||
i += 1
|
||||
|
||||
return {
|
||||
|
||||
@@ -172,6 +172,8 @@ class AccountMove(models.Model):
|
||||
states={'draft': [('readonly', False)]},
|
||||
string='Salesperson',
|
||||
default=lambda self: self.env.user)
|
||||
user_id = fields.Many2one(string='User', related='invoice_user_id',
|
||||
help='Technical field used to fit the generic behavior in mail templates.')
|
||||
invoice_payment_state = fields.Selection(selection=[
|
||||
('not_paid', 'Not Paid'),
|
||||
('in_payment', 'In Payment'),
|
||||
@@ -426,25 +428,50 @@ class AccountMove(models.Model):
|
||||
:param base_line: The account.move.line owning the taxes.
|
||||
:return: The result of the compute_all method.
|
||||
'''
|
||||
move = base_line.move_id
|
||||
|
||||
if move.is_invoice():
|
||||
sign = -1 if move.is_inbound() else 1
|
||||
quantity = base_line.quantity
|
||||
if base_line.currency_id:
|
||||
price_unit_foreign_curr = sign * base_line.price_unit * (1 - (base_line.discount / 100.0))
|
||||
price_unit_comp_curr = base_line.currency_id._convert(price_unit_foreign_curr, move.company_id.currency_id, move.company_id, move.date)
|
||||
else:
|
||||
price_unit_foreign_curr = 0.0
|
||||
price_unit_comp_curr = sign * base_line.price_unit * (1 - (base_line.discount / 100.0))
|
||||
else:
|
||||
quantity = 1.0
|
||||
price_unit_foreign_curr = base_line.amount_currency
|
||||
price_unit_comp_curr = base_line.balance
|
||||
|
||||
balance_taxes_res = base_line.tax_ids._origin.compute_all(
|
||||
base_line.balance,
|
||||
price_unit_comp_curr,
|
||||
currency=base_line.company_currency_id,
|
||||
quantity=quantity,
|
||||
product=base_line.product_id,
|
||||
partner=base_line.partner_id,
|
||||
is_refund=self.type in ('out_refund', 'in_refund'),
|
||||
handle_price_include=False,
|
||||
)
|
||||
|
||||
if base_line.currency_id:
|
||||
# Multi-currencies mode: Taxes are computed both in company's currency / foreign currency.
|
||||
amount_currency_taxes_res = base_line.tax_ids._origin.compute_all(
|
||||
base_line.amount_currency,
|
||||
price_unit_foreign_curr,
|
||||
currency=base_line.currency_id,
|
||||
quantity=quantity,
|
||||
product=base_line.product_id,
|
||||
partner=base_line.partner_id,
|
||||
is_refund=self.type in ('out_refund', 'in_refund'),
|
||||
handle_price_include=False,
|
||||
)
|
||||
for b_tax_res, ac_tax_res in zip(balance_taxes_res['taxes'], amount_currency_taxes_res['taxes']):
|
||||
tax = self.env['account.tax'].browse(b_tax_res['id'])
|
||||
b_tax_res['amount_currency'] = ac_tax_res['amount']
|
||||
|
||||
# A tax having a fixed amount must be converted into the company currency when dealing with a
|
||||
# foreign currency.
|
||||
if tax.amount_type == 'fixed':
|
||||
b_tax_res['amount'] = base_line.currency_id._convert(b_tax_res['amount'], move.company_id.currency_id, move.company_id, move.date)
|
||||
|
||||
return balance_taxes_res
|
||||
|
||||
taxes_map = {}
|
||||
@@ -897,35 +924,43 @@ class AccountMove(models.Model):
|
||||
total_tax_currency = 0.0
|
||||
total_residual = 0.0
|
||||
total_residual_currency = 0.0
|
||||
total = 0.0
|
||||
total_currency = 0.0
|
||||
currencies = set()
|
||||
|
||||
for line in move.line_ids:
|
||||
if line.currency_id:
|
||||
currencies.add(line.currency_id)
|
||||
|
||||
# Untaxed amount.
|
||||
if (move.is_invoice(include_receipts=True) and not line.exclude_from_invoice_tab)\
|
||||
or (move.type == 'entry' and line.debit and not line.tax_line_id):
|
||||
total_untaxed += line.balance
|
||||
total_untaxed_currency += line.amount_currency
|
||||
if move.is_invoice(include_receipts=True):
|
||||
# === Invoices ===
|
||||
|
||||
# Tax amount.
|
||||
if line.tax_line_id:
|
||||
total_tax += line.balance
|
||||
total_tax_currency += line.amount_currency
|
||||
if not line.exclude_from_invoice_tab:
|
||||
# Untaxed amount.
|
||||
total_untaxed += line.balance
|
||||
total_untaxed_currency += line.amount_currency
|
||||
total += line.balance
|
||||
total_currency += line.amount_currency
|
||||
elif line.tax_line_id:
|
||||
# Tax amount.
|
||||
total_tax += line.balance
|
||||
total_tax_currency += line.amount_currency
|
||||
total += line.balance
|
||||
total_currency += line.amount_currency
|
||||
elif line.account_id.user_type_id.type in ('receivable', 'payable'):
|
||||
# Residual amount.
|
||||
total_residual += line.amount_residual
|
||||
total_residual_currency += line.amount_residual_currency
|
||||
else:
|
||||
# === Miscellaneous journal entry ===
|
||||
if line.debit:
|
||||
total += line.balance
|
||||
total_currency += line.amount_currency
|
||||
|
||||
# Residual amount.
|
||||
if move.type == 'entry' or line.account_id.user_type_id.type in ('receivable', 'payable'):
|
||||
total_residual += line.amount_residual
|
||||
total_residual_currency += line.amount_residual_currency
|
||||
|
||||
total = total_untaxed + total_tax
|
||||
total_currency = total_untaxed_currency + total_tax_currency
|
||||
|
||||
if (move.type == 'entry' and total < 0.0) or move.is_inbound():
|
||||
sign = -1
|
||||
else:
|
||||
if move.type == 'entry' or move.is_outbound():
|
||||
sign = 1
|
||||
else:
|
||||
sign = -1
|
||||
move.amount_untaxed = sign * (total_untaxed_currency if len(currencies) == 1 else total_untaxed)
|
||||
move.amount_tax = sign * (total_tax_currency if len(currencies) == 1 else total_tax)
|
||||
move.amount_total = sign * (total_currency if len(currencies) == 1 else total)
|
||||
@@ -1209,27 +1244,25 @@ class AccountMove(models.Model):
|
||||
if 'posted' in self.mapped('line_ids.payment_id.state'):
|
||||
raise ValidationError(_("You cannot modify a journal entry linked to a posted payment."))
|
||||
|
||||
@api.constrains('name', 'journal_id')
|
||||
@api.constrains('name', 'journal_id', 'state')
|
||||
def _check_unique_sequence_number(self):
|
||||
if not self:
|
||||
moves = self.filtered(lambda move: move.state == 'posted')
|
||||
if not moves:
|
||||
return
|
||||
|
||||
self.flush()
|
||||
|
||||
# /!\ Computed stored fields are not yet inside the database.
|
||||
self._cr.execute('''
|
||||
WITH current_moves (id, name, state, company_id, journal_id, type) as ( values {} )
|
||||
SELECT move2.id
|
||||
FROM ( SELECT id, name, state, company_id, journal_id, type from account_move
|
||||
UNION ALL SELECT id, name, state, company_id, journal_id, type from current_moves
|
||||
) move
|
||||
FROM account_move move
|
||||
INNER JOIN account_move move2 ON
|
||||
move2.name = move.name
|
||||
AND move2.company_id = move.company_id
|
||||
AND move2.journal_id = move.journal_id
|
||||
AND move2.type = move.type
|
||||
AND move2.id != move.id
|
||||
WHERE move.id IN %s
|
||||
AND move.state = 'posted'
|
||||
AND move2.state = 'posted'
|
||||
'''.format(", ".join(["(%s, %s, %s, %s, %s, %s)"] * len(self))), list(chain(*self.mapped(lambda r: [r.id, r.name, r.state, r.company_id.id, r.journal_id.id, r.type]))) + [tuple(self.ids)])
|
||||
WHERE move.id IN %s AND move2.state = 'posted'
|
||||
''', [tuple(moves.ids)])
|
||||
res = self._cr.fetchone()
|
||||
if res:
|
||||
raise ValidationError(_('Posted journal entry must have an unique sequence number per company.'))
|
||||
@@ -1240,23 +1273,28 @@ class AccountMove(models.Model):
|
||||
if not moves:
|
||||
return
|
||||
|
||||
self.env["account.move"].flush([
|
||||
"ref", "type", "invoice_date", "journal_id",
|
||||
"company_id", "partner_id", "commercial_partner_id",
|
||||
])
|
||||
self.env["account.journal"].flush(["company_id"])
|
||||
self.env["res.partner"].flush(["commercial_partner_id"])
|
||||
|
||||
# /!\ Computed stored fields are not yet inside the database.
|
||||
self._cr.execute('''
|
||||
WITH current_moves (id, ref, company_id, commercial_partner_id, type, invoice_date) as ( values {} )
|
||||
SELECT move2.id
|
||||
FROM ( SELECT id, ref, company_id, commercial_partner_id, type, invoice_date from account_move
|
||||
UNION ALL SELECT id, ref, company_id, commercial_partner_id, type, invoice_date from current_moves
|
||||
) move
|
||||
FROM account_move move
|
||||
JOIN account_journal journal ON journal.id = move.journal_id
|
||||
JOIN res_partner partner ON partner.id = move.partner_id
|
||||
INNER JOIN account_move move2 ON
|
||||
move2.ref = move.ref
|
||||
AND move2.company_id = move.company_id
|
||||
AND move2.commercial_partner_id = move.commercial_partner_id
|
||||
AND move2.company_id = journal.company_id
|
||||
AND move2.commercial_partner_id = partner.commercial_partner_id
|
||||
AND move2.type = move.type
|
||||
AND (move2.invoice_date = move.invoice_date OR move.invoice_date is NULL)
|
||||
AND (move.invoice_date is NULL OR move2.invoice_date = move.invoice_date)
|
||||
AND move2.id != move.id
|
||||
WHERE move.id IN %s
|
||||
AND move.type in ('in_invoice', 'in_refund')
|
||||
AND move.ref IS NOT NULL
|
||||
'''.format(", ".join(["(%s, %s, %s, %s, %s, CAST(%s as DATE))"] * len(moves))), list(chain(*moves.mapped(lambda r: [r.id, r.ref, r.company_id.id, r.commercial_partner_id.id, r.type, r.invoice_date or None]))) + [tuple(moves.ids)])
|
||||
''', [tuple(moves.ids)])
|
||||
if self._cr.fetchone():
|
||||
raise ValidationError(_('Duplicated vendor reference detected. You probably encoded twice the same vendor bill/credit note.'))
|
||||
|
||||
@@ -1304,24 +1342,22 @@ class AccountMove(models.Model):
|
||||
return True
|
||||
|
||||
def _check_tax_lock_date(self):
|
||||
if not self:
|
||||
return
|
||||
|
||||
self._cr.execute('''
|
||||
SELECT move.id, company.tax_lock_date
|
||||
FROM account_move move
|
||||
JOIN account_journal journal ON journal.id = move.journal_id
|
||||
JOIN res_company company ON company.id = journal.company_id
|
||||
WHERE move.id IN %s
|
||||
AND move.date < company.tax_lock_date
|
||||
''', [tuple(self.ids)])
|
||||
|
||||
query_res = self._cr.fetchone()
|
||||
if query_res:
|
||||
raise UserError(_('''
|
||||
The operation is refused as it would impact an already issued tax statement.
|
||||
Please change the journal entry date or the tax lock date set in the settings (%s) to proceed
|
||||
''' % query_res[1]))
|
||||
for move in self:
|
||||
if (
|
||||
move.company_id.tax_lock_date
|
||||
and move.date <= move.company_id.tax_lock_date
|
||||
and any(
|
||||
line.tax_ids
|
||||
or line.tax_line_id
|
||||
or line.tag_ids.filtered(lambda x: x.applicability == "taxes")
|
||||
for line in move.line_ids
|
||||
)
|
||||
):
|
||||
raise UserError(
|
||||
_(
|
||||
"The operation is refused as it would impact an already issued tax statement. Please change the journal entry date or the tax lock date set in the settings ({}) to proceed"
|
||||
).format(move.company_id.tax_lock_date or date.min)
|
||||
)
|
||||
|
||||
def _check_move_consistency(self):
|
||||
for move in self:
|
||||
@@ -1330,6 +1366,7 @@ class AccountMove(models.Model):
|
||||
raise UserError(_("Cannot create moves for different companies."))
|
||||
|
||||
self._check_balanced()
|
||||
self._check_tax_lock_date()
|
||||
self._check_fiscalyear_lock_date()
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -1694,6 +1731,13 @@ class AccountMove(models.Model):
|
||||
else:
|
||||
return abs(total_reconciled / total_amount)
|
||||
|
||||
def _get_reconciled_payments(self):
|
||||
"""Helper used to retrieve the reconciled payments on this journal entry"""
|
||||
pay_term_line_ids = self.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
|
||||
reconciled_amls = pay_term_line_ids.mapped('matched_debit_ids.debit_move_id') + \
|
||||
pay_term_line_ids.mapped('matched_credit_ids.credit_move_id')
|
||||
return reconciled_amls.mapped('payment_id')
|
||||
|
||||
def _reverse_move_vals(self, default_values, cancel=True):
|
||||
''' Reverse values passed as parameter being the copied values of the original journal entry.
|
||||
For example, debit / credit must be switched. The tax lines must be edited in case of refunds.
|
||||
@@ -1820,7 +1864,16 @@ class AccountMove(models.Model):
|
||||
'reversed_entry_id': move.id,
|
||||
})
|
||||
move_vals_list.append(move._reverse_move_vals(default_values, cancel=cancel))
|
||||
|
||||
reverse_moves = self.env['account.move'].create(move_vals_list)
|
||||
for move, reverse_move in zip(self, reverse_moves.with_context(check_move_validity=False)):
|
||||
# Update amount_currency if the date has changed.
|
||||
if move.date != reverse_move.date:
|
||||
for line in reverse_move.line_ids:
|
||||
if line.currency_id:
|
||||
line._onchange_currency()
|
||||
reverse_move._recompute_dynamic_lines(recompute_all_taxes=False)
|
||||
reverse_moves._check_balanced()
|
||||
|
||||
# Reconcile moves together to cancel the previous one.
|
||||
if cancel:
|
||||
@@ -1861,7 +1914,6 @@ class AccountMove(models.Model):
|
||||
move.invoice_date = fields.Date.context_today(self)
|
||||
move.with_context(check_move_validity=False)._onchange_invoice_date()
|
||||
|
||||
self._check_tax_lock_date()
|
||||
self._check_move_consistency()
|
||||
# Create the analytic lines in batch is faster as it leads to less cache invalidation.
|
||||
self.mapped('line_ids').create_analytic_lines()
|
||||
@@ -1946,6 +1998,7 @@ class AccountMove(models.Model):
|
||||
self.write({'state': 'draft'})
|
||||
|
||||
def button_cancel(self):
|
||||
self._check_tax_lock_date()
|
||||
AccountMoveLine = self.env['account.move.line']
|
||||
excluded_move_ids = []
|
||||
|
||||
@@ -1953,6 +2006,10 @@ class AccountMove(models.Model):
|
||||
excluded_move_ids = AccountMoveLine.search(AccountMoveLine._get_suspense_moves_domain() + [('move_id', 'in', self.ids)]).mapped('move_id').ids
|
||||
|
||||
for move in self:
|
||||
if move in move.line_ids.mapped('full_reconcile_id.exchange_move_id'):
|
||||
raise UserError(_('You cannot cancel an exchange difference journal entry.'))
|
||||
if move.tax_cash_basis_rec_id:
|
||||
raise UserError(_('You cannot cancel a tax cash basis journal entry.'))
|
||||
if not move.journal_id.update_posted and move.id not in excluded_move_ids:
|
||||
raise UserError(_('You cannot modify a posted entry of this journal.\nFirst you should set the journal to allow cancelling entries.'))
|
||||
# We remove all the analytics entries for this journal
|
||||
@@ -2002,6 +2059,7 @@ class AccountMove(models.Model):
|
||||
"""
|
||||
if any(not move.is_invoice(include_receipts=True) for move in self):
|
||||
raise UserError(_("Only invoices could be printed."))
|
||||
self._check_tax_lock_date()
|
||||
|
||||
self.filtered(lambda inv: not inv.invoice_sent).write({'invoice_sent': True})
|
||||
if self.user_has_groups('account.group_account_invoice'):
|
||||
@@ -2695,7 +2753,7 @@ class AccountMoveLine(models.Model):
|
||||
else:
|
||||
date = partial_line.credit_move_id.date if partial_line.debit_move_id == line else partial_line.debit_move_id.date
|
||||
rate = line.currency_id.with_context(date=date).rate
|
||||
amount_residual_currency += sign_partial_line * partial_line.amount * rate
|
||||
amount_residual_currency += sign_partial_line * line.currency_id.round(partial_line.amount * rate)
|
||||
|
||||
#computing the `reconciled` field.
|
||||
reconciled = False
|
||||
@@ -2778,36 +2836,21 @@ class AccountMoveLine(models.Model):
|
||||
if line.reconciled:
|
||||
raise UserError(_('Lines from "Off-Balance Sheet" accounts cannot be reconciled'))
|
||||
|
||||
@api.constrains('tax_ids', 'tax_line_id')
|
||||
def _check_tax_lock_date1(self):
|
||||
for line in self:
|
||||
if line.date <= (line.company_id.tax_lock_date or date.min):
|
||||
raise ValidationError(
|
||||
_("The operation is refused as it would impact an already issued tax statement. " +
|
||||
"Please change the journal entry date or the tax lock date set in the settings ({}) to proceed").format(
|
||||
line.company_id.tax_lock_date or date.min))
|
||||
|
||||
@api.constrains('credit', 'debit', 'date')
|
||||
def _check_tax_lock_date2(self):
|
||||
for line in self:
|
||||
if (line.tax_ids or line.tax_line_id) and line.date <= (line.company_id.tax_lock_date or date.min):
|
||||
raise ValidationError(
|
||||
_("The operation is refused as it would impact an already issued tax statement. " +
|
||||
"Please change the journal entry date or the tax lock date set in the settings ({}) to proceed").format(
|
||||
line.company_id.tax_lock_date or date.min))
|
||||
|
||||
def _update_check(self):
|
||||
""" Raise Warning to cause rollback if the move is posted, some entries are reconciled or the move is older than the lock date"""
|
||||
move_ids = set()
|
||||
for line in self:
|
||||
err_msg = _('Move name (id): %s (%s)') % (line.move_id.name, str(line.move_id.id))
|
||||
if line.move_id.state != 'draft':
|
||||
if line.move_id.state == 'posted':
|
||||
raise UserError(_('You cannot do this modification on a posted journal entry, you can just change some non legal fields. You must revert the journal entry to cancel it.\n%s.') % err_msg)
|
||||
if line.reconciled and not (line.debit == 0 and line.credit == 0):
|
||||
raise UserError(_('You cannot do this modification on a reconciled entry. You can just change some non legal fields or you must unreconcile first.\n%s.') % err_msg)
|
||||
if line.move_id.id not in move_ids:
|
||||
move_ids.add(line.move_id.id)
|
||||
self.env['account.move'].browse(list(move_ids))._check_fiscalyear_lock_date()
|
||||
if move_ids:
|
||||
moves = self.env['account.move'].browse(list(move_ids))
|
||||
moves._check_fiscalyear_lock_date()
|
||||
moves._check_tax_lock_date()
|
||||
return True
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -2963,7 +3006,6 @@ class AccountMoveLine(models.Model):
|
||||
|
||||
def unlink(self):
|
||||
self._update_check()
|
||||
self._check_tax_lock_date2()
|
||||
move_ids = set()
|
||||
for line in self:
|
||||
if line.move_id.id not in move_ids:
|
||||
@@ -2997,6 +3039,11 @@ class AccountMoveLine(models.Model):
|
||||
|
||||
# Suggest default value for debit / credit to balance the journal entry.
|
||||
balance = sum(line['debit'] - line['credit'] for line in move.line_ids)
|
||||
# if we are here, line_ids is in context, so journal_id should also be.
|
||||
journal = self.env['account.journal'].browse(self._context.get('default_journal_id') or self._context['journal_id'])
|
||||
currency = journal.exists() and journal.company_id.currency_id
|
||||
if currency:
|
||||
balance = currency.round(balance)
|
||||
if balance < 0.0:
|
||||
values.update({'debit': -balance})
|
||||
if balance > 0.0:
|
||||
@@ -3201,7 +3248,11 @@ class AccountMoveLine(models.Model):
|
||||
for after_rec_dict in cash_basis_subjected:
|
||||
new_rec = part_rec.create(after_rec_dict)
|
||||
# if the pair belongs to move being reverted, do not create CABA entry
|
||||
if cash_basis and not (new_rec.debit_move_id + new_rec.credit_move_id).mapped('move_id.reversed_entry_id'):
|
||||
if cash_basis and not (
|
||||
new_rec.debit_move_id.move_id == new_rec.credit_move_id.move_id.reversed_entry_id
|
||||
or
|
||||
new_rec.credit_move_id.move_id == new_rec.debit_move_id.move_id.reversed_entry_id
|
||||
):
|
||||
new_rec.create_tax_cash_basis_entry(cash_basis_percentage_before_rec)
|
||||
return debit_moves+credit_moves
|
||||
|
||||
@@ -3369,8 +3420,11 @@ class AccountMoveLine(models.Model):
|
||||
|
||||
def copy_data(self, default=None):
|
||||
res = super(AccountMoveLine, self).copy_data(default=default)
|
||||
if self._context.get('include_business_fields'):
|
||||
for line, values in zip(self, res):
|
||||
for line, values in zip(self, res):
|
||||
# Don't copy the name of a payment term line.
|
||||
if line.move_id.is_invoice() and line.account_id.user_type_id.type in ('receivable', 'payable'):
|
||||
values['name'] = ''
|
||||
if self._context.get('include_business_fields'):
|
||||
line._copy_data_extend_business_fields(values)
|
||||
return res
|
||||
|
||||
|
||||
@@ -443,7 +443,8 @@ class account_payment(models.Model):
|
||||
for move in rec.move_line_ids.mapped('move_id'):
|
||||
if rec.reconciled_invoice_ids:
|
||||
move.line_ids.remove_move_reconcile()
|
||||
move.button_cancel()
|
||||
if move.state != 'draft':
|
||||
move.button_cancel()
|
||||
move.unlink()
|
||||
rec.write({
|
||||
'state': 'cancelled',
|
||||
|
||||
@@ -399,7 +399,7 @@ class AccountReconcileModel(models.Model):
|
||||
query += ' AND st_line.name NOT ILIKE %s'
|
||||
params += ['%%%s%%' % rule['match_' + field + '_param']]
|
||||
elif rule['match_' + field] == 'match_regex':
|
||||
query += ' AND st_line.name ~ %s'
|
||||
query += ' AND st_line.name ~* %s'
|
||||
params += [rule['match_' + field + '_param']]
|
||||
|
||||
# Filter on partners.
|
||||
@@ -469,12 +469,23 @@ class AccountReconcileModel(models.Model):
|
||||
aml.amount_currency AS aml_amount_currency,
|
||||
account.internal_type AS account_internal_type,
|
||||
|
||||
-- Determine a matching or not with the statement line communication using the move.name or move.ref.
|
||||
regexp_split_to_array(TRIM(REGEXP_REPLACE(move.name, '[^0-9|^\s]', '', 'g')),'\s+')
|
||||
&& regexp_split_to_array(TRIM(REGEXP_REPLACE(st_line.name, '[^0-9|^\s]', '', 'g')), '\s+')
|
||||
-- Determine a matching or not with the statement line communication using the aml.name, move.name or move.ref.
|
||||
(
|
||||
aml.name IS NOT NULL
|
||||
AND
|
||||
TRIM(REGEXP_REPLACE(aml.name, '[^0-9|^\s]', '', 'g')) != ''
|
||||
AND
|
||||
regexp_split_to_array(TRIM(REGEXP_REPLACE(aml.name, '[^0-9|^\s]', '', 'g')),'\s+')
|
||||
&& regexp_split_to_array(TRIM(REGEXP_REPLACE(st_line.name, '[^0-9|^\s]', '', 'g')), '\s+')
|
||||
)
|
||||
OR
|
||||
regexp_split_to_array(TRIM(REGEXP_REPLACE(move.name, '[^0-9|^\s]', '', 'g')),'\s+')
|
||||
&& regexp_split_to_array(TRIM(REGEXP_REPLACE(st_line.name, '[^0-9|^\s]', '', 'g')), '\s+')
|
||||
OR
|
||||
(
|
||||
move.ref IS NOT NULL
|
||||
AND
|
||||
TRIM(REGEXP_REPLACE(move.ref, '[^0-9|^\s]', '', 'g')) != ''
|
||||
AND
|
||||
regexp_split_to_array(TRIM(REGEXP_REPLACE(move.ref, '[^0-9|^\s]', '', 'g')),'\s+')
|
||||
&& regexp_split_to_array(TRIM(REGEXP_REPLACE(st_line.name, '[^0-9|^\s]', '', 'g')), '\s+')
|
||||
@@ -518,11 +529,22 @@ class AccountReconcileModel(models.Model):
|
||||
TRIM(REGEXP_REPLACE(st_line.name, '[^0-9|^\s]', '', 'g')) != ''
|
||||
AND
|
||||
(
|
||||
regexp_split_to_array(TRIM(REGEXP_REPLACE(move.name, '[^0-9|^\s]', '', 'g')),'\s+')
|
||||
&& regexp_split_to_array(TRIM(REGEXP_REPLACE(st_line.name, '[^0-9|^\s]', '', 'g')), '\s+')
|
||||
(
|
||||
aml.name IS NOT NULL
|
||||
AND
|
||||
TRIM(REGEXP_REPLACE(aml.name, '[^0-9|^\s]', '', 'g')) != ''
|
||||
AND
|
||||
regexp_split_to_array(TRIM(REGEXP_REPLACE(aml.name, '[^0-9|^\s]', '', 'g')),'\s+')
|
||||
&& regexp_split_to_array(TRIM(REGEXP_REPLACE(st_line.name, '[^0-9|^\s]', '', 'g')), '\s+')
|
||||
)
|
||||
OR
|
||||
regexp_split_to_array(TRIM(REGEXP_REPLACE(move.name, '[^0-9|^\s]', '', 'g')),'\s+')
|
||||
&& regexp_split_to_array(TRIM(REGEXP_REPLACE(st_line.name, '[^0-9|^\s]', '', 'g')), '\s+')
|
||||
OR
|
||||
(
|
||||
move.ref IS NOT NULL
|
||||
AND
|
||||
TRIM(REGEXP_REPLACE(move.ref, '[^0-9|^\s]', '', 'g')) != ''
|
||||
AND
|
||||
regexp_split_to_array(TRIM(REGEXP_REPLACE(move.ref, '[^0-9|^\s]', '', 'g')),'\s+')
|
||||
&& regexp_split_to_array(TRIM(REGEXP_REPLACE(st_line.name, '[^0-9|^\s]', '', 'g')), '\s+')
|
||||
@@ -633,8 +655,10 @@ class AccountReconcileModel(models.Model):
|
||||
|
||||
if line_residual > total_residual:
|
||||
amount_percentage = (total_residual / line_residual) * 100
|
||||
else:
|
||||
elif total_residual:
|
||||
amount_percentage = (line_residual / total_residual) * 100 if total_residual else 0
|
||||
else:
|
||||
return False
|
||||
return amount_percentage >= self.match_total_amount_param
|
||||
|
||||
def _apply_rules(self, st_lines, excluded_ids=None, partner_map=None):
|
||||
|
||||
@@ -141,7 +141,8 @@ class AccountReconciliation(models.AbstractModel):
|
||||
query += 'LEFT JOIN res_partner_bank bank ON bank.id = st_line.bank_account_id OR bank.acc_number = st_line.account_number %s\n' % (where_bank)
|
||||
query += 'LEFT JOIN res_partner p1 ON st_line.partner_id=p1.id \n'
|
||||
query += 'LEFT JOIN res_partner p2 ON bank.partner_id=p2.id \n'
|
||||
query += 'LEFT JOIN res_partner p3 ON p3.name ILIKE st_line.partner_name %s\n' % (where_partner)
|
||||
# By definition the commercial partner_id doesn't have a parent_id set
|
||||
query += 'LEFT JOIN res_partner p3 ON p3.name ILIKE st_line.partner_name %s AND p3.parent_id is NULL \n' % (where_partner)
|
||||
query += 'WHERE st_line.id IN %s'
|
||||
|
||||
params += [tuple(st_lines.ids)]
|
||||
@@ -538,8 +539,10 @@ class AccountReconciliation(models.AbstractModel):
|
||||
])
|
||||
|
||||
domain = expression.OR([domain_reconciliation, domain_matching])
|
||||
partner_domain = []
|
||||
if partner_id:
|
||||
domain = expression.AND([domain, [('partner_id', '=', partner_id)]])
|
||||
partner_domain = [('partner_id', '=', partner_id)]
|
||||
domain = expression.AND([domain, partner_domain])
|
||||
if mode == 'rp':
|
||||
domain = expression.AND([domain,
|
||||
[('account_id.internal_type', 'in', ['receivable', 'payable', 'liquidity'])]
|
||||
@@ -553,10 +556,13 @@ class AccountReconciliation(models.AbstractModel):
|
||||
if search_str:
|
||||
str_domain = self._domain_move_lines(search_str=search_str)
|
||||
if not partner_id:
|
||||
str_domain = expression.OR([
|
||||
str_domain,
|
||||
[('partner_id.name', 'ilike', search_str)]
|
||||
])
|
||||
partner_domain = [('partner_id.name', 'ilike', search_str)]
|
||||
|
||||
str_domain = expression.OR([
|
||||
str_domain,
|
||||
partner_domain,
|
||||
])
|
||||
|
||||
domain = expression.AND([
|
||||
domain,
|
||||
str_domain
|
||||
|
||||
@@ -63,19 +63,19 @@ Tour.register('bank_statement_reconciliation', {
|
||||
},
|
||||
{
|
||||
content: "use filter",
|
||||
trigger: '.o_reconciliation_line:nth-child(2) .match .match_controls .filter',
|
||||
trigger: '.o_reconciliation_line:nth-child(1) .match .match_controls .filter',
|
||||
run: 'text 4610'
|
||||
},
|
||||
{
|
||||
content: "select a line linked to Deco Addict ",
|
||||
extra_trigger: ".o_reconciliation_line:nth-child(2) .match .line_info_button:last[data-content*='4,610']",
|
||||
trigger: ".o_reconciliation_line:nth-child(2) .match .line_info_button[data-content*='Deco Addict']"
|
||||
extra_trigger: ".o_reconciliation_line:nth-child(1) .match .line_info_button:last[data-content*='4,610']",
|
||||
trigger: ".o_reconciliation_line:nth-child(1) .match .line_info_button[data-content*='Deco Addict']"
|
||||
},
|
||||
{
|
||||
content: "deselect the line",
|
||||
trigger: '.o_reconciliation_line:nth-child(2) .accounting_view tbody .cell_label:first',
|
||||
trigger: '.o_reconciliation_line:nth-child(1) .accounting_view tbody .cell_label:first',
|
||||
run: function() {
|
||||
$('.o_reconciliation_line:nth-child(2) .accounting_view tbody .cell_label:first').trigger('click');
|
||||
$('.o_reconciliation_line:nth-child(1) .accounting_view tbody .cell_label:first').trigger('click');
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -20,5 +20,6 @@ from . import test_templates_consistency
|
||||
from . import test_account_fiscal_year
|
||||
from . import test_account_all_l10n
|
||||
from . import test_reconciliation_matching_rules
|
||||
from . import test_account_move_tax_lock_date
|
||||
from . import test_account_onboarding
|
||||
from . import test_portal_attachment
|
||||
|
||||
@@ -69,34 +69,27 @@ class TestAccountMove(InvoiceTestCommon):
|
||||
# lines[3] = 'revenue line 2'
|
||||
lines = self.test_move.line_ids.sorted('debit')
|
||||
|
||||
# Writing not affecting a tax is allowed.
|
||||
self.test_move.write({
|
||||
'line_ids': [
|
||||
(1, lines[0].id, {'credit': 1750.0}), # counterpart line
|
||||
(1, lines[2].id, {'debit': 600.0}), # revenue line 1
|
||||
],
|
||||
})
|
||||
|
||||
self.test_move.flush()
|
||||
self.cr.execute('SAVEPOINT test_misc_tax_lock_date_1')
|
||||
|
||||
# Writing something affecting a tax is not allowed.
|
||||
with self.assertRaises(ValidationError):
|
||||
with self.assertRaises(UserError):
|
||||
self.test_move.write({
|
||||
'line_ids': [
|
||||
(1, lines[0].id, {'credit': 2750.0}),
|
||||
(1, lines[3].id, {'debit': 2000.0}),
|
||||
(1, lines[3].id, {'debit': 2100.0}),
|
||||
],
|
||||
})
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
with self.assertRaises(UserError):
|
||||
self.test_move.write({
|
||||
'line_ids': [
|
||||
(1, lines[3].id, {'tax_ids': [(6, 0, self.company_data['default_tax_purchase'].ids)]}),
|
||||
],
|
||||
})
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
with self.assertRaises(UserError):
|
||||
self.test_move.write({
|
||||
'line_ids': [
|
||||
(1, lines[0].id, {'credit': 1900.0}),
|
||||
@@ -104,12 +97,22 @@ class TestAccountMove(InvoiceTestCommon):
|
||||
],
|
||||
})
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
with self.assertRaises(UserError):
|
||||
self.test_move.unlink()
|
||||
|
||||
self.test_move.flush()
|
||||
self.test_move.invalidate_cache()
|
||||
self.cr.execute('ROLLBACK TO SAVEPOINT test_misc_tax_lock_date_1')
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
self.test_move.post()
|
||||
def test_misc_unique_sequence_number(self):
|
||||
''' Ensure two journal entries can't share the same name when using the same sequence. '''
|
||||
self.test_move.post()
|
||||
|
||||
# Edit the sequence to force the next move to get the same name.
|
||||
self.test_move.journal_id\
|
||||
.sequence_id.date_range_ids\
|
||||
.filtered(lambda seq: seq.date_from == fields.Date.from_string('2016-01-01')).number_next -= 1
|
||||
|
||||
test_move2 = self.test_move.copy()
|
||||
with self.assertRaises(ValidationError):
|
||||
test_move2.post()
|
||||
|
||||
@@ -3,6 +3,7 @@ from odoo.addons.account.tests.invoice_test_common import InvoiceTestCommon
|
||||
from odoo.tests.common import Form
|
||||
from odoo.tests import tagged
|
||||
from odoo import fields
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
@@ -851,7 +852,7 @@ class TestAccountMoveInInvoiceOnchanges(InvoiceTestCommon):
|
||||
self.term_line_vals_1,
|
||||
], self.move_vals)
|
||||
|
||||
def test_in_invoice_create_draft_refund(self):
|
||||
def test_in_invoice_create_refund(self):
|
||||
self.invoice.post()
|
||||
|
||||
move_reversal = self.env['account.move.reversal'].with_context(active_ids=self.invoice.ids).create({
|
||||
@@ -885,8 +886,10 @@ class TestAccountMoveInInvoiceOnchanges(InvoiceTestCommon):
|
||||
},
|
||||
{
|
||||
**self.term_line_vals_1,
|
||||
'name': '',
|
||||
'debit': 1128.0,
|
||||
'credit': 0.0,
|
||||
'date_maturity': move_reversal.date,
|
||||
},
|
||||
], {
|
||||
**self.move_vals,
|
||||
@@ -898,7 +901,7 @@ class TestAccountMoveInInvoiceOnchanges(InvoiceTestCommon):
|
||||
|
||||
move_reversal = self.env['account.move.reversal'].with_context(active_ids=self.invoice.ids).create({
|
||||
'date': fields.Date.from_string('2019-02-01'),
|
||||
'reason': 'because',
|
||||
'reason': 'no reason again',
|
||||
'refund_method': 'cancel',
|
||||
})
|
||||
reversal = move_reversal.reverse_moves()
|
||||
@@ -927,8 +930,10 @@ class TestAccountMoveInInvoiceOnchanges(InvoiceTestCommon):
|
||||
},
|
||||
{
|
||||
**self.term_line_vals_1,
|
||||
'name': '',
|
||||
'debit': 1128.0,
|
||||
'credit': 0.0,
|
||||
'date_maturity': move_reversal.date,
|
||||
},
|
||||
], {
|
||||
**self.move_vals,
|
||||
@@ -938,6 +943,128 @@ class TestAccountMoveInInvoiceOnchanges(InvoiceTestCommon):
|
||||
'invoice_payment_state': 'paid',
|
||||
})
|
||||
|
||||
def test_in_invoice_create_refund_multi_currency(self):
|
||||
''' Test the account.move.reversal takes care about the currency rates when setting
|
||||
a custom reversal date.
|
||||
'''
|
||||
move_form = Form(self.invoice)
|
||||
move_form.date = '2016-01-01'
|
||||
move_form.currency_id = self.currency_data['currency']
|
||||
move_form.save()
|
||||
|
||||
self.invoice.post()
|
||||
|
||||
# The currency rate changed from 1/3 to 1/2.
|
||||
move_reversal = self.env['account.move.reversal'].with_context(active_ids=self.invoice.ids).create({
|
||||
'date': fields.Date.from_string('2017-01-01'),
|
||||
'reason': 'no reason',
|
||||
'refund_method': 'refund',
|
||||
})
|
||||
reversal = move_reversal.reverse_moves()
|
||||
reverse_move = self.env['account.move'].browse(reversal['res_id'])
|
||||
|
||||
self.assertInvoiceValues(reverse_move, [
|
||||
{
|
||||
**self.product_line_vals_1,
|
||||
'amount_currency': -800.0,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'debit': 0.0,
|
||||
'credit': 400.0,
|
||||
},
|
||||
{
|
||||
**self.product_line_vals_2,
|
||||
'amount_currency': -160.0,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'debit': 0.0,
|
||||
'credit': 80.0,
|
||||
},
|
||||
{
|
||||
**self.tax_line_vals_1,
|
||||
'amount_currency': -144.0,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'debit': 0.0,
|
||||
'credit': 72.0,
|
||||
},
|
||||
{
|
||||
**self.tax_line_vals_2,
|
||||
'amount_currency': -24.0,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'debit': 0.0,
|
||||
'credit': 12.0,
|
||||
},
|
||||
{
|
||||
**self.term_line_vals_1,
|
||||
'name': '',
|
||||
'amount_currency': 1128.0,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'debit': 564.0,
|
||||
'credit': 0.0,
|
||||
'date_maturity': move_reversal.date,
|
||||
},
|
||||
], {
|
||||
**self.move_vals,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'date': move_reversal.date,
|
||||
'state': 'draft',
|
||||
'ref': 'Reversal of: %s, %s' % (self.invoice.name, move_reversal.reason),
|
||||
'invoice_payment_state': 'not_paid',
|
||||
})
|
||||
|
||||
move_reversal = self.env['account.move.reversal'].with_context(active_ids=self.invoice.ids).create({
|
||||
'date': fields.Date.from_string('2017-01-01'),
|
||||
'reason': 'no reason again',
|
||||
'refund_method': 'cancel',
|
||||
})
|
||||
reversal = move_reversal.reverse_moves()
|
||||
reverse_move = self.env['account.move'].browse(reversal['res_id'])
|
||||
|
||||
self.assertInvoiceValues(reverse_move, [
|
||||
{
|
||||
**self.product_line_vals_1,
|
||||
'amount_currency': -800.0,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'debit': 0.0,
|
||||
'credit': 400.0,
|
||||
},
|
||||
{
|
||||
**self.product_line_vals_2,
|
||||
'amount_currency': -160.0,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'debit': 0.0,
|
||||
'credit': 80.0,
|
||||
},
|
||||
{
|
||||
**self.tax_line_vals_1,
|
||||
'amount_currency': -144.0,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'debit': 0.0,
|
||||
'credit': 72.0,
|
||||
},
|
||||
{
|
||||
**self.tax_line_vals_2,
|
||||
'amount_currency': -24.0,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'debit': 0.0,
|
||||
'credit': 12.0,
|
||||
},
|
||||
{
|
||||
**self.term_line_vals_1,
|
||||
'name': '',
|
||||
'amount_currency': 1128.0,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'debit': 564.0,
|
||||
'credit': 0.0,
|
||||
'date_maturity': move_reversal.date,
|
||||
},
|
||||
], {
|
||||
**self.move_vals,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'date': move_reversal.date,
|
||||
'state': 'posted',
|
||||
'ref': 'Reversal of: %s, %s' % (self.invoice.name, move_reversal.reason),
|
||||
'invoice_payment_state': 'paid',
|
||||
})
|
||||
|
||||
def test_in_invoice_create_1(self):
|
||||
# Test creating an account_move with the least information.
|
||||
move = self.env['account.move'].create({
|
||||
@@ -1042,6 +1169,14 @@ class TestAccountMoveInInvoiceOnchanges(InvoiceTestCommon):
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
})
|
||||
|
||||
def test_in_invoice_duplicate_supplier_reference(self):
|
||||
''' Ensure two vendor bills can't share the same vendor reference. '''
|
||||
self.invoice.ref = 'a supplier reference'
|
||||
invoice2 = self.invoice.copy()
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
invoice2.ref = 'a supplier reference'
|
||||
|
||||
def test_in_invoice_switch_in_refund_1(self):
|
||||
# Test creating an account_move with an in_invoice_type and switch it in an in_refund.
|
||||
move = self.env['account.move'].create({
|
||||
|
||||
@@ -836,7 +836,7 @@ class TestAccountMoveOutInvoiceOnchanges(InvoiceTestCommon):
|
||||
|
||||
self.assertRecordValues(invoice_copy, [{'name': 'INV/2019/0043'}])
|
||||
|
||||
def test_out_invoice_create_draft_refund(self):
|
||||
def test_out_invoice_create_refund(self):
|
||||
self.invoice.post()
|
||||
|
||||
move_reversal = self.env['account.move.reversal'].with_context(active_ids=self.invoice.ids).create({
|
||||
@@ -870,12 +870,14 @@ class TestAccountMoveOutInvoiceOnchanges(InvoiceTestCommon):
|
||||
},
|
||||
{
|
||||
**self.term_line_vals_1,
|
||||
'name': self.invoice.name,
|
||||
'name': '',
|
||||
'debit': 0.0,
|
||||
'credit': 1410.0,
|
||||
'date_maturity': move_reversal.date,
|
||||
},
|
||||
], {
|
||||
**self.move_vals,
|
||||
'name': '/',
|
||||
'date': move_reversal.date,
|
||||
'state': 'draft',
|
||||
'ref': 'Reversal of: %s, %s' % (self.invoice.name, move_reversal.reason),
|
||||
@@ -913,9 +915,10 @@ class TestAccountMoveOutInvoiceOnchanges(InvoiceTestCommon):
|
||||
},
|
||||
{
|
||||
**self.term_line_vals_1,
|
||||
'name': self.invoice.name,
|
||||
'name': '',
|
||||
'debit': 0.0,
|
||||
'credit': 1410.0,
|
||||
'date_maturity': move_reversal.date,
|
||||
},
|
||||
], {
|
||||
**self.move_vals,
|
||||
@@ -925,6 +928,128 @@ class TestAccountMoveOutInvoiceOnchanges(InvoiceTestCommon):
|
||||
'invoice_payment_state': 'paid',
|
||||
})
|
||||
|
||||
def test_out_invoice_create_refund_multi_currency(self):
|
||||
''' Test the account.move.reversal takes care about the currency rates when setting
|
||||
a custom reversal date.
|
||||
'''
|
||||
move_form = Form(self.invoice)
|
||||
move_form.date = '2016-01-01'
|
||||
move_form.currency_id = self.currency_data['currency']
|
||||
move_form.save()
|
||||
|
||||
self.invoice.post()
|
||||
|
||||
# The currency rate changed from 1/3 to 1/2.
|
||||
move_reversal = self.env['account.move.reversal'].with_context(active_ids=self.invoice.ids).create({
|
||||
'date': fields.Date.from_string('2017-01-01'),
|
||||
'reason': 'no reason',
|
||||
'refund_method': 'refund',
|
||||
})
|
||||
reversal = move_reversal.reverse_moves()
|
||||
reverse_move = self.env['account.move'].browse(reversal['res_id'])
|
||||
|
||||
self.assertInvoiceValues(reverse_move, [
|
||||
{
|
||||
**self.product_line_vals_1,
|
||||
'amount_currency': 1000.0,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'debit': 500.0,
|
||||
'credit': 0.0,
|
||||
},
|
||||
{
|
||||
**self.product_line_vals_2,
|
||||
'amount_currency': 200.0,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'debit': 100.0,
|
||||
'credit': 0.0,
|
||||
},
|
||||
{
|
||||
**self.tax_line_vals_1,
|
||||
'amount_currency': 180.0,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'debit': 90.0,
|
||||
'credit': 0.0,
|
||||
},
|
||||
{
|
||||
**self.tax_line_vals_2,
|
||||
'amount_currency': 30.0,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'debit': 15.0,
|
||||
'credit': 0.0,
|
||||
},
|
||||
{
|
||||
**self.term_line_vals_1,
|
||||
'name': '',
|
||||
'amount_currency': -1410.0,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'debit': 0.0,
|
||||
'credit': 705.0,
|
||||
'date_maturity': move_reversal.date,
|
||||
},
|
||||
], {
|
||||
**self.move_vals,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'date': move_reversal.date,
|
||||
'state': 'draft',
|
||||
'ref': 'Reversal of: %s, %s' % (self.invoice.name, move_reversal.reason),
|
||||
'invoice_payment_state': 'not_paid',
|
||||
})
|
||||
|
||||
move_reversal = self.env['account.move.reversal'].with_context(active_ids=self.invoice.ids).create({
|
||||
'date': fields.Date.from_string('2017-01-01'),
|
||||
'reason': 'no reason',
|
||||
'refund_method': 'cancel',
|
||||
})
|
||||
reversal = move_reversal.reverse_moves()
|
||||
reverse_move = self.env['account.move'].browse(reversal['res_id'])
|
||||
|
||||
self.assertInvoiceValues(reverse_move, [
|
||||
{
|
||||
**self.product_line_vals_1,
|
||||
'amount_currency': 1000.0,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'debit': 500.0,
|
||||
'credit': 0.0,
|
||||
},
|
||||
{
|
||||
**self.product_line_vals_2,
|
||||
'amount_currency': 200.0,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'debit': 100.0,
|
||||
'credit': 0.0,
|
||||
},
|
||||
{
|
||||
**self.tax_line_vals_1,
|
||||
'amount_currency': 180.0,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'debit': 90.0,
|
||||
'credit': 0.0,
|
||||
},
|
||||
{
|
||||
**self.tax_line_vals_2,
|
||||
'amount_currency': 30.0,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'debit': 15.0,
|
||||
'credit': 0.0,
|
||||
},
|
||||
{
|
||||
**self.term_line_vals_1,
|
||||
'name': '',
|
||||
'amount_currency': -1410.0,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'debit': 0.0,
|
||||
'credit': 705.0,
|
||||
'date_maturity': move_reversal.date,
|
||||
},
|
||||
], {
|
||||
**self.move_vals,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'date': move_reversal.date,
|
||||
'state': 'posted',
|
||||
'ref': 'Reversal of: %s, %s' % (self.invoice.name, move_reversal.reason),
|
||||
'invoice_payment_state': 'paid',
|
||||
})
|
||||
|
||||
def test_out_invoice_create_1(self):
|
||||
# Test creating an account_move with the least information.
|
||||
move = self.env['account.move'].create({
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
from odoo.addons.account.tests.account_test_classes import AccountingTestCase
|
||||
from odoo.exceptions import UserError
|
||||
from datetime import date
|
||||
from calendar import monthrange
|
||||
# from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestTaxBlockDate(AccountingTestCase):
|
||||
"""
|
||||
Forbid creation, edition and deletion of Journal Items related to taxes with
|
||||
a date prior to the Tax Block Date.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestTaxBlockDate, self).setUp()
|
||||
self.user_id = self.env.user
|
||||
self.company = self.env.company
|
||||
company_id = self.company.id
|
||||
|
||||
last_day_month = date.today()
|
||||
self.last_day_month = last_day_month.replace(day=monthrange(last_day_month.year, last_day_month.month)[1])
|
||||
first_day_month = date.today()
|
||||
self.first_day_month = first_day_month.replace(day=1)
|
||||
middle_day_month = date.today()
|
||||
middle_day_month = middle_day_month.replace(day=15)
|
||||
|
||||
self.sale_journal_id = self.env['account.journal'].search([('type', '=', 'sale'), ('company_id', '=', company_id)], limit=1)[0]
|
||||
self.account_id = self.env['account.account'].search([('internal_type', '=', 'receivable'), ('company_id', '=', company_id)], limit=1)[0]
|
||||
self.other_account_id = self.env['account.account'].search([('internal_type', '!=', 'receivable'), ('company_id', '=', company_id)], limit=1)[0]
|
||||
self.tax_id = self.env['account.tax'].search([('company_id', '=', company_id)], limit=1)[0]
|
||||
|
||||
self.move = {
|
||||
'name': '/',
|
||||
'journal_id': self.sale_journal_id.id,
|
||||
'date': middle_day_month,
|
||||
'line_ids': [(0, 0, {
|
||||
'name': 'foo',
|
||||
'debit': 10,
|
||||
'account_id': self.account_id.id,
|
||||
'tax_ids': [(6, False, [self.tax_id.id])]
|
||||
}), (0, 0, {
|
||||
'name': 'bar',
|
||||
'credit': 10,
|
||||
'account_id': self.account_id.id,
|
||||
})]
|
||||
}
|
||||
|
||||
self.move_no_tax = {
|
||||
'name': '/',
|
||||
'journal_id': self.sale_journal_id.id,
|
||||
'date': middle_day_month,
|
||||
'line_ids': [(0, 0, {
|
||||
'name': 'foo',
|
||||
'debit': 10,
|
||||
'account_id': self.account_id.id,
|
||||
}), (0, 0, {
|
||||
'name': 'bar',
|
||||
'credit': 10,
|
||||
'account_id': self.account_id.id,
|
||||
})]
|
||||
}
|
||||
|
||||
def test_tax_lock_date_post(self):
|
||||
""" Checks that it is impossible to post an entry
|
||||
before the tax lock date.
|
||||
"""
|
||||
# Posting after the lock date is always allowed
|
||||
self.company.tax_lock_date = self.first_day_month
|
||||
# copy() because mail.thread modifies the dictionary given to create
|
||||
move_after_lock = self.env['account.move'].create(self.move.copy())
|
||||
move_after_lock.post()
|
||||
|
||||
# Posting before the tax lock date is allowed only if the move doesn't contain any tax
|
||||
self.user_id.company_id.tax_lock_date = self.last_day_month
|
||||
move_before_lock_no_tax = self.env['account.move'].create(self.move_no_tax)
|
||||
move_before_lock_no_tax.post()
|
||||
|
||||
# Posting before the tax lock date is not allowed for moves impacting the tax report
|
||||
self.user_id.company_id.tax_lock_date = self.last_day_month
|
||||
with self.assertRaises(UserError):
|
||||
self.env['account.move'].create(self.move.copy())
|
||||
|
||||
def test_tax_lock_date_cancel(self):
|
||||
""" Checks that it is not possible to cancel an entry posted before the
|
||||
tax lock date if it impacts the tax report. """
|
||||
self.sale_journal_id.update_posted = True
|
||||
move = self.env['account.move'].create(self.move)
|
||||
move.post()
|
||||
move_no_tax = self.env['account.move'].create(self.move_no_tax)
|
||||
move_no_tax.post()
|
||||
|
||||
self.user_id.company_id.tax_lock_date = self.last_day_month
|
||||
|
||||
move_no_tax.button_cancel()
|
||||
with self.assertRaises(UserError):
|
||||
move.button_cancel()
|
||||
@@ -149,6 +149,24 @@ class TestReconciliation(AccountingTestCase):
|
||||
invoice.post()
|
||||
return invoice
|
||||
|
||||
def create_invoice_partner(self, type='out_invoice', invoice_amount=50, currency_id=None, partner_id=False, payment_term_id=False):
|
||||
move = self.env['account.move'].with_context(default_type=type).create({
|
||||
'type': type,
|
||||
'partner_id': partner_id,
|
||||
'invoice_date': time.strftime('%Y') + '-07-01',
|
||||
'date': time.strftime('%Y') + '-07-01',
|
||||
'currency_id': currency_id,
|
||||
'invoice_payment_term_id': payment_term_id,
|
||||
'invoice_line_ids': [(0, 0, {
|
||||
'quantity': 1,
|
||||
'price_unit': invoice_amount,
|
||||
|
||||
'name': 'product that cost ' + str(invoice_amount),
|
||||
})],
|
||||
})
|
||||
move.post()
|
||||
return move
|
||||
|
||||
def make_payment(self, invoice_record, bank_journal, amount=0.0, amount_currency=0.0, currency_id=None):
|
||||
bank_stmt = self.acc_bank_stmt_model.create({
|
||||
'journal_id': bank_journal.id,
|
||||
@@ -895,22 +913,6 @@ class TestReconciliationExec(TestReconciliation):
|
||||
self.assertEqual(reversed_bank_line.full_reconcile_id.id, bank_line.full_reconcile_id.id)
|
||||
self.assertEqual(reversed_customer_line.full_reconcile_id.id, customer_line.full_reconcile_id.id)
|
||||
|
||||
def create_invoice_partner(self, type='out_invoice', invoice_amount=50, currency_id=None, partner_id=False, payment_term_id=False):
|
||||
move = self.env['account.move'].with_context(default_type=type).create({
|
||||
'type': type,
|
||||
'partner_id': partner_id,
|
||||
'invoice_date': time.strftime('%Y') + '-07-01',
|
||||
'date': time.strftime('%Y') + '-07-01',
|
||||
'currency_id': currency_id,
|
||||
'invoice_payment_term_id': payment_term_id,
|
||||
'invoice_line_ids': [(0, 0, {
|
||||
'quantity': 1,
|
||||
'price_unit': invoice_amount,
|
||||
'name': 'product that cost ' + str(invoice_amount),
|
||||
})],
|
||||
})
|
||||
move.post()
|
||||
return move
|
||||
|
||||
def test_aged_report(self):
|
||||
AgedReport = self.env['report.account.report_agedpartnerbalance'].with_context(include_nullified_amount=True)
|
||||
@@ -1169,6 +1171,68 @@ class TestReconciliationExec(TestReconciliation):
|
||||
# because they owe us still 50 CC.
|
||||
self.assertEqual(invoice_cust_1.invoice_payment_state, 'not_paid', 'Invoice is in status %s' % invoice_cust_1.state)
|
||||
|
||||
def test_inv_refund_foreign_payment_writeoff_domestic(self):
|
||||
company = self.env.ref('base.main_company')
|
||||
self.env['res.currency.rate'].search([]).unlink()
|
||||
self.env['res.currency.rate'].create({
|
||||
'name': time.strftime('%Y') + '-07-01',
|
||||
'rate': 1.0,
|
||||
'currency_id': self.currency_euro_id,
|
||||
'company_id': company.id
|
||||
})
|
||||
self.env['res.currency.rate'].create({
|
||||
'name': time.strftime('%Y') + '-07-01',
|
||||
'rate': 1.113900, # Don't change this !
|
||||
'currency_id': self.currency_usd_id,
|
||||
'company_id': self.env.ref('base.main_company').id
|
||||
})
|
||||
inv1 = self.create_invoice(invoice_amount=480, currency_id=self.currency_usd_id)
|
||||
inv2 = self.create_invoice(type="out_refund", invoice_amount=140, currency_id=self.currency_usd_id)
|
||||
|
||||
payment = self.env['account.payment'].create({
|
||||
'payment_method_id': self.inbound_payment_method.id,
|
||||
'payment_type': 'inbound',
|
||||
'partner_type': 'customer',
|
||||
'partner_id': inv1.partner_id.id,
|
||||
'amount': 287.20,
|
||||
'journal_id': self.bank_journal_euro.id,
|
||||
'company_id': company.id,
|
||||
})
|
||||
payment.post()
|
||||
|
||||
inv1_receivable = inv1.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
|
||||
inv2_receivable = inv2.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
|
||||
pay_receivable = payment.move_line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
|
||||
|
||||
data_for_reconciliation = [
|
||||
{
|
||||
'type': 'partner',
|
||||
'id': inv1.partner_id.id,
|
||||
'mv_line_ids': (inv1_receivable + inv2_receivable + pay_receivable).ids,
|
||||
'new_mv_line_dicts': [
|
||||
{
|
||||
'credit': 18.04,
|
||||
'debit': 0.00,
|
||||
'journal_id': self.bank_journal_euro.id,
|
||||
'name': 'Total WriteOff (Fees)',
|
||||
'account_id': self.diff_expense_account.id
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
self.env["account.reconciliation.widget"].process_move_lines(data_for_reconciliation)
|
||||
|
||||
self.assertTrue(inv1_receivable.full_reconcile_id.exists())
|
||||
self.assertEquals(inv1_receivable.full_reconcile_id, inv2_receivable.full_reconcile_id)
|
||||
self.assertEquals(inv1_receivable.full_reconcile_id, pay_receivable.full_reconcile_id)
|
||||
|
||||
self.assertTrue(all(l.reconciled for l in inv1_receivable))
|
||||
self.assertTrue(all(l.reconciled for l in inv2_receivable))
|
||||
|
||||
self.assertEquals(inv1.invoice_payment_state, 'paid')
|
||||
self.assertEquals(inv2.invoice_payment_state, 'paid')
|
||||
|
||||
def test_multiple_term_reconciliation_opw_1906665(self):
|
||||
'''Test that when registering a payment to an invoice with multiple
|
||||
payment term lines the reconciliation happens against the line
|
||||
|
||||
@@ -53,3 +53,82 @@ class TestReconciliationWidget(TestReconciliation):
|
||||
|
||||
result = self.env['account.reconciliation.widget'].get_bank_statement_line_data(bank_stmt_line.ids)
|
||||
self.assertEqual(result['lines'][0]['reconciliation_proposition'][0]['amount_str'], '$ 50.00')
|
||||
|
||||
def test_filter_partner1(self):
|
||||
inv1 = self.create_invoice(currency_id=self.currency_euro_id)
|
||||
inv2 = self.create_invoice(currency_id=self.currency_euro_id)
|
||||
|
||||
receivable1 = inv1.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
|
||||
receivable2 = inv2.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
|
||||
|
||||
bank_stmt = self.acc_bank_stmt_model.create({
|
||||
'company_id': self.env.ref('base.main_company').id,
|
||||
'journal_id': self.bank_journal_euro.id,
|
||||
'date': time.strftime('%Y-07-15'),
|
||||
'name': 'test',
|
||||
})
|
||||
|
||||
bank_stmt_line = self.acc_bank_stmt_line_model.create({
|
||||
'name': 'testLine',
|
||||
'statement_id': bank_stmt.id,
|
||||
'amount': 100,
|
||||
'date': time.strftime('%Y-07-15'),
|
||||
})
|
||||
|
||||
# This is like opening the widget, and type "deco" in the filter
|
||||
mv_lines_rec = self.env['account.reconciliation.widget'].get_move_lines_for_bank_statement_line(
|
||||
bank_stmt_line.id,
|
||||
partner_id=False,
|
||||
excluded_ids=[],
|
||||
search_str="deco",
|
||||
mode="rp",
|
||||
)
|
||||
mv_lines_ids = [l['id'] for l in mv_lines_rec]
|
||||
|
||||
self.assertIn(receivable1.id, mv_lines_ids)
|
||||
self.assertIn(receivable2.id, mv_lines_ids)
|
||||
|
||||
# This is like clicking on the first receivable
|
||||
partner = inv1.partner_id
|
||||
mv_lines_rec = self.env['account.reconciliation.widget'].get_move_lines_for_bank_statement_line(
|
||||
bank_stmt_line.id,
|
||||
partner_id=partner.id,
|
||||
excluded_ids=[receivable1.id],
|
||||
search_str="deco",
|
||||
mode="rp",
|
||||
)
|
||||
mv_lines_ids = [l['id'] for l in mv_lines_rec]
|
||||
|
||||
self.assertNotIn(receivable1.id, mv_lines_ids)
|
||||
self.assertIn(receivable2.id, mv_lines_ids)
|
||||
|
||||
def test_partner_name_with_parent(self):
|
||||
parent_partner = self.env['res.partner'].create({
|
||||
'name': 'test',
|
||||
})
|
||||
child_partner = self.env['res.partner'].create({
|
||||
'name': 'test',
|
||||
'parent_id': parent_partner.id,
|
||||
'type': 'delivery',
|
||||
})
|
||||
self.create_invoice_partner(currency_id=self.currency_euro_id, partner_id=child_partner.id)
|
||||
|
||||
bank_stmt = self.acc_bank_stmt_model.create({
|
||||
'company_id': self.env.ref('base.main_company').id,
|
||||
'journal_id': self.bank_journal_euro.id,
|
||||
'date': time.strftime('%Y-07-15'),
|
||||
'name': 'test',
|
||||
})
|
||||
|
||||
bank_stmt_line = self.acc_bank_stmt_line_model.create({
|
||||
'name': 'testLine',
|
||||
'statement_id': bank_stmt.id,
|
||||
'amount': 100,
|
||||
'date': time.strftime('%Y-07-15'),
|
||||
'partner_name': 'test',
|
||||
})
|
||||
|
||||
bkstmt_data = self.env['account.reconciliation.widget'].get_bank_statement_line_data(bank_stmt_line.ids)
|
||||
|
||||
self.assertEqual(len(bkstmt_data['lines']), 1)
|
||||
self.assertEqual(bkstmt_data['lines'][0]['partner_id'], parent_partner.id)
|
||||
|
||||
@@ -348,12 +348,32 @@ class TestTax(AccountTestUsers):
|
||||
'price_include': True,
|
||||
'include_base_amount': True,
|
||||
'sequence': 1,
|
||||
'invoice_repartition_line_ids': [
|
||||
(0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
],
|
||||
'refund_repartition_line_ids': [
|
||||
(0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
],
|
||||
})
|
||||
tax_2 = self.env['account.tax'].create({
|
||||
'name': 'test_advanced_taxes_computation_0_2',
|
||||
'amount_type': 'percent',
|
||||
'amount': 10,
|
||||
'sequence': 2,
|
||||
'invoice_repartition_line_ids': [
|
||||
(0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
],
|
||||
'refund_repartition_line_ids': [
|
||||
(0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
],
|
||||
})
|
||||
tax_3 = self.env['account.tax'].create({
|
||||
'name': 'test_advanced_taxes_computation_0_3',
|
||||
@@ -361,12 +381,32 @@ class TestTax(AccountTestUsers):
|
||||
'amount': 10,
|
||||
'price_include': True,
|
||||
'sequence': 3,
|
||||
'invoice_repartition_line_ids': [
|
||||
(0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
],
|
||||
'refund_repartition_line_ids': [
|
||||
(0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
],
|
||||
})
|
||||
tax_4 = self.env['account.tax'].create({
|
||||
'name': 'test_advanced_taxes_computation_0_4',
|
||||
'amount_type': 'percent',
|
||||
'amount': 10,
|
||||
'sequence': 4,
|
||||
'invoice_repartition_line_ids': [
|
||||
(0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
],
|
||||
'refund_repartition_line_ids': [
|
||||
(0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
],
|
||||
})
|
||||
tax_5 = self.env['account.tax'].create({
|
||||
'name': 'test_advanced_taxes_computation_0_5',
|
||||
@@ -374,21 +414,229 @@ class TestTax(AccountTestUsers):
|
||||
'amount': 10,
|
||||
'price_include': True,
|
||||
'sequence': 5,
|
||||
'invoice_repartition_line_ids': [
|
||||
(0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
],
|
||||
'refund_repartition_line_ids': [
|
||||
(0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
],
|
||||
})
|
||||
taxes = tax_1 + tax_2 + tax_3 + tax_4 + tax_5
|
||||
res = taxes.compute_all(132.0)
|
||||
|
||||
# Test with positive amount.
|
||||
self._check_compute_all_results(
|
||||
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 |
|
||||
(100.0, 5.0), # | 1 | 10% | t | t
|
||||
(100.0, 5.0), # | 1 | 10% | t | t
|
||||
(110.0, 5.5), # | 2 | 10% | |
|
||||
(110.0, 5.5), # | 2 | 10% | |
|
||||
(110.0, 5.5), # | 3 | 10% | t |
|
||||
(110.0, 5.5), # | 3 | 10% | t |
|
||||
(110.0, 5.5), # | 4 | 10% | |
|
||||
(110.0, 5.5), # | 4 | 10% | |
|
||||
(110.0, 5.5), # | 5 | 10% | t |
|
||||
(110.0, 5.5), # | 5 | 10% | t |
|
||||
# ---------------------------------------------------
|
||||
],
|
||||
res
|
||||
taxes.compute_all(132.0)
|
||||
)
|
||||
|
||||
# Test with negative amount.
|
||||
self._check_compute_all_results(
|
||||
-154, # 'total_included'
|
||||
-100, # 'total_excluded'
|
||||
[
|
||||
# base , amount | seq | amount | incl | incl_base
|
||||
# ---------------------------------------------------
|
||||
(-100.0, -5.0), # | 1 | 10% | t | t
|
||||
(-100.0, -5.0), # | 1 | 10% | t | t
|
||||
(-110.0, -5.5), # | 2 | 10% | |
|
||||
(-110.0, -5.5), # | 2 | 10% | |
|
||||
(-110.0, -5.5), # | 3 | 10% | t |
|
||||
(-110.0, -5.5), # | 3 | 10% | t |
|
||||
(-110.0, -5.5), # | 4 | 10% | |
|
||||
(-110.0, -5.5), # | 4 | 10% | |
|
||||
(-110.0, -5.5), # | 5 | 10% | t |
|
||||
(-110.0, -5.5), # | 5 | 10% | t |
|
||||
# ---------------------------------------------------
|
||||
],
|
||||
taxes.compute_all(-132.0)
|
||||
)
|
||||
|
||||
def test_intracomm_taxes_computation_0(self):
|
||||
''' Test usage of intracomm taxes having e.g.+100%, -100% as repartition lines. '''
|
||||
intracomm_tax = self.env['account.tax'].create({
|
||||
'name': 'test_intracomm_taxes_computation_0_1',
|
||||
'amount_type': 'percent',
|
||||
'amount': 21,
|
||||
'invoice_repartition_line_ids': [
|
||||
(0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': -100.0}),
|
||||
],
|
||||
'refund_repartition_line_ids': [
|
||||
(0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': -100.0}),
|
||||
],
|
||||
})
|
||||
|
||||
# Test with positive amount.
|
||||
self._check_compute_all_results(
|
||||
100, # 'total_included'
|
||||
100, # 'total_excluded'
|
||||
[
|
||||
# base , amount
|
||||
# ---------------
|
||||
(100.0, 21.0),
|
||||
(100.0, -21.0),
|
||||
# ---------------
|
||||
],
|
||||
intracomm_tax.compute_all(100.0)
|
||||
)
|
||||
|
||||
# Test with negative amount.
|
||||
self._check_compute_all_results(
|
||||
-100, # 'total_included'
|
||||
-100, # 'total_excluded'
|
||||
[
|
||||
# base , amount
|
||||
# ---------------
|
||||
(-100.0, -21.0),
|
||||
(-100.0, 21.0),
|
||||
# ---------------
|
||||
],
|
||||
intracomm_tax.compute_all(-100.0)
|
||||
)
|
||||
|
||||
def test_rounding_issues_0(self):
|
||||
''' Test taxes having a complex setup of repartition lines. '''
|
||||
tax = self.env['account.tax'].create({
|
||||
'name': 'test_rounding_issues_0',
|
||||
'amount_type': 'percent',
|
||||
'amount': 3,
|
||||
'invoice_repartition_line_ids': [
|
||||
(0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
],
|
||||
'refund_repartition_line_ids': [
|
||||
(0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
],
|
||||
})
|
||||
|
||||
# Test with positive amount.
|
||||
self._check_compute_all_results(
|
||||
1.09, # 'total_included'
|
||||
1, # 'total_excluded'
|
||||
[
|
||||
# base , amount
|
||||
# ---------------
|
||||
(1.0, 0.01),
|
||||
(1.0, 0.01),
|
||||
(1.0, 0.01),
|
||||
(1.0, 0.02),
|
||||
(1.0, 0.02),
|
||||
(1.0, 0.02),
|
||||
# ---------------
|
||||
],
|
||||
tax.compute_all(1.0)
|
||||
)
|
||||
|
||||
# Test with negative amount.
|
||||
self._check_compute_all_results(
|
||||
-1.09, # 'total_included'
|
||||
-1, # 'total_excluded'
|
||||
[
|
||||
# base , amount
|
||||
# ---------------
|
||||
(-1.0, -0.01),
|
||||
(-1.0, -0.01),
|
||||
(-1.0, -0.01),
|
||||
(-1.0, -0.02),
|
||||
(-1.0, -0.02),
|
||||
(-1.0, -0.02),
|
||||
# ---------------
|
||||
],
|
||||
tax.compute_all(-1.0)
|
||||
)
|
||||
|
||||
def test_rounding_issues_1(self):
|
||||
''' Test taxes having a complex setup of repartition lines. '''
|
||||
tax = self.env['account.tax'].create({
|
||||
'name': 'test_advanced_taxes_repartition_lines_computation_1',
|
||||
'amount_type': 'percent',
|
||||
'amount': 3,
|
||||
'invoice_repartition_line_ids': [
|
||||
(0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': -50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 25.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 25.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': -25.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': -25.0}),
|
||||
],
|
||||
'refund_repartition_line_ids': [
|
||||
(0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': -50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 25.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 25.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': -25.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': -25.0}),
|
||||
],
|
||||
})
|
||||
|
||||
# Test with positive amount.
|
||||
self._check_compute_all_results(
|
||||
1, # 'total_included'
|
||||
1, # 'total_excluded'
|
||||
[
|
||||
# base , amount
|
||||
# ---------------
|
||||
(1.0, 0.02),
|
||||
(1.0, -0.02),
|
||||
(1.0, 0.01),
|
||||
(1.0, 0.01),
|
||||
(1.0, -0.01),
|
||||
(1.0, -0.01),
|
||||
# ---------------
|
||||
],
|
||||
tax.compute_all(1.0)
|
||||
)
|
||||
|
||||
# Test with negative amount.
|
||||
self._check_compute_all_results(
|
||||
-1, # 'total_included'
|
||||
-1, # 'total_excluded'
|
||||
[
|
||||
# base , amount
|
||||
# ---------------
|
||||
(-1.0, -0.02),
|
||||
(-1.0, 0.02),
|
||||
(-1.0, -0.01),
|
||||
(-1.0, -0.01),
|
||||
(-1.0, 0.01),
|
||||
(-1.0, 0.01),
|
||||
# ---------------
|
||||
],
|
||||
tax.compute_all(-1.0)
|
||||
)
|
||||
|
||||
@@ -61,6 +61,9 @@
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="amount_currency"/>
|
||||
</group>
|
||||
<group string="Product" attrs="{'invisible': [('product_id', '=', False)]}">
|
||||
<field name="product_id"/>
|
||||
</group>
|
||||
<group string="States">
|
||||
<field name="blocked"/>
|
||||
</group>
|
||||
@@ -611,7 +614,7 @@
|
||||
<label for="invoice_date" string="Bill Date" style="font-weight:bold;"
|
||||
attrs="{'invisible': [('type', 'not in', ('in_invoice', 'in_refund', 'in_receipt'))]}"/>
|
||||
</div>
|
||||
<field name="invoice_date" nolabel="1"
|
||||
<field name="invoice_date" nolabel="1" options="{'datepicker': {'warn_future': true}}"
|
||||
attrs="{'invisible': [('type', 'not in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund', 'out_receipt', 'in_receipt'))]}"/>
|
||||
|
||||
<field name="date" string="Accounting Date"
|
||||
@@ -880,6 +883,8 @@
|
||||
attrs="{'invisible': [('type', '!=', 'entry')], 'readonly': [('reversed_entry_id', '!=', False)]}"/>
|
||||
<field name="reversed_entry_id"
|
||||
attrs="{'invisible': [('type', '!=', 'entry')]}"/>
|
||||
<field name="to_check"
|
||||
attrs="{'invisible': [('type', '!=', 'entry')]}" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="company_id" groups="base.group_multi_company" required="1"/>
|
||||
|
||||
@@ -182,7 +182,7 @@
|
||||
<p t-if="o.fiscal_position_id.note" name="note">
|
||||
<span t-field="o.fiscal_position_id.note"/>
|
||||
</p>
|
||||
<div id="qrcode" t-if="(o.partner_id.country_id.code in ['BE', 'AT', 'DE', 'FI', 'NL']) and (o.company_id.qr_code) and (o.currency_id.name == 'EUR') and (o.invoice_partner_bank_id.acc_number != False)">
|
||||
<div id="qrcode" t-if="(o.company_id.qr_code) and (o.currency_id.name == 'EUR') and (o.invoice_partner_bank_id.acc_number != False)">
|
||||
<p t-if="(o.invoice_partner_bank_id.qr_code_valid)">
|
||||
<strong class="text-center">Scan me with your banking app.</strong><br /><br />
|
||||
<img class="border border-dark rounded" t-att-src="o.invoice_partner_bank_id.build_qr_code_url(o.amount_residual,(o.ref) if (o.ref) else o.name)"/>
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
<field name="model">account.journal</field>
|
||||
<field name="inherit_id" ref="account.view_account_journal_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='loss_account_id']/parent::group" position="attributes">
|
||||
<attribute name="attrs">{'invisible': 0}</attribute>
|
||||
</xpath>
|
||||
<field name="loss_account_id" position="after">
|
||||
<field name="update_posted" groups="base.group_no_one"/>
|
||||
</field>
|
||||
|
||||
@@ -99,6 +99,11 @@ class AccountPayment(models.Model):
|
||||
""" This method is a hook for l10n_xx_check_printing modules to implement actual check printing capabilities """
|
||||
raise UserError(_("You have to choose a check layout. For this, go in Apps, search for 'Checks layout' and install one."))
|
||||
|
||||
|
||||
def set_check_amount_in_words(self):
|
||||
for payment in self:
|
||||
payment.check_amount_in_words = payment.currency_id.amount_to_text(payment.amount) if payment.currency_id else ''
|
||||
|
||||
#######################
|
||||
#CHECK PRINTING METHODS
|
||||
#######################
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<xpath expr="//button[@name='post']" position="before">
|
||||
<button name="print_checks" class="oe_highlight" attrs="{'invisible': ['|', ('payment_method_code', '!=', 'check_printing'), ('state', '!=', 'posted')]}" string="Print Check" type="object"/>
|
||||
<button name="unmark_sent" attrs="{'invisible': ['|', ('payment_method_code', '!=', 'check_printing'), ('state', '!=', 'sent')]}" string="Unmark Sent" type="object"/>
|
||||
<button name="set_check_amount_in_words" attrs="{'invisible': ['|', ('payment_method_code', '!=', 'check_printing'), ('check_amount_in_words', '!=', False)]}" string="Reset Amount in Words" type="object" groups="base.group_no_one"/>
|
||||
</xpath>
|
||||
<xpath expr="//div[@name='amount_div']" position="after">
|
||||
<field name="check_amount_in_words" attrs="{'invisible': [('payment_method_code', '!=', 'check_printing')], 'readonly': [('state', '!=', 'draft')]}" groups="base.group_no_one"/>
|
||||
|
||||
@@ -28,6 +28,8 @@ class AccountTaxPython(models.Model):
|
||||
|
||||
def _compute_amount(self, base_amount, price_unit, quantity=1.0, product=None, partner=None):
|
||||
self.ensure_one()
|
||||
if product and product._name == 'product.template':
|
||||
product = product.product_variant_id
|
||||
if self.amount_type == 'code':
|
||||
company = self.env.company
|
||||
localdict = {'base_amount': base_amount, 'price_unit':price_unit, 'quantity': quantity, 'product':product, 'partner':partner, 'company': company}
|
||||
@@ -38,6 +40,8 @@ class AccountTaxPython(models.Model):
|
||||
def compute_all(self, price_unit, currency=None, quantity=1.0, product=None, partner=None, is_refund=False, handle_price_include=True):
|
||||
taxes = self.filtered(lambda r: r.amount_type != 'code')
|
||||
company = self.env.company
|
||||
if product and product._name == 'product.template':
|
||||
product = product.product_variant_id
|
||||
for tax in self.filtered(lambda r: r.amount_type == 'code'):
|
||||
localdict = self._context.get('tax_computation_context', {})
|
||||
localdict.update({'price_unit': price_unit, 'quantity': quantity, 'product': product, 'partner': partner, 'company': company})
|
||||
|
||||
@@ -109,19 +109,10 @@ class OAuthLogin(Home):
|
||||
|
||||
return response
|
||||
|
||||
@http.route()
|
||||
def web_auth_signup(self, *args, **kw):
|
||||
providers = self.list_providers()
|
||||
response = super(OAuthLogin, self).web_auth_signup(*args, **kw)
|
||||
response.qcontext.update(providers=providers)
|
||||
return response
|
||||
|
||||
@http.route()
|
||||
def web_auth_reset_password(self, *args, **kw):
|
||||
providers = self.list_providers()
|
||||
response = super(OAuthLogin, self).web_auth_reset_password(*args, **kw)
|
||||
response.qcontext.update(providers=providers)
|
||||
return response
|
||||
def get_auth_signup_qcontext(self):
|
||||
result = super(OAuthLogin, self).get_auth_signup_qcontext()
|
||||
result["providers"] = self.list_providers()
|
||||
return result
|
||||
|
||||
|
||||
class OAuthController(http.Controller):
|
||||
|
||||
@@ -31,6 +31,7 @@ class Partner(models.Model):
|
||||
replacement_xml = """
|
||||
<div>
|
||||
<field name="country_enforce_cities" invisible="1"/>
|
||||
<field name="parent_id" invisible="1"/>
|
||||
<field name='city' placeholder="%(placeholder)s" class="o_address_city"
|
||||
attrs="{
|
||||
'invisible': [('country_enforce_cities', '=', True), ('city_id', '!=', False)],
|
||||
|
||||
@@ -445,7 +445,11 @@ class Import(models.TransientModel):
|
||||
'date_format') else []
|
||||
user_date_format = self.env['res.lang']._lang_get(self.env.user.lang).date_format
|
||||
if user_date_format:
|
||||
date_patterns.append(user_date_format)
|
||||
try:
|
||||
to_re(user_date_format)
|
||||
date_patterns.append(user_date_format)
|
||||
except KeyError:
|
||||
pass
|
||||
date_patterns.extend(DATE_PATTERNS)
|
||||
match = check_patterns(date_patterns, preview_values)
|
||||
if match:
|
||||
|
||||
@@ -79,6 +79,12 @@ var CrossTabBus = Longpolling.extend({
|
||||
this._super.apply(this, arguments);
|
||||
this._callLocalStorage('setItem', 'channels', this._channels);
|
||||
},
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
getTabId: function () {
|
||||
return this._id;
|
||||
},
|
||||
/**
|
||||
* Tells whether this bus is related to the master tab.
|
||||
*
|
||||
|
||||
@@ -1051,11 +1051,13 @@ class Meeting(models.Model):
|
||||
meeting_attendees |= attendee
|
||||
meeting_partners |= partner
|
||||
|
||||
if meeting_attendees:
|
||||
if meeting_attendees and not self._context.get('detaching'):
|
||||
to_notify = meeting_attendees.filtered(lambda a: a.email != current_user.email)
|
||||
to_notify._send_mail_to_attendees('calendar.calendar_template_meeting_invitation')
|
||||
|
||||
if meeting_attendees:
|
||||
meeting.write({'attendee_ids': [(4, meeting_attendee.id) for meeting_attendee in meeting_attendees]})
|
||||
|
||||
if meeting_partners:
|
||||
meeting.message_subscribe(partner_ids=meeting_partners.ids)
|
||||
|
||||
@@ -1397,7 +1399,7 @@ class Meeting(models.Model):
|
||||
# do not copy the id
|
||||
if data.get('id'):
|
||||
del data['id']
|
||||
return meeting_origin.copy(default=data)
|
||||
return meeting_origin.with_context(detaching=True).copy(default=data)
|
||||
|
||||
def action_detach_recurring_event(self):
|
||||
meeting = self.detach_recurring_event()
|
||||
|
||||
@@ -7,6 +7,7 @@ from datetime import datetime, timedelta, time
|
||||
from odoo import fields
|
||||
from odoo.tests.common import TransactionCase
|
||||
import pytz
|
||||
import re
|
||||
|
||||
|
||||
class TestCalendar(TransactionCase):
|
||||
@@ -384,3 +385,60 @@ class TestCalendar(TransactionCase):
|
||||
})
|
||||
|
||||
self.assertEqual(str(activity_id.date_deadline), '2018-10-16')
|
||||
|
||||
def test_event_creation_mail(self):
|
||||
"""
|
||||
Check that mail are sent to the attendees on event creation
|
||||
Check that mail are sent to the added attendees on event edit
|
||||
Check that mail are NOT sent to the attendees when detaching a recurring event
|
||||
"""
|
||||
|
||||
def _test_one_mail_per_attendee(self, m, partners):
|
||||
# check that every attendee receive a (single) mail for the event
|
||||
for partner in partners:
|
||||
mail = self.env['mail.mail'].search([
|
||||
('recipient_ids', 'in', partner.id),
|
||||
('subject', 'like', m.name),
|
||||
])
|
||||
self.assertEqual(len(mail), 1)
|
||||
|
||||
partners = [
|
||||
self.env['res.partner'].create({'name':'testuser0','email': u'bob@example.com'}),
|
||||
self.env['res.partner'].create({'name':'testuser1','email': u'alice@example.com'}),
|
||||
]
|
||||
partner_ids = [(6, False, [p.id for p in partners]),]
|
||||
now = fields.Datetime.now()
|
||||
m = self.CalendarEvent.create({
|
||||
'name': "mailTest1",
|
||||
'allday': False,
|
||||
'rrule': u'FREQ=DAILY;INTERVAL=1;COUNT=5',
|
||||
'duration': 0.5,
|
||||
'partner_ids': partner_ids,
|
||||
'start': fields.Datetime.to_string(now + timedelta(days=10)),
|
||||
'stop': fields.Datetime.to_string(now + timedelta(days=15)),
|
||||
})
|
||||
|
||||
# every partner should have 1 mail sent
|
||||
_test_one_mail_per_attendee(self, m, partners)
|
||||
|
||||
# adding more partners to the event
|
||||
partners.extend([
|
||||
self.env['res.partner'].create({'name':'testuser2','email': u'marc@example.com'}),
|
||||
self.env['res.partner'].create({'name':'testuser3','email': u'carl@example.com'}),
|
||||
self.env['res.partner'].create({'name':'testuser4','email': u'alain@example.com'}),
|
||||
])
|
||||
partner_ids = [(6, False, [p.id for p in partners]),]
|
||||
m.write({'partner_ids': partner_ids})
|
||||
|
||||
# more email should be sent
|
||||
_test_one_mail_per_attendee(self, m, partners)
|
||||
|
||||
# calculate virtualid to detach one event
|
||||
virtid = str(m.id) + '-' + ''.join(re.split('[\D]', fields.Datetime.to_string(now + timedelta(days=12))))
|
||||
|
||||
# detaching a virtual event in the chain
|
||||
self.env['calendar.event'].browse(virtid).detach_recurring_event(values={'active':False})
|
||||
|
||||
# since the detach actually create an event in the backend
|
||||
# we check that no mail notifications are sent to the attendees
|
||||
_test_one_mail_per_attendee(self, m, partners)
|
||||
|
||||
@@ -202,7 +202,7 @@
|
||||
|
||||
<page name="page_invitations" string="Invitations" groups="base.group_no_one">
|
||||
<button name="action_sendmail" type="object" string="Send mail" icon="fa-envelope" class="oe_link"/>
|
||||
<field name="attendee_ids" widget="one2many" >
|
||||
<field name="attendee_ids" widget="one2many" mode="tree,kanban">
|
||||
<tree string="Invitation details" editable="top" create="false" delete="false">
|
||||
<field name="partner_id" />
|
||||
<field name="state" />
|
||||
@@ -212,6 +212,27 @@
|
||||
<button name="do_accept" string="Accept" states="needsAction,tentative,declined" type="object" icon="fa-check text-success"/>
|
||||
<button name="do_decline" string="Decline" states="needsAction,tentative,accepted" type="object" icon="fa-times-circle text-danger"/>
|
||||
</tree>
|
||||
<kanban class="o_kanban_mobile" create="false" delete="false">
|
||||
<field name="partner_id" />
|
||||
<field name="state" />
|
||||
<field name="email" widget="email"/>
|
||||
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="d-flex flex-column justify-content-between">
|
||||
<field name="partner_id"/>
|
||||
<field name="email" widget="email"/>
|
||||
<span>Status: <field name="state" /></span>
|
||||
|
||||
<div class="text-right">
|
||||
<button name="do_tentative" states="needsAction,declined,accepted" string="Uncertain" type="object" class="btn fa fa-asterisk"/>
|
||||
<button name="do_accept" states="needsAction,tentative,declined" string="Accept" type="object" class="btn fa fa-check text-success"/>
|
||||
<button name="do_decline" states="needsAction,tentative,accepted" string="Decline" type="object" class="btn fa fa-times-circle text-danger"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</page>
|
||||
<page name="page_misc" string="Misc" groups="base.group_no_one">
|
||||
|
||||
@@ -379,9 +379,9 @@ class Challenge(models.Model):
|
||||
|
||||
# the goal is initialised over the limit to make sure we will compute it at least once
|
||||
if line.condition == 'higher':
|
||||
values['current'] = line.target_goal - 1
|
||||
values['current'] = min(line.target_goal - 1, 0)
|
||||
else:
|
||||
values['current'] = line.target_goal + 1
|
||||
values['current'] = max(line.target_goal + 1, 0)
|
||||
|
||||
if challenge.remind_update_delay:
|
||||
values['remind_update_delay'] = challenge.remind_update_delay
|
||||
@@ -737,7 +737,7 @@ class Challenge(models.Model):
|
||||
all_reached = False
|
||||
if goal.definition_condition == 'higher':
|
||||
# can be over 100
|
||||
total_completeness += 100.0 * goal.current / goal.target_goal
|
||||
total_completeness += (100.0 * goal.current / goal.target_goal) if goal.target_goal else 0
|
||||
elif goal.state == 'reached':
|
||||
# for lower goals, can not get percentage so 0 or 100
|
||||
total_completeness += 100
|
||||
|
||||
@@ -195,7 +195,7 @@ class Goal(models.Model):
|
||||
if goal.current >= goal.target_goal:
|
||||
goal.completeness = 100.0
|
||||
else:
|
||||
goal.completeness = round(100.0 * goal.current / goal.target_goal, 2)
|
||||
goal.completeness = round(100.0 * goal.current / goal.target_goal, 2) if goal.target_goal else 0
|
||||
elif goal.current < goal.target_goal:
|
||||
# a goal 'lower than' has only two values possible: 0 or 100%
|
||||
goal.completeness = 100.0
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<record id="goals_from_challenge_act" model="ir.actions.act_window">
|
||||
<field name="res_model">gamification.goal</field>
|
||||
<field name="name">Related Goals</field>
|
||||
<field name="view_mode">kanban,tree</field>
|
||||
<field name="view_mode">kanban,tree,form</field>
|
||||
<field name="context">{'search_default_group_by_definition': True, 'search_default_inprogress': True, 'search_default_challenge_id': active_id, 'default_challenge_id': active_id}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_empty_folder">
|
||||
@@ -61,7 +61,7 @@
|
||||
<label for="user_domain" class="oe_edit_only" string="Assign Challenge To"/>
|
||||
<div>
|
||||
<field name="user_domain" widget="domain" options="{'model': 'res.users'}" />
|
||||
<field name="user_ids" groups="base.group_no_one" widget="many2many_tags" readonly="True" />
|
||||
<field name="user_ids" widget="many2many_tags" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -171,7 +171,7 @@
|
||||
<record id="challenge_list_action" model="ir.actions.act_window">
|
||||
<field name="name">Challenges</field>
|
||||
<field name="res_model">gamification.challenge</field>
|
||||
<field name="view_mode">kanban,tree,form</field>
|
||||
<field name="view_mode">kanban,tree</field>
|
||||
<field name="context">{'search_default_inprogress':True, 'default_inprogress':True}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
|
||||
@@ -135,6 +135,7 @@
|
||||
<field name="completeness"/>
|
||||
<field name="state"/>
|
||||
<field name="target_goal"/>
|
||||
<field name="definition_description"/>
|
||||
<field name="definition_condition"/>
|
||||
<field name="definition_suffix"/>
|
||||
<field name="definition_display"/>
|
||||
@@ -143,10 +144,12 @@
|
||||
<field name="last_update"/>
|
||||
<templates>
|
||||
<t t-name="kanban-tooltip">
|
||||
<field name="definition_description"/>
|
||||
<ul class="oe_kanban_tooltip">
|
||||
<li><t t-esc="record.definition_description.value" /></li>
|
||||
</ul>
|
||||
</t>
|
||||
<t t-name="kanban-box">
|
||||
<div t-attf-class="oe_kanban_card o_kanban_gamification #{record.end_date.raw_value < record.last_update.raw_value & record.state.raw_value == 'failed' ? 'oe_kanban_color_2' : ''} #{record.end_date.raw_value < record.last_update.raw_value & record.state.raw_value == 'reached' ? 'oe_kanban_color_5' : ''}">
|
||||
<div t-attf-class="oe_kanban_card oe_kanban_global_click o_kanban_gamification #{record.end_date.raw_value < record.last_update.raw_value & record.state.raw_value == 'failed' ? 'oe_kanban_color_2' : ''} #{record.end_date.raw_value < record.last_update.raw_value & record.state.raw_value == 'reached' ? 'oe_kanban_color_5' : ''}">
|
||||
<div class="o_kanban_content">
|
||||
<p><strong><h4 class="oe_goal_name text-center" tooltip="kanban-tooltip"><field name="definition_id" /></h4></strong></p>
|
||||
<div class="float-left">
|
||||
|
||||
@@ -33,10 +33,16 @@ class HrExpense(models.Model):
|
||||
if self.user_has_groups('hr_expense.group_hr_expense_user') or self.user_has_groups('account.group_account_user'):
|
||||
res = "['|', ('company_id', '=', False), ('company_id', '=', company_id)]" # Then, domain accepts everything
|
||||
elif self.user_has_groups('hr_expense.group_hr_expense_team_approver') and self.env.user.employee_ids:
|
||||
employee = self.env.user.employee_ids[0]
|
||||
res = ['|', '|', '|', ('department_id.manager_id.id', '=', employee.id),
|
||||
('parent_id.id', '=', employee.id), ('expense_manager_id.id', '=', employee.user_id.id), ('id', '=', employee.id),
|
||||
'|', ('company_id', '=', False), ('company_id', '=', employee.company_id.id)]
|
||||
user = self.env.user
|
||||
employee = user.employee_ids[0]
|
||||
res = [
|
||||
'|', '|', '|',
|
||||
('department_id.manager_id', '=', employee.id),
|
||||
('parent_id', '=', employee.id),
|
||||
('id', '=', employee.id),
|
||||
('expense_manager_id', '=', user.id),
|
||||
'|', ('company_id', '=', False), ('company_id', '=', employee.company_id.id),
|
||||
]
|
||||
elif self.env.user.employee_ids:
|
||||
employee = self.env.user.employee_ids[0]
|
||||
res = [('id', '=', employee.id), '|', ('company_id', '=', False), ('company_id', '=', employee.company_id.id)]
|
||||
|
||||
@@ -187,6 +187,72 @@ class TestAccountEntry(TestExpenseCommon):
|
||||
self.assertEquals(expense.total_amount, 9876.0)
|
||||
self.assertTrue(expense.employee_id in user_demo.employee_ids)
|
||||
|
||||
def test_partial_payment_multiexpense(self):
|
||||
bank_journal = self.env['account.journal'].create({
|
||||
'name': 'Payment Journal',
|
||||
'code': 'PAY',
|
||||
'type': 'bank',
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
|
||||
outbound_pay_method = self.env['account.payment.method'].create({
|
||||
'name': 'outbound',
|
||||
'code': 'out',
|
||||
'payment_type': 'outbound',
|
||||
})
|
||||
|
||||
expense = self.env['hr.expense.sheet'].create({
|
||||
'name': 'Expense for John Smith',
|
||||
'employee_id': self.employee.id,
|
||||
})
|
||||
expense_line = self.env['hr.expense'].create({
|
||||
'name': 'Car Travel Expenses',
|
||||
'employee_id': self.employee.id,
|
||||
'product_id': self.product_expense.id,
|
||||
'unit_amount': 200.00,
|
||||
'tax_ids': [(6, 0, [self.tax.id])],
|
||||
'sheet_id': expense.id,
|
||||
'analytic_account_id': self.analytic_account.id,
|
||||
})
|
||||
expense_line.copy({
|
||||
'sheet_id': expense.id
|
||||
})
|
||||
expense.approve_expense_sheets()
|
||||
expense.action_sheet_move_create()
|
||||
|
||||
exp_move_lines = expense.account_move_id.line_ids
|
||||
payable_move_lines = exp_move_lines.filtered(lambda l: l.account_id.internal_type == 'payable')
|
||||
self.assertEquals(len(payable_move_lines), 2)
|
||||
|
||||
WizardRegister = self.env["hr.expense.sheet.register.payment.wizard"].with_context(
|
||||
active_model=expense._name, active_id=expense.id, active_ids=expense.ids
|
||||
)
|
||||
|
||||
register_pay1 = WizardRegister.create({
|
||||
'journal_id': bank_journal.id,
|
||||
'payment_method_id': outbound_pay_method.id,
|
||||
'amount': 300,
|
||||
})
|
||||
register_pay1.expense_post_payment()
|
||||
|
||||
exp_move_lines = expense.account_move_id.line_ids
|
||||
payable_move_lines = exp_move_lines.filtered(lambda l: l.account_id.internal_type == 'payable')
|
||||
self.assertEquals(len(payable_move_lines.filtered(lambda l: l.reconciled)), 1)
|
||||
|
||||
register_pay2 = WizardRegister.create({
|
||||
'journal_id': bank_journal.id,
|
||||
'payment_method_id': outbound_pay_method.id,
|
||||
'amount': 100,
|
||||
})
|
||||
register_pay2.expense_post_payment()
|
||||
exp_move_lines = expense.account_move_id.line_ids
|
||||
payable_move_lines = exp_move_lines.filtered(lambda l: l.account_id.internal_type == 'payable')
|
||||
self.assertEquals(len(payable_move_lines.filtered(lambda l: l.reconciled)), 2)
|
||||
|
||||
full_reconcile = payable_move_lines.mapped('full_reconcile_id')
|
||||
self.assertEquals(len(full_reconcile), 1)
|
||||
|
||||
|
||||
class TestExpenseRights(TestExpenseCommon):
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -119,7 +119,7 @@ class HrExpenseSheetRegisterPaymentWizard(models.TransientModel):
|
||||
# Reconcile the payment and the expense, i.e. lookup on the payable account move lines
|
||||
account_move_lines_to_reconcile = self.env['account.move.line']
|
||||
for line in payment.move_line_ids + expense_sheet.account_move_id.line_ids:
|
||||
if line.account_id.internal_type == 'payable':
|
||||
if line.account_id.internal_type == 'payable' and not line.reconciled:
|
||||
account_move_lines_to_reconcile |= line
|
||||
account_move_lines_to_reconcile.reconcile()
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
import logging
|
||||
import math
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
from datetime import datetime, time
|
||||
from pytz import timezone, UTC
|
||||
|
||||
@@ -18,6 +20,9 @@ from odoo.tools.translate import _
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# Used to agglomerate the attendances in order to find the hour_from and hour_to
|
||||
# See _onchange_request_parameters
|
||||
DummyAttendance = namedtuple('DummyAttendance', 'hour_from, hour_to, dayofweek, day_period')
|
||||
|
||||
class HolidaysRequest(models.Model):
|
||||
""" Leave Requests Access specifications
|
||||
@@ -279,7 +284,12 @@ class HolidaysRequest(models.Model):
|
||||
|
||||
resource_calendar_id = self.employee_id.resource_calendar_id or self.env.company.resource_calendar_id
|
||||
domain = [('calendar_id', '=', resource_calendar_id.id), ('display_type', '=', False)]
|
||||
attendances = self.env['resource.calendar.attendance'].search(domain, order='dayofweek, day_period DESC')
|
||||
attendances = self.env['resource.calendar.attendance'].read_group(domain, ['ids:array_agg(id)', 'hour_from:min(hour_from)', 'hour_to:max(hour_to)', 'dayofweek', 'day_period'], ['dayofweek', 'day_period'], lazy=False)
|
||||
|
||||
# Must be sorted by dayofweek ASC and day_period DESC
|
||||
attendances = sorted([DummyAttendance(group['hour_from'], group['hour_to'], group['dayofweek'], group['day_period']) for group in attendances], key=lambda att: (att.dayofweek, att.day_period != 'morning'))
|
||||
|
||||
default_value = DummyAttendance(0, 0, 0, 'morning')
|
||||
|
||||
if resource_calendar_id.two_weeks_calendar:
|
||||
# find week type of start_date
|
||||
@@ -306,9 +316,9 @@ class HolidaysRequest(models.Model):
|
||||
attendance_to = attendance_filtred_reversed[0]
|
||||
else:
|
||||
# find first attendance coming after first_day
|
||||
attendance_from = next((att for att in attendances if int(att.dayofweek) >= self.request_date_from.weekday()), attendances[0])
|
||||
attendance_from = next((att for att in attendances if int(att.dayofweek) >= self.request_date_from.weekday()), attendances[0] if attendances else default_value)
|
||||
# find last attendance coming before last_day
|
||||
attendance_to = next((att for att in reversed(attendances) if int(att.dayofweek) <= self.request_date_to.weekday()), attendances[-1])
|
||||
attendance_to = next((att for att in reversed(attendances) if int(att.dayofweek) <= self.request_date_to.weekday()), attendances[-1] if attendances else default_value)
|
||||
|
||||
if self.request_unit_half:
|
||||
if self.request_date_from_period == 'am':
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_access_rights
|
||||
from . import test_automatic_leave_dates
|
||||
from . import test_holidays_flow
|
||||
from . import test_hr_leave_type
|
||||
from . import test_accrual_allocations
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import date, datetime
|
||||
|
||||
from odoo.tests.common import Form, tagged
|
||||
|
||||
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysBase
|
||||
|
||||
|
||||
@tagged('prout')
|
||||
class TestAutomaticLeaveDates(TestHrHolidaysBase):
|
||||
def setUp(self):
|
||||
super(TestAutomaticLeaveDates, self).setUp()
|
||||
|
||||
self.leave_type = self.env['hr.leave.type'].create({
|
||||
'name': 'Automatic Test',
|
||||
'time_type': 'leave',
|
||||
'allocation_type': 'no',
|
||||
'validity_start': False,
|
||||
})
|
||||
|
||||
def test_no_attendances(self):
|
||||
calendar = self.env['resource.calendar'].create({
|
||||
'name': 'No Attendances',
|
||||
'attendance_ids': [(5, 0, 0)],
|
||||
})
|
||||
employee = self.employee_emp
|
||||
employee.resource_calendar_id = calendar
|
||||
|
||||
with Form(self.env['hr.leave']) as leave_form:
|
||||
leave_form.employee_id = employee
|
||||
leave_form.holiday_status_id = self.leave_type
|
||||
leave_form.request_date_from = date(2019, 9, 2)
|
||||
leave_form.request_date_to = date(2019, 9, 2)
|
||||
leave_form.request_unit_half = True
|
||||
leave_form.request_date_from_period = 'am'
|
||||
|
||||
self.assertEqual(leave_form.number_of_days_display, 0)
|
||||
self.assertEqual(leave_form.number_of_hours_display, 0)
|
||||
|
||||
def test_single_attendance_on_morning_and_afternoon(self):
|
||||
calendar = self.env['resource.calendar'].create({
|
||||
'name': 'simple morning + afternoon',
|
||||
'attendance_ids': [(5, 0, 0),
|
||||
(0, 0, {
|
||||
'name': 'monday morning',
|
||||
'hour_from': 8,
|
||||
'hour_to': 12,
|
||||
'day_period': 'morning',
|
||||
'dayofweek': '0',
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': 'monday afternoon',
|
||||
'hour_from': 13,
|
||||
'hour_to': 17,
|
||||
'day_period': 'afternoon',
|
||||
'dayofweek': '0',
|
||||
})]
|
||||
})
|
||||
|
||||
employee = self.employee_emp
|
||||
employee.resource_calendar_id = calendar
|
||||
|
||||
with Form(self.env['hr.leave']) as leave_form:
|
||||
leave_form.employee_id = employee
|
||||
leave_form.holiday_status_id = self.leave_type
|
||||
leave_form.request_date_from = date(2019, 9, 2)
|
||||
leave_form.request_date_to = date(2019, 9, 2)
|
||||
leave_form.request_unit_half = True
|
||||
leave_form.request_date_from_period = 'am'
|
||||
|
||||
self.assertEqual(leave_form.number_of_days_display, .5)
|
||||
self.assertEqual(leave_form.number_of_hours_display, 4)
|
||||
|
||||
leave_form.request_date_from_period = 'pm'
|
||||
|
||||
self.assertEqual(leave_form.number_of_days_display, .5)
|
||||
self.assertEqual(leave_form.number_of_hours_display, 4)
|
||||
|
||||
def test_multiple_attendance_on_morning(self):
|
||||
calendar = self.env['resource.calendar'].create({
|
||||
'name': 'multi morning',
|
||||
'attendance_ids': [(5, 0, 0),
|
||||
(0, 0, {
|
||||
'name': 'monday morning 1',
|
||||
'hour_from': 8,
|
||||
'hour_to': 10,
|
||||
'day_period': 'morning',
|
||||
'dayofweek': '0',
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': 'monday morning 2',
|
||||
'hour_from': 10.25,
|
||||
'hour_to': 12.25,
|
||||
'day_period': 'morning',
|
||||
'dayofweek': '0',
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': 'monday afternoon',
|
||||
'hour_from': 13,
|
||||
'hour_to': 17,
|
||||
'day_period': 'afternoon',
|
||||
'dayofweek': '0',
|
||||
})]
|
||||
})
|
||||
employee = self.employee_emp
|
||||
employee.resource_calendar_id = calendar
|
||||
|
||||
with Form(self.env['hr.leave']) as leave_form:
|
||||
leave_form.employee_id = employee
|
||||
leave_form.holiday_status_id = self.leave_type
|
||||
leave_form.request_date_from = date(2019, 9, 2)
|
||||
leave_form.request_date_to = date(2019, 9, 2)
|
||||
leave_form.request_unit_half = True
|
||||
leave_form.request_date_from_period = 'am'
|
||||
|
||||
self.assertEqual(leave_form.number_of_days_display, .5)
|
||||
self.assertEqual(leave_form.number_of_hours_display, 4)
|
||||
|
||||
leave_form.request_date_from_period = 'pm'
|
||||
|
||||
self.assertEqual(leave_form.number_of_days_display, .5)
|
||||
self.assertEqual(leave_form.number_of_hours_display, 4)
|
||||
|
||||
def test_attendance_on_morning(self):
|
||||
calendar = self.env['resource.calendar'].create({
|
||||
'name': 'Morning only',
|
||||
'attendance_ids': [(5, 0, 0),
|
||||
(0, 0, {
|
||||
'name': 'Monday All day',
|
||||
'hour_from': 8,
|
||||
'hour_to': 16,
|
||||
'day_period': 'morning',
|
||||
'dayofweek': '0',
|
||||
})],
|
||||
})
|
||||
employee = self.employee_emp
|
||||
employee.resource_calendar_id = calendar
|
||||
with Form(self.env['hr.leave']) as leave_form:
|
||||
leave_form.employee_id = employee
|
||||
leave_form.holiday_status_id = self.leave_type
|
||||
leave_form.request_date_from = date(2019, 9, 2)
|
||||
leave_form.request_date_to = date(2019, 9, 2)
|
||||
leave_form.request_unit_half = True
|
||||
# Ask for morning
|
||||
leave_form.request_date_from_period = 'am'
|
||||
|
||||
self.assertEqual(leave_form.number_of_days_display, 1)
|
||||
self.assertEqual(leave_form.number_of_hours_display, 8)
|
||||
|
||||
# Ask for afternoon
|
||||
leave_form.request_date_from_period = 'pm'
|
||||
|
||||
self.assertEqual(leave_form.number_of_days_display, 1)
|
||||
self.assertEqual(leave_form.number_of_hours_display, 8)
|
||||
|
||||
def test_attendance_next_day(self):
|
||||
calendar = self.env['resource.calendar'].create({
|
||||
'name': 'auto next day',
|
||||
'attendance_ids': [(5, 0, 0),
|
||||
(0, 0, {
|
||||
'name': 'tuesday morning',
|
||||
'hour_from': 8,
|
||||
'hour_to': 12,
|
||||
'day_period': 'morning',
|
||||
'dayofweek': '1',
|
||||
})]
|
||||
})
|
||||
employee = self.employee_emp
|
||||
employee.resource_calendar_id = calendar
|
||||
|
||||
with Form(self.env['hr.leave']) as leave_form:
|
||||
leave_form.employee_id = employee
|
||||
leave_form.holiday_status_id = self.leave_type
|
||||
leave_form.request_date_from = date(2019, 9, 2)
|
||||
leave_form.request_date_to = date(2019, 9, 2)
|
||||
leave_form.request_unit_half = True
|
||||
leave_form.request_date_from_period = 'am'
|
||||
|
||||
|
||||
self.assertEqual(leave_form.number_of_days_display, 0)
|
||||
self.assertEqual(leave_form.number_of_hours_display, 0)
|
||||
self.assertEqual(leave_form.date_from, datetime(2019, 9, 2, 6, 0, 0))
|
||||
self.assertEqual(leave_form.date_to, datetime(2019, 9, 2, 10, 0, 0))
|
||||
|
||||
def test_attendance_previous_day(self):
|
||||
calendar = self.env['resource.calendar'].create({
|
||||
'name': 'auto next day',
|
||||
'attendance_ids': [(5, 0, 0),
|
||||
(0, 0, {
|
||||
'name': 'monday morning',
|
||||
'hour_from': 8,
|
||||
'hour_to': 12,
|
||||
'day_period': 'morning',
|
||||
'dayofweek': '0',
|
||||
})]
|
||||
})
|
||||
employee = self.employee_emp
|
||||
employee.resource_calendar_id = calendar
|
||||
|
||||
with Form(self.env['hr.leave']) as leave_form:
|
||||
leave_form.employee_id = employee
|
||||
leave_form.holiday_status_id = self.leave_type
|
||||
leave_form.request_date_from = date(2019, 9, 3)
|
||||
leave_form.request_date_to = date(2019, 9, 3)
|
||||
leave_form.request_unit_half = True
|
||||
leave_form.request_date_from_period = 'am'
|
||||
|
||||
|
||||
self.assertEqual(leave_form.number_of_days_display, 0)
|
||||
self.assertEqual(leave_form.number_of_hours_display, 0)
|
||||
self.assertEqual(leave_form.date_from, datetime(2019, 9, 3, 6, 0, 0))
|
||||
self.assertEqual(leave_form.date_to, datetime(2019, 9, 3, 10, 0, 0))
|
||||
@@ -275,6 +275,7 @@
|
||||
<field name="user_email"/>
|
||||
<field name="partner_name"/>
|
||||
<field name="type_id"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="job_id"/>
|
||||
<field name="department_id"/>
|
||||
<field name="message_needaction_counter"/>
|
||||
|
||||
@@ -103,7 +103,6 @@ class LivechatController(http.Controller):
|
||||
@http.route('/im_livechat/feedback', type='json', auth='public', cors="*")
|
||||
def feedback(self, uuid, rate, reason=None, **kwargs):
|
||||
Channel = request.env['mail.channel']
|
||||
Rating = request.env['rating.rating']
|
||||
channel = Channel.sudo().search([('uuid', '=', uuid)], limit=1)
|
||||
if channel:
|
||||
# limit the creation : only ONE rating per session
|
||||
@@ -124,7 +123,11 @@ class LivechatController(http.Controller):
|
||||
# if logged in user, set its partner on rating
|
||||
values['partner_id'] = request.env.user.partner_id.id if request.session.uid else False
|
||||
# create the rating
|
||||
rating = Rating.sudo().create(values)
|
||||
|
||||
channel.write({
|
||||
'rating_ids': [(0, False, values)]
|
||||
})
|
||||
rating = channel.rating_ids[0]
|
||||
else:
|
||||
rating = channel.rating_ids[0]
|
||||
rating.write(values)
|
||||
|
||||
@@ -123,6 +123,14 @@ var LivechatButton = Widget.extend({
|
||||
serverURL: this._serverURL,
|
||||
});
|
||||
var message = new WebsiteLivechatMessage(this, data, options);
|
||||
|
||||
var hasAlreadyMessage = _.some(this._messages, function (msg) {
|
||||
return message.id === msg.id;
|
||||
});
|
||||
if (hasAlreadyMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._livechat) {
|
||||
this._livechat.addMessage(message);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="uae_sale_tax_5_dubai" model="account.tax.template">
|
||||
<field name="name">TVA 5% (Dubai)</field>
|
||||
<field name="name">VAT 5% (Dubai)</field>
|
||||
<field name="type_tax_use">sale</field>
|
||||
<field name="amount">5</field>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="description">TVA 5%</field>
|
||||
<field name="description">VAT 5%</field>
|
||||
<field name="tax_group_id" ref="ae_tax_group_5"/>
|
||||
<field name="chart_template_id" ref="uae_chart_template_standard"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
@@ -36,11 +36,11 @@
|
||||
]"/>
|
||||
</record>
|
||||
<record id="uae_sale_tax_5_abu_dhabi" model="account.tax.template">
|
||||
<field name="name">TVA 5% (Abu Dhabi)</field>
|
||||
<field name="name">VAT 5% (Abu Dhabi)</field>
|
||||
<field name="type_tax_use">sale</field>
|
||||
<field name="amount">5</field>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="description">TVA 5%</field>
|
||||
<field name="description">VAT 5%</field>
|
||||
<field name="tax_group_id" ref="ae_tax_group_5"/>
|
||||
<field name="chart_template_id" ref="uae_chart_template_standard"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
@@ -71,11 +71,11 @@
|
||||
]"/>
|
||||
</record>
|
||||
<record id="uae_sale_tax_5_sharjah" model="account.tax.template">
|
||||
<field name="name">TVA 5% (Sharjah)</field>
|
||||
<field name="name">VAT 5% (Sharjah)</field>
|
||||
<field name="type_tax_use">sale</field>
|
||||
<field name="amount">5</field>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="description">TVA 5%</field>
|
||||
<field name="description">VAT 5%</field>
|
||||
<field name="tax_group_id" ref="ae_tax_group_5"/>
|
||||
<field name="chart_template_id" ref="uae_chart_template_standard"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
@@ -106,11 +106,11 @@
|
||||
]"/>
|
||||
</record>
|
||||
<record id="uae_sale_tax_5_ajman" model="account.tax.template">
|
||||
<field name="name">TVA 5% (Ajman)</field>
|
||||
<field name="name">VAT 5% (Ajman)</field>
|
||||
<field name="type_tax_use">sale</field>
|
||||
<field name="amount">5</field>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="description">TVA 5%</field>
|
||||
<field name="description">VAT 5%</field>
|
||||
<field name="tax_group_id" ref="ae_tax_group_5"/>
|
||||
<field name="chart_template_id" ref="uae_chart_template_standard"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
@@ -141,11 +141,11 @@
|
||||
]"/>
|
||||
</record>
|
||||
<record id="uae_sale_tax_5_umm_al_quwain" model="account.tax.template">
|
||||
<field name="name">TVA 5% (Umm Al Quwain)</field>
|
||||
<field name="name">VAT 5% (Umm Al Quwain)</field>
|
||||
<field name="type_tax_use">sale</field>
|
||||
<field name="amount">5</field>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="description">TVA 5%</field>
|
||||
<field name="description">VAT 5%</field>
|
||||
<field name="tax_group_id" ref="ae_tax_group_5"/>
|
||||
<field name="chart_template_id" ref="uae_chart_template_standard"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
@@ -176,11 +176,11 @@
|
||||
]"/>
|
||||
</record>
|
||||
<record id="uae_sale_tax_5_ras_al_khaima" model="account.tax.template">
|
||||
<field name="name">TVA 5% (Ras Al-Khaima)</field>
|
||||
<field name="name">VAT 5% (Ras Al-Khaima)</field>
|
||||
<field name="type_tax_use">sale</field>
|
||||
<field name="amount">5</field>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="description">TVA 5%</field>
|
||||
<field name="description">VAT 5%</field>
|
||||
<field name="tax_group_id" ref="ae_tax_group_5"/>
|
||||
<field name="chart_template_id" ref="uae_chart_template_standard"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
@@ -211,11 +211,11 @@
|
||||
]"/>
|
||||
</record>
|
||||
<record id="uae_sale_tax_5_fujairah" model="account.tax.template">
|
||||
<field name="name">TVA 5% (Fujairah)</field>
|
||||
<field name="name">VAT 5% (Fujairah)</field>
|
||||
<field name="type_tax_use">sale</field>
|
||||
<field name="amount">5</field>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="description">TVA 5%</field>
|
||||
<field name="description">VAT 5%</field>
|
||||
<field name="tax_group_id" ref="ae_tax_group_5"/>
|
||||
<field name="chart_template_id" ref="uae_chart_template_standard"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
@@ -277,11 +277,11 @@
|
||||
]"/>
|
||||
</record>
|
||||
<record id="uae_sale_tax_0" model="account.tax.template">
|
||||
<field name="name">TVA 0%</field>
|
||||
<field name="name">VAT 0%</field>
|
||||
<field name="type_tax_use">sale</field>
|
||||
<field name="amount">0</field>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="description">TVA 0%</field>
|
||||
<field name="description">VAT 0%</field>
|
||||
<field name="tax_group_id" ref="ae_tax_group_0"/>
|
||||
<field name="chart_template_id" ref="uae_chart_template_standard"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
@@ -693,11 +693,11 @@
|
||||
</record>
|
||||
<!-- purchase taxes -->
|
||||
<record id="uae_purchase_tax_5" model="account.tax.template">
|
||||
<field name="name">TVA 5%</field>
|
||||
<field name="name">VAT 5%</field>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="amount">5</field>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="description">TVA 5%</field>
|
||||
<field name="description">VAT 5%</field>
|
||||
<field name="tax_group_id" ref="ae_tax_group_5"/>
|
||||
<field name="chart_template_id" ref="uae_chart_template_standard"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
@@ -757,11 +757,11 @@
|
||||
]"/>
|
||||
</record>
|
||||
<record id="uae_purchase_tax_0" model="account.tax.template">
|
||||
<field name="name">TVA 0%</field>
|
||||
<field name="name">VAT 0%</field>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="amount">0</field>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="description">TVA 0%</field>
|
||||
<field name="description">VAT 0%</field>
|
||||
<field name="tax_group_id" ref="ae_tax_group_0"/>
|
||||
<field name="chart_template_id" ref="uae_chart_template_standard"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
@@ -817,28 +817,6 @@
|
||||
}),
|
||||
]"/>
|
||||
</record>
|
||||
<record id="uae_purchase_tax_5_child_global" model="account.tax.template">
|
||||
<field name="name">TVA 5%</field>
|
||||
<field name="type_tax_use">none</field>
|
||||
<field name="amount">5</field>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="description">TVA 5%</field>
|
||||
<field name="account_id" ref="uae_account_3726"/>
|
||||
<field name="refund_account_id" ref="uae_account_3726"/>
|
||||
<field name="tax_group_id" ref="ae_tax_group_5"/>
|
||||
<field name="chart_template_id" ref="uae_chart_template_standard"/>
|
||||
</record>
|
||||
<record id="uae_purchase_tax_5_child_reverse" model="account.tax.template">
|
||||
<field name="name">Reverse Charge TVA +5%(Purchase)</field>
|
||||
<field name="type_tax_use">none</field>
|
||||
<field name="amount">5</field>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="description">TVA 5%</field>
|
||||
<field name="account_id" ref="uae_account_3726"/>
|
||||
<field name="refund_account_id" ref="uae_account_3726"/>
|
||||
<field name="tax_group_id" ref="ae_tax_group_5"/>
|
||||
<field name="chart_template_id" ref="uae_chart_template_standard"/>
|
||||
</record>
|
||||
<record id="uae_purchase_tax_reverse_charge" model="account.tax.template">
|
||||
<field name="name">Reverse Charge Provision</field>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
|
||||
@@ -189,3 +189,17 @@ class AccountMove(models.Model):
|
||||
if self.env.context.get('l10n_ch_mark_isr_as_sent'):
|
||||
self.filtered(lambda inv: not inv.l10n_ch_isr_sent).write({'l10n_ch_isr_sent': True})
|
||||
return super(AccountMove, self.with_context(mail_post_autofollow=True)).message_post(**kwargs)
|
||||
|
||||
def _get_reference_ch_invoice(self):
|
||||
""" This sets ISR reference number which is generated based on customer's `Bank Account` and set it as
|
||||
`Payment Reference` of the invoice when invoice's journal is using Switzerland's communication standard
|
||||
"""
|
||||
self.ensure_one()
|
||||
return self.l10n_ch_isr_number_spaced
|
||||
|
||||
def _get_reference_ch_partner(self):
|
||||
""" This sets ISR reference number which is generated based on customer's `Bank Account` and set it as
|
||||
`Payment Reference` of the invoice when invoice's journal is using Switzerland's communication standard
|
||||
"""
|
||||
self.ensure_one()
|
||||
return self.l10n_ch_isr_number_spaced
|
||||
|
||||
@@ -14,6 +14,7 @@ class AccountJournal(models.Model):
|
||||
|
||||
# creation of bank journals by giving the account number, allow craetion of the
|
||||
l10n_ch_postal = fields.Char(related='bank_account_id.l10n_ch_postal', readonly=False)
|
||||
invoice_reference_model = fields.Selection(selection_add=[('ch', 'Switzerland')])
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
|
||||
@@ -445,7 +445,6 @@
|
||||
<field name="description">P_IVA21_SP_IN</field>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
|
||||
<field name="tag_ids" eval="[(6, False, [ref('mod_303_10_11'), ref('mod_303_14_15_purchase'), ref('mod_303_36_37'), ref('mod_303_40_41')])]"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="amount" eval="21"/>
|
||||
<field name="tax_group_id" ref="tax_group_iva_21"/>
|
||||
@@ -551,7 +550,6 @@
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="amount" eval="21"/>
|
||||
<field name="tag_ids" eval="[(6, False, [ref('mod_303_38_39'), ref('mod_303_40_41'), ref('mod_303_10_11'), ref('mod_303_14_15_purchase')])]"/>
|
||||
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
|
||||
<field name="tax_group_id" ref="tax_group_iva_21"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
@@ -718,7 +716,6 @@
|
||||
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="tag_ids" eval="[(6, False, [ref('mod_303_12_13'), ref('mod_303_14_15_purchase'), ref('mod_303_28_29'), ref('mod_303_40_41')])]"/>
|
||||
<field name="name">IVA 4% Adquisición de servicios extracomunitarios</field>
|
||||
<field name="tax_group_id" ref="tax_group_iva_4"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
@@ -770,7 +767,6 @@
|
||||
<field name="description">P_IVA10_SP_EX</field>
|
||||
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="tag_ids" eval="[(6, False, [ref('mod_303_12_13'), ref('mod_303_14_15_purchase'), ref('mod_303_28_29'), ref('mod_303_40_41')])]"/>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="name">IVA 10% Adquisición de servicios extracomunitarios</field>
|
||||
<field name="tax_group_id" ref="tax_group_iva_10"/>
|
||||
@@ -824,7 +820,6 @@
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="amount" eval="21"/>
|
||||
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
|
||||
<field name="tag_ids" eval="[(6, False, [ref('mod_303_12_13'), ref('mod_303_14_15_purchase'), ref('mod_303_28_29'), ref('mod_303_40_41')])]"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="tax_group_id" ref="tax_group_iva_21"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
@@ -874,7 +869,6 @@
|
||||
<field name="amount" eval="4"/>
|
||||
<field name="description">P_IVA4_IC_BC</field>
|
||||
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
|
||||
<field name="tag_ids" eval="[(6, False, [ref('mod_303_36_37'), ref('mod_303_40_41'), ref('mod_303_10_11'), ref('mod_303_14_15_purchase')])]"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="name">IVA 4% Adquisición Intracomunitario. Bienes corrientes</field>
|
||||
@@ -930,7 +924,6 @@
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="name">IVA 4% Adquisición Intracomunitario. Bienes de inversión</field>
|
||||
<field name="tag_ids" eval="[(6, False, [ref('mod_303_38_39'), ref('mod_303_40_41'), ref('mod_303_10_11'), ref('mod_303_14_15_purchase')])]"/>
|
||||
<field name="tax_group_id" ref="tax_group_iva_4"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
@@ -979,7 +972,6 @@
|
||||
<field name="amount" eval="10"/>
|
||||
<field name="description">P_IVA10_IC_BC</field>
|
||||
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
|
||||
<field name="tag_ids" eval="[(6, False, [ref('mod_303_36_37'), ref('mod_303_40_41'), ref('mod_303_10_11'), ref('mod_303_14_15_purchase')])]"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="name">IVA 10% Adquisición Intracomunitario. Bienes corrientes</field>
|
||||
@@ -1034,7 +1026,6 @@
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="name">IVA 10% Adquisición Intracomunitario. Bienes de inversión</field>
|
||||
<field name="tag_ids" eval="[(6, False, [ref('mod_303_38_39'), ref('mod_303_40_41'), ref('mod_303_10_11'), ref('mod_303_14_15_purchase')])]"/>
|
||||
<field name="tax_group_id" ref="tax_group_iva_10"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
@@ -3077,7 +3068,6 @@
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="name">IVA 10% Adquisición de servicios intracomunitarios</field>
|
||||
<field name="tag_ids" eval="[(6, False, [ref('mod_303_10_11'), ref('mod_303_14_15_purchase'), ref('mod_303_36_37'), ref('mod_303_40_41')])]"/>
|
||||
<field name="tax_group_id" ref="tax_group_iva_10"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
@@ -3129,7 +3119,6 @@
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="name">IVA 4% Adquisición de servicios intracomunitarios</field>
|
||||
<field name="tag_ids" eval="[(6, False, [ref('mod_303_10_11'), ref('mod_303_14_15_purchase'), ref('mod_303_36_37'), ref('mod_303_40_41')])]"/>
|
||||
<field name="tax_group_id" ref="tax_group_iva_4"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
@@ -3403,7 +3392,6 @@
|
||||
<field name="description">P_IVA4_ISP</field>
|
||||
<field name="name">IVA 4% Compra con Inversión del Sujeto Pasivo Nacional</field>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="tag_ids" eval="[(6, False, [ref('mod_303_28_29'), ref('mod_303_40_41'), ref('mod_303_12_13'), ref('mod_303_14_15_purchase')])]"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="amount" eval="4"/>
|
||||
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
|
||||
@@ -3455,7 +3443,6 @@
|
||||
<field name="description">P_IVA10_ISP</field>
|
||||
<field name="name">IVA 10% Compra con Inversión del Sujeto Pasivo Nacional</field>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="tag_ids" eval="[(6, False, [ref('mod_303_28_29'), ref('mod_303_40_41'), ref('mod_303_12_13'), ref('mod_303_14_15_purchase')])]"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="amount" eval="10"/>
|
||||
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
|
||||
@@ -3509,7 +3496,6 @@
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="amount" eval="21"/>
|
||||
<field name="tag_ids" eval="[(6, False, [ref('mod_303_28_29'), ref('mod_303_40_41'), ref('mod_303_12_13'), ref('mod_303_14_15_purchase')])]"/>
|
||||
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
|
||||
<field name="tax_group_id" ref="tax_group_iva_21"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
<xpath expr="//table[@name='invoice_line_table']" position="before">
|
||||
<t t-if="o.company_id.country_id.code == 'IN'">
|
||||
<!-- Tag wise Tax Calculation -->
|
||||
<t t-set="tag_cgst_id" t-value="env.ref('l10n_in.cgst_tag_tax').id"/>
|
||||
<t t-set="tag_sgst_id" t-value="env.ref('l10n_in.sgst_tag_tax').id"/>
|
||||
<t t-set="tag_igst_id" t-value="env.ref('l10n_in.igst_tag_tax').id"/>
|
||||
<t t-set="tag_cess_id" t-value="env.ref('l10n_in.cess_tag_tax').id"/>
|
||||
<t t-set="tax_map" t-value="o.invoice_line_ids.mapped('tax_ids.tag_ids.id')"/>
|
||||
<t t-set="tag_cgst_id" t-value="env.ref('l10n_in.tax_report_line_cgst').id"/>
|
||||
<t t-set="tag_sgst_id" t-value="env.ref('l10n_in.tax_report_line_sgst').id"/>
|
||||
<t t-set="tag_igst_id" t-value="env.ref('l10n_in.tax_report_line_igst').id"/>
|
||||
<t t-set="tag_cess_id" t-value="env.ref('l10n_in.tax_report_line_cess').id"/>
|
||||
<t t-set="tax_map" t-value="o.tax_line_ids.mapped('tag_ids.tax_report_line_ids').ids"/>
|
||||
<t t-set="is_igst" t-value="tag_igst_id in tax_map"/>
|
||||
<t t-set="is_cgst" t-value="tag_cgst_id in tax_map"/>
|
||||
<t t-set="is_cess" t-value="tag_cess_id in tax_map"/>
|
||||
@@ -43,21 +43,21 @@
|
||||
<t t-if="o.company_id.country_id.code == 'IN'">
|
||||
<td class="text-right" t-if="is_cgst">
|
||||
<t t-foreach="tax_datas[line.id]" t-as="tax_data">
|
||||
<t t-if="tag_cgst_id in tax_data['tag_ids']">
|
||||
<t t-if="tag_cgst_id in tax_data['report_line_ids']">
|
||||
<t t-esc="tax_data['amount']" t-options="{'widget': 'monetary', 'display_currency': o.currency_id}"/>
|
||||
</t>
|
||||
</t>
|
||||
</td>
|
||||
<td class="text-right" t-if="is_cgst">
|
||||
<t t-foreach="tax_datas[line.id]" t-as="tax_data">
|
||||
<t t-if="tag_sgst_id in tax_data['tag_ids']">
|
||||
<t t-if="tag_sgst_id in tax_data['report_line_ids']">
|
||||
<t t-esc="tax_data['amount']" t-options="{'widget': 'monetary', 'display_currency': o.currency_id}"/>
|
||||
</t>
|
||||
</t>
|
||||
</td>
|
||||
<td class="text-right" t-if="is_igst">
|
||||
<t t-foreach="tax_datas[line.id]" t-as="tax_data">
|
||||
<t t-if="tag_igst_id in tax_data['tag_ids']">
|
||||
<t t-if="tag_igst_id in tax_data['report_line_ids']">
|
||||
<t t-esc="tax_data['amount']" t-options="{'widget': 'monetary', 'display_currency': o.currency_id}"/>
|
||||
</t>
|
||||
</t>
|
||||
@@ -65,7 +65,7 @@
|
||||
<td class="text-right" t-if="is_cess">
|
||||
<t t-set="total_cess" t-value="0.0"/>
|
||||
<t t-foreach="tax_datas[line.id]" t-as="tax_data">
|
||||
<t t-if="tag_cess_id in tax_data['tag_ids']">
|
||||
<t t-if="tag_cess_id in tax_data['report_line_ids']">
|
||||
<t t-set="total_cess" t-value="total_cess + tax_data['amount']" t-options="{'widget': 'monetary', 'display_currency': o.currency_id}"/>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
@@ -31,10 +31,10 @@ class FetchmailServer(models.Model):
|
||||
l10n_it_is_pec = fields.Boolean('PEC server', help="If PEC Server, only mail from '...@pec.fatturapa.it' will be processed.")
|
||||
l10n_it_last_uid = fields.Integer(string='Last message UID', default=1)
|
||||
|
||||
@api.constrains('l10n_it_is_pec', 'type')
|
||||
@api.constrains('l10n_it_is_pec', 'server_type')
|
||||
def _check_pec(self):
|
||||
for record in self:
|
||||
if record.l10n_it_is_pec and record.type != 'imap':
|
||||
if record.l10n_it_is_pec and record.server_type != 'imap':
|
||||
raise ValidationError(_("PEC mail server must be of type IMAP."))
|
||||
|
||||
def fetch_mail(self):
|
||||
@@ -42,7 +42,7 @@ class FetchmailServer(models.Model):
|
||||
|
||||
MailThread = self.env['mail.thread']
|
||||
for server in self.filtered(lambda s: s.l10n_it_is_pec):
|
||||
_logger.info('start checking for new emails on %s PEC server %s', server.type, server.name)
|
||||
_logger.info('start checking for new emails on %s PEC server %s', server.server_type, server.name)
|
||||
|
||||
count, failed = 0, 0
|
||||
imap_server = None
|
||||
@@ -80,14 +80,14 @@ class FetchmailServer(models.Model):
|
||||
self._attachment_invoice(msg_txt)
|
||||
new_max_uid = max(new_max_uid, int(uid))
|
||||
except Exception:
|
||||
_logger.info('Failed to process mail from %s server %s.', server.type, server.name, exc_info=True)
|
||||
_logger.info('Failed to process mail from %s server %s.', server.server_type, server.name, exc_info=True)
|
||||
failed += 1
|
||||
self._cr.commit()
|
||||
count += 1
|
||||
server.write({'l10n_it_last_uid': new_max_uid})
|
||||
_logger.info("Fetched %d email(s) on %s server %s; %d succeeded, %d failed.", count, server.type, server.name, (count - failed), failed)
|
||||
_logger.info("Fetched %d email(s) on %s server %s; %d succeeded, %d failed.", count, server.server_type, server.name, (count - failed), failed)
|
||||
except Exception:
|
||||
_logger.info("General failure when trying to fetch mail from %s server %s.", server.type, server.name, exc_info=True)
|
||||
_logger.info("General failure when trying to fetch mail from %s server %s.", server.server_type, server.name, exc_info=True)
|
||||
finally:
|
||||
if imap_server:
|
||||
imap_server.close()
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, SUPERUSER_ID
|
||||
|
||||
from . import models
|
||||
|
||||
def _post_init_hook(cr, registry):
|
||||
_preserve_tag_on_taxes(cr, registry)
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
env.ref('l10n_lu.lu_2011_chart_1').process_coa_translations()
|
||||
|
||||
def _preserve_tag_on_taxes(cr, registry):
|
||||
from odoo.addons.account.models.chart_template import preserve_existing_tags_on_taxes
|
||||
preserve_existing_tags_on_taxes(cr, registry, 'l10n_lu')
|
||||
|
||||
@@ -43,5 +43,5 @@ Notes:
|
||||
'data/account.chart.template.csv',
|
||||
'data/account_chart_template_data.xml',
|
||||
],
|
||||
'post_init_hook': '_preserve_tag_on_taxes',
|
||||
'post_init_hook': '_post_init_hook',
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
<odoo>
|
||||
<!-- account chart -->
|
||||
<record id="l10n_kzc_temp_chart" model="account.chart.template">
|
||||
<field name="property_account_receivable_id" ref="pcg_3488"/>
|
||||
<field name="property_account_payable_id" ref="pcg_4488"/>
|
||||
<field name="property_account_receivable_id" ref="pcg_34211"/>
|
||||
<field name="property_account_payable_id" ref="pcg_4411"/>
|
||||
<field name="property_account_income_categ_id" ref="pcg_7111"/>
|
||||
<field name="property_account_expense_categ_id" ref="pcg_1486"/>
|
||||
<field name="property_account_expense_categ_id" ref="pcg_6111"/>
|
||||
<field name="income_currency_exchange_account_id" ref="pcg_733"/>
|
||||
<field name="expense_currency_exchange_account_id" ref="pcg_633"/>
|
||||
<field name="default_pos_receivable_account_id" ref="pcg_3489" />
|
||||
|
||||
@@ -1190,13 +1190,15 @@
|
||||
<record id="pcg_34211" model="account.account.template">
|
||||
<field name="name">Clients - catégorie A</field>
|
||||
<field name="code">34211</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_assets"/>
|
||||
<field name="user_type_id" ref="account.data_account_type_receivable"/>
|
||||
<field name="reconcile" eval='True'/>
|
||||
<field name="chart_template_id" ref="l10n_kzc_temp_chart"/>
|
||||
</record>
|
||||
<record id="pcg_34212" model="account.account.template">
|
||||
<field name="name">Clients - catégorie B</field>
|
||||
<field name="code">34212</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_assets"/>
|
||||
<field name="user_type_id" ref="account.data_account_type_receivable"/>
|
||||
<field name="reconcile" eval='True'/>
|
||||
<field name="chart_template_id" ref="l10n_kzc_temp_chart"/>
|
||||
</record>
|
||||
<record id="pcg_3423" model="account.account.template">
|
||||
@@ -1370,7 +1372,7 @@
|
||||
<record id="pcg_3488" model="account.account.template">
|
||||
<field name="name">Divers débiteurs</field>
|
||||
<field name="code">3488</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_receivable"/>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_assets"/>
|
||||
<field name="reconcile" eval='True'/>
|
||||
<field name="chart_template_id" ref="l10n_kzc_temp_chart"/>
|
||||
</record>
|
||||
@@ -1522,7 +1524,8 @@
|
||||
<record id="pcg_4411" model="account.account.template">
|
||||
<field name="name">Fournisseurs</field>
|
||||
<field name="code">4411</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||
<field name="user_type_id" ref="account.data_account_type_payable"/>
|
||||
<field name="reconcile" eval='True'/>
|
||||
<field name="chart_template_id" ref="l10n_kzc_temp_chart"/>
|
||||
</record>
|
||||
<record id="pcg_4413" model="account.account.template">
|
||||
@@ -1768,7 +1771,7 @@
|
||||
<record id="pcg_4488" model="account.account.template">
|
||||
<field name="name">Divers créanciers</field>
|
||||
<field name="code">4488</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_payable"/>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||
<field name="chart_template_id" ref="l10n_kzc_temp_chart"/>
|
||||
<field name="reconcile" eval='True'/>
|
||||
</record>
|
||||
@@ -2072,6 +2075,12 @@
|
||||
<field name="user_type_id" ref="account.data_account_type_expenses"/>
|
||||
<field name="chart_template_id" ref="l10n_kzc_temp_chart"/>
|
||||
</record>
|
||||
<record id="pcg_61263" model="account.account.template">
|
||||
<field name="name">Achats des prestations de service</field>
|
||||
<field name="code">61263</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_expenses"/>
|
||||
<field name="chart_template_id" ref="l10n_kzc_temp_chart"/>
|
||||
</record>
|
||||
<record id="pcg_6128" model="account.account.template">
|
||||
<field name="name">Achats de matières et de fournitures des exercices antérieurs</field>
|
||||
<field name="code">6128</field>
|
||||
|
||||
@@ -12,6 +12,19 @@ _logger = logging.getLogger(__name__)
|
||||
class AccountChartTemplate(models.Model):
|
||||
_inherit = 'account.chart.template'
|
||||
|
||||
def load_for_current_company(self, sale_tax_rate, purchase_tax_rate):
|
||||
res = super(AccountChartTemplate, self).load_for_current_company(sale_tax_rate, purchase_tax_rate)
|
||||
# Copy chart of account translations when loading chart of account
|
||||
for chart_template in self.filtered('spoken_languages'):
|
||||
external_id = self.env['ir.model.data'].search([
|
||||
('model', '=', 'account.chart.template'),
|
||||
('res_id', '=', chart_template.id),
|
||||
], order='id', limit=1)
|
||||
module = external_id and self.env.ref('base.module_' + external_id.module)
|
||||
if module and module.state == 'installed':
|
||||
chart_template.process_coa_translations()
|
||||
return res
|
||||
|
||||
def process_translations(self, langs, in_field, in_ids, out_ids):
|
||||
"""
|
||||
This method copies translations values of templates into new Accounts/Taxes/Journals for languages selected
|
||||
|
||||
@@ -15,7 +15,7 @@ This is the latest UK Odoo localisation necessary to run Odoo accounting for UK
|
||||
- InfoLogic UK counties listing
|
||||
- a few other adaptations""",
|
||||
'author': 'SmartMode LTD',
|
||||
'website': 'http://www.smartmode.co.uk',
|
||||
'website': 'https://www.odoo.com/page/accounting',
|
||||
'depends': [
|
||||
'account',
|
||||
'base_iban',
|
||||
|
||||
@@ -13,6 +13,7 @@ var LunchKanbanModel = KanbanModel.extend({
|
||||
init: function () {
|
||||
this.locationId = false;
|
||||
this.userId = false;
|
||||
this._promInitLocation = null;
|
||||
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
@@ -27,15 +28,23 @@ var LunchKanbanModel = KanbanModel.extend({
|
||||
getCurrentLocationId: function () {
|
||||
return this.locationId;
|
||||
},
|
||||
/**
|
||||
* @return {Promise} resolved with the location domain
|
||||
*/
|
||||
getLocationDomain: function () {
|
||||
var self = this;
|
||||
return this._initUserLocation().then(function () {
|
||||
return self._buildLocationDomainLeaf() ? [self._buildLocationDomainLeaf()]: [];
|
||||
});
|
||||
},
|
||||
load: function () {
|
||||
var self = this;
|
||||
var args = arguments;
|
||||
var _super = this._super;
|
||||
|
||||
return this._getUserLocation().then(function (locationId) {
|
||||
return this._initUserLocation().then(function () {
|
||||
var params = args[0];
|
||||
self.locationId = locationId;
|
||||
self._addOrUpdate(params.domain, ['is_available_at', 'in', [locationId]]);
|
||||
self._addOrUpdate(params.domain, self._buildLocationDomainLeaf());
|
||||
|
||||
return _super.apply(self, args);
|
||||
});
|
||||
@@ -43,7 +52,7 @@ var LunchKanbanModel = KanbanModel.extend({
|
||||
reload: function (id, options) {
|
||||
var domain = options && options.domain || this.localData[id].domain;
|
||||
|
||||
this._addOrUpdate(domain, ['is_available_at', 'in', [this.locationId]]);
|
||||
this._addOrUpdate(domain, this._buildLocationDomainLeaf());
|
||||
options = _.extend(options, {domain: domain});
|
||||
|
||||
return this._super.apply(this, arguments);
|
||||
@@ -54,19 +63,35 @@ var LunchKanbanModel = KanbanModel.extend({
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
_addOrUpdate: function (domain, subDomain) {
|
||||
var key = subDomain[0];
|
||||
var index = _.findIndex(domain, function (val) {
|
||||
return val[0] === key;
|
||||
});
|
||||
if (subDomain && subDomain.length) {
|
||||
var key = subDomain[0];
|
||||
var index = _.findIndex(domain, function (val) {
|
||||
return val[0] === key;
|
||||
});
|
||||
|
||||
if (index < 0) {
|
||||
domain.push(subDomain);
|
||||
} else {
|
||||
domain[index] = subDomain;
|
||||
if (index < 0) {
|
||||
domain.push(subDomain);
|
||||
} else {
|
||||
domain[index] = subDomain;
|
||||
}
|
||||
|
||||
return domain;
|
||||
}
|
||||
|
||||
return domain;
|
||||
},
|
||||
/**
|
||||
* Builds the domain leaf corresponding to the current user's location
|
||||
*
|
||||
* @private
|
||||
* @return {Array}
|
||||
*/
|
||||
_buildLocationDomainLeaf: function () {
|
||||
var locationId = this.getCurrentLocationId();
|
||||
if (locationId) {
|
||||
return ['is_available_at', 'in', [locationId]];
|
||||
}
|
||||
},
|
||||
_getUserLocation: function () {
|
||||
return this._rpc({
|
||||
route: '/lunch/user_location_get',
|
||||
@@ -76,18 +101,34 @@ var LunchKanbanModel = KanbanModel.extend({
|
||||
},
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Gets the user location once.
|
||||
* Can be triggered from anywhere
|
||||
* Useful to inject the location domain in the search panel
|
||||
*
|
||||
* @private
|
||||
* @return {Promise}
|
||||
*/
|
||||
_initUserLocation: function () {
|
||||
var self = this;
|
||||
if (!this._promInitLocation) {
|
||||
this._promInitLocation = new Promise(function (resolve) {
|
||||
self._getUserLocation().then(function (locationId) {
|
||||
self.locationId = locationId;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
return this._promInitLocation;
|
||||
},
|
||||
_updateLocation: function (locationId) {
|
||||
this.locationId = locationId;
|
||||
return Promise.resolve();
|
||||
},
|
||||
_updateUser: function (userId) {
|
||||
var self = this;
|
||||
|
||||
this.userId = userId;
|
||||
|
||||
return this._getUserLocation().then(function (locationId) {
|
||||
self.locationId = locationId;
|
||||
});
|
||||
this._promInitLocation = null;
|
||||
return this._initUserLocation();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -18,6 +18,26 @@ var LunchKanbanView = KanbanView.extend({
|
||||
Renderer: LunchKanbanRenderer,
|
||||
}),
|
||||
display_name: _lt('Lunch Kanban'),
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Injects an additional domain in the search panel
|
||||
*
|
||||
* @private
|
||||
* @override
|
||||
*/
|
||||
_createSearchPanel: function (parent, params) {
|
||||
var self = this;
|
||||
var model = this.getModel(parent);
|
||||
var _super = this._super.bind(this);
|
||||
return model.getLocationDomain().then(function (domain) {
|
||||
self.loadParams.domain = self.loadParams.domain.concat(domain);
|
||||
return _super(parent, params);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
view_registry.add('lunch_kanban', LunchKanbanView);
|
||||
|
||||
@@ -211,6 +211,56 @@ QUnit.module('LunchKanbanView', {
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('search panel domain location', async function (assert) {
|
||||
assert.expect(10);
|
||||
const locationId = this.data['lunch.location'].records[0].id;
|
||||
const regularInfos = _.extend({}, this.regularInfos);
|
||||
|
||||
const kanban = await createLunchKanbanView({
|
||||
View: LunchKanbanView,
|
||||
model: 'product',
|
||||
data: this.data,
|
||||
arch: `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div><field name="name"/></div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
`,
|
||||
mockRPC: function (route, args) {
|
||||
assert.step(route);
|
||||
|
||||
if (route.startsWith('/lunch')) {
|
||||
return mockLunchRPC({
|
||||
infos: regularInfos,
|
||||
userLocation: locationId,
|
||||
}).apply(this, arguments);
|
||||
}
|
||||
if (args.method === 'search_panel_select_multi_range') {
|
||||
assert.deepEqual(args.kwargs.search_domain, [["is_available_at", "in", [locationId]]],
|
||||
'The initial domain of the search panel must contain the user location');
|
||||
}
|
||||
if (route === '/web/dataset/search_read') {
|
||||
assert.deepEqual(args.domain, [["is_available_at", "in", [locationId]]],
|
||||
'The domain for fetching actual data should be correct');
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
assert.verifySteps([
|
||||
'/lunch/user_location_get',
|
||||
'/web/dataset/call_kw/product/search_panel_select_multi_range',
|
||||
'/web/dataset/call_kw/product/search_panel_select_multi_range',
|
||||
'/web/dataset/search_read',
|
||||
'/lunch/infos',
|
||||
'/web/dataset/call_kw/ir.model.data/xmlid_to_res_id',
|
||||
])
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('non-empty cart', async function (assert) {
|
||||
assert.expect(17);
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ class MailController(http.Controller):
|
||||
@http.route('/mail/read_followers', type='json', auth='user')
|
||||
def read_followers(self, follower_ids, res_model):
|
||||
followers = []
|
||||
is_editable = request.env.user.has_group('base.group_no_one')
|
||||
is_editable = request.env['mail.followers'].user_has_groups('base.group_no_one')
|
||||
partner_id = request.env.user.partner_id
|
||||
follower_id = None
|
||||
follower_recs = request.env['mail.followers'].sudo().browse(follower_ids)
|
||||
|
||||
@@ -62,9 +62,16 @@ var DocumentThread = Thread.extend({
|
||||
* shared between tabs, and restored on F5.
|
||||
*
|
||||
* @override
|
||||
* @param {Object} [options]
|
||||
* @param {boolean} [options.skipCrossTabSync=false] if set, it should
|
||||
* not notify other tabs from updating document thread state. This happens
|
||||
* in case the `close` operation comes from local storage event.
|
||||
*/
|
||||
close: function () {
|
||||
close: function (options) {
|
||||
this._super.apply(this, arguments);
|
||||
if (options && options.skipCrossTabSync) {
|
||||
return;
|
||||
}
|
||||
this.call('mail_service', 'updateDocumentThreadState', this._id, {
|
||||
name: this.getName(),
|
||||
windowState: 'closed',
|
||||
@@ -75,9 +82,16 @@ var DocumentThread = Thread.extend({
|
||||
* shared between tabs, and restored on F5.
|
||||
*
|
||||
* @override
|
||||
* @param {Object} [options]
|
||||
* @param {boolean} [options.skipCrossTabSync=false] if set, it should
|
||||
* not notify other tabs from updating document thread state. This happens
|
||||
* in case the `detach` operation comes from local storage event.
|
||||
*/
|
||||
detach: function () {
|
||||
detach: function (options) {
|
||||
this._super.apply(this, arguments);
|
||||
if (options && options.skipCrossTabSync) {
|
||||
return;
|
||||
}
|
||||
var windowState = this._folded ? 'folded' : 'open';
|
||||
this.call('mail_service', 'updateDocumentThreadState', this._id, {
|
||||
name: this.getName(),
|
||||
@@ -105,9 +119,16 @@ var DocumentThread = Thread.extend({
|
||||
* shared between tabs, and restored on F5.
|
||||
*
|
||||
* @override
|
||||
* @param {Object} [options]
|
||||
* @param {boolean} [options.skipCrossTabSync=false] if set, it should
|
||||
* not notify other tabs from updating document thread state. This happens
|
||||
* in case the `detach` operation comes from local storage event.
|
||||
*/
|
||||
fold: function () {
|
||||
fold: function (folded, options) {
|
||||
this._super.apply(this, arguments);
|
||||
if (options && options.skipCrossTabSync) {
|
||||
return;
|
||||
}
|
||||
var windowState = this._folded ? 'folded' : 'open';
|
||||
this.call('mail_service', 'updateDocumentThreadState', this._id, {
|
||||
name: this.getName(),
|
||||
|
||||
@@ -149,7 +149,11 @@ var SearchableThread = Thread.extend({
|
||||
if (cache.messages[index] !== message) {
|
||||
cache.messages.splice(index, 0, message);
|
||||
}
|
||||
if (!message.isMyselfAuthor() && options.incrementUnread) {
|
||||
if (
|
||||
!message.isMyselfAuthor() &&
|
||||
options.incrementUnread &&
|
||||
message.getType() !== 'notification'
|
||||
) {
|
||||
this._incrementUnreadCounter();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -21,17 +21,23 @@ MailManager.include({
|
||||
|
||||
start: function () {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
this._documentThreadWindowStates = {};
|
||||
this._tabId = this.call('bus_service', 'getTabId');
|
||||
// retrieve the open DocumentThreads from the localStorage
|
||||
var state = this.call('local_storage', 'getItem', this.DOCUMENT_THREAD_STATE_KEY);
|
||||
if (!state) {
|
||||
this.call('local_storage', 'setItem', this.DOCUMENT_THREAD_STATE_KEY, {});
|
||||
} else {
|
||||
this.isReady().then(this._updateDocumentThreadWindows.bind(this, state));
|
||||
var localStorageLength = this.call('local_storage', 'length');
|
||||
for (var i = 0; i < localStorageLength; i++) {
|
||||
var key = this.call('local_storage', 'key', i);
|
||||
if (key.indexOf(this.DOCUMENT_THREAD_STATE_KEY) === 0) {
|
||||
// key format: DOCUMENT_THREAD_STATE_KEY + '/' + threadId
|
||||
var documentThreadId = key.substring(this.DOCUMENT_THREAD_STATE_KEY.length+1);
|
||||
this._documentThreadWindowStates[documentThreadId] =
|
||||
this.call('local_storage', 'getItem', key);
|
||||
}
|
||||
}
|
||||
this.isReady().then(this._updateDocumentThreadWindows.bind(this, this._documentThreadWindowStates));
|
||||
// listen to localStorage changes to synchronize DocumentThread's
|
||||
// windows between tabs
|
||||
window.addEventListener('storage', this._onStorage.bind(this));
|
||||
this.call('local_storage', 'onStorage', this, this._onStorage);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -60,7 +66,10 @@ MailManager.include({
|
||||
options.postedFromDocumentThread
|
||||
) {
|
||||
var key = this.DOCUMENT_THREAD_MESSAGE_KEY;
|
||||
this.call('local_storage', 'setItem', key, data);
|
||||
this.call('local_storage', 'setItem', key, {
|
||||
messageData: data,
|
||||
tabId: this._tabId,
|
||||
});
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
@@ -144,12 +153,11 @@ MailManager.include({
|
||||
* @param {string} state.windowState ('closed', 'folded' or 'open')
|
||||
*/
|
||||
updateDocumentThreadState: function (threadID, state) {
|
||||
var states = this.call('local_storage', 'getItem', this.DOCUMENT_THREAD_STATE_KEY);
|
||||
states = _.omit(states, function (state) {
|
||||
return state.windowState === 'closed';
|
||||
this._documentThreadWindowStates[threadID] = state;
|
||||
this.call('local_storage', 'setItem', this.DOCUMENT_THREAD_STATE_KEY + '/' + threadID, {
|
||||
state: state,
|
||||
tabId: this._tabId,
|
||||
});
|
||||
states[threadID] = state;
|
||||
this.call('local_storage', 'setItem', this.DOCUMENT_THREAD_STATE_KEY, states);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -194,10 +202,17 @@ MailManager.include({
|
||||
resModel: info[0],
|
||||
});
|
||||
if (state.windowState === 'closed') {
|
||||
documentThread.close();
|
||||
documentThread.close({
|
||||
skipCrossTabSync: true,
|
||||
});
|
||||
} else {
|
||||
documentThread.fold(state.windowState === 'folded');
|
||||
self.openThreadWindow(documentThread.getID(), { keepFoldState: true });
|
||||
documentThread.fold(state.windowState === 'folded', {
|
||||
skipCrossTabSync: true,
|
||||
});
|
||||
self.openThreadWindow(documentThread.getID(), {
|
||||
keepFoldState: true,
|
||||
skipCrossTabSync: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -213,13 +228,27 @@ MailManager.include({
|
||||
* @param {StorageEvent} ev
|
||||
*/
|
||||
_onStorage: function (ev) {
|
||||
if (ev.key === this.DOCUMENT_THREAD_STATE_KEY) {
|
||||
var state = this.call('local_storage', 'getItem', this.DOCUMENT_THREAD_STATE_KEY);
|
||||
this._updateDocumentThreadWindows(state);
|
||||
var value;
|
||||
try {
|
||||
value = JSON.parse(ev.newValue);
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
if (ev.key.indexOf(this.DOCUMENT_THREAD_STATE_KEY) === 0) {
|
||||
if (value.tabId === this._tabId) {
|
||||
return;
|
||||
}
|
||||
// key format: DOCUMENT_THREAD_STATE_KEY + '/' + threadId
|
||||
var documentThreadId = ev.key.substring(this.DOCUMENT_THREAD_STATE_KEY.length+1);
|
||||
var param = {};
|
||||
param[documentThreadId] = value.state;
|
||||
this._updateDocumentThreadWindows(param);
|
||||
} else if (ev.key === this.DOCUMENT_THREAD_MESSAGE_KEY) {
|
||||
var message = this.call('local_storage', 'getItem', this.DOCUMENT_THREAD_MESSAGE_KEY);
|
||||
if (message) {
|
||||
this.addMessage(message);
|
||||
if (value.tabId === this._tabId) {
|
||||
return;
|
||||
}
|
||||
if (value.messageData) {
|
||||
this.addMessage(value.messageData);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -94,6 +94,8 @@ MailManager.include({
|
||||
* is not open yet, and do nothing otherwise.
|
||||
* @param {boolean} [options.keepFoldState=false] if set to true, keep the
|
||||
* fold state of the thread
|
||||
* @param {boolean} [options.skipCrossTabSync=false] if set, thread
|
||||
* should not notify other tabs from new document thread chat window state.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
openThreadWindow: function (threadID, options) {
|
||||
@@ -132,7 +134,9 @@ MailManager.include({
|
||||
// thread window could not be open, which may happen due to
|
||||
// access error while fetching messages to the document.
|
||||
// abort opening the thread window in this case.
|
||||
thread.close();
|
||||
thread.close({
|
||||
skipCrossTabSync: true,
|
||||
});
|
||||
thread.isCreatingWindow = false;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ odoo.define('mail.ThreadWindow', function (require) {
|
||||
|
||||
var AbstractThreadWindow = require('mail.AbstractThreadWindow');
|
||||
var BasicComposer = require('mail.composer.Basic');
|
||||
var ExtendedComposer = require('mail.composer.Extended');
|
||||
|
||||
var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
@@ -65,19 +67,27 @@ var ThreadWindow = AbstractThreadWindow.extend({
|
||||
if (!this.hasThread()) {
|
||||
this._startWithoutThread();
|
||||
} else if (this.needsComposer()) {
|
||||
var basicComposer = new BasicComposer(this, {
|
||||
var composer;
|
||||
var composerParams = {
|
||||
mentionPartnersRestricted: this._thread.getType() !== 'document_thread',
|
||||
isMini: true,
|
||||
thread: this._thread,
|
||||
});
|
||||
basicComposer.on('post_message', this, this._postMessage);
|
||||
basicComposer.once('input_focused', this, function () {
|
||||
};
|
||||
if (
|
||||
config.device.isMobile &&
|
||||
this._thread.isMassMailing()
|
||||
) {
|
||||
composer = new ExtendedComposer(this, composerParams);
|
||||
} else {
|
||||
composer = new BasicComposer(this, _.extend({ isMini: true }, composerParams));
|
||||
}
|
||||
composer.on('post_message', this, this._postMessage);
|
||||
composer.once('input_focused', this, function () {
|
||||
var commands = this._thread.getCommands();
|
||||
var partners = this._thread.getMentionPartnerSuggestions();
|
||||
basicComposer.mentionSetCommands(commands);
|
||||
basicComposer.mentionSetPrefetchedPartners(partners);
|
||||
composer.mentionSetCommands(commands);
|
||||
composer.mentionSetPrefetchedPartners(partners);
|
||||
});
|
||||
composerDef = basicComposer.replace(this.$('.o_thread_composer'));
|
||||
composerDef = composer.replace(this.$('.o_thread_composer'));
|
||||
composerDef.then(function () {
|
||||
self.$input = self.$('.o_composer_text_field');
|
||||
});
|
||||
@@ -129,24 +139,6 @@ var ThreadWindow = AbstractThreadWindow.extend({
|
||||
isPassive: function () {
|
||||
return this._passive;
|
||||
},
|
||||
/**
|
||||
* States whether the input of the thread window should be displayed or not.
|
||||
* This is based on the type of the thread:
|
||||
*
|
||||
* Do not display the input in the following cases:
|
||||
*
|
||||
* - no thread related to this window
|
||||
* - window of a mailbox (temp: let us have mailboxes in window mode)
|
||||
* - window of a thread with mass mailing
|
||||
*
|
||||
* Any other threads show the input in the window.
|
||||
*
|
||||
* @override
|
||||
* @returns {boolean}
|
||||
*/
|
||||
needsComposer: function () {
|
||||
return this._super() && !this._thread.isMassMailing();
|
||||
},
|
||||
/**
|
||||
* Turn the thread window in active mode, so that when the bottom of the
|
||||
* thread is visible, it is automatically marked as read.
|
||||
|
||||
@@ -8,7 +8,12 @@ var _t = core._t;
|
||||
function parseAndTransform(htmlString, transformFunction) {
|
||||
var openToken = "OPEN" + Date.now();
|
||||
var string = htmlString.replace(/</g, openToken);
|
||||
var children = $('<div>').html(string).contents();
|
||||
var children;
|
||||
try {
|
||||
children = $('<div>').html(string).contents();
|
||||
} catch (e) {
|
||||
children = $('<div>').html('<pre>' + string + '</pre>').contents();
|
||||
}
|
||||
return _parseAndTransform(children, transformFunction)
|
||||
.replace(new RegExp(openToken, "g"), "<");
|
||||
}
|
||||
|
||||
@@ -251,6 +251,9 @@
|
||||
.o_composer_send .o_composer_button_send, .o_chatter_avatar {
|
||||
display: none !important;
|
||||
}
|
||||
.o_attachment {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.o_file_drop_zone_container {
|
||||
@@ -292,6 +295,10 @@
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
|
||||
.o_attachment_wrap {
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.o_attachments_previews {
|
||||
> .o_attachment {
|
||||
width: 50%;
|
||||
|
||||
@@ -183,6 +183,7 @@
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.o_mail_body_long {
|
||||
|
||||
@@ -201,7 +201,7 @@ QUnit.test('post messages in a document thread window', async function (assert)
|
||||
testUtils.mock.intercept(messagingMenu, 'call_service', function (ev) {
|
||||
if (ev.data.service === 'local_storage' && ev.data.method === 'setItem' &&
|
||||
ev.data.args[0] === 'mail.document_threads_last_message') {
|
||||
assert.deepEqual(ev.data.args[1], newMessage,
|
||||
assert.deepEqual(ev.data.args[1].messageData, newMessage,
|
||||
"should write sent message in local storage, to share info with other tabs");
|
||||
}
|
||||
}, true);
|
||||
@@ -227,7 +227,7 @@ QUnit.test('post messages in a document thread window', async function (assert)
|
||||
});
|
||||
|
||||
QUnit.test('open, fold, unfold and close a document thread window', async function (assert) {
|
||||
assert.expect(8);
|
||||
assert.expect(24);
|
||||
var messagingMenu = new MessagingMenu();
|
||||
testUtils.mock.addMockEnvironment(messagingMenu, {
|
||||
services: this.services,
|
||||
@@ -237,7 +237,13 @@ QUnit.test('open, fold, unfold and close a document thread window', async functi
|
||||
await testUtils.nextTick();
|
||||
testUtils.mock.intercept(messagingMenu, 'call_service', function (ev) {
|
||||
if (ev.data.service === 'local_storage' && ev.data.method === 'setItem') {
|
||||
assert.step(JSON.stringify(ev.data.args));
|
||||
const key = ev.data.args[0];
|
||||
const state = ev.data.args[1].state;
|
||||
assert.strictEqual(key, 'mail.document_threads_state/some.res.model_1');
|
||||
assert.ok(state);
|
||||
assert.ok(state.name);
|
||||
assert.ok(state.windowState);
|
||||
assert.step(`${key}: { name: "${state.name}", windowState: '${state.windowState}' }`);
|
||||
}
|
||||
}, true);
|
||||
await messagingMenu.appendTo($('#qunit-fixture'));
|
||||
@@ -263,10 +269,10 @@ QUnit.test('open, fold, unfold and close a document thread window', async functi
|
||||
await testUtils.nextTick();
|
||||
|
||||
assert.verifySteps([
|
||||
'["mail.document_threads_state",{"some.res.model_1":{"name":"Some Record","windowState":"open"}}]',
|
||||
'["mail.document_threads_state",{"some.res.model_1":{"name":"Some Record","windowState":"folded"}}]',
|
||||
'["mail.document_threads_state",{"some.res.model_1":{"name":"Some Record","windowState":"open"}}]',
|
||||
'["mail.document_threads_state",{"some.res.model_1":{"name":"Some Record","windowState":"closed"}}]',
|
||||
`mail.document_threads_state/some.res.model_1: { name: "Some Record", windowState: 'open' }`,
|
||||
`mail.document_threads_state/some.res.model_1: { name: "Some Record", windowState: 'folded' }`,
|
||||
`mail.document_threads_state/some.res.model_1: { name: "Some Record", windowState: 'open' }`,
|
||||
`mail.document_threads_state/some.res.model_1: { name: "Some Record", windowState: 'closed' }`,
|
||||
]);
|
||||
|
||||
messagingMenu.destroy();
|
||||
|
||||
@@ -427,6 +427,88 @@ QUnit.test('update messaging preview on receiving a new message in channel previ
|
||||
messagingMenu.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('new message of type "notification" are not considered as unread messages', async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
const messagingMenu = new MessagingMenu();
|
||||
testUtils.mock.addMockEnvironment(messagingMenu, {
|
||||
data: this.data,
|
||||
services: this.services,
|
||||
});
|
||||
await messagingMenu.appendTo($('#qunit-fixture'));
|
||||
// open messaging menu
|
||||
await testUtils.dom.click(messagingMenu.$('.dropdown-toggle'));
|
||||
assert.containsOnce(
|
||||
messagingMenu,
|
||||
'.o_mail_preview',
|
||||
"should display a single channel preview");
|
||||
assert.strictEqual(
|
||||
messagingMenu
|
||||
.$('.o_preview_name')
|
||||
.text()
|
||||
.trim(),
|
||||
"general",
|
||||
"should display channel preview of 'general' channel");
|
||||
assert.strictEqual(
|
||||
messagingMenu
|
||||
.$('.o_preview_counter')
|
||||
.text()
|
||||
.trim(),
|
||||
"",
|
||||
"should have unread counter of 0 for 'general' channel (no unread messages)");
|
||||
assert.strictEqual(
|
||||
messagingMenu
|
||||
.$('.o_last_message_preview')
|
||||
.text()
|
||||
.replace(/\s/g, ""),
|
||||
"Me:test",
|
||||
"should display author name and inline body of currently last message in the channel");
|
||||
// close messaging menu
|
||||
await testUtils.dom.click(messagingMenu.$('.dropdown-toggle'));
|
||||
|
||||
// simulate receiving a new message on the channel 'general' (id 1)
|
||||
// of type 'notification'
|
||||
const data = {
|
||||
id: 100,
|
||||
author_id: [42, "Someone"],
|
||||
body: "<p>Left #general</p>",
|
||||
channel_ids: [1],
|
||||
message_type: 'notification',
|
||||
};
|
||||
const notification = [[false, 'mail.channel', 1], data];
|
||||
messagingMenu.call('bus_service', 'trigger', 'notification', [notification]);
|
||||
await testUtils.nextTick();
|
||||
// open messaging menu
|
||||
await testUtils.dom.click(messagingMenu.$('.dropdown-toggle'));
|
||||
assert.containsOnce(
|
||||
messagingMenu,
|
||||
'.o_mail_preview',
|
||||
"should still display a single channel preview");
|
||||
assert.strictEqual(
|
||||
messagingMenu
|
||||
.$('.o_preview_name')
|
||||
.text()
|
||||
.trim(),
|
||||
"general",
|
||||
"should still display channel preview of 'general' channel");
|
||||
assert.strictEqual(
|
||||
messagingMenu
|
||||
.$('.o_preview_counter')
|
||||
.text()
|
||||
.trim(),
|
||||
"",
|
||||
"should still have unread counter of 0 for 'general' channel (no unread messages)");
|
||||
assert.strictEqual(
|
||||
messagingMenu
|
||||
.$('.o_last_message_preview')
|
||||
.text()
|
||||
.replace(/\s/g, ""),
|
||||
"Someone:Left#general",
|
||||
"should display author name and inline body of notification message (last message of channel)");
|
||||
|
||||
messagingMenu.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('preview of inbox message not linked to document + mark as read', async function (assert) {
|
||||
assert.expect(17);
|
||||
|
||||
|
||||
@@ -15,21 +15,13 @@
|
||||
<group name="activity_details">
|
||||
<field name="active" invisible="1"/>
|
||||
<field name="category"/>
|
||||
<field name="default_user_id" widget="selection"/>
|
||||
<field name="res_model_id" groups="base.group_no_one"/>
|
||||
<field name="res_model_change" invisible="1"/>
|
||||
<field name="initial_res_model_id" invisible="1"/>
|
||||
<field name="summary"/>
|
||||
<field name="icon" groups="base.group_no_one"/>
|
||||
<field name="decoration_type" groups="base.group_no_one"/>
|
||||
<label for="delay_count"/>
|
||||
<div>
|
||||
<div class="o_row">
|
||||
<field name="delay_count"/>
|
||||
<field name="delay_unit"/>
|
||||
</div>
|
||||
<field name="delay_from"/>
|
||||
</div>
|
||||
<field name="default_user_id" widget="selection"/>
|
||||
</group>
|
||||
<group name="activity_planning">
|
||||
<field name="force_next"/>
|
||||
@@ -43,6 +35,14 @@
|
||||
</group>
|
||||
<group>
|
||||
<field name="default_description"/>
|
||||
<label for="delay_count"/>
|
||||
<div>
|
||||
<div class="o_row">
|
||||
<field name="delay_count"/>
|
||||
<field name="delay_unit"/>
|
||||
</div>
|
||||
<field name="delay_from"/>
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
<p class="alert alert-info" role="alert" attrs="{'invisible': [('res_model_change', '=', False)]}">Modifying the model can have an impact on existing activities using this activity type, be careful.</p>
|
||||
|
||||
@@ -128,8 +128,8 @@
|
||||
<templates>
|
||||
<t t-name="kanban-tooltip">
|
||||
<ul class="oe_kanban_tooltip">
|
||||
<li><b>Category:</b> <field name="category_id"/></li>
|
||||
<li><b>Request to:</b> <field name="user_id"/></li>
|
||||
<li t-if="record.category_id.raw_value"><b>Category:</b> <t t-esc="record.category_id.value"/></li>
|
||||
<li t-if="record.user_id.raw_value"><b>Request to:</b> <t t-esc="record.user_id.value"/></li>
|
||||
</ul>
|
||||
</t>
|
||||
<t t-name="kanban-box">
|
||||
@@ -430,8 +430,8 @@
|
||||
<templates>
|
||||
<t t-name="kanban-tooltip">
|
||||
<ul class="oe_kanban_tooltip">
|
||||
<li><b>Serial Number:</b> <field name="serial_no"/></li>
|
||||
<li><b>Model Number:</b> <field name="model"/></li>
|
||||
<li t-if="record.serial_no.raw_value"><b>Serial Number:</b> <t t-esc="record.serial_no.value"/></li>
|
||||
<li t-if="record.model.raw_value"><b>Model Number:</b> <t t-esc="record.model.value"/></li>
|
||||
</ul>
|
||||
</t>
|
||||
<t t-name="kanban-box">
|
||||
|
||||
@@ -369,6 +369,7 @@ class MrpProduction(models.Model):
|
||||
order.post_visible = order.is_locked and any((x.quantity_done > 0 and x.state not in ['done', 'cancel']) for x in order.move_raw_ids | order.move_finished_ids)
|
||||
else:
|
||||
order.post_visible = order.is_locked and any((x.quantity_done > 0 and x.state not in ['done', 'cancel']) for x in order.move_finished_ids)
|
||||
order.post_visible &= all(wo.state in ['done', 'cancel'] for wo in order.workorder_ids) or all(m.product_id.tracking == 'none' for m in order.move_raw_ids)
|
||||
|
||||
@api.depends('workorder_ids.state', 'move_finished_ids', 'move_finished_ids.quantity_done', 'is_locked')
|
||||
def _get_produced_qty(self):
|
||||
@@ -669,6 +670,8 @@ class MrpProduction(models.Model):
|
||||
|
||||
def open_produce_product(self):
|
||||
self.ensure_one()
|
||||
if self.bom_id.type == 'phantom':
|
||||
raise UserError(_('You cannot produce a MO with a bom kit product.'))
|
||||
action = self.env.ref('mrp.act_mrp_product_produce').read()[0]
|
||||
return action
|
||||
|
||||
|
||||
@@ -234,6 +234,11 @@ class StockMove(models.Model):
|
||||
res = super(StockMove, self)._should_be_assigned()
|
||||
return bool(res and not (self.production_id or self.raw_material_production_id))
|
||||
|
||||
def _key_assign_picking(self):
|
||||
keys = super(StockMove, self)._key_assign_picking()
|
||||
return keys + (self.created_production_id,)
|
||||
|
||||
|
||||
def _compute_kit_quantities(self, product_id, kit_qty, kit_bom, filters):
|
||||
""" Computes the quantity delivered or received when a kit is sold or purchased.
|
||||
A ratio 'qty_processed/qty_needed' is computed for each component, and the lowest one is kept
|
||||
@@ -267,4 +272,4 @@ class StockMove(models.Model):
|
||||
# doesn't make sense.
|
||||
return min(qty_ratios) // 1
|
||||
else:
|
||||
return 0.0
|
||||
return 0.0
|
||||
@@ -130,7 +130,7 @@
|
||||
<div t-if="payment_tx_id.acquirer_id.provider == 'transfer' and reference">
|
||||
<b>Communication: </b><span t-esc='reference'/>
|
||||
</div>
|
||||
<div t-if="(payment_tx_id.partner_id.country_id.code in ['BE', 'AT', 'DE', 'FI', 'NL']) and (payment_tx_id.acquirer_id.qr_code == True) and (payment_tx_id.currency_id.name == 'EUR')">
|
||||
<div t-if="(payment_tx_id.acquirer_id.qr_code == True) and (payment_tx_id.currency_id.name == 'EUR')">
|
||||
<div t-if="payment_tx_id.acquirer_id.journal_id.bank_account_id.qr_code_valid">
|
||||
<h3>Or scan me with your banking app.</h3>
|
||||
<img class="border border-dark rounded" t-att-src="payment_tx_id.acquirer_id.journal_id.bank_account_id.build_qr_code_url(payment_tx_id.amount,payment_tx_id.reference)"/>
|
||||
|
||||
@@ -44,9 +44,9 @@ class AcquirerSips(models.Model):
|
||||
provider = fields.Selection(selection_add=[('sips', 'Sips')])
|
||||
sips_merchant_id = fields.Char('Merchant ID', help="Used for production only", required_if_provider='sips', groups='base.group_user')
|
||||
sips_secret = fields.Char('Secret Key', size=64, required_if_provider='sips', groups='base.group_user')
|
||||
sips_test_url = fields.Char("Test url", required_if_provider='sips', groups='base.group_no_one', default='https://payment-webinit.sips-atos.com/paymentInit')
|
||||
sips_prod_url = fields.Char("Production url", required_if_provider='sips', groups='base.group_no_one', default='https://payment-webinit.simu.sips-atos.com/paymentInit')
|
||||
sips_version = fields.Char("Interface Version", required_if_provider='sips', groups='base.group_no_one', default='HP_2.3')
|
||||
sips_test_url = fields.Char("Test url", required_if_provider='sips', default='https://payment-webinit.simu.sips-atos.com/paymentInit')
|
||||
sips_prod_url = fields.Char("Production url", required_if_provider='sips', default='https://payment-webinit.sips-atos.com/paymentInit')
|
||||
sips_version = fields.Char("Interface Version", required_if_provider='sips', default='HP_2.3')
|
||||
|
||||
def _sips_generate_shasign(self, values):
|
||||
""" Generate the shasign for incoming or outgoing communications.
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
<group attrs="{'invisible': [('provider', '!=', 'sips')]}">
|
||||
<field name="sips_merchant_id" attrs="{'required':[ ('provider', '=', 'sips'), ('state', '!=', 'disabled')]}"/>
|
||||
<field name="sips_secret" attrs="{'required':[ ('provider', '=', 'sips'), ('state', '!=', 'disabled')]}"/>
|
||||
<field name="sips_test_url" attrs="{'required':[ ('provider', '=', 'sips'), ('state', '!=', 'disabled')]}"/>
|
||||
<field name="sips_prod_url" attrs="{'required':[ ('provider', '=', 'sips'), ('state', '!=', 'disabled')]}"/>
|
||||
<field name="sips_version" attrs="{'required':[ ('provider', '=', 'sips'), ('state', '!=', 'disabled')]}"/>
|
||||
<field name="sips_test_url" attrs="{'required':[ ('provider', '=', 'sips'), ('state', '!=', 'disabled')]}" groups='base.group_no_one'/>
|
||||
<field name="sips_prod_url" attrs="{'required':[ ('provider', '=', 'sips'), ('state', '!=', 'disabled')]}" groups='base.group_no_one'/>
|
||||
<field name="sips_version" attrs="{'required':[ ('provider', '=', 'sips'), ('state', '!=', 'disabled')]}" groups='base.group_no_one'/>
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
|
||||
@@ -24,3 +24,12 @@ class AccountTax(models.Model):
|
||||
'You must close the POS sessions before modifying the tax.'
|
||||
))
|
||||
return super(AccountTax, self).write(vals)
|
||||
|
||||
def get_real_tax_amount(self):
|
||||
tax_list = []
|
||||
for tax in self:
|
||||
tax_repartition_lines = tax.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax')
|
||||
total_factor = sum(tax_repartition_lines.mapped('factor'))
|
||||
real_amount = tax.amount * total_factor
|
||||
tax_list.append({'id': tax.id, 'amount': real_amount})
|
||||
return tax_list
|
||||
|
||||
@@ -122,8 +122,7 @@ body .pos-customer_facing_display .pos_orderlines .pos_orderlines_item.pos_order
|
||||
box-shadow: none;
|
||||
animation: none;
|
||||
}
|
||||
body .pos-customer_facing_display .pos_orderlines .pos_orderlines_item.pos_orderlines_header > div,
|
||||
body .pos-customer_facing_display .pos_orderlines .pos_orderlines_item.pos_orderlines_header > div:last-child {
|
||||
body .pos-customer_facing_display .pos_orderlines .pos_orderlines_item.pos_orderlines_header > div, body .pos-customer_facing_display .pos_orderlines .pos_orderlines_item.pos_orderlines_header > div:last-child {
|
||||
border-left-width: 0;
|
||||
text-align: center;
|
||||
font-size: 70%;
|
||||
@@ -423,7 +422,7 @@ body .pos-customer_facing_display .pos-payment_info .pos-payment_info_details .p
|
||||
text-align: center;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-payment_info .pos-company_logo {
|
||||
display: none!important;
|
||||
display: none !important;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-payment_info .pos-payment_info_details {
|
||||
-webkit-box-flex: 1 0 100%;
|
||||
@@ -439,6 +438,7 @@ body .pos-customer_facing_display .pos-payment_info .pos-payment_info_details .p
|
||||
font-size: 6.5vw;
|
||||
}
|
||||
}
|
||||
|
||||
body .pos-hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -460,7 +460,7 @@ body.editor_enable #oe_manipulators .oe_overlay > .oe_overlay_options > .btn-gro
|
||||
}
|
||||
body.editor_enable #oe_manipulators .oe_overlay > .oe_overlay_options > .btn-group > .dropdown.oe_options.btn-group.snippet-option-pos_palette > .dropdown-menu > a {
|
||||
position: relative;
|
||||
padding: 14px 0!important;
|
||||
padding: 14px 0 !important;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
body.editor_enable #oe_manipulators .oe_overlay > .oe_overlay_options > .btn-group > .dropdown.oe_options.btn-group.snippet-option-pos_palette > .dropdown-menu > a:before {
|
||||
@@ -470,6 +470,7 @@ body.editor_enable #oe_manipulators .no_parent_options .snippet-option-pos_palet
|
||||
body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pos-palette_01 .pos-payment_info {
|
||||
background: #3E3E3E;
|
||||
color: #f6f6f6;
|
||||
@@ -506,6 +507,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
|
||||
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_01:before {
|
||||
text-shadow: 0 0 1px #090909;
|
||||
}
|
||||
|
||||
.pos-palette_02 .pos-payment_info {
|
||||
background: #364152;
|
||||
color: #e6e7e8;
|
||||
@@ -542,6 +544,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
|
||||
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_02:before {
|
||||
text-shadow: 0 0 1px #130d09;
|
||||
}
|
||||
|
||||
.pos-palette_03 .pos-payment_info {
|
||||
background: #1BA39C;
|
||||
color: #f6f6f6;
|
||||
@@ -578,6 +581,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
|
||||
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_03:before {
|
||||
text-shadow: 0 0 1px #131313;
|
||||
}
|
||||
|
||||
.pos-palette_04 .pos-payment_info {
|
||||
background: #0b7b6c;
|
||||
color: #f6f6f6;
|
||||
@@ -614,6 +618,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
|
||||
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_04:before {
|
||||
text-shadow: 0 0 1px #101113;
|
||||
}
|
||||
|
||||
.pos-palette_05 .pos-payment_info {
|
||||
background: #E26868;
|
||||
color: #f6f6f6;
|
||||
@@ -650,6 +655,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
|
||||
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_05:before {
|
||||
text-shadow: 0 0 1px #131313;
|
||||
}
|
||||
|
||||
.pos-palette_06 .pos-payment_info {
|
||||
background: #9E373B;
|
||||
color: #f6f6f6;
|
||||
@@ -686,6 +692,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
|
||||
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_06:before {
|
||||
text-shadow: 0 0 1px #090909;
|
||||
}
|
||||
|
||||
.pos-palette_07 .pos-payment_info {
|
||||
background: #ce9934;
|
||||
color: white;
|
||||
@@ -722,6 +729,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
|
||||
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_07:before {
|
||||
text-shadow: 0 0 1px #131313;
|
||||
}
|
||||
|
||||
.pos-palette_08 .pos-payment_info {
|
||||
background: #a48c77;
|
||||
color: #f6f6f6;
|
||||
@@ -758,6 +766,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
|
||||
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_08:before {
|
||||
text-shadow: 0 0 1px #131313;
|
||||
}
|
||||
|
||||
.pos-palette_09 .pos-payment_info {
|
||||
background: linear-gradient(30deg, #014d43, #127e71);
|
||||
color: #f6f6f6;
|
||||
@@ -794,6 +803,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
|
||||
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_09:before {
|
||||
text-shadow: 0 0 1px #131313;
|
||||
}
|
||||
|
||||
.pos-palette_10 .pos-payment_info {
|
||||
background: linear-gradient(30deg, #e2316c, #ea4c89);
|
||||
color: white;
|
||||
@@ -830,6 +840,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
|
||||
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_10:before {
|
||||
text-shadow: 0 0 1px #131313;
|
||||
}
|
||||
|
||||
.pos-palette_11 .pos-payment_info {
|
||||
background: linear-gradient(30deg, #362b3d, #5b4a63);
|
||||
color: white;
|
||||
@@ -866,6 +877,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
|
||||
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_11:before {
|
||||
text-shadow: 0 0 1px #131313;
|
||||
}
|
||||
|
||||
.pos-palette_12 .pos-payment_info {
|
||||
background: #434343;
|
||||
color: #e6e6e6;
|
||||
@@ -902,6 +914,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
|
||||
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_12:before {
|
||||
text-shadow: 0 0 1px #a4a4a4;
|
||||
}
|
||||
|
||||
.pos-palette_13 .pos-payment_info {
|
||||
background: linear-gradient(30deg, #1a1b1f, #3d3f45);
|
||||
color: white;
|
||||
|
||||
@@ -239,6 +239,19 @@ exports.PosModel = Backbone.Model.extend({
|
||||
return self.taxes_by_id[child_tax_id];
|
||||
});
|
||||
});
|
||||
return new Promise(function (resolve, reject) {
|
||||
var tax_ids = _.pluck(self.taxes, 'id');
|
||||
rpc.query({
|
||||
model: 'account.tax',
|
||||
method: 'get_real_tax_amount',
|
||||
args: [tax_ids],
|
||||
}).then(function (taxes) {
|
||||
_.each(taxes, function (tax) {
|
||||
self.taxes_by_id[tax.id].amount = tax.amount;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
},{
|
||||
model: 'pos.session',
|
||||
@@ -848,7 +861,7 @@ exports.PosModel = Backbone.Model.extend({
|
||||
self.flush_mutex.exec(function () {
|
||||
var flushed = self._flush_orders(self.db.get_orders(), opts);
|
||||
|
||||
flushed.then(resolve, resolve);
|
||||
flushed.then(resolve, reject);
|
||||
|
||||
return flushed;
|
||||
});
|
||||
@@ -929,8 +942,9 @@ exports.PosModel = Backbone.Model.extend({
|
||||
return this._save_to_server(orders, options).then(function (server_ids) {
|
||||
self.set_synch('connected');
|
||||
return _.pluck(server_ids, 'id');
|
||||
}).catch(function(){
|
||||
}).catch(function(error){
|
||||
self.set_synch(self.get('failed') ? 'error' : 'disconnected');
|
||||
return Promise.reject(error);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -99,7 +99,8 @@ body {
|
||||
@include flex-display;
|
||||
@include flex-direction(row);
|
||||
|
||||
.pos-customer_products, .pos-payment_info {
|
||||
.pos-customer_products,
|
||||
.pos-payment_info {
|
||||
height: 100%;
|
||||
padding: 2%;
|
||||
@include flex-display;
|
||||
@@ -139,11 +140,14 @@ body {
|
||||
box-shadow: none;
|
||||
animation: none;
|
||||
|
||||
> div, > div:last-child {
|
||||
border-left-width: 0;
|
||||
text-align: center;
|
||||
font-size: 70%;
|
||||
font-weight: normal;
|
||||
> div {
|
||||
&,
|
||||
&:last-child {
|
||||
border-left-width: 0;
|
||||
text-align: center;
|
||||
font-size: 70%;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
> div:last-child {
|
||||
@@ -194,7 +198,8 @@ body {
|
||||
@include flex-direction(column);
|
||||
@include justify-content(space-between);
|
||||
|
||||
.pos-adv, .pos-company_logo {
|
||||
.pos-adv,
|
||||
.pos-company_logo {
|
||||
@include pos-bg;
|
||||
}
|
||||
|
||||
@@ -212,7 +217,8 @@ body {
|
||||
}
|
||||
|
||||
.pos-payment_info_details{
|
||||
.pos-total, .pos-paymentlines {
|
||||
.pos-total,
|
||||
.pos-paymentlines {
|
||||
@include flex-direction(row);
|
||||
@include flex-display;
|
||||
@include flex-wrap(wrap);
|
||||
@@ -221,6 +227,7 @@ body {
|
||||
|
||||
> div {
|
||||
@include flex(1 0 48%);
|
||||
|
||||
&:nth-child(even) {
|
||||
font-weight: bold;
|
||||
font-size: 120%;
|
||||
@@ -433,7 +440,7 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
#oe_manipulators .oe_overlay > .oe_overlay_options > .btn-group> .dropdown.oe_options.btn-group.snippet-option-pos_palette > ul {
|
||||
#oe_manipulators .oe_overlay > .oe_overlay_options > .btn-group> .dropdown.oe_options.btn-group.snippet-option-pos_palette > .dropdown-menu {
|
||||
min-width: 100px;
|
||||
max-width: 100px;
|
||||
|
||||
@@ -441,7 +448,7 @@ body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> li > a {
|
||||
> a {
|
||||
position: relative;
|
||||
padding: 14px 0!important;
|
||||
margin-bottom: 1px;
|
||||
@@ -453,7 +460,8 @@ body {
|
||||
}
|
||||
|
||||
#oe_manipulators .no_parent_options {
|
||||
.snippet-option-pos_palette, .o_parent_editor_header {
|
||||
.snippet-option-pos_palette,
|
||||
.o_parent_editor_header {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -462,7 +470,7 @@ body {
|
||||
|
||||
|
||||
// =========== PALETTE GENERATOR ===========
|
||||
.palette_variant (
|
||||
@mixin palette-variant (
|
||||
$bg-info: $color-gray-dark,
|
||||
$text-info: $color-gray-lighter,
|
||||
$bg-products: $color-gray-lighter,
|
||||
@@ -497,7 +505,7 @@ body {
|
||||
}
|
||||
|
||||
// Editor dropdown buttons
|
||||
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > ul > li > & {
|
||||
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > & {
|
||||
background: $bg-products!important;
|
||||
|
||||
&:after {
|
||||
@@ -511,7 +519,7 @@ body {
|
||||
}
|
||||
|
||||
&:before {
|
||||
text-shadow: 0 0 1px (#fff - $bg-products);
|
||||
text-shadow: 0 0 1px invert($bg-products);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ portal.
|
||||
This module contains most code coming from odoo v10 website_portal. Purpose
|
||||
of this module is to allow the display of a customer portal without having
|
||||
a dependency towards website edition and customization capabilities.""",
|
||||
'depends': ['web', 'web_editor', 'http_routing', 'mail'],
|
||||
'depends': ['web', 'web_editor', 'http_routing', 'mail', 'auth_signup'],
|
||||
'data': [
|
||||
'data/portal_data.xml',
|
||||
'views/assets.xml',
|
||||
|
||||
@@ -130,6 +130,7 @@ class Pricelist(models.Model):
|
||||
self.ensure_one()
|
||||
if not date:
|
||||
date = self._context.get('date') or fields.Date.context_today(self)
|
||||
date = fields.Date.to_date(date) # handwritten query below does not work with non-dates (e.g. datetimes)
|
||||
if not uom_id and self._context.get('uom'):
|
||||
uom_id = self._context['uom']
|
||||
if uom_id:
|
||||
@@ -305,10 +306,10 @@ class Pricelist(models.Model):
|
||||
list(zip(**products_by_qty_by_partner)))
|
||||
|
||||
def _get_partner_pricelist_multi_search_domain_hook(self):
|
||||
return []
|
||||
return [('active', '=', True)]
|
||||
|
||||
def _get_partner_pricelist_multi_filter_hook(self):
|
||||
return self
|
||||
return self.filtered('active')
|
||||
|
||||
def _get_partner_pricelist_multi(self, partner_ids, company_id=None):
|
||||
""" Retrieve the applicable pricelist for given partners in a given company.
|
||||
|
||||
@@ -420,13 +420,20 @@ class ProductTemplate(models.Model):
|
||||
|
||||
Product = self.env['product.product']
|
||||
templates = self.browse([])
|
||||
domain_no_variant = [('product_variant_ids', '=', False)]
|
||||
while True:
|
||||
domain = templates and [('product_tmpl_id', 'not in', templates.ids)] or []
|
||||
args = args if args is not None else []
|
||||
products_ns = Product._name_search(name, args+domain, operator=operator, name_get_uid=name_get_uid)
|
||||
products = Product.browse([x[0] for x in products_ns])
|
||||
templates |= products.mapped('product_tmpl_id')
|
||||
if (not products) or (limit and (len(templates) > limit)):
|
||||
current_round_templates = self.browse([])
|
||||
if not products:
|
||||
domain_template = args + domain_no_variant + (templates and [('id', 'not in', templates.ids)] or [])
|
||||
template_ns = super(ProductTemplate, self)._name_search(name=name, args=domain_template, operator=operator, limit=limit, name_get_uid=name_get_uid)
|
||||
current_round_templates |= self.browse([ns[0] for ns in template_ns])
|
||||
templates |= current_round_templates
|
||||
if (not products and not current_round_templates) or (limit and (len(templates) > limit)):
|
||||
break
|
||||
|
||||
searched_ids = set(templates.ids)
|
||||
|
||||
@@ -827,6 +827,28 @@ class TestVariantsArchive(common.TestProductCommon):
|
||||
|
||||
Product._revert_method('unlink')
|
||||
|
||||
def test_name_search_dynamic_attributes(self):
|
||||
dynamic_attr = self.env['product.attribute'].create({
|
||||
'name': 'Dynamic',
|
||||
'create_variant': 'dynamic',
|
||||
'value_ids': [(0, False, {'name': 'ValueDynamic'})],
|
||||
})
|
||||
template = self.env['product.template'].create({
|
||||
'name': 'cimanyd'
|
||||
})
|
||||
self.assertEqual(len(template.product_variant_ids), 1)
|
||||
|
||||
template.write({
|
||||
'attribute_line_ids': [(0, False, {
|
||||
'attribute_id': dynamic_attr.id,
|
||||
'value_ids': [(4, dynamic_attr.value_ids[0].id, False)],
|
||||
})]
|
||||
})
|
||||
self.assertEqual(len(template.product_variant_ids), 0)
|
||||
|
||||
name_searched = self.env['product.template'].name_search(name='cima')
|
||||
self.assertIn(template.id, [ng[0] for ng in name_searched])
|
||||
|
||||
def _update_color_vars(self, ptal):
|
||||
self.ptal_color = ptal
|
||||
self.assertEqual(self.ptal_color.attribute_id, self.pa_color)
|
||||
|
||||
@@ -396,8 +396,6 @@ class PurchaseOrder(models.Model):
|
||||
'default_company_id': self.company_id.id,
|
||||
'default_purchase_id': self.id,
|
||||
}
|
||||
if self.user_id:
|
||||
result['context']['default_user_id'] = self.user_id.id
|
||||
# choose the view_mode accordingly
|
||||
if len(self.invoice_ids) > 1 and not create_bill:
|
||||
result['domain'] = "[('id', 'in', " + str(self.invoice_ids.ids) + ")]"
|
||||
|
||||
@@ -206,10 +206,12 @@ class SaleOrder(models.Model):
|
||||
|
||||
def _get_reward_line_values(self, program):
|
||||
self.ensure_one()
|
||||
self = self.with_context(lang=self.partner_id.lang)
|
||||
program = program.with_context(lang=self.partner_id.lang)
|
||||
if program.reward_type == 'discount':
|
||||
return self._get_reward_values_discount(program.with_context(lang=self.partner_id.lang))
|
||||
return self._get_reward_values_discount(program)
|
||||
elif program.reward_type == 'product':
|
||||
return [self._get_reward_values_product(program.with_context(lang=self.partner_id.lang))]
|
||||
return [self._get_reward_values_product(program)]
|
||||
|
||||
def _create_reward_line(self, program):
|
||||
self.write({'order_line': [(0, False, value) for value in self._get_reward_line_values(program)]})
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<group name="conditions" string="Conditions">
|
||||
<field name="active" invisible="1"/>
|
||||
<field name="program_type" invisible="1"/>
|
||||
<field name="rule_products_domain" placeholder="Select product" widget="domain" options="{'model': 'product.product'}"/>
|
||||
<field name="rule_products_domain" placeholder="Select product" widget="domain" options="{'model': 'product.product', 'in_dialog': true}"/>
|
||||
<label string="Quantity" for="rule_min_quantity" attrs="{'invisible': [('rule_products_domain', '=', False)]}"/>
|
||||
<div attrs="{'invisible': [('rule_products_domain', '=',False)]}">
|
||||
<field name="rule_min_quantity" class="oe_inline"/>
|
||||
@@ -211,7 +211,7 @@
|
||||
<field name="sequence" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='program_type']" position="after">
|
||||
<field name="rule_partners_domain" placeholder="Select customer" widget="domain" options="{'model': 'res.partner'}"/>
|
||||
<field name="rule_partners_domain" placeholder="Select customer" widget="domain" options="{'model': 'res.partner', 'in_dialog': true}"/>
|
||||
</xpath>
|
||||
<xpath expr="//div[@name='rule_minimum_amount']" position="after">
|
||||
<field name="promo_code_usage" widget="radio"/>
|
||||
|
||||
@@ -67,11 +67,11 @@ class SaleOrderLine(models.Model):
|
||||
_inherit = 'sale.order.line'
|
||||
|
||||
purchase_line_ids = fields.One2many('purchase.order.line', 'sale_line_id', string="Generated Purchase Lines", readonly=True, help="Purchase line generated by this Sales item on order confirmation, or when the quantity was increased.")
|
||||
purchase_line_count = fields.Integer("Number of generated purchase items", compute='_compute_purchase_count', compute_sudo=True)
|
||||
purchase_line_count = fields.Integer("Number of generated purchase items", compute='_compute_purchase_count')
|
||||
|
||||
@api.depends('purchase_line_ids')
|
||||
def _compute_purchase_count(self):
|
||||
database_data = self.env['purchase.order.line'].read_group([('sale_line_id', 'in', self.ids)], ['sale_line_id'], ['sale_line_id'])
|
||||
database_data = self.env['purchase.order.line'].sudo().read_group([('sale_line_id', 'in', self.ids)], ['sale_line_id'], ['sale_line_id'])
|
||||
mapped_data = dict([(db['sale_line_id'][0], db['sale_line_id_count']) for db in database_data])
|
||||
for line in self:
|
||||
line.purchase_line_count = mapped_data.get(line.id, 0)
|
||||
|
||||
@@ -11,7 +11,7 @@ class StockLocationRoute(models.Model):
|
||||
|
||||
class StockMove(models.Model):
|
||||
_inherit = "stock.move"
|
||||
sale_line_id = fields.Many2one('sale.order.line', 'Sale Line')
|
||||
sale_line_id = fields.Many2one('sale.order.line', 'Sale Line', index=True)
|
||||
|
||||
@api.model
|
||||
def _prepare_merge_moves_distinct_fields(self):
|
||||
|
||||
@@ -1325,7 +1325,7 @@ class StockMove(models.Model):
|
||||
for result_package in moves_todo\
|
||||
.mapped('move_line_ids.result_package_id')\
|
||||
.filtered(lambda p: p.quant_ids and len(p.quant_ids) > 1):
|
||||
if len(result_package.quant_ids.mapped('location_id')) > 1:
|
||||
if len(result_package.quant_ids.filtered(lambda q: float_is_zero(abs(q.quantity) + abs(q.reserved_quantity), precision_rounding=q.product_uom_id.rounding)).mapped('location_id')) > 1:
|
||||
raise UserError(_('You cannot move the same package content more than once in the same transfer or split the same package into two location.'))
|
||||
picking = moves_todo.mapped('picking_id')
|
||||
moves_todo.write({'state': 'done', 'date': fields.Datetime.now()})
|
||||
|
||||
@@ -549,7 +549,7 @@ class Picking(models.Model):
|
||||
|
||||
# As the on_change in one2many list is WIP, we will overwrite the locations on the stock moves here
|
||||
# As it is a create the format will be a list of (0, 0, dict)
|
||||
moves = vals.get('move_lines') or vals.get('move_ids_without_package')
|
||||
moves = vals.get('move_lines', []) + vals.get('move_ids_without_package', [])
|
||||
if moves and vals.get('location_id') and vals.get('location_dest_id'):
|
||||
for move in moves:
|
||||
if len(move) == 3 and move[0] == 0:
|
||||
|
||||
@@ -50,7 +50,7 @@ class StockQuant(models.Model):
|
||||
product_id = fields.Many2one(
|
||||
'product.product', 'Product',
|
||||
domain=lambda self: self._domain_product_id(),
|
||||
ondelete='restrict', readonly=True, required=True, check_company=True)
|
||||
ondelete='restrict', readonly=True, required=True, index=True, check_company=True)
|
||||
# so user can filter on template in webclient
|
||||
product_tmpl_id = fields.Many2one(
|
||||
'product.template', string='Product Template',
|
||||
@@ -63,7 +63,7 @@ class StockQuant(models.Model):
|
||||
location_id = fields.Many2one(
|
||||
'stock.location', 'Location',
|
||||
domain=lambda self: self._domain_location_id(),
|
||||
auto_join=True, ondelete='restrict', readonly=True, required=True, check_company=True)
|
||||
auto_join=True, ondelete='restrict', readonly=True, required=True, index=True, check_company=True)
|
||||
lot_id = fields.Many2one(
|
||||
'stock.production.lot', 'Lot/Serial Number',
|
||||
ondelete='restrict', readonly=True, check_company=True,
|
||||
|
||||
@@ -515,7 +515,7 @@ class Warehouse(models.Model):
|
||||
delivery_steps = vals.get('delivery_steps', def_values['delivery_steps'])
|
||||
code = vals.get('code') or code
|
||||
code = code.replace(' ', '').upper()
|
||||
company_id = vals.get('company_id', self.company_id.id)
|
||||
company_id = vals.get('company_id', self.default_get(['company_id'])['company_id'])
|
||||
sub_locations = {
|
||||
'lot_stock_id': {
|
||||
'name': _('Stock'),
|
||||
@@ -564,12 +564,13 @@ class Warehouse(models.Model):
|
||||
avoid mistakes during picking types and rules creation.
|
||||
"""
|
||||
for warehouse in self:
|
||||
sub_locations = warehouse._get_locations_values(vals, warehouse.code)
|
||||
company_id = vals.get('company_id', warehouse.company_id.id)
|
||||
sub_locations = warehouse._get_locations_values(dict(vals, company_id=company_id), warehouse.code)
|
||||
missing_location = {}
|
||||
for location, location_values in sub_locations.items():
|
||||
if not warehouse[location] and location not in vals:
|
||||
location_values['location_id'] = vals.get('view_location_id', warehouse.view_location_id.id)
|
||||
location_values['company_id'] = vals.get('company_id', warehouse.company_id.id)
|
||||
location_values['company_id'] = company_id
|
||||
missing_location[location] = self.env['stock.location'].create(location_values).id
|
||||
if missing_location:
|
||||
warehouse.write(missing_location)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests.common import SavepointCase
|
||||
from odoo.tools import float_round
|
||||
|
||||
|
||||
class TestPacking(SavepointCase):
|
||||
@@ -265,3 +266,49 @@ class TestPacking(SavepointCase):
|
||||
qp1 = pack2.quant_ids[0]
|
||||
qp2 = pack2.quant_ids[1]
|
||||
self.assertEqual(qp1.quantity + qp2.quantity, 12, 'The quant has not the good quantity')
|
||||
|
||||
def test_move_picking_with_package(self):
|
||||
"""
|
||||
355.4 rounded with 0.001 precision is 355.40000000000003.
|
||||
check that nonetheless, moving a picking is accepted
|
||||
"""
|
||||
self.assertEqual(self.productA.uom_id.rounding, 0.001)
|
||||
self.assertEqual(
|
||||
float_round(355.4, precision_rounding=self.productA.uom_id.rounding),
|
||||
355.40000000000003,
|
||||
)
|
||||
location_dict = {
|
||||
'location_id': self.stock_location.id,
|
||||
}
|
||||
quant = self.env['stock.quant'].create({
|
||||
**location_dict,
|
||||
**{'product_id': self.productA.id, 'quantity': 355.4}, # important number
|
||||
})
|
||||
package = self.env['stock.quant.package'].create({
|
||||
**location_dict, **{'quant_ids': [(6, 0, [quant.id])]},
|
||||
})
|
||||
location_dict.update({
|
||||
'state': 'draft',
|
||||
'location_dest_id': self.ship_location.id,
|
||||
})
|
||||
move = self.env['stock.move'].create({
|
||||
**location_dict,
|
||||
**{
|
||||
'name': "XXX",
|
||||
'product_id': self.productA.id,
|
||||
'product_uom': self.productA.uom_id.id,
|
||||
'product_uom_qty': 355.40000000000003, # other number
|
||||
}})
|
||||
picking = self.env['stock.picking'].create({
|
||||
**location_dict,
|
||||
**{
|
||||
'picking_type_id': self.warehouse.in_type_id.id,
|
||||
'move_lines': [(6, 0, [move.id])],
|
||||
}})
|
||||
|
||||
picking.action_confirm()
|
||||
picking.action_assign()
|
||||
move.quantity_done = move.reserved_availability
|
||||
picking.action_done()
|
||||
# if we managed to get there, there was not any exception
|
||||
# complaining that 355.4 is not 355.40000000000003. Good job!
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from odoo.addons.stock.tests.common2 import TestStockCommon
|
||||
from odoo.tests import Form
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
class TestWarehouse(TestStockCommon):
|
||||
@@ -449,6 +450,19 @@ class TestWarehouse(TestStockCommon):
|
||||
# Ensure there still no quants in distribution warehouse
|
||||
self.assertEqual(sum(self.env['stock.quant']._gather(product, warehouse_distribution_wavre.lot_stock_id).mapped('quantity')), 0)
|
||||
|
||||
def test_noleak(self):
|
||||
# non-regression test to avoid company_id leaking to other warehouses (see blame)
|
||||
wh = self.env["stock.warehouse"].search([])
|
||||
|
||||
assert len(set(wh.mapped("company_id.id"))) > 1
|
||||
|
||||
companies_before = wh.mapped(lambda w: (w.id, w.company_id))
|
||||
# writing on any field should change the company of warehouses
|
||||
wh.write({"name": "whatever"})
|
||||
companies_after = wh.mapped(lambda w: (w.id, w.company_id))
|
||||
|
||||
self.assertEqual(companies_after, companies_before)
|
||||
|
||||
def test_toggle_active_warehouse_1(self):
|
||||
""" Basic test that create a warehouse with classic configuration.
|
||||
Archive it and check that locations, picking types, routes, rules are
|
||||
|
||||
@@ -172,13 +172,13 @@ class LandedCost(models.Model):
|
||||
prec_digits = self.env.company.currency_id.decimal_places
|
||||
for landed_cost in self:
|
||||
total_amount = sum(landed_cost.valuation_adjustment_lines.mapped('additional_landed_cost'))
|
||||
if not tools.float_compare(total_amount, landed_cost.amount_total, precision_digits=prec_digits) == 0:
|
||||
if not tools.float_is_zero(total_amount - landed_cost.amount_total, precision_digits=prec_digits):
|
||||
return False
|
||||
|
||||
val_to_cost_lines = defaultdict(lambda: 0.0)
|
||||
for val_line in landed_cost.valuation_adjustment_lines:
|
||||
val_to_cost_lines[val_line.cost_line_id] += val_line.additional_landed_cost
|
||||
if any(tools.float_compare(cost_line.price_unit, val_amount, precision_digits=prec_digits) != 0
|
||||
if any(not tools.float_is_zero(cost_line.price_unit - val_amount, precision_digits=prec_digits)
|
||||
for cost_line, val_amount in val_to_cost_lines.items()):
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -52,6 +52,7 @@ class Image(models.AbstractModel):
|
||||
filename = options['filename']
|
||||
else:
|
||||
filename = record.display_name
|
||||
filename = filename.replace('/', '-').replace('\\', '-')
|
||||
|
||||
src = '/web/image/%s/%s/%s%s/%s?unique=%s' % (record._name, record.id, options.get('preview_image', field_name), max_size, url_quote(filename), sha)
|
||||
|
||||
|
||||
@@ -44,6 +44,19 @@ var AbstractStorageService = AbstractService.extend({
|
||||
var val = this.storage.getItem(key);
|
||||
return val ? JSON.parse(val) : defaultValue;
|
||||
},
|
||||
/**
|
||||
* @param {integer} index
|
||||
* @return {string}
|
||||
*/
|
||||
key: function (index) {
|
||||
return this.storage.key(index);
|
||||
},
|
||||
/**
|
||||
* @return {integer}
|
||||
*/
|
||||
length: function () {
|
||||
return this.storage.length;
|
||||
},
|
||||
/**
|
||||
* Removes the given key from the storage
|
||||
*
|
||||
|
||||
@@ -35,12 +35,10 @@ try {
|
||||
var newValue = e.originalEvent.newValue;
|
||||
try {
|
||||
JSON.parse(newValue);
|
||||
if (localStorage.getItem(key) === newValue) {
|
||||
storage.trigger('storage', {
|
||||
key: key,
|
||||
newValue: newValue,
|
||||
});
|
||||
}
|
||||
storage.trigger('storage', {
|
||||
key: key,
|
||||
newValue: newValue,
|
||||
});
|
||||
} catch (error) {}
|
||||
});
|
||||
return storage;
|
||||
|
||||
@@ -31,6 +31,7 @@ var RamStorage = Class.extend(mixins.EventDispatcherMixin, {
|
||||
*/
|
||||
clear: function () {
|
||||
this.storage = Object.create(null);
|
||||
this.length = 0;
|
||||
},
|
||||
/**
|
||||
* Returns the value associated with a given key in the storage
|
||||
@@ -41,12 +42,22 @@ var RamStorage = Class.extend(mixins.EventDispatcherMixin, {
|
||||
getItem: function (key) {
|
||||
return this.storage[key];
|
||||
},
|
||||
/**
|
||||
* @param {integer} index
|
||||
* @return {string}
|
||||
*/
|
||||
key: function (index) {
|
||||
return _.keys(this.storage)[index];
|
||||
},
|
||||
/**
|
||||
* Removes the given key from the storage
|
||||
*
|
||||
* @param {string} key
|
||||
*/
|
||||
removeItem: function (key) {
|
||||
if (key in this.storage) {
|
||||
this.length--;
|
||||
}
|
||||
delete this.storage[key];
|
||||
this.trigger('storage', {key: key, newValue: null});
|
||||
},
|
||||
@@ -58,6 +69,9 @@ var RamStorage = Class.extend(mixins.EventDispatcherMixin, {
|
||||
* @param {string} value
|
||||
*/
|
||||
setItem: function (key, value) {
|
||||
if (!(key in this.storage)) {
|
||||
this.length++;
|
||||
}
|
||||
this.storage[key] = value;
|
||||
this.trigger('storage', {key: key, newValue: value});
|
||||
},
|
||||
|
||||
@@ -2576,7 +2576,7 @@ var FieldSelection = AbstractField.extend({
|
||||
description: _lt("Selection"),
|
||||
template: 'FieldSelection',
|
||||
specialData: "_fetchSpecialRelation",
|
||||
supportedFieldTypes: ['selection', 'many2one'],
|
||||
supportedFieldTypes: ['selection'],
|
||||
events: _.extend({}, AbstractField.prototype.events, {
|
||||
'change': '_onChange',
|
||||
}),
|
||||
@@ -2629,10 +2629,14 @@ var FieldSelection = AbstractField.extend({
|
||||
*/
|
||||
_renderEdit: function () {
|
||||
this.$el.empty();
|
||||
var required = this.attrs.modifiersValue && this.attrs.modifiersValue.required;
|
||||
for (var i = 0 ; i < this.values.length ; i++) {
|
||||
var disabled = required && this.values[i][0] === false;
|
||||
|
||||
this.$el.append($('<option/>', {
|
||||
value: JSON.stringify(this.values[i][0]),
|
||||
text: this.values[i][1]
|
||||
text: this.values[i][1],
|
||||
style: disabled ? "display: none" : "",
|
||||
}));
|
||||
}
|
||||
var value = this.value;
|
||||
@@ -2671,9 +2675,7 @@ var FieldSelection = AbstractField.extend({
|
||||
return v[0] === false && v[1] === '';
|
||||
});
|
||||
}
|
||||
if (!this.attrs.modifiersValue || !this.attrs.modifiersValue.required) {
|
||||
this.values = [[false, this.attrs.placeholder || '']].concat(this.values);
|
||||
}
|
||||
this.values = [[false, this.attrs.placeholder || '']].concat(this.values);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
@@ -2068,9 +2068,11 @@ var BasicModel = AbstractModel.extend({
|
||||
if (field.onChange) {
|
||||
hasOnchange = true;
|
||||
}
|
||||
_.each(fieldInfo.views, function (view) {
|
||||
generateSpecs(view.fieldsInfo[view.type], view.fields, key + '.');
|
||||
});
|
||||
if (field.type === 'one2many' || field.type === 'many2many') {
|
||||
_.each(fieldInfo.views, function (view) {
|
||||
generateSpecs(view.fieldsInfo[view.type], view.fields, key + '.');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
return hasOnchange ? specs : false;
|
||||
|
||||
@@ -631,7 +631,9 @@ var FormController = BasicController.extend({
|
||||
* @private
|
||||
*/
|
||||
_onEdit: function () {
|
||||
this._setMode('edit');
|
||||
// wait for potential pending changes to be saved (done with widgets
|
||||
// allowing to edit in readonly)
|
||||
this.mutex.getUnlockedDef().then(this._setMode.bind(this, 'edit'));
|
||||
},
|
||||
/**
|
||||
* This method is called when someone tries to freeze the order, most likely
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user