[IMP] account: Optional bank statements

Bank statements are now optional in accounting. The users add statements directly and can assign statements for control reasons.
The users can enter end date and end balance real for the statements and attach the bank statement scan.

The system checks if there is a gap (lines without statement) before current statement and warns the user
with decoration, also we check if the ending balance match the accumulated balance of the last line.

Bank statement lines now show an accumulated balance and can be sorted by drag and drop in the same date, if the user drags a line
to a position with lines of another date, it simply doesn't have any effect.
Rename accumulated balance to cumulative balance, a better name

closes odoo/odoo#99092

Task: 2879904
Related: odoo/upgrade#3906
Related: odoo/enterprise#30824
Signed-off-by: Laurent Smet <las@odoo.com>
This commit is contained in:
PoMa
2022-09-20 15:24:23 +02:00
committed by Laurent Smet
parent 7e23eda02f
commit 4fb0933986
35 changed files with 1905 additions and 2388 deletions
-1
View File
@@ -31,7 +31,6 @@ You could use this simplified accounting in case you work with an (external) acc
'wizard/account_payment_register_views.xml',
'views/account_move_views.xml',
'wizard/setup_wizards_view.xml',
'wizard/pos_box.xml',
'views/account_account_views.xml',
'views/account_group_views.xml',
'views/account_journal_views.xml',
+25 -8
View File
@@ -5,7 +5,6 @@ from datetime import timedelta
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models, Command
from odoo.exceptions import UserError, ValidationError
from odoo.tools.misc import file_open, formatLang
_logger = logging.getLogger(__name__)
@@ -104,51 +103,71 @@ class AccountChartTemplate(models.Model):
def _get_demo_data_statement(self):
cid = self.env.company.id
ref = self.env.ref
bnk_journal = self.env['account.journal'].search(
domain=[('type', '=', 'bank'), ('company_id', '=', cid)],
limit=1,
)
return ('account.bank.statement', {
f'{cid}_demo_bank_statement_0': {
'name': f'{bnk_journal.name} - {time.strftime("%Y")}-01-01/1',
'balance_end_real': 5103.0,
'balance_start': 0.0,
'line_ids': [
Command.create({
'journal_id': bnk_journal.id,
'payment_ref': 'Initial balance',
'amount': 5103.0,
'date': time.strftime('%Y-01-01'),
}),
]
},
f'{cid}_demo_bank_statement_1': {
'journal_id': self.env['account.journal'].search([
('type', '=', 'bank'),
('company_id', '=', cid),
], limit=1).id,
'date': time.strftime('%Y')+'-01-01',
'name': f'{bnk_journal.name} - {time.strftime("%Y")}-01-01/2',
'balance_end_real': 9944.87,
'balance_start': 5103.0,
'line_ids': [
Command.create({
'journal_id': bnk_journal.id,
'payment_ref': time.strftime('INV/%Y/00002 and INV/%Y/00003'),
'amount': 1275.0,
'date': time.strftime('%Y-01-01'),
'partner_id': ref('base.res_partner_12').id
}),
Command.create({
'journal_id': bnk_journal.id,
'payment_ref': 'Bank Fees',
'amount': -32.58,
'date': time.strftime('%Y-01-01'),
}),
Command.create({
'journal_id': bnk_journal.id,
'payment_ref': 'Prepayment',
'amount': 650,
'date': time.strftime('%Y-01-01'),
'partner_id': ref('base.res_partner_12').id
}),
Command.create({
'journal_id': bnk_journal.id,
'payment_ref': time.strftime(f'First {formatLang(self.env, 2000, currency_obj=self.env.company.currency_id)} of invoice %Y/00001'),
'amount': 2000,
'date': time.strftime('%Y-01-01'),
'partner_id': ref('base.res_partner_12').id
}),
Command.create({
'journal_id': bnk_journal.id,
'payment_ref': 'Last Year Interests',
'amount': 102.78,
'date': time.strftime('%Y-01-01'),
}),
Command.create({
'journal_id': bnk_journal.id,
'payment_ref': time.strftime('INV/%Y/00002'),
'amount': 750,
'date': time.strftime('%Y-01-01'),
'partner_id': ref('base.res_partner_2').id
}),
Command.create({
'journal_id': bnk_journal.id,
'payment_ref': f'R:9772938 10/07 AX 9415116318 T:5 BRT: {formatLang(self.env, 100.0, digits=2)} C/ croip',
'amount': 96.67,
'date': time.strftime('%Y-01-01'),
@@ -315,8 +334,6 @@ class AccountChartTemplate(models.Model):
move.action_post()
except Exception:
_logger.exception('Error while posting demo data')
elif created._name == 'account.bank.statement':
created.button_post()
@api.model
def _get_demo_account(self, xml_id, account_type, company):
+1
View File
@@ -17,6 +17,7 @@ from . import account_full_reconcile
from . import account_payment
from . import account_payment_method
from . import account_bank_statement
from . import account_bank_statement_line
from . import chart_template
from . import account_analytic_account
from . import account_analytic_distribution_model
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,806 @@
from odoo import api, Command, fields, models, _
from odoo.exceptions import UserError, ValidationError
from odoo.tools import html2plaintext
from odoo.osv.expression import get_unaccent_wrapper
from odoo.addons.base.models.res_bank import sanitize_account_number
from xmlrpc.client import MAXINT
class AccountBankStatementLine(models.Model):
_name = "account.bank.statement.line"
_inherits = {'account.move': 'move_id'}
_description = "Bank Statement Line"
_order = "internal_index desc"
_check_company_auto = True
# FIXME: Field having the same name in both tables are confusing (partner_id). We don't change it because:
# - It's a mess to track/fix.
# - Some fields here could be simplified when the onchanges will be gone in account.move.
# Should be improved in the future.
# - there should be a better way for syncing account_moves with bank transactions, payments, invoices, etc.
# == Business fields ==
def default_get(self, fields_list):
defaults = super().default_get(fields_list)
# copy the date and statement from the latest transaction of the same journal to help the user
# to enter the next transaction, they do not have to enter the date and the statement every time until the
# statement is completed. It is only possible if we know the journal that is used, so it can only be done
# in a view in which the journal is already set and so is single journal view.
if'journal_id' in defaults:
last_line = self.search([('journal_id', '=', defaults.get('journal_id'))], limit=1)
statement = last_line.statement_id
if statement and not statement.is_complete:
defaults.setdefault(
'statement_id', statement.id
)
if statement.date:
defaults.setdefault(
'date', statement.date
)
return defaults
move_id = fields.Many2one(
comodel_name='account.move',
auto_join=True,
string='Journal Entry', required=True, readonly=True, ondelete='cascade',
check_company=True)
statement_id = fields.Many2one(
comodel_name='account.bank.statement',
string='Statement',
)
# Payments generated during the reconciliation of this bank statement lines.
payment_ids = fields.Many2many(
comodel_name='account.payment',
relation='account_payment_account_bank_statement_line_rel',
string='Auto-generated Payments',
)
# This sequence is working reversed because the default order is reversed, more info in compute_internal_index
sequence = fields.Integer(default=1)
partner_id = fields.Many2one(
comodel_name='res.partner',
string='Partner', ondelete='restrict',
domain="['|', ('parent_id','=', False), ('is_company','=',True)]",
check_company=True)
# Technical field used to store the bank account number before its creation, upon the line's processing
account_number = fields.Char(string='Bank Account Number')
# This field is used to record the third party name when importing bank statement in electronic format,
# when the partner doesn't exist yet in the database (or cannot be found).
partner_name = fields.Char()
# Transaction type is used in electronic format, when the type of transaction is available in the imported file.
transaction_type = fields.Char()
payment_ref = fields.Char(string='Label')
currency_id = fields.Many2one(
comodel_name='res.currency',
string='Journal Currency',
compute='_compute_currency_id', store=True,
)
amount = fields.Monetary()
# Note the values of this field does not necessarily correspond to the cumulated balance in the account move line.
# here these values correspond to occurrence order (the reality) and they should match the bank report but in
# the move lines, it corresponds to the recognition order. Also, the statements act as checkpoints on this field
running_balance = fields.Monetary(
compute='_compute_running_balance'
)
foreign_currency_id = fields.Many2one(
comodel_name='res.currency',
string="Foreign Currency",
help="The optional other currency if it is a multi-currency entry.",
)
amount_currency = fields.Monetary(
string="Amount in Currency",
currency_field='foreign_currency_id',
help="The amount expressed in an optional other currency if it is a multi-currency entry.",
)
# == Technical fields ==
# The amount left to be reconciled on this statement line (signed according to its move lines' balance),
# expressed in its currency. This is a technical field use to speed up the application of reconciliation models.
amount_residual = fields.Float(
string="Residual Amount",
compute="_compute_is_reconciled",
store=True,
)
country_code = fields.Char(
related='company_id.account_fiscal_country_id.code'
)
# Technical field used to store the internal reference of the statement line for fast indexing and easier comparing
# of statement lines. It holds the combination of the date, sequence and id of each line. Without this field,
# the search/sorting lines would be very slow. The date field is related and stored in the account.move model,
# so it is not possible to have an index on it (unless we use a sql view which is too complicated).
# Using this prevents us having a compound index, and extensive `where` clauses.
# Without this finding lines before current line (which we need e.g. for calculating the running balance)
# would need a query like this:
# date < current date OR (date = current date AND sequence > current date) or (
# date = current date AND sequence = current sequence AND id < current id)
# which needs to be repeated all over the code.
# This would be simply "internal index < current internal index" using this field.
internal_index = fields.Char(
string='Internal Reference',
compute='_compute_internal_index', store=True,
index=True,
)
# Technical field indicating if the statement line is already reconciled.
is_reconciled = fields.Boolean(
string='Is Reconciled',
compute='_compute_is_reconciled', store=True,
)
statement_complete = fields.Boolean(
related='statement_id.is_complete',
)
statement_valid = fields.Boolean(
related='statement_id.is_valid',
)
# -------------------------------------------------------------------------
# COMPUTE METHODS
# -------------------------------------------------------------------------
@api.depends('journal_id.currency_id')
def _compute_currency_id(self):
for st_line in self:
st_line.currency_id = st_line.journal_id.currency_id or st_line.company_id.currency_id
def _compute_running_balance(self):
# It looks back to find the latest statement and uses its balance_start as an anchor point for calculation, so
# that the running balance is always relative to the latest statement. In this way we do not need to calculate
# the running balance for all statement lines every time.
# If there are statements inside the computed range, their balance_start has priority over calculated balance.
self.statement_id.flush_model(['balance_start', 'first_line_index'])
self.flush_model(['internal_index', 'date', 'journal_id', 'statement_id', 'amount'])
record_by_id = {x.id: x for x in self}
for journal in self.journal_id:
journal_lines = self.filtered(lambda line: line.journal_id == journal).sorted('internal_index')
max_index = max(journal_lines.mapped('internal_index'))
min_index = min(journal_lines.mapped('internal_index'))
# Find the oldest index for each journal.
self._cr.execute(
"""
SELECT first_line_index, balance_start
FROM account_bank_statement
WHERE
first_line_index < %s
AND journal_id = %s
ORDER BY first_line_index DESC
LIMIT 1
""",
[min_index, journal.id],
)
current_running_balance = 0.0
extra_clause = ''
extra_params = []
row = self._cr.fetchone()
if row:
starting_index, current_running_balance = row
extra_clause = "AND st_line.internal_index >= %s"
extra_params.append(starting_index)
self._cr.execute(
f"""
SELECT
st_line.id,
st_line.amount,
st.first_line_index = st_line.internal_index AS is_anchor,
st.balance_start
FROM account_bank_statement_line st_line
JOIN account_move move ON move.statement_line_id = st_line.id
LEFT JOIN account_bank_statement st ON st.id = st_line.statement_id
WHERE
st_line.internal_index <= %s
AND move.journal_id = %s
{extra_clause}
ORDER BY st_line.internal_index
""",
[max_index, journal.id] + extra_params,
)
for st_line_id, amount, is_anchor, balance_start in self._cr.fetchall():
if is_anchor:
current_running_balance = balance_start
current_running_balance += amount
if record_by_id.get(st_line_id):
record_by_id[st_line_id].running_balance = current_running_balance
@api.depends('date', 'sequence')
def _compute_internal_index(self):
"""
Internal index is a field that holds the combination of the date, compliment of sequence and id of each line.
Using this prevents us having a compound index, and extensive where clauses.
Without this finding lines before current line (which we need for calculating the running balance)
would need a query like this:
date < current date OR (date = current date AND sequence > current date) or (
date = current date AND sequence = current sequence AND id < current id)
which needs to be repeated all over the code.
This would be simply "internal index < current internal index" using this field.
Also, we would need a compound index of date + sequence + id
on the table which is not possible because date is not in this table (it is in the account move table)
unless we use a sql view which is more complicated.
"""
# ensure we are using correct value for reversing sequence in the index (2147483647)
# NOTE: assert self._fields['sequence'].column_type[1] == 'int4'
# if for any reason it changes (how unlikely), we need to update this code
for st_line in self.filtered(lambda line: line._origin.id):
st_line.internal_index = f'{st_line.date.strftime("%Y%m%d")}' \
f'{MAXINT - st_line.sequence:0>10}' \
f'{st_line._origin.id:0>10}'
@api.depends('journal_id', 'currency_id', 'amount', 'foreign_currency_id', 'amount_currency',
'move_id.to_check',
'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).
Also computes the residual amount of the statement line.
"""
for st_line in self:
_liquidity_lines, suspense_lines, _other_lines = st_line._seek_for_lines()
# 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
elif suspense_lines.account_id.reconcile:
st_line.amount_residual = sum(suspense_lines.mapped('amount_residual_currency'))
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
@api.onchange('journal_id')
def _onchange_journal_id(self):
"""
Reset the statement line when the journal is changed. In some rare cases that journal is not in the context
the journal_id field might be accessible to the user. In this cse we need to reset the statement_id field when
the journal_id is changed.
:return:
"""
self.statement_id = self._get_default_statement(self.journal_id.id, self.date)
# -------------------------------------------------------------------------
# CONSTRAINT METHODS
# -------------------------------------------------------------------------
@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. """
for st_line in self:
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 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."))
if not st_line.amount_currency and st_line.foreign_currency_id:
raise ValidationError(_("You can't provide a foreign currency without specifying an amount in "
"'Amount in Currency' field."))
# -------------------------------------------------------------------------
# LOW-LEVEL METHODS
# -------------------------------------------------------------------------
def new(self, values=None, origin=None, ref=None):
st_line = super().new(values, origin, ref)
if not st_line.journal_id: # might not be computed because declared by inheritance
st_line.move_id._compute_journal_id()
return st_line
@api.model_create_multi
def create(self, vals_list):
# OVERRIDE
counterpart_account_ids = []
for vals in vals_list:
if 'statement_id' in vals and 'journal_id' not in vals:
statement = self.env['account.bank.statement'].browse(vals['statement_id'])
# Ensure the journal is the same as the statement one.
# journal_id is a required field in the view, so it should be always available if the user
# is creating the record, however, if a sync/import modules tries to add a line to an existing
# statement they can omit the journal field because it can be obtained from the statement
if statement.journal_id:
vals['journal_id'] = statement.journal_id.id
# Force the move_type to avoid inconsistency with residual 'default_move_type' inside the context.
vals['move_type'] = 'entry'
# Hack to force different account instead of the suspense account.
counterpart_account_ids.append(vals.pop('counterpart_account_id', None))
st_lines = super().create(vals_list)
for i, st_line in enumerate(st_lines):
counterpart_account_id = counterpart_account_ids[i]
to_write = {'statement_line_id': st_line.id, 'narration': st_line.narration}
if 'line_ids' not in vals_list[i]:
to_write['line_ids'] = [(0, 0, line_vals) for line_vals in st_line._prepare_move_line_default_vals(
counterpart_account_id=counterpart_account_id)]
st_line.move_id.write(to_write)
# Otherwise field narration will be recomputed silently (at next flush) when writing on partner_id
self.env.remove_to_compute(st_line.move_id._fields['narration'], st_line.move_id)
# No need for the user to manage their status (from 'Draft' to 'Posted')
st_lines.move_id.action_post()
return st_lines
def write(self, vals):
# OVERRIDE
res = super().write(vals)
self._synchronize_to_moves(set(vals.keys()))
return res
def unlink(self):
# OVERRIDE to unlink the inherited account.move (move_id field) as well.
moves = self.with_context(force_delete=True).mapped('move_id')
res = super().unlink()
moves.unlink()
return res
# -------------------------------------------------------------------------
# ACTION METHODS
# -------------------------------------------------------------------------
def action_undo_reconciliation(self):
""" Undo the reconciliation made on the statement line and reset their journal items
to their original states.
"""
self.line_ids.remove_move_reconcile()
self.payment_ids.unlink()
for st_line in self:
st_line.with_context(force_delete=True).write({
'to_check': False,
'line_ids': [Command.clear()] + [
Command.create(line_vals) for line_vals in st_line._prepare_move_line_default_vals()],
})
# -------------------------------------------------------------------------
# HELPERS
# -------------------------------------------------------------------------
def _find_or_create_bank_account(self):
bank_account = self.env['res.partner.bank'].search([
('acc_number', '=', self.account_number),
('partner_id', '=', self.partner_id.id),
])
if not bank_account:
bank_account = self.env['res.partner.bank'].create({
'acc_number': self.account_number,
'partner_id': self.partner_id.id,
})
return bank_account
def _get_amounts_with_currencies(self):
"""
Returns the line amount in company, journal and foreign currencies
"""
self.ensure_one()
company_currency = self.journal_id.company_id.currency_id
journal_currency = self.journal_id.currency_id or company_currency
foreign_currency = self.foreign_currency_id or journal_currency or company_currency
journal_amount = self.amount
if foreign_currency == journal_currency:
transaction_amount = journal_amount
else:
transaction_amount = self.amount_currency
if journal_currency == company_currency:
company_amount = journal_amount
elif foreign_currency == company_currency:
company_amount = transaction_amount
else:
company_amount = journal_currency._convert(journal_amount, company_currency,
self.journal_id.company_id, self.date)
return company_amount, company_currency, journal_amount, journal_currency, transaction_amount, foreign_currency
def _get_default_amls_matching_domain(self):
return [
# Base domain.
('display_type', 'not in', ('line_section', 'line_note')),
('parent_state', '=', 'posted'),
('company_id', '=', self.company_id.id),
# Reconciliation domain.
('reconciled', '=', False),
('account_id.reconcile', '=', True),
# Special domain for payments.
'|',
('account_id.account_type', 'not in', ('asset_receivable', 'liability_payable')),
('payment_id', '=', False),
# Special domain for statement lines.
('statement_line_id', '!=', self.id),
]
@api.model
def _get_default_journal(self):
journal_type = self.env.context.get('journal_type', 'bank')
return self.env['account.journal'].search([
('type', '=', journal_type),
('company_id', '=', self.env.company.id)
], limit=1)
@api.model
def _get_default_statement(self, journal_id=None, date=None):
statement = self.search(
domain=[
('journal_id', '=', journal_id or self._get_default_journal().id),
('date', '<=', date or fields.Date.today()),
],
limit=1
).statement_id
if not statement.is_complete:
return statement
def _get_st_line_strings_for_matching(self, allowed_fields=None):
""" Collect the strings that could be used on the statement line to perform some matching.
:param allowed_fields: A explicit list of fields to consider.
:return: A list of strings.
"""
self.ensure_one()
def _get_text_value(field_name):
if self._fields[field_name].type == 'html':
return self[field_name] and html2plaintext(self[field_name])
else:
return self[field_name]
st_line_text_values = []
if allowed_fields is None or 'payment_ref' in allowed_fields:
value = _get_text_value('payment_ref')
if value:
st_line_text_values.append(value)
if allowed_fields is None or 'narration' in allowed_fields:
value = _get_text_value('narration')
if value:
st_line_text_values.append(value)
if allowed_fields is None or 'ref' in allowed_fields:
value = _get_text_value('ref')
if value:
st_line_text_values.append(value)
return st_line_text_values
def _prepare_counterpart_amounts_using_st_line_rate(self, currency, balance, amount_currency):
""" Convert the amounts passed as parameters to the statement line currency using the rates provided by the
bank. The computed amounts are the one that could be set on the statement line as a counterpart journal item
to fully paid the provided amounts as parameters.
:param currency: The currency in which is expressed 'amount_currency'.
:param balance: The amount expressed in company currency. Only needed when the currency passed as
parameter is neither the statement line's foreign currency, neither the journal's
currency.
:param amount_currency: The amount expressed in the 'currency' passed as parameter.
:return: A python dictionary containing:
* balance: The amount to consider expressed in company's currency.
* amount_currency: The amount to consider expressed in statement line's foreign currency.
"""
self.ensure_one()
company_amount, company_currency, journal_amount, journal_currency, transaction_amount, foreign_currency \
= self._get_amounts_with_currencies()
rate_journal2foreign_curr = journal_amount and abs(transaction_amount) / abs(journal_amount)
rate_comp2journal_curr = company_amount and abs(journal_amount) / abs(company_amount)
if currency == foreign_currency:
trans_amount_currency = amount_currency
if rate_journal2foreign_curr:
journ_amount_currency = journal_currency.round(trans_amount_currency / rate_journal2foreign_curr)
else:
journ_amount_currency = 0.0
if rate_comp2journal_curr:
new_balance = company_currency.round(journ_amount_currency / rate_comp2journal_curr)
else:
new_balance = 0.0
elif currency == journal_currency:
trans_amount_currency = foreign_currency.round(amount_currency * rate_journal2foreign_curr)
if rate_comp2journal_curr:
new_balance = company_currency.round(amount_currency / rate_comp2journal_curr)
else:
new_balance = 0.0
else:
journ_amount_currency = journal_currency.round(balance * rate_comp2journal_curr)
trans_amount_currency = foreign_currency.round(journ_amount_currency * rate_journal2foreign_curr)
new_balance = balance
return {
'amount_currency': trans_amount_currency,
'balance': new_balance,
}
def _prepare_move_line_default_vals(self, counterpart_account_id=None):
""" Prepare the dictionary to create the default account.move.lines for the current account.bank.statement.line
record.
:return: A list of python dictionary to be passed to the account.move.line's 'create' method.
"""
self.ensure_one()
if not counterpart_account_id:
counterpart_account_id = self.journal_id.suspense_account_id.id
if not counterpart_account_id:
raise UserError(_(
"You can't create a new statement line without a suspense account set on the %s journal.",
self.journal_id.display_name,
))
company_amount, _company_currency, journal_amount, journal_currency, transaction_amount, foreign_currency \
= self._get_amounts_with_currencies()
liquidity_line_vals = {
'name': self.payment_ref,
'move_id': self.move_id.id,
'partner_id': self.partner_id.id,
'account_id': self.journal_id.default_account_id.id,
'currency_id': journal_currency.id,
'amount_currency': journal_amount,
'debit': company_amount > 0 and company_amount or 0.0,
'credit': company_amount < 0 and -company_amount or 0.0,
}
# Create the counterpart line values.
counterpart_line_vals = {
'name': self.payment_ref,
'account_id': counterpart_account_id,
'move_id': self.move_id.id,
'partner_id': self.partner_id.id,
'currency_id': foreign_currency.id,
'amount_currency': -transaction_amount,
'debit': -company_amount if company_amount < 0.0 else 0.0,
'credit': company_amount if company_amount > 0.0 else 0.0,
}
return [liquidity_line_vals, counterpart_line_vals]
def _retrieve_partner(self):
self.ensure_one()
# Retrieve the partner from the statement line.
if self.partner_id:
return self.partner_id
# Retrieve the partner from the bank account.
if self.account_number:
account_number_nums = sanitize_account_number(self.account_number)
if account_number_nums:
domain = [('sanitized_acc_number', 'ilike', account_number_nums)]
for extra_domain in ([('company_id', '=', self.company_id.id)], []):
bank_accounts = self.env['res.partner.bank'].search(extra_domain + domain)
if len(bank_accounts.partner_id) == 1:
return bank_accounts.partner_id
# Retrieve the partner from the partner name.
if self.partner_name:
domain = [
('parent_id', '=', False),
('name', 'ilike', self.partner_name),
]
for extra_domain in ([('company_id', '=', self.company_id.id)], []):
partner = self.env['res.partner'].search(extra_domain + domain, limit=1)
if partner:
return partner
# Retrieve the partner from the 'reconcile models'.
rec_models = self.env['account.reconcile.model'].search([
('rule_type', '!=', 'writeoff_button'),
('company_id', '=', self.company_id.id),
])
for rec_model in rec_models:
partner = rec_model._get_partner_from_mapping(self)
if partner and rec_model._is_applicable_for(self, partner):
return partner
# Retrieve the partner from statement line text values.
st_line_text_values = self._get_st_line_strings_for_matching()
unaccent = get_unaccent_wrapper(self._cr)
sub_queries = []
params = []
for text_value in st_line_text_values:
if not text_value:
continue
# Find a partner having a name contained inside the statement line values.
# Take care a partner could contain some special characters in its name that needs to be escaped.
sub_queries.append(rf'''
{unaccent("%s")} ~* ('^' || (
SELECT STRING_AGG(CONCAT('(?=.*\m', chunk[1], '\M)'), '')
FROM regexp_matches({unaccent('name')}, '\w{{3,}}', 'g') AS chunk
))
''')
params.append(text_value)
if sub_queries:
self.env['res.partner'].flush_model(['company_id', 'name'])
self._cr.execute(
'''
SELECT id
FROM res_partner
WHERE (company_id IS NULL OR company_id = %s)
AND name IS NOT NULL
AND (''' + ') OR ('.join(sub_queries) + ''')
''',
[self.company_id.id] + params,
)
rows = self._cr.fetchall()
if len(rows) == 1:
return self.env['res.partner'].browse(rows[0][0])
return self.env['res.partner']
def _seek_for_lines(self):
""" Helper used to dispatch the journal items between:
- The lines using the liquidity account.
- The lines using the transfer account.
- The lines being not in one of the two previous categories.
:return: (liquidity_lines, suspense_lines, other_lines)
"""
liquidity_lines = self.env['account.move.line']
suspense_lines = self.env['account.move.line']
other_lines = self.env['account.move.line']
for line in self.move_id.line_ids:
if line.account_id == self.journal_id.default_account_id:
liquidity_lines += line
elif line.account_id == self.journal_id.suspense_account_id:
suspense_lines += line
else:
other_lines += line
return liquidity_lines, suspense_lines, other_lines
# SYNCHRONIZATION account.bank.statement.line <-> account.move
# -------------------------------------------------------------------------
def _synchronize_from_moves(self, changed_fields):
""" Update the account.bank.statement.line regarding its related account.move.
Also, check both models are still consistent.
:param changed_fields: A set containing all modified fields on account.move.
"""
if self._context.get('skip_account_move_synchronization'):
return
for st_line in self.with_context(skip_account_move_synchronization=True):
move = st_line.move_id
move_vals_to_write = {}
st_line_vals_to_write = {}
if 'line_ids' in changed_fields:
liquidity_lines, suspense_lines, _other_lines = st_line._seek_for_lines()
company_currency = st_line.journal_id.company_id.currency_id
journal_currency = st_line.journal_id.currency_id if st_line.journal_id.currency_id != company_currency\
else False
if len(liquidity_lines) != 1:
raise UserError(_(
"The journal entry %s reached an invalid state regarding its related statement line.\n"
"To be consistent, the journal entry must always have exactly one journal item involving the "
"bank/cash account."
) % st_line.move_id.display_name)
st_line_vals_to_write.update({
'payment_ref': liquidity_lines.name,
'partner_id': liquidity_lines.partner_id.id,
})
# Update 'amount' according to the liquidity line.
if journal_currency:
st_line_vals_to_write.update({
'amount': liquidity_lines.amount_currency,
})
else:
st_line_vals_to_write.update({
'amount': liquidity_lines.balance,
})
if len(suspense_lines) == 1:
if journal_currency and suspense_lines.currency_id == journal_currency:
# The suspense line is expressed in the journal's currency meaning the foreign currency
# set on the statement line is no longer needed.
st_line_vals_to_write.update({
'amount_currency': 0.0,
'foreign_currency_id': False,
})
elif not journal_currency and suspense_lines.currency_id == company_currency:
# Don't set a specific foreign currency on the statement line.
st_line_vals_to_write.update({
'amount_currency': 0.0,
'foreign_currency_id': False,
})
else:
# Update the statement line regarding the foreign currency of the suspense line.
st_line_vals_to_write.update({
'amount_currency': -suspense_lines.amount_currency,
'foreign_currency_id': suspense_lines.currency_id.id,
})
move_vals_to_write.update({
'partner_id': liquidity_lines.partner_id.id,
'currency_id': (st_line.foreign_currency_id or journal_currency or company_currency).id,
})
move.write(move._cleanup_write_orm_values(move, move_vals_to_write))
st_line.write(move._cleanup_write_orm_values(st_line, st_line_vals_to_write))
def _synchronize_to_moves(self, changed_fields):
""" Update the account.move regarding the modified account.bank.statement.line.
:param changed_fields: A list containing all modified fields on account.bank.statement.line.
"""
if self._context.get('skip_account_move_synchronization'):
return
if not any(field_name in changed_fields for field_name in (
'payment_ref', 'amount', 'amount_currency',
'foreign_currency_id', 'currency_id', 'partner_id',
)):
return
for st_line in self.with_context(skip_account_move_synchronization=True):
liquidity_lines, suspense_lines, other_lines = st_line._seek_for_lines()
journal = st_line.journal_id
company_currency = journal.company_id.currency_id
journal_currency = journal.currency_id if journal.currency_id != company_currency else False
line_vals_list = st_line._prepare_move_line_default_vals()
line_ids_commands = [(1, liquidity_lines.id, line_vals_list[0])]
if suspense_lines:
line_ids_commands.append((1, suspense_lines.id, line_vals_list[1]))
else:
line_ids_commands.append((0, 0, line_vals_list[1]))
for line in other_lines:
line_ids_commands.append((2, line.id))
st_line_vals = {
'currency_id': (st_line.foreign_currency_id or journal_currency or company_currency).id,
'line_ids': line_ids_commands,
}
if st_line.move_id.journal_id != journal:
st_line_vals['journal_id'] = journal.id
if st_line.move_id.partner_id != st_line.partner_id:
st_line_vals['partner_id'] = st_line.partner_id.id
st_line.move_id.write(st_line_vals)
# For optimization purpose, creating the reverse relation of m2o in _inherits saves
# a lot of SQL queries
class AccountMove(models.Model):
_name = "account.move"
_inherit = ['account.move']
statement_line_ids = fields.One2many('account.bank.statement.line', 'move_id', string='Statements')
+1 -1
View File
@@ -892,7 +892,7 @@ class AccountJournal(models.Model):
:return: An account.bank.statement record or an empty recordset.
'''
self.ensure_one()
last_statement_domain = (domain or []) + [('journal_id', '=', self.id)]
last_statement_domain = (domain or []) + [('journal_id', '=', self.id), ('statement_id', '!=', False)]
last_st_line = self.env['account.bank.statement.line'].search(last_statement_domain, order='date desc, id desc', limit=1)
return last_st_line.statement_id
@@ -3,6 +3,7 @@ from datetime import datetime, timedelta
from babel.dates import format_datetime, format_date
from odoo import models, api, _, fields
from odoo.exceptions import UserError
from odoo.osv import expression
from odoo.release import version
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT as DF
@@ -125,7 +126,6 @@ class account_journal(models.Model):
return {'x':short_name,'y': amount, 'name':name}
self.ensure_one()
BankStatement = self.env['account.bank.statement']
data = []
today = datetime.today()
last_month = today + timedelta(days=-30)
@@ -278,11 +278,10 @@ class account_journal(models.Model):
SELECT COUNT(st_line.id)
FROM account_bank_statement_line st_line
JOIN account_move st_line_move ON st_line_move.id = st_line.move_id
JOIN account_bank_statement st ON st_line.statement_id = st.id
WHERE st_line_move.journal_id IN %s
AND st.state = 'posted'
AND NOT st_line.is_reconciled
AND st_line_move.to_check IS NOT TRUE
AND st_line_move.state = 'posted'
''', [tuple(self.ids)])
number_to_reconcile = self.env.cr.fetchone()[0]
@@ -449,27 +448,7 @@ class account_journal(models.Model):
}
def create_cash_statement(self):
ctx = self._context.copy()
ctx.update({'journal_id': self.id, 'default_journal_id': self.id, 'default_journal_type': 'cash'})
open_statements = self.env['account.bank.statement'].search([('journal_id', '=', self.id), ('state', '=', 'open')])
action = {
'name': _('Create cash statement'),
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'account.bank.statement',
'context': ctx,
}
if len(open_statements) == 1:
action.update({
'view_mode': 'form',
'res_id': open_statements.id,
})
elif len(open_statements) > 1:
action.update({
'view_mode': 'tree,form',
'domain': [('id', 'in', open_statements.ids)],
})
return action
raise UserError(_('Please install Accounting for this feature'))
def action_create_vendor_bill(self):
""" This function is called by the "Import" button of Vendor Bills,
+3 -17
View File
@@ -162,13 +162,6 @@ class AccountMove(models.Model):
copy=False,
check_company=True,
)
# used to open the linked bank statement from the edit button in a group by
# view, or via the smart button on journal entries.
statement_id = fields.Many2one(
related='statement_line_id.statement_id',
copy=False,
readonly=True,
)
# === Cash basis feature fields === #
# used to keep track of the tax cash basis reconciliation. This is needed
@@ -614,6 +607,8 @@ class AccountMove(models.Model):
return self.payment_id.journal_id
if self.statement_line_id and self.statement_line_id.journal_id:
return self.statement_line_id.journal_id
if self.statement_line_ids.statement_id.journal_id:
return self.statement_line_ids.statement_id.journal_id[:1]
if self.is_sale_document(include_receipts=True):
journal_types = ['sale']
@@ -2403,7 +2398,7 @@ class AccountMove(models.Model):
# EXTENDS account sequence.mixin
self.ensure_one()
is_payment = self.payment_id or self._context.get('is_payment')
if self.journal_id.type == 'sale':
if self.journal_id.type in ['sale', 'bank', 'cash']:
starting_sequence = "%s/%04d/00000" % (self.journal_id.code, self.date.year)
else:
starting_sequence = "%s/%04d/%02d/0000" % (self.journal_id.code, self.date.year, self.date.month)
@@ -3320,15 +3315,6 @@ class AccountMove(models.Model):
'views': [(False, 'form')],
}
def open_bank_statement_view(self):
return {
'type': 'ir.actions.act_window',
'res_model': 'account.bank.statement',
'view_mode': 'form',
'res_id': self.statement_id.id,
'views': [(False, 'form')],
}
def open_payment_view(self):
return {
'type': 'ir.actions.act_window',
+13 -16
View File
@@ -132,11 +132,10 @@ class AccountPayment(models.Model):
compute='_compute_stat_buttons_from_reconciliation',
help="Statements lines matched to this payment",
)
reconciled_statement_ids = fields.Many2many('account.bank.statement', string="Reconciled Statements",
compute='_compute_stat_buttons_from_reconciliation',
help="Statements matched to this payment")
reconciled_statements_count = fields.Integer(string="# Reconciled Statements",
compute="_compute_stat_buttons_from_reconciliation")
reconciled_statement_lines_count = fields.Integer(
string="# Reconciled Statement Lines",
compute="_compute_stat_buttons_from_reconciliation",
)
# == Display purpose fields ==
payment_method_code = fields.Char(
@@ -563,8 +562,7 @@ class AccountPayment(models.Model):
self.reconciled_bill_ids = False
self.reconciled_bills_count = 0
self.reconciled_statement_line_ids = False
self.reconciled_statement_ids = False
self.reconciled_statements_count = 0
self.reconciled_statement_lines_count = 0
return
self.env['account.move'].flush_model()
@@ -629,7 +627,7 @@ class AccountPayment(models.Model):
WHERE account.id = payment.outstanding_account_id
AND payment.id IN %(payment_ids)s
AND line.id != counterpart_line.id
AND counterpart_line.statement_id IS NOT NULL
AND counterpart_line.statement_line_id IS NOT NULL
GROUP BY payment.id
''', {
'payment_ids': tuple(stored_payments.ids)
@@ -639,8 +637,7 @@ class AccountPayment(models.Model):
for pay in self:
statement_line_ids = query_res.get(pay.id, [])
pay.reconciled_statement_line_ids = [Command.set(statement_line_ids)]
pay.reconciled_statement_ids = [Command.set(pay.reconciled_statement_line_ids.statement_id.ids)]
pay.reconciled_statements_count = len(statement_line_ids)
pay.reconciled_statement_lines_count = len(statement_line_ids)
if len(pay.reconciled_invoice_ids.mapped('move_type')) == 1 and pay.reconciled_invoice_ids[0].move_type == 'out_refund':
pay.reconciled_invoices_type = 'credit_note'
else:
@@ -981,27 +978,27 @@ class AccountPayment(models.Model):
})
return action
def button_open_statements(self):
def button_open_statement_lines(self):
''' Redirect the user to the statement line(s) reconciled to this payment.
:return: An action on account.move.
'''
self.ensure_one()
action = {
'name': _("Matched Statements"),
'name': _("Matched Transactions"),
'type': 'ir.actions.act_window',
'res_model': 'account.bank.statement',
'res_model': 'account.bank.statement.line',
'context': {'create': False},
}
if len(self.reconciled_statement_ids) == 1:
if len(self.reconciled_statement_lines_ids) == 1:
action.update({
'view_mode': 'form',
'res_id': self.reconciled_statement_ids.id,
'res_id': self.reconciled_statement_lines_ids.id,
})
else:
action.update({
'view_mode': 'list,form',
'domain': [('id', 'in', self.reconciled_statement_ids.ids)],
'domain': [('id', 'in', self.reconciled_statement_lines_ids.ids)],
})
return action
+1 -1
View File
@@ -408,7 +408,7 @@ class AccountChartTemplate(models.Model):
the provided company (meaning hence that its chart of accounts cannot
be changed anymore).
"""
model_to_check = ['account.payment', 'account.bank.statement']
model_to_check = ['account.payment', 'account.bank.statement.line']
for model in model_to_check:
if self.env[model].sudo().search([('company_id', '=', company_id.id)], limit=1):
return True
+6 -6
View File
@@ -286,22 +286,22 @@ class ResCompany(models.Model):
:param unreconciled_statement_lines: The statement lines.
:return: A dictionary representing a window action.
"""
statements = unreconciled_statement_lines.statement_id
action = {
'name': _("Unreconciled Statements"),
'name': _("Unreconciled Transactions"),
'type': 'ir.actions.act_window',
'res_model': 'account.bank.statement',
'res_model': 'account.bank.statement.line',
'context': {'create': False},
}
if len(statements) == 1:
if len(unreconciled_statement_lines) == 1:
action.update({
'view_mode': 'form',
'res_id': statements.id,
'res_id': unreconciled_statement_lines.id,
})
else:
action.update({
'view_mode': 'list,form',
'domain': [('id', 'in', statements.ids)],
'domain': [('id', 'in', unreconciled_statement_lines.ids)],
})
return action
@@ -1,11 +1,11 @@
# -*- coding: utf-8 -*-
"""Classes defining the populate factory for Bank Statements and related models."""
from odoo import models
from odoo import models, Command
from odoo.tools import populate
from dateutil.relativedelta import relativedelta
from functools import lru_cache
from collections import defaultdict
import logging
_logger = logging.getLogger(__name__)
@@ -15,28 +15,42 @@ class AccountBankStatement(models.Model):
"""Populate factory part for account.bank.statements."""
_inherit = "account.bank.statement"
_populate_sizes = {
'small': 10,
'medium': 1000,
'large': 20000,
}
_populate_dependencies = ['account.bank.statement.line']
_populate_dependencies = ['account.journal', 'res.company']
def _populate(self, size):
"""
Populate the bank statements with random lines.
:param size:
:return:
"""
rand = populate.Random('account_bank_statement+Populate')
def _populate_factories(self):
company_ids = self.env['res.company'].search([
('chart_template_id', '!=', False),
('id', 'in', self.env.registry.populated_models['res.company']),
])
journal_ids = self.env['account.journal'].search([
('company_id', 'in', company_ids.ids),
('type', 'in', ('cash', 'bank')),
]).ids
return [
('journal_id', populate.iterate(journal_ids)),
('name', populate.constant('statement_{counter}')),
('date', populate.randdatetime(relative_before=relativedelta(years=-4))),
]
read_group_res = self.env['account.bank.statement.line'].read_group(
[('statement_id', '=', False)],
['ids:array_agg(id)'],
['journal_id'],
)
bank_statement_vals_list = []
for res in read_group_res:
available_ids = res['ids']
nb_ids = len(available_ids)
while nb_ids > 0:
batch_size = min(rand.randint(1, 19), nb_ids)
nb_ids -= batch_size
# 50% to create a statement.
statement_needed = bool(rand.randint(0, 1))
if not statement_needed:
continue
bank_statement_vals_list.append({
'name': f"statement_{len(bank_statement_vals_list) + 1}",
'journal_id': res['journal_id'][0],
'line_ids': [Command.set(res['ids'])],
})
return self.env['account.bank.statement'].create(bank_statement_vals_list)
class AccountBankStatementLine(models.Model):
@@ -50,7 +64,7 @@ class AccountBankStatementLine(models.Model):
'large': 200000,
}
_populate_dependencies = ['account.bank.statement', 'res.partner']
_populate_dependencies = ['account.journal', 'res.company', 'res.partner']
def _populate_factories(self):
@lru_cache()
@@ -74,61 +88,48 @@ class AccountBankStatementLine(models.Model):
:param values (dict): the values already selected for the record.
:return (int): an id of a partner accessible by the company of the statement.
"""
company_id = self.env['account.bank.statement'].browse(values['statement_id']).company_id.id
company_id = self.env['account.journal'].browse(values['journal_id']).company_id.id
partner = search_partner_ids(company_id)
return random.choices(partner + [False], [1/len(partner)] * len(partner) + [1])[0]
def get_date(random, values, **kwargs):
"""Get a date in the past.
This date can but up to 31 days before the statement linked to this line.
:param random: seeded random number generator.
:param values (dict): the values already selected for the record.
:return (datetime.date): a date up to 31 days before the date of the statement.
def get_amount_currency(random, values, **kwargs):
"""
statement_date = self.env['account.bank.statement'].browse(values['statement_id']).date
return statement_date + relativedelta(days=random.randint(-31, 0))
Get a random amount currency between one tenth of amount and 10 times amount with the same sign
if foreign_currency_id is set
def get_amount(random, **kwargs):
"""Get a random amount between -1000 and 1000.
It is impossible to get a null amount. Because it would not be a valid statement line.
:param random: seeded random number generator.
:return (float): a number between -1000 and 1000.
:return (float): a number between amount / 10 and amount * 10.
"""
return random.uniform(-1000, 1000) or 1
return random.uniform(0.1 * values['amount'], 10 * values['amount']) if values['foreign_currency_id'] else 0
def get_currency(random, values, **kwargs):
"""Get a randome currency.
"""Get a random currency.
The currency has to be empty if it is the same as the currency of the statement's journal's.
The currency has to be empty if it is the same as the currency of the line's journal's.
:param random: seeded random number generator.
:param values (dict): the values already selected for the record.
:return (int, bool): the id of an active currency or False if it is the same currency as
the statement's journal's currency.
the lines's journal's currency.
"""
journal = self.env['account.bank.statement'].browse(values['statement_id']).journal_id
journal = self.env['account.journal'].browse(values['journal_id'])
currency = random.choice(self.env['res.currency'].search([('active', '=', True)]).ids)
return currency if currency != (journal.currency_id or journal.company_id.currency_id).id else False
# Because we are accessing related fields of bank statements, a prefetch can improve the performances.
self = self.with_prefetch(self.env.registry.populated_models['account.bank.statement'])
return [
('statement_id', populate.randomize(self.env.registry.populated_models['account.bank.statement'])),
('partner_id', populate.compute(get_partner)),
('payment_ref', populate.constant('statement_{values[statement_id]}_{counter}')),
('date', populate.compute(get_date)),
('amount', populate.compute(get_amount)),
('currency_id', populate.compute(get_currency)),
]
company_ids = self.env['res.company'].search([
('chart_template_id', '!=', False),
('id', 'in', self.env.registry.populated_models['res.company']),
])
def _populate(self, size):
records = super()._populate(size)
_logger.info('Posting Bank Statements')
statements = records.statement_id.sorted(lambda r: (r.date, r.name, r.id))
previous = defaultdict(int)
for statement in statements:
statement.balance_start = previous[statement.journal_id]
previous[statement.journal_id] = statement.balance_end_real = statement.balance_start + statement.total_entry_encoding
statements.button_post()
return records
journal_ids = self.env['account.journal'].search([
('company_id', 'in', company_ids.ids),
('type', 'in', ('cash', 'bank')),
]).ids
return [
('journal_id', populate.iterate(journal_ids)),
('partner_id', populate.compute(get_partner)),
('date', populate.randdatetime(relative_before=relativedelta(years=-4))),
('payment_ref', populate.constant('transaction_{values[date]}_{counter}')),
('amount', populate.randint(-1000, 1000)),
('foreign_currency_id', populate.compute(get_currency)),
('amount_currency', populate.compute(get_amount_currency)),
]
@@ -35,8 +35,6 @@ access_account_fiscal_position_account,account.fiscal.position all,model_account
access_product_product_account_user,product.product.account.user,product.model_product_product,group_account_readonly,1,0,0,0
access_product_product_account_manager,product.product.account.manager,product.model_product_product,account.group_account_manager,1,1,1,1
access_account_cashbox,account.bank.statement.cashbox,model_account_bank_statement_cashbox,account.group_account_user,1,1,1,1
access_account_cashbox_line,account.bank.statement.cashbox.line,model_account_cashbox_line,account.group_account_user,1,1,1,1
access_account_bank_statement_group_readonly,account.bank.statement.group.invoice,model_account_bank_statement,account.group_account_readonly,1,0,0,0
access_account_bank_statement_group_invoice,account.bank.statement.group.invoice,model_account_bank_statement,account.group_account_invoice,1,0,0,0
access_account_bank_statement_line_group_readonly,account.bank.statement.line.group.invoice,model_account_bank_statement_line,account.group_account_readonly,1,0,0,0
@@ -122,12 +120,10 @@ access_account_payment_readonly,account.payment,model_account_payment,account.gr
access_account_payment,account.payment,model_account_payment,account.group_account_invoice,1,1,1,1
access_account_payment_register,access.account.payment.register,model_account_payment_register,account.group_account_invoice,1,1,1,0
access_account_bank_statement_closebalance,access.account.bank.statement.closebalance,model_account_bank_statement_closebalance,account.group_account_user,1,1,1,0
access_account_automatic_entry_wizard,access.account.automatic.entry.wizard,model_account_automatic_entry_wizard,account.group_account_user,1,1,1,0
access_account_unreconcile,access.account.unreconcile,model_account_unreconcile,account.group_account_user,1,1,1,0
access_account_resequence,access.account.resequence.wizard,model_account_resequence_wizard,account.group_account_manager,1,1,1,0
access_validate_account_move,access.validate.account.move,model_validate_account_move,account.group_account_invoice,1,1,1,0
access_cash_box_out,access.cash.box.out,model_cash_box_out,account.group_account_user,1,1,1,0
access_account_move_reversal,access.account.move.reversal,model_account_move_reversal,account.group_account_invoice,1,1,1,0
access_account_financial_year_op,access.account.financial.year.op,model_account_financial_year_op,account.group_account_manager,1,1,1,0
access_account_setup_bank_manual_config,access.account.setup.bank.manual.config,model_account_setup_bank_manual_config,account.group_account_manager,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
35 access_account_bank_statement_line_group_readonly access_account_bank_statement account.bank.statement.line.group.invoice account.bank.statement model_account_bank_statement_line model_account_bank_statement account.group_account_readonly account.group_account_user 1 0 1 0 1 0 1
36 access_account_bank_statement_line_group_invoice access_account_bank_statement_line account.bank.statement.line.group.invoice account.bank.statement.line model_account_bank_statement_line account.group_account_invoice account.group_account_user 1 0 1 0 1 0 1
37 access_account_bank_statement access_account_move_line_manager account.bank.statement account.move.line manager model_account_bank_statement model_account_move_line account.group_account_user account.group_account_manager 1 1 0 1 0 1 0
access_account_bank_statement_line account.bank.statement.line model_account_bank_statement_line account.group_account_user 1 1 1 1
access_account_move_line_manager account.move.line manager model_account_move_line account.group_account_manager 1 0 0 0
38 access_account_move_manager account.move manager model_account_move account.group_account_manager 1 0 0 0
39 access_account_move_readonly account.move readonly model_account_move account.group_account_readonly 1 0 0 0
40 access_account_move_uinvoice account.move model_account_move account.group_account_invoice 1 1 1 1
120 access_account_accrued_orders_wizard access_account_report_ac_user account.account.accrued.orders.wizard account.report.ac.user account.model_account_accrued_orders_wizard model_account_report group_account_user account.group_account_manager 1 1 1 0 1
121 access_account_report_readonly access_account_report_line_readonly account.report.readonly account.report.line.readonly model_account_report model_account_report_line account.group_account_readonly 1 0 0 0
122 access_account_report_ac_user access_account_report_line_ac_user account.report.ac.user account.report.line.ac.user model_account_report model_account_report_line account.group_account_manager 1 1 1 1
access_account_report_line_readonly account.report.line.readonly model_account_report_line account.group_account_readonly 1 0 0 0
123 access_account_report_line_ac_user access_account_report_expression_readonly account.report.line.ac.user account.report.expression.readonly model_account_report_line model_account_report_expression account.group_account_manager account.group_account_readonly 1 1 0 1 0 1 0
124 access_account_report_expression_readonly access_account_report_expression_ac_user account.report.expression.readonly account.report.expression.ac.user model_account_report_expression account.group_account_readonly account.group_account_manager 1 0 1 0 1 0 1
125 access_account_report_expression_ac_user access_account_report_column_readonly account.report.expression.ac.user account.report.column.readonly model_account_report_expression model_account_report_column account.group_account_manager account.group_account_readonly 1 1 0 1 0 1 0
126 access_account_report_column_readonly access_account_report_column_ac_user account.report.column.readonly account.report.column.ac.user model_account_report_column account.group_account_readonly account.group_account_manager 1 0 1 0 1 0 1
access_account_report_column_ac_user account.report.column.ac.user model_account_report_column account.group_account_manager 1 1 1 1
127 access_account_report_external_value_readonly account.report.external.value.readonly model_account_report_external_value account.group_account_readonly 1 0 0 0
128 access_account_report_external_value_ac_user account.report.external.value.ac.user model_account_report_external_value account.group_account_manager 1 1 1 1
129
File diff suppressed because it is too large Load Diff
@@ -212,14 +212,12 @@ class TestAccountMove(AccountTestInvoicingCommon):
self.test_move.action_post()
# Create a bank statement to get a balance in the suspense account.
statement = self.env['account.bank.statement'].create({
self.env['account.bank.statement.line'].create({
'journal_id': self.company_data['default_journal_bank'].id,
'date': '2016-01-01',
'line_ids': [
(0, 0, {'payment_ref': 'test', 'amount': 10.0})
],
'payment_ref': 'test',
'amount': 10.0,
})
statement.button_post()
# You can't lock the fiscal year if there is some unreconciled statement.
with self.assertRaises(RedirectWarning), self.cr.savepoint():
+7 -14
View File
@@ -747,19 +747,12 @@ class TestAccountPayment(AccountTestInvoicingCommon):
'is_matched': False,
}])
statement = self.env['account.bank.statement'].create({
'name': 'test_statement',
statement_line = self.env['account.bank.statement.line'].create({
'payment_ref': '50 to pay',
'journal_id': self.company_data['default_journal_bank'].id,
'line_ids': [
(0, 0, {
'payment_ref': '50 to pay',
'partner_id': self.partner_a.id,
'amount': 50.0,
}),
],
'partner_id': self.partner_a.id,
'amount': 50.0,
})
statement.button_post()
statement_line = statement.line_ids
# Reconcile without the bank reconciliation widget since the widget is in enterprise.
_st_liquidity_lines, st_suspense_lines, _st_other_lines = statement_line\
@@ -780,17 +773,17 @@ class TestAccountPayment(AccountTestInvoicingCommon):
payment = AccountPayment.create({
'journal_id': self.company_data['default_journal_bank'].id,
})
self.assertRegex(payment.name, r'BNK1/\d{4}/\d{2}/0001')
self.assertRegex(payment.name, r'BNK1/\d{4}/00001')
with Form(AccountPayment.with_context(default_move_journal_types=('bank', 'cash'))) as payment_form:
self.assertEqual(payment_form._values['name'], '/')
payment_form.journal_id = self.company_data['default_journal_cash']
self.assertRegex(payment_form._values['name'], r'CSH1/\d{4}/\d{2}/0001')
self.assertRegex(payment_form._values['name'], r'CSH1/\d{4}/00001')
payment_form.journal_id = self.company_data['default_journal_bank']
payment = payment_form.save()
self.assertEqual(payment.name, '/')
payment.action_post()
self.assertRegex(payment.name, r'BNK1/\d{4}/\d{2}/0002')
self.assertRegex(payment.name, r'BNK1/\d{4}/00002')
def test_payment_without_default_company_account(self):
""" The purpose of this test is to check the specific behavior when duplicating an inbound payment, then change
@@ -92,74 +92,61 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
####################
# TODO : account_number, partner_name, transaction_type, narration
invoice_number = cls.invoice_line_1.move_id.name
cls.bank_st, cls.bank_st_2, cls.cash_st = cls.env['account.bank.statement'].create([
cls.bank_line_1, cls.bank_line_2,\
cls.bank_line_3, cls.bank_line_4,\
cls.bank_line_5, cls.cash_line_1 = cls.env['account.bank.statement.line'].create([
{
'name': 'test bank journal',
'journal_id': cls.bank_journal.id,
'line_ids': [
(0, 0, {
'date': '2020-01-01',
'payment_ref': 'invoice %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,
'sequence': 2,
}),
],
}, {
'name': 'second test bank journal',
'date': '2020-01-01',
'payment_ref': 'invoice %s-%s' % tuple(invoice_number.split('/')[1:]),
'partner_id': cls.partner_1.id,
'amount': 100,
'sequence': 1,
},
{
'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,
'amount': 600,
'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,
'amount': 600,
'sequence': 2,
}),
],
}, {
'name': 'test cash journal',
'date': '2020-01-01',
'payment_ref': 'xxxxx',
'partner_id': cls.partner_1.id,
'amount': 600,
'sequence': 2,
},
{
'journal_id': cls.bank_journal.id,
'date': '2020-01-01',
'payment_ref': 'nawak',
'narration': 'Communication: RF12 3456',
'partner_id': cls.partner_3.id,
'amount': 600,
'sequence': 1,
},
{
'journal_id': cls.bank_journal.id,
'date': '2020-01-01',
'payment_ref': 'RF12 3456',
'partner_id': cls.partner_3.id,
'amount': 600,
'sequence': 2,
},
{
'journal_id': cls.bank_journal.id,
'date': '2020-01-01',
'payment_ref': 'baaaaah',
'ref': 'RF12 3456',
'partner_id': cls.partner_3.id,
'amount': 600,
'sequence': 2,
},
{
'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,
'sequence': 1,
}),
],
}
'date': '2020-01-01',
'payment_ref': 'yyyyy',
'partner_id': cls.partner_2.id,
'amount': -1000,
'sequence': 1,
},
])
cls.bank_line_1, cls.bank_line_2 = cls.bank_st.line_ids
cls.bank_line_3, cls.bank_line_4, cls.bank_line_5 = cls.bank_st_2.line_ids
cls.cash_line_1 = cls.cash_st.line_ids
cls._post_statements(cls)
@classmethod
def _create_invoice_line(cls, amount, partner, move_type, currency=None, pay_reference=None, ref=None, name=None, inv_date='2019-09-01'):
''' Create an invoice on the fly.'''
@@ -185,20 +172,15 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
@classmethod
def _create_st_line(cls, amount=1000.0, date='2019-01-01', payment_ref='turlututu', **kwargs):
st = cls.env['account.bank.statement'].create({
'name': 'test_allow_payment_tolerance_1',
st_line = cls.env['account.bank.statement.line'].create({
'journal_id': kwargs.get('journal_id', cls.bank_journal.id),
'line_ids': [Command.create({
'amount': amount,
'date': date,
'payment_ref': payment_ref,
'partner_id': cls.partner_a.id,
**kwargs,
})],
'amount': amount,
'date': date,
'payment_ref': payment_ref,
'partner_id': cls.partner_a.id,
**kwargs,
})
st.balance_end_real = st.balance_end
st.button_post()
return st.line_ids
return st_line
@classmethod
def _create_reconcile_model(cls, **kwargs):
@@ -224,12 +206,6 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
],
})
def _post_statements(self):
self.bank_st.balance_end_real = self.bank_st.balance_end
self.bank_st_2.balance_end_real = self.bank_st_2.balance_end
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_list):
for statement_line, expected_values in expected_values_list.items():
@@ -321,7 +297,7 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
})
def test_matching_fields_match_journal_ids(self):
self.rule_1.match_journal_ids |= self.cash_st.journal_id
self.rule_1.match_journal_ids |= self.cash_line_1.journal_id
self._check_statement_matching(self.rule_1, {
self.bank_line_1: {},
self.bank_line_2: {},
@@ -938,24 +914,15 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
'past_months_limit': False,
})
statement = self.env['account.bank.statement'].create({
'name': 'test_match_multi_currencies',
statement_line = self.env['account.bank.statement.line'].create({
'journal_id': journal.id,
'line_ids': [
(0, 0, {
'journal_id': journal.id,
'date': '2016-01-01',
'payment_ref': 'line',
'partner_id': partner.id,
'foreign_currency_id': self.currency_data_2['currency'].id,
'amount': 300.0, # Rate is 3 GOL = 1 USD in 2016.
'amount_currency': 900.0, # Rate is 10 DAR = 1 USD in 2016 but the rate used by the bank is 9:1.
}),
],
'date': '2016-01-01',
'payment_ref': 'line',
'partner_id': partner.id,
'foreign_currency_id': self.currency_data_2['currency'].id,
'amount': 300.0, # Rate is 3 GOL = 1 USD in 2016.
'amount_currency': 900.0, # Rate is 10 DAR = 1 USD in 2016 but the rate used by the bank is 9:1.
})
statement_line = statement.line_ids
statement.button_post()
move = self.env['account.move'].create({
'move_type': 'entry',
@@ -2,83 +2,14 @@
<odoo>
<data>
<record id="view_account_bnk_stmt_cashbox" model="ir.ui.view">
<field name="name">account.bnk_stmt_cashbox.form</field>
<field name="model">account.bank.statement.cashbox</field>
<field name="arch" type="xml">
<form>
<sheet>
<field name="start_bank_stmt_ids" invisible="1"/>
<field name="end_bank_stmt_ids" invisible="1"/>
<field name="currency_id" invisible="1"/>
<field name="cashbox_lines_ids" nolabel="1" context="{'default_currency_id': currency_id}">
<tree editable="bottom">
<field name="currency_id" invisible="1"/>
<field name="number"/>
<field name="coin_value" widget="monetary" options="{'currency_field': 'currency_id'}"/>
<field name="subtotal" widget="monetary" options="{'currency_field': 'currency_id'}"/>
</tree>
</field>
<div>
<group>
<group class="oe_subtotal_footer oe_right" cols="6">
<field name="currency_id" invisible="1" />
<div class="o_td_label"></div>
<field name="total" widget="monetary" options="{'currency_field': 'currency_id'}"/>
</group>
</group>
</div>
</sheet>
</form>
</field>
</record>
<record id="view_account_bnk_stmt_cashbox_footer" model="ir.ui.view">
<field name="name">account.bnk_stmt_cashbox.form</field>
<field name="model">account.bank.statement.cashbox</field>
<field name="priority">1000</field>
<field name="mode">primary</field>
<field name="inherit_id" ref="view_account_bnk_stmt_cashbox"/>
<field name="arch" type="xml">
<xpath expr="//sheet" position="after">
<footer>
<button string="Confirm" class="btn-primary" special="save" data-hotkey="v"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z"/>
</footer>
</xpath>
</field>
</record>
<record id="view_account_bnk_stmt_check" model="ir.ui.view">
<field name="name">account.bnk_stmt_check.form</field>
<field name="model">account.bank.statement.closebalance</field>
<field name="arch" type="xml">
<form>
<div>
<p>The closing balance is different than the computed one!</p>
<p>Confirming this will create automatically a journal entry with the difference in the profit/loss account set on the cash journal.</p>
<footer>
<button string="Confirm" name="validate" type="object" class="btn-primary" data-hotkey="q"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z"/>
</footer>
</div>
</form>
</field>
</record>
<record id="action_view_account_bnk_stmt_check" model="ir.actions.act_window">
<field name="name">Check Closing Balance</field>
<field name="res_model">account.bank.statement.closebalance</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="view_account_bnk_stmt_check"/>
<field name="target">new</field>
</record>
<record id="view_bank_statement_tree" model="ir.ui.view">
<field name="name">account.bank.statement.tree</field>
<field name="model">account.bank.statement</field>
<field name="arch" type="xml">
<tree decoration-danger="not is_difference_zero" decoration-info="state=='open' and is_difference_zero" string="Statements">
<tree decoration-danger="not is_complete or not is_valid"
create="false"
string="Statements">
<field name="name"/>
<field name="date"/>
<field name="journal_id"/>
@@ -87,8 +18,8 @@
<field name="balance_end_real"/>
<field name="balance_end" invisible="1"/>
<field name="currency_id" invisible="1"/>
<field name="state"/>
<field name="is_difference_zero" invisible="1"/>
<field name="is_complete" invisible="1"/>
<field name="is_valid" invisible="1"/>
</tree>
</field>
</record>
@@ -100,154 +31,26 @@
<search string="Search Bank Statements">
<field name="name" string="Bank Statement"/>
<field name="date"/>
<filter string="New" name="draft" domain="[('state','=','open')]"/>
<filter string="Processing" name="processing" domain="[('state','=','posted')]"/>
<filter string="Validated" name="confirmed" domain="[('state','=','confirm')]"/>
<filter string="Empty" name="empty" domain="[('line_ids','=',[])]"/>
<filter name="invalid" string="Invalid"
domain="['|', ('is_valid', '=', False),('is_complete', '=', False)]"/>
<separator/>
<filter name="filter_date" date="date"/>
<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'}"/>
<filter string="Date" name="date" context="{'group_by': 'date'}"/>
</group>
</search>
</field>
</record>
<record id="view_bank_statement_form" model="ir.ui.view">
<field name="name">account.bank.statement.form</field>
<field name="model">account.bank.statement</field>
<field name="priority">1</field>
<field name="arch" type="xml">
<form string="Bank Statement">
<header>
<button string="Post" states="open" class="oe_highlight"
name="button_post" type="object"/>
<button string="Validate" class="oe_highlight"
name="button_validate_or_action" type="object"
attrs="{'invisible':['|', '|', ('all_lines_reconciled', '=', False), ('line_ids','=',[]), ('state', '!=', 'posted')]}"/>
<button string="Reset to New"
name="button_reopen" type="object"
attrs="{'invisible': [('state', '!=', 'posted')]}"/>
<button string="Reset to Processing"
name="button_reprocess" type="object"
attrs="{'invisible': [('state', '!=', 'confirm')]}"/>
<field name="state" widget="statusbar" statusbar_visible="open,posted,confirm"/>
</header>
<div class="alert alert-warning" role="alert" attrs="{'invisible': ['|', ('previous_statement_id', '=', False), ('is_valid_balance_start', '=', True)]}">
The starting balance <b>doesn't match</b> with the ending balance of the previous bank statement.
</div>
<sheet>
<!-- Invisible fields -->
<field name="all_lines_reconciled" invisible="1" />
<field name="move_line_count" invisible="1"/>
<field name="country_code" invisible="1"/>
<div class="oe_button_box" name="button_box">
<button class="oe_stat_button" name="button_journal_entries"
string="Journal Entries" type="object"
attrs="{'invisible':[('move_line_count','=', 0)]}" icon="fa-bars"/>
</div>
<div class="oe_title oe_inline">
<label for="name"/>
<h1><field name="name" placeholder="e.g. BNK/2021/0001"/></h1>
</div>
<group>
<group>
<field name="previous_statement_id" invisible="1"/>
<field name="is_valid_balance_start" invisible="1"/>
<field name="journal_type" invisible="1"/>
<field name="cashbox_start_id" invisible="1"/>
<field name="cashbox_end_id" invisible="1"/>
<field name="journal_id" domain="[('type', '=', journal_type)]" attrs="{'readonly': [('move_line_count','!=', 0)]}" options="{'no_open': True, 'no_create': True}"/>
<field name="date"
attrs="{'readonly': [('state', '!=', 'open')]}"
options="{'datepicker': {'warn_future': true}}"/>
<field name='company_id' options="{'no_create': True}" groups="base.group_multi_company" />
<field name="currency_id" invisible="1"/>
</group><group>
<label for="balance_start"/>
<div>
<field name="balance_start" class="oe_inline" attrs="{'readonly': [('state', '=', 'confirm')]}"/>
<button name="open_cashbox_id" attrs="{'invisible': ['|',('state','!=','open'),('journal_type','!=','cash')]}" string="&#8594; Count" type="object" class="oe_edit_only oe_link oe_inline" context="{'balance':'start'}"/>
</div>
<label for="balance_end_real"/>
<div>
<field name="balance_end_real" class="oe_inline" attrs="{'readonly': [('state', '=', 'confirm')]}"/>
<button name="open_cashbox_id" attrs="{'invisible': ['|',('state','!=','open'),('journal_type','!=','cash')]}" string="&#8594; Count" type="object" class="oe_edit_only oe_link oe_inline" context="{'balance':'close'}"/>
</div>
</group>
</group>
<notebook>
<page string="Transactions" name="statement_line_ids">
<field name="line_ids"
attrs="{'readonly': [('state', '!=', 'open')]}"
context="{'default_date': date, 'default_journal_id': journal_id}">
<tree string="Statement lines"
editable="bottom"
decoration-muted="is_reconciled"
limit="500">
<!-- Invisible fields -->
<field name="company_id" invisible="1"/>
<field name="state" invisible="1"/>
<field name="is_reconciled" invisible="1"/>
<field name="currency_id" invisible="1"/>
<field name="partner_bank_id" invisible="1"/>
<field name="country_code" invisible="1"/>
<!-- Visible fields -->
<field name="sequence" widget="handle"/>
<field name="date"
attrs="{'readonly': [('parent.state', '!=', 'open')]}"/>
<field name="payment_ref"/>
<field name="partner_id"
attrs="{'readonly': [('parent.state', '!=', 'open')]}"
domain="['|', ('parent_id','=', False), ('is_company','=',True)]"/>
<field name="ref" optional="hidden"/>
<field name="narration" string="Notes" optional="hidden"/>
<field name="transaction_type" optional="hidden"/>
<field name="amount_currency" optional="hidden" groups="base.group_multi_currency"
attrs="{'readonly': [('parent.state', '!=', 'open')]}"/>
<field name="foreign_currency_id" optional="hidden" groups="base.group_multi_currency"
attrs="{'readonly': [('parent.state', '!=', 'open')]}"/>
<field name="amount"
attrs="{'readonly': [('parent.state', '!=', 'open')]}"/>
<field name="account_number" optional="hidden"/>
<!-- Buttons -->
<button name="button_undo_reconciliation" type="object"
attrs="{'invisible': [('is_reconciled', '=', False)], 'column_invisible': [('parent.state', '!=', 'posted')]}"
string="Revert reconciliation" icon="fa-undo"/>
</tree>
</field>
</page>
</notebook>
<group class="oe_subtotal_footer oe_right" colspan="2" name="sale_total">
<div class="oe_subtotal_footer_separator oe_inline">
<label for="balance_end" />
</div>
<field name="balance_end" nolabel="1" class="oe_subtotal_footer_separator"/>
</group>
<div class="clearfix"/>
</sheet>
<div class="o_attachment_preview"/>
<div class="oe_chatter">
<field name="message_follower_ids"/>
<field name="message_ids"/>
</div>
</form>
</field>
</record>
<record id="action_bank_statement_tree" model="ir.actions.act_window">
<field name="name">Bank Statements</field>
<field name="res_model">account.bank.statement</field>
<field name="view_mode">tree,form,pivot,graph</field>
<field name="domain">[('journal_id.type', '=', 'bank')]</field>
<field name="view_mode">tree,pivot,graph</field>
<field name="domain">['|', ('journal_id', '=', False), ('journal_id.type', '=', 'bank')]</field>
<field name="context">{'journal_type':'bank'}</field>
<field name="search_view_id" ref="view_bank_statement_search"/>
<field name="help" type="html">
@@ -269,113 +72,6 @@
<field name="view_id" ref="view_bank_statement_tree"/>
<field name="act_window_id" ref="action_bank_statement_tree"/>
</record>
<record model="ir.actions.act_window.view" id="action_bank_statement_form_bank">
<field name="sequence" eval="2"/>
<field name="view_mode">form</field>
<field name="view_id" ref="view_bank_statement_form"/>
<field name="act_window_id" ref="action_bank_statement_tree"/>
</record>
<record id="view_bank_statement_line_search" model="ir.ui.view">
<field name="name">account.bank.statement.line.search</field>
<field name="model">account.bank.statement.line</field>
<field name="arch" type="xml">
<search string="Search Bank Statements Line">
<field name="payment_ref"/>
<field name="statement_id"/>
<field name="partner_id"/>
<field name="date"/>
<field name="journal_id" domain="[('type', 'in', ('bank', 'cash'))]" />
<field name="narration" string="Notes"/>
<field name="transaction_type"/>
<field name="move_id"/>
<field name="amount"/>
<filter name="statement" string="Statement" context="{'group_by':'statement_id'}"/>
</search>
</field>
</record>
<record id="view_bank_statement_line_form" model="ir.ui.view">
<field name="name">bank.statement.line.form</field>
<field name="model">account.bank.statement.line</field>
<field name="priority">8</field>
<field name="arch" type="xml">
<form string="Statement Line" create="false">
<sheet>
<group col="4">
<!-- Invisible fields -->
<field name="state" invisible="1"/>
<field name="company_id" invisible="1"/>
<!-- Displayed fields -->
<field name="statement_id" attrs="{'readonly': [('state', '!=', 'open')]}"/>
<field name="date" attrs="{'readonly': [('state', '!=', 'open')]}"/>
<field name="payment_ref" attrs="{'readonly': [('state', '!=', 'open')]}"/>
<field name="ref" attrs="{'readonly': [('state', '!=', 'open')]}"/>
<field name="partner_id" attrs="{'readonly': [('state', '!=', 'open')]}"/>
<field name="amount" attrs="{'readonly': [('state', '!=', 'open')]}"/>
<field name="sequence" attrs="{'readonly': [('state', '!=', 'open')]}"/>
<field name="narration" string="Notes" attrs="{'readonly': [('state', '!=', 'open')]}"/>
<field name="transaction_type" attrs="{'readonly': [('state', '!=', 'open')]}"/>
<field name="move_id" attrs="{'readonly': [('state', '!=', 'open')]}"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_bank_statement_line_tree" model="ir.ui.view">
<field name="name">account.bank.statement.line.tree</field>
<field name="model">account.bank.statement.line</field>
<field name="priority">8</field>
<field name="arch" type="xml">
<tree string="Statement lines" create="false" default_order="date desc, statement_id desc, sequence desc, id desc">
<!-- Invisible fields -->
<field name="state" invisible="1"/>
<field name="sequence" readonly="1" invisible="1"/>
<field name="company_id" invisible="1"/>
<field name="journal_id" invisible="1" />
<field name="is_reconciled" invisible="1"/>
<field name="partner_bank_id" invisible="1"/>
<field name="currency_id" invisible="1"/>
<!-- Displayed fields -->
<field name="statement_id" optional="hide"/>
<field name="move_id"/>
<field name="date"/>
<field name="payment_ref"/>
<field name="partner_id"/>
<field name="ref" groups="base.group_no_one" optional="hidden"/>
<field name="narration" string="Notes" optional="hidden"/>
<field name="transaction_type" optional="hidden"/>
<field name="amount_currency" optional="hidden" groups="base.group_multi_currency"/>
<field name="foreign_currency_id" optional="hidden" groups="base.group_multi_currency"/>
<field name="amount"/>
<field name="account_number" optional="hidden"/>
<!-- Buttons -->
<button name="button_undo_reconciliation" type="object"
attrs="{'invisible': [('is_reconciled', '=', False)]}"
string="Revert reconciliation" icon="fa-undo"/>
</tree>
</field>
</record>
<record id="action_bank_statement_line" model="ir.actions.act_window">
<field name="name">Bank Statement Lines</field>
<field name="res_model">account.bank.statement.line</field>
<field name="view_mode">tree,form</field>
<field name="domain">[]</field>
<field name="context">{}</field>
<field name="search_view_id" ref="view_bank_statement_line_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_empty_folder">
A bank statement line is a financial transaction on a bank account
</p><p>
Odoo allows you to reconcile a statement line directly with
the related sale or purchase invoice(s).
</p>
</field>
</record>
<record id="account_bank_statement_pivot" model="ir.ui.view">
<field name="name">account.bank.statement.pivot</field>
@@ -405,10 +101,10 @@
<field name="name">Cash Registers</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">account.bank.statement</field>
<field name="view_mode">tree,form,pivot,graph</field>
<field name="view_mode">tree,pivot,graph</field>
<field name="view_id" ref="view_bank_statement_tree"/>
<field name="search_view_id" ref="view_bank_statement_search"/>
<field name="domain">[('journal_id.type', '=', 'cash')]</field>
<field name="domain">['|', ('journal_id', '=', False), ('journal_id.type', '=', 'cash')]</field>
<field name="context">{'journal_type':'cash'}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
@@ -416,9 +112,7 @@
</p><p>
A Cash Register allows you to manage cash entries in your cash
journals. This feature provides an easy way to follow up cash
payments on a daily basis. You can enter the coins that are in
your cash box, and then post entries when money comes in or
goes out of the cash box.
payments on a daily basis.
</p>
</field>
</record>
@@ -90,7 +90,7 @@
<!-- For bank and cash -->
<div t-if="journal_type == 'bank' || journal_type == 'cash'" class="row">
<div class="col-4 o_kanban_card_manage_section o_kanban_manage_view">
<div class="o_kanban_card_manage_title">
<div id="card_action_view_menus" class="o_kanban_card_manage_title">
<span role="separator">View</span>
</div>
<div id="action_card_statements">
@@ -115,15 +115,7 @@
<div class="o_kanban_card_manage_title">
<span role="separator">New</span>
</div>
<div name="bank_cash_commands">
<t t-if="journal_type == 'bank'">
<a role="menuitem" type="object" name="create_bank_statement">Statement</a>
</t>
<t t-if="journal_type == 'cash'">
<a role="menuitem" type="object" name="create_cash_statement">Statement</a>
</t>
</div>
<div>
<div name="bank_customer_payment">
<a role="menuitem" type="object" name="create_customer_payment">Cust. Payment</a>
</div>
<div>
@@ -284,11 +276,7 @@
<a t-if="dashboard.number_to_reconcile > 0" name="action_configure_bank_journal" type="object" class="oe_inline" groups="account.group_account_invoice">Connect</a>
<button t-if="dashboard.number_to_reconcile == 0" name="action_configure_bank_journal" type="object" class="btn btn-primary" groups="account.group_account_invoice">Connect</button>
</t>
<div name="bank_journal_cta" class="mt-3 mt-sm-0">
<div name="bank_statement_create_button" groups="account.group_account_invoice">
<a type="object" name="create_bank_statement" class="oe_inline">Create</a><span class="d-inline-flex" name="button_import_placeholder"/> Statements
</div>
</div>
<div name="bank_journal_cta" class="mt-3 mt-sm-0"/>
</t>
<t t-if="journal_type == 'cash'">
<a t-if="dashboard.number_to_reconcile > 0" type="object" name="create_cash_statement" class="oe_inline" groups="account.group_account_invoice">New Transaction</a>
+3 -10
View File
@@ -32,7 +32,7 @@
</group>
<group string="Accounting Documents">
<field name="move_id" readonly="1"/>
<field name="statement_id" readonly="True" attrs="{'invisible': [('statement_id','=',False)]}"/>
<field name="statement_line_id" readonly="True" attrs="{'invisible': [('statement_line_id','=',False)]}"/>
</group>
<group string="Dates">
<field name="date" groups="account.group_account_readonly"/>
@@ -196,7 +196,7 @@
<field name="parent_state" invisible="1"/>
<field name="account_type" invisible="1"/>
<field name="statement_id" invisible="1"/>
<field name="statement_line_id" invisible="1"/>
<field name="company_currency_id" invisible="1"/>
<field name="is_same_currency" invisible="1"/>
<field name="is_account_reconcile" invisible="1"/>
@@ -674,13 +674,6 @@
</div>
<sheet>
<div name="button_box" class="oe_button_box">
<button name="open_move"
class="oe_stat_button"
icon="fa-bars"
type="object"
attrs="{'invisible': ['|', '|', ('move_type', '!=', 'entry'), ('id', '=', False), ('statement_id', '=', False)]}"
string="1 Statement">
</button>
<button name="open_payment_view"
class="oe_stat_button"
icon="fa-bars"
@@ -742,7 +735,7 @@
<field name="restrict_mode_hash_table" invisible="1"/>
<field name="country_code" invisible="1"/>
<field name="display_inactive_currency_warning" invisible="1"/>
<field name="statement_id" invisible="1"/>
<field name="statement_line_id" invisible="1"/>
<field name="payment_id" invisible="1"/>
<field name="tax_country_id" invisible="1"/>
<field name="tax_cash_basis_created_move_ids" invisible="1"/>
@@ -210,12 +210,12 @@
</button>
<!-- Statement stat button -->
<button name="button_open_statements" type="object"
<button name="button_open_statement_lines" type="object"
class="oe_stat_button" icon="fa-bars"
attrs="{'invisible': [('reconciled_statements_count','=', 0)]}">
<field name="reconciled_statements_count"/>
<span attrs="{'invisible': [('reconciled_statements_count','&gt;', 1)]}">Statement</span>
<span attrs="{'invisible': [('reconciled_statements_count','&lt;=', 1)]}">Statements</span>
attrs="{'invisible': [('reconciled_statement_lines_count','=', 0)]}">
<field name="reconciled_statement_lines_count"/>
<span attrs="{'invisible': [('reconciled_statement_lines_count','&gt;', 1)]}">Statement line</span>
<span attrs="{'invisible': [('reconciled_statement_lines_count','&lt;=', 1)]}">Statement lines</span>
</button>
<!-- Journal Entry button -->
-1
View File
@@ -5,7 +5,6 @@
from . import account_automatic_entry_wizard
from . import account_unreconcile
from . import account_validate_account_move
from . import pos_box
from . import account_move_reversal
from . import account_resequence
from . import setup_wizards
-53
View File
@@ -1,53 +0,0 @@
from odoo import models, fields, api, _
from odoo.exceptions import UserError
class CashBox(models.TransientModel):
_register = False
name = fields.Char(string='Reason', required=True)
# Attention, we don't set a domain, because there is a journal_type key
# in the context of the action
amount = fields.Float(string='Amount', digits=0, required=True)
def run(self):
context = dict(self._context or {})
active_model = context.get('active_model', False)
active_ids = context.get('active_ids', [])
records = self.env[active_model].browse(active_ids)
return self._run(records)
def _run(self, records):
for box in self:
for record in records:
if not record.journal_id:
raise UserError(_("Please check that the field 'Journal' is set on the Bank Statement"))
if not record.journal_id.company_id.transfer_account_id:
raise UserError(_("Please check that the field 'Transfer Account' is set on the company."))
box._create_bank_statement_line(record)
return {}
def _create_bank_statement_line(self, record):
for box in self:
if record.state == 'confirm':
raise UserError(_("You cannot put/take money in/out for a bank statement which is closed."))
values = box._calculate_values_for_statement_line(record)
self.env['account.bank.statement.line'].sudo().create(values)
class CashBoxOut(CashBox):
_name = 'cash.box.out'
_description = 'Cash Box Out'
def _calculate_values_for_statement_line(self, record):
if not record.journal_id.company_id.transfer_account_id:
raise UserError(_("You have to define an 'Internal Transfer Account' in your cash register's journal."))
amount = self.amount or 0.0
return {
'date': record.date,
'statement_id': record.id,
'journal_id': record.journal_id.id,
'amount': amount,
'payment_ref': self.name,
}
-23
View File
@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data>
<record model="ir.ui.view" id="cash_box_out_form">
<field name="name">cash_box_out</field>
<field name="model">cash.box.out</field>
<field name="arch" type="xml">
<form string="Take Money In/Out">
<separator string="Describe why you put/take money from the cash register:"/>
<group>
<field name="name" class="oe_inline"/>
<field name="amount" class="oe_inline"/>
</group>
<footer>
<button name="run" string="Take Money In/Out" type="object" class="btn-primary" data-hotkey="q"/>
<button class="btn-secondary" special="cancel" data-hotkey="z" string="Cancel" />
</footer>
</form>
</field>
</record>
</data>
</odoo>
+4 -11
View File
@@ -85,19 +85,12 @@ class TestExpenses(TestExpenseCommon):
in_payment_state = expense_sheet.account_move_id._get_invoice_in_payment_state()
self.assertEqual(expense_sheet.payment_state, in_payment_state, 'payment_state should be ' + in_payment_state)
statement = self.env['account.bank.statement'].create({
'name': 'test_statement',
statement_line = self.env['account.bank.statement.line'].create({
'journal_id': self.company_data['default_journal_bank'].id,
'line_ids': [
(0, 0, {
'payment_ref': 'pay_ref',
'amount': -350.0,
'partner_id': self.expense_employee.address_home_id.id,
}),
],
'payment_ref': 'pay_ref',
'amount': -350.0,
'partner_id': self.expense_employee.address_home_id.id,
})
statement.button_post()
statement_line = statement.line_ids
# Reconcile without the bank reconciliation widget since the widget is in enterprise.
_st_liquidity_lines, st_suspense_lines, _st_other_lines = statement_line\
-1
View File
@@ -26,7 +26,6 @@ The module adds following features:
'installable': True,
'auto_install': True,
'data': [
'views/account_views.xml',
'views/pos_views.xml',
'views/account_sale_closure.xml',
'views/pos_inalterability_menuitem.xml',
@@ -1,26 +0,0 @@
<odoo>
<record id="view_bank_statement_form_readonly" model="ir.ui.view">
<field name="name">account.bank.statement.form</field>
<field name="model">account.bank.statement</field>
<field name="inherit_id" ref="account.view_bank_statement_form"/>
<field name="mode">extension</field>
<field name="arch" type="xml">
<xpath expr="//form" position="inside">
<field name="pos_session_id" invisible="1"/>
</xpath>
<xpath expr="//field[@name='name']" position="attributes">
<attribute name="attrs">{'readonly': [('pos_session_id', '!=', False)]}</attribute>
</xpath>
<xpath expr="//field[@name='journal_id']" position="attributes">
<attribute name="attrs">{'readonly': ['|', ('move_line_count','!=', 0), ('pos_session_id', '!=', False)]}</attribute>
</xpath>
<xpath expr="//field[@name='date']" position="attributes">
<attribute name="attrs">{'readonly': ['|', ('state', '!=', 'open'), ('pos_session_id', '!=', False)]}</attribute>
</xpath>
<xpath expr="//field[@name='line_ids']" position="attributes">
<attribute name="attrs">{'readonly': ['|', ('state', '!=', 'open'), ('pos_session_id', '!=', False)]}</attribute>
</xpath>
</field>
</record>
</odoo>
@@ -9,15 +9,6 @@ class AccountBankStatementLine(models.Model):
_inherit = 'account.bank.statement.line'
pos_session_id = fields.Many2one('pos.session', string="Session", copy=False)
# TODO:PoMa check if they need to be converted
# account_id = fields.Many2one('account.account', related='journal_id.default_account_id', readonly=True)
# def button_validate_or_action(self):
# # OVERRIDE to check the consistency of the statement's state regarding the session's state.
# for statement in self:
# if statement.pos_session_id.state in ('opened', 'closing_control') and statement.state == 'open':
# raise UserError(_("You can't validate a bank statement that is used in an opened Session of a Point of Sale."))
# return super(AccountBankStatement, self).button_validate_or_action()
@api.ondelete(at_uninstall=False)
def _unlink_except_linked_to_pos_session(self):
+1 -1
View File
@@ -219,7 +219,7 @@ class PosConfig(models.Model):
for pos_config in self:
session = PosSession.search_read(
[('config_id', '=', pos_config.id), ('state', '=', 'closed')],
['cash_register_balance_end_real', 'stop_at', 'cash_journal_id'],
['cash_register_balance_end_real', 'stop_at'],
order="stop_at desc", limit=1)
if session:
timezone = pytz.timezone(self._context.get('tz') or self.env.user.tz or 'UTC')
+2 -2
View File
@@ -170,7 +170,7 @@ class PosOrder(models.Model):
order_bank_statement_lines= self.env['pos.payment'].search([('pos_order_id', '=', order.id)])
order_bank_statement_lines.unlink()
for payments in pos_order['statement_ids']: # this is not related to session.statement_ids it is just a variable with a weird name holding payments
for payments in pos_order['statement_ids']:
order.add_payment(self._payment_fields(order, payments[2]))
order.amount_paid = sum(order.payment_ids.mapped('amount'))
@@ -1011,7 +1011,7 @@ class PosOrder(models.Model):
timezone = pytz.timezone(self._context.get('tz') or self.env.user.tz or 'UTC')
return {
'lines': [[0, 0, line] for line in order.lines.export_for_ui()],
'statement_ids': [[0, 0, payment] for payment in order.payment_ids.export_for_ui()], # this is not related to session.statement_ids it is just a variable with a weird name holding payments
'statement_ids': [[0, 0, payment] for payment in order.payment_ids.export_for_ui()],
'name': order.pos_reference,
'uid': re.search('([0-9-]){14}', order.pos_reference).group(0),
'amount_paid': order.amount_paid,
+8 -34
View File
@@ -53,7 +53,7 @@ class PosSession(models.Model):
login_number = fields.Integer(string='Login Sequence Number', help='A sequence number that is incremented each time a user resumes the pos session', default=0)
opening_notes = fields.Text(string="Opening Notes")
cash_control = fields.Boolean(compute='_compute_cash_all', string='Has Cash Control', compute_sudo=True, store=True)
cash_control = fields.Boolean(compute='_compute_cash_all', string='Has Cash Control', compute_sudo=True)
cash_journal_id = fields.Many2one('account.journal', compute='_compute_cash_all', string='Cash Journal', store=True)
cash_register_balance_end_real = fields.Monetary(
@@ -175,14 +175,6 @@ class PosSession(models.Model):
if (company.period_lock_date and start_date <= company.period_lock_date) or (company.fiscalyear_lock_date and start_date <= company.fiscalyear_lock_date):
raise ValidationError(_("You cannot create a session before the accounting lock date."))
def _check_bank_statement_state(self):
for session in self:
posted_stmt_lines = session.statement_line_ids.filtered(lambda x: x.state == "posted")
if posted_stmt_lines:
raise UserError(_("Some Cash Transactions are already posted. "
"Please reset them to new in order to close the session.\n"
"Cash Registers: %r", list(stmt_line.name for stmt_line in posted_stmt_lines)))
def _check_invoices_are_posted(self):
unposted_invoices = self.order_ids.sudo().with_company(self.company_id).account_move.filtered(lambda x: x.state != 'posted')
if unposted_invoices:
@@ -247,8 +239,7 @@ class PosSession(models.Model):
session.cash_register_balance_start = last_session.cash_register_balance_end_real # defaults to 0 if lastsession is empty
else:
values['state'] = 'opened'
if values:
session.write(values)
session.write(values)
return True
def action_pos_session_closing_control(self, balancing_account=False, amount_to_balance=0, bank_payment_method_diffs=None):
@@ -266,7 +257,6 @@ class PosSession(models.Model):
if session.rescue and session.config_id.cash_control:
default_cash_payment_method_id = self.payment_method_ids.filtered(lambda pm: pm.type == 'cash')[0]
orders = self.order_ids.filtered(lambda o: o.state == 'paid' or o.state == 'invoiced')
total_cash = sum(
orders.payment_ids.filtered(lambda p: p.payment_method_id == default_cash_payment_method_id).mapped('amount')
) + self.cash_register_balance_start
@@ -285,7 +275,6 @@ class PosSession(models.Model):
# Session without cash payment method will not have a cash register.
# However, there could be other payment methods, thus, session still
# needs to be validated.
self._check_bank_statement_state()
return self._validate_session(balancing_account, amount_to_balance, bank_payment_method_diffs)
def _validate_session(self, balancing_account=False, amount_to_balance=0, bank_payment_method_diffs=None):
@@ -333,7 +322,6 @@ class PosSession(models.Model):
self.sudo().with_company(self.company_id)._reconcile_account_move_lines(data)
else:
if self.config_id.cash_control:
st_line_vals = {
'journal_id': self.cash_journal_id.id,
'amount': self.cash_register_difference,
@@ -402,12 +390,6 @@ class PosSession(models.Model):
if check_closing_session:
return check_closing_session
# For now we won't simply do
# self._check_pos_session_balance()
# self._check_bank_statement_state()
# validate_result = self._validate_session()
# because some functions are being used and overridden in other modules...
# so we'll try to use the original flow as of now for the moment
validate_result = self.action_pos_session_closing_control(bank_payment_method_diffs=bank_payment_method_diffs)
# If an error is raised, the user will still be redirected to the back end to manually close the session.
@@ -555,7 +537,7 @@ class PosSession(models.Model):
'name': default_cash_payment_method_id.name,
'amount': self.cash_register_balance_start
+ total_default_cash_payment_amount
+ self.cash_real_transaction,
+ sum(self.statement_line_ids.mapped('amount')),
'opening': self.cash_register_balance_start,
'payment_amount': total_default_cash_payment_amount,
'moves': cash_in_out_list,
@@ -1399,16 +1381,6 @@ class PosSession(models.Model):
# self should be single record as this method is only called in the subfunctions of self._validate_session
return self.currency_id._convert(amount, self.company_id.currency_id, self.company_id, date, round=round)
def show_cash_register(self):
self.ensure_one()
return {
'name': _('Cash register for %s') % (self.name, ),
'type': 'ir.actions.act_window',
'res_model': 'account.bank.statement.line',
'view_mode': 'tree',
'domain': [('pos_session_id', '=', self.id)],
}
def show_journal_items(self):
self.ensure_one()
all_related_moves = self._get_related_account_moves()
@@ -1542,14 +1514,16 @@ class PosSession(models.Model):
if not sessions:
raise UserError(_("There is no cash payment method for this PoS Session"))
for session in sessions:
self.env['account.bank.statement.line'].create({
self.env['account.bank.statement.line'].create([
{
'pos_session_id': session.id,
'journal_id': session.cash_journal_id.id,
'amount': sign * amount,
'date': fields.Date.context_today(self),
'payment_ref': '-'.join([session.name, extras['translatedType'], reason]),
})
}
for session in sessions
])
message_content = [f"Cash {extras['translatedType']}", f'- Amount: {extras["formattedAmount"]}']
if reason:
@@ -40,7 +40,7 @@
<td>Opening</td>
<td t-esc="env.pos.format_currency(defaultCashDetails.opening)"/>
</tr>
<tr t-foreach="defaultCashDetails.moves" t-as="move" t-key="move.name">
<tr t-foreach="defaultCashDetails.moves" t-as="move" t-key="move_index">
<td>
<div class="flex">
<div class="cash-sign" t-esc="move.amount &lt; 0 ? '-' : '+'"/>
@@ -549,53 +549,6 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
# I close the session to generate the journal entries
current_session.action_pos_session_closing_control()
"""In order to test the reports on Bank Statement defined in point_of_sale module, I create a bank statement line, confirm it and print the reports"""
# I select the period and journal for the bank statement
# FIXME:poma
#
# context_journal = {'journal_type': 'bank'}
# self.assertTrue(self.AccountBankStatement.with_context(
# context_journal)._default_journal(), 'Journal has not been selected')
# journal = self.env['account.journal'].create({
# 'name': 'Bank Test',
# 'code': 'BNKT',
# 'type': 'bank',
# 'company_id': self.env.company.id,
# })
# # I create a bank statement with Opening and Closing balance 0.
# account_statement = self.AccountBankStatement.create({
# 'balance_start': 0.0,
# 'balance_end_real': 0.0,
# 'date': time.strftime('%Y-%m-%d'),
# 'journal_id': journal.id,
# 'company_id': self.env.company.id,
# 'name': 'pos session test',
# })
# # I create bank statement line
# account_statement_line = self.AccountBankStatementLine.create({
# 'amount': 1000,
# 'partner_id': self.partner4.id,
# 'statement_id': account_statement.id,
# 'payment_ref': 'EXT001'
# })
# # I modify the bank statement and set the Closing Balance.
# account_statement.write({
# 'balance_end_real': 1000.0,
# })
#
# # I reconcile the bank statement.
# new_aml_dicts = [{
# 'account_id': self.partner4.property_account_receivable_id.id,
# 'name': "EXT001",
# 'credit': 1000.0,
# 'debit': 0.0,
# }]
#
# # I confirm the bank statement using Confirm button
#
# self.AccountBankStatement.button_validate()
def test_create_from_ui(self):
"""
Simulation of sales coming from the interface, even after closing the session
@@ -45,16 +45,6 @@
groups="account.group_account_readonly"
>
</button>
<!--todo:poma find a replacement -->
<button
name="show_cash_register"
type="object"
class="oe_stat_button"
icon="fa-bars"
string="Cash Register"
attrs="{'invisible':[('cash_control', '=', False)]}"
groups="account.group_account_readonly"
/>
</div>
<h1 class="oe_title">
<field name="name" attrs="{'invisible': [('name','=','/')]}" class="oe_inline"/>
@@ -901,35 +901,33 @@ tour.stepUtils.mobileModifier(tour.stepUtils.autoExpandMoreButtons('.o_control_p
content: _t('Go to Accounting'),
position: 'bottom',
}, {
// FIXME WOWL: this selector needs to work in both legacy and non-legacy views
mobile: false,
edition: "enterprise",
trigger: 'div[name=bank_statement_create_button] > a[data-name=create_bank_statement], div[name=bank_statement_create_button] > a[name=create_bank_statement]',
content: _t('Create a new bank statement'),
position: 'bottom',
trigger: "div.o_account_kanban div.o_kanban_card_header a.oe_kanban_action span:contains('Bank')",
content: _t("Open the bank reconciliation widget"),
}, {
mobile: false,
edition: "enterprise",
trigger: '.o_field_widget[name=name] input',
content: _t("Let's enter the reference."),
position: 'left',
run: 'text the_flow.statement',
trigger: "button.o_switch_view.o_list",
content: _t("Move to the list view"),
}, {
mobile: false,
edition: "enterprise",
trigger: ".o_field_widget[name=balance_end_real] input",
content: _t("Let's calculate the ending balance."),
position: 'right',
run: 'text 9955.87',
trigger: "button.o_list_button_add",
content: _t("Create a new bank transaction"),
}, {
// FIXME convert list in kanban + form
mobile: false,
edition: "enterprise",
trigger: ".o_field_widget[name=line_ids] .o_field_x2many_list_row_add > a",
content: _t("Click here to add some lines."),
position: "bottom",
}, {
edition: "enterprise",
trigger: ".o_selected_row .o_field_widget[name=amount] input",
trigger: '.o_field_widget[name=amount] input',
content: _t("Write the amount received."),
position: "bottom",
run: "text 11.00",
}, {
mobile: false,
edition: "enterprise",
trigger: ".o_selected_row .o_field_widget[name=payment_ref] input",
content: _t("Let's enter a name."),
run: "text the_flow.statement.line",
}, {
mobile: false,
edition: "enterprise",
@@ -944,42 +942,49 @@ tour.stepUtils.mobileModifier(tour.stepUtils.autoExpandMoreButtons('.o_control_p
in_modal: false,
auto: true,
}, {
mobile: true,
mobile: false,
edition: "enterprise",
trigger: ".o_selected_row .o_field_widget[name=partner_id] input",
content: _t("Write the name of your customer."),
position: "bottom",
},
...tour.stepUtils.mobileKanbanSearchMany2X('Partner', 'the_flow.customer'),
{
edition: "enterprise",
trigger: ".o_selected_row .o_field_widget[name=payment_ref] input",
trigger: '.o_list_button_save',
extra_trigger: ".o_selected_row .o_field_widget[name=partner_id] .o_external_button",
content: _t("Let's enter a name."),
position: "bottom",
run: "text the_flow.statement.line",
}, {
edition: "enterprise",
trigger: '.o_form_button_save',
content: _t('Save.'),
position: 'bottom',
},
...tour.stepUtils.statusbarButtonsSteps('Post', _t('Processing'), ".breadcrumb-item.active:contains('the_flow.statement')").map(tour.stepUtils.editionEnterpriseModifier),
...tour.stepUtils.statusbarButtonsSteps('Reconcile', _t('Reconcile'), ".o_statusbar_status .btn.dropdown-toggle:contains(Processing)")
.map(function(step){return {mobile: false, ...step}})
.map(tour.stepUtils.editionEnterpriseModifier),
{
}, {
mobile: false,
edition: "enterprise",
trigger: "button[name='button_validate']",
content: Markup(_t('<p><b>Click on Reconcile</p>')),
position: "right",
},
{
trigger: "button.o_switch_view.o_kanban",
extra_trigger: ".o_list_button_add",
content: _t("Move back to the kanban view"),
}, {
mobile: false,
edition: "enterprise",
trigger: ".o_tag_badge_text:contains('Matched')",
auto: true,
trigger: "div.o_bank_rec_st_line_kanban_card span:contains('the_flow.customer')",
extra_trigger: "div.o_bank_rec_st_line_kanban_card span:contains('the_flow.customer')",
content: _t("Select the newly created bank transaction"),
}, {
mobile: false,
edition: "enterprise",
trigger: "button[name='button_validate'].btn-primary",
extra_trigger: "button[name='button_validate'].btn-primary",
content: _t("Reconcile the bank transaction"),
}, {
mobile: false,
edition: "enterprise",
trigger: ".o_control_panel .o_filter_menu .o_dropdown_title:contains('Filters'):first",
extra_trigger: "a.active[name='amls_tab']",
content: _t("Click on 'Filters'"),
}, {
mobile: false,
edition: "enterprise",
trigger: ".o_control_panel .o_filter_menu .dropdown-item:contains('Matched')",
extra_trigger: ".o_control_panel .o_filter_menu .dropdown-item:contains('Matched')",
content: _t("Add the 'Matched' filters"),
}, {
mobile: false,
edition: "enterprise",
trigger: "div.o_bank_rec_st_line_kanban_card span:contains('the_flow.customer')",
extra_trigger: "div.o_bank_rec_st_line_kanban_card span:contains('the_flow.customer')",
content: _t("Ensure the line is well reconciled"),
run: function(){},
},
// exit reconciliation widget
{