[REF] account: create 'account.reconciliation.widget' model
Contains widget reconciliation methods. This refactoring does not modify the feature, some errors have been notified in the code but not modified.
This commit is contained in:
@@ -13,3 +13,4 @@ from . import product
|
||||
from . import company
|
||||
from . import res_config_settings
|
||||
from . import account_cash_rounding
|
||||
from . import reconciliation_widget
|
||||
|
||||
@@ -275,66 +275,6 @@ class AccountBankStatement(models.Model):
|
||||
statement.name = st_number
|
||||
statement.state = 'open'
|
||||
|
||||
@api.multi
|
||||
def reconciliation_widget_preprocess(self):
|
||||
""" Get statement lines of the specified statements or all unreconciled statement lines and try to automatically reconcile them / find them a partner.
|
||||
Return ids of statement lines left to reconcile and other data for the reconciliation widget.
|
||||
"""
|
||||
statements = self
|
||||
# NB : The field account_id can be used at the statement line creation/import to avoid the reconciliation process on it later on,
|
||||
# this is why we filter out statements lines where account_id is set
|
||||
|
||||
sql_query = """SELECT stl.id
|
||||
FROM account_bank_statement_line stl
|
||||
WHERE account_id IS NULL AND stl.amount != 0.0 AND not exists (select 1 from account_move_line aml where aml.statement_line_id = stl.id)
|
||||
AND company_id = %s
|
||||
"""
|
||||
params = (self.env.user.company_id.id,)
|
||||
if statements:
|
||||
sql_query += ' AND stl.statement_id IN %s'
|
||||
params += (tuple(statements.ids),)
|
||||
sql_query += ' ORDER BY stl.id'
|
||||
self.env.cr.execute(sql_query, params)
|
||||
st_lines_left = self.env['account.bank.statement.line'].browse([line.get('id') for line in self.env.cr.dictfetchall()])
|
||||
|
||||
#try to assign partner to bank_statement_line
|
||||
stl_to_assign = st_lines_left.filtered(lambda stl: not stl.partner_id)
|
||||
refs = set(stl_to_assign.mapped('name'))
|
||||
if stl_to_assign and refs\
|
||||
and st_lines_left[0].journal_id.default_credit_account_id\
|
||||
and st_lines_left[0].journal_id.default_debit_account_id:
|
||||
|
||||
sql_query = """SELECT aml.partner_id, aml.ref, stl.id
|
||||
FROM account_move_line aml
|
||||
JOIN account_account acc ON acc.id = aml.account_id
|
||||
JOIN account_bank_statement_line stl ON aml.ref = stl.name
|
||||
WHERE (aml.company_id = %s
|
||||
AND aml.partner_id IS NOT NULL)
|
||||
AND (
|
||||
(aml.statement_id IS NULL AND aml.account_id IN %s)
|
||||
OR
|
||||
(acc.internal_type IN ('payable', 'receivable') AND aml.reconciled = false)
|
||||
)
|
||||
AND aml.ref IN %s
|
||||
"""
|
||||
params = (self.env.user.company_id.id, (st_lines_left[0].journal_id.default_credit_account_id.id, st_lines_left[0].journal_id.default_debit_account_id.id), tuple(refs))
|
||||
if statements:
|
||||
sql_query += 'AND stl.id IN %s'
|
||||
params += (tuple(stl_to_assign.ids),)
|
||||
self.env.cr.execute(sql_query, params)
|
||||
results = self.env.cr.dictfetchall()
|
||||
st_line = self.env['account.bank.statement.line']
|
||||
for line in results:
|
||||
st_line.browse(line.get('id')).write({'partner_id': line.get('partner_id')})
|
||||
|
||||
return {
|
||||
'st_lines_ids': st_lines_left.ids,
|
||||
'notifications': [],
|
||||
'statement_name': len(statements) == 1 and statements[0].name or False,
|
||||
'journal_id': statements and statements[0].journal_id.id or False,
|
||||
'num_already_reconciled_lines': 0,
|
||||
}
|
||||
|
||||
|
||||
class AccountBankStatementLine(models.Model):
|
||||
_name = "account.bank.statement.line"
|
||||
@@ -434,163 +374,10 @@ class AccountBankStatementLine(models.Model):
|
||||
if payment_to_cancel:
|
||||
payment_to_cancel.unlink()
|
||||
|
||||
####################################################
|
||||
# Reconciliation interface methods
|
||||
####################################################
|
||||
@api.multi
|
||||
def reconciliation_widget_auto_reconcile(self, num_already_reconciled_lines):
|
||||
automatic_reconciliation_entries = self.env['account.bank.statement.line']
|
||||
unreconciled = self.env['account.bank.statement.line']
|
||||
for stl in self:
|
||||
res = stl.auto_reconcile()
|
||||
if res:
|
||||
automatic_reconciliation_entries += stl
|
||||
else:
|
||||
unreconciled += stl
|
||||
|
||||
# Collect various information for the reconciliation widget
|
||||
notifications = []
|
||||
num_auto_reconciled = len(automatic_reconciliation_entries)
|
||||
if num_auto_reconciled > 0:
|
||||
auto_reconciled_message = num_auto_reconciled > 1 \
|
||||
and _("%d transactions were automatically reconciled.") % num_auto_reconciled \
|
||||
or _("1 transaction was automatically reconciled.")
|
||||
notifications += [{
|
||||
'type': 'info',
|
||||
'message': auto_reconciled_message,
|
||||
'details': {
|
||||
'name': _("Automatically reconciled items"),
|
||||
'model': 'account.move',
|
||||
'ids': automatic_reconciliation_entries.mapped('journal_entry_ids').mapped('move_id').ids
|
||||
}
|
||||
}]
|
||||
return {
|
||||
'st_lines_ids': unreconciled.ids,
|
||||
'notifications': notifications,
|
||||
'statement_name': False,
|
||||
'num_already_reconciled_lines': num_auto_reconciled + num_already_reconciled_lines,
|
||||
}
|
||||
|
||||
@api.multi
|
||||
def get_data_for_reconciliation_widget(self, excluded_ids=None):
|
||||
""" Returns the data required to display a reconciliation widget, for each statement line in self """
|
||||
excluded_ids = excluded_ids or []
|
||||
ret = []
|
||||
|
||||
for st_line in self:
|
||||
aml_recs = st_line.get_reconciliation_proposition(excluded_ids=excluded_ids)
|
||||
target_currency = st_line.currency_id or st_line.journal_id.currency_id or st_line.journal_id.company_id.currency_id
|
||||
rp = aml_recs.prepare_move_lines_for_reconciliation_widget(target_currency=target_currency, target_date=st_line.date)
|
||||
excluded_ids += [move_line['id'] for move_line in rp]
|
||||
ret.append({
|
||||
'st_line': st_line.get_statement_line_for_reconciliation_widget(),
|
||||
'reconciliation_proposition': rp
|
||||
})
|
||||
return ret
|
||||
|
||||
def get_statement_line_for_reconciliation_widget(self):
|
||||
""" Returns the data required by the bank statement reconciliation widget to display a statement line """
|
||||
statement_currency = self.journal_id.currency_id or self.journal_id.company_id.currency_id
|
||||
if self.amount_currency and self.currency_id:
|
||||
amount = self.amount_currency
|
||||
amount_currency = self.amount
|
||||
amount_currency_str = formatLang(self.env, abs(amount_currency), currency_obj=statement_currency)
|
||||
else:
|
||||
amount = self.amount
|
||||
amount_currency = amount
|
||||
amount_currency_str = ""
|
||||
amount_str = formatLang(self.env, abs(amount), currency_obj=self.currency_id or statement_currency)
|
||||
|
||||
data = {
|
||||
'id': self.id,
|
||||
'ref': self.ref,
|
||||
'note': self.note or "",
|
||||
'name': self.name,
|
||||
'date': self.date,
|
||||
'amount': amount,
|
||||
'amount_str': amount_str, # Amount in the statement line currency
|
||||
'currency_id': self.currency_id.id or statement_currency.id,
|
||||
'partner_id': self.partner_id.id,
|
||||
'journal_id': self.journal_id.id,
|
||||
'statement_id': self.statement_id.id,
|
||||
'account_id': [self.journal_id.default_debit_account_id.id, self.journal_id.default_debit_account_id.display_name],
|
||||
'account_code': self.journal_id.default_debit_account_id.code,
|
||||
'account_name': self.journal_id.default_debit_account_id.name,
|
||||
'partner_name': self.partner_id.name,
|
||||
'communication_partner_name': self.partner_name,
|
||||
'amount_currency_str': amount_currency_str, # Amount in the statement currency
|
||||
'amount_currency': amount_currency, # Amount in the statement currency
|
||||
'has_no_partner': not self.partner_id.id,
|
||||
}
|
||||
if self.partner_id:
|
||||
if amount > 0:
|
||||
data['open_balance_account_id'] = self.partner_id.property_account_receivable_id.id
|
||||
else:
|
||||
data['open_balance_account_id'] = self.partner_id.property_account_payable_id.id
|
||||
|
||||
return data
|
||||
|
||||
@api.multi
|
||||
def get_move_lines_for_reconciliation_widget(self, partner_id=None, excluded_ids=None, str=False, offset=0, limit=None):
|
||||
""" Returns move lines for the bank statement reconciliation widget, formatted as a list of dicts
|
||||
"""
|
||||
aml_recs = self.get_move_lines_for_reconciliation(partner_id=partner_id, excluded_ids=excluded_ids, str=str, offset=offset, limit=limit)
|
||||
target_currency = self.currency_id or self.journal_id.currency_id or self.journal_id.company_id.currency_id
|
||||
return aml_recs.prepare_move_lines_for_reconciliation_widget(target_currency=target_currency, target_date=self.date)
|
||||
|
||||
####################################################
|
||||
# Reconciliation methods
|
||||
####################################################
|
||||
|
||||
def get_move_lines_for_reconciliation(self, partner_id=None, excluded_ids=None, str=False, offset=0, limit=None, additional_domain=None, overlook_partner=False):
|
||||
""" Return account.move.line records which can be used for bank statement reconciliation.
|
||||
|
||||
:param partner_id:
|
||||
:param excluded_ids:
|
||||
:param str:
|
||||
:param offset:
|
||||
:param limit:
|
||||
:param additional_domain:
|
||||
:param overlook_partner:
|
||||
"""
|
||||
if partner_id is None:
|
||||
partner_id = self.partner_id.id
|
||||
|
||||
# Blue lines = payment on bank account not assigned to a statement yet
|
||||
reconciliation_aml_accounts = [self.journal_id.default_credit_account_id.id, self.journal_id.default_debit_account_id.id]
|
||||
domain_reconciliation = ['&', '&', ('statement_line_id', '=', False), ('account_id', 'in', reconciliation_aml_accounts), ('payment_id','<>', False)]
|
||||
|
||||
# Black lines = unreconciled & (not linked to a payment or open balance created by statement
|
||||
domain_matching = [('reconciled', '=', False)]
|
||||
if partner_id or overlook_partner:
|
||||
domain_matching = expression.AND([domain_matching, [('account_id.internal_type', 'in', ['payable', 'receivable'])]])
|
||||
else:
|
||||
# TODO : find out what use case this permits (match a check payment, registered on a journal whose account type is other instead of liquidity)
|
||||
domain_matching = expression.AND([domain_matching, [('account_id.reconcile', '=', True)]])
|
||||
|
||||
# Let's add what applies to both
|
||||
domain = expression.OR([domain_reconciliation, domain_matching])
|
||||
if partner_id and not overlook_partner:
|
||||
domain = expression.AND([domain, [('partner_id', '=', partner_id)]])
|
||||
|
||||
# Domain factorized for all reconciliation use cases
|
||||
if str:
|
||||
str_domain = self.env['account.move.line'].domain_move_lines_for_reconciliation(str=str)
|
||||
if not partner_id:
|
||||
str_domain = expression.OR([str_domain, [('partner_id.name', 'ilike', str)]])
|
||||
domain = expression.AND([domain, str_domain])
|
||||
if excluded_ids:
|
||||
domain = expression.AND([[('id', 'not in', excluded_ids)], domain])
|
||||
|
||||
# Domain from caller
|
||||
if additional_domain is None:
|
||||
additional_domain = []
|
||||
else:
|
||||
additional_domain = expression.normalize_domain(additional_domain)
|
||||
domain = expression.AND([domain, additional_domain])
|
||||
|
||||
return self.env['account.move.line'].search(domain, offset=offset, limit=limit, order="date_maturity desc, id desc")
|
||||
|
||||
def _get_common_sql_query(self, overlook_partner = False, excluded_ids = None, split = False):
|
||||
acc_type = "acc.internal_type IN ('payable', 'receivable')" if (self.partner_id or overlook_partner) else "acc.reconcile = true"
|
||||
select_clause = "SELECT aml.id "
|
||||
@@ -609,56 +396,12 @@ class AccountBankStatementLine(models.Model):
|
||||
return select_clause, from_clause, where_clause
|
||||
return select_clause + from_clause + where_clause
|
||||
|
||||
def get_reconciliation_proposition(self, excluded_ids=None):
|
||||
""" Returns move lines that constitute the best guess to reconcile a statement line
|
||||
Note: it only looks for move lines in the same currency as the statement line.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if not excluded_ids:
|
||||
excluded_ids = []
|
||||
amount = self.amount_currency or self.amount
|
||||
company_currency = self.journal_id.company_id.currency_id
|
||||
st_line_currency = self.currency_id or self.journal_id.currency_id
|
||||
currency = (st_line_currency and st_line_currency != company_currency) and st_line_currency.id or False
|
||||
precision = st_line_currency and st_line_currency.decimal_places or company_currency.decimal_places
|
||||
params = {'company_id': self.env.user.company_id.id,
|
||||
'account_payable_receivable': (self.journal_id.default_credit_account_id.id, self.journal_id.default_debit_account_id.id),
|
||||
'amount': float_repr(float_round(amount, precision_digits=precision), precision_digits=precision),
|
||||
'partner_id': self.partner_id.id,
|
||||
'excluded_ids': tuple(excluded_ids),
|
||||
'ref': self.name,
|
||||
}
|
||||
# Look for structured communication match
|
||||
if self.name:
|
||||
add_to_select = ", CASE WHEN aml.ref = %(ref)s THEN 1 ELSE 2 END as temp_field_order "
|
||||
add_to_from = " JOIN account_move m ON m.id = aml.move_id "
|
||||
select_clause, from_clause, where_clause = self._get_common_sql_query(overlook_partner=True, excluded_ids=excluded_ids, split=True)
|
||||
sql_query = select_clause + add_to_select + from_clause + add_to_from + where_clause
|
||||
sql_query += " AND (aml.ref= %(ref)s or m.name = %(ref)s) \
|
||||
ORDER BY temp_field_order, date_maturity desc, aml.id desc"
|
||||
self.env.cr.execute(sql_query, params)
|
||||
results = self.env.cr.fetchone()
|
||||
if results:
|
||||
return self.env['account.move.line'].browse(results[0])
|
||||
|
||||
# Look for a single move line with the same amount
|
||||
field = currency and 'amount_residual_currency' or 'amount_residual'
|
||||
liquidity_field = currency and 'amount_currency' or amount > 0 and 'debit' or 'credit'
|
||||
liquidity_amt_clause = currency and '%(amount)s::numeric' or 'abs(%(amount)s::numeric)'
|
||||
sql_query = self._get_common_sql_query(excluded_ids=excluded_ids) + \
|
||||
" AND ("+field+" = %(amount)s::numeric OR (acc.internal_type = 'liquidity' AND "+liquidity_field+" = " + liquidity_amt_clause + ")) \
|
||||
ORDER BY date_maturity desc, aml.id desc LIMIT 1"
|
||||
self.env.cr.execute(sql_query, params)
|
||||
results = self.env.cr.fetchone()
|
||||
if results:
|
||||
return self.env['account.move.line'].browse(results[0])
|
||||
|
||||
return self.env['account.move.line']
|
||||
|
||||
# todo: remove this => coverage task
|
||||
def _get_move_lines_for_auto_reconcile(self):
|
||||
""" Returns the move lines that the method auto_reconcile can use to try to reconcile the statement line """
|
||||
pass
|
||||
|
||||
# this method is only call by the widget reconciliation, move it or not ?
|
||||
@api.multi
|
||||
def auto_reconcile(self):
|
||||
""" Try to automatically reconcile the statement.line ; return the counterpart journal entry/ies if the automatic reconciliation succeeded, False otherwise.
|
||||
@@ -798,24 +541,6 @@ class AccountBankStatementLine(models.Model):
|
||||
'amount_currency': amount_currency,
|
||||
}
|
||||
|
||||
@api.multi
|
||||
def process_reconciliations(self, data):
|
||||
""" Handles data sent from the bank statement reconciliation widget (and can otherwise serve as an old-API bridge)
|
||||
|
||||
:param list of dicts data: must contains the keys 'counterpart_aml_dicts', 'payment_aml_ids' and 'new_aml_dicts',
|
||||
whose value is the same as described in process_reconciliation except that ids are used instead of recordsets.
|
||||
"""
|
||||
AccountMoveLine = self.env['account.move.line']
|
||||
ctx = dict(self._context, force_price_include=False)
|
||||
for st_line, datum in pycompat.izip(self, data):
|
||||
payment_aml_rec = AccountMoveLine.browse(datum.get('payment_aml_ids', []))
|
||||
for aml_dict in datum.get('counterpart_aml_dicts', []):
|
||||
aml_dict['move_line'] = AccountMoveLine.browse(aml_dict['counterpart_aml_id'])
|
||||
del aml_dict['counterpart_aml_id']
|
||||
if datum.get('partner_id') is not None:
|
||||
st_line.write({'partner_id': datum['partner_id']})
|
||||
st_line.with_context(ctx).process_reconciliation(datum.get('counterpart_aml_dicts', []), payment_aml_rec, datum.get('new_aml_dicts', []))
|
||||
|
||||
def fast_counterpart_creation(self):
|
||||
"""This function is called when confirming a bank statement and will allow to automatically process lines without
|
||||
going in the bank reconciliation widget. By setting an account_id on bank statement lines, it will create a journal
|
||||
|
||||
@@ -552,384 +552,6 @@ class AccountMoveLine(models.Model):
|
||||
line.debit = amount > 0 and amount or 0.0
|
||||
line.credit = amount < 0 and -amount or 0.0
|
||||
|
||||
####################################################
|
||||
# Reconciliation interface methods
|
||||
####################################################
|
||||
|
||||
@api.model
|
||||
def get_data_for_manual_reconciliation_widget(self, partner_ids, account_ids):
|
||||
""" Returns the data required for the invoices & payments matching of partners/accounts.
|
||||
If an argument is None, fetch all related reconciliations. Use [] to fetch nothing.
|
||||
"""
|
||||
return {
|
||||
'customers': self.get_data_for_manual_reconciliation('partner', partner_ids, 'receivable'),
|
||||
'suppliers': self.get_data_for_manual_reconciliation('partner', partner_ids, 'payable'),
|
||||
'accounts': self.get_data_for_manual_reconciliation('account', account_ids),
|
||||
}
|
||||
|
||||
@api.model
|
||||
def get_data_for_manual_reconciliation(self, res_type, res_ids=None, account_type=None):
|
||||
""" Returns the data required for the invoices & payments matching of partners/accounts (list of dicts).
|
||||
If no res_ids is passed, returns data for all partners/accounts that can be reconciled.
|
||||
|
||||
:param res_type: either 'partner' or 'account'
|
||||
:param res_ids: ids of the partners/accounts to reconcile, use None to fetch data indiscriminately
|
||||
of the id, use [] to prevent from fetching any data at all.
|
||||
:param account_type: if a partner is both customer and vendor, you can use 'payable' to reconcile
|
||||
the vendor-related journal entries and 'receivable' for the customer-related entries.
|
||||
"""
|
||||
if res_ids is not None and len(res_ids) == 0:
|
||||
# Note : this short-circuiting is better for performances, but also required
|
||||
# since postgresql doesn't implement empty list (so 'AND id in ()' is useless)
|
||||
return []
|
||||
res_ids = res_ids and tuple(res_ids)
|
||||
|
||||
assert res_type in ('partner', 'account')
|
||||
assert account_type in ('payable', 'receivable', None)
|
||||
is_partner = res_type == 'partner'
|
||||
res_alias = is_partner and 'p' or 'a'
|
||||
|
||||
query = ("""
|
||||
SELECT {0} account_id, account_name, account_code, max_date,
|
||||
to_char(last_time_entries_checked, 'YYYY-MM-DD') AS last_time_entries_checked
|
||||
FROM (
|
||||
SELECT {1}
|
||||
{res_alias}.last_time_entries_checked AS last_time_entries_checked,
|
||||
a.id AS account_id,
|
||||
a.name AS account_name,
|
||||
a.code AS account_code,
|
||||
MAX(l.write_date) AS max_date
|
||||
FROM
|
||||
account_move_line l
|
||||
RIGHT JOIN account_account a ON (a.id = l.account_id)
|
||||
RIGHT JOIN account_account_type at ON (at.id = a.user_type_id)
|
||||
{2}
|
||||
WHERE
|
||||
a.reconcile IS TRUE
|
||||
AND l.full_reconcile_id is NULL
|
||||
{3}
|
||||
{4}
|
||||
{5}
|
||||
AND l.company_id = {6}
|
||||
AND EXISTS (
|
||||
SELECT NULL
|
||||
FROM account_move_line l
|
||||
WHERE l.account_id = a.id
|
||||
{7}
|
||||
AND l.amount_residual > 0
|
||||
)
|
||||
AND EXISTS (
|
||||
SELECT NULL
|
||||
FROM account_move_line l
|
||||
WHERE l.account_id = a.id
|
||||
{7}
|
||||
AND l.amount_residual < 0
|
||||
)
|
||||
GROUP BY {8} a.id, a.name, a.code, {res_alias}.last_time_entries_checked
|
||||
ORDER BY {res_alias}.last_time_entries_checked
|
||||
) as s
|
||||
WHERE (last_time_entries_checked IS NULL OR max_date > last_time_entries_checked)
|
||||
""".format(
|
||||
is_partner and 'partner_id, partner_name,' or ' ',
|
||||
is_partner and 'p.id AS partner_id, p.name AS partner_name,' or ' ',
|
||||
is_partner and 'RIGHT JOIN res_partner p ON (l.partner_id = p.id)' or ' ',
|
||||
is_partner and ' ' or "AND at.type <> 'payable' AND at.type <> 'receivable'",
|
||||
account_type and "AND at.type = %(account_type)s" or '',
|
||||
res_ids and 'AND ' + res_alias + '.id in %(res_ids)s' or '',
|
||||
self.env.user.company_id.id,
|
||||
is_partner and 'AND l.partner_id = p.id' or ' ',
|
||||
is_partner and 'l.partner_id, p.id,' or ' ',
|
||||
res_alias=res_alias
|
||||
))
|
||||
self.env.cr.execute(query, locals())
|
||||
|
||||
# Apply ir_rules by filtering out
|
||||
rows = self.env.cr.dictfetchall()
|
||||
ids = [x['account_id'] for x in rows]
|
||||
allowed_ids = set(self.env['account.account'].browse(ids).ids)
|
||||
rows = [row for row in rows if row['account_id'] in allowed_ids]
|
||||
if is_partner:
|
||||
ids = [x['partner_id'] for x in rows]
|
||||
allowed_ids = set(self.env['res.partner'].browse(ids).ids)
|
||||
rows = [row for row in rows if row['partner_id'] in allowed_ids]
|
||||
|
||||
# Fetch other data
|
||||
for row in rows:
|
||||
account = self.env['account.account'].browse(row['account_id'])
|
||||
row['currency_id'] = account.currency_id.id or account.company_id.currency_id.id
|
||||
partner_id = is_partner and row['partner_id'] or None
|
||||
row['reconciliation_proposition'] = self.get_reconciliation_proposition(account.id, partner_id)
|
||||
return rows
|
||||
|
||||
@api.model
|
||||
def get_reconciliation_proposition(self, account_id, partner_id=False):
|
||||
""" Returns two lines whose amount are opposite """
|
||||
|
||||
target_currency = (self.currency_id and self.amount_currency) and self.currency_id or self.company_id.currency_id
|
||||
partner_id_condition = partner_id and 'AND a.partner_id = %(partner_id)s' or ''
|
||||
|
||||
rec_prop = self.env['account.move.line']
|
||||
# Get pairs
|
||||
move_line_id = self.env.context.get('move_line_id', False)
|
||||
if move_line_id:
|
||||
move_line = self.env['account.move.line'].browse(move_line_id)
|
||||
amount = move_line.amount_residual;
|
||||
rec_prop = move_line
|
||||
query = """
|
||||
SELECT a.id, a.id FROM account_move_line a
|
||||
WHERE a.amount_residual = -%(amount)s
|
||||
AND NOT a.reconciled
|
||||
AND a.account_id = %(account_id)s
|
||||
AND a.id != %(move_line_id)s
|
||||
{partner_id_condition}
|
||||
ORDER BY a.date desc
|
||||
LIMIT 10
|
||||
""".format(**locals())
|
||||
else:
|
||||
partner_id_condition = partner_id_condition and partner_id_condition+' AND b.partner_id = %(partner_id)s' or ''
|
||||
query = """
|
||||
SELECT a.id, b.id
|
||||
FROM account_move_line a, account_move_line b
|
||||
WHERE a.amount_residual = -b.amount_residual
|
||||
AND NOT a.reconciled AND NOT b.reconciled
|
||||
AND a.account_id = %(account_id)s AND b.account_id = %(account_id)s
|
||||
{partner_id_condition}
|
||||
ORDER BY a.date desc
|
||||
LIMIT 10
|
||||
""".format(**locals())
|
||||
|
||||
self.env.cr.execute(query, locals())
|
||||
pairs = self.env.cr.fetchall()
|
||||
|
||||
# Apply ir_rules by filtering out
|
||||
all_pair_ids = [element for tupl in pairs for element in tupl]
|
||||
allowed_ids = set(self.env['account.move.line'].browse(all_pair_ids).ids)
|
||||
pairs = [pair for pair in pairs if pair[0] in allowed_ids and pair[1] in allowed_ids]
|
||||
|
||||
if len(pairs) > 0:
|
||||
rec_prop += self.browse(list(set(pairs[0])))
|
||||
|
||||
if len(rec_prop) > 0:
|
||||
# Return lines formatted
|
||||
return rec_prop.prepare_move_lines_for_reconciliation_widget(target_currency=target_currency)
|
||||
return []
|
||||
|
||||
@api.model
|
||||
def domain_move_lines_for_reconciliation(self, str):
|
||||
""" Returns the domain from the str search
|
||||
:param str: search string
|
||||
"""
|
||||
if not str:
|
||||
return []
|
||||
str_domain = [
|
||||
'|', ('move_id.name', 'ilike', str),
|
||||
'|', ('move_id.ref', 'ilike', str),
|
||||
'|', ('date_maturity', 'like', str),
|
||||
'&', ('name', '!=', '/'), ('name', 'ilike', str)
|
||||
]
|
||||
if str[0] in ['-', '+']:
|
||||
try:
|
||||
amounts_str = str.split('|')
|
||||
for amount_str in amounts_str:
|
||||
amount = amount_str[0] == '-' and float(amount_str) or float(amount_str[1:])
|
||||
amount_domain = [
|
||||
'|', ('amount_residual', '=', amount),
|
||||
'|', ('amount_residual_currency', '=', amount),
|
||||
'|', (amount_str[0] == '-' and 'credit' or 'debit', '=', float(amount_str[1:])),
|
||||
('amount_currency', '=', amount),
|
||||
]
|
||||
str_domain = expression.OR([str_domain, amount_domain])
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
amount = float(str)
|
||||
amount_domain = [
|
||||
'|', ('amount_residual', '=', amount),
|
||||
'|', ('amount_residual_currency', '=', amount),
|
||||
'|', ('amount_residual', '=', -amount),
|
||||
'|', ('amount_residual_currency', '=', -amount),
|
||||
'&', ('account_id.internal_type', '=', 'liquidity'),
|
||||
'|', '|', '|', ('debit', '=', amount), ('credit', '=', amount),
|
||||
('amount_currency', '=', amount),
|
||||
('amount_currency', '=', -amount),
|
||||
]
|
||||
str_domain = expression.OR([str_domain, amount_domain])
|
||||
except:
|
||||
pass
|
||||
return str_domain
|
||||
|
||||
def _domain_move_lines_for_manual_reconciliation(self, account_id, partner_id=False, excluded_ids=None, str=False):
|
||||
""" Create domain criteria that are relevant to manual reconciliation. """
|
||||
domain = ['&', ('reconciled', '=', False), ('account_id', '=', account_id)]
|
||||
if partner_id:
|
||||
domain = expression.AND([domain, [('partner_id', '=', partner_id)]])
|
||||
if excluded_ids:
|
||||
domain = expression.AND([[('id', 'not in', excluded_ids)], domain])
|
||||
if str:
|
||||
str_domain = self.domain_move_lines_for_reconciliation(str=str)
|
||||
domain = expression.AND([domain, str_domain])
|
||||
return domain
|
||||
|
||||
@api.model
|
||||
def get_move_lines_for_manual_reconciliation(self, account_id, partner_id=False, excluded_ids=None, str=False, offset=0, limit=None, target_currency_id=False):
|
||||
""" Returns unreconciled move lines for an account or a partner+account, formatted for the manual reconciliation widget """
|
||||
domain = self._domain_move_lines_for_manual_reconciliation(account_id, partner_id, excluded_ids, str)
|
||||
lines = self.search(domain, offset=offset, limit=limit, order="date_maturity desc, id desc")
|
||||
if target_currency_id:
|
||||
target_currency = self.env['res.currency'].browse(target_currency_id)
|
||||
else:
|
||||
account = self.env['account.account'].browse(account_id)
|
||||
target_currency = account.currency_id or account.company_id.currency_id
|
||||
return lines.prepare_move_lines_for_reconciliation_widget(target_currency=target_currency)
|
||||
|
||||
@api.multi
|
||||
def prepare_move_lines_for_reconciliation_widget(self, target_currency=False, target_date=False):
|
||||
""" Returns move lines formatted for the manual/bank reconciliation widget
|
||||
|
||||
:param target_currency: currency (Model or ID) you want the move line debit/credit converted into
|
||||
:param target_date: date to use for the monetary conversion
|
||||
"""
|
||||
context = dict(self._context or {})
|
||||
ret = []
|
||||
|
||||
if target_currency:
|
||||
# re-browse in case we were passed a currency ID via RPC call
|
||||
target_currency = self.env['res.currency'].browse(int(target_currency))
|
||||
|
||||
for line in self:
|
||||
company_currency = line.account_id.company_id.currency_id
|
||||
line_currency = (line.currency_id and line.amount_currency) and line.currency_id or company_currency
|
||||
ret_line = {
|
||||
'id': line.id,
|
||||
'name': line.name and line.name != '/' and line.move_id.name + ': ' + line.name or line.move_id.name,
|
||||
'ref': line.move_id.ref or '',
|
||||
# For reconciliation between statement transactions and already registered payments (eg. checks)
|
||||
# NB : we don't use the 'reconciled' field because the line we're selecting is not the one that gets reconciled
|
||||
'account_id': [line.account_id.id, line.account_id.display_name],
|
||||
'already_paid': line.account_id.internal_type == 'liquidity',
|
||||
'account_code': line.account_id.code,
|
||||
'account_name': line.account_id.name,
|
||||
'account_type': line.account_id.internal_type,
|
||||
'date_maturity': line.date_maturity,
|
||||
'date': line.date,
|
||||
'journal_id': [line.journal_id.id, line.journal_id.display_name],
|
||||
'partner_id': line.partner_id.id,
|
||||
'partner_name': line.partner_id.name,
|
||||
'currency_id': line_currency.id,
|
||||
}
|
||||
|
||||
debit = line.debit
|
||||
credit = line.credit
|
||||
amount = line.amount_residual
|
||||
amount_currency = line.amount_residual_currency
|
||||
|
||||
# For already reconciled lines, don't use amount_residual(_currency)
|
||||
if line.account_id.internal_type == 'liquidity':
|
||||
amount = debit - credit
|
||||
amount_currency = line.amount_currency
|
||||
|
||||
target_currency = target_currency or company_currency
|
||||
|
||||
ctx = context.copy()
|
||||
ctx.update({'date': target_date or line.date})
|
||||
# Use case:
|
||||
# Let's assume that company currency is in USD and that we have the 3 following move lines
|
||||
# Debit Credit Amount currency Currency
|
||||
# 1) 25 0 0 NULL
|
||||
# 2) 17 0 25 EUR
|
||||
# 3) 33 0 25 YEN
|
||||
#
|
||||
# If we ask to see the information in the reconciliation widget in company currency, we want to see
|
||||
# The following information
|
||||
# 1) 25 USD (no currency information)
|
||||
# 2) 17 USD [25 EUR] (show 25 euro in currency information, in the little bill)
|
||||
# 3) 33 USD [25 YEN] (show 25 yen in currency information)
|
||||
#
|
||||
# If we ask to see the information in another currency than the company let's say EUR
|
||||
# 1) 35 EUR [25 USD]
|
||||
# 2) 25 EUR (no currency information)
|
||||
# 3) 50 EUR [25 YEN]
|
||||
# In that case, we have to convert the debit-credit to the currency we want and we show next to it
|
||||
# the value of the amount_currency or the debit-credit if no amount currency
|
||||
if target_currency == company_currency:
|
||||
if line_currency == target_currency:
|
||||
amount = amount
|
||||
amount_currency = ""
|
||||
total_amount = debit - credit
|
||||
total_amount_currency = ""
|
||||
else:
|
||||
amount = amount
|
||||
amount_currency = amount_currency
|
||||
total_amount = debit - credit
|
||||
total_amount_currency = line.amount_currency
|
||||
|
||||
if target_currency != company_currency:
|
||||
if line_currency == target_currency:
|
||||
amount = amount_currency
|
||||
amount_currency = ""
|
||||
total_amount = line.amount_currency
|
||||
total_amount_currency = ""
|
||||
else:
|
||||
amount_currency = line.currency_id and amount_currency or amount
|
||||
amount = company_currency.with_context(ctx).compute(amount, target_currency)
|
||||
total_amount = company_currency.with_context(ctx).compute((line.debit - line.credit), target_currency)
|
||||
total_amount_currency = line.currency_id and line.amount_currency or (line.debit - line.credit)
|
||||
|
||||
ret_line['debit'] = amount > 0 and amount or 0
|
||||
ret_line['credit'] = amount < 0 and -amount or 0
|
||||
ret_line['amount_currency'] = amount_currency
|
||||
ret_line['amount_str'] = formatLang(self.env, abs(amount), currency_obj=target_currency)
|
||||
ret_line['total_amount_str'] = formatLang(self.env, abs(total_amount), currency_obj=target_currency)
|
||||
ret_line['amount_currency_str'] = amount_currency and formatLang(self.env, abs(amount_currency), currency_obj=line_currency) or ""
|
||||
ret_line['total_amount_currency_str'] = total_amount_currency and formatLang(self.env, abs(total_amount_currency), currency_obj=line_currency) or ""
|
||||
ret.append(ret_line)
|
||||
return ret
|
||||
|
||||
@api.model
|
||||
def process_reconciliations(self, data):
|
||||
""" Used to validate a batch of reconciliations in a single call
|
||||
:param data: list of dicts containing:
|
||||
- 'type': either 'partner' or 'account'
|
||||
- 'id': id of the affected res.partner or account.account
|
||||
- 'mv_line_ids': ids of existing account.move.line to reconcile
|
||||
- 'new_mv_line_dicts': list of dicts containing values suitable for account_move_line.create()
|
||||
"""
|
||||
for datum in data:
|
||||
if len(datum['mv_line_ids']) >= 1 or len(datum['mv_line_ids']) + len(datum['new_mv_line_dicts']) >= 2:
|
||||
self.browse(datum['mv_line_ids']).process_reconciliation(datum['new_mv_line_dicts'])
|
||||
|
||||
if datum['type'] == 'partner':
|
||||
partners = self.env['res.partner'].browse(datum['id'])
|
||||
partners.mark_as_reconciled()
|
||||
if datum['type'] == 'account':
|
||||
accounts = self.env['account.account'].browse(datum['id'])
|
||||
accounts.mark_as_reconciled()
|
||||
|
||||
@api.multi
|
||||
def process_reconciliation(self, new_mv_line_dicts):
|
||||
""" Create new move lines from new_mv_line_dicts (if not empty) then call reconcile_partial on self and new move lines
|
||||
|
||||
:param new_mv_line_dicts: list of dicts containing values suitable for account_move_line.create()
|
||||
"""
|
||||
if len(self) < 1 or len(self) + len(new_mv_line_dicts) < 2:
|
||||
raise UserError(_('A reconciliation must involve at least 2 move lines.'))
|
||||
|
||||
# Create writeoff move lines
|
||||
if len(new_mv_line_dicts) > 0:
|
||||
writeoff_lines = self.env['account.move.line']
|
||||
company_currency = self[0].account_id.company_id.currency_id
|
||||
writeoff_currency = self[0].currency_id or company_currency
|
||||
for mv_line_dict in new_mv_line_dicts:
|
||||
if writeoff_currency != company_currency:
|
||||
mv_line_dict['debit'] = writeoff_currency.compute(mv_line_dict['debit'], company_currency)
|
||||
mv_line_dict['credit'] = writeoff_currency.compute(mv_line_dict['credit'], company_currency)
|
||||
writeoff_lines += self._create_writeoff(mv_line_dict)
|
||||
|
||||
(self + writeoff_lines).reconcile()
|
||||
else:
|
||||
self.reconcile()
|
||||
|
||||
####################################################
|
||||
# Reconciliation methods
|
||||
####################################################
|
||||
|
||||
@@ -0,0 +1,776 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import api, models, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import pycompat
|
||||
from odoo.tools import float_round, float_repr
|
||||
from odoo.tools.misc import formatLang
|
||||
|
||||
|
||||
class AccountReconciliation(models.AbstractModel):
|
||||
_name = 'account.reconciliation.widget'
|
||||
|
||||
####################################################
|
||||
# Public
|
||||
####################################################
|
||||
|
||||
@api.model
|
||||
# model: 'account.bank.statement.line',
|
||||
# method: 'reconciliation_widget_auto_reconcile',
|
||||
def auto_reconcile(self, st_line_ids, num_already_reconciled_lines=0):
|
||||
""" Use statement line auto_reconcile method and return the details
|
||||
to the widget interface.
|
||||
|
||||
:param st_line_ids: ids of the statement lines to reconciliate
|
||||
:param num_already_reconciled_lines: number of reconcilied lines
|
||||
used to increment the progress bar in the interface in function
|
||||
of the real number of reconcilied lines.
|
||||
"""
|
||||
st_lines = self.env['account.bank.statement.line'].browse(st_line_ids)
|
||||
automatic_reconciliation_entries = self.env['account.bank.statement.line']
|
||||
unreconciled = self.env['account.bank.statement.line']
|
||||
|
||||
for stl in st_lines:
|
||||
res = stl.auto_reconcile()
|
||||
if res:
|
||||
automatic_reconciliation_entries += stl
|
||||
else:
|
||||
unreconciled += stl
|
||||
|
||||
# Collect various information for the reconciliation widget
|
||||
notifications = []
|
||||
num_auto_reconciled = len(automatic_reconciliation_entries)
|
||||
if num_auto_reconciled > 0:
|
||||
auto_reconciled_message = num_auto_reconciled > 1 \
|
||||
and _("%d transactions were automatically reconciled.") % num_auto_reconciled \
|
||||
or _("1 transaction was automatically reconciled.")
|
||||
notifications += [{
|
||||
'type': 'info',
|
||||
'message': auto_reconciled_message,
|
||||
'details': {
|
||||
'name': _("Automatically reconciled items"),
|
||||
'model': 'account.move',
|
||||
'ids': automatic_reconciliation_entries.mapped('journal_entry_ids').mapped('move_id').ids
|
||||
}
|
||||
}]
|
||||
return {
|
||||
'st_lines_ids': unreconciled.ids,
|
||||
'notifications': notifications,
|
||||
'statement_name': False,
|
||||
'num_already_reconciled_lines': num_auto_reconciled + num_already_reconciled_lines,
|
||||
}
|
||||
|
||||
@api.model
|
||||
# model: 'account.bank.statement.line',
|
||||
# method: 'process_reconciliations',
|
||||
def process_bank_statement_line(self, st_line_ids, data):
|
||||
""" Handles data sent from the bank statement reconciliation widget
|
||||
(and can otherwise serve as an old-API bridge)
|
||||
|
||||
:param st_line_ids
|
||||
:param list of dicts data: must contains the keys
|
||||
'counterpart_aml_dicts', 'payment_aml_ids' and 'new_aml_dicts',
|
||||
whose value is the same as described in process_reconciliation
|
||||
except that ids are used instead of recordsets.
|
||||
"""
|
||||
st_lines = self.env['account.bank.statement.line'].browse(st_line_ids)
|
||||
AccountMoveLine = self.env['account.move.line']
|
||||
ctx = dict(self._context, force_price_include=False)
|
||||
|
||||
for st_line, datum in pycompat.izip(st_lines, data):
|
||||
payment_aml_rec = AccountMoveLine.browse(datum.get('payment_aml_ids', []))
|
||||
|
||||
for aml_dict in datum.get('counterpart_aml_dicts', []):
|
||||
aml_dict['move_line'] = AccountMoveLine.browse(aml_dict['counterpart_aml_id'])
|
||||
del aml_dict['counterpart_aml_id']
|
||||
|
||||
if datum.get('partner_id') is not None:
|
||||
st_line.write({'partner_id': datum['partner_id']})
|
||||
|
||||
st_line.with_context(ctx).process_reconciliation(
|
||||
datum.get('counterpart_aml_dicts', []),
|
||||
payment_aml_rec,
|
||||
datum.get('new_aml_dicts', []))
|
||||
|
||||
@api.model
|
||||
# model: 'account.bank.statement.line',
|
||||
# method: 'get_move_lines_for_reconciliation_widget',
|
||||
def get_move_lines_for_bank_statement_line(self, st_line_id, partner_id=None, excluded_ids=None, str=False, offset=0, limit=None):
|
||||
""" Returns move lines for the bank statement reconciliation widget,
|
||||
formatted as a list of dicts
|
||||
|
||||
:param st_line_id: ids of the statement lines
|
||||
:param partner_id: optional partner id to select only the moves
|
||||
line corresponding to the partner
|
||||
:param excluded_ids: optional move lines ids excluded from the
|
||||
result
|
||||
:param str: optional search (can be the amout, display_name,
|
||||
partner name, move line name)
|
||||
:param offset: offset of the search result (to display pager)
|
||||
:param limit: number of the result to search
|
||||
"""
|
||||
st_line = self.env['account.bank.statement.line'].browse(st_line_id)
|
||||
|
||||
# Blue lines = payment on bank account not assigned to a statement yet
|
||||
aml_accounts = [
|
||||
st_line.journal_id.default_credit_account_id.id,
|
||||
st_line.journal_id.default_debit_account_id.id
|
||||
]
|
||||
|
||||
if partner_id is None:
|
||||
partner_id = st_line.partner_id.id
|
||||
|
||||
domain = self._domain_move_lines_for_reconciliation(aml_accounts, partner_id, excluded_ids=excluded_ids, str=str)
|
||||
aml_recs = self.env['account.move.line'].search(domain, offset=offset, limit=limit, order="date_maturity desc, id desc")
|
||||
target_currency = st_line.currency_id or st_line.journal_id.currency_id or st_line.journal_id.company_id.currency_id
|
||||
return self._prepare_move_lines(aml_recs, target_currency=target_currency, target_date=st_line.date)
|
||||
|
||||
@api.model
|
||||
# model: 'account.bank.statement.line',
|
||||
# method: 'get_data_for_reconciliation_widget',
|
||||
def get_bank_statement_line_data(self, st_line_ids, excluded_ids=None):
|
||||
""" Returns the data required to display a reconciliation widget, for
|
||||
each statement line in self
|
||||
|
||||
:param st_line_id: ids of the statement lines
|
||||
:param excluded_ids: optional move lines ids excluded from the
|
||||
result
|
||||
"""
|
||||
excluded_ids = excluded_ids or []
|
||||
ret = []
|
||||
st_lines = self.env['account.bank.statement.line'].browse(st_line_ids)
|
||||
|
||||
for st_line in st_lines:
|
||||
aml_recs = self._get_statement_line_reconciliation_proposition(st_line, excluded_ids=excluded_ids)
|
||||
target_currency = st_line.currency_id or st_line.journal_id.currency_id or st_line.journal_id.company_id.currency_id
|
||||
rp = self._prepare_move_lines(aml_recs, target_currency=target_currency, target_date=st_line.date)
|
||||
excluded_ids += [move_line['id'] for move_line in rp]
|
||||
ret.append({
|
||||
'st_line': self._get_statement_line(st_line),
|
||||
'reconciliation_proposition': rp
|
||||
})
|
||||
return ret
|
||||
|
||||
@api.model
|
||||
# model: 'account.bank.statement',
|
||||
# method: 'reconciliation_widget_preprocess',
|
||||
def get_bank_statement_data(self, bank_statement_ids):
|
||||
""" Get statement lines of the specified statements or all unreconciled
|
||||
statement lines and try to automatically reconcile them / find them
|
||||
a partner.
|
||||
Return ids of statement lines left to reconcile and other data for
|
||||
the reconciliation widget.
|
||||
|
||||
:param st_line_id: ids of the bank statement
|
||||
"""
|
||||
bank_statements = self.env['account.bank.statement'].browse(bank_statement_ids)
|
||||
Bank_statement_line = self.env['account.bank.statement.line']
|
||||
|
||||
# NB : The field account_id can be used at the statement line creation/import to avoid the reconciliation process on it later on,
|
||||
# this is why we filter out statements lines where account_id is set
|
||||
|
||||
sql_query = """SELECT stl.id
|
||||
FROM account_bank_statement_line stl
|
||||
WHERE account_id IS NULL AND stl.amount != 0.0 AND not exists (select 1 from account_move_line aml where aml.statement_line_id = stl.id)
|
||||
AND company_id = %s
|
||||
"""
|
||||
params = (self.env.user.company_id.id,)
|
||||
if bank_statements:
|
||||
sql_query += ' AND stl.statement_id IN %s'
|
||||
params += (tuple(bank_statements.ids),)
|
||||
sql_query += ' ORDER BY stl.id'
|
||||
self.env.cr.execute(sql_query, params)
|
||||
st_lines_left = Bank_statement_line.browse([line.get('id') for line in self.env.cr.dictfetchall()])
|
||||
|
||||
#try to assign partner to bank_statement_line
|
||||
stl_to_assign = st_lines_left.filtered(lambda stl: not stl.partner_id)
|
||||
refs = set(stl_to_assign.mapped('name'))
|
||||
if stl_to_assign and refs\
|
||||
and st_lines_left[0].journal_id.default_credit_account_id\
|
||||
and st_lines_left[0].journal_id.default_debit_account_id:
|
||||
|
||||
sql_query = """SELECT aml.partner_id, aml.ref, stl.id
|
||||
FROM account_move_line aml
|
||||
JOIN account_account acc ON acc.id = aml.account_id
|
||||
JOIN account_bank_statement_line stl ON aml.ref = stl.name
|
||||
WHERE (aml.company_id = %s
|
||||
AND aml.partner_id IS NOT NULL)
|
||||
AND (
|
||||
(aml.statement_id IS NULL AND aml.account_id IN %s)
|
||||
OR
|
||||
(acc.internal_type IN ('payable', 'receivable') AND aml.reconciled = false)
|
||||
)
|
||||
AND aml.ref IN %s
|
||||
"""
|
||||
params = (self.env.user.company_id.id, (st_lines_left[0].journal_id.default_credit_account_id.id, st_lines_left[0].journal_id.default_debit_account_id.id), tuple(refs))
|
||||
if bank_statements:
|
||||
sql_query += 'AND stl.id IN %s'
|
||||
params += (tuple(stl_to_assign.ids),)
|
||||
self.env.cr.execute(sql_query, params)
|
||||
results = self.env.cr.dictfetchall()
|
||||
for line in results:
|
||||
Bank_statement_line.browse(line.get('id')).write({'partner_id': line.get('partner_id')})
|
||||
|
||||
return {
|
||||
'st_lines_ids': st_lines_left.ids,
|
||||
'notifications': [],
|
||||
'statement_name': len(bank_statements) == 1 and bank_statements[0].name or False,
|
||||
'journal_id': bank_statements and bank_statements[0].journal_id.id or False,
|
||||
'num_already_reconciled_lines': 0,
|
||||
}
|
||||
|
||||
@api.model
|
||||
# model: 'account.move.line',
|
||||
# method: 'get_move_lines_for_manual_reconciliation',
|
||||
def get_move_lines_for_manual_reconciliation(self, account_id, partner_id=False, excluded_ids=None, str=False, offset=0, limit=None, target_currency_id=False):
|
||||
""" Returns unreconciled move lines for an account or a partner+account, formatted for the manual reconciliation widget """
|
||||
|
||||
Account_move_line = self.env['account.move.line']
|
||||
Account = self.env['account.account']
|
||||
Currency = self.env['res.currency']
|
||||
|
||||
domain = self._domain_move_lines_for_manual_reconciliation(account_id, partner_id, excluded_ids, str)
|
||||
lines = Account_move_line.search(domain, offset=offset, limit=limit, order="date_maturity desc, id desc")
|
||||
if target_currency_id:
|
||||
target_currency = Currency.browse(target_currency_id)
|
||||
else:
|
||||
account = Account.browse(account_id)
|
||||
target_currency = account.currency_id or account.company_id.currency_id
|
||||
return self._prepare_move_lines(lines, target_currency=target_currency)
|
||||
|
||||
@api.model
|
||||
# model: 'account.move.line',
|
||||
# method: 'get_data_for_manual_reconciliation_widget',
|
||||
def get_all_data_for_manual_reconciliation(self, partner_ids, account_ids):
|
||||
""" Returns the data required for the invoices & payments matching of partners/accounts.
|
||||
If an argument is None, fetch all related reconciliations. Use [] to fetch nothing.
|
||||
"""
|
||||
return {
|
||||
'customers': self.get_data_for_manual_reconciliation('partner', partner_ids, 'receivable'),
|
||||
'suppliers': self.get_data_for_manual_reconciliation('partner', partner_ids, 'payable'),
|
||||
'accounts': self.get_data_for_manual_reconciliation('account', account_ids),
|
||||
}
|
||||
|
||||
@api.model
|
||||
# model: 'account.move.line',
|
||||
# method: 'get_data_for_manual_reconciliation',
|
||||
def get_data_for_manual_reconciliation(self, res_type, res_ids=None, account_type=None):
|
||||
""" Returns the data required for the invoices & payments matching of partners/accounts (list of dicts).
|
||||
If no res_ids is passed, returns data for all partners/accounts that can be reconciled.
|
||||
|
||||
:param res_type: either 'partner' or 'account'
|
||||
:param res_ids: ids of the partners/accounts to reconcile, use None to fetch data indiscriminately
|
||||
of the id, use [] to prevent from fetching any data at all.
|
||||
:param account_type: if a partner is both customer and vendor, you can use 'payable' to reconcile
|
||||
the vendor-related journal entries and 'receivable' for the customer-related entries.
|
||||
"""
|
||||
|
||||
Account = self.env['account.account']
|
||||
Partner = self.env['res.partner']
|
||||
|
||||
if res_ids is not None and len(res_ids) == 0:
|
||||
# Note : this short-circuiting is better for performances, but also required
|
||||
# since postgresql doesn't implement empty list (so 'AND id in ()' is useless)
|
||||
return []
|
||||
res_ids = res_ids and tuple(res_ids)
|
||||
|
||||
assert res_type in ('partner', 'account')
|
||||
assert account_type in ('payable', 'receivable', None)
|
||||
is_partner = res_type == 'partner'
|
||||
res_alias = is_partner and 'p' or 'a'
|
||||
|
||||
query = ("""
|
||||
SELECT {0} account_id, account_name, account_code, max_date,
|
||||
to_char(last_time_entries_checked, 'YYYY-MM-DD') AS last_time_entries_checked
|
||||
FROM (
|
||||
SELECT {1}
|
||||
{res_alias}.last_time_entries_checked AS last_time_entries_checked,
|
||||
a.id AS account_id,
|
||||
a.name AS account_name,
|
||||
a.code AS account_code,
|
||||
MAX(l.write_date) AS max_date
|
||||
FROM
|
||||
account_move_line l
|
||||
RIGHT JOIN account_account a ON (a.id = l.account_id)
|
||||
RIGHT JOIN account_account_type at ON (at.id = a.user_type_id)
|
||||
{2}
|
||||
WHERE
|
||||
a.reconcile IS TRUE
|
||||
AND l.full_reconcile_id is NULL
|
||||
{3}
|
||||
{4}
|
||||
{5}
|
||||
AND l.company_id = {6}
|
||||
AND EXISTS (
|
||||
SELECT NULL
|
||||
FROM account_move_line l
|
||||
WHERE l.account_id = a.id
|
||||
{7}
|
||||
AND l.amount_residual > 0
|
||||
)
|
||||
AND EXISTS (
|
||||
SELECT NULL
|
||||
FROM account_move_line l
|
||||
WHERE l.account_id = a.id
|
||||
{7}
|
||||
AND l.amount_residual < 0
|
||||
)
|
||||
GROUP BY {8} a.id, a.name, a.code, {res_alias}.last_time_entries_checked
|
||||
ORDER BY {res_alias}.last_time_entries_checked
|
||||
) as s
|
||||
WHERE (last_time_entries_checked IS NULL OR max_date > last_time_entries_checked)
|
||||
""".format(
|
||||
is_partner and 'partner_id, partner_name,' or ' ',
|
||||
is_partner and 'p.id AS partner_id, p.name AS partner_name,' or ' ',
|
||||
is_partner and 'RIGHT JOIN res_partner p ON (l.partner_id = p.id)' or ' ',
|
||||
is_partner and ' ' or "AND at.type <> 'payable' AND at.type <> 'receivable'",
|
||||
account_type and "AND at.type = %(account_type)s" or '',
|
||||
res_ids and 'AND ' + res_alias + '.id in %(res_ids)s' or '',
|
||||
self.env.user.company_id.id,
|
||||
is_partner and 'AND l.partner_id = p.id' or ' ',
|
||||
is_partner and 'l.partner_id, p.id,' or ' ',
|
||||
res_alias=res_alias
|
||||
))
|
||||
self.env.cr.execute(query, locals())
|
||||
|
||||
# Apply ir_rules by filtering out
|
||||
rows = self.env.cr.dictfetchall()
|
||||
ids = [x['account_id'] for x in rows]
|
||||
allowed_ids = set(Account.browse(ids).ids)
|
||||
rows = [row for row in rows if row['account_id'] in allowed_ids]
|
||||
if is_partner:
|
||||
ids = [x['partner_id'] for x in rows]
|
||||
allowed_ids = set(Partner.browse(ids).ids)
|
||||
rows = [row for row in rows if row['partner_id'] in allowed_ids]
|
||||
|
||||
# Fetch other data
|
||||
for row in rows:
|
||||
account = Account.browse(row['account_id'])
|
||||
currency = account.currency_id or account.company_id.currency_id
|
||||
row['currency_id'] = currency.id
|
||||
partner_id = is_partner and row['partner_id'] or None
|
||||
rec_prop = self._get_move_line_reconciliation_proposition(account.id, partner_id)
|
||||
row['reconciliation_proposition'] = self._prepare_move_lines(rec_prop, target_currency=currency)
|
||||
return rows
|
||||
|
||||
@api.model
|
||||
# model: 'account.move.line',
|
||||
# method: 'process_reconciliations',
|
||||
def process_move_lines(self, move_line_ids, data):
|
||||
""" Used to validate a batch of reconciliations in a single call
|
||||
:param data: list of dicts containing:
|
||||
- 'type': either 'partner' or 'account'
|
||||
- 'id': id of the affected res.partner or account.account
|
||||
- 'mv_line_ids': ids of existing account.move.line to reconcile
|
||||
- 'new_mv_line_dicts': list of dicts containing values suitable for account_move_line.create()
|
||||
"""
|
||||
|
||||
Partner = self.env['res.partner']
|
||||
Account = self.env['account.account']
|
||||
|
||||
for datum in data:
|
||||
if len(datum['mv_line_ids']) >= 1 or len(datum['mv_line_ids']) + len(datum['new_mv_line_dicts']) >= 2:
|
||||
self._process_move_lines(datum['mv_line_ids'], datum['new_mv_line_dicts'])
|
||||
|
||||
if datum['type'] == 'partner':
|
||||
partners = Partner.browse(datum['id'])
|
||||
partners.mark_as_reconciled()
|
||||
if datum['type'] == 'account':
|
||||
accounts = Account.browse(datum['id'])
|
||||
accounts.mark_as_reconciled()
|
||||
|
||||
####################################################
|
||||
# Private
|
||||
####################################################
|
||||
|
||||
@api.model
|
||||
# domain_move_lines_for_reconciliation
|
||||
def _domain_move_lines(self, str):
|
||||
""" Returns the domain from the str search
|
||||
:param str: search string
|
||||
"""
|
||||
if not str:
|
||||
return []
|
||||
str_domain = [
|
||||
'|', ('move_id.name', 'ilike', str),
|
||||
'|', ('move_id.ref', 'ilike', str),
|
||||
'|', ('date_maturity', 'like', str),
|
||||
'&', ('name', '!=', '/'), ('name', 'ilike', str)
|
||||
]
|
||||
if str[0] in ['-', '+']:
|
||||
try:
|
||||
amounts_str = str.split('|')
|
||||
for amount_str in amounts_str:
|
||||
amount = amount_str[0] == '-' and float(amount_str) or float(amount_str[1:])
|
||||
amount_domain = [
|
||||
'|', ('amount_residual', '=', amount),
|
||||
'|', ('amount_residual_currency', '=', amount),
|
||||
'|', (amount_str[0] == '-' and 'credit' or 'debit', '=', float(amount_str[1:])),
|
||||
('amount_currency', '=', amount),
|
||||
]
|
||||
str_domain = expression.OR([str_domain, amount_domain])
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
amount = float(str)
|
||||
amount_domain = [
|
||||
'|', ('amount_residual', '=', amount),
|
||||
'|', ('amount_residual_currency', '=', amount),
|
||||
'|', ('amount_residual', '=', -amount),
|
||||
'|', ('amount_residual_currency', '=', -amount),
|
||||
'&', ('account_id.internal_type', '=', 'liquidity'),
|
||||
'|', '|', '|', ('debit', '=', amount), ('credit', '=', amount),
|
||||
('amount_currency', '=', amount),
|
||||
('amount_currency', '=', -amount),
|
||||
]
|
||||
str_domain = expression.OR([str_domain, amount_domain])
|
||||
except:
|
||||
pass
|
||||
return str_domain
|
||||
|
||||
@api.model
|
||||
# get_move_lines_for_reconciliation (now return the domain)
|
||||
def _domain_move_lines_for_reconciliation(self, aml_accounts, partner_id, excluded_ids=None, str=False):
|
||||
""" Return the domain for account.move.line records which can be used for bank statement reconciliation.
|
||||
|
||||
:param aml_accounts:
|
||||
:param partner_id:
|
||||
:param excluded_ids:
|
||||
:param str:
|
||||
"""
|
||||
|
||||
domain_reconciliation = [
|
||||
'&', '&',
|
||||
('statement_line_id', '=', False),
|
||||
('account_id', 'in', aml_accounts),
|
||||
('payment_id', '<>', False)
|
||||
]
|
||||
|
||||
# Black lines = unreconciled & (not linked to a payment or open balance created by statement
|
||||
domain_matching = [('reconciled', '=', False)]
|
||||
if partner_id:
|
||||
domain_matching = expression.AND([
|
||||
domain_matching,
|
||||
[('account_id.internal_type', 'in', ['payable', 'receivable'])]
|
||||
])
|
||||
else:
|
||||
# TODO : find out what use case this permits (match a check payment, registered on a journal whose account type is other instead of liquidity)
|
||||
domain_matching = expression.AND([
|
||||
domain_matching,
|
||||
[('account_id.reconcile', '=', True)]
|
||||
])
|
||||
|
||||
# Let's add what applies to both
|
||||
domain = expression.OR([domain_reconciliation, domain_matching])
|
||||
if partner_id:
|
||||
domain = expression.AND([domain, [('partner_id', '=', partner_id)]])
|
||||
|
||||
# Domain factorized for all reconciliation use cases
|
||||
if str:
|
||||
str_domain = self._domain_move_lines(str=str)
|
||||
if not partner_id:
|
||||
str_domain = expression.OR([
|
||||
str_domain,
|
||||
[('partner_id.name', 'ilike', str)]
|
||||
])
|
||||
domain = expression.AND([
|
||||
domain,
|
||||
str_domain
|
||||
])
|
||||
|
||||
if excluded_ids:
|
||||
domain = expression.AND([
|
||||
[('id', 'not in', excluded_ids)],
|
||||
domain
|
||||
])
|
||||
return domain
|
||||
|
||||
@api.model
|
||||
# _domain_move_lines_for_manual_reconciliation
|
||||
def _domain_move_lines_for_manual_reconciliation(self, account_id, partner_id=False, excluded_ids=None, str=False):
|
||||
""" Create domain criteria that are relevant to manual reconciliation. """
|
||||
domain = ['&', ('reconciled', '=', False), ('account_id', '=', account_id)]
|
||||
if partner_id:
|
||||
domain = expression.AND([domain, [('partner_id', '=', partner_id)]])
|
||||
if excluded_ids:
|
||||
domain = expression.AND([[('id', 'not in', excluded_ids)], domain])
|
||||
if str:
|
||||
str_domain = self._domain_move_lines(str=str)
|
||||
domain = expression.AND([domain, str_domain])
|
||||
return domain
|
||||
|
||||
@api.model
|
||||
# prepare_move_lines_for_reconciliation_widget
|
||||
def _prepare_move_lines(self, move_lines, target_currency=False, target_date=False):
|
||||
""" Returns move lines formatted for the manual/bank reconciliation widget
|
||||
|
||||
:param move_line_ids:
|
||||
:param target_currency: currency (browse) you want the move line debit/credit converted into
|
||||
:param target_date: date to use for the monetary conversion
|
||||
"""
|
||||
context = dict(self._context or {})
|
||||
ret = []
|
||||
|
||||
for line in move_lines:
|
||||
company_currency = line.account_id.company_id.currency_id
|
||||
line_currency = (line.currency_id and line.amount_currency) and line.currency_id or company_currency
|
||||
ret_line = {
|
||||
'id': line.id,
|
||||
'name': line.name and line.name != '/' and line.move_id.name + ': ' + line.name or line.move_id.name,
|
||||
'ref': line.move_id.ref or '',
|
||||
# For reconciliation between statement transactions and already registered payments (eg. checks)
|
||||
# NB : we don't use the 'reconciled' field because the line we're selecting is not the one that gets reconciled
|
||||
'account_id': [line.account_id.id, line.account_id.display_name],
|
||||
'already_paid': line.account_id.internal_type == 'liquidity',
|
||||
'account_code': line.account_id.code,
|
||||
'account_name': line.account_id.name,
|
||||
'account_type': line.account_id.internal_type,
|
||||
'date_maturity': line.date_maturity,
|
||||
'date': line.date,
|
||||
'journal_id': [line.journal_id.id, line.journal_id.display_name],
|
||||
'partner_id': line.partner_id.id,
|
||||
'partner_name': line.partner_id.name,
|
||||
'currency_id': line_currency.id,
|
||||
}
|
||||
|
||||
debit = line.debit
|
||||
credit = line.credit
|
||||
amount = line.amount_residual
|
||||
amount_currency = line.amount_residual_currency
|
||||
|
||||
# For already reconciled lines, don't use amount_residual(_currency)
|
||||
if line.account_id.internal_type == 'liquidity':
|
||||
amount = debit - credit
|
||||
amount_currency = line.amount_currency
|
||||
|
||||
target_currency = target_currency or company_currency
|
||||
|
||||
ctx = context.copy()
|
||||
ctx.update({'date': target_date or line.date})
|
||||
# Use case:
|
||||
# Let's assume that company currency is in USD and that we have the 3 following move lines
|
||||
# Debit Credit Amount currency Currency
|
||||
# 1) 25 0 0 NULL
|
||||
# 2) 17 0 25 EUR
|
||||
# 3) 33 0 25 YEN
|
||||
#
|
||||
# If we ask to see the information in the reconciliation widget in company currency, we want to see
|
||||
# The following information
|
||||
# 1) 25 USD (no currency information)
|
||||
# 2) 17 USD [25 EUR] (show 25 euro in currency information, in the little bill)
|
||||
# 3) 33 USD [25 YEN] (show 25 yen in currency information)
|
||||
#
|
||||
# If we ask to see the information in another currency than the company let's say EUR
|
||||
# 1) 35 EUR [25 USD]
|
||||
# 2) 25 EUR (no currency information)
|
||||
# 3) 50 EUR [25 YEN]
|
||||
# In that case, we have to convert the debit-credit to the currency we want and we show next to it
|
||||
# the value of the amount_currency or the debit-credit if no amount currency
|
||||
if target_currency == company_currency:
|
||||
if line_currency == target_currency:
|
||||
amount = amount
|
||||
amount_currency = ""
|
||||
total_amount = debit - credit
|
||||
total_amount_currency = ""
|
||||
else:
|
||||
amount = amount
|
||||
amount_currency = amount_currency
|
||||
total_amount = debit - credit
|
||||
total_amount_currency = line.amount_currency
|
||||
|
||||
if target_currency != company_currency:
|
||||
if line_currency == target_currency:
|
||||
amount = amount_currency
|
||||
amount_currency = ""
|
||||
total_amount = line.amount_currency
|
||||
total_amount_currency = ""
|
||||
else:
|
||||
amount_currency = line.currency_id and amount_currency or amount
|
||||
amount = company_currency.with_context(ctx).compute(amount, target_currency)
|
||||
total_amount = company_currency.with_context(ctx).compute((line.debit - line.credit), target_currency)
|
||||
total_amount_currency = line.currency_id and line.amount_currency or (line.debit - line.credit)
|
||||
|
||||
ret_line['debit'] = amount > 0 and amount or 0
|
||||
ret_line['credit'] = amount < 0 and -amount or 0
|
||||
ret_line['amount_currency'] = amount_currency
|
||||
ret_line['amount_str'] = formatLang(self.env, abs(amount), currency_obj=target_currency)
|
||||
ret_line['total_amount_str'] = formatLang(self.env, abs(total_amount), currency_obj=target_currency)
|
||||
ret_line['amount_currency_str'] = amount_currency and formatLang(self.env, abs(amount_currency), currency_obj=line_currency) or ""
|
||||
ret_line['total_amount_currency_str'] = total_amount_currency and formatLang(self.env, abs(total_amount_currency), currency_obj=line_currency) or ""
|
||||
ret.append(ret_line)
|
||||
return ret
|
||||
|
||||
@api.model
|
||||
# get_statement_line_for_reconciliation_widget
|
||||
def _get_statement_line(self, st_line):
|
||||
""" Returns the data required by the bank statement reconciliation widget to display a statement line """
|
||||
|
||||
statement_currency = st_line.journal_id.currency_id or st_line.journal_id.company_id.currency_id
|
||||
if st_line.amount_currency and st_line.currency_id:
|
||||
amount = st_line.amount_currency
|
||||
amount_currency = st_line.amount
|
||||
amount_currency_str = formatLang(self.env, abs(amount_currency), currency_obj=statement_currency)
|
||||
else:
|
||||
amount = st_line.amount
|
||||
amount_currency = amount
|
||||
amount_currency_str = ""
|
||||
amount_str = formatLang(self.env, abs(amount), currency_obj=st_line.currency_id or statement_currency)
|
||||
|
||||
data = {
|
||||
'id': st_line.id,
|
||||
'ref': st_line.ref,
|
||||
'note': st_line.note or "",
|
||||
'name': st_line.name,
|
||||
'date': st_line.date,
|
||||
'amount': amount,
|
||||
'amount_str': amount_str, # Amount in the statement line currency
|
||||
'currency_id': st_line.currency_id.id or statement_currency.id,
|
||||
'partner_id': st_line.partner_id.id,
|
||||
'journal_id': st_line.journal_id.id,
|
||||
'statement_id': st_line.statement_id.id,
|
||||
'account_id': [st_line.journal_id.default_debit_account_id.id, st_line.journal_id.default_debit_account_id.display_name],
|
||||
'account_code': st_line.journal_id.default_debit_account_id.code,
|
||||
'account_name': st_line.journal_id.default_debit_account_id.name,
|
||||
'partner_name': st_line.partner_id.name,
|
||||
'communication_partner_name': st_line.partner_name,
|
||||
'amount_currency_str': amount_currency_str, # Amount in the statement currency
|
||||
'amount_currency': amount_currency, # Amount in the statement currency
|
||||
'has_no_partner': not st_line.partner_id.id,
|
||||
}
|
||||
if st_line.partner_id:
|
||||
if amount > 0:
|
||||
data['open_balance_account_id'] = st_line.partner_id.property_account_receivable_id.id
|
||||
else:
|
||||
data['open_balance_account_id'] = st_line.partner_id.property_account_payable_id.id
|
||||
|
||||
return data
|
||||
|
||||
@api.model
|
||||
# get_reconciliation_proposition
|
||||
def _get_statement_line_reconciliation_proposition(self, st_line, excluded_ids=None):
|
||||
""" Returns move lines that constitute the best guess to reconcile a statement line
|
||||
Note: it only looks for move lines in the same currency as the statement line.
|
||||
"""
|
||||
Account_move_line = self.env['account.move.line']
|
||||
|
||||
if not excluded_ids:
|
||||
excluded_ids = []
|
||||
amount = st_line.amount_currency or st_line.amount
|
||||
company_currency = st_line.journal_id.company_id.currency_id
|
||||
st_line_currency = st_line.currency_id or st_line.journal_id.currency_id
|
||||
currency = (st_line_currency and st_line_currency != company_currency) and st_line_currency.id or False
|
||||
precision = st_line_currency and st_line_currency.decimal_places or company_currency.decimal_places
|
||||
params = {'company_id': self.env.user.company_id.id,
|
||||
'account_payable_receivable': (st_line.journal_id.default_credit_account_id.id, st_line.journal_id.default_debit_account_id.id),
|
||||
'amount': float_repr(float_round(amount, precision_digits=precision), precision_digits=precision),
|
||||
'partner_id': st_line.partner_id.id,
|
||||
'excluded_ids': tuple(excluded_ids),
|
||||
'ref': st_line.name,
|
||||
}
|
||||
# Look for structured communication match
|
||||
if st_line.name:
|
||||
add_to_select = ", CASE WHEN aml.ref = %(ref)s THEN 1 ELSE 2 END as temp_field_order "
|
||||
add_to_from = " JOIN account_move m ON m.id = aml.move_id "
|
||||
select_clause, from_clause, where_clause = st_line._get_common_sql_query(overlook_partner=True, excluded_ids=excluded_ids, split=True)
|
||||
sql_query = select_clause + add_to_select + from_clause + add_to_from + where_clause
|
||||
sql_query += " AND (aml.ref= %(ref)s or m.name = %(ref)s) \
|
||||
ORDER BY temp_field_order, date_maturity desc, aml.id desc"
|
||||
self.env.cr.execute(sql_query, params)
|
||||
results = self.env.cr.fetchone()
|
||||
if results:
|
||||
return Account_move_line.browse(results[0])
|
||||
|
||||
# Look for a single move line with the same amount
|
||||
field = currency and 'amount_residual_currency' or 'amount_residual'
|
||||
liquidity_field = currency and 'amount_currency' or amount > 0 and 'debit' or 'credit'
|
||||
liquidity_amt_clause = currency and '%(amount)s::numeric' or 'abs(%(amount)s::numeric)'
|
||||
sql_query = st_line._get_common_sql_query(excluded_ids=excluded_ids) + \
|
||||
" AND ("+field+" = %(amount)s::numeric OR (acc.internal_type = 'liquidity' AND "+liquidity_field+" = " + liquidity_amt_clause + ")) \
|
||||
ORDER BY date_maturity desc, aml.id desc LIMIT 1"
|
||||
self.env.cr.execute(sql_query, params)
|
||||
results = self.env.cr.fetchone()
|
||||
if results:
|
||||
return Account_move_line.browse(results[0])
|
||||
|
||||
return Account_move_line
|
||||
|
||||
@api.model
|
||||
# get_reconciliation_proposition
|
||||
def _get_move_line_reconciliation_proposition(self, account_id, partner_id=False):
|
||||
""" Returns two lines whose amount are opposite """
|
||||
|
||||
Account_move_line = self.env['account.move.line']
|
||||
rec_prop = self.env['account.move.line']
|
||||
|
||||
partner_id_condition = partner_id and 'AND a.partner_id = %(partner_id)s' or ''
|
||||
|
||||
# Get pairs
|
||||
move_line_id = self.env.context.get('move_line_id', False) # see open_payment_matching_screen in account.payment
|
||||
if move_line_id:
|
||||
move_line = Account_move_line.browse(move_line_id)
|
||||
amount = move_line.amount_residual;
|
||||
rec_prop = move_line
|
||||
query = """
|
||||
SELECT a.id, a.id FROM account_move_line a
|
||||
WHERE a.amount_residual = -%(amount)s
|
||||
AND NOT a.reconciled
|
||||
AND a.account_id = %(account_id)s
|
||||
AND a.id != %(move_line_id)s
|
||||
{partner_id_condition}
|
||||
ORDER BY a.date desc
|
||||
LIMIT 10
|
||||
""".format(**locals())
|
||||
else:
|
||||
partner_id_condition = partner_id_condition and partner_id_condition+' AND b.partner_id = %(partner_id)s' or ''
|
||||
query = """
|
||||
SELECT a.id, b.id
|
||||
FROM account_move_line a, account_move_line b
|
||||
WHERE a.amount_residual = -b.amount_residual
|
||||
AND NOT a.reconciled AND NOT b.reconciled
|
||||
AND a.account_id = %(account_id)s AND b.account_id = %(account_id)s
|
||||
{partner_id_condition}
|
||||
ORDER BY a.date desc
|
||||
LIMIT 10
|
||||
""".format(**locals())
|
||||
|
||||
self.env.cr.execute(query, locals())
|
||||
pairs = self.env.cr.fetchall()
|
||||
|
||||
# Apply ir_rules by filtering out
|
||||
all_pair_ids = [element for tupl in pairs for element in tupl]
|
||||
allowed_ids = set(Account_move_line.browse(all_pair_ids).ids)
|
||||
pairs = [pair for pair in pairs if pair[0] in allowed_ids and pair[1] in allowed_ids]
|
||||
|
||||
if len(pairs) > 0:
|
||||
rec_prop += Account_move_line.browse(list(set(pairs[0])))
|
||||
|
||||
return rec_prop
|
||||
|
||||
@api.model
|
||||
# process_reconciliation
|
||||
def _process_move_lines(self, move_line_ids, new_mv_line_dicts):
|
||||
""" Create new move lines from new_mv_line_dicts (if not empty) then call reconcile_partial on self and new move lines
|
||||
|
||||
:param new_mv_line_dicts: list of dicts containing values suitable for account_move_line.create()
|
||||
"""
|
||||
if len(move_line_ids) < 1 or len(move_line_ids) + len(new_mv_line_dicts) < 2:
|
||||
raise UserError(_('A reconciliation must involve at least 2 move lines.'))
|
||||
|
||||
account_move_line = self.env['account.move.line'].browse(move_line_ids)
|
||||
writeoff_lines = self.env['account.move.line']
|
||||
|
||||
# Create writeoff move lines
|
||||
if len(new_mv_line_dicts) > 0:
|
||||
company_currency = account_move_line[0].account_id.company_id.currency_id
|
||||
writeoff_currency = account_move_line[0].currency_id or company_currency
|
||||
for mv_line_dict in new_mv_line_dicts:
|
||||
if writeoff_currency != company_currency:
|
||||
mv_line_dict['debit'] = writeoff_currency.compute(mv_line_dict['debit'], company_currency)
|
||||
mv_line_dict['credit'] = writeoff_currency.compute(mv_line_dict['credit'], company_currency)
|
||||
writeoff_lines += account_move_line._create_writeoff(mv_line_dict)
|
||||
|
||||
(account_move_line + writeoff_lines).reconcile()
|
||||
else:
|
||||
account_move_line.reconcile()
|
||||
@@ -57,7 +57,7 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
|
||||
this.action_manager = parent;
|
||||
this.params = params;
|
||||
this.model = new this.config.Model(this, {
|
||||
modelName: "account.bank.statement.line",
|
||||
modelName: "account.reconciliation.widget",
|
||||
limitMoveLines: params.params && params.params.limitMoveLines || this.config.limitMoveLines,
|
||||
});
|
||||
this.widgets = [];
|
||||
|
||||
@@ -11,8 +11,8 @@ var _t = core._t;
|
||||
|
||||
|
||||
/**
|
||||
* Model use to fetch, format and update 'account.bank.statement' and
|
||||
* 'account.bank.statement.line' datas allowing reconciliation
|
||||
* Model use to fetch, format and update 'account.reconciliation.widget',
|
||||
* datas allowing reconciliation
|
||||
*
|
||||
* The statement internal structure::
|
||||
*
|
||||
@@ -133,8 +133,8 @@ var StatementModel = BasicModel.extend({
|
||||
return $.when(this._computeLine(line), this._performMoveLine(handle));
|
||||
},
|
||||
/**
|
||||
* send information 'account.bank.statement.line' model to reconciliate
|
||||
* lines, call rpc to 'reconciliation_widget_auto_reconcile'
|
||||
* send information 'account.reconciliation.widget' model to reconciliate
|
||||
* lines, call rpc to 'auto_reconcile'
|
||||
* Update the number of validated line
|
||||
*
|
||||
* @returns {Deferred<Object>} resolved with an object who contains
|
||||
@@ -144,8 +144,8 @@ var StatementModel = BasicModel.extend({
|
||||
var self = this;
|
||||
var ids = _.pluck(_.filter(this.lines, {'reconciled': false}), 'id');
|
||||
return this._rpc({
|
||||
model: 'account.bank.statement.line',
|
||||
method: 'reconciliation_widget_auto_reconcile',
|
||||
model: 'account.reconciliation.widget',
|
||||
method: 'auto_reconcile',
|
||||
args: [ids, self.valuenow],
|
||||
})
|
||||
.then(function (result) {
|
||||
@@ -342,7 +342,7 @@ var StatementModel = BasicModel.extend({
|
||||
* - 'account.bank.statement' fetch the line id and bank_statement_id info
|
||||
* - 'account.reconcile.model' fetch all reconcile model (for quick add)
|
||||
* - 'account.account' fetch all account code
|
||||
* - 'account.bank.statement.line' fetch each line data
|
||||
* - 'account.reconciliation.widget' fetch each line data
|
||||
*
|
||||
* @param {Object} context
|
||||
* @param {number[]} context.statement_ids
|
||||
@@ -357,8 +357,8 @@ var StatementModel = BasicModel.extend({
|
||||
this.context = context;
|
||||
|
||||
var def_statement = this._rpc({
|
||||
model: 'account.bank.statement',
|
||||
method: 'reconciliation_widget_preprocess',
|
||||
model: 'account.reconciliation.widget',
|
||||
method: 'get_bank_statement_data',
|
||||
args: [statement_ids],
|
||||
})
|
||||
.then(function (statement) {
|
||||
@@ -430,8 +430,8 @@ var StatementModel = BasicModel.extend({
|
||||
loadData: function(ids, excluded_ids) {
|
||||
var self = this;
|
||||
return self._rpc({
|
||||
model: 'account.bank.statement.line',
|
||||
method: 'get_data_for_reconciliation_widget',
|
||||
model: 'account.reconciliation.widget',
|
||||
method: 'get_bank_statement_line_data',
|
||||
args: [ids, excluded_ids],
|
||||
})
|
||||
.then(self._formatLine.bind(self));
|
||||
@@ -619,7 +619,7 @@ var StatementModel = BasicModel.extend({
|
||||
return this._computeLine(line);
|
||||
},
|
||||
/**
|
||||
* Format the value and send it to 'account.bank.statement.line' model
|
||||
* Format the value and send it to 'account.reconciliation.widget' model
|
||||
* Update the number of validated lines
|
||||
*
|
||||
* @param {(string|string[])} handle
|
||||
@@ -688,8 +688,8 @@ var StatementModel = BasicModel.extend({
|
||||
});
|
||||
|
||||
return this._rpc({
|
||||
model: 'account.bank.statement.line',
|
||||
method: 'process_reconciliations',
|
||||
model: 'account.reconciliation.widget',
|
||||
method: 'process_bank_statement_line',
|
||||
args: [ids, values],
|
||||
})
|
||||
.then(function () {
|
||||
@@ -1062,7 +1062,7 @@ var StatementModel = BasicModel.extend({
|
||||
return !isNaN(prop.id) || prop.account_id && prop.amount && prop.label && !!prop.label.length;
|
||||
},
|
||||
/**
|
||||
* Fetch 'account.bank.statement.line' propositions.
|
||||
* Fetch 'account.reconciliation.widget' propositions.
|
||||
*
|
||||
* @see '_formatMoveLine'
|
||||
*
|
||||
@@ -1081,8 +1081,8 @@ var StatementModel = BasicModel.extend({
|
||||
var offset = line.offset;
|
||||
var limit = this.limitMoveLines+1;
|
||||
return this._rpc({
|
||||
model: 'account.bank.statement.line',
|
||||
method: 'get_move_lines_for_reconciliation_widget',
|
||||
model: 'account.reconciliation.widget',
|
||||
method: 'get_move_lines_for_bank_statement_line',
|
||||
args: [line.id, line.st_line.partner_id, excluded_ids, filter, offset, limit],
|
||||
})
|
||||
.then(this._formatMoveLine.bind(this, handle));
|
||||
@@ -1141,7 +1141,7 @@ var ManualModel = StatementModel.extend({
|
||||
|
||||
/**
|
||||
* load data from
|
||||
* - 'account.move.line' fetch the lines to reconciliate
|
||||
* - 'account.reconciliation.widget' fetch the lines to reconciliate
|
||||
* - 'account.account' fetch all account code
|
||||
*
|
||||
* @param {Object} context
|
||||
@@ -1177,7 +1177,7 @@ var ManualModel = StatementModel.extend({
|
||||
var mode = context.mode === 'customers' ? 'receivable' : 'payable';
|
||||
var args = ['partner', context.partner_ids || null, mode];
|
||||
return self._rpc({
|
||||
model: 'account.move.line',
|
||||
model: 'account.reconciliation.widget',
|
||||
method: 'get_data_for_manual_reconciliation',
|
||||
args: args,
|
||||
context: context,
|
||||
@@ -1190,7 +1190,7 @@ var ManualModel = StatementModel.extend({
|
||||
});
|
||||
case 'accounts':
|
||||
return self._rpc({
|
||||
model: 'account.move.line',
|
||||
model: 'account.reconciliation.widget',
|
||||
method: 'get_data_for_manual_reconciliation',
|
||||
args: ['account', context.account_ids || self.account_ids],
|
||||
context: context,
|
||||
@@ -1209,8 +1209,8 @@ var ManualModel = StatementModel.extend({
|
||||
account_ids = null; // TOFIX: REMOVE ME
|
||||
partner_ids = null; // TOFIX: REMOVE ME
|
||||
return self._rpc({
|
||||
model: 'account.move.line',
|
||||
method: 'get_data_for_manual_reconciliation_widget',
|
||||
model: 'account.reconciliation.widget',
|
||||
method: 'get_all_data_for_manual_reconciliation',
|
||||
args: [partner_ids, account_ids],
|
||||
context: context,
|
||||
})
|
||||
@@ -1278,8 +1278,8 @@ var ManualModel = StatementModel.extend({
|
||||
|
||||
if (process_reconciliations.length) {
|
||||
def = self._rpc({
|
||||
model: 'account.move.line',
|
||||
method: 'process_reconciliations',
|
||||
model: 'account.reconciliation.widget',
|
||||
method: 'process_move_lines',
|
||||
args: [process_reconciliations],
|
||||
});
|
||||
}
|
||||
@@ -1440,7 +1440,7 @@ var ManualModel = StatementModel.extend({
|
||||
var limit = this.limitMoveLines+1;
|
||||
var args = [line.account_id.id, line.partner_id, excluded_ids, filter, offset, limit];
|
||||
return this._rpc({
|
||||
model: 'account.move.line',
|
||||
model: 'account.reconciliation.widget',
|
||||
method: 'get_move_lines_for_manual_reconciliation',
|
||||
args: args,
|
||||
})
|
||||
|
||||
@@ -637,6 +637,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {input event} event
|
||||
*/
|
||||
_onFilterChange: function (event) {
|
||||
this.trigger_up('change_filter', {'data': _.str.strip($(event.target).val())});
|
||||
@@ -688,7 +689,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
_onSelectMoveLine: function (event) {
|
||||
var $el = $(event.target)
|
||||
var $el = $(event.target);
|
||||
this._destroyPopover($el);
|
||||
var moveLineId = $el.closest('.mv_line').data('line-id');
|
||||
this.trigger_up('add_proposition', {'data': moveLineId});
|
||||
@@ -698,7 +699,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
_onSelectProposition: function (event) {
|
||||
var $el = $(event.target)
|
||||
var $el = $(event.target);
|
||||
this._destroyPopover($el);
|
||||
var moveLineId = $el.closest('.mv_line').data('line-id');
|
||||
this.trigger_up('remove_proposition', {'data': moveLineId});
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
odoo.define('account.tour_bank_statement_reconciliation', function(require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var rpc = require('web.rpc');
|
||||
var Tour = require('web_tour.tour');
|
||||
|
||||
Tour.register('bank_statement_reconciliation', {
|
||||
test: true,
|
||||
// Go to the reconciliation page of the statement: "BNK/2014/001"
|
||||
}, [
|
||||
{
|
||||
content: "wait web client",
|
||||
extra_trigger: 'body:not(:has(.o_reconciliation))',
|
||||
trigger: '.o_web_client',
|
||||
run: function () {
|
||||
console.log("looking for 'bank_statement_reconciliation' url");
|
||||
rpc.query({
|
||||
model: 'account.bank.statement',
|
||||
method: 'search',
|
||||
args: [[['name', '=', 'BNK/2014/001']]], // account in l10n_generic_coa
|
||||
}).then(function(ids) {
|
||||
var path = "/web#statement_ids=" + ids[0] + "&action=bank_statement_reconciliation_view";
|
||||
console.log("'bank_statement_reconciliation' url is: '" + path + "'");
|
||||
window.location.href = path;
|
||||
}).fail(function () {
|
||||
throw new Error("'account.bank.statement' named 'BNK/2014/001' not found");
|
||||
});
|
||||
},
|
||||
timeout: 5000
|
||||
},
|
||||
{
|
||||
content: "wait reconciliation page",
|
||||
trigger: '.o_reconciliation',
|
||||
run: function () {},
|
||||
},
|
||||
|
||||
// open a line and reconcile a line proposed by the server
|
||||
|
||||
{
|
||||
content: "open the last line in match mode to test the reconcile button",
|
||||
extra_trigger: '.o_reconciliation_line:last .o_reconcile:visible',
|
||||
trigger: '.o_reconciliation_line:last .accounting_view thead .cell_label:contains("/002")',
|
||||
},
|
||||
{
|
||||
content: "deselect the proposed line",
|
||||
extra_trigger: '.o_reconciliation_line:last[data-mode="match"]',
|
||||
trigger: '.o_reconciliation_line:last .accounting_view .cell_label:contains("/0002")'
|
||||
},
|
||||
{
|
||||
content: "re-select the line",
|
||||
extra_trigger: '.o_reconciliation_line:last .o_no_valid:visible', // the user can't validate the line and display the write-off line
|
||||
trigger: '.o_reconciliation_line:last .match .cell_label:contains("/0002")'
|
||||
},
|
||||
{
|
||||
content: "reconcile the line",
|
||||
trigger: '.o_reconciliation_line:last .o_reconcile:visible',
|
||||
},
|
||||
|
||||
// Make a partial reconciliation
|
||||
|
||||
{
|
||||
content: "open the last line in match mode to test the partial reconciliation",
|
||||
extra_trigger: '.o_reconciliation_line:first[data-mode="match"]',
|
||||
trigger: '.o_reconciliation_line:last .cell_label:contains("First")'
|
||||
},
|
||||
{
|
||||
content: "select a line with with a higher amount",
|
||||
trigger: '.o_reconciliation_line:last .match .cell_right:contains($ 4,610.00)'
|
||||
},
|
||||
{
|
||||
content: "click on partial reconcile",
|
||||
trigger: '.o_reconciliation_line:last .accounting_view .do_partial_reconcile_true'
|
||||
},
|
||||
{
|
||||
content: "reconcile the line",
|
||||
trigger: '.o_reconciliation_line:last .o_reconcile:visible',
|
||||
},
|
||||
|
||||
// Test changing the partner
|
||||
|
||||
{
|
||||
content: "change the partner of the second line",
|
||||
trigger: '.o_reconciliation_line:nth-child(2) .o_field_many2one input',
|
||||
run: 'text Agro'
|
||||
},
|
||||
{
|
||||
content: "select Agrolait",
|
||||
extra_trigger: '.ui-autocomplete:visible li:eq(1):contains(Create "Agro")',
|
||||
trigger: '.ui-autocomplete:visible li:contains(Agrolait)',
|
||||
},
|
||||
{
|
||||
content: "use filter",
|
||||
extra_trigger: '.o_reconciliation_line:nth-child(2) .match:not(:has(tr:eq(2))) tr:eq(1)',
|
||||
trigger: '.o_reconciliation_line:nth-child(2) .match .match_controls .filter',
|
||||
run: 'text 4610'
|
||||
},
|
||||
{
|
||||
content: "select a line linked to Agrolait",
|
||||
extra_trigger: '.o_reconciliation_line:nth-child(2) .match:not(:has(tr:eq(1)))',
|
||||
trigger: '.o_reconciliation_line:nth-child(2) .match .line_info_button[data-content*=Agrolait]'
|
||||
},
|
||||
{
|
||||
content: "deselect the line",
|
||||
trigger: '.o_reconciliation_line:nth-child(2) .accounting_view tbody .cell_label:first'
|
||||
},
|
||||
{
|
||||
content: "create a write-off",
|
||||
extra_trigger: '.o_reconciliation_line:nth-child(2) .accounting_view tbody:not(:has(.cell_label))',
|
||||
trigger: '.o_reconciliation_line:nth-child(2) .accounting_view tfoot .cell_label'
|
||||
},
|
||||
{
|
||||
content: "enter an account",
|
||||
trigger: '.o_reconciliation_line:nth-child(2) .o_field_many2one[name="account_id"] input',
|
||||
run: 'text 100000'
|
||||
},
|
||||
{
|
||||
content: "select the first account",
|
||||
extra_trigger: '.ui-autocomplete:visible li:eq(1):contains(Create "100000")',
|
||||
trigger: '.ui-autocomplete:visible li:contains(100000)',
|
||||
},
|
||||
{
|
||||
content: "reconcile the line with the write-off",
|
||||
trigger: '.o_reconciliation_line:nth-child(2) .o_reconcile:visible',
|
||||
},
|
||||
|
||||
// Be done
|
||||
{
|
||||
content: "check the number off validate lines",
|
||||
trigger: '.o_reconciliation .progress-text:contains(3 / 5)'
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
});
|
||||
@@ -1,149 +0,0 @@
|
||||
odoo.define('account.tour_bank_statement_reconciliation', function(require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var Tour = require('web.Tour');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
Tour.register({
|
||||
id: 'bank_statement_reconciliation',
|
||||
name: _t("Reconcile the demo bank statement"),
|
||||
path: '/web',
|
||||
mode: 'test',
|
||||
steps: [
|
||||
// Go to the first statement reconciliation
|
||||
{
|
||||
title: "go to accounting",
|
||||
element: '.oe_menu_toggler:contains("Accounting"):visible',
|
||||
},
|
||||
{
|
||||
title: "go to bank statements",
|
||||
element: '.oe_menu_leaf:contains("Bank Statement"):visible',
|
||||
},
|
||||
{
|
||||
title: "select first bank statement",
|
||||
element: '.oe_list_content tbody tr:contains("BNK/2014/001")',
|
||||
},
|
||||
{
|
||||
title: "click the reconcile button",
|
||||
element: '.oe_form_container header button:contains("Reconcile")',
|
||||
},
|
||||
|
||||
|
||||
// Check mutual exclusion of move lines
|
||||
{
|
||||
title: "set second reconciliation in match mode",
|
||||
element: '.oe_bank_statement_reconciliation_line:nth-child(2) .initial_line'
|
||||
},
|
||||
{
|
||||
title: "deselect SAJ/2014/002 from second reconciliation",
|
||||
element: '.oe_bank_statement_reconciliation_line:nth-child(2) .accounting_view .mv_line:contains("SAJ/2014/002")'
|
||||
},
|
||||
{
|
||||
title: "check it appeared in first reconciliation's matches list and select SAJ/2014/002 in second reconciliation",
|
||||
waitNot: '.oe_bank_statement_reconciliation_line:nth-child(2) .accounting_view .mv_line:contains("SAJ/2014/002")',
|
||||
waitFor: '.oe_bank_statement_reconciliation_line:first-child .mv_line:contains("SAJ/2014/002")',
|
||||
element: '.oe_bank_statement_reconciliation_line:nth-child(2) .mv_line:contains("SAJ/2014/002")'
|
||||
},
|
||||
|
||||
|
||||
// Make a partial reconciliation
|
||||
{
|
||||
title: "select SAJ/2014/001",
|
||||
element: '.oe_bank_statement_reconciliation_line:first-child .mv_line:contains("SAJ/2014/001")'
|
||||
},
|
||||
{
|
||||
title: "click on the partial reconciliation button",
|
||||
element: '.oe_bank_statement_reconciliation_line:first-child .mv_line:contains("SAJ/2014/001") .do_partial_reconcile_button'
|
||||
},
|
||||
{
|
||||
title: "click on the OK button",
|
||||
element: '.oe_bank_statement_reconciliation_line:first-child .button_ok.oe_highlight'
|
||||
},
|
||||
|
||||
|
||||
// Test changing the partner
|
||||
{
|
||||
title: "change the partner (1)",
|
||||
waitNot: '.oe_bank_statement_reconciliation_line:nth-child(4)', // wait for the reconciliation to be processed
|
||||
element: '.oe_bank_statement_reconciliation_line:first-child .partner_name'
|
||||
},
|
||||
{
|
||||
title: "change the partner (2)",
|
||||
element: '.oe_bank_statement_reconciliation_line:first-child .change_partner_container input',
|
||||
sampleText: 'Vauxoo',
|
||||
},
|
||||
{
|
||||
title: "change the partner (3)",
|
||||
element: '.ui-autocomplete .ui-menu-item:contains("Vauxoo")'
|
||||
},
|
||||
{
|
||||
title: "check the reconciliation is reloaded and has no match",
|
||||
element: '.oe_bank_statement_reconciliation_line:first-child.no_match',
|
||||
},
|
||||
{
|
||||
title: "change the partner back (1)",
|
||||
element: '.oe_bank_statement_reconciliation_line:first-child .partner_name'
|
||||
},
|
||||
{
|
||||
title: "change the partner back (2)",
|
||||
element: '.oe_bank_statement_reconciliation_line:first-child .change_partner_container input',
|
||||
sampleText: 'Best Designers',
|
||||
},
|
||||
{
|
||||
title: "change the partner back (3)",
|
||||
element: '.ui-autocomplete .ui-menu-item:contains("Best Designers")'
|
||||
},
|
||||
{
|
||||
title: "select SAJ/2014/002",
|
||||
element: '.oe_bank_statement_reconciliation_line:first-child .mv_line:contains("SAJ/2014/002")'
|
||||
},
|
||||
{
|
||||
title: "click on the OK button",
|
||||
element: '.oe_bank_statement_reconciliation_line:first-child .button_ok.oe_highlight'
|
||||
},
|
||||
|
||||
|
||||
// Create a new move line in first reconciliation and validate it
|
||||
{
|
||||
title: "check following reconciliation passes in mode create",
|
||||
waitNot: '.oe_bank_statement_reconciliation_line:nth-child(3)', // wait for the reconciliation to be processed
|
||||
element: '.oe_bank_statement_reconciliation_line:first-child[data-mode="create"]'
|
||||
},
|
||||
{
|
||||
title: "click the Profit/Loss preset",
|
||||
element: '.oe_bank_statement_reconciliation_line:first-child button:contains("Profit / Loss")'
|
||||
},
|
||||
{
|
||||
title: "click on the OK button",
|
||||
element: '.oe_bank_statement_reconciliation_line:first-child .button_ok.oe_highlight'
|
||||
},
|
||||
|
||||
|
||||
// Leave an open balance
|
||||
{
|
||||
title: "select SAJ/2014/003",
|
||||
waitNot: '.oe_bank_statement_reconciliation_line:nth-child(2)', // wait for the reconciliation to be processed
|
||||
element: '.oe_bank_statement_reconciliation_line:first-child .mv_line:contains("SAJ/2014/003")'
|
||||
},
|
||||
{
|
||||
title: "click on the Keep Open button",
|
||||
element: '.oe_bank_statement_reconciliation_line:first-child .button_ok:not(.oe_highlight)'
|
||||
},
|
||||
|
||||
|
||||
// Be done
|
||||
{
|
||||
title: "check 'finish screen' and close the statement",
|
||||
waitFor: '.done_message',
|
||||
element: '.button_close_statement'
|
||||
},
|
||||
{
|
||||
title: "check the statement is closed",
|
||||
element: '.oe_form_container header .label:contains("Closed")'
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
});
|
||||
@@ -111,9 +111,6 @@ var db = {
|
||||
},
|
||||
'account.bank.statement': {
|
||||
fields: {},
|
||||
reconciliation_widget_preprocess: function () {
|
||||
return $.when(Datas.used.data_preprocess);
|
||||
},
|
||||
},
|
||||
'account.bank.statement.line': {
|
||||
fields: {
|
||||
@@ -127,70 +124,9 @@ var db = {
|
||||
{id: 7, display_name: "Prepayment"},
|
||||
{id: 8, display_name: "First 2000 \u20ac of SAJ/2014/001"},
|
||||
],
|
||||
get_move_lines_for_reconciliation_widget: function (args) {
|
||||
var partner_id = args.splice(1, 1)[0];
|
||||
var excluded_ids = args.splice(1, 1)[0];
|
||||
var key = JSON.stringify(args);
|
||||
if (!Datas.used.mv_lines[key]) {
|
||||
throw new Error("Unknown parameters for get_move_lines_for_reconciliation_widget: '"+ key + "'");
|
||||
}
|
||||
return $.when(_.filter(Datas.used.mv_lines[key], function (line) {
|
||||
return excluded_ids.indexOf(line.id) === -1 && (!partner_id || partner_id === line.partner_id);
|
||||
}));
|
||||
},
|
||||
get_data_for_reconciliation_widget: function (args) {
|
||||
var ids = args[0];
|
||||
return $.when(_.filter(Datas.used.data_widget, function (w) {return _.contains(ids, w.st_line.id);}));
|
||||
},
|
||||
reconciliation_widget_auto_reconcile: function () {
|
||||
return $.when(Datas.used.auto_reconciliation);
|
||||
},
|
||||
process_reconciliations: function (args) {
|
||||
var datas = args[1];
|
||||
var ids = _.flatten(_.pluck(_.pluck(datas, 'counterpart_aml_dicts'), 'counterpart_aml_id'));
|
||||
ids = ids.concat(_.flatten(_.pluck(datas, 'payment_aml_ids')));
|
||||
ids = _.compact(ids);
|
||||
|
||||
for (var key in Datas.used.move_lines_for_manual_reconciliation) {
|
||||
Datas.used.move_lines_for_manual_reconciliation[key] = _.filter(Datas.used.move_lines_for_manual_reconciliation[key], function (mv_line) {
|
||||
return ids.indexOf(mv_line.id) === -1;
|
||||
});
|
||||
}
|
||||
return $.when();
|
||||
},
|
||||
},
|
||||
'account.move.line': {
|
||||
fields: {},
|
||||
get_data_for_manual_reconciliation_widget: function (args) {
|
||||
var key = JSON.stringify(args);
|
||||
if (!Datas.used.data_for_manual_reconciliation_widget[key]) {
|
||||
throw new Error("Unknown parameters for get_data_for_manual_reconciliation_widget: '"+ key + "'");
|
||||
}
|
||||
return $.when(Datas.used.data_for_manual_reconciliation_widget[key]);
|
||||
},
|
||||
get_move_lines_for_manual_reconciliation: function (args) {
|
||||
var excluded_ids = args.splice(2, 1)[0];
|
||||
var key = JSON.stringify(args);
|
||||
if (!Datas.used.move_lines_for_manual_reconciliation[key]) {
|
||||
throw new Error("Unknown parameters for get_move_lines_for_manual_reconciliation: '"+ key + "'");
|
||||
}
|
||||
return $.when(_.filter(Datas.used.move_lines_for_manual_reconciliation[key], function (line) {
|
||||
return excluded_ids.indexOf(line.id) === -1;
|
||||
}));
|
||||
},
|
||||
// for manual reconciliation
|
||||
process_reconciliations: function (args) {
|
||||
var datas = args[0];
|
||||
for (var i in datas) {
|
||||
var data = datas[i];
|
||||
for (var key in Datas.used.move_lines_for_manual_reconciliation) {
|
||||
Datas.used.move_lines_for_manual_reconciliation[key] = _.filter(Datas.used.move_lines_for_manual_reconciliation[key], function (mv_line) {
|
||||
return data.mv_line_ids.indexOf(mv_line.id) === -1;
|
||||
});
|
||||
}
|
||||
}
|
||||
return $.when();
|
||||
},
|
||||
},
|
||||
'account.reconcile.model': {
|
||||
fields: {
|
||||
@@ -220,6 +156,72 @@ var db = {
|
||||
{'second_analytic_account_id': false, 'second_amount_type': "percentage", 'second_journal_id': false, 'id': 3, 'analytic_account_id': false, 'display_name': "ATOS", 'second_tax_id': 7, 'has_second_line': true, 'journal_id': false, 'label': "ATOS Banque", 'second_label': "ATOS Frais", 'second_account_id': 286, 'account_id': 285, 'company_id': [1, "Demo SPRL"], 'tax_id': 6, 'amount_type': "percentage", 'name': "ATOS", 'amount': 97.5, 'second_amount': 2.5},
|
||||
{'second_analytic_account_id': false, 'second_amount_type': "percentage", 'second_journal_id': false, 'id': 10, 'analytic_account_id': false, 'display_name': "Double", 'second_tax_id': false, 'has_second_line': true, 'journal_id': false, 'label': "Double Banque", 'second_label': "Double Frais", 'second_account_id': 286, 'account_id': 285, 'company_id': [1, "Demo SPRL"], 'tax_id': false, 'amount_type': "percentage", 'name': "Double", 'amount': 97.5, 'second_amount': 2.5},
|
||||
]
|
||||
},
|
||||
'account.reconciliation.widget': {
|
||||
fields: {},
|
||||
auto_reconcile: function () {
|
||||
return $.when(Datas.used.auto_reconciliation);
|
||||
},
|
||||
process_bank_statement_line: function (args) {
|
||||
var datas = args[1];
|
||||
var ids = _.flatten(_.pluck(_.pluck(datas, 'counterpart_aml_dicts'), 'counterpart_aml_id'));
|
||||
ids = ids.concat(_.flatten(_.pluck(datas, 'payment_aml_ids')));
|
||||
ids = _.compact(ids);
|
||||
|
||||
for (var key in Datas.used.move_lines_for_manual_reconciliation) {
|
||||
Datas.used.move_lines_for_manual_reconciliation[key] = _.filter(Datas.used.move_lines_for_manual_reconciliation[key], function (mv_line) {
|
||||
return ids.indexOf(mv_line.id) === -1;
|
||||
});
|
||||
}
|
||||
return $.when();
|
||||
},
|
||||
get_move_lines_for_bank_statement_line: function (args) {
|
||||
var partner_id = args.splice(1, 1)[0];
|
||||
var excluded_ids = args.splice(1, 1)[0];
|
||||
var key = JSON.stringify(args);
|
||||
if (!Datas.used.mv_lines[key]) {
|
||||
throw new Error("Unknown parameters for get_move_lines_for_bank_statement_line: '"+ key + "'");
|
||||
}
|
||||
return $.when(_.filter(Datas.used.mv_lines[key], function (line) {
|
||||
return excluded_ids.indexOf(line.id) === -1 && (!partner_id || partner_id === line.partner_id);
|
||||
}));
|
||||
},
|
||||
get_bank_statement_line_data: function (args) {
|
||||
var ids = args[0];
|
||||
return $.when(_.filter(Datas.used.data_widget, function (w) {return _.contains(ids, w.st_line.id);}));
|
||||
},
|
||||
get_bank_statement_data: function () {
|
||||
return $.when(Datas.used.data_preprocess);
|
||||
},
|
||||
get_move_lines_for_manual_reconciliation: function (args) {
|
||||
var excluded_ids = args.splice(2, 1)[0];
|
||||
var key = JSON.stringify(args);
|
||||
if (!Datas.used.move_lines_for_manual_reconciliation[key]) {
|
||||
throw new Error("Unknown parameters for get_move_lines_for_manual_reconciliation: '"+ key + "'");
|
||||
}
|
||||
return $.when(_.filter(Datas.used.move_lines_for_manual_reconciliation[key], function (line) {
|
||||
return excluded_ids.indexOf(line.id) === -1;
|
||||
}));
|
||||
},
|
||||
get_all_data_for_manual_reconciliation: function (args) {
|
||||
var key = JSON.stringify(args);
|
||||
if (!Datas.used.data_for_manual_reconciliation_widget[key]) {
|
||||
throw new Error("Unknown parameters for get_all_data_for_manual_reconciliation: '"+ key + "'");
|
||||
}
|
||||
return $.when(Datas.used.data_for_manual_reconciliation_widget[key]);
|
||||
},
|
||||
process_move_lines: function (args) {
|
||||
var datas = args[0];
|
||||
for (var i in datas) {
|
||||
var data = datas[i];
|
||||
for (var key in Datas.used.move_lines_for_manual_reconciliation) {
|
||||
Datas.used.move_lines_for_manual_reconciliation[key] = _.filter(Datas.used.move_lines_for_manual_reconciliation[key], function (mv_line) {
|
||||
return data.mv_line_ids.indexOf(mv_line.id) === -1;
|
||||
});
|
||||
}
|
||||
}
|
||||
return $.when();
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -659,7 +661,7 @@ QUnit.module('account', {
|
||||
"analytic_tag_ids": [[6, null, []]]
|
||||
}],
|
||||
payment_aml_ids: [], new_aml_dicts: []}]],
|
||||
"Should call process_reconciliations with ids");
|
||||
"Should call process_bank_statement_line with ids");
|
||||
});
|
||||
|
||||
// click on reconcile button
|
||||
@@ -701,7 +703,7 @@ QUnit.module('account', {
|
||||
name: 'SAJ/2014/002 and SAJ/2014/003',
|
||||
analytic_tag_ids: [[6, null, []]]
|
||||
}]}]],
|
||||
"Should call process_reconciliations with ids");
|
||||
"Should call process_bank_statement_line with ids");
|
||||
});
|
||||
|
||||
// click on validate button
|
||||
@@ -754,7 +756,7 @@ QUnit.module('account', {
|
||||
debit: 0,
|
||||
name: 'SAJ/2014/002 and SAJ/2014/003 : Open balance'
|
||||
}]}]],
|
||||
"Should call process_reconciliations with ids");
|
||||
"Should call process_bank_statement_line with ids");
|
||||
});
|
||||
|
||||
// click on validate button
|
||||
@@ -769,7 +771,8 @@ QUnit.module('account', {
|
||||
testUtils.addMockEnvironment(clientAction, {
|
||||
data: this.params.data,
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'process_reconciliations') {
|
||||
console.log(args.method);
|
||||
if (args.method === 'process_bank_statement_line') {
|
||||
assert.deepEqual(args.args, [
|
||||
[6],
|
||||
[{
|
||||
@@ -778,7 +781,7 @@ QUnit.module('account', {
|
||||
payment_aml_ids: [392],
|
||||
new_aml_dicts: [],
|
||||
}]
|
||||
], "should call process_reconciliations with partial reconcile values");
|
||||
], "should call process_bank_statement_line with partial reconcile values");
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
|
||||
@@ -8,10 +8,8 @@ from . import test_account_validate_account_move
|
||||
from . import test_account_invoice_rounding
|
||||
from . import test_account_supplier_invoice_recurrent
|
||||
from . import test_bank_statement_reconciliation
|
||||
#TODO re-enableand fix this test
|
||||
#from . import test_bank_stmt_reconciliation_widget_ui
|
||||
from . import test_fiscal_position
|
||||
from . import test_manual_reconciliation
|
||||
from . import test_reconciliation_widget
|
||||
from . import test_payment
|
||||
from . import test_product_id_change
|
||||
from . import test_reconciliation
|
||||
|
||||
@@ -11,6 +11,7 @@ class TestBankStatementReconciliation(AccountingTestCase):
|
||||
self.il_model = self.env['account.invoice.line']
|
||||
self.bs_model = self.env['account.bank.statement']
|
||||
self.bsl_model = self.env['account.bank.statement.line']
|
||||
self.reconciliation_widget = self.env['account.reconciliation.widget']
|
||||
self.partner_agrolait = self.env.ref("base.res_partner_2")
|
||||
|
||||
def test_reconciliation_proposition(self):
|
||||
@@ -18,9 +19,11 @@ class TestBankStatementReconciliation(AccountingTestCase):
|
||||
st_line = self.create_statement_line(100)
|
||||
|
||||
# exact amount match
|
||||
rec_prop = st_line.get_reconciliation_proposition()
|
||||
self.assertEqual(len(rec_prop), 1)
|
||||
self.assertEqual(rec_prop[0].id, rcv_mv_line.id)
|
||||
rec_prop = self.reconciliation_widget.get_bank_statement_line_data(st_line.ids)
|
||||
prop = rec_prop[0]['reconciliation_proposition']
|
||||
|
||||
self.assertEqual(len(prop), 1)
|
||||
self.assertEqual(prop[0]['id'], rcv_mv_line.id)
|
||||
|
||||
def test_full_reconcile(self):
|
||||
rcv_mv_line = self.create_invoice(100)
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
from odoo.tests import HttpCase, tagged
|
||||
|
||||
@odoo.tests.tagged('post_install','-at_install')
|
||||
class TestUi(HttpCase):
|
||||
|
||||
def test_01_admin_bank_statement_reconciliation(self):
|
||||
self.phantom_js("/", "odoo.__DEBUG__.services['web.Tour'].run('bank_statement_reconciliation', 'test')", "odoo.__DEBUG__.services['web.Tour'].tours.bank_statement_reconciliation", login="admin")
|
||||
@@ -1,18 +0,0 @@
|
||||
from odoo.addons.account.tests.account_test_classes import AccountingTestCase
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged('post_install','-at_install')
|
||||
class TestManualReconciliation(AccountingTestCase):
|
||||
|
||||
def test_reconciliation_proposition(self):
|
||||
pass
|
||||
|
||||
def test_full_reconcile(self):
|
||||
pass
|
||||
|
||||
def test_partial_reconcile(self):
|
||||
pass
|
||||
|
||||
def test_reconcile_with_write_off(self):
|
||||
pass
|
||||
@@ -0,0 +1,12 @@
|
||||
import odoo.tests
|
||||
|
||||
|
||||
@odoo.tests.tagged('post_install', '-at_install')
|
||||
@odoo.tests.common.at_install(False)
|
||||
@odoo.tests.common.post_install(True)
|
||||
class TestUi(odoo.tests.HttpCase):
|
||||
|
||||
def test_01_admin_bank_statement_reconciliation(self):
|
||||
self.phantom_js("/web",
|
||||
"odoo.__DEBUG__.services['web_tour.tour'].run('bank_statement_reconciliation')",
|
||||
"odoo.__DEBUG__.services['web_tour.tour'].tours.bank_statement_reconciliation.ready", login="admin")
|
||||
@@ -11,8 +11,8 @@
|
||||
<script type="text/javascript" src="/account/static/src/js/reconciliation/reconciliation_action.js"></script>
|
||||
<script type="text/javascript" src="/account/static/src/js/reconciliation/reconciliation_model.js"></script>
|
||||
<script type="text/javascript" src="/account/static/src/js/reconciliation/reconciliation_renderer.js"></script>
|
||||
<script type="text/javascript" src="/account/static/src/js/reconciliation/tour_reconciliation.js"></script>
|
||||
|
||||
<!-- <script type="text/javascript" src="/account/static/src/js/tour_bank_statement_reconciliation.js"></script> -->
|
||||
<script type="text/javascript" src="/account/static/src/js/account_payment_field.js"></script>
|
||||
<script type="text/javascript" src="/account/static/src/js/account_dashboard_setup_bar.js"></script>
|
||||
</xpath>
|
||||
|
||||
@@ -73,5 +73,18 @@
|
||||
<field name="amount">102.78</field>
|
||||
<field name="date" eval="time.strftime('%Y')+'-01-01'"/>
|
||||
</record>
|
||||
|
||||
<record id="demo_bank_statement_line_6" model="account.bank.statement.line">
|
||||
<field name="ref">''</field>
|
||||
<field name="statement_id" ref="l10n_generic_coa.demo_bank_statement_1"/>
|
||||
<field name="sequence">1</field>
|
||||
<field name="name" eval="'SAJ/'+time.strftime('%Y')+'/002'"/>
|
||||
<field name="journal_id" model="account.journal" search="[
|
||||
('type', '=', 'bank'),
|
||||
('company_id', '=', obj().env['res.company']._company_default_get('account.journal').id)]"/>
|
||||
<field name="amount">750.0</field>
|
||||
<field name="date" eval="time.strftime('%Y')+'-01-01'"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -446,7 +446,7 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
|
||||
'debit': 0.0,
|
||||
}]
|
||||
|
||||
account_statement_line.process_reconciliations([{'new_aml_dicts': new_aml_dicts}])
|
||||
self.env['account.reconciliation.widget'].process_bank_statement_line(account_statement_line.ids, [{'new_aml_dicts': new_aml_dicts}])
|
||||
|
||||
# I confirm the bank statement using Confirm button
|
||||
|
||||
|
||||
Reference in New Issue
Block a user