[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:
Christophe Simonis
2019-09-13 13:32:51 +00:00
152 changed files with 2856 additions and 555 deletions
+38 -23
View File
@@ -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 {
+143 -89
View File
@@ -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
+2 -1
View File
@@ -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):
+12 -6
View File
@@ -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');
}
},
{
+1
View File
@@ -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
+17 -14
View File
@@ -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()
+80 -16
View File
@@ -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)
+255 -7
View File
@@ -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)
)
+6 -1
View File
@@ -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"/>
+1 -1
View File
@@ -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})
+4 -13
View File
@@ -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)],
+5 -1
View File
@@ -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:
+6
View File
@@ -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.
*
+4 -2
View File
@@ -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()
+58
View File
@@ -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)
+22 -1
View File
@@ -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">
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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">
+5 -2
View File
@@ -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 &lt; record.last_update.raw_value &amp; record.state.raw_value == 'failed' ? 'oe_kanban_color_2' : ''} #{record.end_date.raw_value &lt; record.last_update.raw_value &amp; 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 &lt; record.last_update.raw_value &amp; record.state.raw_value == 'failed' ? 'oe_kanban_color_2' : ''} #{record.end_date.raw_value &lt; record.last_update.raw_value &amp; 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">
+10 -4
View File
@@ -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)]
+66
View File
@@ -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()
+13 -3
View File
@@ -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':
+1
View File
@@ -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"/>
+5 -2
View File
@@ -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>
+14
View File
@@ -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
+1
View File
@@ -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):
-14
View File
@@ -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),
+9 -9
View File
@@ -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>
+6 -6
View File
@@ -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()
+7
View File
@@ -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')
+1 -1
View File
@@ -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',
}
+3 -3
View File
@@ -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" />
+14 -5
View File
@@ -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
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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.
+6 -1
View File
@@ -8,7 +8,12 @@ var _t = core._t;
function parseAndTransform(htmlString, transformFunction) {
var openToken = "OPEN" + Date.now();
var string = htmlString.replace(/&lt;/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"), "&lt;");
}
@@ -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%;
+1
View File
@@ -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);
+9 -9
View File
@@ -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">
+3
View File
@@ -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
+6 -1
View File
@@ -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)"/>
+3 -3
View File
@@ -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.
+3 -3
View File
@@ -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;
+16 -2
View File
@@ -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);
}
}
};
+1 -1
View File
@@ -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',
+3 -2
View File
@@ -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.
+8 -1
View File
@@ -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)
+22
View File
@@ -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)
-2
View File
@@ -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) + ")]"
+4 -2
View File
@@ -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"/>
+2 -2
View File
@@ -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)
+1 -1
View File
@@ -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):
+1 -1
View File
@@ -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()})
+1 -1
View File
@@ -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:
+2 -2
View File
@@ -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,
+4 -3
View File
@@ -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)
+47
View File
@@ -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!
+14
View File
@@ -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
+1
View File
@@ -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