mirror of
https://gitlab.com/flectra-hq/flectra.git
synced 2025-02-25 18:55:21 -06:00
[PATCH] Upstream patch for addons
This commit is contained in:
@@ -76,7 +76,7 @@ class AccountAccount(models.Model):
|
||||
company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True,
|
||||
default=lambda self: self.env.company)
|
||||
tag_ids = fields.Many2many('account.account.tag', 'account_account_account_tag', string='Tags', help="Optional tags you may want to assign for custom reporting")
|
||||
group_id = fields.Many2one('account.group', compute='_compute_account_group', store=True, readonly=False)
|
||||
group_id = fields.Many2one('account.group', compute='_compute_account_group', store=True, readonly=True)
|
||||
root_id = fields.Many2one('account.root', compute='_compute_account_root', store=True)
|
||||
allowed_journal_ids = fields.Many2many('account.journal', string="Allowed Journals", help="Define in which journals this account can be used. If empty, can be used in all journals.")
|
||||
|
||||
|
||||
@@ -188,7 +188,13 @@ class AccountBankStatement(models.Model):
|
||||
@api.depends('balance_start', 'previous_statement_id')
|
||||
def _compute_is_valid_balance_start(self):
|
||||
for bnk in self:
|
||||
bnk.is_valid_balance_start = float_is_zero(bnk.balance_start - bnk.previous_statement_id.balance_end_real, precision_digits=bnk.currency_id.decimal_places)
|
||||
bnk.is_valid_balance_start = (
|
||||
bnk.currency_id.is_zero(
|
||||
bnk.balance_start - bnk.previous_statement_id.balance_end_real
|
||||
)
|
||||
if bnk.previous_statement_id
|
||||
else True
|
||||
)
|
||||
|
||||
@api.depends('date', 'journal_id')
|
||||
def _get_previous_statement(self):
|
||||
@@ -706,7 +712,7 @@ class AccountBankStatementLine(models.Model):
|
||||
**counterpart_vals,
|
||||
'name': counterpart_vals.get('name', move_line.name if move_line else ''),
|
||||
'move_id': self.move_id.id,
|
||||
'partner_id': self.partner_id.id,
|
||||
'partner_id': self.partner_id.id or (move_line.partner_id.id if move_line else False),
|
||||
'currency_id': currency_id,
|
||||
'account_id': counterpart_vals.get('account_id', move_line.account_id.id if move_line else False),
|
||||
'debit': balance if balance > 0.0 else 0.0,
|
||||
@@ -771,8 +777,10 @@ class AccountBankStatementLine(models.Model):
|
||||
# COMPUTE METHODS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@api.depends('currency_id', 'amount', 'foreign_currency_id', 'amount_currency',
|
||||
'move_id.line_ids', 'move_id.line_ids.matched_debit_ids', 'move_id.line_ids.matched_credit_ids')
|
||||
@api.depends('journal_id', 'currency_id', 'amount', 'foreign_currency_id', 'amount_currency',
|
||||
'move_id.line_ids.account_id', 'move_id.line_ids.amount_currency',
|
||||
'move_id.line_ids.amount_residual_currency', 'move_id.line_ids.currency_id',
|
||||
'move_id.line_ids.matched_debit_ids', 'move_id.line_ids.matched_credit_ids')
|
||||
def _compute_is_reconciled(self):
|
||||
''' Compute the field indicating if the statement lines are already reconciled with something.
|
||||
This field is used for display purpose (e.g. display the 'cancel' button on the statement lines).
|
||||
@@ -781,19 +789,6 @@ class AccountBankStatementLine(models.Model):
|
||||
for st_line in self:
|
||||
liquidity_lines, suspense_lines, other_lines = st_line._seek_for_lines()
|
||||
|
||||
# Compute is_reconciled
|
||||
if not st_line.id:
|
||||
# New record: The journal items are not yet there.
|
||||
st_line.is_reconciled = False
|
||||
elif suspense_lines:
|
||||
# In case of the statement line comes from an older version, it could have a residual amount of zero.
|
||||
st_line.is_reconciled = all(suspense_line.reconciled for suspense_line in suspense_lines)
|
||||
elif st_line.currency_id.is_zero(st_line.amount):
|
||||
st_line.is_reconciled = True
|
||||
else:
|
||||
# The journal entry seems reconciled.
|
||||
st_line.is_reconciled = True
|
||||
|
||||
# Compute residual amount
|
||||
if st_line.to_check:
|
||||
st_line.amount_residual = -st_line.amount_currency if st_line.foreign_currency_id else -st_line.amount
|
||||
@@ -802,6 +797,19 @@ class AccountBankStatementLine(models.Model):
|
||||
else:
|
||||
st_line.amount_residual = sum(suspense_lines.mapped('amount_currency'))
|
||||
|
||||
# Compute is_reconciled
|
||||
if not st_line.id:
|
||||
# New record: The journal items are not yet there.
|
||||
st_line.is_reconciled = False
|
||||
elif suspense_lines:
|
||||
# In case of the statement line comes from an older version, it could have a residual amount of zero.
|
||||
st_line.is_reconciled = suspense_lines.currency_id.is_zero(st_line.amount_residual)
|
||||
elif st_line.currency_id.is_zero(st_line.amount):
|
||||
st_line.is_reconciled = True
|
||||
else:
|
||||
# The journal entry seems reconciled.
|
||||
st_line.is_reconciled = True
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# CONSTRAINT METHODS
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -809,18 +817,12 @@ class AccountBankStatementLine(models.Model):
|
||||
@api.constrains('amount', 'amount_currency', 'currency_id', 'foreign_currency_id', 'journal_id')
|
||||
def _check_amounts_currencies(self):
|
||||
''' Ensure the consistency the specified amounts and the currencies. '''
|
||||
if self._context.get('skip_check_amounts_currencies'):
|
||||
return
|
||||
|
||||
for st_line in self:
|
||||
if st_line.journal_id != st_line.statement_id.journal_id:
|
||||
raise ValidationError(_('The journal of a statement line must always be the same as the bank statement one.'))
|
||||
if st_line.currency_id.is_zero(st_line.amount):
|
||||
raise ValidationError(_("The amount of a statement line can't be equal to zero."))
|
||||
if st_line.foreign_currency_id == st_line.currency_id:
|
||||
raise ValidationError(_("The foreign currency must be different than the journal one: %s", st_line.currency_id.name))
|
||||
if st_line.foreign_currency_id and st_line.foreign_currency_id.is_zero(st_line.amount_currency):
|
||||
raise ValidationError(_("The amount in foreign currency must be set if the amount is not equal to zero."))
|
||||
if not st_line.foreign_currency_id and st_line.amount_currency:
|
||||
raise ValidationError(_("You can't provide an amount in foreign currency without specifying a foreign currency."))
|
||||
|
||||
@@ -1024,6 +1026,9 @@ class AccountBankStatementLine(models.Model):
|
||||
'''
|
||||
|
||||
self.ensure_one()
|
||||
journal = self.journal_id
|
||||
company_currency = journal.company_id.currency_id
|
||||
foreign_currency = self.foreign_currency_id or journal.currency_id or company_currency
|
||||
|
||||
liquidity_lines, suspense_lines, other_lines = self._seek_for_lines()
|
||||
|
||||
@@ -1040,6 +1045,7 @@ class AccountBankStatementLine(models.Model):
|
||||
reconciliation_overview = []
|
||||
|
||||
total_balance = liquidity_lines.balance
|
||||
total_amount_currency = liquidity_lines.amount_currency
|
||||
|
||||
# Step 1: Split 'lines_vals_list' into two batches:
|
||||
# - The existing account.move.lines that need to be reconciled with the statement line.
|
||||
@@ -1061,6 +1067,7 @@ class AccountBankStatementLine(models.Model):
|
||||
# Newly created account.move.line from scratch.
|
||||
line_vals = self._prepare_counterpart_move_line_vals(vals)
|
||||
total_balance += line_vals['debit'] - line_vals['credit']
|
||||
total_amount_currency += line_vals['amount_currency']
|
||||
|
||||
reconciliation_overview.append({
|
||||
'line_vals': line_vals,
|
||||
@@ -1072,6 +1079,7 @@ class AccountBankStatementLine(models.Model):
|
||||
for line, counterpart_vals in zip(existing_lines, to_process_vals):
|
||||
line_vals = self._prepare_counterpart_move_line_vals(counterpart_vals, move_line=line)
|
||||
balance = line_vals['debit'] - line_vals['credit']
|
||||
amount_currency = line_vals['amount_currency']
|
||||
|
||||
reconciliation_vals = {
|
||||
'line_vals': line_vals,
|
||||
@@ -1084,7 +1092,7 @@ class AccountBankStatementLine(models.Model):
|
||||
payment_vals = self.env['account.payment.register']\
|
||||
.with_context(active_model='account.move.line', active_ids=line.ids)\
|
||||
.create({
|
||||
'amount': abs(line_vals['amount_currency']) if line_vals['currency_id'] else abs(balance),
|
||||
'amount': abs(amount_currency) if line_vals['currency_id'] else abs(balance),
|
||||
'payment_date': self.date,
|
||||
'payment_type': 'inbound' if balance < 0.0 else 'outbound',
|
||||
'journal_id': self.journal_id.id,
|
||||
@@ -1127,21 +1135,46 @@ class AccountBankStatementLine(models.Model):
|
||||
reconciliation_overview.append(reconciliation_vals)
|
||||
|
||||
total_balance += balance
|
||||
total_amount_currency += amount_currency
|
||||
|
||||
# Step 3: If the journal entry is not yet balanced, create an open balance.
|
||||
# Step 3: Fix rounding issue due to currency conversions.
|
||||
# Add the remaining balance on the first encountered line starting with the custom ones.
|
||||
|
||||
if foreign_currency.is_zero(total_amount_currency) and not company_currency.is_zero(total_balance):
|
||||
vals = reconciliation_overview[0]['line_vals']
|
||||
new_balance = vals['debit'] - vals['credit'] - total_balance
|
||||
vals.update({
|
||||
'debit': new_balance if new_balance > 0.0 else 0.0,
|
||||
'credit': -new_balance if new_balance < 0.0 else 0.0,
|
||||
})
|
||||
total_balance = 0.0
|
||||
|
||||
# Step 4: If the journal entry is not yet balanced, create an open balance.
|
||||
|
||||
if self.company_currency_id.round(total_balance):
|
||||
if self.amount > 0:
|
||||
open_balance_account = self.partner_id.with_company(self.company_id).property_account_receivable_id
|
||||
else:
|
||||
open_balance_account = self.partner_id.with_company(self.company_id).property_account_payable_id
|
||||
|
||||
open_balance_vals = self._prepare_counterpart_move_line_vals({
|
||||
counterpart_vals = {
|
||||
'name': '%s: %s' % (self.payment_ref, _('Open Balance')),
|
||||
'account_id': open_balance_account.id,
|
||||
'balance': -total_balance,
|
||||
'currency_id': self.company_currency_id.id,
|
||||
})
|
||||
}
|
||||
|
||||
partner = self.partner_id or existing_lines.mapped('partner_id')[:1]
|
||||
if partner:
|
||||
if self.amount > 0:
|
||||
open_balance_account = partner.with_company(self.company_id).property_account_receivable_id
|
||||
else:
|
||||
open_balance_account = partner.with_company(self.company_id).property_account_payable_id
|
||||
|
||||
counterpart_vals['account_id'] = open_balance_account.id
|
||||
counterpart_vals['partner_id'] = partner.id
|
||||
else:
|
||||
if self.amount > 0:
|
||||
open_balance_account = self.company_id.partner_id.with_company(self.company_id).property_account_receivable_id
|
||||
else:
|
||||
open_balance_account = self.company_id.partner_id.with_company(self.company_id).property_account_payable_id
|
||||
counterpart_vals['account_id'] = open_balance_account.id
|
||||
|
||||
open_balance_vals = self._prepare_counterpart_move_line_vals(counterpart_vals)
|
||||
else:
|
||||
open_balance_vals = None
|
||||
|
||||
@@ -1215,6 +1248,7 @@ class AccountBankStatementLine(models.Model):
|
||||
|
||||
line_vals_list = [reconciliation_vals['line_vals'] for reconciliation_vals in reconciliation_overview]
|
||||
new_lines = self.env['account.move.line'].create(line_vals_list)
|
||||
new_lines = new_lines.with_context(skip_account_move_synchronization=True)
|
||||
for reconciliation_vals, line in zip(reconciliation_overview, new_lines):
|
||||
if reconciliation_vals.get('payment'):
|
||||
accounts = (self.journal_id.payment_debit_account_id, self.journal_id.payment_credit_account_id)
|
||||
@@ -1226,6 +1260,19 @@ class AccountBankStatementLine(models.Model):
|
||||
|
||||
(line + counterpart_line).reconcile()
|
||||
|
||||
# Assign partner if needed (for example, when reconciling a statement
|
||||
# line with no partner, with an invoice; assign the partner of this invoice)
|
||||
if not self.partner_id:
|
||||
rec_overview_partners = set(overview['counterpart_line'].partner_id.id
|
||||
for overview in reconciliation_overview
|
||||
if overview.get('counterpart_line') and overview['counterpart_line'].partner_id)
|
||||
if len(rec_overview_partners) == 1:
|
||||
self.line_ids.write({'partner_id': rec_overview_partners.pop()})
|
||||
|
||||
# Refresh analytic lines.
|
||||
self.move_id.line_ids.analytic_line_ids.unlink()
|
||||
self.move_id.line_ids.create_analytic_lines()
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# BUSINESS METHODS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@@ -43,6 +43,16 @@ class AccountJournal(models.Model):
|
||||
|
||||
def _default_alias_domain(self):
|
||||
return self.env["ir.config_parameter"].sudo().get_param("mail.catchall.domain")
|
||||
|
||||
def _default_invoice_reference_model(self):
|
||||
"""Get the invoice reference model according to the company's country."""
|
||||
country_code = self.env.company.country_id.code
|
||||
country_code = country_code and country_code.lower()
|
||||
if country_code:
|
||||
for model in self._fields['invoice_reference_model'].get_values(self.env):
|
||||
if model.startswith(country_code):
|
||||
return model
|
||||
return 'flectra'
|
||||
|
||||
name = fields.Char(string='Journal Name', required=True)
|
||||
code = fields.Char(string='Short Code', size=5, required=True, help="Shorter name used for display. The journal entries of this journal will also be named using this prefix by default.")
|
||||
@@ -67,7 +77,7 @@ class AccountJournal(models.Model):
|
||||
comodel_name='account.account', check_company=True, copy=False, ondelete='restrict',
|
||||
string='Default Account',
|
||||
domain="[('deprecated', '=', False), ('company_id', '=', company_id),"
|
||||
"('user_type_id', '=', default_account_type),"
|
||||
"'|', ('user_type_id', '=', default_account_type), ('user_type_id', 'in', type_control_ids),"
|
||||
"('user_type_id.type', 'not in', ('receivable', 'payable'))]")
|
||||
payment_debit_account_id = fields.Many2one(
|
||||
comodel_name='account.account', check_company=True, copy=False, ondelete='restrict',
|
||||
@@ -100,7 +110,7 @@ class AccountJournal(models.Model):
|
||||
sequence = fields.Integer(help='Used to order Journals in the dashboard view', default=10)
|
||||
|
||||
invoice_reference_type = fields.Selection(string='Communication Type', required=True, selection=[('none', 'Free'), ('partner', 'Based on Customer'), ('invoice', 'Based on Invoice')], default='invoice', help='You can set here the default communication that will appear on customer invoices, once validated, to help the customer to refer to that particular invoice when making the payment.')
|
||||
invoice_reference_model = fields.Selection(string='Communication Standard', required=True, selection=[('flectra', 'Flectra'),('euro', 'European')], default='flectra', help="You can choose different models for each type of reference. The default one is the Flectra reference.")
|
||||
invoice_reference_model = fields.Selection(string='Communication Standard', required=True, selection=[('flectra', 'Flectra'),('euro', 'European')], default=_default_invoice_reference_model, help="You can choose different models for each type of reference. The default one is the Flectra reference.")
|
||||
|
||||
#groups_id = fields.Many2many('res.groups', 'account_journal_group_rel', 'journal_id', 'group_id', string='Groups')
|
||||
currency_id = fields.Many2one('res.currency', help='The currency used to enter statement', string="Currency")
|
||||
@@ -212,9 +222,7 @@ class AccountJournal(models.Model):
|
||||
}
|
||||
|
||||
for journal in self:
|
||||
if journal.type in ('bank', 'cash'):
|
||||
journal.default_account_type = True
|
||||
elif journal.type in default_account_id_types:
|
||||
if journal.type in default_account_id_types:
|
||||
journal.default_account_type = self.env.ref(default_account_id_types[journal.type]).id
|
||||
else:
|
||||
journal.default_account_type = False
|
||||
@@ -309,46 +317,6 @@ class AccountJournal(models.Model):
|
||||
if self._cr.fetchone():
|
||||
raise UserError(_("You can't change the company of your journal since there are some journal entries linked to it."))
|
||||
|
||||
@api.constrains('default_account_id', 'payment_debit_account_id', 'payment_credit_account_id')
|
||||
def _check_journal_not_shared_accounts(self):
|
||||
liquidity_journals = self.filtered(lambda journal: journal.type in ('bank', 'cash'))
|
||||
|
||||
accounts = liquidity_journals.default_account_id \
|
||||
+ liquidity_journals.payment_debit_account_id \
|
||||
+ liquidity_journals.payment_credit_account_id
|
||||
|
||||
if not accounts:
|
||||
return
|
||||
|
||||
self.env['account.journal'].flush([
|
||||
'default_account_id',
|
||||
'payment_debit_account_id',
|
||||
'payment_credit_account_id',
|
||||
])
|
||||
self._cr.execute('''
|
||||
SELECT
|
||||
account.name,
|
||||
ARRAY_AGG(DISTINCT journal.name) AS journal_names
|
||||
FROM account_account account
|
||||
LEFT JOIN account_journal journal ON
|
||||
journal.default_account_id = account.id
|
||||
OR
|
||||
journal.payment_debit_account_id = account.id
|
||||
OR
|
||||
journal.payment_credit_account_id = account.id
|
||||
WHERE account.id IN %s
|
||||
AND journal.type IN ('bank', 'cash')
|
||||
GROUP BY account.name
|
||||
HAVING COUNT(DISTINCT journal.id) > 1
|
||||
''', [tuple(accounts.ids)])
|
||||
res = self._cr.fetchone()
|
||||
if res:
|
||||
raise ValidationError(_(
|
||||
"The account %(account_name)s can't be shared between multiple journals: %(journals)s",
|
||||
account_name=res[0],
|
||||
journals=', '.join(res[1])
|
||||
))
|
||||
|
||||
@api.constrains('type', 'default_account_id')
|
||||
def _check_type_default_account_id_type(self):
|
||||
for journal in self:
|
||||
@@ -446,8 +414,9 @@ class AccountJournal(models.Model):
|
||||
result = super(AccountJournal, self).write(vals)
|
||||
|
||||
# Ensure the liquidity accounts are sharing the same foreign currency.
|
||||
for journal in self.filtered(lambda journal: journal.type in ('bank', 'cash')):
|
||||
journal.default_account_id.currency_id = journal.currency_id
|
||||
if 'currency_id' in vals:
|
||||
for journal in self.filtered(lambda journal: journal.type in ('bank', 'cash')):
|
||||
journal.default_account_id.currency_id = journal.currency_id
|
||||
|
||||
# Create the bank_account_id if necessary
|
||||
if 'bank_acc_number' in vals:
|
||||
@@ -623,7 +592,16 @@ class AccountJournal(models.Model):
|
||||
invoices = self.env['account.move']
|
||||
for attachment in attachments:
|
||||
attachment.write({'res_model': 'mail.compose.message'})
|
||||
invoices += self._create_invoice_from_single_attachment(attachment)
|
||||
decoders = self.env['account.move']._get_create_invoice_from_attachment_decoders()
|
||||
invoice = False
|
||||
for decoder in sorted(decoders, key=lambda d: d[0]):
|
||||
invoice = decoder[1](attachment)
|
||||
if invoice:
|
||||
break
|
||||
if not invoice:
|
||||
invoice = self.env['account.move'].create({})
|
||||
invoice.with_context(no_new_invoice=True).message_post(attachment_ids=[attachment.id])
|
||||
invoices += invoice
|
||||
|
||||
action_vals = {
|
||||
'name': _('Generated Documents'),
|
||||
@@ -642,12 +620,11 @@ class AccountJournal(models.Model):
|
||||
def _create_invoice_from_single_attachment(self, attachment):
|
||||
""" Creates an invoice and post the attachment. If the related modules
|
||||
are installed, it will trigger OCR or the import from the EDI.
|
||||
DEPRECATED : use create_invoice_from_attachment instead
|
||||
|
||||
:returns: the created invoice.
|
||||
"""
|
||||
invoice = self.env['account.move'].create({})
|
||||
invoice.message_post(attachment_ids=[attachment.id])
|
||||
return invoice
|
||||
return self.create_invoice_from_attachment(attachment.ids)
|
||||
|
||||
def _create_secure_sequence(self, sequence_fields):
|
||||
"""This function creates a no_gap sequence on each journal in self that will ensure
|
||||
@@ -682,13 +659,14 @@ class AccountJournal(models.Model):
|
||||
a logic based on accounts.
|
||||
|
||||
:param domain: An additional domain to be applied on the account.move.line model.
|
||||
:return: The balance expressed in the journal's currency.
|
||||
:return: Tuple having balance expressed in journal's currency
|
||||
along with the total number of move lines having the same account as of the journal's default account.
|
||||
'''
|
||||
self.ensure_one()
|
||||
self.env['account.move.line'].check_access_rights('read')
|
||||
|
||||
if not self.default_account_id:
|
||||
return 0.0
|
||||
return 0.0, 0
|
||||
|
||||
domain = (domain or []) + [
|
||||
('account_id', 'in', tuple(self.default_account_id.ids)),
|
||||
|
||||
@@ -480,7 +480,7 @@ class account_journal(models.Model):
|
||||
if self.type == 'sale':
|
||||
action['domain'] = [(domain_type_field, 'in', ('out_invoice', 'out_refund', 'out_receipt'))]
|
||||
elif self.type == 'purchase':
|
||||
action['domain'] = [(domain_type_field, 'in', ('in_invoice', 'in_refund', 'in_receipt'))]
|
||||
action['domain'] = [(domain_type_field, 'in', ('in_invoice', 'in_refund', 'in_receipt', 'entry'))]
|
||||
|
||||
return action
|
||||
|
||||
@@ -518,7 +518,7 @@ class account_journal(models.Model):
|
||||
ctx = dict(self.env.context, default_journal_id=self.id)
|
||||
if ctx.get('search_default_journal', False):
|
||||
ctx.update(search_default_journal_id=self.id)
|
||||
del ctx['search_default_journal'] # otherwise it will do a useless groupby in bank statements
|
||||
ctx['search_default_journal'] = False # otherwise it will do a useless groupby in bank statements
|
||||
ctx.pop('group_by', None)
|
||||
action = self.env['ir.actions.act_window']._for_xml_id(f"account.{action_name}")
|
||||
action['context'] = ctx
|
||||
|
||||
@@ -375,6 +375,8 @@ class AccountMove(models.Model):
|
||||
if new_currency != self.currency_id:
|
||||
self.currency_id = new_currency
|
||||
self._onchange_currency()
|
||||
if self.state == 'draft' and self._get_last_sequence() and self.name and self.name != '/':
|
||||
self.name = '/'
|
||||
|
||||
@api.onchange('partner_id')
|
||||
def _onchange_partner_id(self):
|
||||
@@ -446,7 +448,7 @@ class AccountMove(models.Model):
|
||||
@api.onchange('payment_reference')
|
||||
def _onchange_payment_reference(self):
|
||||
for line in self.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable')):
|
||||
line.name = self.payment_reference
|
||||
line.name = self.payment_reference or ''
|
||||
|
||||
@api.onchange('invoice_vendor_bill_id')
|
||||
def _onchange_invoice_vendor_bill(self):
|
||||
@@ -617,19 +619,22 @@ class AccountMove(models.Model):
|
||||
'tax_base_amount': 0.0,
|
||||
'grouping_dict': False,
|
||||
}
|
||||
self.line_ids -= to_remove
|
||||
if not recompute_tax_base_amount:
|
||||
self.line_ids -= to_remove
|
||||
|
||||
# ==== Mount base lines ====
|
||||
for line in self.line_ids.filtered(lambda line: not line.tax_repartition_line_id):
|
||||
# Don't call compute_all if there is no tax.
|
||||
if not line.tax_ids:
|
||||
line.tax_tag_ids = [(5, 0, 0)]
|
||||
if not recompute_tax_base_amount:
|
||||
line.tax_tag_ids = [(5, 0, 0)]
|
||||
continue
|
||||
|
||||
compute_all_vals = _compute_base_line_taxes(line)
|
||||
|
||||
# Assign tags on base line
|
||||
line.tax_tag_ids = compute_all_vals['base_tags'] or [(5, 0, 0)]
|
||||
if not recompute_tax_base_amount:
|
||||
line.tax_tag_ids = compute_all_vals['base_tags'] or [(5, 0, 0)]
|
||||
|
||||
tax_exigible = True
|
||||
for tax_vals in compute_all_vals['taxes']:
|
||||
@@ -651,20 +656,22 @@ class AccountMove(models.Model):
|
||||
taxes_map_entry['amount'] += tax_vals['amount']
|
||||
taxes_map_entry['tax_base_amount'] += self._get_base_amount_to_display(tax_vals['base'], tax_repartition_line, tax_vals['group'])
|
||||
taxes_map_entry['grouping_dict'] = grouping_dict
|
||||
line.tax_exigible = tax_exigible
|
||||
if not recompute_tax_base_amount:
|
||||
line.tax_exigible = tax_exigible
|
||||
|
||||
# ==== Process taxes_map ====
|
||||
for taxes_map_entry in taxes_map.values():
|
||||
# The tax line is no longer used in any base lines, drop it.
|
||||
if taxes_map_entry['tax_line'] and not taxes_map_entry['grouping_dict']:
|
||||
self.line_ids -= taxes_map_entry['tax_line']
|
||||
if not recompute_tax_base_amount:
|
||||
self.line_ids -= taxes_map_entry['tax_line']
|
||||
continue
|
||||
|
||||
currency = self.env['res.currency'].browse(taxes_map_entry['grouping_dict']['currency_id'])
|
||||
|
||||
# Don't create tax lines with zero balance.
|
||||
if currency.is_zero(taxes_map_entry['amount']):
|
||||
if taxes_map_entry['tax_line']:
|
||||
if taxes_map_entry['tax_line'] and not recompute_tax_base_amount:
|
||||
self.line_ids -= taxes_map_entry['tax_line']
|
||||
continue
|
||||
|
||||
@@ -672,8 +679,9 @@ class AccountMove(models.Model):
|
||||
tax_base_amount = currency._convert(taxes_map_entry['tax_base_amount'], self.company_currency_id, self.company_id, self.date or fields.Date.context_today(self))
|
||||
|
||||
# Recompute only the tax_base_amount.
|
||||
if taxes_map_entry['tax_line'] and recompute_tax_base_amount:
|
||||
taxes_map_entry['tax_line'].tax_base_amount = tax_base_amount
|
||||
if recompute_tax_base_amount:
|
||||
if taxes_map_entry['tax_line']:
|
||||
taxes_map_entry['tax_line'].tax_base_amount = tax_base_amount
|
||||
continue
|
||||
|
||||
balance = currency._convert(
|
||||
@@ -937,7 +945,8 @@ class AccountMove(models.Model):
|
||||
# Recompute amls: update existing line or create new one for each payment term.
|
||||
new_terms_lines = self.env['account.move.line']
|
||||
for date_maturity, balance, amount_currency in to_compute:
|
||||
if self.journal_id.company_id.currency_id.is_zero(balance) and len(to_compute) > 1:
|
||||
currency = self.journal_id.company_id.currency_id
|
||||
if currency and currency.is_zero(balance) and len(to_compute) > 1:
|
||||
continue
|
||||
|
||||
if existing_terms_lines_index < len(existing_terms_lines):
|
||||
@@ -1046,7 +1055,8 @@ class AccountMove(models.Model):
|
||||
def _compute_suitable_journal_ids(self):
|
||||
for m in self:
|
||||
journal_type = m.invoice_filter_type_domain or 'general'
|
||||
domain = [('company_id', '=', m.company_id.id), ('type', '=', journal_type)]
|
||||
company_id = m.company_id.id or self.env.company.id
|
||||
domain = [('company_id', '=', company_id), ('type', '=', journal_type)]
|
||||
m.suitable_journal_ids = self.env['account.journal'].search(domain)
|
||||
|
||||
@api.depends('posted_before', 'state', 'journal_id', 'date')
|
||||
@@ -1098,7 +1108,11 @@ class AccountMove(models.Model):
|
||||
final_batches = []
|
||||
for journal_group in grouped.values():
|
||||
for date_group in journal_group.values():
|
||||
if not final_batches or final_batches[-1]['format'] != date_group['format']:
|
||||
if (
|
||||
not final_batches
|
||||
or final_batches[-1]['format'] != date_group['format']
|
||||
or dict(final_batches[-1]['format_values'], seq=0) != dict(date_group['format_values'], seq=0)
|
||||
):
|
||||
final_batches += [date_group]
|
||||
elif date_group['reset'] == 'never':
|
||||
final_batches[-1]['records'] += date_group['records']
|
||||
@@ -1140,6 +1154,9 @@ class AccountMove(models.Model):
|
||||
|
||||
if not relaxed:
|
||||
domain = [('journal_id', '=', self.journal_id.id), ('id', '!=', self.id or self._origin.id), ('name', 'not in', ('/', False))]
|
||||
if self.journal_id.refund_sequence:
|
||||
refund_types = ('out_refund', 'in_refund')
|
||||
domain += [('move_type', 'in' if self.move_type in refund_types else 'not in', refund_types)]
|
||||
reference_move_name = self.search(domain + [('date', '<=', self.date)], order='date desc', limit=1).name
|
||||
if not reference_move_name:
|
||||
reference_move_name = self.search(domain, order='date asc', limit=1).name
|
||||
@@ -1147,9 +1164,15 @@ class AccountMove(models.Model):
|
||||
if sequence_number_reset == 'year':
|
||||
where_string += " AND date_trunc('year', date::timestamp without time zone) = date_trunc('year', %(date)s) "
|
||||
param['date'] = self.date
|
||||
param['anti_regex'] = re.sub(r"\?P<\w+>", "?:", self._sequence_monthly_regex.split('(?P<seq>')[0]) + '$'
|
||||
elif sequence_number_reset == 'month':
|
||||
where_string += " AND date_trunc('month', date::timestamp without time zone) = date_trunc('month', %(date)s) "
|
||||
param['date'] = self.date
|
||||
else:
|
||||
param['anti_regex'] = re.sub(r"\?P<\w+>", "?:", self._sequence_yearly_regex.split('(?P<seq>')[0]) + '$'
|
||||
|
||||
if param.get('anti_regex') and not self.journal_id.sequence_override_regex:
|
||||
where_string += " AND sequence_prefix !~ %(anti_regex)s "
|
||||
|
||||
if self.journal_id.refund_sequence:
|
||||
if self.move_type in ('out_refund', 'in_refund'):
|
||||
@@ -1207,8 +1230,10 @@ class AccountMove(models.Model):
|
||||
return 'paid'
|
||||
|
||||
@api.depends(
|
||||
'line_ids.matched_debit_ids.debit_move_id.move_id.payment_id.is_matched',
|
||||
'line_ids.matched_debit_ids.debit_move_id.move_id.line_ids.amount_residual',
|
||||
'line_ids.matched_debit_ids.debit_move_id.move_id.line_ids.amount_residual_currency',
|
||||
'line_ids.matched_credit_ids.credit_move_id.move_id.payment_id.is_matched',
|
||||
'line_ids.matched_credit_ids.credit_move_id.move_id.line_ids.amount_residual',
|
||||
'line_ids.matched_credit_ids.credit_move_id.move_id.line_ids.amount_residual_currency',
|
||||
'line_ids.debit',
|
||||
@@ -1669,6 +1694,14 @@ class AccountMove(models.Model):
|
||||
raise UserError(message)
|
||||
return True
|
||||
|
||||
@api.constrains('move_type', 'journal_id')
|
||||
def _check_journal_type(self):
|
||||
for record in self:
|
||||
journal_type = record.journal_id.type
|
||||
|
||||
if record.is_sale_document() and journal_type != 'sale' or record.is_purchase_document() and journal_type != 'purchase':
|
||||
raise ValidationError(_("The chosen journal has a type that is not compatible with your invoice type. Sales operations should go to 'sale' journals, and purchase operations to 'purchase' ones."))
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# LOW-LEVEL METHODS
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -1725,30 +1758,15 @@ class AccountMove(models.Model):
|
||||
'''
|
||||
new_vals_list = []
|
||||
for vals in vals_list:
|
||||
if not vals.get('invoice_line_ids'):
|
||||
new_vals_list.append(vals)
|
||||
continue
|
||||
if vals.get('line_ids'):
|
||||
vals.pop('invoice_line_ids', None)
|
||||
new_vals_list.append(vals)
|
||||
continue
|
||||
if not vals.get('move_type') and not self._context.get('default_move_type'):
|
||||
vals.pop('invoice_line_ids', None)
|
||||
new_vals_list.append(vals)
|
||||
continue
|
||||
vals['move_type'] = vals.get('move_type', self._context.get('default_move_type', 'entry'))
|
||||
if not vals['move_type'] in self.get_invoice_types(include_receipts=True):
|
||||
new_vals_list.append(vals)
|
||||
continue
|
||||
|
||||
vals['line_ids'] = vals.pop('invoice_line_ids')
|
||||
vals = dict(vals)
|
||||
|
||||
if vals.get('invoice_date') and not vals.get('date'):
|
||||
vals['date'] = vals['invoice_date']
|
||||
|
||||
ctx_vals = {'default_move_type': vals.get('move_type') or self._context.get('default_move_type')}
|
||||
if vals.get('currency_id'):
|
||||
ctx_vals['default_currency_id'] = vals['currency_id']
|
||||
default_move_type = vals.get('move_type') or self._context.get('default_move_type')
|
||||
ctx_vals = {}
|
||||
if default_move_type:
|
||||
ctx_vals['default_move_type'] = default_move_type
|
||||
if vals.get('journal_id'):
|
||||
ctx_vals['default_journal_id'] = vals['journal_id']
|
||||
# reorder the companies in the context so that the company of the journal
|
||||
@@ -1759,9 +1777,21 @@ class AccountMove(models.Model):
|
||||
reordered_companies = sorted(allowed_companies, key=lambda cid: cid != journal_company.id)
|
||||
ctx_vals['allowed_company_ids'] = reordered_companies
|
||||
self_ctx = self.with_context(**ctx_vals)
|
||||
new_vals = self_ctx._add_missing_default_values(vals)
|
||||
vals = self_ctx._add_missing_default_values(vals)
|
||||
|
||||
move = self_ctx.new(new_vals)
|
||||
is_invoice = vals.get('move_type') in self.get_invoice_types(include_receipts=True)
|
||||
|
||||
if 'line_ids' in vals:
|
||||
vals.pop('invoice_line_ids', None)
|
||||
new_vals_list.append(vals)
|
||||
continue
|
||||
|
||||
if is_invoice and 'invoice_line_ids' in vals:
|
||||
vals['line_ids'] = vals['invoice_line_ids']
|
||||
|
||||
vals.pop('invoice_line_ids', None)
|
||||
|
||||
move = self_ctx.new(vals)
|
||||
new_vals_list.append(move._move_autocomplete_invoice_lines_values())
|
||||
|
||||
return new_vals_list
|
||||
@@ -1943,7 +1973,6 @@ class AccountMove(models.Model):
|
||||
values = {
|
||||
'move': self,
|
||||
'to_process_lines': self.env['account.move.line'],
|
||||
'payment_term_lines': self.env['account.move.line'],
|
||||
'total_balance': 0.0,
|
||||
'total_residual': 0.0,
|
||||
'total_amount_currency': 0.0,
|
||||
@@ -1978,17 +2007,9 @@ class AccountMove(models.Model):
|
||||
# Don't support the case where there is multiple involved currencies.
|
||||
return None
|
||||
|
||||
if len(values['payment_term_lines'].account_id) > 1:
|
||||
# Don't support the case where there is multiple involved receivable/payable accounts.
|
||||
# It could lead to some weird situation regarding the cash basis exchange difference
|
||||
# journal items.
|
||||
return None
|
||||
|
||||
# Determine is the move is now fully paid.
|
||||
if values['currency'] == self.company_id.currency_id:
|
||||
values['is_fully_paid'] = values['currency'].is_zero(values['total_residual'])
|
||||
else:
|
||||
values['is_fully_paid'] = values['currency'].is_zero(values['total_residual_currency'])
|
||||
values['is_fully_paid'] = self.company_id.currency_id.is_zero(values['total_residual']) \
|
||||
or values['currency'].is_zero(values['total_residual_currency'])
|
||||
|
||||
return values
|
||||
|
||||
@@ -2502,7 +2523,8 @@ class AccountMove(models.Model):
|
||||
return action
|
||||
|
||||
def action_post(self):
|
||||
return self._post(soft=False)
|
||||
self._post(soft=False)
|
||||
return False
|
||||
|
||||
def js_assign_outstanding_line(self, line_id):
|
||||
''' Called by the 'payment' widget to reconcile a suggested journal item to the present
|
||||
@@ -2837,6 +2859,55 @@ class AccountMove(models.Model):
|
||||
|
||||
return rslt
|
||||
|
||||
def _message_post_after_hook(self, new_message, message_values):
|
||||
# OVERRIDE
|
||||
# When posting a message, check the attachment to see if it's an invoice and update with the imported data.
|
||||
res = super()._message_post_after_hook(new_message, message_values)
|
||||
|
||||
attachments = new_message.attachment_ids
|
||||
if len(self) != 1 or not attachments or self.env.context.get('no_new_invoice') or not self.is_invoice(include_receipts=True):
|
||||
return res
|
||||
|
||||
flectrabot = self.env.ref('base.partner_root')
|
||||
if attachments and self.state != 'draft':
|
||||
self.message_post(body=_('The invoice is not a draft, it was not updated from the attachment.'),
|
||||
message_type='comment',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
author_id=flectrabot.id)
|
||||
return res
|
||||
if attachments and self.line_ids:
|
||||
self.message_post(body=_('The invoice already contains lines, it was not updated from the attachment.'),
|
||||
message_type='comment',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
author_id=flectrabot.id)
|
||||
return res
|
||||
|
||||
decoders = self.env['account.move']._get_update_invoice_from_attachment_decoders(self)
|
||||
for decoder in sorted(decoders, key=lambda d: d[0]):
|
||||
# start with message_main_attachment_id, that way if OCR is installed, only that one will be parsed.
|
||||
# this is based on the fact that the ocr will be the last decoder.
|
||||
for attachment in attachments.sorted(lambda x: x != self.message_main_attachment_id):
|
||||
invoice = decoder[1](attachment, self)
|
||||
if invoice:
|
||||
return res
|
||||
|
||||
return res
|
||||
|
||||
def _get_create_invoice_from_attachment_decoders(self):
|
||||
""" Returns a list of method that are able to create an invoice from an attachment and a priority.
|
||||
|
||||
:returns: A list of tuples (priority, method) where method takes an attachment as parameter.
|
||||
"""
|
||||
return []
|
||||
|
||||
def _get_update_invoice_from_attachment_decoders(self, invoice):
|
||||
""" Returns a list of method that are able to create an invoice from an attachment and a priority.
|
||||
|
||||
:param invoice: The invoice on which to update the data.
|
||||
:returns: A list of tuples (priority, method) where method takes an attachment as parameter.
|
||||
"""
|
||||
return []
|
||||
|
||||
class AccountMoveLine(models.Model):
|
||||
_name = "account.move.line"
|
||||
_description = "Journal Item"
|
||||
@@ -2915,7 +2986,12 @@ class AccountMoveLine(models.Model):
|
||||
help="The bank statement used for bank reconciliation")
|
||||
|
||||
# ==== Tax fields ====
|
||||
tax_ids = fields.Many2many('account.tax', string='Taxes', help="Taxes that apply on the base amount", check_company=True)
|
||||
tax_ids = fields.Many2many(
|
||||
comodel_name='account.tax',
|
||||
string="Taxes",
|
||||
context={'active_test': False},
|
||||
check_company=True,
|
||||
help="Taxes that apply on the base amount")
|
||||
tax_line_id = fields.Many2one('account.tax', string='Originator Tax', ondelete='restrict', store=True,
|
||||
compute='_compute_tax_line_id', help="Indicates that this journal item is a tax line")
|
||||
tax_group_id = fields.Many2one(related='tax_line_id.tax_group_id', string='Originator tax group',
|
||||
@@ -3145,7 +3221,7 @@ class AccountMoveLine(models.Model):
|
||||
# adapt the price_unit to the new tax.
|
||||
# E.g. mapping a 10% price-included tax to a 20% price-included tax for a price_unit of 110 should preserve
|
||||
# 100 as balance but set 120 as price_unit.
|
||||
if self.tax_ids and self.move_id.fiscal_position_id:
|
||||
if self.tax_ids and self.move_id.fiscal_position_id and self.move_id.fiscal_position_id.tax_ids:
|
||||
price_subtotal = self._get_price_total_and_subtotal()['price_subtotal']
|
||||
self.tax_ids = self.move_id.fiscal_position_id.map_tax(
|
||||
self.tax_ids._origin,
|
||||
@@ -3390,7 +3466,7 @@ class AccountMoveLine(models.Model):
|
||||
|
||||
# Convert the unit price to the invoice's currency.
|
||||
company = line.move_id.company_id
|
||||
line.price_unit = company.currency_id._convert(line.price_unit, line.move_id.currency_id, company, line.move_id.date)
|
||||
line.price_unit = company.currency_id._convert(line.price_unit, line.move_id.currency_id, company, line.move_id.date, round=False)
|
||||
|
||||
@api.onchange('product_uom_id')
|
||||
def _onchange_uom_id(self):
|
||||
@@ -3407,7 +3483,7 @@ class AccountMoveLine(models.Model):
|
||||
|
||||
# Convert the unit price to the invoice's currency.
|
||||
company = self.move_id.company_id
|
||||
self.price_unit = company.currency_id._convert(price_unit, self.move_id.currency_id, company, self.move_id.date)
|
||||
self.price_unit = company.currency_id._convert(price_unit, self.move_id.currency_id, company, self.move_id.date, round=False)
|
||||
|
||||
@api.onchange('account_id')
|
||||
def _onchange_account_id(self):
|
||||
@@ -3627,10 +3703,10 @@ class AccountMoveLine(models.Model):
|
||||
raise UserError(_('You cannot use this account (%s) in this journal, check the field \'Allowed Journals\' on the related account.', account.display_name))
|
||||
|
||||
failed_check = False
|
||||
if journal.type_control_ids or journal.account_control_ids:
|
||||
if (journal.type_control_ids - journal.default_account_id.user_type_id) or journal.account_control_ids:
|
||||
failed_check = True
|
||||
if journal.type_control_ids:
|
||||
failed_check = account.user_type_id not in journal.type_control_ids
|
||||
failed_check = account.user_type_id not in (journal.type_control_ids - journal.default_account_id.user_type_id)
|
||||
if failed_check and journal.account_control_ids:
|
||||
failed_check = account not in journal.account_control_ids
|
||||
|
||||
@@ -3820,56 +3896,26 @@ class AccountMoveLine(models.Model):
|
||||
or (account_type != 'payable' and account_to_write.user_type_id.type == 'payable'):
|
||||
raise UserError(_("You can only set an account having the payable type on payment terms lines for vendor bill."))
|
||||
|
||||
# Get all tracked fields (without related fields because these fields must be manage on their own model)
|
||||
tracking_fields = []
|
||||
for value in vals:
|
||||
field = self._fields[value]
|
||||
if hasattr(field, 'related') and field.related:
|
||||
continue # We don't want to track related field.
|
||||
if hasattr(field, 'tracking') and field.tracking:
|
||||
tracking_fields.append(value)
|
||||
ref_fields = self.env['account.move.line'].fields_get(tracking_fields)
|
||||
# Tracking stuff can be skipped for perfs using tracking_disable context key
|
||||
if not self.env.context.get('tracking_disable', False):
|
||||
# Get all tracked fields (without related fields because these fields must be manage on their own model)
|
||||
tracking_fields = []
|
||||
for value in vals:
|
||||
field = self._fields[value]
|
||||
if hasattr(field, 'related') and field.related:
|
||||
continue # We don't want to track related field.
|
||||
if hasattr(field, 'tracking') and field.tracking:
|
||||
tracking_fields.append(value)
|
||||
ref_fields = self.env['account.move.line'].fields_get(tracking_fields)
|
||||
|
||||
# Get initial values for each line
|
||||
move_initial_values = {}
|
||||
for line in self.filtered(lambda l: l.move_id.posted_before): # Only lines with posted once move.
|
||||
for field in tracking_fields:
|
||||
# Group initial values by move_id
|
||||
if line.move_id.id not in move_initial_values:
|
||||
move_initial_values[line.move_id.id] = {}
|
||||
move_initial_values[line.move_id.id].update({field: line[field]})
|
||||
|
||||
# Create the dict for the message post
|
||||
tracking_values = {} # Tracking values to write in the message post
|
||||
for move_id, modified_lines in move_initial_values.items():
|
||||
tmp_move = {move_id: []}
|
||||
for line in self.filtered(lambda l: l.move_id.id == move_id):
|
||||
changes, tracking_value_ids = line._mail_track(ref_fields, modified_lines) # Return a tuple like (changed field, ORM command)
|
||||
tmp = {'line_id': line.id}
|
||||
if tracking_value_ids:
|
||||
selected_field = tracking_value_ids[0][2] # Get the last element of the tuple in the list of ORM command. (changed, [(0, 0, THIS)])
|
||||
tmp.update({
|
||||
**{'field_name': selected_field.get('field_desc')},
|
||||
**self._get_formated_values(selected_field)
|
||||
})
|
||||
elif changes:
|
||||
field_name = line._fields[changes.pop()].string # Get the field name
|
||||
tmp.update({
|
||||
'error': True,
|
||||
'field_error': field_name
|
||||
})
|
||||
else:
|
||||
continue
|
||||
tmp_move[move_id].append(tmp)
|
||||
if len(tmp_move[move_id]) > 0:
|
||||
tracking_values.update(tmp_move)
|
||||
|
||||
# Write in the chatter.
|
||||
for move in self.mapped('move_id'):
|
||||
fields = tracking_values.get(move.id, [])
|
||||
if len(fields) > 0:
|
||||
msg = self._get_tracking_field_string(tracking_values.get(move.id))
|
||||
move.message_post(body=msg) # Write for each concerned move the message in the chatter
|
||||
# Get initial values for each line
|
||||
move_initial_values = {}
|
||||
for line in self.filtered(lambda l: l.move_id.posted_before): # Only lines with posted once move.
|
||||
for field in tracking_fields:
|
||||
# Group initial values by move_id
|
||||
if line.move_id.id not in move_initial_values:
|
||||
move_initial_values[line.move_id.id] = {}
|
||||
move_initial_values[line.move_id.id].update({field: line[field]})
|
||||
|
||||
result = True
|
||||
for line in self:
|
||||
@@ -3916,6 +3962,39 @@ class AccountMoveLine(models.Model):
|
||||
|
||||
self.mapped('move_id')._synchronize_business_models({'line_ids'})
|
||||
|
||||
if not self.env.context.get('tracking_disable', False):
|
||||
# Create the dict for the message post
|
||||
tracking_values = {} # Tracking values to write in the message post
|
||||
for move_id, modified_lines in move_initial_values.items():
|
||||
tmp_move = {move_id: []}
|
||||
for line in self.filtered(lambda l: l.move_id.id == move_id):
|
||||
changes, tracking_value_ids = line._mail_track(ref_fields, modified_lines) # Return a tuple like (changed field, ORM command)
|
||||
tmp = {'line_id': line.id}
|
||||
if tracking_value_ids:
|
||||
selected_field = tracking_value_ids[0][2] # Get the last element of the tuple in the list of ORM command. (changed, [(0, 0, THIS)])
|
||||
tmp.update({
|
||||
**{'field_name': selected_field.get('field_desc')},
|
||||
**self._get_formated_values(selected_field)
|
||||
})
|
||||
elif changes:
|
||||
field_name = line._fields[changes.pop()].string # Get the field name
|
||||
tmp.update({
|
||||
'error': True,
|
||||
'field_error': field_name
|
||||
})
|
||||
else:
|
||||
continue
|
||||
tmp_move[move_id].append(tmp)
|
||||
if len(tmp_move[move_id]) > 0:
|
||||
tracking_values.update(tmp_move)
|
||||
|
||||
# Write in the chatter.
|
||||
for move in self.mapped('move_id'):
|
||||
fields = tracking_values.get(move.id, [])
|
||||
if len(fields) > 0:
|
||||
msg = self._get_tracking_field_string(tracking_values.get(move.id))
|
||||
move.message_post(body=msg) # Write for each concerned move the message in the chatter
|
||||
|
||||
return result
|
||||
|
||||
def _valid_field_parameter(self, field, name):
|
||||
@@ -4042,8 +4121,8 @@ class AccountMoveLine(models.Model):
|
||||
|
||||
:return: A recordset of account.partial.reconcile.
|
||||
'''
|
||||
debit_lines = iter(self.filtered('debit'))
|
||||
credit_lines = iter(self.filtered('credit'))
|
||||
debit_lines = iter(self.filtered(lambda line: line.balance > 0.0 or line.amount_currency > 0.0))
|
||||
credit_lines = iter(self.filtered(lambda line: line.balance < 0.0 or line.amount_currency < 0.0))
|
||||
debit_line = None
|
||||
credit_line = None
|
||||
|
||||
@@ -4087,17 +4166,21 @@ class AccountMoveLine(models.Model):
|
||||
credit_line_currency = credit_line.company_currency_id
|
||||
|
||||
min_amount_residual = min(debit_amount_residual, -credit_amount_residual)
|
||||
has_debit_residual_left = not debit_line.company_currency_id.is_zero(debit_amount_residual) and debit_amount_residual > 0.0
|
||||
has_credit_residual_left = not credit_line.company_currency_id.is_zero(credit_amount_residual) and credit_amount_residual < 0.0
|
||||
has_debit_residual_curr_left = not debit_line_currency.is_zero(debit_amount_residual_currency) and debit_amount_residual_currency > 0.0
|
||||
has_credit_residual_curr_left = not credit_line_currency.is_zero(credit_amount_residual_currency) and credit_amount_residual_currency < 0.0
|
||||
|
||||
if debit_line_currency == credit_line_currency:
|
||||
# Reconcile on the same currency.
|
||||
|
||||
# The debit line is now fully reconciled.
|
||||
if debit_line_currency.is_zero(debit_amount_residual_currency) or debit_amount_residual_currency < 0.0:
|
||||
if not has_debit_residual_curr_left and (has_credit_residual_curr_left or not has_debit_residual_left):
|
||||
debit_line = None
|
||||
continue
|
||||
|
||||
# The credit line is now fully reconciled.
|
||||
if credit_line_currency.is_zero(credit_amount_residual_currency) or credit_amount_residual_currency > 0.0:
|
||||
if not has_credit_residual_curr_left and (has_debit_residual_curr_left or not has_credit_residual_left):
|
||||
credit_line = None
|
||||
continue
|
||||
|
||||
@@ -4109,12 +4192,12 @@ class AccountMoveLine(models.Model):
|
||||
# Reconcile on the company's currency.
|
||||
|
||||
# The debit line is now fully reconciled.
|
||||
if debit_line.company_currency_id.is_zero(debit_amount_residual) or debit_amount_residual < 0.0:
|
||||
if not has_debit_residual_left and (has_credit_residual_left or not has_debit_residual_curr_left):
|
||||
debit_line = None
|
||||
continue
|
||||
|
||||
# The credit line is now fully reconciled.
|
||||
if credit_line.company_currency_id.is_zero(credit_amount_residual) or credit_amount_residual > 0.0:
|
||||
if not has_credit_residual_left and (has_debit_residual_left or not has_credit_residual_curr_left):
|
||||
credit_line = None
|
||||
continue
|
||||
|
||||
@@ -4246,7 +4329,7 @@ class AccountMoveLine(models.Model):
|
||||
:param exchange_diff_move_vals: The current vals of the exchange difference journal entry.
|
||||
'''
|
||||
for move in lines.move_id:
|
||||
transfer_account_vals_to_fix = {}
|
||||
account_vals_to_fix = {}
|
||||
|
||||
move_values = move._collect_tax_cash_basis_values()
|
||||
|
||||
@@ -4255,40 +4338,42 @@ class AccountMoveLine(models.Model):
|
||||
if not move_values or not move_values['is_fully_paid']:
|
||||
continue
|
||||
|
||||
# The percentage of the tax cash basis entries are expressed using the company's currency and then,
|
||||
# there is no exchange difference to make for such journal entry.
|
||||
if move_values['currency'] == move.company_id.currency_id:
|
||||
continue
|
||||
|
||||
# ==========================================================================
|
||||
# Add the balance of all tax lines of the current move in order in order
|
||||
# to compute the residual amount for each of them.
|
||||
# ==========================================================================
|
||||
|
||||
is_exchange_diff_needed = False
|
||||
for line in move_values['to_process_lines']:
|
||||
|
||||
if not line.tax_repartition_line_id:
|
||||
continue
|
||||
vals = {
|
||||
'currency_id': line.currency_id.id,
|
||||
'partner_id': line.partner_id.id,
|
||||
'tax_ids': [(6, 0, line.tax_ids.ids)],
|
||||
'tax_tag_ids': [(6, 0, line._convert_tags_for_cash_basis(line.tax_tag_ids).ids)],
|
||||
'debit': line.debit,
|
||||
'credit': line.credit,
|
||||
}
|
||||
|
||||
if not line.account_id.reconcile:
|
||||
is_exchange_diff_needed = True
|
||||
if line.tax_repartition_line_id:
|
||||
# Tax line.
|
||||
grouping_key = self.env['account.partial.reconcile']._get_cash_basis_tax_line_grouping_key_from_record(line)
|
||||
transfer_account_vals_to_fix[grouping_key] = {
|
||||
account_vals_to_fix[grouping_key] = {
|
||||
**vals,
|
||||
'account_id': line.account_id.id,
|
||||
'currency_id': line.currency_id.id,
|
||||
'partner_id': line.partner_id.id,
|
||||
'tax_base_amount': line.tax_base_amount,
|
||||
'tax_repartition_line_id': line.tax_repartition_line_id.id,
|
||||
'tax_ids': [(6, 0, line.tax_ids.ids)],
|
||||
'tax_tag_ids': [(6, 0, line._convert_tags_for_cash_basis(line.tax_tag_ids).ids)],
|
||||
'debit': line.debit,
|
||||
'credit': line.credit,
|
||||
}
|
||||
elif line.tax_ids:
|
||||
# Base line.
|
||||
account_to_fix = line.company_id.account_cash_basis_base_account_id
|
||||
if not account_to_fix:
|
||||
continue
|
||||
|
||||
# No tax line on the current move.
|
||||
if not is_exchange_diff_needed:
|
||||
continue
|
||||
grouping_key = self.env['account.partial.reconcile']._get_cash_basis_base_line_grouping_key_from_record(line, account=account_to_fix)
|
||||
account_vals_to_fix[grouping_key] = {
|
||||
**vals,
|
||||
'account_id': account_to_fix.id,
|
||||
}
|
||||
|
||||
# ==========================================================================
|
||||
# Subtract the balance of all previously generated cash basis journal entries
|
||||
@@ -4297,61 +4382,86 @@ class AccountMoveLine(models.Model):
|
||||
|
||||
cash_basis_moves = self.env['account.move'].search([('tax_cash_basis_move_id', '=', move.id)])
|
||||
for line in cash_basis_moves.line_ids:
|
||||
if not line.tax_repartition_line_id:
|
||||
grouping_key = None
|
||||
if line.tax_repartition_line_id:
|
||||
# Tax line.
|
||||
grouping_key = self.env['account.partial.reconcile']._get_cash_basis_tax_line_grouping_key_from_record(
|
||||
line,
|
||||
account=line.tax_line_id.cash_basis_transition_account_id,
|
||||
)
|
||||
elif line.tax_ids:
|
||||
# Base line.
|
||||
grouping_key = self.env['account.partial.reconcile']._get_cash_basis_base_line_grouping_key_from_record(
|
||||
line,
|
||||
account=line.company_id.account_cash_basis_base_account_id,
|
||||
)
|
||||
|
||||
if grouping_key not in account_vals_to_fix:
|
||||
continue
|
||||
|
||||
grouping_key = self.env['account.partial.reconcile']._get_cash_basis_tax_line_grouping_key_from_record(
|
||||
line,
|
||||
account=line.tax_line_id.cash_basis_transition_account_id,
|
||||
)
|
||||
|
||||
if grouping_key not in transfer_account_vals_to_fix:
|
||||
continue
|
||||
|
||||
transfer_account_vals_to_fix[grouping_key]['debit'] -= line.debit
|
||||
transfer_account_vals_to_fix[grouping_key]['credit'] -= line.credit
|
||||
account_vals_to_fix[grouping_key]['debit'] -= line.debit
|
||||
account_vals_to_fix[grouping_key]['credit'] -= line.credit
|
||||
|
||||
# ==========================================================================
|
||||
# Generate the exchange difference journal items to reset the balance of
|
||||
# all transfer account to zero.
|
||||
# Generate the exchange difference journal items:
|
||||
# - to reset the balance of all transfer account to zero.
|
||||
# - fix rounding issues on the tax account/base tax account.
|
||||
# ==========================================================================
|
||||
|
||||
for values in transfer_account_vals_to_fix.values():
|
||||
for values in account_vals_to_fix.values():
|
||||
balance = values['debit'] - values['credit']
|
||||
values.update({
|
||||
'debit': balance if balance > 0.0 else 0.0,
|
||||
'credit': -balance if balance < 0.0 else 0.0,
|
||||
})
|
||||
|
||||
account = self.env['account.account'].browse(values['account_id'])
|
||||
if account.company_id.currency_id.is_zero(balance):
|
||||
if move.company_currency_id.is_zero(balance):
|
||||
continue
|
||||
|
||||
journal = account.company_id.currency_exchange_journal_id
|
||||
if values.get('tax_repartition_line_id'):
|
||||
# Tax line.
|
||||
tax_repartition_line = self.env['account.tax.repartition.line'].browse(values['tax_repartition_line_id'])
|
||||
account = tax_repartition_line.account_id or self.env['account.account'].browse(values['account_id'])
|
||||
|
||||
if balance > 0.0:
|
||||
exchange_line_account = journal.company_id.expense_currency_exchange_account_id
|
||||
sequence = len(exchange_diff_move_vals['line_ids'])
|
||||
exchange_diff_move_vals['line_ids'] += [
|
||||
(0, 0, {
|
||||
**values,
|
||||
'name': _('Currency exchange rate difference (cash basis)'),
|
||||
'debit': balance if balance > 0.0 else 0.0,
|
||||
'credit': -balance if balance < 0.0 else 0.0,
|
||||
'account_id': account.id,
|
||||
'sequence': sequence,
|
||||
}),
|
||||
(0, 0, {
|
||||
**values,
|
||||
'name': _('Currency exchange rate difference (cash basis)'),
|
||||
'debit': -balance if balance < 0.0 else 0.0,
|
||||
'credit': balance if balance > 0.0 else 0.0,
|
||||
'account_id': values['account_id'],
|
||||
'tax_ids': [],
|
||||
'tax_tag_ids': [],
|
||||
'tax_repartition_line_id': False,
|
||||
'sequence': sequence + 1,
|
||||
}),
|
||||
]
|
||||
else:
|
||||
exchange_line_account = journal.company_id.income_currency_exchange_account_id
|
||||
|
||||
sequence = len(exchange_diff_move_vals['line_ids'])
|
||||
exchange_diff_move_vals['line_ids'] += [
|
||||
(0, 0, {
|
||||
**values,
|
||||
'name': _('Currency exchange rate difference (cash basis)'),
|
||||
'debit': values['credit'],
|
||||
'credit': values['debit'],
|
||||
'amount_currency': 0.0,
|
||||
'sequence': sequence,
|
||||
}),
|
||||
(0, 0, {
|
||||
**values,
|
||||
'name': _('Currency exchange rate difference (cash basis)'),
|
||||
'amount_currency': 0.0,
|
||||
'account_id': exchange_line_account.id,
|
||||
'sequence': sequence + 1,
|
||||
}),
|
||||
]
|
||||
# Base line.
|
||||
sequence = len(exchange_diff_move_vals['line_ids'])
|
||||
exchange_diff_move_vals['line_ids'] += [
|
||||
(0, 0, {
|
||||
**values,
|
||||
'name': _('Currency exchange rate difference (cash basis)'),
|
||||
'debit': balance if balance > 0.0 else 0.0,
|
||||
'credit': -balance if balance < 0.0 else 0.0,
|
||||
'sequence': sequence,
|
||||
}),
|
||||
(0, 0, {
|
||||
**values,
|
||||
'name': _('Currency exchange rate difference (cash basis)'),
|
||||
'debit': -balance if balance < 0.0 else 0.0,
|
||||
'credit': balance if balance > 0.0 else 0.0,
|
||||
'tax_ids': [],
|
||||
'tax_tag_ids': [],
|
||||
'sequence': sequence + 1,
|
||||
}),
|
||||
]
|
||||
|
||||
if not self:
|
||||
return self.env['account.move']
|
||||
@@ -4552,6 +4662,12 @@ class AccountMoveLine(models.Model):
|
||||
# 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'] = ''
|
||||
# Don't copy restricted fields of notes
|
||||
if line.display_type in ('line_section', 'line_note'):
|
||||
values['amount_currency'] = 0
|
||||
values['debit'] = 0
|
||||
values['credit'] = 0
|
||||
values['account_id'] = False
|
||||
if self._context.get('include_business_fields'):
|
||||
line._copy_data_extend_business_fields(values)
|
||||
return res
|
||||
@@ -4622,6 +4738,7 @@ class AccountMoveLine(models.Model):
|
||||
'name': default_name,
|
||||
'date': self.date,
|
||||
'account_id': distribution.account_id.id,
|
||||
'group_id': distribution.account_id.group_id.id,
|
||||
'partner_id': self.partner_id.id,
|
||||
'tag_ids': [(6, 0, [distribution.tag_id.id] + self._get_analytic_tag_ids())],
|
||||
'unit_amount': self.quantity,
|
||||
|
||||
@@ -167,11 +167,15 @@ class AccountPartialReconcile(models.Model):
|
||||
partial_amount_currency += partial.debit_amount_currency
|
||||
rate_amount -= partial.credit_move_id.balance
|
||||
rate_amount_currency -= partial.credit_move_id.amount_currency
|
||||
source_line = partial.debit_move_id
|
||||
counterpart_line = partial.credit_move_id
|
||||
if partial.credit_move_id.move_id == move:
|
||||
partial_amount += partial.amount
|
||||
partial_amount_currency += partial.credit_amount_currency
|
||||
rate_amount += partial.debit_move_id.balance
|
||||
rate_amount_currency += partial.debit_move_id.amount_currency
|
||||
source_line = partial.credit_move_id
|
||||
counterpart_line = partial.debit_move_id
|
||||
|
||||
if move_values['currency'] == move.company_id.currency_id:
|
||||
# Percentage made on company's currency.
|
||||
@@ -180,7 +184,16 @@ class AccountPartialReconcile(models.Model):
|
||||
# Percentage made on foreign currency.
|
||||
percentage = partial_amount_currency / move_values['total_amount_currency']
|
||||
|
||||
if rate_amount:
|
||||
if source_line.currency_id != counterpart_line.currency_id:
|
||||
# When the invoice and the payment are not sharing the same foreign currency, the rate is computed
|
||||
# on-the-fly using the payment date.
|
||||
payment_rate = self.env['res.currency']._get_conversion_rate(
|
||||
counterpart_line.company_currency_id,
|
||||
source_line.currency_id,
|
||||
counterpart_line.company_id,
|
||||
counterpart_line.date,
|
||||
)
|
||||
elif rate_amount:
|
||||
payment_rate = rate_amount_currency / rate_amount
|
||||
else:
|
||||
payment_rate = 0.0
|
||||
@@ -361,6 +374,7 @@ class AccountPartialReconcile(models.Model):
|
||||
:param pending_cash_basis_lines: The previously generated lines during this reconciliation but not yet created.
|
||||
:param partial_lines_to_create: The generated lines for the current and last partial making the move fully paid.
|
||||
'''
|
||||
# DEPRECATED: TO BE REMOVED IN MASTER
|
||||
residual_amount_per_group = {}
|
||||
move = move_values['move']
|
||||
|
||||
@@ -458,9 +472,8 @@ class AccountPartialReconcile(models.Model):
|
||||
move = move_values['move']
|
||||
pending_cash_basis_lines = []
|
||||
|
||||
for i, partial_values in enumerate(move_values['partials']):
|
||||
for partial_values in move_values['partials']:
|
||||
partial = partial_values['partial']
|
||||
is_last_partial = i == len(move_values['partials']) - 1
|
||||
|
||||
# Init the journal entry.
|
||||
move_vals = {
|
||||
@@ -530,19 +543,6 @@ class AccountPartialReconcile(models.Model):
|
||||
'vals': cb_base_line_vals,
|
||||
}
|
||||
|
||||
# ==========================================================================
|
||||
# Ensure the full coverage by replacing the balance of the journal items
|
||||
# created by the last partial.
|
||||
# ==========================================================================
|
||||
|
||||
if move_values['is_fully_paid'] and is_last_partial:
|
||||
self._fix_cash_basis_full_balance_coverage(
|
||||
move_values,
|
||||
partial_values,
|
||||
pending_cash_basis_lines,
|
||||
partial_lines_to_create,
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# Create the counterpart journal items.
|
||||
# ==========================================================================
|
||||
|
||||
@@ -224,7 +224,7 @@ class AccountPayment(models.Model):
|
||||
}
|
||||
|
||||
default_line_name = self.env['account.move.line']._get_default_line_name(
|
||||
payment_display_name['%s-%s' % (self.payment_type, self.partner_type)],
|
||||
_("Internal Transfer") if self.is_internal_transfer else payment_display_name['%s-%s' % (self.payment_type, self.partner_type)],
|
||||
self.amount,
|
||||
self.currency_id,
|
||||
self.date,
|
||||
@@ -272,7 +272,7 @@ class AccountPayment(models.Model):
|
||||
# COMPUTE METHODS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@api.depends('move_id.line_ids.amount_residual', 'move_id.line_ids.amount_residual_currency')
|
||||
@api.depends('move_id.line_ids.amount_residual', 'move_id.line_ids.amount_residual_currency', 'move_id.line_ids.account_id')
|
||||
def _compute_reconciliation_status(self):
|
||||
''' Compute the field indicating if the payments are already reconciled with something.
|
||||
This field is used for display purpose (e.g. display the 'reconcile' button redirecting to the reconciliation
|
||||
@@ -319,7 +319,7 @@ class AccountPayment(models.Model):
|
||||
def _compute_partner_bank_id(self):
|
||||
''' The default partner_bank_id will be the first available on the partner. '''
|
||||
for pay in self:
|
||||
available_partner_bank_accounts = pay.partner_id.bank_ids
|
||||
available_partner_bank_accounts = pay.partner_id.bank_ids.filtered(lambda x: x.company_id in (False, pay.company_id))
|
||||
if available_partner_bank_accounts:
|
||||
pay.partner_bank_id = available_partner_bank_accounts[0]._origin
|
||||
else:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from flectra import api, fields, models, _
|
||||
from flectra.tools import float_compare, float_is_zero
|
||||
from flectra.osv.expression import get_unaccent_wrapper
|
||||
from flectra.exceptions import UserError, ValidationError
|
||||
import re
|
||||
from math import copysign
|
||||
@@ -393,6 +394,8 @@ class AccountReconcileModel(models.Model):
|
||||
|
||||
partner = partner or st_line.partner_id
|
||||
|
||||
has_full_write_off= any(rec_mod_line.amount == 100.0 for rec_mod_line in self.line_ids)
|
||||
|
||||
lines_vals_list = []
|
||||
amls = self.env['account.move.line'].browse(aml_ids)
|
||||
st_line_residual_before = st_line_residual
|
||||
@@ -403,6 +406,9 @@ class AccountReconcileModel(models.Model):
|
||||
if aml.balance * st_line_residual > 0:
|
||||
# Meaning they have the same signs, so they can't be reconciled together
|
||||
assigned_balance = -aml.amount_residual
|
||||
elif has_full_write_off:
|
||||
assigned_balance = -aml.amount_residual
|
||||
st_line_residual -= min(-aml.amount_residual, st_line_residual, key=abs)
|
||||
else:
|
||||
assigned_balance = min(-aml.amount_residual, st_line_residual, key=abs)
|
||||
st_line_residual -= assigned_balance
|
||||
@@ -423,13 +429,8 @@ class AccountReconcileModel(models.Model):
|
||||
st_line_residual -= st_line.company_currency_id.round(line_vals['balance'])
|
||||
|
||||
# Check we have enough information to create an open balance.
|
||||
if not st_line.company_currency_id.is_zero(st_line_residual):
|
||||
if st_line.amount > 0:
|
||||
open_balance_account = partner.property_account_receivable_id
|
||||
else:
|
||||
open_balance_account = partner.property_account_payable_id
|
||||
if not open_balance_account:
|
||||
return []
|
||||
if open_balance_vals and not open_balance_vals.get('account_id'):
|
||||
return []
|
||||
|
||||
return lines_vals_list + writeoff_vals_list
|
||||
|
||||
@@ -582,6 +583,8 @@ class AccountReconcileModel(models.Model):
|
||||
if self.rule_type != 'invoice_matching':
|
||||
raise UserError(_('Programmation Error: Can\'t call _get_invoice_matching_query() for different rules than \'invoice_matching\''))
|
||||
|
||||
unaccent = get_unaccent_wrapper(self._cr)
|
||||
|
||||
# N.B: 'communication_flag' is there to distinguish invoice matching through the number/reference
|
||||
# (higher priority) from invoice matching using the partner (lower priority).
|
||||
query = r'''
|
||||
@@ -641,7 +644,10 @@ class AccountReconcileModel(models.Model):
|
||||
within the payment_ref, in any order, with any characters between them. */
|
||||
|
||||
aml_partner.name IS NOT NULL
|
||||
AND st_line.payment_ref ~* concat('(?=.*', array_to_string(regexp_split_to_array(lower(aml_partner.name), ' '),'.*)(?=.*'), '.*)')
|
||||
AND """ + unaccent("st_line.payment_ref") + r""" ~* ('^' || (
|
||||
SELECT string_agg(concat('(?=.*\m', chunk[1], '\M)'), '')
|
||||
FROM regexp_matches(""" + unaccent("aml_partner.name") + r""", '\w{3,}', 'g') AS chunk
|
||||
))
|
||||
)
|
||||
"""
|
||||
|
||||
@@ -803,7 +809,8 @@ class AccountReconcileModel(models.Model):
|
||||
# We check the amount criteria of the reconciliation model, and select the
|
||||
# candidates if they pass the verification. Candidates from the first priority
|
||||
# level (even already selected) bypass this check, and are selected anyway.
|
||||
if priorities & {1,2} or self._check_rule_propositions(st_line, candidates):
|
||||
disable_bypass = self.env['ir.config_parameter'].sudo().get_param('account.disable_rec_models_bypass')
|
||||
if (not disable_bypass and priorities & {1,2}) or self._check_rule_propositions(st_line, candidates):
|
||||
rslt = {
|
||||
'model': self,
|
||||
'aml_ids': [candidate['aml_id'] for candidate in candidates],
|
||||
|
||||
@@ -654,12 +654,12 @@ class AccountTaxRepartitionLine(models.Model):
|
||||
help="The order in which distribution lines are displayed and matched. For refunds to work properly, invoice distribution lines should be arranged in the same order as the credit note distribution lines they correspond to.")
|
||||
use_in_tax_closing = fields.Boolean(string="Tax Closing Entry")
|
||||
|
||||
@api.onchange('account_id')
|
||||
@api.onchange('account_id', 'repartition_type')
|
||||
def _on_change_account_id(self):
|
||||
if not self.account_id:
|
||||
if not self.account_id or self.repartition_type == 'base':
|
||||
self.use_in_tax_closing = False
|
||||
else:
|
||||
self.use_in_tax_closing = not(self.account_id.internal_group == 'income' or self.account_id.internal_group == 'expense')
|
||||
self.use_in_tax_closing = self.account_id.internal_group not in ('income', 'expense')
|
||||
|
||||
@api.constrains('invoice_tax_id', 'refund_tax_id')
|
||||
def validate_tax_template_link(self):
|
||||
|
||||
@@ -54,7 +54,11 @@ class AccountTaxReport(models.Model):
|
||||
copied_report = super(AccountTaxReport, self).copy(default=copy_default) #This copies the report without its lines
|
||||
|
||||
lines_map = {} # maps original lines to their copies (using ids)
|
||||
for line in self.line_ids:
|
||||
lines_to_treat = list(self.line_ids.filtered(lambda x: not x.parent_id))
|
||||
while lines_to_treat:
|
||||
line = lines_to_treat.pop()
|
||||
lines_to_treat += list(line.children_line_ids)
|
||||
|
||||
copy = line.copy({'parent_id': lines_map.get(line.parent_id.id, None), 'report_id': copied_report.id})
|
||||
lines_map[line.id] = copy.id
|
||||
|
||||
@@ -65,10 +69,10 @@ class AccountTaxReport(models.Model):
|
||||
ar all directly followed by their children.
|
||||
"""
|
||||
self.ensure_one()
|
||||
lines_to_treat = list(self.line_ids.filtered(lambda x: not x.parent_id)) # Used as a stack, whose index 0 is the top
|
||||
lines_to_treat = list(self.line_ids.filtered(lambda x: not x.parent_id).sorted(lambda x: x.sequence)) # Used as a stack, whose index 0 is the top
|
||||
while lines_to_treat:
|
||||
to_yield = lines_to_treat[0]
|
||||
lines_to_treat = list(to_yield.children_line_ids) + lines_to_treat[1:]
|
||||
lines_to_treat = list(to_yield.children_line_ids.sorted(lambda x: x.sequence)) + lines_to_treat[1:]
|
||||
yield to_yield
|
||||
|
||||
def get_checks_to_perform(self, d):
|
||||
|
||||
@@ -27,4 +27,9 @@ class IrActionsReport(models.Model):
|
||||
# don't save the 'account.report_original_vendor_bill' report as it's just a mean to print existing attachments
|
||||
if self.report_name == 'account.report_original_vendor_bill':
|
||||
return None
|
||||
return super(IrActionsReport, self)._postprocess_pdf_report(record, buffer)
|
||||
res = super(IrActionsReport, self)._postprocess_pdf_report(record, buffer)
|
||||
if self.model == 'account.move' and record.state == 'posted' and record.is_sale_document(include_receipts=True):
|
||||
attachment = self.retrieve_attachment(record)
|
||||
if attachment:
|
||||
attachment.register_as_main_attachment(force=False)
|
||||
return res
|
||||
|
||||
@@ -24,7 +24,7 @@ class ResCurrency(models.Model):
|
||||
rounding_val = vals['rounding']
|
||||
for record in self:
|
||||
if (rounding_val > record.rounding or rounding_val == 0) and record._has_accounting_entries():
|
||||
raise UserError(_("You cannot reduce the number of decimal places of a currency which has already been used to make accounting entries. If you really need to do that, please contact tech support."))
|
||||
raise UserError(_("You cannot reduce the number of decimal places of a currency which has already been used to make accounting entries."))
|
||||
|
||||
return super(ResCurrency, self).write(vals)
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ class SequenceMixin(models.AbstractModel):
|
||||
_sequence_field = "name"
|
||||
_sequence_date_field = "date"
|
||||
_sequence_index = False
|
||||
_sequence_monthly_regex = r'^(?P<prefix1>.*?)(?P<year>((?<=\D)|(?<=^))(\d{4}|(\d{2}(?=\D))))(?P<prefix2>\D*?)(?P<month>\d{2})(?P<prefix3>\D+?)(?P<seq>\d*)(?P<suffix>\D*?)$'
|
||||
_sequence_yearly_regex = r'^(?P<prefix1>.*?)(?P<year>((?<=\D)|(?<=^))(\d{4}|\d{2}))(?P<prefix2>\D+?)(?P<seq>\d*)(?P<suffix>\D*?)$'
|
||||
_sequence_monthly_regex = r'^(?P<prefix1>.*?)(?P<year>((?<=\D)|(?<=^))((20|21)\d{2}|(\d{2}(?=\D))))(?P<prefix2>\D*?)(?P<month>(0[1-9]|1[0-2]))(?P<prefix3>\D+?)(?P<seq>\d*)(?P<suffix>\D*?)$'
|
||||
_sequence_yearly_regex = r'^(?P<prefix1>.*?)(?P<year>((?<=\D)|(?<=^))((20|21)?\d{2}))(?P<prefix2>\D+?)(?P<seq>\d*)(?P<suffix>\D*?)$'
|
||||
_sequence_fixed_regex = r'^(?P<prefix1>.*?)(?P<seq>\d{0,9})(?P<suffix>\D*?)$'
|
||||
|
||||
sequence_prefix = fields.Char(compute='_compute_split_sequence', store=True)
|
||||
@@ -91,30 +91,16 @@ class SequenceMixin(models.AbstractModel):
|
||||
periodicity. Typically, it is the last before the one you want to give a
|
||||
sequence.
|
||||
"""
|
||||
def _check_grouping(grouping, required):
|
||||
sequence_dict = grouping.groupdict()
|
||||
if 'seq' not in sequence_dict or any(not sequence_dict.get(key) for key in required):
|
||||
return False
|
||||
if 'year' in required and not (
|
||||
2000 <= int(sequence_dict.get('year') or -1) <= 2100
|
||||
or len(sequence_dict.get('year') or '') == 2
|
||||
):
|
||||
return False
|
||||
if 'month' in required and not 1 <= int(sequence_dict.get('month') or -1) <= 12:
|
||||
return False
|
||||
return True
|
||||
|
||||
if not name:
|
||||
return False
|
||||
sequence = re.match(self._sequence_monthly_regex, name)
|
||||
if sequence and _check_grouping(sequence, ['year', 'month']):
|
||||
return 'month'
|
||||
sequence = re.match(self._sequence_yearly_regex, name)
|
||||
if sequence and _check_grouping(sequence, ['year']):
|
||||
return 'year'
|
||||
sequence = re.match(self._sequence_fixed_regex, name)
|
||||
if sequence and _check_grouping(sequence, []):
|
||||
return 'never'
|
||||
for regex, ret_val, requirements in [
|
||||
(self._sequence_monthly_regex, 'month', ['seq', 'month', 'year']),
|
||||
(self._sequence_yearly_regex, 'year', ['seq', 'year']),
|
||||
(self._sequence_fixed_regex, 'never', ['seq']),
|
||||
]:
|
||||
match = re.match(regex, name or '')
|
||||
if match:
|
||||
groupdict = match.groupdict()
|
||||
if all(req in groupdict for req in requirements):
|
||||
return ret_val
|
||||
raise ValidationError(_(
|
||||
'The sequence regex should at least contain the seq grouping keys. For instance:\n'
|
||||
'^(?P<prefix1>.*?)(?P<seq>\d*)(?P<suffix>\D*?)$'
|
||||
|
||||
@@ -260,6 +260,13 @@
|
||||
<field name="groups" eval="[(4, ref('account.group_account_invoice'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="account_invoice_send_rule_group_invoice" model="ir.rule">
|
||||
<field name="name">Readonly Invoice Send and Print</field>
|
||||
<field name="model_id" ref="model_account_invoice_send"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="groups" eval="[(4, ref('account.group_account_invoice'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- account analytic default-->
|
||||
<record id="analytic_default_comp_rule" model="ir.rule">
|
||||
<field name="name">Analytic Default multi company rule</field>
|
||||
|
||||
@@ -27,7 +27,7 @@ flectra.define('account.hierarchy.selection', function (require) {
|
||||
self.values = _.map(arg, v => [v['id'], v['display_name']])
|
||||
self.hierarchy_groups = [
|
||||
{
|
||||
'name': _('Balance Sheet'),
|
||||
'name': _t('Balance Sheet'),
|
||||
'children': [
|
||||
{'name': _t('Assets'), 'ids': _.map(_.filter(arg, v => v['internal_group'] == 'asset'), v => v['id'])},
|
||||
{'name': _t('Liabilities'), 'ids': _.map(_.filter(arg, v => v['internal_group'] == 'liability'), v => v['id'])},
|
||||
|
||||
@@ -8,6 +8,7 @@ var _t = core._t;
|
||||
|
||||
tour.register('account_tour', {
|
||||
url: "/web",
|
||||
sequence: 60,
|
||||
}, [
|
||||
...tour.stepUtils.goToAppSteps('account.menu_finance', _t('Send invoices to your customers in no time with the <b>Invoicing app</b>.')),
|
||||
{
|
||||
|
||||
@@ -362,7 +362,7 @@ class AccountTestInvoicingCommon(SavepointCase):
|
||||
|
||||
@classmethod
|
||||
def init_invoice(cls, move_type, partner=None, invoice_date=None, post=False, products=[], amounts=[], taxes=None):
|
||||
move_form = Form(cls.env['account.move'].with_context(default_move_type=move_type))
|
||||
move_form = Form(cls.env['account.move'].with_context(default_move_type=move_type, account_predictive_bills_disable_prediction=True))
|
||||
move_form.invoice_date = invoice_date or fields.Date.from_string('2019-01-01')
|
||||
move_form.partner_id = partner or cls.partner_a
|
||||
|
||||
@@ -375,6 +375,9 @@ class AccountTestInvoicingCommon(SavepointCase):
|
||||
|
||||
for amount in amounts:
|
||||
with move_form.invoice_line_ids.new() as line_form:
|
||||
line_form.name = "test line"
|
||||
# We use account_predictive_bills_disable_prediction context key so that
|
||||
# this doesn't trigger prediction in case enterprise (hence account_predictive_bills) is installed
|
||||
line_form.price_unit = amount
|
||||
if taxes:
|
||||
line_form.tax_ids.clear()
|
||||
@@ -624,11 +627,11 @@ class TestAccountReconciliationCommon(AccountTestInvoicingCommon):
|
||||
}
|
||||
])
|
||||
|
||||
def _create_invoice(self, type='out_invoice', invoice_amount=50, currency_id=None, partner_id=None, date_invoice=None, payment_term_id=False, auto_validate=False):
|
||||
def _create_invoice(self, move_type='out_invoice', invoice_amount=50, currency_id=None, partner_id=None, date_invoice=None, payment_term_id=False, auto_validate=False):
|
||||
date_invoice = date_invoice or time.strftime('%Y') + '-07-01'
|
||||
|
||||
invoice_vals = {
|
||||
'move_type': type,
|
||||
'move_type': move_type,
|
||||
'partner_id': partner_id or self.partner_agrolait_id,
|
||||
'invoice_date': date_invoice,
|
||||
'date': date_invoice,
|
||||
@@ -651,12 +654,12 @@ class TestAccountReconciliationCommon(AccountTestInvoicingCommon):
|
||||
invoice.action_post()
|
||||
return invoice
|
||||
|
||||
def create_invoice(self, type='out_invoice', invoice_amount=50, currency_id=None):
|
||||
return self._create_invoice(type=type, invoice_amount=invoice_amount, currency_id=currency_id, auto_validate=True)
|
||||
def create_invoice(self, move_type='out_invoice', invoice_amount=50, currency_id=None):
|
||||
return self._create_invoice(move_type=move_type, invoice_amount=invoice_amount, currency_id=currency_id, auto_validate=True)
|
||||
|
||||
def create_invoice_partner(self, type='out_invoice', invoice_amount=50, currency_id=None, partner_id=False, payment_term_id=False):
|
||||
def create_invoice_partner(self, move_type='out_invoice', invoice_amount=50, currency_id=None, partner_id=False, payment_term_id=False):
|
||||
return self._create_invoice(
|
||||
type=type,
|
||||
move_type=move_type,
|
||||
invoice_amount=invoice_amount,
|
||||
currency_id=currency_id,
|
||||
partner_id=partner_id,
|
||||
@@ -684,14 +687,14 @@ class TestAccountReconciliationCommon(AccountTestInvoicingCommon):
|
||||
|
||||
def make_customer_and_supplier_flows(self, invoice_currency_id, invoice_amount, bank_journal, amount, amount_currency, transaction_currency_id):
|
||||
#we create an invoice in given invoice_currency
|
||||
invoice_record = self.create_invoice(type='out_invoice', invoice_amount=invoice_amount, currency_id=invoice_currency_id)
|
||||
invoice_record = self.create_invoice(move_type='out_invoice', invoice_amount=invoice_amount, currency_id=invoice_currency_id)
|
||||
#we encode a payment on it, on the given bank_journal with amount, amount_currency and transaction_currency given
|
||||
line = invoice_record.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
|
||||
bank_stmt = self.make_payment(invoice_record, bank_journal, amount=amount, amount_currency=amount_currency, currency_id=transaction_currency_id, reconcile_param=[{'id': line.id}])
|
||||
customer_move_lines = bank_stmt.line_ids.line_ids
|
||||
|
||||
#we create a supplier bill in given invoice_currency
|
||||
invoice_record = self.create_invoice(type='in_invoice', invoice_amount=invoice_amount, currency_id=invoice_currency_id)
|
||||
invoice_record = self.create_invoice(move_type='in_invoice', invoice_amount=invoice_amount, currency_id=invoice_currency_id)
|
||||
#we encode a payment on it, on the given bank_journal with amount, amount_currency and transaction_currency given
|
||||
line = invoice_record.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
|
||||
bank_stmt = self.make_payment(invoice_record, bank_journal, amount=-amount, amount_currency=-amount_currency, currency_id=transaction_currency_id, reconcile_param=[{'id': line.id}])
|
||||
|
||||
@@ -538,7 +538,7 @@ class TestAccountBankStatementLine(TestAccountBankStatementCommon):
|
||||
def test_zero_amount_journal_curr_1_statement_curr_2(self):
|
||||
self.bank_journal_2.currency_id = self.currency_1
|
||||
|
||||
statement = self.env['account.bank.statement'].with_context(skip_check_amounts_currencies=True).create({
|
||||
statement = self.env['account.bank.statement'].create({
|
||||
'name': 'test_statement',
|
||||
'date': '2019-01-01',
|
||||
'journal_id': self.bank_journal_2.id,
|
||||
@@ -562,7 +562,7 @@ class TestAccountBankStatementLine(TestAccountBankStatementCommon):
|
||||
def test_zero_amount_currency_journal_curr_1_statement_curr_2(self):
|
||||
self.bank_journal_2.currency_id = self.currency_1
|
||||
|
||||
statement = self.env['account.bank.statement'].with_context(skip_check_amounts_currencies=True).create({
|
||||
statement = self.env['account.bank.statement'].create({
|
||||
'name': 'test_statement',
|
||||
'date': '2019-01-01',
|
||||
'journal_id': self.bank_journal_2.id,
|
||||
@@ -586,7 +586,7 @@ class TestAccountBankStatementLine(TestAccountBankStatementCommon):
|
||||
def test_zero_amount_journal_curr_2_statement_curr_1(self):
|
||||
self.bank_journal_2.currency_id = self.currency_2
|
||||
|
||||
statement = self.env['account.bank.statement'].with_context(skip_check_amounts_currencies=True).create({
|
||||
statement = self.env['account.bank.statement'].create({
|
||||
'name': 'test_statement',
|
||||
'date': '2019-01-01',
|
||||
'journal_id': self.bank_journal_2.id,
|
||||
@@ -610,7 +610,7 @@ class TestAccountBankStatementLine(TestAccountBankStatementCommon):
|
||||
def test_zero_amount_currency_journal_curr_2_statement_curr_1(self):
|
||||
self.bank_journal_2.currency_id = self.currency_2
|
||||
|
||||
statement = self.env['account.bank.statement'].with_context(skip_check_amounts_currencies=True).create({
|
||||
statement = self.env['account.bank.statement'].create({
|
||||
'name': 'test_statement',
|
||||
'date': '2019-01-01',
|
||||
'journal_id': self.bank_journal_2.id,
|
||||
@@ -634,7 +634,7 @@ class TestAccountBankStatementLine(TestAccountBankStatementCommon):
|
||||
def test_zero_amount_journal_curr_2_statement_curr_3(self):
|
||||
self.bank_journal_2.currency_id = self.currency_2
|
||||
|
||||
statement = self.env['account.bank.statement'].with_context(skip_check_amounts_currencies=True).create({
|
||||
statement = self.env['account.bank.statement'].create({
|
||||
'name': 'test_statement',
|
||||
'date': '2019-01-01',
|
||||
'journal_id': self.bank_journal_2.id,
|
||||
@@ -658,7 +658,7 @@ class TestAccountBankStatementLine(TestAccountBankStatementCommon):
|
||||
def test_zero_amount_currency_journal_curr_2_statement_curr_3(self):
|
||||
self.bank_journal_2.currency_id = self.currency_2
|
||||
|
||||
statement = self.env['account.bank.statement'].with_context(skip_check_amounts_currencies=True).create({
|
||||
statement = self.env['account.bank.statement'].create({
|
||||
'name': 'test_statement',
|
||||
'date': '2019-01-01',
|
||||
'journal_id': self.bank_journal_2.id,
|
||||
@@ -703,25 +703,12 @@ class TestAccountBankStatementLine(TestAccountBankStatementCommon):
|
||||
|
||||
# ==== Test constraints at creation ====
|
||||
|
||||
# Amount can't be 0.0 on a statement line.
|
||||
assertStatementLineConstraint(statement_vals, {
|
||||
**statement_line_vals,
|
||||
'amount': 0.0,
|
||||
})
|
||||
|
||||
# Foreign currency must not be the same as the journal one.
|
||||
assertStatementLineConstraint(statement_vals, {
|
||||
**statement_line_vals,
|
||||
'foreign_currency_id': self.currency_1.id,
|
||||
})
|
||||
|
||||
# Can't have amount_currency = 0.0 with a specified foreign currency.
|
||||
assertStatementLineConstraint(statement_vals, {
|
||||
**statement_line_vals,
|
||||
'foreign_currency_id': self.currency_2.id,
|
||||
'amount_currency': 0.0,
|
||||
})
|
||||
|
||||
# Can't have a stand alone amount in foreign currency without foreign currency set.
|
||||
assertStatementLineConstraint(statement_vals, {
|
||||
**statement_line_vals,
|
||||
@@ -1498,3 +1485,110 @@ class TestAccountBankStatementLine(TestAccountBankStatementCommon):
|
||||
'amount_residual_currency': 0.0,
|
||||
},
|
||||
])
|
||||
|
||||
def test_conversion_rate_rounding_issue(self):
|
||||
''' Ensure the reconciliation is well handling the rounding issue due to multiple currency conversion rates.
|
||||
|
||||
In this test, the resulting journal entry after reconciliation is:
|
||||
{'amount_currency': 7541.66, 'debit': 6446.97, 'credit': 0.0}
|
||||
{'amount_currency': 226.04, 'debit': 193.22, 'credit': 0.0}
|
||||
{'amount_currency': -7767.70, 'debit': 0.0, 'credit': 6640.19}
|
||||
... but 226.04 / 1.1698 = 193.23. In this situation, 0.01 has been removed from this write-off line in order to
|
||||
avoid an unecessary open-balance line being an exchange difference issue.
|
||||
'''
|
||||
self.bank_journal_2.currency_id = self.currency_2
|
||||
self.currency_data['rates'][-1].rate = 1.1698
|
||||
|
||||
statement = self.env['account.bank.statement'].create({
|
||||
'name': 'test_statement',
|
||||
'date': '2017-01-01',
|
||||
'journal_id': self.bank_journal_2.id,
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'date': '2019-01-01',
|
||||
'payment_ref': 'line_1',
|
||||
'partner_id': self.partner_a.id,
|
||||
'amount': 7541.66,
|
||||
}),
|
||||
],
|
||||
})
|
||||
statement.button_post()
|
||||
statement_line = statement.line_ids
|
||||
|
||||
payment = self.env['account.payment'].create({
|
||||
'amount': 7767.70,
|
||||
'date': '2019-01-01',
|
||||
'currency_id': self.currency_2.id,
|
||||
'payment_type': 'inbound',
|
||||
'partner_type': 'customer',
|
||||
})
|
||||
payment.action_post()
|
||||
liquidity_lines, counterpart_lines, writeoff_lines = payment._seek_for_lines()
|
||||
self.assertRecordValues(liquidity_lines, [{'amount_currency': 7767.70}])
|
||||
|
||||
statement_line.reconcile([
|
||||
{'id': liquidity_lines.id},
|
||||
{'balance': 226.04, 'account_id': self.company_data['default_account_revenue'].id, 'name': "write-off"},
|
||||
])
|
||||
|
||||
self.assertRecordValues(statement_line.line_ids, [
|
||||
{'amount_currency': 7541.66, 'debit': 6446.97, 'credit': 0.0},
|
||||
{'amount_currency': 226.04, 'debit': 193.22, 'credit': 0.0},
|
||||
{'amount_currency': -7767.70, 'debit': 0.0, 'credit': 6640.19},
|
||||
])
|
||||
|
||||
def test_zero_amount_statement_line(self):
|
||||
''' Ensure the statement line is directly marked as reconciled when having an amount of zero. '''
|
||||
self.company_data['company'].account_journal_suspense_account_id.reconcile = False
|
||||
|
||||
statement = self.env['account.bank.statement'].with_context(skip_check_amounts_currencies=True).create({
|
||||
'name': 'test_statement',
|
||||
'date': '2017-01-01',
|
||||
'journal_id': self.bank_journal_2.id,
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'date': '2019-01-01',
|
||||
'payment_ref': "Happy new year",
|
||||
'amount': 0.0,
|
||||
}),
|
||||
],
|
||||
})
|
||||
statement_line = statement.line_ids
|
||||
|
||||
self.assertRecordValues(statement_line, [{'is_reconciled': True, 'amount_residual': 0.0}])
|
||||
|
||||
def test_bank_statement_line_analytic(self):
|
||||
''' Ensure the analytic lines are generated during the reconciliation. '''
|
||||
analytic_account = self.env['account.analytic.account'].create({'name': 'analytic_account'})
|
||||
|
||||
statement = self.env['account.bank.statement'].with_context(skip_check_amounts_currencies=True).create({
|
||||
'name': 'test_statement',
|
||||
'date': '2017-01-01',
|
||||
'journal_id': self.bank_journal_2.id,
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'date': '2019-01-01',
|
||||
'payment_ref': "line",
|
||||
'amount': 100.0,
|
||||
}),
|
||||
],
|
||||
})
|
||||
statement_line = statement.line_ids
|
||||
|
||||
statement_line.reconcile([{
|
||||
'balance': -100.0,
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
'name': "write-off",
|
||||
'analytic_account_id': analytic_account.id,
|
||||
}])
|
||||
|
||||
# Check the analytic account is there.
|
||||
self.assertRecordValues(statement_line.line_ids.sorted('balance'), [
|
||||
{'balance': -100.0, 'analytic_account_id': analytic_account.id},
|
||||
{'balance': 100.0, 'analytic_account_id': False},
|
||||
])
|
||||
|
||||
# Check the analytic lines.
|
||||
self.assertRecordValues(statement_line.line_ids.analytic_line_ids, [
|
||||
{'amount': 100.0, 'account_id': analytic_account.id},
|
||||
])
|
||||
|
||||
@@ -12,12 +12,12 @@ class TestAccountIncomingSupplierInvoice(AccountTestInvoicingCommon):
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
|
||||
cls.env['ir.config_parameter'].sudo().set_param('mail.catchall.domain', 'test-company.flectrahq.com')
|
||||
cls.env['ir.config_parameter'].sudo().set_param('mail.catchall.domain', 'test-company.flectra.com')
|
||||
|
||||
cls.internal_user = cls.env['res.users'].create({
|
||||
'name': 'Internal User',
|
||||
'login': 'internal.user@test.flectrahq.com',
|
||||
'email': 'internal.user@test.flectrahq.com',
|
||||
'login': 'internal.user@test.flectra.com',
|
||||
'email': 'internal.user@test.flectra.com',
|
||||
})
|
||||
|
||||
cls.supplier_partner = cls.env['res.partner'].create({
|
||||
|
||||
@@ -18,22 +18,6 @@ class TestAccountJournal(AccountTestInvoicingCommon):
|
||||
with self.assertRaises(ValidationError), self.cr.savepoint():
|
||||
journal_bank.default_account_id.currency_id = self.company_data['currency']
|
||||
|
||||
def test_constraint_shared_accounts(self):
|
||||
''' Ensure the bank/outstanding accounts are not shared between multiple journals. '''
|
||||
journal_bank = self.company_data['default_journal_bank']
|
||||
|
||||
account_fields = (
|
||||
'default_account_id',
|
||||
'payment_debit_account_id',
|
||||
'payment_credit_account_id',
|
||||
)
|
||||
for account_field in account_fields:
|
||||
with self.assertRaises(ValidationError), self.cr.savepoint():
|
||||
journal_bank.copy(default={
|
||||
'name': 'test_constraint_shared_accounts %s' % account_field,
|
||||
account_field: journal_bank[account_field].id,
|
||||
})
|
||||
|
||||
def test_changing_journal_company(self):
|
||||
''' Ensure you can't change the company of an account.journal if there are some journal entries '''
|
||||
|
||||
|
||||
@@ -380,6 +380,11 @@ class TestAccountMove(AccountTestInvoicingCommon):
|
||||
self.assertEqual(copy2.name, 'MISC2/2016/01/0001')
|
||||
with Form(copy2) as move_form: # It is editable in the form
|
||||
move_form.name = 'MyMISC/2016/0001'
|
||||
move_form.journal_id = self.test_move.journal_id
|
||||
self.assertEqual(move_form.name, '/')
|
||||
move_form.journal_id = new_journal
|
||||
self.assertEqual(move_form.name, 'MISC2/2016/01/0001')
|
||||
move_form.name = 'MyMISC/2016/0001'
|
||||
copy2.action_post()
|
||||
self.assertEqual(copy2.name, 'MyMISC/2016/0001')
|
||||
|
||||
@@ -470,6 +475,33 @@ class TestAccountMove(AccountTestInvoicingCommon):
|
||||
self.assertEqual(refund.name, 'RINV/2016/01/0001')
|
||||
self.assertEqual(refund2.name, 'RINV/2016/01/0002')
|
||||
|
||||
def test_journal_sequence_groupby_compute(self):
|
||||
# Setup two journals with a sequence that resets yearly
|
||||
journals = self.env['account.journal'].create([{
|
||||
'name': f'Journal{i}',
|
||||
'code': f'J{i}',
|
||||
'type': 'general',
|
||||
} for i in range(2)])
|
||||
account = self.env['account.account'].search([], limit=1)
|
||||
moves = self.env['account.move'].create([{
|
||||
'journal_id': journals[i].id,
|
||||
'line_ids': [(0, 0, {'account_id': account.id, 'name': 'line'})],
|
||||
'date': '2010-01-01',
|
||||
} for i in range(2)])._post()
|
||||
for i in range(2):
|
||||
moves[i].name = f'J{i}/2010/00001'
|
||||
|
||||
# Check that the moves are correctly batched
|
||||
moves = self.env['account.move'].create([{
|
||||
'journal_id': journals[journal_index].id,
|
||||
'line_ids': [(0, 0, {'account_id': account.id, 'name': 'line'})],
|
||||
'date': f'2010-{month}-01',
|
||||
} for journal_index, month in [(1, 1), (0, 1), (1, 2), (1, 1)]])._post()
|
||||
self.assertEqual(
|
||||
moves.mapped('name'),
|
||||
['J1/2010/00002', 'J0/2010/00002', 'J1/2010/00004', 'J1/2010/00003'],
|
||||
)
|
||||
|
||||
def test_journal_override_sequence_regex(self):
|
||||
other_moves = self.env['account.move'].search([('journal_id', '=', self.test_move.journal_id.id)]) - self.test_move
|
||||
other_moves.unlink() # Do not interfere when trying to get the highest name for new periods
|
||||
@@ -481,10 +513,13 @@ class TestAccountMove(AccountTestInvoicingCommon):
|
||||
|
||||
next.journal_id.sequence_override_regex = r'^(?P<seq>\d*)(?P<suffix1>.*?)(?P<year>(\d{4})?)(?P<suffix2>)$'
|
||||
next.name = '/'
|
||||
next._compute_name()
|
||||
next.action_post()
|
||||
self.assertEqual(next.name, '00000877-G 0002/2020') # Pfew, better!
|
||||
next = self.test_move.copy({'date': self.test_move.date})
|
||||
next.action_post()
|
||||
self.assertEqual(next.name, '00000878-G 0002/2020')
|
||||
|
||||
next = next = self.test_move.copy({'date': self.test_move.date})
|
||||
next = self.test_move.copy({'date': self.test_move.date})
|
||||
next.date = "2017-05-02"
|
||||
next.action_post()
|
||||
self.assertEqual(next.name, '00000001-G 0002/2017')
|
||||
@@ -552,6 +587,32 @@ class TestAccountMove(AccountTestInvoicingCommon):
|
||||
self.assertEqual(copies[5].name, 'XMISC/2019/10005')
|
||||
self.assertEqual(copies[5].state, 'draft')
|
||||
|
||||
def test_sequence_get_more_specific(self):
|
||||
def test_date(date, name):
|
||||
test = self.test_move.copy({'date': date})
|
||||
test.action_post()
|
||||
self.assertEqual(test.name, name)
|
||||
|
||||
def set_sequence(date, name):
|
||||
return self.test_move.copy({'date': date, 'name': name})._post()
|
||||
|
||||
# Start with a continuous sequence
|
||||
self.test_move.name = 'MISC/00001'
|
||||
|
||||
# Change the prefix to reset every year starting in 2017
|
||||
new_year = set_sequence(self.test_move.date + relativedelta(years=1), 'MISC/2017/00001')
|
||||
|
||||
# Change the prefix to reset every month starting in February 2017
|
||||
new_month = set_sequence(new_year.date + relativedelta(months=1), 'MISC/2017/02/00001')
|
||||
|
||||
test_date(self.test_move.date, 'MISC/00002') # Keep the old prefix in 2016
|
||||
test_date(new_year.date, 'MISC/2017/00002') # Keep the new prefix in 2017
|
||||
test_date(new_month.date, 'MISC/2017/02/00002') # Keep the new prefix in February 2017
|
||||
|
||||
# Change the prefix to never reset (again) year starting in 2018 (Please don't do that)
|
||||
reset_never = set_sequence(self.test_move.date + relativedelta(years=2), 'MISC/00100')
|
||||
test_date(reset_never.date, 'MISC/00101') # Keep the new prefix in 2018
|
||||
|
||||
def test_sequence_concurency(self):
|
||||
with self.env.registry.cursor() as cr0,\
|
||||
self.env.registry.cursor() as cr1,\
|
||||
|
||||
@@ -674,6 +674,293 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon):
|
||||
|
||||
self.assertFullReconcile(res['full_reconcile'], line_1 + line_2 + line_3 + line_4 + line_5)
|
||||
|
||||
def test_reverse_exchange_difference_same_foreign_currency(self):
|
||||
move_2016 = self.env['account.move'].create({
|
||||
'move_type': 'entry',
|
||||
'date': '2016-01-01',
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'debit': 1200.0,
|
||||
'credit': 0.0,
|
||||
'amount_currency': 3600.0,
|
||||
'account_id': self.company_data['default_account_receivable'].id,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
}),
|
||||
(0, 0, {
|
||||
'debit': 0.0,
|
||||
'credit': 1200.0,
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
}),
|
||||
],
|
||||
})
|
||||
move_2017 = self.env['account.move'].create({
|
||||
'move_type': 'entry',
|
||||
'date': '2017-01-01',
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'debit': 0.0,
|
||||
'credit': 1800.0,
|
||||
'amount_currency': -3600.0,
|
||||
'account_id': self.company_data['default_account_receivable'].id,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
}),
|
||||
(0, 0, {
|
||||
'debit': 1800.0,
|
||||
'credit': 0.0,
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
}),
|
||||
],
|
||||
})
|
||||
(move_2016 + move_2017).action_post()
|
||||
|
||||
rec_line_2016 = move_2016.line_ids.filtered(lambda line: line.account_id.internal_type == 'receivable')
|
||||
rec_line_2017 = move_2017.line_ids.filtered(lambda line: line.account_id.internal_type == 'receivable')
|
||||
|
||||
self.assertRecordValues(rec_line_2016 + rec_line_2017, [
|
||||
{'amount_residual': 1200.0, 'amount_residual_currency': 3600.0, 'reconciled': False},
|
||||
{'amount_residual': -1800.0, 'amount_residual_currency': -3600.0, 'reconciled': False},
|
||||
])
|
||||
|
||||
# Reconcile.
|
||||
|
||||
res = (rec_line_2016 + rec_line_2017).reconcile()
|
||||
|
||||
self.assertRecordValues(rec_line_2016 + rec_line_2017, [
|
||||
{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
|
||||
{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
|
||||
])
|
||||
|
||||
exchange_diff = res['full_reconcile'].exchange_move_id
|
||||
exchange_diff_lines = exchange_diff.line_ids.sorted('balance')
|
||||
|
||||
self.assertRecordValues(exchange_diff_lines, [
|
||||
{
|
||||
'debit': 0.0,
|
||||
'credit': 600.0,
|
||||
'amount_currency': 0.0,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'account_id': exchange_diff.journal_id.company_id.income_currency_exchange_account_id.id,
|
||||
},
|
||||
{
|
||||
'debit': 600.0,
|
||||
'credit': 0.0,
|
||||
'amount_currency': 0.0,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'account_id': self.company_data['default_account_receivable'].id,
|
||||
},
|
||||
])
|
||||
|
||||
self.assertRecordValues(exchange_diff_lines, [
|
||||
{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': False},
|
||||
{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
|
||||
])
|
||||
|
||||
# Unreconcile.
|
||||
# A reversal is created to cancel the exchange difference journal entry.
|
||||
|
||||
(rec_line_2016 + rec_line_2017).remove_move_reconcile()
|
||||
|
||||
reverse_exchange_diff = exchange_diff_lines[1].matched_credit_ids.credit_move_id.move_id
|
||||
reverse_exchange_diff_lines = reverse_exchange_diff.line_ids.sorted('balance')
|
||||
|
||||
self.assertRecordValues(reverse_exchange_diff_lines, [
|
||||
{
|
||||
'debit': 0.0,
|
||||
'credit': 600.0,
|
||||
'amount_currency': 0.0,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'account_id': self.company_data['default_account_receivable'].id,
|
||||
},
|
||||
{
|
||||
'debit': 600.0,
|
||||
'credit': 0.0,
|
||||
'amount_currency': 0.0,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'account_id': exchange_diff.journal_id.company_id.income_currency_exchange_account_id.id,
|
||||
},
|
||||
])
|
||||
|
||||
self.assertRecordValues(exchange_diff_lines + reverse_exchange_diff_lines, [
|
||||
{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': False},
|
||||
{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
|
||||
{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
|
||||
{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': False},
|
||||
])
|
||||
|
||||
partials = reverse_exchange_diff_lines.matched_debit_ids + reverse_exchange_diff_lines.matched_credit_ids
|
||||
self.assertPartialReconcile(partials, [{
|
||||
'amount': 600.0,
|
||||
'debit_amount_currency': 0.0,
|
||||
'credit_amount_currency': 0.0,
|
||||
'debit_move_id': exchange_diff_lines[1].id,
|
||||
'credit_move_id': reverse_exchange_diff_lines[0].id,
|
||||
}])
|
||||
|
||||
def test_reverse_exchange_multiple_foreign_currencies(self):
|
||||
move_2016 = self.env['account.move'].create({
|
||||
'move_type': 'entry',
|
||||
'date': '2016-01-01',
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'debit': 1200.0,
|
||||
'credit': 0.0,
|
||||
'amount_currency': 7200.0,
|
||||
'account_id': self.company_data['default_account_receivable'].id,
|
||||
'currency_id': self.currency_data_2['currency'].id,
|
||||
}),
|
||||
(0, 0, {
|
||||
'debit': 0.0,
|
||||
'credit': 1200.0,
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
}),
|
||||
],
|
||||
})
|
||||
move_2017 = self.env['account.move'].create({
|
||||
'move_type': 'entry',
|
||||
'date': '2017-01-01',
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'debit': 0.0,
|
||||
'credit': 1200.0,
|
||||
'amount_currency': -2400.0,
|
||||
'account_id': self.company_data['default_account_receivable'].id,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
}),
|
||||
(0, 0, {
|
||||
'debit': 1200.0,
|
||||
'credit': 0.0,
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
}),
|
||||
],
|
||||
})
|
||||
(move_2016 + move_2017).action_post()
|
||||
|
||||
rec_line_2016 = move_2016.line_ids.filtered(lambda line: line.account_id.internal_type == 'receivable')
|
||||
rec_line_2017 = move_2017.line_ids.filtered(lambda line: line.account_id.internal_type == 'receivable')
|
||||
|
||||
self.assertRecordValues(rec_line_2016 + rec_line_2017, [
|
||||
{'amount_residual': 1200.0, 'amount_residual_currency': 7200.0, 'reconciled': False},
|
||||
{'amount_residual': -1200.0, 'amount_residual_currency': -2400.0, 'reconciled': False},
|
||||
])
|
||||
|
||||
# Reconcile.
|
||||
|
||||
res = (rec_line_2016 + rec_line_2017).reconcile()
|
||||
|
||||
self.assertRecordValues(rec_line_2016 + rec_line_2017, [
|
||||
{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
|
||||
{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
|
||||
])
|
||||
|
||||
exchange_diff = res['full_reconcile'].exchange_move_id
|
||||
exchange_diff_lines = exchange_diff.line_ids.sorted('amount_currency')
|
||||
|
||||
self.assertRecordValues(exchange_diff_lines, [
|
||||
{
|
||||
'debit': 0.0,
|
||||
'credit': 0.0,
|
||||
'amount_currency': -2400.0,
|
||||
'currency_id': self.currency_data_2['currency'].id,
|
||||
'account_id': self.company_data['default_account_receivable'].id,
|
||||
},
|
||||
{
|
||||
'debit': 0.0,
|
||||
'credit': 0.0,
|
||||
'amount_currency': -1200.0,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'account_id': self.company_data['default_account_receivable'].id,
|
||||
},
|
||||
{
|
||||
'debit': 0.0,
|
||||
'credit': 0.0,
|
||||
'amount_currency': 1200.0,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'account_id': exchange_diff.journal_id.company_id.expense_currency_exchange_account_id.id,
|
||||
},
|
||||
{
|
||||
'debit': 0.0,
|
||||
'credit': 0.0,
|
||||
'amount_currency': 2400.0,
|
||||
'currency_id': self.currency_data_2['currency'].id,
|
||||
'account_id': exchange_diff.journal_id.company_id.expense_currency_exchange_account_id.id,
|
||||
},
|
||||
])
|
||||
|
||||
self.assertRecordValues(exchange_diff_lines, [
|
||||
{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
|
||||
{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
|
||||
{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': False},
|
||||
{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': False},
|
||||
])
|
||||
|
||||
# Unreconcile.
|
||||
# A reversal is created to cancel the exchange difference journal entry.
|
||||
|
||||
(rec_line_2016 + rec_line_2017).remove_move_reconcile()
|
||||
|
||||
reverse_exchange_diff = exchange_diff_lines[1].matched_debit_ids.debit_move_id.move_id
|
||||
reverse_exchange_diff_lines = reverse_exchange_diff.line_ids.sorted('amount_currency')
|
||||
|
||||
self.assertRecordValues(reverse_exchange_diff_lines, [
|
||||
{
|
||||
'debit': 0.0,
|
||||
'credit': 0.0,
|
||||
'amount_currency': -2400.0,
|
||||
'currency_id': self.currency_data_2['currency'].id,
|
||||
'account_id': exchange_diff.journal_id.company_id.expense_currency_exchange_account_id.id,
|
||||
},
|
||||
{
|
||||
'debit': 0.0,
|
||||
'credit': 0.0,
|
||||
'amount_currency': -1200.0,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'account_id': exchange_diff.journal_id.company_id.expense_currency_exchange_account_id.id,
|
||||
},
|
||||
{
|
||||
'debit': 0.0,
|
||||
'credit': 0.0,
|
||||
'amount_currency': 1200.0,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'account_id': self.company_data['default_account_receivable'].id,
|
||||
},
|
||||
{
|
||||
'debit': 0.0,
|
||||
'credit': 0.0,
|
||||
'amount_currency': 2400.0,
|
||||
'currency_id': self.currency_data_2['currency'].id,
|
||||
'account_id': self.company_data['default_account_receivable'].id,
|
||||
},
|
||||
])
|
||||
|
||||
self.assertRecordValues(exchange_diff_lines + reverse_exchange_diff_lines, [
|
||||
{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
|
||||
{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
|
||||
{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': False},
|
||||
{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': False},
|
||||
{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': False},
|
||||
{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': False},
|
||||
{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
|
||||
{'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
|
||||
])
|
||||
|
||||
partials = reverse_exchange_diff_lines.matched_debit_ids + reverse_exchange_diff_lines.matched_credit_ids
|
||||
self.assertPartialReconcile(partials, [
|
||||
{
|
||||
'amount': 0.0,
|
||||
'debit_amount_currency': 1200.0,
|
||||
'credit_amount_currency': 1200.0,
|
||||
'debit_move_id': reverse_exchange_diff_lines[2].id,
|
||||
'credit_move_id': exchange_diff_lines[1].id,
|
||||
},
|
||||
{
|
||||
'amount': 0.0,
|
||||
'debit_amount_currency': 2400.0,
|
||||
'credit_amount_currency': 2400.0,
|
||||
'debit_move_id': reverse_exchange_diff_lines[3].id,
|
||||
'credit_move_id': exchange_diff_lines[0].id,
|
||||
},
|
||||
])
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Test creation of extra journal entries during the reconciliation to
|
||||
# deal with taxes that are exigible on payment (cash basis).
|
||||
@@ -762,6 +1049,7 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon):
|
||||
(self.cash_basis_transfer_account, -33.34, -33.34),
|
||||
(self.tax_account_1, 0.0, 0.0),
|
||||
(self.tax_account_2, 0.0, 0.0),
|
||||
(self.cash_basis_base_account, 0.0, 0.0),
|
||||
])
|
||||
|
||||
# There is 44.45 + 44.45 + 44.45 + 0.01 = 133.36 to reconcile on 'cash_basis_move'.
|
||||
@@ -874,8 +1162,13 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon):
|
||||
{'debit': 0.0, 'credit': 0.0, 'account_id': self.cash_basis_transfer_account.id},
|
||||
{'debit': 0.0, 'credit': 0.0, 'account_id': self.tax_account_1.id},
|
||||
# tax_2:
|
||||
{'debit': 0.01, 'credit': 0.0, 'account_id': self.cash_basis_transfer_account.id},
|
||||
{'debit': 0.0, 'credit': 0.01, 'account_id': self.tax_account_2.id},
|
||||
{'debit': 0.0, 'credit': 0.0, 'account_id': self.cash_basis_transfer_account.id},
|
||||
{'debit': 0.0, 'credit': 0.0, 'account_id': self.tax_account_2.id},
|
||||
])
|
||||
|
||||
self.assertRecordValues(res['full_reconcile'].exchange_move_id.line_ids, [
|
||||
{'account_id': self.tax_account_2.id, 'debit': 0.0, 'credit': 0.01, 'tax_ids': [], 'tax_line_id': self.cash_basis_tax_tiny_amount.id},
|
||||
{'account_id': self.cash_basis_transfer_account.id, 'debit': 0.01, 'credit': 0.0, 'tax_ids': [], 'tax_line_id': False},
|
||||
])
|
||||
|
||||
self.assertAmountsGroupByAccount([
|
||||
@@ -889,6 +1182,7 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon):
|
||||
''' Same as before with a foreign currency. '''
|
||||
|
||||
currency_id = self.currency_data['currency'].id
|
||||
taxes = self.cash_basis_tax_a_third_amount + self.cash_basis_tax_tiny_amount
|
||||
|
||||
cash_basis_move = self.env['account.move'].create({
|
||||
'move_type': 'entry',
|
||||
@@ -901,7 +1195,7 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon):
|
||||
'amount_currency': -100.0,
|
||||
'currency_id': currency_id,
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
'tax_ids': [(6, 0, (self.cash_basis_tax_a_third_amount + self.cash_basis_tax_tiny_amount).ids)],
|
||||
'tax_ids': [(6, 0, taxes.ids)],
|
||||
'tax_exigible': False,
|
||||
}),
|
||||
|
||||
@@ -1085,8 +1379,8 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon):
|
||||
self.assertEqual(len(res.get('tax_cash_basis_moves', [])), 1)
|
||||
self.assertRecordValues(res['tax_cash_basis_moves'].line_ids, [
|
||||
# Base amount of tax_1 & tax_2:
|
||||
{'debit': 0.01, 'credit': 0.0, 'amount_currency': 0.008, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
|
||||
{'debit': 0.0, 'credit': 0.01, 'amount_currency': -0.008, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
|
||||
{'debit': 0.01, 'credit': 0.0, 'amount_currency': 0.007, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
|
||||
{'debit': 0.0, 'credit': 0.01, 'amount_currency': -0.007, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
|
||||
# tax_1:
|
||||
{'debit': 0.0, 'credit': 0.0, 'amount_currency': 0.002, 'currency_id': currency_id, 'account_id': self.cash_basis_transfer_account.id},
|
||||
{'debit': 0.0, 'credit': 0.0, 'amount_currency': -0.002, 'currency_id': currency_id, 'account_id': self.tax_account_1.id},
|
||||
@@ -1095,19 +1389,26 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon):
|
||||
{'debit': 0.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': currency_id, 'account_id': self.tax_account_2.id},
|
||||
])
|
||||
|
||||
self.assertRecordValues(res['full_reconcile'].exchange_move_id.line_ids, [
|
||||
{'account_id': self.cash_basis_base_account.id, 'debit': 16.71, 'credit': 0.0, 'tax_ids': taxes.ids, 'tax_line_id': False},
|
||||
{'account_id': self.cash_basis_base_account.id, 'debit': 0.0, 'credit': 16.71, 'tax_ids': [], 'tax_line_id': False},
|
||||
{'account_id': self.tax_account_1.id, 'debit': 5.58, 'credit': 0.0, 'tax_ids': [], 'tax_line_id': self.cash_basis_tax_a_third_amount.id},
|
||||
{'account_id': self.cash_basis_transfer_account.id, 'debit': 0.0, 'credit': 5.58, 'tax_ids': [], 'tax_line_id': False},
|
||||
{'account_id': self.tax_account_2.id, 'debit': 0.0, 'credit': 0.01, 'tax_ids': [], 'tax_line_id': self.cash_basis_tax_tiny_amount.id},
|
||||
{'account_id': self.cash_basis_transfer_account.id, 'debit': 0.01, 'credit': 0.0, 'tax_ids': [], 'tax_line_id': False},
|
||||
])
|
||||
|
||||
self.assertAmountsGroupByAccount([
|
||||
# Account Balance Amount Currency
|
||||
(self.cash_basis_transfer_account, 0.0, 0.0),
|
||||
(self.tax_account_1, -16.68, -33.33),
|
||||
(self.tax_account_2, 0.0, -0.01),
|
||||
(self.tax_account_1, -11.1, -33.33),
|
||||
(self.tax_account_2, -0.01, -0.01),
|
||||
])
|
||||
|
||||
def test_reconcile_cash_basis_exchange_difference_transfer_account_not_reconcile(self):
|
||||
def test_reconcile_cash_basis_exchange_difference_transfer_account_check_entries_1(self):
|
||||
''' Test the generation of the exchange difference for a tax cash basis journal entry when the transfer
|
||||
account is not a reconcile one.
|
||||
'''
|
||||
self.cash_basis_transfer_account.reconcile = False
|
||||
|
||||
currency_id = self.currency_data['currency'].id
|
||||
|
||||
# Rate 1/3 in 2016.
|
||||
@@ -1234,27 +1535,30 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon):
|
||||
self.assertAmountsGroupByAccount([
|
||||
# Account Balance Amount Currency
|
||||
(self.cash_basis_transfer_account, 0.0, 0.0),
|
||||
(self.tax_account_1, -50.0, -100.0),
|
||||
(self.tax_account_1, -33.33, -100.0),
|
||||
])
|
||||
|
||||
def test_reconcile_cash_basis_exchange_difference_transfer_account_reconcile(self):
|
||||
def test_reconcile_cash_basis_exchange_difference_transfer_account_check_entries_2(self):
|
||||
''' Test the generation of the exchange difference for a tax cash basis journal entry when the transfer
|
||||
account is a reconcile one.
|
||||
account is not a reconcile one.
|
||||
'''
|
||||
self.cash_basis_transfer_account.reconcile = True
|
||||
currency_id = self.setup_multi_currency_data(default_values={
|
||||
'name': 'bitcoin',
|
||||
'symbol': 'bc',
|
||||
'currency_unit_label': 'Bitcoin',
|
||||
'currency_subunit_label': 'Tiny bitcoin',
|
||||
}, rate2016=0.5, rate2017=0.66666666666666)['currency'].id
|
||||
|
||||
currency_id = self.currency_data['currency'].id
|
||||
|
||||
# Rate 1/3 in 2016.
|
||||
cash_basis_move = self.env['account.move'].create({
|
||||
# Rate 2/1 in 2016.
|
||||
caba_inv = self.env['account.move'].create({
|
||||
'move_type': 'entry',
|
||||
'date': '2016-01-01',
|
||||
'line_ids': [
|
||||
# Base Tax line
|
||||
(0, 0, {
|
||||
'debit': 0.0,
|
||||
'credit': 100.0,
|
||||
'amount_currency': -300.0,
|
||||
'credit': 200.0,
|
||||
'amount_currency': -100.0,
|
||||
'currency_id': currency_id,
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
'tax_ids': [(6, 0, self.cash_basis_tax_a_third_amount.ids)],
|
||||
@@ -1264,8 +1568,8 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon):
|
||||
# Tax line
|
||||
(0, 0, {
|
||||
'debit': 0.0,
|
||||
'credit': 33.33,
|
||||
'amount_currency': -100.0,
|
||||
'credit': 20.0,
|
||||
'amount_currency': -10.0,
|
||||
'currency_id': currency_id,
|
||||
'account_id': self.cash_basis_transfer_account.id,
|
||||
'tax_repartition_line_id': self.cash_basis_tax_a_third_amount.invoice_repartition_line_ids.filtered(lambda line: line.repartition_type == 'tax').id,
|
||||
@@ -1274,87 +1578,137 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon):
|
||||
|
||||
# Receivable lines
|
||||
(0, 0, {
|
||||
'debit': 133.33,
|
||||
'debit': 220.0,
|
||||
'credit': 0.0,
|
||||
'amount_currency': 400.0,
|
||||
'amount_currency': 110.0,
|
||||
'currency_id': currency_id,
|
||||
'account_id': self.extra_receivable_account_1.id,
|
||||
}),
|
||||
]
|
||||
})
|
||||
caba_inv.action_post()
|
||||
|
||||
# Rate 1/2 in 2017.
|
||||
payment_move = self.env['account.move'].create({
|
||||
'move_type': 'entry',
|
||||
'date': '2017-01-01',
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'debit': 0.0,
|
||||
'credit': 200.0,
|
||||
'amount_currency': -400.0,
|
||||
'currency_id': currency_id,
|
||||
'account_id': self.extra_receivable_account_1.id,
|
||||
}),
|
||||
(0, 0, {
|
||||
'debit': 200.0,
|
||||
'credit': 0.0,
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
}),
|
||||
]
|
||||
# Rate 3/2 in 2017. Full payment of 110 in foreign currency
|
||||
pmt_wizard = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=caba_inv.ids).create({
|
||||
'payment_date': '2017-01-01',
|
||||
'journal_id': self.company_data['default_journal_bank'].id,
|
||||
'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
|
||||
})
|
||||
pmt_wizard._create_payments()
|
||||
partial_rec = caba_inv.mapped('line_ids.matched_credit_ids')
|
||||
caba_move = self.env['account.move'].search([('tax_cash_basis_rec_id', 'in', partial_rec.ids)])
|
||||
|
||||
(cash_basis_move + payment_move).action_post()
|
||||
|
||||
self.assertAmountsGroupByAccount([
|
||||
# Account Balance Amount Currency
|
||||
(self.cash_basis_transfer_account, -33.33, -100.0),
|
||||
(self.tax_account_1, 0.0, 0.0),
|
||||
self.assertRecordValues(caba_move.line_ids, [
|
||||
{'account_id': self.cash_basis_base_account.id, 'debit': 150.0, 'credit': 0.0, 'amount_currency': 100.0, 'tax_ids': [], 'tax_line_id': False},
|
||||
{'account_id': self.cash_basis_base_account.id, 'debit': 0.0, 'credit': 150.0, 'amount_currency': -100.0, 'tax_ids': self.cash_basis_tax_a_third_amount.ids, 'tax_line_id': False},
|
||||
{'account_id': self.cash_basis_transfer_account.id, 'debit': 15.0, 'credit': 0.0, 'amount_currency': 10.0, 'tax_ids': [], 'tax_line_id': False},
|
||||
{'account_id': self.tax_account_1.id, 'debit': 0.0, 'credit': 15.0, 'amount_currency': -10.0, 'tax_ids': [], 'tax_line_id': self.cash_basis_tax_a_third_amount.id},
|
||||
])
|
||||
|
||||
receivable_lines = (cash_basis_move + payment_move).line_ids\
|
||||
.filtered(lambda line: line.account_id == self.extra_receivable_account_1)
|
||||
res = receivable_lines.reconcile()
|
||||
receivable_line = caba_inv.line_ids.filtered(lambda x: x.account_id.internal_type == 'receivable')
|
||||
self.assertTrue(receivable_line.full_reconcile_id, "Invoice should be fully paid")
|
||||
|
||||
self.assertFullReconcile(res['full_reconcile'], receivable_lines)
|
||||
self.assertEqual(len(res.get('tax_cash_basis_moves', [])), 1)
|
||||
self.assertRecordValues(res['tax_cash_basis_moves'].line_ids, [
|
||||
# Base amount:
|
||||
{'debit': 150.0, 'credit': 0.0, 'amount_currency': 300.0, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
|
||||
{'debit': 0.0, 'credit': 150.0, 'amount_currency': -300.0, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
|
||||
# tax:
|
||||
{'debit': 50.0, 'credit': 0.0, 'amount_currency': 100.0, 'currency_id': currency_id, 'account_id': self.cash_basis_transfer_account.id},
|
||||
{'debit': 0.0, 'credit': 50.0, 'amount_currency': -100.0, 'currency_id': currency_id, 'account_id': self.tax_account_1.id},
|
||||
])
|
||||
|
||||
transfer_lines = (cash_basis_move + res['tax_cash_basis_moves']).line_ids\
|
||||
.filtered(lambda line: line.account_id == self.cash_basis_transfer_account)
|
||||
self.assertTrue(transfer_lines.full_reconcile_id)
|
||||
self.assertFullReconcile(transfer_lines.full_reconcile_id, transfer_lines)
|
||||
|
||||
transfer_exchange_diff = transfer_lines.full_reconcile_id.exchange_move_id
|
||||
transfer_exchange_diff_lines = transfer_exchange_diff.line_ids.sorted(lambda line: (line.account_id, -line.balance))
|
||||
|
||||
self.assertRecordValues(transfer_exchange_diff_lines, [
|
||||
{
|
||||
'debit': 0.0,
|
||||
'credit': 16.67,
|
||||
'amount_currency': 0.0,
|
||||
'currency_id': currency_id,
|
||||
'account_id': self.cash_basis_transfer_account.id,
|
||||
},
|
||||
{
|
||||
'debit': 16.67,
|
||||
'credit': 0.0,
|
||||
'amount_currency': 0.0,
|
||||
'currency_id': currency_id,
|
||||
'account_id': transfer_exchange_diff.journal_id.company_id.expense_currency_exchange_account_id.id,
|
||||
},
|
||||
exchange_move = receivable_line.full_reconcile_id.exchange_move_id
|
||||
self.assertTrue(exchange_move, "There should be an exchange difference move created")
|
||||
self.assertRecordValues(exchange_move.line_ids, [
|
||||
{'account_id': receivable_line.account_id.id, 'debit': 0.0, 'credit': 55.0, 'amount_currency': 0.0, 'tax_ids': [], 'tax_line_id': False},
|
||||
{'account_id': caba_move.company_id.expense_currency_exchange_account_id.id, 'debit': 55.0, 'credit': 0.0, 'amount_currency': 0.0, 'tax_ids': [], 'tax_line_id': False},
|
||||
{'account_id': self.cash_basis_base_account.id, 'debit': 0.0, 'credit': 50.0, 'amount_currency': 0.0, 'tax_ids': self.cash_basis_tax_a_third_amount.ids, 'tax_line_id': False},
|
||||
{'account_id': self.cash_basis_base_account.id, 'debit': 50.0, 'credit': 0.0, 'amount_currency': 0.0, 'tax_ids': [], 'tax_line_id': False},
|
||||
{'account_id': self.tax_account_1.id, 'debit': 0.0, 'credit': 5.0, 'amount_currency': 0.0, 'tax_ids': [], 'tax_line_id': self.cash_basis_tax_a_third_amount.id},
|
||||
{'account_id': self.cash_basis_transfer_account.id, 'debit': 5.0, 'credit': 0.0, 'amount_currency': 0.0, 'tax_ids': [], 'tax_line_id': False},
|
||||
])
|
||||
|
||||
self.assertAmountsGroupByAccount([
|
||||
# Account Balance Amount Currency
|
||||
(self.cash_basis_transfer_account, 0.0, 0.0),
|
||||
(self.tax_account_1, -50.0, -100.0),
|
||||
(self.tax_account_1, -20.0, -10.0),
|
||||
])
|
||||
|
||||
def test_reconcile_cash_basis_exchange_difference_transfer_account_check_entries_3(self):
|
||||
''' Test the generation of the exchange difference for a tax cash basis journal entry when the transfer
|
||||
account is not a reconcile one.
|
||||
'''
|
||||
currency_id = self.setup_multi_currency_data(default_values={
|
||||
'name': 'bitcoin',
|
||||
'symbol': 'bc',
|
||||
'currency_unit_label': 'Bitcoin',
|
||||
'currency_subunit_label': 'Tiny bitcoin',
|
||||
'rounding': 0.01,
|
||||
}, rate2016=0.5, rate2017=0.66666666666666)['currency'].id
|
||||
|
||||
# Rate 2/1 in 2016.
|
||||
caba_inv = self.env['account.move'].create({
|
||||
'move_type': 'entry',
|
||||
'date': '2016-01-01',
|
||||
'line_ids': [
|
||||
# Base Tax line
|
||||
(0, 0, {
|
||||
'debit': 0.0,
|
||||
'credit': 200.0,
|
||||
'amount_currency': -100.0,
|
||||
'currency_id': currency_id,
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
'tax_ids': [(6, 0, self.cash_basis_tax_a_third_amount.ids)],
|
||||
'tax_exigible': False,
|
||||
}),
|
||||
|
||||
# Tax line
|
||||
(0, 0, {
|
||||
'debit': 0.0,
|
||||
'credit': 20.0,
|
||||
'amount_currency': -10.0,
|
||||
'currency_id': currency_id,
|
||||
'account_id': self.cash_basis_transfer_account.id,
|
||||
'tax_repartition_line_id': self.cash_basis_tax_a_third_amount.invoice_repartition_line_ids.filtered(lambda line: line.repartition_type == 'tax').id,
|
||||
'tax_exigible': False,
|
||||
}),
|
||||
|
||||
# Receivable lines
|
||||
(0, 0, {
|
||||
'debit': 220.0,
|
||||
'credit': 0.0,
|
||||
'amount_currency': 110.0,
|
||||
'currency_id': currency_id,
|
||||
'account_id': self.extra_receivable_account_1.id,
|
||||
}),
|
||||
]
|
||||
})
|
||||
caba_inv.action_post()
|
||||
|
||||
# Rate 3/2 in 2017. Full payment of 220 in company currency
|
||||
pmt_wizard = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=caba_inv.ids).create({
|
||||
'payment_date': '2017-01-01',
|
||||
'journal_id': self.company_data['default_journal_bank'].id,
|
||||
'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
|
||||
'currency_id': self.company_data['currency'].id,
|
||||
'amount': 220.0,
|
||||
})
|
||||
pmt_wizard._create_payments()
|
||||
|
||||
caba_move = self.env['account.move'].search([('tax_cash_basis_rec_id', 'in', caba_inv.line_ids.matched_credit_ids.ids)])
|
||||
self.assertRecordValues(caba_move.line_ids, [
|
||||
{'account_id': self.cash_basis_base_account.id, 'debit': 200.01, 'credit': 0.0, 'amount_currency': 133.34, 'tax_ids': [], 'tax_line_id': False},
|
||||
{'account_id': self.cash_basis_base_account.id, 'debit': 0.0, 'credit': 200.01, 'amount_currency': -133.34, 'tax_ids': self.cash_basis_tax_a_third_amount.ids, 'tax_line_id': False},
|
||||
{'account_id': self.cash_basis_transfer_account.id, 'debit': 20.0, 'credit': 0.0, 'amount_currency': 13.33, 'tax_ids': [], 'tax_line_id': False},
|
||||
{'account_id': self.tax_account_1.id, 'debit': 0.0, 'credit': 20.0, 'amount_currency': -13.33, 'tax_ids': [], 'tax_line_id': self.cash_basis_tax_a_third_amount.id},
|
||||
])
|
||||
|
||||
receivable_line = caba_inv.line_ids.filtered(lambda x: x.account_id.internal_type == 'receivable')
|
||||
self.assertTrue(receivable_line.full_reconcile_id, "Invoice should be fully paid")
|
||||
|
||||
exchange_move = receivable_line.full_reconcile_id.exchange_move_id
|
||||
self.assertRecordValues(exchange_move.line_ids, [
|
||||
{'account_id': self.extra_receivable_account_1.id, 'debit': 0.0, 'credit': 0.0, 'amount_currency': 36.67, 'tax_ids': [], 'tax_line_id': False},
|
||||
{'account_id': caba_move.company_id.income_currency_exchange_account_id.id, 'debit': 0.0, 'credit': 0.0, 'amount_currency': -36.67, 'tax_ids': [], 'tax_line_id': False},
|
||||
{'account_id': self.cash_basis_base_account.id, 'debit': 0.01, 'credit': 0.0, 'amount_currency': 0.0, 'tax_ids': self.cash_basis_tax_a_third_amount.ids, 'tax_line_id': False},
|
||||
{'account_id': self.cash_basis_base_account.id, 'debit': 0.0, 'credit': 0.01, 'amount_currency': 0.0, 'tax_ids': [], 'tax_line_id': False},
|
||||
])
|
||||
|
||||
self.assertAmountsGroupByAccount([
|
||||
# Account Balance Amount Currency
|
||||
(self.cash_basis_transfer_account, 0.0, 3.33),
|
||||
(self.tax_account_1, -20.0, -13.33),
|
||||
])
|
||||
|
||||
def test_reconcile_cash_basis_revert(self):
|
||||
@@ -1767,4 +2121,4 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon):
|
||||
lines_to_reconcile.reconcile()
|
||||
|
||||
# Check full reconciliation
|
||||
self.assertTrue(all(line.full_reconcile_id for line in lines_to_reconcile), "All tax lines should be fully reconciled")
|
||||
self.assertTrue(all(line.full_reconcile_id for line in lines_to_reconcile), "All tax lines should be fully reconciled")
|
||||
|
||||
@@ -119,6 +119,7 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon):
|
||||
})._create_payments()
|
||||
|
||||
self.assertRecordValues(payments, [{
|
||||
'ref': 'INV/2017/01/0001 INV/2017/01/0002',
|
||||
'payment_method_id': self.custom_payment_method_in.id,
|
||||
}])
|
||||
self.assertRecordValues(payments.line_ids.sorted('balance'), [
|
||||
@@ -152,6 +153,7 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon):
|
||||
})._create_payments()
|
||||
|
||||
self.assertRecordValues(payments, [{
|
||||
'ref': 'INV/2017/01/0001 INV/2017/01/0002',
|
||||
'payment_method_id': self.custom_payment_method_in.id,
|
||||
}])
|
||||
self.assertRecordValues(payments.line_ids.sorted('balance'), [
|
||||
@@ -186,6 +188,7 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon):
|
||||
})._create_payments()
|
||||
|
||||
self.assertRecordValues(payments, [{
|
||||
'ref': 'INV/2017/01/0001 INV/2017/01/0002',
|
||||
'payment_method_id': self.custom_payment_method_in.id,
|
||||
}])
|
||||
self.assertRecordValues(payments.line_ids.sorted('balance'), [
|
||||
@@ -228,6 +231,7 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon):
|
||||
})._create_payments()
|
||||
|
||||
self.assertRecordValues(payments, [{
|
||||
'ref': 'INV/2017/01/0001 INV/2017/01/0002',
|
||||
'payment_method_id': self.custom_payment_method_in.id,
|
||||
}])
|
||||
self.assertRecordValues(payments.line_ids.sorted('balance'), [
|
||||
@@ -270,6 +274,7 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon):
|
||||
})._create_payments()
|
||||
|
||||
self.assertRecordValues(payments, [{
|
||||
'ref': 'BILL/2017/01/0001 BILL/2017/01/0002',
|
||||
'payment_method_id': self.custom_payment_method_in.id,
|
||||
}])
|
||||
self.assertRecordValues(payments.line_ids.sorted('balance'), [
|
||||
@@ -312,6 +317,7 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon):
|
||||
})._create_payments()
|
||||
|
||||
self.assertRecordValues(payments, [{
|
||||
'ref': 'BILL/2017/01/0001 BILL/2017/01/0002',
|
||||
'payment_method_id': self.custom_payment_method_in.id,
|
||||
}])
|
||||
self.assertRecordValues(payments.line_ids.sorted('balance'), [
|
||||
@@ -349,8 +355,14 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon):
|
||||
})._create_payments()
|
||||
|
||||
self.assertRecordValues(payments, [
|
||||
{'payment_method_id': self.manual_payment_method_in.id},
|
||||
{'payment_method_id': self.manual_payment_method_in.id},
|
||||
{
|
||||
'ref': 'INV/2017/01/0001',
|
||||
'payment_method_id': self.manual_payment_method_in.id,
|
||||
},
|
||||
{
|
||||
'ref': 'INV/2017/01/0002',
|
||||
'payment_method_id': self.manual_payment_method_in.id,
|
||||
},
|
||||
])
|
||||
self.assertRecordValues(payments[0].line_ids.sorted('balance') + payments[1].line_ids.sorted('balance'), [
|
||||
# == Payment 1: to pay out_invoice_1 ==
|
||||
@@ -399,8 +411,14 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon):
|
||||
})._create_payments()
|
||||
|
||||
self.assertRecordValues(payments, [
|
||||
{'payment_method_id': self.manual_payment_method_out.id},
|
||||
{'payment_method_id': self.manual_payment_method_out.id},
|
||||
{
|
||||
'ref': 'BILL/2017/01/0001 BILL/2017/01/0002',
|
||||
'payment_method_id': self.manual_payment_method_out.id,
|
||||
},
|
||||
{
|
||||
'ref': 'BILL/2017/01/0003',
|
||||
'payment_method_id': self.manual_payment_method_out.id,
|
||||
},
|
||||
])
|
||||
self.assertRecordValues(payments[0].line_ids.sorted('balance') + payments[1].line_ids.sorted('balance'), [
|
||||
# == Payment 1: to pay in_invoice_1 & in_invoice_2 ==
|
||||
@@ -449,9 +467,18 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon):
|
||||
})._create_payments()
|
||||
|
||||
self.assertRecordValues(payments, [
|
||||
{'payment_method_id': self.manual_payment_method_out.id},
|
||||
{'payment_method_id': self.manual_payment_method_out.id},
|
||||
{'payment_method_id': self.manual_payment_method_out.id},
|
||||
{
|
||||
'ref': 'BILL/2017/01/0001',
|
||||
'payment_method_id': self.manual_payment_method_out.id,
|
||||
},
|
||||
{
|
||||
'ref': 'BILL/2017/01/0002',
|
||||
'payment_method_id': self.manual_payment_method_out.id,
|
||||
},
|
||||
{
|
||||
'ref': 'BILL/2017/01/0003',
|
||||
'payment_method_id': self.manual_payment_method_out.id,
|
||||
},
|
||||
])
|
||||
self.assertRecordValues(payments[0].line_ids.sorted('balance') + payments[1].line_ids.sorted('balance') + payments[2].line_ids.sorted('balance'), [
|
||||
# == Payment 1: to pay in_invoice_1 ==
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from flectra import fields
|
||||
from flectra.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from flectra.tests import tagged, Form
|
||||
|
||||
@@ -668,54 +667,3 @@ class TestInvoiceTaxes(AccountTestInvoicingCommon):
|
||||
(50, self.percent_tax_3_incl),
|
||||
], currency_id=self.currency_data['currency'], invoice_payment_term_id=self.pay_terms_a)
|
||||
invoice.action_post()
|
||||
|
||||
def test_changing_tax_to_caba_after_create_invoice(self):
|
||||
tax_waiting_account = self.env['account.account'].create({
|
||||
'name': 'TAX_WAIT',
|
||||
'code': 'TWAIT',
|
||||
'user_type_id': self.env.ref('account.data_account_type_current_liabilities').id,
|
||||
'reconcile': True,
|
||||
'company_id': self.company_data['company'].id,
|
||||
})
|
||||
sale_tax = self.company_data['default_tax_sale']
|
||||
invoice = self._create_invoice([
|
||||
(100, sale_tax),
|
||||
])
|
||||
# turn on cash basis on the same tax that was used
|
||||
self.company_data['company'].tax_exigibility = True
|
||||
sale_tax.write({
|
||||
'tax_exigibility': 'on_payment',
|
||||
'cash_basis_transition_account_id': tax_waiting_account.id,
|
||||
})
|
||||
invoice.action_post()
|
||||
|
||||
tax_lines = invoice.line_ids.filtered('tax_line_id')
|
||||
self.assertEqual(len(tax_lines), 1, 'Should have only 1 tax line')
|
||||
self.assertNotEqual(tax_lines.price_unit, 0.0)
|
||||
self.assertEqual(tax_lines.tax_base_amount, 100.0)
|
||||
self.assertEqual(tax_lines.account_id.name, 'TAX_WAIT')
|
||||
|
||||
def test_recompute_tax_when_change_currency(self):
|
||||
"""
|
||||
- In a multi currency company, create an invoice with the domestic
|
||||
currency ($);
|
||||
- Add a product (100$) with taxes;
|
||||
- Change the currency to a foreign one (€);
|
||||
- The product don't change of amount but only of currency (500€);
|
||||
- Save the invoice, and print it.
|
||||
"""
|
||||
invoice = self._create_invoice([
|
||||
(100, self.percent_tax_1),
|
||||
])
|
||||
tax_line = invoice.line_ids.filtered('tax_line_id')
|
||||
self.env['res.currency.rate'].create({
|
||||
'name': fields.Date.to_string(fields.Date.today()),
|
||||
'rate': 4.0,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
with Form(invoice) as invoice_form:
|
||||
invoice_form.currency_id = self.currency_data['currency']
|
||||
|
||||
self.assertEqual(tax_line.tax_base_amount, 25, 'Should convert tax base amount to company currency')
|
||||
self.assertEqual(tax_line.balance, -5.25, 'Should convert balance to company currency')
|
||||
|
||||
@@ -711,7 +711,7 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
|
||||
'company_id': self.company.id
|
||||
})
|
||||
inv1 = self.create_invoice(invoice_amount=800, currency_id=self.currency_usd_id)
|
||||
inv2 = self.create_invoice(type="out_refund", invoice_amount=400, currency_id=self.currency_usd_id)
|
||||
inv2 = self.create_invoice(move_type="out_refund", invoice_amount=400, currency_id=self.currency_usd_id)
|
||||
|
||||
payment = self.env['account.payment'].create({
|
||||
'date': time.strftime('%Y') + '-07-15',
|
||||
@@ -777,7 +777,7 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
|
||||
'company_id': company.id
|
||||
})
|
||||
inv1 = self.create_invoice(invoice_amount=658, currency_id=self.currency_usd_id)
|
||||
inv2 = self.create_invoice(type="out_refund", invoice_amount=225, currency_id=self.currency_usd_id)
|
||||
inv2 = self.create_invoice(move_type="out_refund", invoice_amount=225, currency_id=self.currency_usd_id)
|
||||
|
||||
payment = self.env['account.payment'].create({
|
||||
'payment_method_id': self.inbound_payment_method.id,
|
||||
@@ -851,7 +851,7 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
|
||||
'company_id': company.id
|
||||
})
|
||||
inv1 = self._create_invoice(invoice_amount=658, currency_id=self.currency_usd_id, date_invoice=time.strftime('%Y') + '-07-01', auto_validate=True)
|
||||
inv2 = self._create_invoice(type="out_refund", invoice_amount=225, currency_id=self.currency_usd_id, date_invoice=time.strftime('%Y') + '-07-15', auto_validate=True)
|
||||
inv2 = self._create_invoice(move_type="out_refund", invoice_amount=225, currency_id=self.currency_usd_id, date_invoice=time.strftime('%Y') + '-07-15', auto_validate=True)
|
||||
|
||||
payment = self.env['account.payment'].create({
|
||||
'date': time.strftime('%Y') + '-07-15',
|
||||
@@ -923,7 +923,7 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
|
||||
})
|
||||
|
||||
inv1 = self._create_invoice(invoice_amount=600, currency_id=self.currency_usd_id, date_invoice=time.strftime('%Y') + '-07-15', auto_validate=True)
|
||||
inv2 = self._create_invoice(type="out_refund", invoice_amount=250, currency_id=self.currency_usd_id, date_invoice=time.strftime('%Y') + '-07-15', auto_validate=True)
|
||||
inv2 = self._create_invoice(move_type="out_refund", invoice_amount=250, currency_id=self.currency_usd_id, date_invoice=time.strftime('%Y') + '-07-15', auto_validate=True)
|
||||
|
||||
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')
|
||||
@@ -981,7 +981,7 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
|
||||
'company_id': company.id
|
||||
})
|
||||
inv1 = self._create_invoice(invoice_amount=600, currency_id=self.currency_usd_id, date_invoice=time.strftime('%Y') + '-07-15', auto_validate=True)
|
||||
inv2 = self._create_invoice(type="out_refund", invoice_amount=250, currency_id=self.currency_usd_id, date_invoice=time.strftime('%Y') + '-07-15', auto_validate=True)
|
||||
inv2 = self._create_invoice(move_type="out_refund", invoice_amount=250, currency_id=self.currency_usd_id, date_invoice=time.strftime('%Y') + '-07-15', auto_validate=True)
|
||||
|
||||
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')
|
||||
@@ -1051,7 +1051,7 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
|
||||
'company_id': company.id
|
||||
})
|
||||
inv1 = self._create_invoice(invoice_amount=600, currency_id=foreign_1.id, date_invoice=time.strftime('%Y') + '-07-15', auto_validate=True)
|
||||
inv2 = self._create_invoice(type="out_refund", invoice_amount=250, currency_id=foreign_1.id, date_invoice=time.strftime('%Y') + '-07-15', auto_validate=True)
|
||||
inv2 = self._create_invoice(move_type="out_refund", invoice_amount=250, currency_id=foreign_1.id, date_invoice=time.strftime('%Y') + '-07-15', auto_validate=True)
|
||||
|
||||
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')
|
||||
@@ -1205,4 +1205,4 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
|
||||
exchange_rcv = inv1_receivable.full_reconcile_id.exchange_move_id.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
|
||||
self.assertEqual(exchange_rcv.amount_currency, 0.01)
|
||||
|
||||
self.assertTrue(inv1.payment_state in ('in_payment', 'paid'), "Invoice should be paid")
|
||||
self.assertTrue(inv1.payment_state in ('in_payment', 'paid'), "Invoice should be paid")
|
||||
|
||||
@@ -96,12 +96,14 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
|
||||
'journal_id': cls.bank_journal.id,
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'date': '2020-01-01',
|
||||
'payment_ref': 'invoice %s-%s-%s' % tuple(invoice_number.split('/')[1:]),
|
||||
'partner_id': cls.partner_1.id,
|
||||
'amount': 100,
|
||||
'sequence': 1,
|
||||
}),
|
||||
(0, 0, {
|
||||
'date': '2020-01-01',
|
||||
'payment_ref': 'xxxxx',
|
||||
'partner_id': cls.partner_1.id,
|
||||
'amount': 600,
|
||||
@@ -113,6 +115,7 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
|
||||
'journal_id': cls.bank_journal.id,
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'date': '2020-01-01',
|
||||
'payment_ref': 'nawak',
|
||||
'narration': 'Communication: RF12 3456',
|
||||
'partner_id': cls.partner_3.id,
|
||||
@@ -120,12 +123,14 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
|
||||
'sequence': 1,
|
||||
}),
|
||||
(0, 0, {
|
||||
'date': '2020-01-01',
|
||||
'payment_ref': 'RF12 3456',
|
||||
'partner_id': cls.partner_3.id,
|
||||
'amount': 600,
|
||||
'sequence': 2,
|
||||
}),
|
||||
(0, 0, {
|
||||
'date': '2020-01-01',
|
||||
'payment_ref': 'baaaaah',
|
||||
'ref': 'RF12 3456',
|
||||
'partner_id': cls.partner_3.id,
|
||||
@@ -138,6 +143,7 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
|
||||
'journal_id': cls.cash_journal.id,
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'date': '2020-01-01',
|
||||
'payment_ref': 'yyyyy',
|
||||
'partner_id': cls.partner_2.id,
|
||||
'amount': -1000,
|
||||
@@ -181,6 +187,7 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
|
||||
self.cash_st.balance_end_real = self.cash_st.balance_end
|
||||
(self.bank_st + self.bank_st_2 + self.cash_st).button_post()
|
||||
|
||||
@freeze_time('2020-01-01')
|
||||
def _check_statement_matching(self, rules, expected_values, statements=None):
|
||||
if statements is None:
|
||||
statements = self.bank_st + self.cash_st
|
||||
@@ -448,12 +455,14 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
|
||||
])
|
||||
|
||||
def test_larger_invoice_auto_reconcile(self):
|
||||
''' Test auto reconciliation with an invoice with larger amount than the statement line's.'''
|
||||
''' Test auto reconciliation with an invoice with larger amount than the
|
||||
statement line's, for rules without write-offs.'''
|
||||
self.bank_line_1.amount = 40
|
||||
self.invoice_line_1.move_id.payment_reference = self.bank_line_1.payment_ref
|
||||
|
||||
self.rule_1.sequence = 2
|
||||
self.rule_1.auto_reconcile = True
|
||||
self.rule_1.line_ids = [(5, 0, 0)]
|
||||
|
||||
self._check_statement_matching(self.rule_1, {
|
||||
self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'status': 'reconciled', 'partner': self.bank_line_1.partner_id},
|
||||
@@ -565,6 +574,7 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
|
||||
self.bank_line_1.write({
|
||||
'payment_ref': 'Tournicoti66',
|
||||
'partner_id': None,
|
||||
'amount': 95,
|
||||
})
|
||||
|
||||
self.rule_1.write({
|
||||
@@ -579,6 +589,39 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
|
||||
self.bank_line_2.id: {'aml_ids': []},
|
||||
}, self.bank_st)
|
||||
|
||||
def test_inv_matching_rule_auto_rec_no_partner_with_writeoff(self):
|
||||
self.invoice_line_1.move_id.write({'payment_reference': 'doudlidou355'})
|
||||
|
||||
self.bank_line_1.write({
|
||||
'payment_ref': 'doudlidou355',
|
||||
'partner_id': None,
|
||||
'amount': 95,
|
||||
})
|
||||
|
||||
self.rule_1.write({
|
||||
'match_partner': False,
|
||||
'match_label': 'contains',
|
||||
'match_label_param': 'doudlidou', # So that we only match what we want to test
|
||||
'match_total_amount_param': 90,
|
||||
'auto_reconcile': True,
|
||||
})
|
||||
|
||||
# Check bank reconciliation
|
||||
|
||||
self._check_statement_matching(self.rule_1, {
|
||||
self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'partner': self.bank_line_1.partner_id, 'status': 'reconciled'},
|
||||
self.bank_line_2.id: {'aml_ids': []},
|
||||
}, self.bank_st)
|
||||
|
||||
# Check invoice line has been fully reconciled, with a write-off.
|
||||
self.assertRecordValues(self.bank_line_1.line_ids, [
|
||||
{'partner_id': self.partner_1.id, 'debit': 95.0, 'credit': 0.0, 'account_id': self.bank_journal.default_account_id.id, 'reconciled': False},
|
||||
{'partner_id': self.partner_1.id, 'debit': 5.0, 'credit': 0.0, 'account_id': self.current_assets_account.id, 'reconciled': False},
|
||||
{'partner_id': self.partner_1.id, 'debit': 0.0, 'credit': 100.0, 'account_id': self.invoice_line_1.account_id.id, 'reconciled': True},
|
||||
])
|
||||
|
||||
self.assertEqual(self.invoice_line_1.amount_residual, 0.0, "The invoice should have been fully reconciled")
|
||||
|
||||
def test_partner_mapping_rule(self):
|
||||
self.bank_line_1.write({'partner_id': None, 'payment_ref': 'toto42', 'narration': None})
|
||||
self.bank_line_2.write({'partner_id': None})
|
||||
@@ -642,6 +685,18 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
|
||||
self.bank_line_2.id: {'aml_ids': []},
|
||||
}, self.bank_st)
|
||||
|
||||
def test_partner_name_with_regexp_chars(self):
|
||||
self.invoice_line_1.partner_id.write({'name': "Archibald + Haddock"})
|
||||
self.bank_line_1.write({'partner_id': None, 'payment_ref': '1234//HADDOCK+Archibald'})
|
||||
self.bank_line_2.write({'partner_id': None})
|
||||
self.rule_1.write({'match_partner': False})
|
||||
|
||||
# The query should still work
|
||||
self._check_statement_matching(self.rule_1, {
|
||||
self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'partner': self.bank_line_1.partner_id},
|
||||
self.bank_line_2.id: {'aml_ids': []},
|
||||
}, self.bank_st)
|
||||
|
||||
def test_match_multi_currencies(self):
|
||||
''' Ensure the matching of candidates is made using the right statement line currency.
|
||||
|
||||
@@ -671,6 +726,7 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
|
||||
'match_total_amount_param': 95.0,
|
||||
'match_same_currency': False,
|
||||
'company_id': self.company_data['company'].id,
|
||||
'past_months_limit': False,
|
||||
})
|
||||
|
||||
statement = self.env['account.bank.statement'].create({
|
||||
@@ -695,7 +751,7 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
|
||||
move = self.env['account.move'].create({
|
||||
'move_type': 'entry',
|
||||
'date': '2017-01-01',
|
||||
'journal_id': self.company_data['default_journal_sale'].id,
|
||||
'journal_id': self.company_data['default_journal_misc'].id,
|
||||
'line_ids': [
|
||||
# Rate is 2 GOL = 1 USD in 2017.
|
||||
# The statement line will consider this line equivalent to 600 DAR.
|
||||
@@ -729,11 +785,11 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
|
||||
move_line_1 = move.line_ids.filtered(lambda line: line.debit == 100.0)
|
||||
move_line_2 = move.line_ids.filtered(lambda line: line.debit == 14.0)
|
||||
|
||||
with freeze_time('2017-01-01'):
|
||||
self._check_statement_matching(matching_rule, {
|
||||
statement_line.id: {'aml_ids': (move_line_1 + move_line_2).ids, 'model': matching_rule, 'partner': statement_line.partner_id}
|
||||
}, statements=statement)
|
||||
self._check_statement_matching(matching_rule, {
|
||||
statement_line.id: {'aml_ids': (move_line_1 + move_line_2).ids, 'model': matching_rule, 'partner': statement_line.partner_id}
|
||||
}, statements=statement)
|
||||
|
||||
@freeze_time('2020-01-01')
|
||||
def test_inv_matching_with_write_off(self):
|
||||
self.rule_1.match_total_amount_param = 90
|
||||
self.bank_st.line_ids[1].unlink() # We don't need this one here
|
||||
@@ -767,3 +823,56 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
|
||||
}
|
||||
|
||||
self.assertDictEqual(expected_write_off, to_compare)
|
||||
|
||||
def test_inv_matching_with_write_off_autoreconcile(self):
|
||||
self.bank_line_1.amount = 95
|
||||
|
||||
self.rule_1.sequence = 2
|
||||
self.rule_1.auto_reconcile = True
|
||||
self.rule_1.match_total_amount_param = 90
|
||||
|
||||
self._check_statement_matching(self.rule_1, {
|
||||
self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'status': 'reconciled', 'partner': self.bank_line_1.partner_id},
|
||||
self.bank_line_2.id: {'aml_ids': []},
|
||||
}, statements=self.bank_st)
|
||||
|
||||
# Check first line has been properly reconciled.
|
||||
self.assertRecordValues(self.bank_line_1.line_ids, [
|
||||
{'partner_id': self.partner_1.id, 'debit': 95.0, 'credit': 0.0, 'account_id': self.bank_journal.default_account_id.id, 'reconciled': False},
|
||||
{'partner_id': self.partner_1.id, 'debit': 5.0, 'credit': 0.0, 'account_id': self.current_assets_account.id, 'reconciled': False},
|
||||
{'partner_id': self.partner_1.id, 'debit': 0.0, 'credit': 100.0, 'account_id': self.invoice_line_1.account_id.id, 'reconciled': True},
|
||||
])
|
||||
|
||||
self.assertEqual(self.invoice_line_1.amount_residual, 0.0, "The invoice should have been fully reconciled")
|
||||
|
||||
def test_avoid_amount_matching_bypass(self):
|
||||
""" By the default, if the label of statement lines exactly matches a payment reference, it bypasses any kind of amount verification.
|
||||
This is annoying in some setups, so a config parameter was introduced to handle that.
|
||||
"""
|
||||
self.env['ir.config_parameter'].set_param('account.disable_rec_models_bypass', '1')
|
||||
self.rule_1.match_total_amount_param = 90
|
||||
second_inv_matching_rule = self.env['account.reconcile.model'].create({
|
||||
'name': 'Invoices Matching Rule',
|
||||
'sequence': 2,
|
||||
'rule_type': 'invoice_matching',
|
||||
'auto_reconcile': False,
|
||||
'match_nature': 'both',
|
||||
'match_same_currency': False,
|
||||
'match_total_amount': False,
|
||||
'match_partner': True,
|
||||
'company_id': self.company.id,
|
||||
})
|
||||
|
||||
self.bank_line_1.write({
|
||||
'payment_ref': self.invoice_line_1.move_id.payment_reference,
|
||||
'amount': 99,
|
||||
})
|
||||
self.bank_line_2.write({
|
||||
'payment_ref': self.invoice_line_2.move_id.payment_reference,
|
||||
'amount': 1,
|
||||
})
|
||||
|
||||
self._check_statement_matching(self.rule_1 + second_inv_matching_rule, {
|
||||
self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'status': 'write_off', 'partner': self.bank_line_1.partner_id},
|
||||
self.bank_line_2.id: {'aml_ids': [self.invoice_line_2.id], 'model': second_inv_matching_rule, 'partner': self.bank_line_2.partner_id}
|
||||
}, statements=self.bank_st)
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
<filter string="Validated" name="confirmed" domain="[('state','=','confirm')]"/>
|
||||
<separator/>
|
||||
<filter name="filter_date" date="date"/>
|
||||
<field name="journal_id" domain="[('type', '=', 'bank')]" />
|
||||
<field name="journal_id" domain="[('type', 'in', ('bank', 'cash'))]" />
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Journal" name="journal" context="{'group_by': 'journal_id'}"/>
|
||||
<filter string="Status" name="status" context="{'group_by': 'state'}"/>
|
||||
|
||||
@@ -60,7 +60,8 @@
|
||||
<span role="separator">View</span>
|
||||
</div>
|
||||
<div>
|
||||
<a role="menuitem" type="object" name="open_action_with_context" context="{'action_name': 'action_bank_statement_tree', 'search_default_journal': True}">Statements</a>
|
||||
<a t-if="journal_type == 'bank'" role="menuitem" type="object" name="open_action_with_context" context="{'action_name': 'action_bank_statement_tree', 'search_default_journal': True}">Statements</a>
|
||||
<a t-if="journal_type == 'cash'" role="menuitem" type="object" name="open_action_with_context" context="{'action_name': 'action_view_bank_statement_tree', 'search_default_journal': True}">Statements</a>
|
||||
</div>
|
||||
<div>
|
||||
<a role="menuitem" type="object" name="open_action_with_context" context="{'action_name': 'action_bank_statement_line', 'search_default_journal': True}">Operations</a>
|
||||
|
||||
@@ -60,11 +60,11 @@
|
||||
<field name="loss_account_id" attrs="{'invisible': [('type', '!=', 'cash')]}"/>
|
||||
<!-- Sales -->
|
||||
<field name="default_account_id" string="Default Income Account"
|
||||
attrs="{'required': [('id', '!=', False), ('type', '=', 'sale')], 'invisible': [('type', '!=', 'sale')]}"
|
||||
attrs="{'required': [('type', '=', 'sale')], 'invisible': [('type', '!=', 'sale')]}"
|
||||
groups="account.group_account_readonly"/>
|
||||
<!-- Purchase -->
|
||||
<field name="default_account_id" string="Default Expense Account"
|
||||
attrs="{'required': [('id', '!=', False), ('type', '=', 'purchase')], 'invisible': [('type', '!=', 'purchase')]}"
|
||||
attrs="{'required': [('type', '=', 'purchase')], 'invisible': [('type', '!=', 'purchase')]}"
|
||||
groups="account.group_account_readonly"/>
|
||||
<field name="refund_sequence" attrs="{'invisible': [('type', 'not in', ['sale', 'purchase'])]}"/>
|
||||
<field name="code"/>
|
||||
|
||||
@@ -529,13 +529,11 @@
|
||||
type="object"
|
||||
string="Send & Print"
|
||||
attrs="{'invisible':['|', '|', ('state', '!=', 'posted'), ('is_move_sent', '=', True), ('move_type', 'not in', ('out_invoice', 'out_refund'))]}"
|
||||
class="oe_highlight"
|
||||
groups="account.group_account_invoice"/>
|
||||
class="oe_highlight"/>
|
||||
<button name="action_invoice_sent"
|
||||
type="object"
|
||||
string="Send & Print"
|
||||
attrs="{'invisible':['|', '|', ('state', '!=', 'posted'), ('is_move_sent', '=', False), ('move_type', 'not in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund'))]}"
|
||||
groups="account.group_account_invoice"/>
|
||||
attrs="{'invisible':['|', '|', ('state', '!=', 'posted'), ('is_move_sent', '=', False), ('move_type', 'not in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund'))]}"/>
|
||||
<!-- Register Payment (only invoices / receipts) -->
|
||||
<button name="action_register_payment" id="account_invoice_payment_btn"
|
||||
type="object" class="oe_highlight"
|
||||
@@ -997,7 +995,7 @@
|
||||
string="Cut-Off"
|
||||
aria-label="Change Period"
|
||||
class="float-right"
|
||||
attrs="{'invisible': [('account_internal_group', 'not in', ('income', 'expense'))], 'column_invisible': [('parent.move_type', '=', 'entry')]}"
|
||||
attrs="{'invisible': [('account_internal_group', 'not in', ('income', 'expense'))], 'column_invisible': ['|', ('parent.move_type', '=', 'entry'), ('parent.state', '!=', 'posted')]}"
|
||||
context="{'hide_automatic_options': 1, 'default_action': 'change_period'}"/>
|
||||
|
||||
<!-- Others fields -->
|
||||
|
||||
@@ -28,7 +28,9 @@
|
||||
<field name="repartition_type"/>
|
||||
<field name="account_id" attrs="{'invisible': [('repartition_type', '=', 'base')]}" options="{'no_create': True}"/>
|
||||
<field name="tag_ids" widget="many2many_tags" options="{'no_create': True}" domain="[('applicability', '=', 'taxes'), ('country_id', '=', tax_fiscal_country_id)]"/>
|
||||
<field name="use_in_tax_closing" optional="hidden" />
|
||||
<field name="use_in_tax_closing"
|
||||
optional="hidden"
|
||||
attrs="{'invisible': [('repartition_type', '=', 'base')]}"/>
|
||||
<field name="tax_fiscal_country_id" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
</tree>
|
||||
|
||||
@@ -47,10 +47,8 @@ class AutomaticEntryWizard(models.TransientModel):
|
||||
@api.constrains('percentage', 'action')
|
||||
def _constraint_percentage(self):
|
||||
for record in self:
|
||||
if not (0.0 < record.percentage <= 100.0):
|
||||
if not (0.0 < record.percentage <= 100.0) and record.action == 'change_period':
|
||||
raise UserError(_("Percentage must be between 0 and 100"))
|
||||
if record.percentage != 100 and record.action != 'change_period':
|
||||
raise UserError(_("Percentage can only be set for Change Period method"))
|
||||
|
||||
@api.depends('percentage', 'move_line_ids')
|
||||
def _compute_total_amount(self):
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<form>
|
||||
<field name="account_type" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="move_line_ids" invisible="1"/>
|
||||
<field name="display_currency_helper" invisible="1"/>
|
||||
<div attrs="{'invisible': [('display_currency_helper', '=', False)]}" class="alert alert-info text-center" role="status">
|
||||
The selected destination account is set to use a specific currency. Every entry transferred to it will be converted into this currency, causing
|
||||
|
||||
@@ -57,14 +57,16 @@ class AccountInvoiceSend(models.TransientModel):
|
||||
@api.onchange('is_email')
|
||||
def onchange_is_email(self):
|
||||
if self.is_email:
|
||||
res_ids = self._context.get('active_ids')
|
||||
if not self.composer_id:
|
||||
res_ids = self._context.get('active_ids')
|
||||
self.composer_id = self.env['mail.compose.message'].create({
|
||||
'composition_mode': 'comment' if len(res_ids) == 1 else 'mass_mail',
|
||||
'template_id': self.template_id.id
|
||||
})
|
||||
else:
|
||||
self.composer_id.composition_mode = 'comment' if len(res_ids) == 1 else 'mass_mail'
|
||||
self.composer_id.template_id = self.template_id.id
|
||||
self._compute_composition_mode()
|
||||
self.composer_id.onchange_template_id_wrapper()
|
||||
|
||||
@api.onchange('is_email')
|
||||
|
||||
@@ -115,7 +115,8 @@ class AccountPaymentRegister(models.TransientModel):
|
||||
:param batch_result: A batch returned by '_get_batches'.
|
||||
:return: A string representing a communication to be set on payment.
|
||||
'''
|
||||
return ' '.join(label for label in batch_result['lines'].mapped('name') if label)
|
||||
labels = set(line.name or line.move_id.ref or line.move_id.name for line in batch_result['lines'])
|
||||
return ' '.join(sorted(labels))
|
||||
|
||||
@api.model
|
||||
def _get_line_batch_key(self, line):
|
||||
@@ -195,11 +196,12 @@ class AccountPaymentRegister(models.TransientModel):
|
||||
''' Load initial values from the account.moves passed through the context. '''
|
||||
for wizard in self:
|
||||
batches = wizard._get_batches()
|
||||
batch_result = batches[0]
|
||||
wizard_values_from_batch = wizard._get_wizard_values_from_batch(batch_result)
|
||||
|
||||
if len(batches) == 1:
|
||||
# == Single batch to be mounted on the view ==
|
||||
batch_result = batches[0]
|
||||
wizard.update(wizard._get_wizard_values_from_batch(batch_result))
|
||||
wizard.update(wizard_values_from_batch)
|
||||
|
||||
wizard.can_edit_wizard = True
|
||||
wizard.can_group_payments = len(batch_result['lines']) != 1
|
||||
@@ -209,7 +211,7 @@ class AccountPaymentRegister(models.TransientModel):
|
||||
'company_id': batches[0]['lines'][0].company_id.id,
|
||||
'partner_id': False,
|
||||
'partner_type': False,
|
||||
'payment_type': False,
|
||||
'payment_type': wizard_values_from_batch['payment_type'],
|
||||
'source_currency_id': False,
|
||||
'source_amount': False,
|
||||
'source_amount_currency': False,
|
||||
@@ -261,7 +263,7 @@ class AccountPaymentRegister(models.TransientModel):
|
||||
def _compute_partner_bank_id(self):
|
||||
''' The default partner_bank_id will be the first available on the partner. '''
|
||||
for wizard in self:
|
||||
available_partner_bank_accounts = wizard.partner_id.bank_ids
|
||||
available_partner_bank_accounts = wizard.partner_id.bank_ids.filtered(lambda x: x.company_id in (False, wizard.company_id))
|
||||
if available_partner_bank_accounts:
|
||||
wizard.partner_bank_id = available_partner_bank_accounts[0]._origin
|
||||
else:
|
||||
|
||||
@@ -60,7 +60,7 @@ class ReSequenceWizard(models.TransientModel):
|
||||
or (self.sequence_number_reset == 'year' and line['server-date'][0:4] != previous_line['server-date'][0:4])\
|
||||
or (self.sequence_number_reset == 'month' and line['server-date'][0:7] != previous_line['server-date'][0:7]):
|
||||
if in_elipsis:
|
||||
changeLines.append({'current_name': '... (%s other)' % str(in_elipsis), 'new_by_name': '...', 'new_by_date': '...', 'date': '...'})
|
||||
changeLines.append({'current_name': _('... (%s other)', in_elipsis), 'new_by_name': '...', 'new_by_date': '...', 'date': '...'})
|
||||
in_elipsis = 0
|
||||
changeLines.append(line)
|
||||
else:
|
||||
|
||||
@@ -26,9 +26,9 @@ class AccountTourUploadBill(models.TransientModel):
|
||||
journal_alias = self.env['account.journal'] \
|
||||
.search([('type', '=', 'purchase'), ('company_id', '=', self.env.company.id)], limit=1)
|
||||
|
||||
return [('sample', 'Try a sample vendor bill'),
|
||||
('upload', 'Upload your own bill'),
|
||||
('email', 'Or send a bill to %s@%s' % (journal_alias.alias_name, journal_alias.alias_domain))]
|
||||
return [('sample', _('Try a sample vendor bill')),
|
||||
('upload', _('Upload your own bill')),
|
||||
('email', _('Or send a bill to %s@%s', journal_alias.alias_name, journal_alias.alias_domain))]
|
||||
|
||||
def _compute_sample_bill_image(self):
|
||||
""" Retrieve sample bill with facturx to speed up onboarding """
|
||||
@@ -63,7 +63,7 @@ class AccountTourUploadBill(models.TransientModel):
|
||||
'res_model': 'mail.compose.message',
|
||||
'datas': self.sample_bill_preview,
|
||||
})
|
||||
bill = purchase_journal.with_context(default_journal_id=purchase_journal.id, default_move_type='in_invoice')._create_invoice_from_single_attachment(attachment)
|
||||
bill = purchase_journal.with_context(default_journal_id=purchase_journal.id, default_move_type='in_invoice').create_invoice_from_attachment(attachment.ids)
|
||||
if self.selection == 'sample':
|
||||
bill.write({
|
||||
'partner_id': self.env.ref('base.main_partner').id,
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</h1>
|
||||
<group>
|
||||
<field name="report_id" invisible="1"/>
|
||||
<field name="tax_report_line_id" widget="selection" domain="[('tag_name', '!=', None), ('report_id', '=', report_id)]"/>
|
||||
<field name="tax_report_line_id" widget="selection" domain="[('tag_name', '!=', None)]"/>
|
||||
</group>
|
||||
<group>
|
||||
<group>
|
||||
|
||||
@@ -112,7 +112,8 @@ class AccountJournal(models.Model):
|
||||
domain_checks_to_print = [
|
||||
('journal_id', '=', self.id),
|
||||
('payment_method_id.code', '=', 'check_printing'),
|
||||
('state', '=', 'posted')
|
||||
('state', '=', 'posted'),
|
||||
('is_move_sent','=', False),
|
||||
]
|
||||
return dict(
|
||||
super(AccountJournal, self).get_journal_dashboard_datas(),
|
||||
|
||||
@@ -67,7 +67,7 @@ class AccountPayment(models.Model):
|
||||
@api.depends('payment_method_id', 'currency_id', 'amount')
|
||||
def _compute_check_amount_in_words(self):
|
||||
for pay in self:
|
||||
if pay.currency_id and pay.payment_method_id.code == 'check_printing':
|
||||
if pay.currency_id:
|
||||
pay.check_amount_in_words = pay.currency_id.amount_to_text(pay.amount)
|
||||
else:
|
||||
pay.check_amount_in_words = False
|
||||
@@ -203,26 +203,66 @@ class AccountPayment(models.Model):
|
||||
""" The stub is the summary of paid invoices. It may spill on several pages, in which case only the check on
|
||||
first page is valid. This function returns a list of stub lines per page.
|
||||
"""
|
||||
if len(self.move_id._get_reconciled_invoices()) == 0:
|
||||
return None
|
||||
self.ensure_one()
|
||||
|
||||
multi_stub = self.company_id.account_check_printing_multi_stub
|
||||
def prepare_vals(invoice, partials):
|
||||
number = ' - '.join([invoice.name, invoice.ref] if invoice.ref else [invoice.name])
|
||||
|
||||
invoices = self.move_id._get_reconciled_invoices().sorted(key=lambda r: r.invoice_date_due or fields.Date.context_today(self))
|
||||
debits = invoices.filtered(lambda r: r.move_type == 'in_invoice')
|
||||
credits = invoices.filtered(lambda r: r.move_type == 'in_refund')
|
||||
if invoice.is_outbound():
|
||||
invoice_sign = 1
|
||||
partial_field = 'debit_amount_currency'
|
||||
else:
|
||||
invoice_sign = -1
|
||||
partial_field = 'credit_amount_currency'
|
||||
|
||||
# Prepare the stub lines
|
||||
if not credits:
|
||||
stub_lines = [self._check_make_stub_line(inv) for inv in invoices]
|
||||
else:
|
||||
if invoice.currency_id.is_zero(invoice.amount_residual):
|
||||
amount_residual_str = '-'
|
||||
else:
|
||||
amount_residual_str = formatLang(self.env, invoice_sign * invoice.amount_residual, currency_obj=invoice.currency_id)
|
||||
|
||||
return {
|
||||
'due_date': format_date(self.env, invoice.invoice_date_due),
|
||||
'number': number,
|
||||
'amount_total': formatLang(self.env, invoice_sign * invoice.amount_total, currency_obj=invoice.currency_id),
|
||||
'amount_residual': amount_residual_str,
|
||||
'amount_paid': formatLang(self.env, invoice_sign * sum(partials.mapped(partial_field)), currency_obj=self.currency_id),
|
||||
'currency': invoice.currency_id,
|
||||
}
|
||||
|
||||
# Decode the reconciliation to keep only invoices.
|
||||
term_lines = self.line_ids.filtered(lambda line: line.account_id.internal_type in ('receivable', 'payable'))
|
||||
invoices = (term_lines.matched_debit_ids.debit_move_id.move_id + term_lines.matched_credit_ids.credit_move_id.move_id)\
|
||||
.filtered(lambda x: x.is_outbound())
|
||||
invoices = invoices.sorted(lambda x: x.invoice_date_due or x.date)
|
||||
|
||||
# Group partials by invoices.
|
||||
invoice_map = {invoice: self.env['account.partial.reconcile'] for invoice in invoices}
|
||||
for partial in term_lines.matched_debit_ids:
|
||||
invoice = partial.debit_move_id.move_id
|
||||
if invoice in invoice_map:
|
||||
invoice_map[invoice] |= partial
|
||||
for partial in term_lines.matched_credit_ids:
|
||||
invoice = partial.credit_move_id.move_id
|
||||
if invoice in invoice_map:
|
||||
invoice_map[invoice] |= partial
|
||||
|
||||
# Prepare stub_lines.
|
||||
if 'out_refund' in invoices.mapped('move_type'):
|
||||
stub_lines = [{'header': True, 'name': "Bills"}]
|
||||
stub_lines += [self._check_make_stub_line(inv) for inv in debits]
|
||||
stub_lines += [prepare_vals(invoice, partials)
|
||||
for invoice, partials in invoice_map.items()
|
||||
if invoice.move_type == 'in_invoice']
|
||||
stub_lines += [{'header': True, 'name': "Refunds"}]
|
||||
stub_lines += [self._check_make_stub_line(inv) for inv in credits]
|
||||
stub_lines += [prepare_vals(invoice, partials)
|
||||
for invoice, partials in invoice_map.items()
|
||||
if invoice.move_type == 'out_refund']
|
||||
else:
|
||||
stub_lines = [prepare_vals(invoice, partials)
|
||||
for invoice, partials in invoice_map.items()
|
||||
if invoice.move_type == 'in_invoice']
|
||||
|
||||
# Crop the stub lines or split them on multiple pages
|
||||
if not multi_stub:
|
||||
if not self.company_id.account_check_printing_multi_stub:
|
||||
# If we need to crop the stub, leave place for an ellipsis line
|
||||
num_stub_lines = len(stub_lines) > INV_LINES_PER_STUB and INV_LINES_PER_STUB - 1 or INV_LINES_PER_STUB
|
||||
stub_pages = [stub_lines[:num_stub_lines]]
|
||||
@@ -243,6 +283,7 @@ class AccountPayment(models.Model):
|
||||
def _check_make_stub_line(self, invoice):
|
||||
""" Return the dict used to display an invoice/refund in the stub
|
||||
"""
|
||||
# DEPRECATED: TO BE REMOVED IN MASTER
|
||||
# Find the account.partial.reconcile which are common to the invoice and the payment
|
||||
if invoice.move_type in ['in_invoice', 'out_refund']:
|
||||
invoice_sign = 1
|
||||
|
||||
@@ -22,8 +22,8 @@ class TestPrintCheck(AccountTestInvoicingCommon):
|
||||
))],
|
||||
})
|
||||
|
||||
def test_inbound_check_manual_sequencing(self):
|
||||
''' Test the check generation for customer invoices. '''
|
||||
def test_in_invoice_check_manual_sequencing(self):
|
||||
''' Test the check generation for vendor bills. '''
|
||||
nb_invoices_to_test = INV_LINES_PER_STUB + 1
|
||||
|
||||
self.company_data['default_journal_bank'].write({
|
||||
@@ -32,17 +32,17 @@ class TestPrintCheck(AccountTestInvoicingCommon):
|
||||
})
|
||||
|
||||
# Create 10 customer invoices.
|
||||
out_invoices = self.env['account.move'].create([{
|
||||
'move_type': 'out_invoice',
|
||||
in_invoices = self.env['account.move'].create([{
|
||||
'move_type': 'in_invoice',
|
||||
'partner_id': self.partner_a.id,
|
||||
'date': '2017-01-01',
|
||||
'invoice_date': '2017-01-01',
|
||||
'invoice_line_ids': [(0, 0, {'product_id': self.product_a.id, 'price_unit': 100.0})]
|
||||
} for i in range(nb_invoices_to_test)])
|
||||
out_invoices.action_post()
|
||||
in_invoices.action_post()
|
||||
|
||||
# Create a single payment.
|
||||
payment = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=out_invoices.ids).create({
|
||||
payment = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=in_invoices.ids).create({
|
||||
'group_payment': True,
|
||||
'payment_method_id': self.payment_method_check.id,
|
||||
})._create_payments()
|
||||
@@ -57,13 +57,13 @@ class TestPrintCheck(AccountTestInvoicingCommon):
|
||||
# Check pages.
|
||||
self.company_data['company'].account_check_printing_multi_stub = True
|
||||
report_pages = payment._check_get_pages()
|
||||
self.assertEqual(len(report_pages), int(math.ceil(len(out_invoices) / INV_LINES_PER_STUB)))
|
||||
self.assertEqual(len(report_pages), int(math.ceil(len(in_invoices) / INV_LINES_PER_STUB)))
|
||||
|
||||
self.company_data['company'].account_check_printing_multi_stub = False
|
||||
report_pages = payment._check_get_pages()
|
||||
self.assertEqual(len(report_pages), 1)
|
||||
|
||||
def test_outbound_check_manual_sequencing(self):
|
||||
def test_out_refund_check_manual_sequencing(self):
|
||||
''' Test the check generation for refunds. '''
|
||||
nb_invoices_to_test = INV_LINES_PER_STUB + 1
|
||||
|
||||
@@ -103,3 +103,33 @@ class TestPrintCheck(AccountTestInvoicingCommon):
|
||||
self.company_data['company'].account_check_printing_multi_stub = False
|
||||
report_pages = payment._check_get_pages()
|
||||
self.assertEqual(len(report_pages), 1)
|
||||
|
||||
def test_multi_currency_stub_lines(self):
|
||||
# Invoice in company's currency: 100$
|
||||
invoice = self.env['account.move'].create({
|
||||
'move_type': 'in_invoice',
|
||||
'partner_id': self.partner_a.id,
|
||||
'date': '2016-01-01',
|
||||
'invoice_date': '2016-01-01',
|
||||
'invoice_line_ids': [(0, 0, {'product_id': self.product_a.id, 'price_unit': 100.0})]
|
||||
})
|
||||
invoice.action_post()
|
||||
|
||||
# Partial payment in foreign currency: 100Gol = 33.33$.
|
||||
payment = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=invoice.ids).create({
|
||||
'payment_method_id': self.payment_method_check.id,
|
||||
'currency_id': self.currency_data['currency'].id,
|
||||
'amount': 100.0,
|
||||
'payment_date': '2017-01-01',
|
||||
})._create_payments()
|
||||
|
||||
stub_pages = payment._check_make_stub_pages()
|
||||
|
||||
self.assertEqual(stub_pages, [[{
|
||||
'due_date': '01/01/2016',
|
||||
'number': invoice.name,
|
||||
'amount_total': '$ 100.00',
|
||||
'amount_residual': '$ 50.00',
|
||||
'amount_paid': '150.000 ☺',
|
||||
'currency': invoice.currency_id,
|
||||
}]])
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//filter[@name='activities_overdue']" position="before">
|
||||
<separator/>
|
||||
<filter name="checks_to_send" string="Checks to Print" domain="[('payment_method_id.code', '=', 'check_printing'), ('state', '=', 'posted')]"/>
|
||||
<filter name="checks_to_send" string="Checks to Print" domain="[('payment_method_id.code', '=', 'check_printing'), ('state', '=', 'posted'), ('is_move_sent', '=', False)]"/>
|
||||
<separator/>
|
||||
</xpath>
|
||||
</field>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from flectra import models, fields, api, _
|
||||
from flectra import models, fields, api
|
||||
from flectra.addons.account_edi_extended.models.account_edi_document import DEFAULT_BLOCKING_LEVEL
|
||||
from psycopg2 import OperationalError
|
||||
import logging
|
||||
|
||||
@@ -17,7 +18,7 @@ class AccountEdiDocument(models.Model):
|
||||
edi_format_id = fields.Many2one('account.edi.format', required=True)
|
||||
attachment_id = fields.Many2one('ir.attachment', help='The file generated by edi_format_id when the invoice is posted (and this document is processed).')
|
||||
state = fields.Selection([('to_send', 'To Send'), ('sent', 'Sent'), ('to_cancel', 'To Cancel'), ('cancelled', 'Cancelled')])
|
||||
error = fields.Html()
|
||||
error = fields.Html(help='The text of the last error that happened during Electronic Invoice operation.')
|
||||
|
||||
# == Not stored fields ==
|
||||
name = fields.Char(related='attachment_id.name')
|
||||
@@ -31,24 +32,32 @@ class AccountEdiDocument(models.Model):
|
||||
),
|
||||
]
|
||||
|
||||
def write(self, vals):
|
||||
''' If account_edi_extended is not installed, a default behaviour is used instead.
|
||||
'''
|
||||
if 'blocking_level' in vals and 'blocking_level' not in self.env['account.edi.document']._fields:
|
||||
vals.pop('blocking_level')
|
||||
|
||||
return super().write(vals)
|
||||
|
||||
def _prepare_jobs(self):
|
||||
"""Creates a list of jobs to be performed by '_process_jobs' for the documents in self.
|
||||
"""Creates a list of jobs to be performed by '_process_job' for the documents in self.
|
||||
Each document represent a job, BUT if multiple documents have the same state, edi_format_id,
|
||||
doc_type (invoice or payment) and company_id AND the edi_format_id supports batching, they are grouped
|
||||
into a single job.
|
||||
|
||||
:returns: A list of tuples (key, documents)
|
||||
* key: A tuple (edi_format_id, state, doc_type, company_id)
|
||||
** edi_format_id: The format to perform the operation with
|
||||
** state: The state of the documents of this job
|
||||
** doc_type: Are the moves of this job invoice or payments ?
|
||||
** company_id: The company the moves belong to
|
||||
* documents: The documents related to this job. If edi_format_id does not support batch, length must be one
|
||||
:returns: A list of tuples (documents, doc_type)
|
||||
* documents: The documents related to this job. If edi_format_id does not support batch, length is one
|
||||
* doc_type: Are the moves of this job invoice or payments ?
|
||||
"""
|
||||
|
||||
to_process = []
|
||||
batches = {}
|
||||
for edi_doc in self.filtered(lambda d: d.state in ('to_send', 'to_cancel')):
|
||||
# Classify jobs by (edi_format, edi_doc.state, doc_type, move.company_id, custom_key)
|
||||
to_process = {}
|
||||
if 'blocking_level' in self.env['account.edi.document']._fields:
|
||||
documents = self.filtered(lambda d: d.state in ('to_send', 'to_cancel') and d.blocking_level != 'error')
|
||||
else:
|
||||
documents = self.filtered(lambda d: d.state in ('to_send', 'to_cancel'))
|
||||
for edi_doc in documents:
|
||||
move = edi_doc.move_id
|
||||
edi_format = edi_doc.edi_format_id
|
||||
if move.is_invoice(include_receipts=True):
|
||||
@@ -58,20 +67,64 @@ class AccountEdiDocument(models.Model):
|
||||
else:
|
||||
continue
|
||||
|
||||
key = (edi_format, edi_doc.state, doc_type, move.company_id)
|
||||
if edi_format._support_batching():
|
||||
if not batches.get(key, None):
|
||||
batches[key] = self.env['account.edi.document']
|
||||
batches[key] |= edi_doc
|
||||
else:
|
||||
to_process.append((key, edi_doc))
|
||||
to_process.extend(batches.items())
|
||||
return to_process
|
||||
custom_key = edi_format._get_batch_key(edi_doc.move_id, edi_doc.state)
|
||||
key = (edi_format, edi_doc.state, doc_type, move.company_id, custom_key)
|
||||
to_process.setdefault(key, self.env['account.edi.document'])
|
||||
to_process[key] |= edi_doc
|
||||
|
||||
# Order payments/invoice and create batches.
|
||||
invoices = []
|
||||
payments = []
|
||||
for key, documents in to_process.items():
|
||||
edi_format, state, doc_type, company_id, custom_key = key
|
||||
target = invoices if doc_type == 'invoice' else payments
|
||||
batch = self.env['account.edi.document']
|
||||
for doc in documents:
|
||||
if edi_format._support_batching(move=doc.move_id, state=state, company=company_id):
|
||||
batch |= doc
|
||||
else:
|
||||
target.append((doc, doc_type))
|
||||
if batch:
|
||||
target.append((batch, doc_type))
|
||||
return invoices + payments
|
||||
|
||||
@api.model
|
||||
def _convert_to_old_jobs_format(self, jobs):
|
||||
""" See '_prepare_jobs' :
|
||||
Old format : ((edi_format, state, doc_type, company_id), documents)
|
||||
Since edi_format, state and company_id can be deduced from documents, this is redundant and more prone to unexpected behaviours.
|
||||
New format : (doc_type, documents).
|
||||
|
||||
However, for backward compatibility of 'process_jobs', we need a way to convert back to the old format.
|
||||
"""
|
||||
return [(
|
||||
(documents.edi_format_id, documents[0].state, doc_type, documents.move_id.company_id),
|
||||
documents
|
||||
) for documents, doc_type in jobs]
|
||||
|
||||
@api.model
|
||||
def _process_jobs(self, to_process):
|
||||
""" Deprecated, use _process_job instead.
|
||||
|
||||
:param to_process: A list of tuples (key, documents)
|
||||
* key: A tuple (edi_format_id, state, doc_type, company_id)
|
||||
** edi_format_id: The format to perform the operation with
|
||||
** state: The state of the documents of this job
|
||||
** doc_type: Are the moves of this job invoice or payments ?
|
||||
** company_id: The company the moves belong to
|
||||
* documents: The documents related to this job. If edi_format_id does not support batch, length is one
|
||||
"""
|
||||
for key, documents in to_process:
|
||||
edi_format, state, doc_type, company_id = key
|
||||
self._process_job(documents, doc_type)
|
||||
|
||||
@api.model
|
||||
def _process_job(self, documents, doc_type):
|
||||
"""Post or cancel move_id (invoice or payment) by calling the related methods on edi_format_id.
|
||||
Invoices are processed before payments.
|
||||
|
||||
:param documents: The documents related to this job. If edi_format_id does not support batch, length is one
|
||||
:param doc_type: Are the moves of this job invoice or payments ?
|
||||
"""
|
||||
def _postprocess_post_edi_results(documents, edi_result):
|
||||
attachments_to_unlink = self.env['ir.attachment']
|
||||
@@ -83,6 +136,7 @@ class AccountEdiDocument(models.Model):
|
||||
values = {
|
||||
'attachment_id': move_result['attachment'].id,
|
||||
'error': move_result.get('error', False),
|
||||
'blocking_level': move_result.get('blocking_level', DEFAULT_BLOCKING_LEVEL) if 'error' in move_result else False,
|
||||
}
|
||||
if not values.get('error'):
|
||||
values.update({'state': 'sent'})
|
||||
@@ -90,7 +144,10 @@ class AccountEdiDocument(models.Model):
|
||||
if not old_attachment.res_model or not old_attachment.res_id:
|
||||
attachments_to_unlink |= old_attachment
|
||||
else:
|
||||
document.error = move_result.get('error', _("Error when processing the journal entry."))
|
||||
document.write({
|
||||
'error': move_result.get('error', False),
|
||||
'blocking_level': move_result.get('blocking_level', DEFAULT_BLOCKING_LEVEL) if 'error' in move_result else False,
|
||||
})
|
||||
|
||||
# Attachments that are not explicitly linked to a business model could be removed because they are not
|
||||
# supposed to have any traceability from the user.
|
||||
@@ -102,12 +159,13 @@ class AccountEdiDocument(models.Model):
|
||||
for document in documents:
|
||||
move = document.move_id
|
||||
move_result = edi_result.get(move, {})
|
||||
if move_result.get('success'):
|
||||
if move_result.get('success') is True:
|
||||
old_attachment = document.attachment_id
|
||||
document.write({
|
||||
'state': 'cancelled',
|
||||
'error': False,
|
||||
'attachment_id': False,
|
||||
'blocking_level': False,
|
||||
})
|
||||
|
||||
if move.is_invoice(include_receipts=True) and move.state == 'posted':
|
||||
@@ -118,8 +176,11 @@ class AccountEdiDocument(models.Model):
|
||||
if not old_attachment.res_model or not old_attachment.res_id:
|
||||
attachments_to_unlink |= old_attachment
|
||||
|
||||
else:
|
||||
document.error = move_result.get('error') or _("Error when cancelling the journal entry.")
|
||||
elif not move_result.get('success'):
|
||||
document.write({
|
||||
'error': move_result.get('error', False),
|
||||
'blocking_level': move_result.get('blocking_level', DEFAULT_BLOCKING_LEVEL) if move_result.get('error') else False,
|
||||
})
|
||||
|
||||
if invoice_ids_to_cancel:
|
||||
invoices = self.env['account.move'].browse(list(invoice_ids_to_cancel))
|
||||
@@ -132,65 +193,65 @@ class AccountEdiDocument(models.Model):
|
||||
|
||||
test_mode = self._context.get('edi_test_mode', False)
|
||||
|
||||
# ==== Process invoices ====
|
||||
payments = []
|
||||
for key, batches in to_process:
|
||||
edi_format, state, doc_type, company_id = key
|
||||
if doc_type == 'payment':
|
||||
payments.append((key, batches))
|
||||
continue # payments are processed after invoices
|
||||
documents.edi_format_id.ensure_one() # All account.edi.document of a job should have the same edi_format_id
|
||||
documents.move_id.company_id.ensure_one() # All account.edi.document of a job should be from the same company
|
||||
if len(set(doc.state for doc in documents)) != 1:
|
||||
raise ValueError('All account.edi.document of a job should have the same state')
|
||||
|
||||
for documents in batches:
|
||||
try:
|
||||
with self.env.cr.savepoint():
|
||||
# Locks the documents in DB. Avoid sending an invoice twice (the documents can be processed by the CRON but also manually).
|
||||
self._cr.execute('SELECT * FROM account_edi_document WHERE id IN %s FOR UPDATE NOWAIT', [tuple(self.ids)])
|
||||
edi_format = documents.edi_format_id
|
||||
state = documents[0].state
|
||||
if doc_type == 'invoice':
|
||||
if state == 'to_send':
|
||||
edi_result = edi_format._post_invoice_edi(documents.move_id, test_mode=test_mode)
|
||||
_postprocess_post_edi_results(documents, edi_result)
|
||||
elif state == 'to_cancel':
|
||||
edi_result = edi_format._cancel_invoice_edi(documents.move_id, test_mode=test_mode)
|
||||
_postprocess_cancel_edi_results(documents, edi_result)
|
||||
|
||||
if state == 'to_send':
|
||||
edi_result = edi_format._post_invoice_edi(documents.move_id, test_mode=test_mode)
|
||||
_postprocess_post_edi_results(documents, edi_result)
|
||||
elif state == 'to_cancel':
|
||||
edi_result = edi_format._cancel_invoice_edi(documents.move_id, test_mode=test_mode)
|
||||
_postprocess_cancel_edi_results(documents, edi_result)
|
||||
|
||||
except OperationalError as e:
|
||||
if e.pgcode == '55P03':
|
||||
_logger.debug('Another transaction already locked documents rows. Cannot process documents.')
|
||||
else:
|
||||
raise e
|
||||
|
||||
# ==== Process payments ====
|
||||
for key, batches in payments:
|
||||
edi_format, state, doc_type, company_id = key
|
||||
|
||||
for documents in batches:
|
||||
try:
|
||||
with self.env.cr.savepoint():
|
||||
self._cr.execute('SELECT * FROM account_edi_document WHERE id IN %s FOR UPDATE NOWAIT', [tuple(self.ids)])
|
||||
|
||||
if state == 'to_send':
|
||||
edi_result = edi_format._post_payment_edi(documents.move_id, test_mode=test_mode)
|
||||
_postprocess_post_edi_results(documents, edi_result)
|
||||
elif state == 'to_cancel':
|
||||
edi_result = edi_format._cancel_payment_edi(documents.move_id, test_mode=test_mode)
|
||||
_postprocess_cancel_edi_results(documents, edi_result)
|
||||
|
||||
except OperationalError as e:
|
||||
if e.pgcode == '55P03':
|
||||
_logger.debug('Another transaction already locked documents rows. Cannot process documents.')
|
||||
else:
|
||||
raise e
|
||||
elif doc_type == 'payment':
|
||||
if state == 'to_send':
|
||||
edi_result = edi_format._post_payment_edi(documents.move_id, test_mode=test_mode)
|
||||
_postprocess_post_edi_results(documents, edi_result)
|
||||
elif state == 'to_cancel':
|
||||
edi_result = edi_format._cancel_payment_edi(documents.move_id, test_mode=test_mode)
|
||||
_postprocess_cancel_edi_results(documents, edi_result)
|
||||
|
||||
def _process_documents_no_web_services(self):
|
||||
""" Post and cancel all the documents that don't need a web service.
|
||||
"""
|
||||
jobs = self.filtered(lambda d: not d.edi_format_id._needs_web_services())._prepare_jobs()
|
||||
self._process_jobs(jobs)
|
||||
self._process_jobs(self._convert_to_old_jobs_format(jobs))
|
||||
|
||||
def _process_documents_web_services(self, job_count=None):
|
||||
def _process_documents_web_services(self, job_count=None, with_commit=True):
|
||||
""" Post and cancel all the documents that need a web service. This is called by CRON.
|
||||
|
||||
:param job_count: Limit to the number of jobs to process among the ones that are available for treatment.
|
||||
"""
|
||||
jobs = self.filtered(lambda d: d.edi_format_id._needs_web_services())._prepare_jobs()
|
||||
self._process_jobs(jobs[0:job_count or len(jobs)])
|
||||
jobs = jobs[0:job_count or len(jobs)]
|
||||
for documents, doc_type in jobs:
|
||||
move_to_cancel = documents.filtered(lambda doc: doc.attachment_id \
|
||||
and doc.state == 'to_cancel' \
|
||||
and doc.move_id.is_invoice(include_receipts=True) \
|
||||
and doc.edi_format_id._is_required_for_invoice(doc.move_id)).move_id
|
||||
attachments_potential_unlink = documents.attachment_id.filtered(lambda a: not a.res_model and not a.res_id)
|
||||
try:
|
||||
with self.env.cr.savepoint():
|
||||
self._cr.execute('SELECT * FROM account_edi_document WHERE id IN %s FOR UPDATE NOWAIT', [tuple(documents.ids)])
|
||||
# Locks the move that will be cancelled.
|
||||
if move_to_cancel:
|
||||
self._cr.execute('SELECT * FROM account_move WHERE id IN %s FOR UPDATE NOWAIT', [tuple(move_to_cancel.ids)])
|
||||
|
||||
# Locks the attachments that might be unlinked
|
||||
if attachments_potential_unlink:
|
||||
self._cr.execute('SELECT * FROM ir_attachment WHERE id IN %s FOR UPDATE NOWAIT', [tuple(attachments_potential_unlink.ids)])
|
||||
|
||||
self._process_job(documents, doc_type)
|
||||
except OperationalError as e:
|
||||
if e.pgcode == '55P03':
|
||||
_logger.debug('Another transaction already locked documents rows. Cannot process documents.')
|
||||
else:
|
||||
raise e
|
||||
else:
|
||||
if with_commit and len(jobs) > 1:
|
||||
self.env.cr.commit()
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
from flectra import models, fields, api
|
||||
from flectra.exceptions import UserError
|
||||
from flectra.tools.pdf import FlectraPdfFileReader, FlectraPdfFileWriter
|
||||
from flectra.osv import expression
|
||||
|
||||
from lxml import etree
|
||||
import base64
|
||||
@@ -97,7 +98,7 @@ class AccountEdiFormat(models.Model):
|
||||
# TO OVERRIDE
|
||||
return False
|
||||
|
||||
def _support_batching(self):
|
||||
def _support_batching(self, move=None, state=None, company=None):
|
||||
""" Indicate if we can send multiple documents in the same time to the web services.
|
||||
If True, the _post_%s_edi methods will get multiple documents in the same time.
|
||||
Otherwise, these methods will be called with only one record at a time.
|
||||
@@ -107,6 +108,26 @@ class AccountEdiFormat(models.Model):
|
||||
# TO OVERRIDE
|
||||
return False
|
||||
|
||||
def _get_batch_key(self, move, state):
|
||||
""" Returns a tuple that will be used as key to partitionnate the invoices/payments when creating batches
|
||||
with multiple invoices/payments.
|
||||
The type of move (invoice or payment), its company_id, its edi state and the edi_format are used by default, if
|
||||
no further partition is needed for this format, this method should return ().
|
||||
|
||||
:returns: The key to be used when partitionning the batches.
|
||||
"""
|
||||
move.ensure_one()
|
||||
return ()
|
||||
|
||||
def _check_move_configuration(self, move):
|
||||
""" Checks the move and relevant records for potential error (missing data, etc).
|
||||
|
||||
:param invoice: The move to check.
|
||||
:returns: A list of error messages.
|
||||
"""
|
||||
# TO OVERRIDE
|
||||
return []
|
||||
|
||||
def _post_invoice_edi(self, invoices, test_mode=False):
|
||||
""" Create the file content representing the invoice (and calls web services if necessary).
|
||||
|
||||
@@ -115,6 +136,7 @@ class AccountEdiFormat(models.Model):
|
||||
:returns: A dictionary with the invoice as key and as value, another dictionary:
|
||||
* attachment: The attachment representing the invoice in this edi_format if the edi was successfully posted.
|
||||
* error: An error if the edi was not successfully posted.
|
||||
* blocking_level: (optional, requires account_edi_extended) How bad is the error (how should the edi flow be blocked ?)
|
||||
"""
|
||||
# TO OVERRIDE
|
||||
self.ensure_one()
|
||||
@@ -128,6 +150,7 @@ class AccountEdiFormat(models.Model):
|
||||
:returns: A dictionary with the invoice as key and as value, another dictionary:
|
||||
* success: True if the invoice was successfully cancelled.
|
||||
* error: An error if the edi was not successfully cancelled.
|
||||
* blocking_level: (optional, requires account_edi_extended) How bad is the error (how should the edi flow be blocked ?)
|
||||
"""
|
||||
# TO OVERRIDE
|
||||
self.ensure_one()
|
||||
@@ -141,6 +164,7 @@ class AccountEdiFormat(models.Model):
|
||||
:returns: A dictionary with the payment as key and as value, another dictionary:
|
||||
* attachment: The attachment representing the payment in this edi_format if the edi was successfully posted.
|
||||
* error: An error if the edi was not successfully posted.
|
||||
* blocking_level: (optional, requires account_edi_extended) How bad is the error (how should the edi flow be blocked ?)
|
||||
"""
|
||||
# TO OVERRIDE
|
||||
self.ensure_one()
|
||||
@@ -154,6 +178,7 @@ class AccountEdiFormat(models.Model):
|
||||
:returns: A dictionary with the payment as key and as value, another dictionary:
|
||||
* success: True if the payment was successfully cancelled.
|
||||
* error: An error if the edi was not successfully cancelled.
|
||||
* blocking_level: (optional, requires account_edi_extended) How bad is the error (how should the edi flow be blocked ?)
|
||||
"""
|
||||
# TO OVERRIDE
|
||||
self.ensure_one()
|
||||
@@ -223,7 +248,7 @@ class AccountEdiFormat(models.Model):
|
||||
"""
|
||||
attachments = []
|
||||
for edi_format in self:
|
||||
attachment = invoice.edi_document_ids.filtered(lambda d: d.edi_format_id == edi_format).attachment_id
|
||||
attachment = invoice._get_edi_attachment(edi_format)
|
||||
if attachment and edi_format._is_embedding_to_invoice_pdf_needed():
|
||||
datas = base64.b64decode(attachment.with_context(bin_size=False).datas)
|
||||
attachments.append({'name': attachment.name, 'datas': datas})
|
||||
@@ -296,8 +321,11 @@ class AccountEdiFormat(models.Model):
|
||||
return to_process
|
||||
|
||||
# Process embedded files.
|
||||
for xml_name, content in pdf_reader.getAttachments():
|
||||
to_process.extend(self._decode_xml(xml_name, content))
|
||||
try:
|
||||
for xml_name, content in pdf_reader.getAttachments():
|
||||
to_process.extend(self._decode_xml(xml_name, content))
|
||||
except NotImplementedError as e:
|
||||
_logger.warning("Unable to access the attachments of %s. Tried to decrypt it, but %s." % (filename, e))
|
||||
|
||||
# Process the pdf itself.
|
||||
to_process.append({
|
||||
@@ -379,3 +407,84 @@ class AccountEdiFormat(models.Model):
|
||||
res.write({'extract_state': 'done'})
|
||||
return res
|
||||
return self.env['account.move']
|
||||
|
||||
####################################################
|
||||
# Import helpers
|
||||
####################################################
|
||||
|
||||
def _find_value(self, xpath, xml_element, namespaces=None):
|
||||
element = xml_element.xpath(xpath, namespaces=namespaces)
|
||||
return element[0].text if element else None
|
||||
|
||||
def _retrieve_partner(self, name=None, phone=None, mail=None, vat=None):
|
||||
'''Search all partners and find one that matches one of the parameters.
|
||||
|
||||
:param name: The name of the partner.
|
||||
:param phone: The phone or mobile of the partner.
|
||||
:param mail: The mail of the partner.
|
||||
:param vat: The vat number of the partner.
|
||||
:returns: A partner or an empty recordset if not found.
|
||||
'''
|
||||
domains = []
|
||||
for value, domain in (
|
||||
(name, [('name', 'ilike', name)]),
|
||||
(phone, expression.OR([[('phone', '=', phone)], [('mobile', '=', phone)]])),
|
||||
(mail, [('email', '=', mail)]),
|
||||
(vat, [('vat', 'like', vat)]),
|
||||
):
|
||||
if value is not None:
|
||||
domains.append(domain)
|
||||
|
||||
domain = expression.OR(domains)
|
||||
return self.env['res.partner'].search(domain, limit=1)
|
||||
|
||||
def _retrieve_product(self, name=None, default_code=None, barcode=None):
|
||||
'''Search all products and find one that matches one of the parameters.
|
||||
|
||||
:param name: The name of the product.
|
||||
:param default_code: The default_code of the product.
|
||||
:param barcode: The barcode of the product.
|
||||
:returns: A product or an empty recordset if not found.
|
||||
'''
|
||||
domains = []
|
||||
for value, domain in (
|
||||
(name, ('name', 'ilike', name)),
|
||||
(default_code, ('default_code', '=', default_code)),
|
||||
(barcode, ('barcode', '=', barcode)),
|
||||
):
|
||||
if value is not None:
|
||||
domains.append([domain])
|
||||
|
||||
domain = expression.OR(domains)
|
||||
return self.env['product.product'].search(domain, limit=1)
|
||||
|
||||
def _retrieve_tax(self, amount, type_tax_use):
|
||||
'''Search all taxes and find one that matches all of the parameters.
|
||||
|
||||
:param amount: The amount of the tax.
|
||||
:param type_tax_use: The type of the tax.
|
||||
:returns: A tax or an empty recordset if not found.
|
||||
'''
|
||||
domains = [
|
||||
[('amount', '=', float(amount))],
|
||||
[('type_tax_use', '=', type_tax_use)]
|
||||
]
|
||||
|
||||
return self.env['account.tax'].search(expression.AND(domains), order='sequence ASC', limit=1)
|
||||
|
||||
def _retrieve_currency(self, code):
|
||||
'''Search all currencies and find one that matches the code.
|
||||
|
||||
:param code: The code of the currency.
|
||||
:returns: A currency or an empty recordset if not found.
|
||||
'''
|
||||
return self.env['res.currency'].search([('name', '=', code.upper())], limit=1)
|
||||
|
||||
####################################################
|
||||
# Other helpers
|
||||
####################################################
|
||||
|
||||
@api.model
|
||||
def _format_error_message(self, error_title, errors):
|
||||
bullet_list_msg = ''.join('<li>%s</li>' % msg for msg in errors)
|
||||
return '%s<ul>%s</ul>' % (error_title, bullet_list_msg)
|
||||
|
||||
@@ -51,12 +51,3 @@ class AccountJournal(models.Model):
|
||||
|
||||
for journal in self:
|
||||
journal.edi_format_ids += edi_formats.filtered(lambda e: e._is_compatible_with_journal(journal))
|
||||
|
||||
def _create_invoice_from_single_attachment(self, attachment):
|
||||
# OVERRIDE
|
||||
invoice = self.env['account.edi.format'].search([])._create_invoice_from_attachment(attachment)
|
||||
if invoice:
|
||||
# with_context: we don't want to import the attachment since the invoice was just created from it.
|
||||
invoice.with_context(no_new_invoice=True).message_post(attachment_ids=attachment.ids)
|
||||
return invoice
|
||||
return super(AccountJournal, self.with_context(no_new_invoice=True))._create_invoice_from_single_attachment(attachment)
|
||||
|
||||
@@ -29,7 +29,7 @@ class AccountMove(models.Model):
|
||||
@api.depends('edi_document_ids.state')
|
||||
def _compute_edi_state(self):
|
||||
for move in self:
|
||||
all_states = set(move.edi_document_ids.mapped('state'))
|
||||
all_states = set(move.edi_document_ids.filtered(lambda d: d.edi_format_id._needs_web_services()).mapped('state'))
|
||||
if all_states == {'sent'}:
|
||||
move.edi_state = 'sent'
|
||||
elif all_states == {'cancelled'}:
|
||||
@@ -111,6 +111,7 @@ class AccountMove(models.Model):
|
||||
existing_edi_document.write({
|
||||
'state': 'to_send',
|
||||
'error': False,
|
||||
'blocking_level': False,
|
||||
})
|
||||
else:
|
||||
edi_document_vals_list.append({
|
||||
@@ -122,6 +123,7 @@ class AccountMove(models.Model):
|
||||
existing_edi_document.write({
|
||||
'state': False,
|
||||
'error': False,
|
||||
'blocking_level': False,
|
||||
})
|
||||
|
||||
self.env['account.edi.document'].create(edi_document_vals_list)
|
||||
@@ -138,6 +140,10 @@ class AccountMove(models.Model):
|
||||
is_edi_needed = move.is_invoice(include_receipts=False) and edi_format._is_required_for_invoice(move)
|
||||
|
||||
if is_edi_needed:
|
||||
errors = edi_format._check_move_configuration(move)
|
||||
if errors:
|
||||
raise UserError(_("Invalid invoice configuration:\n\n%s") % '\n'.join(errors))
|
||||
|
||||
existing_edi_document = move.edi_document_ids.filtered(lambda x: x.edi_format_id == edi_format)
|
||||
if existing_edi_document:
|
||||
existing_edi_document.write({
|
||||
@@ -160,8 +166,8 @@ class AccountMove(models.Model):
|
||||
# Set the electronic document to be canceled and cancel immediately for synchronous formats.
|
||||
res = super().button_cancel()
|
||||
|
||||
self.edi_document_ids.filtered(lambda doc: doc.attachment_id).write({'state': 'to_cancel', 'error': False})
|
||||
self.edi_document_ids.filtered(lambda doc: not doc.attachment_id).write({'state': 'cancelled', 'error': False})
|
||||
self.edi_document_ids.filtered(lambda doc: doc.attachment_id).write({'state': 'to_cancel', 'error': False, 'blocking_level': False})
|
||||
self.edi_document_ids.filtered(lambda doc: not doc.attachment_id).write({'state': 'cancelled', 'error': False, 'blocking_level': False})
|
||||
self.edi_document_ids._process_documents_no_web_services()
|
||||
|
||||
return res
|
||||
@@ -177,7 +183,7 @@ class AccountMove(models.Model):
|
||||
|
||||
res = super().button_draft()
|
||||
|
||||
self.edi_document_ids.write({'state': False, 'error': False})
|
||||
self.edi_document_ids.write({'state': False, 'error': False, 'blocking_level': False})
|
||||
|
||||
return res
|
||||
|
||||
@@ -198,43 +204,28 @@ class AccountMove(models.Model):
|
||||
if is_move_marked:
|
||||
move.message_post(body=_("A cancellation of the EDI has been requested."))
|
||||
|
||||
to_cancel_documents.write({'state': 'to_cancel', 'error': False})
|
||||
to_cancel_documents.write({'state': 'to_cancel', 'error': False, 'blocking_level': False})
|
||||
|
||||
def _get_edi_document(self, edi_format):
|
||||
return self.edi_document_ids.filtered(lambda d: d.edi_format_id == edi_format)
|
||||
|
||||
def _get_edi_attachment(self, edi_format):
|
||||
return self._get_edi_document(edi_format).attachment_id
|
||||
|
||||
####################################################
|
||||
# Import Electronic Document
|
||||
####################################################
|
||||
|
||||
@api.returns('mail.message', lambda value: value.id)
|
||||
def message_post(self, **kwargs):
|
||||
def _get_create_invoice_from_attachment_decoders(self):
|
||||
# OVERRIDE
|
||||
# When posting a message, analyse the attachment to check if it is an EDI document and update the invoice
|
||||
# with the imported data.
|
||||
res = super().message_post(**kwargs)
|
||||
|
||||
if len(self) != 1 or self.env.context.get('no_new_invoice') or not self.is_invoice(include_receipts=True):
|
||||
return res
|
||||
|
||||
attachments = self.env['ir.attachment'].browse(kwargs.get('attachment_ids', []))
|
||||
flectrabot = self.env.ref('base.partner_root')
|
||||
if attachments and self.state != 'draft':
|
||||
self.message_post(body='The invoice is not a draft, it was not updated from the attachment.',
|
||||
message_type='comment',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
author_id=flectrabot.id)
|
||||
return res
|
||||
if attachments and self.line_ids:
|
||||
self.message_post(body='The invoice already contains lines, it was not updated from the attachment.',
|
||||
message_type='comment',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
author_id=flectrabot.id)
|
||||
return res
|
||||
|
||||
edi_formats = self.env['account.edi.format'].search([])
|
||||
for attachment in attachments:
|
||||
invoice = edi_formats._update_invoice_from_attachment(attachment, self)
|
||||
if invoice:
|
||||
break
|
||||
res = super()._get_create_invoice_from_attachment_decoders()
|
||||
res.append((10, self.env['account.edi.format'].search([])._create_invoice_from_attachment))
|
||||
return res
|
||||
|
||||
def _get_update_invoice_from_attachment_decoders(self, invoice):
|
||||
# OVERRIDE
|
||||
res = super()._get_update_invoice_from_attachment_decoders(invoice)
|
||||
res.append((10, self.env['account.edi.format'].search([])._update_invoice_from_attachment))
|
||||
return res
|
||||
|
||||
####################################################
|
||||
@@ -242,8 +233,10 @@ class AccountMove(models.Model):
|
||||
####################################################
|
||||
|
||||
def action_process_edi_web_services(self):
|
||||
self.edi_document_ids.filtered(lambda d: d.state in ('to_send', 'to_cancel'))._process_documents_web_services()
|
||||
|
||||
docs = self.edi_document_ids.filtered(lambda d: d.state in ('to_send', 'to_cancel'))
|
||||
if 'blocking_level' in self.env['account.edi.document']._fields:
|
||||
docs = docs.filtered(lambda d: d.blocking_level != 'error')
|
||||
docs._process_documents_web_services(with_commit=False)
|
||||
|
||||
class AccountMoveLine(models.Model):
|
||||
_inherit = 'account.move.line'
|
||||
|
||||
@@ -14,7 +14,7 @@ class MailTemplate(models.Model):
|
||||
res_ids = [res_ids]
|
||||
multi_mode = False
|
||||
|
||||
if self.model != 'account.move':
|
||||
if self.model not in ['account.move', 'account.payment']:
|
||||
return res
|
||||
|
||||
records = self.env[self.model].browse(res_ids)
|
||||
|
||||
@@ -32,7 +32,7 @@ class AccountEdiTestCommon(AccountTestInvoicingCommon):
|
||||
####################################################
|
||||
|
||||
def edi_cron(self):
|
||||
self.env['account.edi.document'].sudo().with_context(edi_test_mode=True).search([('state', 'in', ('to_send', 'to_cancel'))])._process_documents_web_services()
|
||||
self.env['account.edi.document'].sudo().with_context(edi_test_mode=True).search([('state', 'in', ('to_send', 'to_cancel'))])._process_documents_web_services(with_commit=False)
|
||||
|
||||
def _create_empty_vendor_bill(self):
|
||||
invoice = self.env['account.move'].create({
|
||||
@@ -67,12 +67,14 @@ class AccountEdiTestCommon(AccountTestInvoicingCommon):
|
||||
})
|
||||
|
||||
journal_id = self.company_data['default_journal_sale']
|
||||
journal_id.with_context(default_move_type='in_invoice')._create_invoice_from_single_attachment(attachment)
|
||||
journal_id.with_context(default_move_type='in_invoice').create_invoice_from_attachment(attachment.ids)
|
||||
|
||||
def assert_generated_file_equal(self, invoice, expected_values, applied_xpath=None):
|
||||
invoice.action_post()
|
||||
invoice.edi_document_ids._process_documents_web_services() # synchronous are called in post, but there's no CRON in tests for asynchronous
|
||||
attachment = invoice.edi_document_ids.filtered(lambda d: d.edi_format_id == self.edi_format).attachment_id
|
||||
invoice.edi_document_ids._process_documents_web_services(with_commit=False) # synchronous are called in post, but there's no CRON in tests for asynchronous
|
||||
attachment = invoice._get_edi_attachment(self.edi_format)
|
||||
if not attachment:
|
||||
raise ValueError('No attachment was generated after posting EDI')
|
||||
xml_content = base64.b64decode(attachment.with_context(bin_size=False).datas)
|
||||
current_etree = self.get_xml_tree_from_string(xml_content)
|
||||
expected_etree = self.get_xml_tree_from_string(expected_values)
|
||||
@@ -80,20 +82,34 @@ class AccountEdiTestCommon(AccountTestInvoicingCommon):
|
||||
expected_etree = self.with_applied_xpath(expected_etree, applied_xpath)
|
||||
self.assertXmlTreeEqual(current_etree, expected_etree)
|
||||
|
||||
def create_edi_document(self, edi_format, state, move=None, move_type=None):
|
||||
""" Creates a document based on an existing invoice or creates one, too.
|
||||
|
||||
:param edi_format: The edi_format of the document.
|
||||
:param state: The state of the document.
|
||||
:param move: The move of the document or None to create a new one.
|
||||
:param move_type: If move is None, the type of the invoice to create, defaults to 'out_invoice'.
|
||||
"""
|
||||
move = move or self.init_invoice(move_type or 'out_invoice', products=self.product_a)
|
||||
return self.env['account.edi.document'].create({
|
||||
'edi_format_id': edi_format.id,
|
||||
'move_id': move.id,
|
||||
'state': state
|
||||
})
|
||||
|
||||
def _process_documents_web_services(self, moves, formats_to_return=None):
|
||||
""" Generates and returns EDI files for the specified moves.
|
||||
formats_to_return is an optional parameter used to pass a set of codes from
|
||||
the formats we want to return the files for (in case we want to test specific formats).
|
||||
Other formats will still generate documents, they simply won't be returned.
|
||||
"""
|
||||
moves.edi_document_ids.with_context(edi_test_mode=True)._process_documents_web_services()
|
||||
moves.edi_document_ids.with_context(edi_test_mode=True)._process_documents_web_services(with_commit=False)
|
||||
|
||||
documents_to_return = moves.edi_document_ids
|
||||
if formats_to_return != None:
|
||||
documents_to_return = documents_to_return.filtered(lambda x: x.edi_format_id.code in formats_to_return)
|
||||
|
||||
attachments = documents_to_return.attachment_id
|
||||
|
||||
data_str_list = []
|
||||
for attachment in attachments.with_context(bin_size=False):
|
||||
data_str_list.append(base64.decodebytes(attachment.datas))
|
||||
|
||||
@@ -8,23 +8,16 @@ from unittest.mock import patch
|
||||
class TestAccountEdi(AccountEdiTestCommon):
|
||||
|
||||
def test_export_edi(self):
|
||||
invoice = self.init_invoice('out_invoice')
|
||||
invoice = self.init_invoice('out_invoice', products=self.product_a)
|
||||
self.assertEqual(len(invoice.edi_document_ids), 0)
|
||||
invoice.action_post()
|
||||
self.assertEqual(len(invoice.edi_document_ids), 1)
|
||||
|
||||
def test_prepare_jobs(self):
|
||||
def create_edi_document(edi_format, state, move=None, move_type=None):
|
||||
move = move or self.init_invoice(move_type or 'out_invoice')
|
||||
return self.env['account.edi.document'].create({
|
||||
'edi_format_id': edi_format.id,
|
||||
'move_id': move.id,
|
||||
'state': state
|
||||
})
|
||||
|
||||
edi_docs = self.env['account.edi.document']
|
||||
edi_docs |= create_edi_document(self.edi_format, 'to_send')
|
||||
edi_docs |= create_edi_document(self.edi_format, 'to_send')
|
||||
edi_docs |= self.create_edi_document(self.edi_format, 'to_send')
|
||||
edi_docs |= self.create_edi_document(self.edi_format, 'to_send')
|
||||
|
||||
to_process = edi_docs._prepare_jobs()
|
||||
self.assertEqual(len(to_process), 2)
|
||||
@@ -38,9 +31,19 @@ class TestAccountEdi(AccountEdiTestCommon):
|
||||
'code': 'test_batch_edi_2',
|
||||
})
|
||||
|
||||
edi_docs |= create_edi_document(other_edi, 'to_send')
|
||||
edi_docs |= create_edi_document(other_edi, 'to_send')
|
||||
edi_docs |= self.create_edi_document(other_edi, 'to_send')
|
||||
edi_docs |= self.create_edi_document(other_edi, 'to_send')
|
||||
|
||||
with patch('flectra.addons.account_edi.models.account_edi_format.AccountEdiFormat._support_batching', return_value=True):
|
||||
to_process = edi_docs._prepare_jobs()
|
||||
self.assertEqual(len(to_process), 2)
|
||||
|
||||
@patch('flectra.addons.account_edi.models.account_edi_format.AccountEdiFormat._post_invoice_edi')
|
||||
def test_error(self, patched):
|
||||
with patch('flectra.addons.account_edi.models.account_edi_format.AccountEdiFormat._needs_web_services',
|
||||
new=lambda edi_format: True):
|
||||
edi_docs = self.create_edi_document(self.edi_format, 'to_send')
|
||||
edi_docs.error = 'Test Error'
|
||||
|
||||
edi_docs.move_id.action_process_edi_web_services()
|
||||
patched.assert_called_once()
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</xpath>
|
||||
<xpath expr="//header" position="after">
|
||||
<div class="alert alert-info" role="alert" style="margin-bottom:0px;"
|
||||
attrs="{'invisible': [('edi_web_services_to_process', 'in', ['', False])]}">
|
||||
attrs="{'invisible': ['|', ('edi_web_services_to_process', 'in', ['', False]), ('state', '=', 'draft')]}">
|
||||
<div>The invoice will be sent asynchronously to :
|
||||
<field name="edi_web_services_to_process" class="oe_inline"/>
|
||||
</div>
|
||||
@@ -39,8 +39,7 @@
|
||||
</xpath>
|
||||
<xpath expr="//div[@name='journal_div']" position="after">
|
||||
<field name="edi_document_ids" invisible="1" />
|
||||
<field name="edi_state" invisible="1" />
|
||||
<field name="edi_state" attrs="{'invisible': ['|', ('edi_document_ids', '=', []), ('state', '=', 'draft')]}"/>
|
||||
<field name="edi_state" attrs="{'invisible': ['|', ('edi_state', '=', False), ('state', '=', 'draft')]}"/>
|
||||
</xpath>
|
||||
<xpath expr="//page[@id='other_tab']" position="after">
|
||||
<page id="edi_documents" string="EDI Documents" groups="base.group_no_one" attrs="{'invisible': [('edi_document_ids', '=', [])]}">
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='journal_id']" position="after">
|
||||
<field name="edi_document_ids" invisible="1" />
|
||||
<field name="edi_state" invisible="1" />
|
||||
<field name="edi_state" attrs="{'invisible': ['|', ('edi_document_ids', '=', []), ('state', '=', 'draft')]}"/>
|
||||
<field name="edi_state" attrs="{'invisible': ['|', ('edi_state', '=', False), ('state', '=', 'draft')]}"/>
|
||||
</xpath>
|
||||
<xpath expr="//group[@name='group3']" position="after">
|
||||
<group groups="base.group_no_one">
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
<flectra>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="account_invoice_send_inherit_account_wizard_form">
|
||||
<!-- Deprecated, the field is now invisible. This view will be removed in future versions. -->
|
||||
<field name="name">account.invoice.send.form.inherited.edi</field>
|
||||
<field name="model">account.invoice.send</field>
|
||||
<field name="inherit_id" ref="account.account_invoice_send_wizard_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='mail_form']" position='before'>
|
||||
<div name="edi_format_option" attrs="{'invisible':[('edi_format_ids', '=', [])]}">
|
||||
<div name="edi_format_option" invisible='1'>
|
||||
<group>
|
||||
<label for="edi_format_ids"/>
|
||||
<div class="oe_inline">
|
||||
|
||||
13
addons/account_edi_extended/__init__.py
Normal file
13
addons/account_edi_extended/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
from . import models
|
||||
|
||||
|
||||
def account_edi_block_level(cr, registery):
|
||||
''' The default value for blocking_level is 'error', but without this module,
|
||||
the behavior is the same as a blocking_level of 'warning' so we need to set
|
||||
all documents in error.
|
||||
'''
|
||||
from flectra import api, SUPERUSER_ID
|
||||
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
env['account.edi.document'].search([('error', '!=', False)]).write({'blocking_level': 'warning'})
|
||||
19
addons/account_edi_extended/__manifest__.py
Normal file
19
addons/account_edi_extended/__manifest__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name' : 'Additionnal features for account_edi',
|
||||
'description':"""
|
||||
This module add features to account_edi to support new Edi formats.
|
||||
""",
|
||||
'version' : '1.0',
|
||||
'category': 'Accounting/Accounting',
|
||||
'depends' : ['account_edi'],
|
||||
'data': [
|
||||
'views/account_edi_document_views.xml',
|
||||
'views/account_move_views.xml',
|
||||
'views/account_payment_views.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'auto_install': False,
|
||||
'post_init_hook': 'account_edi_block_level',
|
||||
}
|
||||
3
addons/account_edi_extended/models/__init__.py
Normal file
3
addons/account_edi_extended/models/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from . import account_edi_document
|
||||
from . import account_move
|
||||
from . import account_payment
|
||||
18
addons/account_edi_extended/models/account_edi_document.py
Normal file
18
addons/account_edi_extended/models/account_edi_document.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
from flectra import models, fields, _
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
DEFAULT_BLOCKING_LEVEL = 'warning' # Keep previous behavior. TODO : when account_edi_extended is merged with account_edi, should be 'error' (document will not be processed again until forced retry or reset to draft)
|
||||
|
||||
|
||||
class AccountEdiDocument(models.Model):
|
||||
_inherit = 'account.edi.document'
|
||||
|
||||
blocking_level = fields.Selection(selection=[('info', 'Info'), ('warning', 'Warning'), ('error', 'Error')],
|
||||
help="Blocks the document current operation depending on the error severity :\n"
|
||||
" * Info: the document is not blocked and everything is working as it should.\n"
|
||||
" * Warning : there is an error that doesn't prevent the current Electronic Invoicing operation to succeed.\n"
|
||||
" * Error : there is an error that blocks the current Electronic Invoicing operation.")
|
||||
|
||||
81
addons/account_edi_extended/models/account_move.py
Normal file
81
addons/account_edi_extended/models/account_move.py
Normal file
@@ -0,0 +1,81 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from flectra import api, models, fields, _
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = 'account.move'
|
||||
|
||||
edi_show_abandon_cancel_button = fields.Boolean(
|
||||
compute='_compute_edi_show_abandon_cancel_button')
|
||||
edi_error_message = fields.Html(compute='_compute_edi_error_message')
|
||||
edi_blocking_level = fields.Selection(selection=[('info', 'Info'), ('warning', 'Warning'), ('error', 'Error')], compute='_compute_edi_error_message')
|
||||
|
||||
@api.depends(
|
||||
'edi_document_ids',
|
||||
'edi_document_ids.state',
|
||||
'edi_document_ids.blocking_level',
|
||||
'edi_document_ids.edi_format_id',
|
||||
'edi_document_ids.edi_format_id.name')
|
||||
def _compute_edi_web_services_to_process(self):
|
||||
# OVERRIDE to take blocking_level into account
|
||||
for move in self:
|
||||
to_process = move.edi_document_ids.filtered(lambda d: d.state in ['to_send', 'to_cancel'] and d.blocking_level != 'error')
|
||||
format_web_services = to_process.edi_format_id.filtered(lambda f: f._needs_web_services())
|
||||
move.edi_web_services_to_process = ', '.join(f.name for f in format_web_services)
|
||||
|
||||
@api.depends(
|
||||
'state',
|
||||
'edi_document_ids.state',
|
||||
'edi_document_ids.attachment_id')
|
||||
def _compute_edi_show_abandon_cancel_button(self):
|
||||
for move in self:
|
||||
move.edi_show_abandon_cancel_button = any(doc.edi_format_id._needs_web_services()
|
||||
and doc.state == 'to_cancel'
|
||||
and move.is_invoice(include_receipts=True)
|
||||
and doc.edi_format_id._is_required_for_invoice(move)
|
||||
for doc in move.edi_document_ids)
|
||||
|
||||
@api.depends('edi_error_count', 'edi_document_ids.error', 'edi_document_ids.blocking_level')
|
||||
def _compute_edi_error_message(self):
|
||||
for move in self:
|
||||
if move.edi_error_count == 0:
|
||||
move.edi_error_message = None
|
||||
move.edi_blocking_level = None
|
||||
elif move.edi_error_count == 1:
|
||||
error_doc = move.edi_document_ids.filtered(lambda d: d.error)
|
||||
move.edi_error_message = error_doc.error
|
||||
move.edi_blocking_level = error_doc.blocking_level
|
||||
else:
|
||||
error_levels = set([doc.blocking_level for doc in move.edi_document_ids])
|
||||
if 'error' in error_levels:
|
||||
move.edi_error_message = str(move.edi_error_count) + _(" Electronic invoicing error(s)")
|
||||
move.edi_blocking_level = 'error'
|
||||
elif 'warning' in error_levels:
|
||||
move.edi_error_message = str(move.edi_error_count) + _(" Electronic invoicing warning(s)")
|
||||
move.edi_blocking_level = 'warning'
|
||||
else:
|
||||
move.edi_error_message = str(move.edi_error_count) + _(" Electronic invoicing info(s)")
|
||||
move.edi_blocking_level = 'info'
|
||||
|
||||
def action_retry_edi_documents_error(self):
|
||||
self.edi_document_ids.write({'error': False, 'blocking_level': False})
|
||||
self.action_process_edi_web_services()
|
||||
|
||||
def button_abandon_cancel_posted_posted_moves(self):
|
||||
'''Cancel the request for cancellation of the EDI.
|
||||
'''
|
||||
documents = self.env['account.edi.document']
|
||||
for move in self:
|
||||
is_move_marked = False
|
||||
for doc in move.edi_document_ids:
|
||||
if doc.state == 'to_cancel' \
|
||||
and move.is_invoice(include_receipts=True) \
|
||||
and doc.edi_format_id._is_required_for_invoice(move):
|
||||
documents |= doc
|
||||
is_move_marked = True
|
||||
if is_move_marked:
|
||||
move.message_post(body=_("A request for cancellation of the EDI has been called off."))
|
||||
|
||||
documents.write({'state': 'sent'})
|
||||
12
addons/account_edi_extended/models/account_payment.py
Normal file
12
addons/account_edi_extended/models/account_payment.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from flectra import models
|
||||
|
||||
|
||||
class AccountPayment(models.Model):
|
||||
_inherit = 'account.payment'
|
||||
|
||||
def action_retry_edi_documents_error(self):
|
||||
self.ensure_one()
|
||||
return self.move_id.action_retry_edi_documents_error()
|
||||
5
addons/account_edi_extended/tests/__init__.py
Normal file
5
addons/account_edi_extended/tests/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import common
|
||||
from . import test_edi
|
||||
114
addons/account_edi_extended/tests/common.py
Normal file
114
addons/account_edi_extended/tests/common.py
Normal file
@@ -0,0 +1,114 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from flectra.addons.account_edi.tests.common import AccountEdiTestCommon
|
||||
from contextlib import contextmanager
|
||||
from unittest.mock import patch
|
||||
import base64
|
||||
|
||||
# TODO for the test to work, we need a chart template (COA) but we don't have any and don't want to add dependency (create empty coa ?)
|
||||
|
||||
|
||||
def _generate_mocked_needs_web_services(needs_web_services):
|
||||
return lambda edi_format: needs_web_services
|
||||
|
||||
|
||||
def _generate_mocked_support_batching(support_batching):
|
||||
return lambda edi_format, move, state, company: support_batching
|
||||
|
||||
|
||||
def _mocked_get_batch_key(edi_format, move, state):
|
||||
return ()
|
||||
|
||||
|
||||
def _mocked_check_move_configuration_success(edi_format, move):
|
||||
return []
|
||||
|
||||
|
||||
def _mocked_check_move_configuration_fail(edi_format, move):
|
||||
return ['Fake error (mocked)']
|
||||
|
||||
|
||||
def _mocked_post(edi_format, invoices, test_mode):
|
||||
res = {}
|
||||
for invoice in invoices:
|
||||
attachment = edi_format.env['ir.attachment'].create({
|
||||
'name': 'mock_simple.xml',
|
||||
'datas': base64.encodebytes(b"<?xml version='1.0' encoding='UTF-8'?><Invoice/>"),
|
||||
'mimetype': 'application/xml'
|
||||
})
|
||||
res[invoice] = {'attachment': attachment}
|
||||
return res
|
||||
|
||||
|
||||
def _mocked_post_two_steps(edi_format, invoices, test_mode):
|
||||
# For this test, we use the field ref to know if the first step is already done or not.
|
||||
# Typically, a technical field for the reference of the upload to the web-service will
|
||||
# be saved on the invoice.
|
||||
invoices_no_ref = invoices.filtered(lambda i: not i.ref)
|
||||
if len(invoices_no_ref) == len(invoices): # first step
|
||||
invoices_no_ref.ref = 'test_ref'
|
||||
return {invoice: {} for invoice in invoices}
|
||||
elif len(invoices_no_ref) == 0: # second step
|
||||
res = {}
|
||||
for invoice in invoices:
|
||||
attachment = edi_format.env['ir.attachment'].create({
|
||||
'name': 'mock_simple.xml',
|
||||
'datas': base64.encodebytes(b"<?xml version='1.0' encoding='UTF-8'?><Invoice/>"),
|
||||
'mimetype': 'application/xml'
|
||||
})
|
||||
res[invoice] = {'attachment': attachment}
|
||||
return res
|
||||
else:
|
||||
raise ValueError('wrong use of "_mocked_post_two_steps"')
|
||||
|
||||
|
||||
def _mocked_cancel_success(edi_format, invoices, test_mode):
|
||||
return {invoice: {'success': True} for invoice in invoices}
|
||||
|
||||
|
||||
def _mocked_cancel_failed(edi_format, invoices, test_mode):
|
||||
return {invoice: {'error': 'Faked error (mocked)'} for invoice in invoices}
|
||||
|
||||
|
||||
class AccountEdiExtendedTestCommon(AccountEdiTestCommon):
|
||||
|
||||
@contextmanager
|
||||
def mock_edi(self,
|
||||
_is_required_for_invoice_method=lambda edi_format, invoice: True,
|
||||
_is_required_for_payment_method=lambda edi_format, invoice: True,
|
||||
_support_batching_method=_generate_mocked_support_batching(False),
|
||||
_get_batch_key_method=_mocked_get_batch_key,
|
||||
_needs_web_services_method=_generate_mocked_needs_web_services(False),
|
||||
_check_move_configuration_method=_mocked_check_move_configuration_success,
|
||||
_post_invoice_edi_method=_mocked_post,
|
||||
_cancel_invoice_edi_method=_mocked_cancel_success,
|
||||
_post_payment_edi_method=_mocked_post,
|
||||
_cancel_payment_edi_method=_mocked_cancel_success,
|
||||
):
|
||||
|
||||
try:
|
||||
with patch('flectra.addons.account_edi.models.account_edi_format.AccountEdiFormat._is_required_for_invoice',
|
||||
new=_is_required_for_invoice_method), \
|
||||
patch('flectra.addons.account_edi.models.account_edi_format.AccountEdiFormat._is_required_for_payment',
|
||||
new=_is_required_for_payment_method), \
|
||||
patch('flectra.addons.account_edi.models.account_edi_format.AccountEdiFormat._needs_web_services',
|
||||
new=_needs_web_services_method), \
|
||||
patch('flectra.addons.account_edi.models.account_edi_format.AccountEdiFormat._support_batching',
|
||||
new=_support_batching_method), \
|
||||
patch('flectra.addons.account_edi.models.account_edi_format.AccountEdiFormat._get_batch_key',
|
||||
new=_get_batch_key_method), \
|
||||
patch('flectra.addons.account_edi.models.account_edi_format.AccountEdiFormat._check_move_configuration',
|
||||
new=_check_move_configuration_method), \
|
||||
patch('flectra.addons.account_edi.models.account_edi_format.AccountEdiFormat._post_invoice_edi',
|
||||
new=_post_invoice_edi_method), \
|
||||
patch('flectra.addons.account_edi.models.account_edi_format.AccountEdiFormat._cancel_invoice_edi',
|
||||
new=_cancel_invoice_edi_method), \
|
||||
patch('flectra.addons.account_edi.models.account_edi_format.AccountEdiFormat._post_payment_edi',
|
||||
new=_post_payment_edi_method), \
|
||||
patch('flectra.addons.account_edi.models.account_edi_format.AccountEdiFormat._cancel_payment_edi',
|
||||
new=_cancel_payment_edi_method):
|
||||
|
||||
yield
|
||||
finally:
|
||||
pass
|
||||
157
addons/account_edi_extended/tests/test_edi.py
Normal file
157
addons/account_edi_extended/tests/test_edi.py
Normal file
@@ -0,0 +1,157 @@
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from flectra.addons.account_edi_extended.tests.common import AccountEdiExtendedTestCommon, _mocked_post, _mocked_post_two_steps, _generate_mocked_needs_web_services, _mocked_cancel_failed, _generate_mocked_support_batching
|
||||
|
||||
|
||||
class TestAccountEdi(AccountEdiExtendedTestCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None, edi_format_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref, edi_format_ref=edi_format_ref)
|
||||
|
||||
cls.invoice = cls.init_invoice('out_invoice', products=cls.product_a + cls.product_b)
|
||||
|
||||
def test_edi_flow(self):
|
||||
with self.mock_edi():
|
||||
doc = self.invoice._get_edi_document(self.edi_format)
|
||||
self.assertFalse(doc)
|
||||
self.invoice.action_post()
|
||||
doc = self.invoice._get_edi_document(self.edi_format)
|
||||
self.assertEqual(len(doc), 1)
|
||||
self.assertEqual(doc.state, 'sent')
|
||||
self.invoice.button_draft()
|
||||
self.invoice.button_cancel()
|
||||
self.assertEqual(doc.state, 'cancelled')
|
||||
|
||||
def test_edi_flow_two_steps(self):
|
||||
with self.mock_edi(_post_invoice_edi_method=_mocked_post_two_steps,
|
||||
_needs_web_services_method=_generate_mocked_needs_web_services(True)):
|
||||
doc = self.invoice._get_edi_document(self.edi_format)
|
||||
self.assertFalse(doc)
|
||||
self.invoice.action_post()
|
||||
doc = self.invoice._get_edi_document(self.edi_format)
|
||||
self.assertEqual(len(doc), 1)
|
||||
self.assertEqual(doc.state, 'to_send')
|
||||
doc._process_documents_web_services(with_commit=False)
|
||||
self.assertEqual(doc.state, 'to_send')
|
||||
doc._process_documents_web_services(with_commit=False)
|
||||
self.assertEqual(doc.state, 'sent')
|
||||
|
||||
def test_edi_flow_request_cancel_success(self):
|
||||
with self.mock_edi(_needs_web_services_method=_generate_mocked_needs_web_services(True)):
|
||||
self.assertEqual(self.invoice.state, 'draft')
|
||||
self.invoice.action_post()
|
||||
doc = self.invoice._get_edi_document(self.edi_format)
|
||||
self.assertEqual(doc.state, 'to_send')
|
||||
self.assertEqual(self.invoice.state, 'posted')
|
||||
doc._process_documents_web_services(with_commit=False)
|
||||
self.assertEqual(doc.state, 'sent')
|
||||
self.assertEqual(self.invoice.state, 'posted')
|
||||
self.invoice.button_cancel_posted_moves()
|
||||
self.assertEqual(doc.state, 'to_cancel')
|
||||
self.assertEqual(self.invoice.state, 'posted')
|
||||
doc._process_documents_web_services()
|
||||
self.assertEqual(doc.state, 'cancelled')
|
||||
self.assertEqual(self.invoice.state, 'cancel')
|
||||
|
||||
def test_edi_flow_request_cancel_failed(self):
|
||||
with self.mock_edi(_needs_web_services_method=_generate_mocked_needs_web_services(True),
|
||||
_cancel_invoice_edi_method=_mocked_cancel_failed):
|
||||
self.assertEqual(self.invoice.state, 'draft')
|
||||
self.invoice.action_post()
|
||||
doc = self.invoice._get_edi_document(self.edi_format)
|
||||
self.assertEqual(doc.state, 'to_send')
|
||||
self.assertEqual(self.invoice.state, 'posted')
|
||||
doc._process_documents_web_services(with_commit=False)
|
||||
self.assertEqual(doc.state, 'sent')
|
||||
self.assertEqual(self.invoice.state, 'posted')
|
||||
self.invoice.button_cancel_posted_moves()
|
||||
self.assertEqual(doc.state, 'to_cancel')
|
||||
self.assertEqual(self.invoice.state, 'posted')
|
||||
# Call off edi Cancellation
|
||||
self.invoice.button_abandon_cancel_posted_posted_moves()
|
||||
self.assertEqual(doc.state, 'sent')
|
||||
self.assertFalse(doc.error)
|
||||
|
||||
# Failed cancel
|
||||
self.invoice.button_cancel_posted_moves()
|
||||
self.assertEqual(doc.state, 'to_cancel')
|
||||
self.assertEqual(self.invoice.state, 'posted')
|
||||
doc._process_documents_web_services()
|
||||
self.assertEqual(doc.state, 'to_cancel')
|
||||
self.assertEqual(self.invoice.state, 'posted')
|
||||
|
||||
# Call off edi Cancellation
|
||||
self.invoice.button_abandon_cancel_posted_posted_moves()
|
||||
self.assertEqual(doc.state, 'sent')
|
||||
self.assertIsNotNone(doc.error)
|
||||
|
||||
def test_edi_flow_two_step_cancel_with_call_off_request(self):
|
||||
def _mock_cancel(edi_format, invoices, test_mode):
|
||||
invoices_no_ref = invoices.filtered(lambda i: not i.ref)
|
||||
if len(invoices_no_ref) == len(invoices): # first step
|
||||
invoices_no_ref.ref = 'test_ref_cancel'
|
||||
return {invoice: {} for invoice in invoices}
|
||||
elif len(invoices_no_ref) == 0: # second step
|
||||
for invoice in invoices:
|
||||
invoice.ref = None
|
||||
return {invoice: {'success': True} for invoice in invoices}
|
||||
else:
|
||||
raise ValueError('wrong use of "_mocked_post_two_steps"')
|
||||
|
||||
def _is_needed_for_invoice(edi_format, invoice):
|
||||
return not bool(invoice.ref)
|
||||
|
||||
with self.mock_edi(_needs_web_services_method=_generate_mocked_needs_web_services(True),
|
||||
_is_required_for_invoice_method=_is_needed_for_invoice,
|
||||
_cancel_invoice_edi_method=_mock_cancel):
|
||||
self.invoice.action_post()
|
||||
doc = self.invoice._get_edi_document(self.edi_format)
|
||||
doc._process_documents_web_services(with_commit=False)
|
||||
self.assertEqual(doc.state, 'sent')
|
||||
|
||||
# Request Cancellation
|
||||
self.invoice.button_cancel_posted_moves()
|
||||
doc._process_documents_web_services(with_commit=False) # first step of cancel
|
||||
self.assertEqual(doc.state, 'to_cancel')
|
||||
|
||||
# Call off edi Cancellation
|
||||
self.invoice.button_abandon_cancel_posted_posted_moves()
|
||||
self.assertEqual(doc.state, 'to_cancel')
|
||||
|
||||
# If we cannot call off edi cancellation, only solution is to post again
|
||||
doc._process_documents_web_services(with_commit=False) # second step of cancel
|
||||
self.assertEqual(doc.state, 'cancelled')
|
||||
self.invoice.action_post()
|
||||
doc._process_documents_web_services(with_commit=False)
|
||||
self.assertEqual(doc.state, 'sent')
|
||||
|
||||
def test_batches(self):
|
||||
def _get_batch_key_method(edi_format, move, state):
|
||||
return (move.ref)
|
||||
|
||||
with self.mock_edi(_get_batch_key_method=_get_batch_key_method,
|
||||
_support_batching_method=_generate_mocked_support_batching(True)):
|
||||
edi_docs = self.env['account.edi.document']
|
||||
doc1 = self.create_edi_document(self.edi_format, 'to_send')
|
||||
edi_docs |= doc1
|
||||
doc2 = self.create_edi_document(self.edi_format, 'to_send')
|
||||
edi_docs |= doc2
|
||||
doc3 = self.create_edi_document(self.edi_format, 'to_send')
|
||||
edi_docs |= doc3
|
||||
|
||||
to_process = edi_docs._prepare_jobs()
|
||||
self.assertEqual(len(to_process), 1)
|
||||
|
||||
doc1.move_id.ref = 'batch1'
|
||||
doc2.move_id.ref = 'batch2'
|
||||
doc3.move_id.ref = 'batch3'
|
||||
|
||||
to_process = edi_docs._prepare_jobs()
|
||||
self.assertEqual(len(to_process), 3)
|
||||
|
||||
doc2.move_id.ref = 'batch1'
|
||||
to_process = edi_docs._prepare_jobs()
|
||||
self.assertEqual(len(to_process), 2)
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<flectra>
|
||||
<data>
|
||||
<record id="view_tree_account_edi_document_inherit" model="ir.ui.view">
|
||||
<field name="name">Account.edi.document.tree.inherit</field>
|
||||
<field name="model">account.edi.document</field>
|
||||
<field name="inherit_id" ref="account_edi.view_tree_account_edi_document"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//tree" position="inside">
|
||||
<field name="blocking_level" invisible="1" />
|
||||
</xpath>
|
||||
<xpath expr="//tree" position="attributes">
|
||||
<attribute name="decoration-info">blocking_level == 'info'</attribute>
|
||||
<attribute name="decoration-warning">blocking_level == 'warning'</attribute>
|
||||
<attribute name="decoration-danger">blocking_level == 'error'</attribute>
|
||||
</xpath>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</flectra>
|
||||
52
addons/account_edi_extended/views/account_move_views.xml
Normal file
52
addons/account_edi_extended/views/account_move_views.xml
Normal file
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<flectra>
|
||||
<data>
|
||||
|
||||
<record id="view_move_form_inherit" model="ir.ui.view">
|
||||
<field name="name">account.move.form.inherit</field>
|
||||
<field name="model">account.move</field>
|
||||
<field name="inherit_id" ref="account_edi.view_move_form_inherit" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[@name='%(account_edi.action_open_edi_documents)d']" position="after">
|
||||
<button name="action_retry_edi_documents_error" type="object" class="oe_link oe_inline" string="Retry" />
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='button_cancel_posted_moves']" position="after">
|
||||
<field name="edi_show_abandon_cancel_button" invisible="1"/>
|
||||
<button name="button_abandon_cancel_posted_posted_moves"
|
||||
string="Call off EDI Cancellation"
|
||||
type="object"
|
||||
groups="account.group_account_invoice"
|
||||
attrs="{'invisible' : [('edi_show_abandon_cancel_button', '=', False)]}"/>
|
||||
</xpath>
|
||||
<!-- Nasty xpath to replace the error count warning banner. In master, it will be merged. -->
|
||||
<xpath expr="//div[hasclass('alert-warning')]" position="replace">
|
||||
<field name="edi_blocking_level" invisible="1" />
|
||||
<field name="edi_error_count" invisible="1" />
|
||||
<div class="alert alert-danger" role="alert" style="margin-bottom:0px;"
|
||||
attrs="{'invisible': ['|', ('edi_error_count', '=', 0), ('edi_blocking_level', '!=', 'error')]}">
|
||||
<div class="o_row">
|
||||
<field name="edi_error_message" />
|
||||
<button name="%(account_edi.action_open_edi_documents)d" string="⇒ See errors" type="action" class="oe_link" attrs="{'invisible': [('edi_error_count', '=', 1)]}" />
|
||||
<button name="action_retry_edi_documents_error" type="object" class="oe_link oe_inline" string="Retry" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-warning" role="alert" style="margin-bottom:0px;"
|
||||
attrs="{'invisible': ['|', ('edi_error_count', '=', 0), ('edi_blocking_level', '!=', 'warning')]}">
|
||||
<div class="o_row">
|
||||
<field name="edi_error_message" />
|
||||
<button name="%(account_edi.action_open_edi_documents)d" string="⇒ See errors" type="action" class="oe_link" attrs="{'invisible': [('edi_error_count', '=', 1)]}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-info" role="alert" style="margin-bottom:0px;"
|
||||
attrs="{'invisible': ['|', ('edi_error_count', '=', 0), ('edi_blocking_level', '!=', 'info')]}">
|
||||
<div class="o_row">
|
||||
<field name="edi_error_message" />
|
||||
<button name="%(account_edi.action_open_edi_documents)d" string="⇒ See errors" type="action" class="oe_link" attrs="{'invisible': [('edi_error_count', '=', 1)]}" />
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</flectra>
|
||||
15
addons/account_edi_extended/views/account_payment_views.xml
Normal file
15
addons/account_edi_extended/views/account_payment_views.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<flectra>
|
||||
<data>
|
||||
<record id="view_payment_form_inherit" model="ir.ui.view">
|
||||
<field name="name">account.payment.form.inherit</field>
|
||||
<field name="model">account.payment</field>
|
||||
<field name="inherit_id" ref="account_edi.view_payment_form_inherit" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[@name='%(account_edi.action_open_payment_edi_documents)d']" position="after">
|
||||
<button name="action_retry_edi_documents_error" type="object" class="oe_link oe_inline" string="Retry" />
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</flectra>
|
||||
@@ -6,7 +6,7 @@ from flectra.tests.common import Form
|
||||
from flectra.exceptions import UserError
|
||||
from flectra.osv import expression
|
||||
|
||||
from datetime import datetime
|
||||
from pathlib import PureWindowsPath
|
||||
|
||||
import logging
|
||||
|
||||
@@ -121,12 +121,17 @@ class AccountEdiFormat(models.Model):
|
||||
attachment_data = element.xpath('cac:Attachment//cbc:EmbeddedDocumentBinaryObject', namespaces=namespaces)
|
||||
if attachment_name and attachment_data:
|
||||
text = attachment_data[0].text
|
||||
# Normalize the name of the file : some e-fff emitters put the full path of the file
|
||||
# (Windows or Linux style) and/or the name of the xml instead of the pdf.
|
||||
# Get only the filename with a pdf extension.
|
||||
name = PureWindowsPath(attachment_name[0].text).stem + '.pdf'
|
||||
attachments |= self.env['ir.attachment'].create({
|
||||
'name': attachment_name[0].text,
|
||||
'name': name,
|
||||
'res_id': invoice.id,
|
||||
'res_model': 'account.move',
|
||||
'datas': text + '=' * (len(text) % 3), # Fix incorrect padding
|
||||
'type': 'binary',
|
||||
'mimetype': 'application/pdf',
|
||||
})
|
||||
if attachments:
|
||||
invoice.with_context(no_new_invoice=True).message_post(attachment_ids=attachments.ids)
|
||||
|
||||
@@ -8,6 +8,8 @@ class AccountMove(models.Model):
|
||||
_inherit = 'account.move'
|
||||
|
||||
def _get_ubl_values(self):
|
||||
self.ensure_one()
|
||||
|
||||
def format_monetary(amount):
|
||||
# Format the monetary values to avoid trailing decimals (e.g. 90.85000000000001).
|
||||
return float_repr(amount, self.currency_id.decimal_places)
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
'installable': True,
|
||||
'auto_install': True,
|
||||
'application': False,
|
||||
'license': 'LGPL-3',
|
||||
'license': 'OEEL-1',
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ class AccountMove(models.Model):
|
||||
log_list = []
|
||||
not_posted_before = self.filtered(lambda r: not r.posted_before)
|
||||
posted = super()._post(soft) # We need the move name to be set, but we also need to know which move are posted for the first time.
|
||||
for line in (not_posted_before & posted).line_ids.filtered(lambda ml: ml.vehicle_id):
|
||||
for line in (not_posted_before & posted).line_ids.filtered(lambda ml: ml.vehicle_id and ml.move_id.move_type == 'in_invoice'):
|
||||
val = {
|
||||
'service_type_id': vendor_bill_service.id,
|
||||
'vehicle_id': line.vehicle_id.id,
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='line_ids']//field[@name='account_id']" position="after">
|
||||
<field name='need_vehicle' invisible='1'/>
|
||||
<field name='vehicle_id' attrs="{'required': [('need_vehicle', '=', True), ('parent.move_type', '=', 'in_invoice')], 'column_invisible': [('parent.move_type', '!=', 'in_invoice')]}" optional='hidden'/>
|
||||
<field name='vehicle_id' attrs="{'required': [('need_vehicle', '=', True), ('parent.move_type', 'in', ('in_invoice', 'in_refund'))], 'column_invisible': [('parent.move_type', 'not in', ('in_invoice', 'in_refund'))]}" optional='hidden'/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='invoice_line_ids']//field[@name='account_id']" position="after">
|
||||
<field name='need_vehicle' invisible='1'/>
|
||||
<field name='vehicle_id' attrs="{'required': [('need_vehicle', '=', True), ('parent.move_type', '=', 'in_invoice')], 'column_invisible': [('parent.move_type', '!=', 'in_invoice')]}" optional='hidden'/>
|
||||
<field name='vehicle_id' attrs="{'required': [('need_vehicle', '=', True), ('parent.move_type', 'in', ('in_invoice', 'in_refund'))], 'column_invisible': [('parent.move_type', 'not in', ('in_invoice', 'in_refund'))]}" optional='hidden'/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
{
|
||||
'name' : 'Irreversible Lock Date',
|
||||
'version' : '1.0',
|
||||
'name': 'Irreversible Lock Date',
|
||||
'version': '1.0',
|
||||
'category': 'Accounting/Accounting',
|
||||
'description': """
|
||||
Make the lock date irreversible:
|
||||
|
||||
* You cannot define stricter conditions on advisors than on users. Then, the lock date on advisor must be set before the lock date for users.
|
||||
* You cannot lock a period that is not finished yet. Then, the lock date for advisors must be set before the last day of the previous month.
|
||||
* The new lock date for advisors must be set after the previous lock date.
|
||||
* You cannot set stricter restrictions on advisors than on users. Therefore, the All Users Lock Date must be anterior (or equal) to the Invoice/Bills Lock Date.
|
||||
* You cannot lock a period that has not yet ended. Therefore, the All Users Lock Date must be anterior (or equal) to the last day of the previous month.
|
||||
* Any new All Users Lock Date must be posterior (or equal) to the previous one.
|
||||
""",
|
||||
'depends' : ['account'],
|
||||
'depends': ['account'],
|
||||
'data': [],
|
||||
}
|
||||
|
||||
@@ -13,11 +13,11 @@ class ResCompany(models.Model):
|
||||
def _autorise_lock_date_changes(self, vals):
|
||||
'''Check the lock dates for the current companies. This can't be done in a api.constrains because we need
|
||||
to perform some comparison between new/old values. This method forces the lock dates to be irreversible.
|
||||
* You cannot define stricter conditions on advisors than on users. Then, the lock date on advisor must be set
|
||||
after the lock date for users.
|
||||
* You cannot lock a period that is not finished yet. Then, the lock date for advisors must be set after the
|
||||
last day of the previous month.
|
||||
* The new lock date for advisors must be set after the previous lock date.
|
||||
* You cannot set stricter restrictions on advisors than on users.
|
||||
Therefore, the All Users Lock Date must be anterior (or equal) to the Invoice/Bills Lock Date.
|
||||
* You cannot lock a period that has not yet ended.
|
||||
Therefore, the All Users Lock Date must be anterior (or equal) to the last day of the previous month.
|
||||
* Any new All Users Lock Date must be posterior (or equal) to the previous one.
|
||||
* You cannot delete a tax lock date, lock a period that is not finished yet or the tax lock date must be set after
|
||||
the last day of the previous month.
|
||||
:param vals: The values passed to the write method.
|
||||
@@ -46,7 +46,7 @@ class ResCompany(models.Model):
|
||||
tax_lock_date = tax_lock_date or old_tax_lock_date
|
||||
# The user attempts to set a tax lock date prior to the last day of previous month
|
||||
if tax_lock_date and tax_lock_date > previous_month:
|
||||
raise UserError(_('You cannot lock a period that is not finished yet. Please make sure that the tax lock date is not set after the last day of the previous month.'))
|
||||
raise UserError(_('You cannot lock a period that has not yet ended. Therefore, the tax lock date must be anterior (or equal) to the last day of the previous month.'))
|
||||
|
||||
# The user attempts to remove the lock date for advisors
|
||||
if old_fiscalyear_lock_date and not fiscalyear_lock_date and 'fiscalyear_lock_date' in vals:
|
||||
@@ -54,7 +54,7 @@ class ResCompany(models.Model):
|
||||
|
||||
# The user attempts to set a lock date for advisors prior to the previous one
|
||||
if old_fiscalyear_lock_date and fiscalyear_lock_date and fiscalyear_lock_date < old_fiscalyear_lock_date:
|
||||
raise UserError(_('The new lock date for advisors must be set after the previous lock date.'))
|
||||
raise UserError(_('Any new All Users Lock Date must be posterior (or equal) to the previous one.'))
|
||||
|
||||
# In case of no new fiscal year in vals, fallback to the oldest
|
||||
fiscalyear_lock_date = fiscalyear_lock_date or old_fiscalyear_lock_date
|
||||
@@ -63,7 +63,7 @@ class ResCompany(models.Model):
|
||||
|
||||
# The user attempts to set a lock date for advisors prior to the last day of previous month
|
||||
if fiscalyear_lock_date > previous_month:
|
||||
raise UserError(_('You cannot lock a period that is not finished yet. Please make sure that the lock date for advisors is not set after the last day of the previous month.'))
|
||||
raise UserError(_('You cannot lock a period that has not yet ended. Therefore, the All Users Lock Date must be anterior (or equal) to the last day of the previous month.'))
|
||||
|
||||
# In case of no new period lock date in vals, fallback to the one defined in the company
|
||||
period_lock_date = period_lock_date or old_period_lock_date
|
||||
@@ -72,7 +72,7 @@ class ResCompany(models.Model):
|
||||
|
||||
# The user attempts to set a lock date for advisors prior to the lock date for users
|
||||
if period_lock_date < fiscalyear_lock_date:
|
||||
raise UserError(_('You cannot define stricter conditions on advisors than on users. Please make sure that the lock date on advisor is set before the lock date for users.'))
|
||||
raise UserError(_('You cannot set stricter restrictions on advisors than on users. Therefore, the All Users Lock Date must be anterior (or equal) to the Invoice/Bills Lock Date.'))
|
||||
|
||||
def write(self, vals):
|
||||
# fiscalyear_lock_date can't be set to a prior date
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<data noupdate="1">
|
||||
<record model="ir.config_parameter" id="adyen_platforms_proxy_url">
|
||||
<field name="key">adyen_platforms.proxy_url</field>
|
||||
<field name="value">https://payment-adyen.flectrahq.com/payment_proxy_adyen/</field>
|
||||
<field name="value">https://payment-adyen.flectra.com/payment_proxy_adyen/</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.config_parameter" id="adyen_platforms_onboarding_url">
|
||||
|
||||
@@ -159,10 +159,10 @@ class AdyenAccount(models.Model):
|
||||
adyen_account_id = super(AdyenAccount, self).create(values)
|
||||
self.env.company.adyen_account_id = adyen_account_id.id
|
||||
|
||||
# Create account on flectrahq.com, proxy and Adyen
|
||||
# Create account on flectra.com, proxy and Adyen
|
||||
response = adyen_account_id._adyen_rpc('create_account_holder', adyen_account_id._format_data())
|
||||
|
||||
# Save adyen_uuid and proxy_token, that have been generated by flectrahq.com and the proxy
|
||||
# Save adyen_uuid and proxy_token, that have been generated by flectra.com and the proxy
|
||||
adyen_account_id.with_context(update_from_adyen=True).write({
|
||||
'adyen_uuid': response['adyen_uuid'],
|
||||
'proxy_token': response['proxy_token'],
|
||||
@@ -192,8 +192,8 @@ class AdyenAccount(models.Model):
|
||||
def action_create_redirect(self):
|
||||
'''
|
||||
Accessing the FormView to create an Adyen account needs to be done through this action.
|
||||
The action will redirect the user to accounts.flectrahq.com to link an Flectra user_id to the Adyen
|
||||
account. After logging in on flectrahq.com the user will be redirected to his DB with a token in
|
||||
The action will redirect the user to accounts.flectra.com to link an Flectra user_id to the Adyen
|
||||
account. After logging in on flectra.com the user will be redirected to his DB with a token in
|
||||
the URL. This token is then needed to create the Adyen account.
|
||||
'''
|
||||
if self.env.company.adyen_account_id:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Adds support for authentication by LDAP server.
|
||||
===============================================
|
||||
This module allows users to login with their LDAP username and password, and
|
||||
will automatically create Flectra users for them on the fly.
|
||||
will automatically create Odoo users for them on the fly.
|
||||
|
||||
**Note:** This module only work on servers that have Python's ``python-ldap`` module installed.
|
||||
|
||||
@@ -10,7 +10,7 @@ Configuration:
|
||||
After installing this module, you need to configure the LDAP parameters in the
|
||||
General Settings menu. Different companies may have different
|
||||
LDAP servers, as long as they have unique usernames (usernames need to be unique
|
||||
in Flectra, even across multiple companies).
|
||||
in Odoo, even across multiple companies).
|
||||
|
||||
Anonymous LDAP binding is also supported (for LDAP servers that allow it), by
|
||||
simply keeping the LDAP user and password empty in the LDAP configuration.
|
||||
@@ -26,24 +26,24 @@ manpage: manpage:`ldap.conf(5)`.
|
||||
|
||||
Security Considerations:
|
||||
------------------------
|
||||
Users' LDAP passwords are never stored in the Flectra database, the LDAP server
|
||||
Users' LDAP passwords are never stored in the Odoo database, the LDAP server
|
||||
is queried whenever a user needs to be authenticated. No duplication of the
|
||||
password occurs, and passwords are managed in one place only.
|
||||
|
||||
Flectra does not manage password changes in the LDAP, so any change of password
|
||||
Odoo does not manage password changes in the LDAP, so any change of password
|
||||
should be conducted by other means in the LDAP directory directly (for LDAP users).
|
||||
|
||||
It is also possible to have local Flectra users in the database along with
|
||||
It is also possible to have local Odoo users in the database along with
|
||||
LDAP-authenticated users (the Administrator account is one obvious example).
|
||||
|
||||
Here is how it works:
|
||||
---------------------
|
||||
* The system first attempts to authenticate users against the local Flectra
|
||||
* The system first attempts to authenticate users against the local Odoo
|
||||
database;
|
||||
* if this authentication fails (for example because the user has no local
|
||||
password), the system then attempts to authenticate against LDAP;
|
||||
|
||||
As LDAP users have blank passwords by default in the local Flectra database
|
||||
As LDAP users have blank passwords by default in the local Odoo database
|
||||
(which means no access), the first step always fails and the LDAP server is
|
||||
queried to do the authentication.
|
||||
|
||||
@@ -61,6 +61,6 @@ allows pre-setting the default groups and menus of the first-time users.
|
||||
assigned as local password for each new LDAP user, effectively setting
|
||||
a *master password* for these users (until manually changed). You
|
||||
usually do not want this. One easy way to setup a template user is to
|
||||
login once with a valid LDAP user, let Flectra create a blank local
|
||||
login once with a valid LDAP user, let Odoo create a blank local
|
||||
user with the same login (and a blank password), then rename this new
|
||||
user to a username that does not exist in LDAP, and setup its groups
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
<data noupdate="1">
|
||||
<record id="provider_openerp" model="auth.oauth.provider">
|
||||
<field name="name">Flectrahq.com Accounts</field>
|
||||
<field name="auth_endpoint">https://accounts.flectrahq.com/oauth2/auth</field>
|
||||
<field name="auth_endpoint">https://accounts.flectra.com/oauth2/auth</field>
|
||||
<field name="scope">userinfo</field>
|
||||
<field name="validation_endpoint">https://accounts.flectrahq.com/oauth2/tokeninfo</field>
|
||||
<field name="validation_endpoint">https://accounts.flectra.com/oauth2/tokeninfo</field>
|
||||
<field name="data_endpoint"></field>
|
||||
<field name="css_class">fa fa-fw o_custom_icon</field>
|
||||
<field name="body">Log in with Flectrahq.com</field>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<label for="auth_oauth_google_client_id" string="Client ID:" class="col-lg-3 o_light_label"/>
|
||||
<field name="auth_oauth_google_client_id" placeholder="e.g. 1234-xyz.apps.googleusercontent.com"/>
|
||||
</div>
|
||||
<a href="https://doc.flectrahq.com//online/general/auth/google.html" target="_blank"><i class="fa fa-fw fa-arrow-right"/>Tutorial</a>
|
||||
<a href="https://flectrahq.com/documentation/user/online/general/auth/google.html" target="_blank"><i class="fa fa-fw fa-arrow-right"/>Tutorial</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -124,7 +124,7 @@ class AuthSignupHome(Home):
|
||||
if values.get('password') != qcontext.get('confirm_password'):
|
||||
raise UserError(_("Passwords do not match; please retype them."))
|
||||
supported_lang_codes = [code for code, _ in request.env['res.lang'].get_installed()]
|
||||
lang = request.context.get('lang', '').split('_')[0]
|
||||
lang = request.context.get('lang', '')
|
||||
if lang in supported_lang_codes:
|
||||
values['lang'] = lang
|
||||
self._signup_with_values(qcontext.get('token'), values)
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
<span class="alert alert-info" role="status">
|
||||
<i class="fa fa-warning"/>
|
||||
Two-factor authentication not enabled
|
||||
<a href="https://doc.flectrahq.com//general/auth/2fa.html"
|
||||
<a href="https://flectrahq.com/documentation/user/general/auth/2fa.html"
|
||||
title="What is this?" class="o_doc_link" target="_blank"></a>
|
||||
</span>
|
||||
<button name="totp_enable_wizard" type="object" string="Enable two-factor authentication"
|
||||
@@ -129,7 +129,7 @@
|
||||
<span class="text-success">
|
||||
<i class="fa fa-check-circle"/>
|
||||
Two-factor authentication enabled
|
||||
<a href="https://doc.flectrahq.com//general/auth/2fa.html"
|
||||
<a href="https://flectrahq.com/documentation/user/general/auth/2fa.html"
|
||||
title="What is this?" class="o_doc_link" target="_blank"></a>
|
||||
</span>
|
||||
<button name="totp_disable" type="object" string="(Disable two-factor authentication)"
|
||||
|
||||
@@ -19,7 +19,7 @@ class Partner(models.Model):
|
||||
self.city = self.city_id.name
|
||||
self.zip = self.city_id.zipcode
|
||||
self.state_id = self.city_id.state_id
|
||||
else:
|
||||
elif self._origin:
|
||||
self.city = False
|
||||
self.zip = False
|
||||
self.state_id = False
|
||||
|
||||
@@ -206,8 +206,8 @@ class BaseAutomation(models.Model):
|
||||
""" Filter the records that satisfy the precondition of action ``self``. """
|
||||
self_sudo = self.sudo()
|
||||
if self_sudo.filter_pre_domain and records:
|
||||
domain = [('id', 'in', records.ids)] + safe_eval.safe_eval(self_sudo.filter_pre_domain, self._get_eval_context())
|
||||
return records.sudo().search(domain).with_env(records.env)
|
||||
domain = safe_eval.safe_eval(self_sudo.filter_pre_domain, self._get_eval_context())
|
||||
return records.sudo().filtered_domain(domain).with_env(records.env)
|
||||
else:
|
||||
return records
|
||||
|
||||
@@ -402,7 +402,12 @@ class BaseAutomation(models.Model):
|
||||
def base_automation_onchange(self):
|
||||
action_rule = self.env['base.automation'].browse(action_rule_id)
|
||||
result = {}
|
||||
server_action = action_rule.sudo().action_server_id.with_context(active_model=self._name, onchange_self=self)
|
||||
server_action = action_rule.sudo().action_server_id.with_context(
|
||||
active_model=self._name,
|
||||
active_id=self._origin.id,
|
||||
active_ids=self._origin.ids,
|
||||
onchange_self=self,
|
||||
)
|
||||
try:
|
||||
res = server_action.run()
|
||||
except Exception as e:
|
||||
|
||||
@@ -5,11 +5,17 @@ import datetime
|
||||
import string
|
||||
import re
|
||||
import stdnum
|
||||
from stdnum.eu.vat import check_vies
|
||||
from stdnum.exceptions import InvalidComponent
|
||||
import logging
|
||||
|
||||
from flectra import api, models, tools, _
|
||||
from flectra.tools.misc import ustr
|
||||
from flectra.exceptions import ValidationError
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
_eu_country_vat = {
|
||||
'GR': 'EL'
|
||||
}
|
||||
@@ -37,7 +43,7 @@ _ref_vat = {
|
||||
'es': 'ESA12345674',
|
||||
'fi': 'FI12345671',
|
||||
'fr': 'FR23334175221',
|
||||
'gb': 'GB123456782',
|
||||
'gb': 'GB123456782 or XI123456782',
|
||||
'gr': 'GR12345670',
|
||||
'hu': 'HU12345676',
|
||||
'hr': 'HR01234567896', # Croatia, contributed by Milan Tribuson
|
||||
@@ -99,20 +105,24 @@ class ResPartner(models.Model):
|
||||
def _check_vies(self, vat):
|
||||
# Store the VIES result in the cache. In case an exception is raised during the request
|
||||
# (e.g. service unavailable), the fallback on simple_vat_check is not kept in cache.
|
||||
return stdnum.eu.vat.check_vies(vat)
|
||||
return check_vies(vat)
|
||||
|
||||
@api.model
|
||||
def vies_vat_check(self, country_code, vat_number):
|
||||
try:
|
||||
# Validate against VAT Information Exchange System (VIES)
|
||||
# see also http://ec.europa.eu/taxation_customs/vies/
|
||||
return self._check_vies(country_code.upper() + vat_number)
|
||||
vies_result = self._check_vies(country_code.upper() + vat_number)
|
||||
return vies_result['valid']
|
||||
except InvalidComponent:
|
||||
return False
|
||||
except Exception:
|
||||
# see http://ec.europa.eu/taxation_customs/vies/checkVatService.wsdl
|
||||
# Fault code may contain INVALID_INPUT, SERVICE_UNAVAILABLE, MS_UNAVAILABLE,
|
||||
# TIMEOUT or SERVER_BUSY. There is no way we can validate the input
|
||||
# with VIES if any of these arise, including the first one (it means invalid
|
||||
# country code or empty VAT number), so we fall back to the simple check.
|
||||
_logger.exception("Failed VIES VAT check.")
|
||||
return self.simple_vat_check(country_code, vat_number)
|
||||
|
||||
@api.model
|
||||
@@ -134,17 +144,18 @@ class ResPartner(models.Model):
|
||||
company = self.env['res.company'].browse(self.env.context['company_id'])
|
||||
else:
|
||||
company = self.env.company
|
||||
if company.vat_check_vies:
|
||||
# force full VIES online check
|
||||
check_func = self.vies_vat_check
|
||||
else:
|
||||
# quick and partial off-line checksum validation
|
||||
check_func = self.simple_vat_check
|
||||
eu_countries = self.env.ref('base.europe').country_ids
|
||||
for partner in self:
|
||||
if not partner.vat:
|
||||
continue
|
||||
#check with country code as prefix of the TIN
|
||||
vat_country, vat_number = self._split_vat(partner.vat)
|
||||
if company.vat_check_vies and partner.commercial_partner_id.country_id in eu_countries:
|
||||
# force full VIES online check
|
||||
check_func = self.vies_vat_check
|
||||
else:
|
||||
# quick and partial off-line checksum validation
|
||||
check_func = self.simple_vat_check
|
||||
if not check_func(vat_country, vat_number):
|
||||
#if fails, check with country code from country
|
||||
country_code = partner.commercial_partner_id.country_id.code
|
||||
@@ -183,7 +194,7 @@ class ResPartner(models.Model):
|
||||
'''
|
||||
# A new VAT number format in Switzerland has been introduced between 2011 and 2013
|
||||
# https://www.estv.admin.ch/estv/fr/home/mehrwertsteuer/fachinformationen/steuerpflicht/unternehmens-identifikationsnummer--uid-.html
|
||||
# The old format "TVA 123456" is not valid since 2014
|
||||
# The old format "TVA 123456" is not valid since 2014
|
||||
# Accepted format are: (spaces are ignored)
|
||||
# CHE#########MWST
|
||||
# CHE#########TVA
|
||||
@@ -461,6 +472,13 @@ class ResPartner(models.Model):
|
||||
res.append(False)
|
||||
return all(res)
|
||||
|
||||
def check_vat_xi(self, vat):
|
||||
""" Temporary Nothern Ireland VAT validation following Brexit
|
||||
As of January 1st 2021, companies in Northern Ireland have a
|
||||
new VAT number starting with XI
|
||||
TODO: remove when stdnum is updated to 1.16 in supported distro"""
|
||||
return stdnum.util.get_cc_module('gb', 'vat').is_valid(vat) if stdnum else True
|
||||
|
||||
def check_vat_in(self, vat):
|
||||
#reference from https://www.gstzen.in/a/format-of-a-gst-number-gstin.html
|
||||
if vat and len(vat) == 15:
|
||||
@@ -474,6 +492,20 @@ class ResPartner(models.Model):
|
||||
return any(re.compile(rx).match(vat) for rx in all_gstin_re)
|
||||
return False
|
||||
|
||||
def check_vat_au(self, vat):
|
||||
'''
|
||||
The Australian equivalent of a VAT number is an ABN number.
|
||||
TFN (Australia Tax file numbers) are private and not to be
|
||||
entered into systems or publicly displayed, so ABN numbers
|
||||
are the public facing number that legally must be displayed
|
||||
on all invoices
|
||||
'''
|
||||
check_func = getattr(stdnum.util.get_cc_module('au', 'abn'), 'is_valid', None)
|
||||
if not check_func:
|
||||
vat = vat.replace(" ", "")
|
||||
return len(vat) == 11 and vat.isdigit()
|
||||
return check_func(vat)
|
||||
|
||||
def format_vat_ch(self, vat):
|
||||
stdnum_vat_format = getattr(stdnum.util.get_cc_module('ch', 'vat'), 'format', None)
|
||||
return stdnum_vat_format('CH' + vat)[2:] if stdnum_vat_format else vat
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
from flectra.tests import common
|
||||
from flectra.tests.common import SavepointCase, tagged
|
||||
from flectra.exceptions import ValidationError
|
||||
from unittest.mock import patch
|
||||
|
||||
from stdnum.eu import vat
|
||||
import stdnum.eu.vat
|
||||
|
||||
class TestStructure(common.TransactionCase):
|
||||
|
||||
class TestStructure(SavepointCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
def check_vies(vat_number):
|
||||
return {'valid': vat_number == 'BE0477472701'}
|
||||
|
||||
super().setUpClass()
|
||||
cls.env.user.company_id.vat_check_vies = False
|
||||
cls._vies_check_func = check_vies
|
||||
|
||||
def test_peru_ruc_format(self):
|
||||
"""Only values that has the length of 11 will be checked as RUC, that's what we are proving. The second part
|
||||
@@ -29,7 +38,7 @@ class TestStructure(common.TransactionCase):
|
||||
def test_parent_validation(self):
|
||||
"""Test the validation with company and contact"""
|
||||
|
||||
# disable the verification to set an invalid vat number
|
||||
# set an invalid vat number
|
||||
self.env.user.company_id.vat_check_vies = False
|
||||
company = self.env["res.partner"].create({
|
||||
"name": "World Company",
|
||||
@@ -43,11 +52,18 @@ class TestStructure(common.TransactionCase):
|
||||
"company_type": "person",
|
||||
})
|
||||
|
||||
def mock_check_vies(vat_number):
|
||||
""" Fake vatnumber method that will only allow one number """
|
||||
return vat_number == 'BE0987654321'
|
||||
|
||||
# reactivate it and correct the vat number
|
||||
with patch.object(vat, 'check_vies', mock_check_vies):
|
||||
with patch('flectra.addons.base_vat.models.res_partner.check_vies', type(self)._vies_check_func):
|
||||
self.env.user.company_id.vat_check_vies = True
|
||||
company.vat = "BE0987654321"
|
||||
with self.assertRaises(ValidationError), self.env.cr.savepoint():
|
||||
company.vat = "BE0987654321" # VIES refused, don't fallback on other check
|
||||
company.vat = "BE0477472701"
|
||||
|
||||
|
||||
@tagged('-standard', 'external')
|
||||
class TestStructureVIES(TestStructure):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.env.user.company_id.vat_check_vies = True
|
||||
cls._vies_check_func = stdnum.eu.vat.check_vies
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
import datetime
|
||||
import time
|
||||
|
||||
from psycopg2 import OperationalError
|
||||
|
||||
from flectra import api, fields, models
|
||||
from flectra import tools
|
||||
from flectra.addons.bus.models.bus import TIMEOUT
|
||||
from flectra.service.model import PG_CONCURRENCY_ERRORS_TO_RETRY
|
||||
from flectra.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
|
||||
|
||||
DISCONNECTION_TIMER = TIMEOUT + 5
|
||||
@@ -34,6 +37,20 @@ class BusPresence(models.Model):
|
||||
""" Updates the last_poll and last_presence of the current user
|
||||
:param inactivity_period: duration in milliseconds
|
||||
"""
|
||||
# This method is called in method _poll() and cursor is closed right
|
||||
# after; see bus/controllers/main.py.
|
||||
try:
|
||||
self._update(inactivity_period)
|
||||
# commit on success
|
||||
self.env.cr.commit()
|
||||
except OperationalError as e:
|
||||
if e.pgcode in PG_CONCURRENCY_ERRORS_TO_RETRY:
|
||||
# ignore concurrency error
|
||||
return self.env.cr.rollback()
|
||||
raise
|
||||
|
||||
@api.model
|
||||
def _update(self, inactivity_period):
|
||||
presence = self.search([('user_id', '=', self._uid)], limit=1)
|
||||
# compute last_presence timestamp
|
||||
last_presence = datetime.datetime.now() - datetime.timedelta(milliseconds=inactivity_period)
|
||||
@@ -51,5 +68,4 @@ class BusPresence(models.Model):
|
||||
# Hide transaction serialization errors, which can be ignored, the presence update is not essential
|
||||
with tools.mute_logger('flectra.sql_db'):
|
||||
presence.write(values)
|
||||
# avoid TransactionRollbackError
|
||||
self.env.cr.commit() # TODO : check if still necessary
|
||||
presence.flush()
|
||||
|
||||
@@ -218,7 +218,7 @@ class AlarmManager(models.AbstractModel):
|
||||
notifications = []
|
||||
users = self.env['res.users'].search([('partner_id', 'in', tuple(partner_ids))])
|
||||
for user in users:
|
||||
notif = self.with_user(user).get_next_notif()
|
||||
notif = self.with_user(user).with_context(allowed_company_ids=user.company_ids.ids).get_next_notif()
|
||||
notifications.append([(self._cr.dbname, 'calendar.alarm', user.partner_id.id), notif])
|
||||
if len(notifications) > 0:
|
||||
self.env['bus.bus'].sendmany(notifications)
|
||||
|
||||
@@ -135,6 +135,8 @@ class Attendee(models.Model):
|
||||
attendee.ids,
|
||||
compute_lang=True)[attendee.id]
|
||||
attendee.event_id.with_context(no_document=True).message_notify(
|
||||
email_from=attendee.event_id.user_id.email_formatted or self.env.user.email_formatted,
|
||||
author_id=attendee.event_id.user_id.partner_id.id or self.env.user.partner_id.id,
|
||||
body=body,
|
||||
subject=subject,
|
||||
partner_ids=attendee.partner_id.ids,
|
||||
|
||||
@@ -345,11 +345,14 @@ class RecurrenceRule(models.Model):
|
||||
data['end_type'] = 'forever'
|
||||
return data
|
||||
|
||||
def _get_lang_week_start(self):
|
||||
lang = self.env['res.lang']._lang_get(self.env.user.lang)
|
||||
week_start = int(lang.week_start) # lang.week_start ranges from '1' to '7'
|
||||
return rrule.weekday(week_start - 1) # rrule expects an int from 0 to 6
|
||||
|
||||
def _get_start_of_period(self, dt):
|
||||
if self.rrule_type == 'weekly':
|
||||
lang = self.env['res.lang']._lang_get(self.env.user.lang)
|
||||
week_start = int(lang.week_start) # lang.week_start ranges from '1' to '7'
|
||||
week_start = rrule.weekday(week_start - 1) # expects an int from 0 to 6
|
||||
week_start = self._get_lang_week_start()
|
||||
start = dt + relativedelta(weekday=week_start(-1))
|
||||
elif self.rrule_type == 'monthly':
|
||||
start = dt + relativedelta(day=1)
|
||||
@@ -461,6 +464,7 @@ class RecurrenceRule(models.Model):
|
||||
if not weekdays:
|
||||
raise UserError(_("You have to choose at least one day in the week"))
|
||||
rrule_params['byweekday'] = weekdays
|
||||
rrule_params['wkst'] = self._get_lang_week_start()
|
||||
|
||||
if self.end_type == 'count': # e.g. stop after X occurence
|
||||
rrule_params['count'] = min(self.count, MAX_RECURRENT_EVENT)
|
||||
|
||||
@@ -315,3 +315,34 @@ class TestCalendar(SavepointCaseWithUserDemo):
|
||||
|
||||
# no more email should be sent
|
||||
_test_one_mail_per_attendee(self, partners)
|
||||
|
||||
def test_event_creation_sudo_other_company(self):
|
||||
""" Check Access right issue when create event with sudo
|
||||
|
||||
Create a company, a user in that company
|
||||
Create an event for someone else in another company as sudo
|
||||
Should not failed for acces right check
|
||||
"""
|
||||
now = fields.Datetime.context_timestamp(self.partner_demo, fields.Datetime.now())
|
||||
|
||||
web_company = self.env['res.company'].sudo().create({'name': "Website Company"})
|
||||
web_user = self.env['res.users'].with_company(web_company).sudo().create({
|
||||
'name': 'web user',
|
||||
'login': 'web',
|
||||
'company_id': web_company.id
|
||||
})
|
||||
self.CalendarEvent.with_user(web_user).with_company(web_company).sudo().create({
|
||||
'name': "Test",
|
||||
'allday': False,
|
||||
'recurrency': False,
|
||||
'partner_ids': [(6, 0, self.partner_demo.ids)],
|
||||
'alarm_ids': [(0, 0, {
|
||||
'name': 'Alarm',
|
||||
'alarm_type': 'notification',
|
||||
'interval': 'minutes',
|
||||
'duration': 30,
|
||||
})],
|
||||
'user_id': self.user_demo.id,
|
||||
'start': fields.Datetime.to_string(now + timedelta(hours=5)),
|
||||
'stop': fields.Datetime.to_string(now + timedelta(hours=6)),
|
||||
})
|
||||
|
||||
@@ -74,6 +74,25 @@ class TestCreateRecurrentEvents(TestRecurrentEvents):
|
||||
(datetime(2019, 11, 5, 8, 0), datetime(2019, 11, 7, 18, 0)),
|
||||
])
|
||||
|
||||
def test_weekly_interval_2_week_start_sunday(self):
|
||||
lang = self.env['res.lang']._lang_get(self.env.user.lang)
|
||||
lang.week_start = '7' # Sunday
|
||||
|
||||
self.event._apply_recurrence_values({
|
||||
'interval': 2,
|
||||
'rrule_type': 'weekly',
|
||||
'tu': True,
|
||||
'count': 2,
|
||||
'event_tz': 'UTC',
|
||||
})
|
||||
recurrence = self.env['calendar.recurrence'].search([('base_event_id', '=', self.event.id)])
|
||||
events = recurrence.calendar_event_ids
|
||||
self.assertEventDates(events, [
|
||||
(datetime(2019, 10, 22, 8, 0), datetime(2019, 10, 24, 18, 0)),
|
||||
(datetime(2019, 11, 5, 8, 0), datetime(2019, 11, 7, 18, 0)),
|
||||
])
|
||||
lang.week_start = '1' # Monday
|
||||
|
||||
def test_weekly_until(self):
|
||||
self.event._apply_recurrence_values({
|
||||
'rrule_type': 'weekly',
|
||||
|
||||
@@ -280,7 +280,7 @@
|
||||
<field name="show_as"/>
|
||||
<filter string="My Meetings" help="My Meetings" name="mymeetings" domain="[('partner_ids.user_ids', 'in', [uid])]"/>
|
||||
<separator/>
|
||||
<filter string="Date" name="filter_start_date" date="start_date"/>
|
||||
<filter string="Date" name="filter_start_date" date="start"/>
|
||||
<separator/>
|
||||
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
|
||||
<group expand="0" string="Group By">
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
'version': '1.0',
|
||||
'depends': ['calendar', 'sms'],
|
||||
'data': [
|
||||
'security/sms_security.xml',
|
||||
'data/sms_data.xml',
|
||||
'views/calendar_views.xml',
|
||||
],
|
||||
|
||||
9
addons/calendar_sms/security/sms_security.xml
Normal file
9
addons/calendar_sms/security/sms_security.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<flectra>
|
||||
<record id="ir_rule_sms_template_system" model="ir.rule">
|
||||
<field name="name">SMS Template: system administrator CRUD on calendar event templates</field>
|
||||
<field name="model_id" ref="sms.model_sms_template"/>
|
||||
<field name="groups" eval="[(4, ref('base.group_system'))]"/>
|
||||
<field name="domain_force">[('model_id.model', '=', 'calendar.event')]</field>
|
||||
</record>
|
||||
</flectra>
|
||||
@@ -86,15 +86,8 @@ class CouponProgram(models.Model):
|
||||
def create(self, vals):
|
||||
program = super(CouponProgram, self).create(vals)
|
||||
if not vals.get('discount_line_product_id', False):
|
||||
discount_line_product_id = self.env['product.product'].create({
|
||||
'name': program.reward_id.display_name,
|
||||
'type': 'service',
|
||||
'taxes_id': False,
|
||||
'supplier_taxes_id': False,
|
||||
'sale_ok': False,
|
||||
'purchase_ok': False,
|
||||
'lst_price': 0, #Do not set a high value to avoid issue with coupon code
|
||||
})
|
||||
values = program._get_discount_product_values()
|
||||
discount_line_product_id = self.env['product.product'].create(values)
|
||||
program.write({'discount_line_product_id': discount_line_product_id.id})
|
||||
return program
|
||||
|
||||
@@ -133,7 +126,7 @@ class CouponProgram(models.Model):
|
||||
return self.currency_id._convert(self[field], currency_to, self.company_id, fields.Date.today())
|
||||
|
||||
def _is_valid_partner(self, partner):
|
||||
if self.rule_partners_domain:
|
||||
if self.rule_partners_domain and self.rule_partners_domain != '[]':
|
||||
domain = ast.literal_eval(self.rule_partners_domain) + [('id', '=', partner.id)]
|
||||
return bool(self.env['res.partner'].search_count(domain))
|
||||
else:
|
||||
@@ -153,3 +146,14 @@ class CouponProgram(models.Model):
|
||||
domain = ast.literal_eval(self.rule_products_domain)
|
||||
return products.filtered_domain(domain)
|
||||
return products
|
||||
|
||||
def _get_discount_product_values(self):
|
||||
return {
|
||||
'name': self.reward_id.display_name,
|
||||
'type': 'service',
|
||||
'taxes_id': False,
|
||||
'supplier_taxes_id': False,
|
||||
'sale_ok': False,
|
||||
'purchase_ok': False,
|
||||
'lst_price': 0, #Do not set a high value to avoid issue with coupon code
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
Use this promo code before
|
||||
<span t-field="o.expiration_date" t-options='{"format": "yyyy-MM-d"}'/>
|
||||
</h4>
|
||||
<h2 class="mt32">
|
||||
<h2 class="mt32" style="margin-top: 32px">
|
||||
<strong class="bg-light" t-esc="o.code" style="padding: 20px 10px;"></strong>
|
||||
</h2>
|
||||
<h4 t-if="o.program_id.rule_min_quantity > 1">
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<field name="rule_minimum_amount" widget='monetary' options="{'currency_field': 'currency_id'}"/>
|
||||
<field name="rule_minimum_amount_tax_inclusion" required="1"/>
|
||||
</div>
|
||||
<field name="company_id" placeholder="Select company" groups="base.group_multi_company"></field>
|
||||
<field name="company_id" placeholder="Select company" groups="base.group_multi_company" required="1"></field>
|
||||
</group>
|
||||
<group name="validity" string="Validity"/>
|
||||
</group>
|
||||
|
||||
@@ -155,20 +155,20 @@ class Lead(models.Model):
|
||||
partner_is_blacklisted = fields.Boolean('Partner is blacklisted', related='partner_id.is_blacklisted', readonly=True)
|
||||
contact_name = fields.Char(
|
||||
'Contact Name', tracking=30,
|
||||
compute='_compute_partner_id_values', readonly=False, store=True)
|
||||
compute='_compute_contact_name', readonly=False, store=True)
|
||||
partner_name = fields.Char(
|
||||
'Company Name', tracking=20, index=True,
|
||||
compute='_compute_partner_id_values', readonly=False, store=True,
|
||||
compute='_compute_partner_name', readonly=False, store=True,
|
||||
help='The name of the future partner company that will be created while converting the lead into opportunity')
|
||||
function = fields.Char('Job Position', compute='_compute_partner_id_values', readonly=False, store=True)
|
||||
title = fields.Many2one('res.partner.title', string='Title',compute='_compute_partner_id_values', readonly=False, store=True)
|
||||
function = fields.Char('Job Position', compute='_compute_function', readonly=False, store=True)
|
||||
title = fields.Many2one('res.partner.title', string='Title', compute='_compute_title', readonly=False, store=True)
|
||||
email_from = fields.Char(
|
||||
'Email', tracking=40, index=True,
|
||||
compute='_compute_email_from', inverse='_inverse_email_from', readonly=False, store=True)
|
||||
phone = fields.Char(
|
||||
'Phone', tracking=50,
|
||||
compute='_compute_phone', inverse='_inverse_phone', readonly=False, store=True)
|
||||
mobile = fields.Char('Mobile', compute='_compute_partner_id_values', readonly=False, store=True)
|
||||
mobile = fields.Char('Mobile', compute='_compute_mobile', readonly=False, store=True)
|
||||
phone_mobile_search = fields.Char('Phone/Mobile', store=False, search='_search_phone_mobile_search')
|
||||
phone_state = fields.Selection([
|
||||
('correct', 'Correct'),
|
||||
@@ -176,20 +176,20 @@ class Lead(models.Model):
|
||||
email_state = fields.Selection([
|
||||
('correct', 'Correct'),
|
||||
('incorrect', 'Incorrect')], string='Email Quality', compute="_compute_email_state", store=True)
|
||||
website = fields.Char('Website', index=True, help="Website of the contact", compute="_compute_partner_id_values", store=True, readonly=False)
|
||||
website = fields.Char('Website', index=True, help="Website of the contact", compute="_compute_website", readonly=False, store=True)
|
||||
lang_id = fields.Many2one('res.lang', string='Language')
|
||||
# Address fields
|
||||
street = fields.Char('Street', compute='_compute_partner_id_values', readonly=False, store=True)
|
||||
street2 = fields.Char('Street2', compute='_compute_partner_id_values', readonly=False, store=True)
|
||||
zip = fields.Char('Zip', change_default=True, compute='_compute_partner_id_values', readonly=False, store=True)
|
||||
city = fields.Char('City', compute='_compute_partner_id_values', readonly=False, store=True)
|
||||
street = fields.Char('Street', compute='_compute_partner_address_values', readonly=False, store=True)
|
||||
street2 = fields.Char('Street2', compute='_compute_partner_address_values', readonly=False, store=True)
|
||||
zip = fields.Char('Zip', change_default=True, compute='_compute_partner_address_values', readonly=False, store=True)
|
||||
city = fields.Char('City', compute='_compute_partner_address_values', readonly=False, store=True)
|
||||
state_id = fields.Many2one(
|
||||
"res.country.state", string='State',
|
||||
compute='_compute_partner_id_values', readonly=False, store=True,
|
||||
compute='_compute_partner_address_values', readonly=False, store=True,
|
||||
domain="[('country_id', '=?', country_id)]")
|
||||
country_id = fields.Many2one(
|
||||
'res.country', string='Country',
|
||||
compute='_compute_partner_id_values', readonly=False, store=True)
|
||||
compute='_compute_partner_address_values', readonly=False, store=True)
|
||||
# Probability (Opportunity only)
|
||||
probability = fields.Float(
|
||||
'Probability', group_operator="avg", copy=False,
|
||||
@@ -300,10 +300,50 @@ class Lead(models.Model):
|
||||
lead.name = _("%s's opportunity") % lead.partner_id.name
|
||||
|
||||
@api.depends('partner_id')
|
||||
def _compute_partner_id_values(self):
|
||||
def _compute_contact_name(self):
|
||||
""" compute the new values when partner_id has changed """
|
||||
for lead in self:
|
||||
lead.update(lead._prepare_values_from_partner(lead.partner_id))
|
||||
lead.update(lead._prepare_contact_name_from_partner(lead.partner_id))
|
||||
|
||||
@api.depends('partner_id')
|
||||
def _compute_partner_name(self):
|
||||
""" compute the new values when partner_id has changed """
|
||||
for lead in self:
|
||||
lead.update(lead._prepare_partner_name_from_partner(lead.partner_id))
|
||||
|
||||
@api.depends('partner_id')
|
||||
def _compute_function(self):
|
||||
""" compute the new values when partner_id has changed """
|
||||
for lead in self:
|
||||
if not lead.function or lead.partner_id.function:
|
||||
lead.function = lead.partner_id.function
|
||||
|
||||
@api.depends('partner_id')
|
||||
def _compute_title(self):
|
||||
""" compute the new values when partner_id has changed """
|
||||
for lead in self:
|
||||
if not lead.title or lead.partner_id.title:
|
||||
lead.title = lead.partner_id.title
|
||||
|
||||
@api.depends('partner_id')
|
||||
def _compute_mobile(self):
|
||||
""" compute the new values when partner_id has changed """
|
||||
for lead in self:
|
||||
if not lead.mobile or lead.partner_id.mobile:
|
||||
lead.mobile = lead.partner_id.mobile
|
||||
|
||||
@api.depends('partner_id')
|
||||
def _compute_website(self):
|
||||
""" compute the new values when partner_id has changed """
|
||||
for lead in self:
|
||||
if not lead.website or lead.partner_id.website:
|
||||
lead.website = lead.partner_id.website
|
||||
|
||||
@api.depends('partner_id')
|
||||
def _compute_partner_address_values(self):
|
||||
""" Sync all or none of address fields """
|
||||
for lead in self:
|
||||
lead.update(lead._prepare_address_values_from_partner(lead.partner_id))
|
||||
|
||||
@api.depends('partner_id.email')
|
||||
def _compute_email_from(self):
|
||||
@@ -314,7 +354,15 @@ class Lead(models.Model):
|
||||
def _inverse_email_from(self):
|
||||
for lead in self:
|
||||
if lead.partner_id and lead.email_from != lead.partner_id.email:
|
||||
lead.partner_id.email = lead.email_from
|
||||
# force reset
|
||||
if not lead.email_from or not lead.partner_id.email:
|
||||
lead.partner_id.email = lead.email_from
|
||||
# compare formatted values as we may have formatting differences between equivalent email
|
||||
else:
|
||||
lead_email_normalized = tools.email_normalize(lead.email_from)
|
||||
partner_email_normalized = tools.email_normalize(lead.partner_id.email)
|
||||
if lead_email_normalized != partner_email_normalized:
|
||||
lead.partner_id.email = lead.email_from
|
||||
|
||||
@api.depends('partner_id.phone')
|
||||
def _compute_phone(self):
|
||||
@@ -406,7 +454,10 @@ class Lead(models.Model):
|
||||
@api.depends('email_from', 'phone', 'partner_id')
|
||||
def _compute_ribbon_message(self):
|
||||
for lead in self:
|
||||
will_write_email = lead.partner_id and lead.email_from != lead.partner_id.email
|
||||
# beware: void user input gives '' which is different from False
|
||||
lead_email_normalized = tools.email_normalize(lead.email_from) or (lead.email_from if lead.email_from else False)
|
||||
partner_email_normalized = tools.email_normalize(lead.partner_id.email) or lead.partner_id.email
|
||||
will_write_email = lead_email_normalized != partner_email_normalized if lead.partner_id else False
|
||||
will_write_phone = False
|
||||
if lead.partner_id and lead.phone != lead.partner_id.phone:
|
||||
# if reset -> obviously new value will be propagated
|
||||
@@ -469,24 +520,34 @@ class Lead(models.Model):
|
||||
values to avoid being reset if partner has no value for them. """
|
||||
|
||||
# Sync all address fields from partner, or none, to avoid mixing them.
|
||||
if any(partner[f] for f in PARTNER_ADDRESS_FIELDS_TO_SYNC):
|
||||
values = {f: partner[f] for f in PARTNER_ADDRESS_FIELDS_TO_SYNC}
|
||||
else:
|
||||
values = {f: self[f] for f in PARTNER_ADDRESS_FIELDS_TO_SYNC}
|
||||
values = self._prepare_address_values_from_partner(partner)
|
||||
|
||||
# For other fields, get the info from the partner, but only if set
|
||||
values.update({f: partner[f] or self[f] for f in PARTNER_FIELDS_TO_SYNC})
|
||||
|
||||
# Fields with specific logic
|
||||
values.update(self._prepare_contact_name_from_partner(partner))
|
||||
values.update(self._prepare_partner_name_from_partner(partner))
|
||||
|
||||
return self._convert_to_write(values)
|
||||
|
||||
def _prepare_address_values_from_partner(self, partner):
|
||||
# Sync all address fields from partner, or none, to avoid mixing them.
|
||||
if any(partner[f] for f in PARTNER_ADDRESS_FIELDS_TO_SYNC):
|
||||
values = {f: partner[f] for f in PARTNER_ADDRESS_FIELDS_TO_SYNC}
|
||||
else:
|
||||
values = {f: self[f] for f in PARTNER_ADDRESS_FIELDS_TO_SYNC}
|
||||
return values
|
||||
|
||||
def _prepare_contact_name_from_partner(self, partner):
|
||||
contact_name = False if partner.is_company else partner.name
|
||||
return {'contact_name': contact_name or self.contact_name}
|
||||
|
||||
def _prepare_partner_name_from_partner(self, partner):
|
||||
partner_name = partner.parent_id.name
|
||||
if not partner_name and partner.is_company:
|
||||
partner_name = partner.name
|
||||
contact_name = False if partner.is_company else partner.name
|
||||
values.update({
|
||||
'partner_name': partner_name or self.partner_name,
|
||||
'contact_name': contact_name or self.contact_name,
|
||||
})
|
||||
return self._convert_to_write(values)
|
||||
return {'partner_name': partner_name or self.partner_name}
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# ORM
|
||||
@@ -509,7 +570,7 @@ class Lead(models.Model):
|
||||
|
||||
for lead, values in zip(leads, vals_list):
|
||||
if any(field in ['active', 'stage_id'] for field in values):
|
||||
lead._handle_won_lost(vals)
|
||||
lead._handle_won_lost(values)
|
||||
|
||||
return leads
|
||||
|
||||
@@ -1336,24 +1397,29 @@ class Lead(models.Model):
|
||||
return self.env.ref('crm.mt_lead_lost')
|
||||
return super(Lead, self)._track_subtype(init_values)
|
||||
|
||||
def _notify_get_groups(self):
|
||||
def _notify_get_groups(self, msg_vals=None):
|
||||
""" Handle salesman recipients that can convert leads into opportunities
|
||||
and set opportunities as won / lost. """
|
||||
groups = super(Lead, self)._notify_get_groups()
|
||||
groups = super(Lead, self)._notify_get_groups(msg_vals=msg_vals)
|
||||
msg_vals = msg_vals or {}
|
||||
|
||||
self.ensure_one()
|
||||
if self.type == 'lead':
|
||||
convert_action = self._notify_get_action_link('controller', controller='/lead/convert')
|
||||
convert_action = self._notify_get_action_link('controller', controller='/lead/convert', **msg_vals)
|
||||
salesman_actions = [{'url': convert_action, 'title': _('Convert to opportunity')}]
|
||||
else:
|
||||
won_action = self._notify_get_action_link('controller', controller='/lead/case_mark_won')
|
||||
lost_action = self._notify_get_action_link('controller', controller='/lead/case_mark_lost')
|
||||
won_action = self._notify_get_action_link('controller', controller='/lead/case_mark_won', **msg_vals)
|
||||
lost_action = self._notify_get_action_link('controller', controller='/lead/case_mark_lost', **msg_vals)
|
||||
salesman_actions = [
|
||||
{'url': won_action, 'title': _('Won')},
|
||||
{'url': lost_action, 'title': _('Lost')}]
|
||||
|
||||
if self.team_id:
|
||||
salesman_actions.append({'url': self._notify_get_action_link('view', res_id=self.team_id.id, model=self.team_id._name), 'title': _('Sales Team Settings')})
|
||||
custom_params = dict(msg_vals, res_id=self.team_id.id, model=self.team_id._name)
|
||||
salesman_actions.append({
|
||||
'url': self._notify_get_action_link('view', **custom_params),
|
||||
'title': _('Sales Team Settings')
|
||||
})
|
||||
|
||||
salesman_group_id = self.env.ref('sales_team.group_sale_salesman').id
|
||||
new_group = (
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from flectra import api, fields, models
|
||||
|
||||
|
||||
@@ -67,7 +69,16 @@ class ResConfigSettings(models.TransientModel):
|
||||
""" As config_parameters does not accept Date field,
|
||||
we get the date back from the Char config field, to ease the configuration in config panel """
|
||||
for setting in self:
|
||||
setting.predictive_lead_scoring_start_date = fields.Date.to_date(setting.predictive_lead_scoring_start_date_str)
|
||||
lead_scoring_start_date = setting.predictive_lead_scoring_start_date_str
|
||||
# if config param is deleted / empty, set the date 8 days prior to current date
|
||||
if not lead_scoring_start_date:
|
||||
setting.predictive_lead_scoring_start_date = fields.Date.to_date(fields.Date.today() - timedelta(days=8))
|
||||
else:
|
||||
try:
|
||||
setting.predictive_lead_scoring_start_date = fields.Date.to_date(lead_scoring_start_date)
|
||||
except ValueError:
|
||||
# the config parameter is malformed, so set the date 8 days prior to current date
|
||||
setting.predictive_lead_scoring_start_date = fields.Date.to_date(fields.Date.today() - timedelta(days=8))
|
||||
|
||||
def _inverse_pls_start_date_str(self):
|
||||
""" As config_parameters does not accept Date field,
|
||||
|
||||
@@ -50,13 +50,6 @@
|
||||
<field name="groups" eval="[(4, ref('sales_team.group_sale_salesman_all_leads'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="calendar_event_global" model="ir.rule">
|
||||
<field name="name">Hide Private Meetings</field>
|
||||
<field ref="model_calendar_event" name="model_id"/>
|
||||
<field eval="1" name="global"/>
|
||||
<field name="domain_force">['|',('user_id','=',user.id),('show_as','=','busy')]</field>
|
||||
</record>
|
||||
|
||||
<record id="crm_activity_report_rule_all_activities" model="ir.rule">
|
||||
<field name="name">All Activities</field>
|
||||
<field ref="model_crm_activity_report" name="model_id"/>
|
||||
|
||||
@@ -9,7 +9,7 @@ var _t = core._t;
|
||||
tour.register('crm_tour', {
|
||||
url: "/web",
|
||||
rainbowManMessage: _t("Congrats, best of luck catching such big fish! :)"),
|
||||
sequence: 5,
|
||||
sequence: 10,
|
||||
}, [tour.stepUtils.showAppsMenuItem(), {
|
||||
trigger: '.o_app[data-menu-xmlid="crm.crm_menu_root"]',
|
||||
content: _t('Ready to boost your sales? Let\'s have a look at your <b>Pipeline</b>.'),
|
||||
@@ -36,11 +36,6 @@ tour.register('crm_tour', {
|
||||
trigger: ".ui-menu-item > a",
|
||||
auto: true,
|
||||
in_modal: false,
|
||||
}, {
|
||||
trigger: '.o_kanban_quick_create .o_field_monetary[name="expected_revenue"] input',
|
||||
content: _t("Define here the Expected Revenue of this Opportunity."),
|
||||
position: 'right',
|
||||
run: "text 12.3",
|
||||
}, {
|
||||
trigger: ".o_kanban_quick_create .o_kanban_add",
|
||||
content: _t("Now, <b>add your Opportunity</b> to your Pipeline."),
|
||||
@@ -49,7 +44,7 @@ tour.register('crm_tour', {
|
||||
trigger: ".o_opportunity_kanban .o_kanban_group:first-child .o_kanban_record:last-child .oe_kanban_content",
|
||||
extra_trigger: ".o_opportunity_kanban",
|
||||
content: _t("<b>Drag & drop opportunities</b> between columns as you progress in your sales cycle."),
|
||||
position: "bottom",
|
||||
position: "right",
|
||||
run: "drag_and_drop .o_opportunity_kanban .o_kanban_group:eq(2) ",
|
||||
}, {
|
||||
trigger: ".o_kanban_record:not(.o_updating) .o_activity_color_default",
|
||||
|
||||
@@ -47,6 +47,9 @@ flectra.define('crm.tour_crm_rainbowman', function (require) {
|
||||
}, {
|
||||
trigger: "button.o_kanban_add",
|
||||
content: "create lead",
|
||||
}, {
|
||||
trigger: ".o_kanban_record .o_kanban_record_title:contains('Test Lead 2')",
|
||||
run: function () {} // wait for the record to be properly created
|
||||
}, {
|
||||
// move first test back to new stage to be able to test rainbowman a second time
|
||||
trigger: ".o_kanban_record .o_kanban_record_title:contains('Test Lead 1')",
|
||||
|
||||
@@ -118,9 +118,32 @@ class TestCrmCommon(TestSalesCommon, MailCase):
|
||||
cls.lead_team_1_lost.action_set_lost()
|
||||
(cls.lead_team_1_won | cls.lead_team_1_lost).flush()
|
||||
|
||||
# email / phone data
|
||||
cls.test_email_data = [
|
||||
'"Planet Express" <planet.express@test.example.com>',
|
||||
'"Philip, J. Fry" <philip.j.fry@test.example.com>',
|
||||
'"Turanga Leela" <turanga.leela@test.example.com>',
|
||||
]
|
||||
cls.test_email_data_normalized = [
|
||||
'planet.express@test.example.com',
|
||||
'philip.j.fry@test.example.com',
|
||||
'turanga.leela@test.example.com',
|
||||
]
|
||||
cls.test_pĥone_data = [
|
||||
'+1 202 555 0122', # formatted US number
|
||||
'202 555 0999', # local US number
|
||||
'202 555 0888', # local US number
|
||||
]
|
||||
cls.test_pĥone_data_sanitized = [
|
||||
'+12025550122',
|
||||
'+12025550999',
|
||||
'+12025550888',
|
||||
]
|
||||
|
||||
# create some test contact and companies
|
||||
cls.contact_company_1 = cls.env['res.partner'].create({
|
||||
'name': 'Planet Express',
|
||||
'email': 'planet.express@test.example.com',
|
||||
'email': cls.test_email_data[0],
|
||||
'is_company': True,
|
||||
'street': '57th Street',
|
||||
'city': 'New New York',
|
||||
@@ -129,8 +152,10 @@ class TestCrmCommon(TestSalesCommon, MailCase):
|
||||
})
|
||||
cls.contact_1 = cls.env['res.partner'].create({
|
||||
'name': 'Philip J Fry',
|
||||
'email': 'philip.j.fry@test.example.com',
|
||||
'mobile': '+1 202 555 0122',
|
||||
'email': cls.test_email_data[1],
|
||||
'mobile': cls.test_pĥone_data[0],
|
||||
'title': cls.env.ref('base.res_partner_title_mister').id,
|
||||
'function': 'Delivery Boy',
|
||||
'phone': False,
|
||||
'parent_id': cls.contact_company_1.id,
|
||||
'is_company': False,
|
||||
@@ -141,13 +166,14 @@ class TestCrmCommon(TestSalesCommon, MailCase):
|
||||
})
|
||||
cls.contact_2 = cls.env['res.partner'].create({
|
||||
'name': 'Turanga Leela',
|
||||
'email': 'turanga.leela@test.example.com',
|
||||
'email': cls.test_email_data[2],
|
||||
'mobile': cls.test_pĥone_data[1],
|
||||
'phone': cls.test_pĥone_data[2],
|
||||
'parent_id': False,
|
||||
'is_company': False,
|
||||
'street': 'Cookieville Minimum-Security Orphanarium',
|
||||
'city': 'New New York',
|
||||
'country_id': cls.env.ref('base.us').id,
|
||||
'mobile': '+1 202 555 0999',
|
||||
'zip': '97648',
|
||||
})
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user