15 changed files with 4757 additions and 4 deletions
@ -0,0 +1,929 @@ |
|||
# -*- coding: utf-8 -*- |
|||
|
|||
import copy |
|||
from odoo import api, fields, models, _ |
|||
from odoo.exceptions import UserError |
|||
from odoo.osv import expression |
|||
from odoo.tools.misc import formatLang, format_date, parse_date |
|||
|
|||
|
|||
class AccountReconciliation(models.AbstractModel): |
|||
_name = 'account.reconciliation.widget' |
|||
_description = 'Account Reconciliation widget' |
|||
|
|||
#################################################### |
|||
# Public |
|||
#################################################### |
|||
|
|||
@api.model |
|||
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. |
|||
:returns dict: used as a hook to add additional keys. |
|||
""" |
|||
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) |
|||
|
|||
processed_moves = self.env['account.move'] |
|||
for st_line, datum in zip(st_lines, copy.deepcopy(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']}) |
|||
|
|||
ctx['default_to_check'] = datum.get('to_check') |
|||
moves = st_line.with_context(ctx).reconcile(datum.get('lines_vals_list', []), to_check=datum.get('to_check', False)) |
|||
|
|||
return {'moves': processed_moves.ids, 'statement_line_ids': processed_moves.mapped('line_ids.statement_line_id').ids} |
|||
|
|||
@api.model |
|||
def get_move_lines_for_bank_statement_line(self, st_line_id, partner_id=None, excluded_ids=None, search_str=False, offset=0, limit=None, mode=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 search_str: optional search (can be the amout, display_name, |
|||
partner name, move line name) |
|||
:param offset: useless but kept in stable to preserve api |
|||
:param limit: number of the result to search |
|||
:param mode: 'rp' for receivable/payable or 'other' |
|||
""" |
|||
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_account_id.id |
|||
] |
|||
|
|||
if partner_id is None: |
|||
partner_id = st_line.partner_id.id |
|||
|
|||
domain = self._domain_move_lines_for_reconciliation(st_line, aml_accounts, partner_id, excluded_ids=excluded_ids, search_str=search_str, mode=mode) |
|||
recs_count = self.env['account.move.line'].search_count(domain) |
|||
|
|||
from_clause, where_clause, where_clause_params = self.env['account.move.line']._where_calc(domain).get_sql() |
|||
query_str = ''' |
|||
SELECT "account_move_line".id FROM {from_clause} |
|||
{where_str} |
|||
ORDER BY ("account_move_line".debit - "account_move_line".credit) = {amount} DESC, |
|||
"account_move_line".date_maturity ASC, |
|||
"account_move_line".id ASC |
|||
{limit_str} |
|||
'''.format( |
|||
from_clause=from_clause, |
|||
where_str=where_clause and (" WHERE %s" % where_clause) or '', |
|||
amount=st_line.amount, |
|||
limit_str=limit and ' LIMIT %s' or '', |
|||
) |
|||
params = where_clause_params + (limit and [limit] or []) |
|||
self.env['account.move'].flush() |
|||
self.env['account.move.line'].flush() |
|||
self.env['account.bank.statement'].flush() |
|||
self._cr.execute(query_str, params) |
|||
res = self._cr.fetchall() |
|||
|
|||
aml_recs = self.env['account.move.line'].browse([i[0] for i in res]) |
|||
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, recs_count=recs_count) |
|||
|
|||
@api.model |
|||
def _get_bank_statement_line_partners(self, st_lines): |
|||
params = [] |
|||
|
|||
# Add the res.partner.ban's IR rules. In case partners are not shared between companies, |
|||
# identical bank accounts may exist in a company we don't have access to. |
|||
ir_rules_query = self.env['res.partner.bank']._where_calc([]) |
|||
self.env['res.partner.bank']._apply_ir_rules(ir_rules_query, 'read') |
|||
from_clause, where_clause, where_clause_params = ir_rules_query.get_sql() |
|||
if where_clause: |
|||
where_bank = ('AND %s' % where_clause).replace('res_partner_bank', 'bank') |
|||
params += where_clause_params |
|||
else: |
|||
where_bank = '' |
|||
|
|||
# Add the res.partner's IR rules. In case partners are not shared between companies, |
|||
# identical partners may exist in a company we don't have access to. |
|||
ir_rules_query = self.env['res.partner']._where_calc([]) |
|||
self.env['res.partner']._apply_ir_rules(ir_rules_query, 'read') |
|||
from_clause, where_clause, where_clause_params = ir_rules_query.get_sql() |
|||
if where_clause: |
|||
where_partner = ('AND %s' % where_clause).replace('res_partner', 'p3') |
|||
params += where_clause_params |
|||
else: |
|||
where_partner = '' |
|||
|
|||
query = ''' |
|||
SELECT |
|||
st_line.id AS id, |
|||
COALESCE(p1.id,p2.id,p3.id) AS partner_id |
|||
FROM account_bank_statement_line st_line |
|||
JOIN account_move move ON move.id = st_line.move_id |
|||
''' |
|||
query += "LEFT JOIN res_partner_bank bank ON bank.id = move.partner_bank_id OR bank.sanitized_acc_number ILIKE regexp_replace(st_line.account_number, '\W+', '', 'g') %s\n" % (where_bank) |
|||
query += 'LEFT JOIN res_partner p1 ON st_line.partner_id=p1.id \n' |
|||
query += 'LEFT JOIN res_partner p2 ON bank.partner_id=p2.id \n' |
|||
# By definition the commercial partner_id doesn't have a parent_id set |
|||
query += 'LEFT JOIN res_partner p3 ON p3.name ILIKE st_line.partner_name %s AND p3.parent_id is NULL \n' % (where_partner) |
|||
query += 'WHERE st_line.id IN %s' |
|||
|
|||
params += [tuple(st_lines.ids)] |
|||
|
|||
self._cr.execute(query, params) |
|||
|
|||
result = {} |
|||
for res in self._cr.dictfetchall(): |
|||
result[res['id']] = res['partner_id'] |
|||
return result |
|||
|
|||
@api.model |
|||
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 |
|||
""" |
|||
results = { |
|||
'lines': [], |
|||
'value_min': 0, |
|||
'value_max': 0, |
|||
'reconciled_aml_ids': [], |
|||
} |
|||
|
|||
if not st_line_ids: |
|||
return results |
|||
|
|||
excluded_ids = excluded_ids or [] |
|||
|
|||
# Make a search to preserve the table's order. |
|||
bank_statement_lines = self.env['account.bank.statement.line'].search([('id', 'in', st_line_ids)]) |
|||
results['value_max'] = len(bank_statement_lines) |
|||
reconcile_model = self.env['account.reconcile.model'].search([('rule_type', '!=', 'writeoff_button')]) |
|||
|
|||
# Search for missing partners when opening the reconciliation widget. |
|||
if bank_statement_lines: |
|||
partner_map = self._get_bank_statement_line_partners(bank_statement_lines) |
|||
matching_amls = reconcile_model._apply_rules(bank_statement_lines, excluded_ids=excluded_ids, partner_map=partner_map) |
|||
|
|||
# Iterate on st_lines to keep the same order in the results list. |
|||
bank_statements_left = self.env['account.bank.statement'] |
|||
for line in bank_statement_lines: |
|||
if matching_amls[line.id].get('status') == 'reconciled': |
|||
reconciled_move_lines = matching_amls[line.id].get('reconciled_lines') |
|||
results['value_min'] += 1 |
|||
results['reconciled_aml_ids'] += reconciled_move_lines and reconciled_move_lines.ids or [] |
|||
else: |
|||
aml_ids = matching_amls[line.id]['aml_ids'] |
|||
bank_statements_left += line.statement_id |
|||
target_currency = line.currency_id or line.journal_id.currency_id or line.journal_id.company_id.currency_id |
|||
|
|||
amls = aml_ids and self.env['account.move.line'].browse(aml_ids) |
|||
line_vals = { |
|||
'st_line': self._get_statement_line(line), |
|||
'reconciliation_proposition': aml_ids and self._prepare_move_lines(amls, target_currency=target_currency, target_date=line.date) or [], |
|||
'model_id': matching_amls[line.id].get('model') and matching_amls[line.id]['model'].id, |
|||
'write_off': matching_amls[line.id].get('status') == 'write_off', |
|||
} |
|||
if not line.partner_id and partner_map.get(line.id): |
|||
partner = self.env['res.partner'].browse(partner_map[line.id]) |
|||
line_vals.update({ |
|||
'partner_id': partner.id, |
|||
'partner_name': partner.name, |
|||
}) |
|||
results['lines'].append(line_vals) |
|||
|
|||
return results |
|||
|
|||
@api.model |
|||
def get_bank_statement_data(self, bank_statement_line_ids, srch_domain=[]): |
|||
""" 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 bank_statement_line_ids: ids of the bank statement lines |
|||
""" |
|||
if not bank_statement_line_ids: |
|||
return {} |
|||
domain = [['id', 'in', tuple(bank_statement_line_ids)], ('is_reconciled', '=', False)] + srch_domain |
|||
bank_statement_lines = self.env['account.bank.statement.line'].search(domain) |
|||
bank_statements = bank_statement_lines.mapped('statement_id') |
|||
|
|||
results = self.get_bank_statement_line_data(bank_statement_lines.ids) |
|||
bank_statement_lines_left = self.env['account.bank.statement.line'].browse([line['st_line']['id'] for line in results['lines']]) |
|||
bank_statements_left = bank_statement_lines_left.mapped('statement_id') |
|||
|
|||
results.update({ |
|||
'statement_name': len(bank_statements_left) == 1 and bank_statements_left.name or False, |
|||
'journal_id': bank_statements and bank_statements[0].journal_id.id or False, |
|||
'notifications': [] |
|||
}) |
|||
|
|||
if len(results['lines']) < len(bank_statement_lines): |
|||
results['notifications'].append({ |
|||
'type': 'info', |
|||
'template': 'reconciliation.notification.reconciled', |
|||
'reconciled_aml_ids': results['reconciled_aml_ids'], |
|||
'nb_reconciled_lines': results['value_min'], |
|||
'details': { |
|||
'name': _('Journal Items'), |
|||
'model': 'account.move.line', |
|||
'ids': results['reconciled_aml_ids'], |
|||
} |
|||
}) |
|||
|
|||
return results |
|||
|
|||
@api.model |
|||
def get_move_lines_for_manual_reconciliation(self, account_id, partner_id=False, excluded_ids=None, search_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, search_str) |
|||
recs_count = Account_move_line.search_count(domain) |
|||
lines = Account_move_line.search(domain, 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,recs_count=recs_count) |
|||
|
|||
@api.model |
|||
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. |
|||
""" |
|||
MoveLine = self.env['account.move.line'] |
|||
aml_ids = self._context.get('active_ids') and self._context.get('active_model') == 'account.move.line' and tuple(self._context.get('active_ids')) |
|||
if aml_ids: |
|||
aml = MoveLine.browse(aml_ids) |
|||
aml._check_reconcile_validity() |
|||
account = aml[0].account_id |
|||
currency = account.currency_id or account.company_id.currency_id |
|||
return { |
|||
'accounts': [{ |
|||
'reconciliation_proposition': self._prepare_move_lines(aml, target_currency=currency), |
|||
'company_id': account.company_id.id, |
|||
'currency_id': currency.id, |
|||
'mode': 'accounts', |
|||
'account_id': account.id, |
|||
'account_name': account.name, |
|||
'account_code': account.code, |
|||
}], |
|||
'customers': [], |
|||
'suppliers': [], |
|||
} |
|||
# If we have specified partner_ids, don't return the list of reconciliation for specific accounts as it will |
|||
# show entries that are not reconciled with other partner. Asking for a specific partner on a specific account |
|||
# is never done. |
|||
accounts_data = [] |
|||
if not partner_ids or not any(partner_ids): |
|||
accounts_data = self.get_data_for_manual_reconciliation('account', account_ids) |
|||
return { |
|||
'customers': self.get_data_for_manual_reconciliation('partner', partner_ids, 'receivable'), |
|||
'suppliers': self.get_data_for_manual_reconciliation('partner', partner_ids, 'payable'), |
|||
'accounts': accounts_data, |
|||
} |
|||
|
|||
@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. |
|||
""" |
|||
|
|||
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' |
|||
aml_ids = self._context.get('active_ids') and self._context.get('active_model') == 'account.move.line' and tuple(self._context.get('active_ids')) |
|||
all_entries = self._context.get('all_entries', False) |
|||
all_entries_query = """ |
|||
AND EXISTS ( |
|||
SELECT NULL |
|||
FROM account_move_line l |
|||
JOIN account_move move ON l.move_id = move.id |
|||
JOIN account_journal journal ON l.journal_id = journal.id |
|||
WHERE l.account_id = a.id |
|||
{inner_where} |
|||
AND l.amount_residual != 0 |
|||
AND move.state = 'posted' |
|||
) |
|||
""".format(inner_where=is_partner and 'AND l.partner_id = p.id' or ' ') |
|||
only_dual_entries_query = """ |
|||
AND EXISTS ( |
|||
SELECT NULL |
|||
FROM account_move_line l |
|||
JOIN account_move move ON l.move_id = move.id |
|||
JOIN account_journal journal ON l.journal_id = journal.id |
|||
WHERE l.account_id = a.id |
|||
{inner_where} |
|||
AND l.amount_residual > 0 |
|||
AND move.state = 'posted' |
|||
) |
|||
AND EXISTS ( |
|||
SELECT NULL |
|||
FROM account_move_line l |
|||
JOIN account_move move ON l.move_id = move.id |
|||
JOIN account_journal journal ON l.journal_id = journal.id |
|||
WHERE l.account_id = a.id |
|||
{inner_where} |
|||
AND l.amount_residual < 0 |
|||
AND move.state = 'posted' |
|||
) |
|||
""".format(inner_where=is_partner and 'AND l.partner_id = p.id' or ' ') |
|||
query = (""" |
|||
SELECT {select} account_id, account_name, account_code, max_date |
|||
FROM ( |
|||
SELECT {inner_select} |
|||
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) |
|||
{inner_from} |
|||
WHERE |
|||
a.reconcile IS TRUE |
|||
AND l.full_reconcile_id is NULL |
|||
{where1} |
|||
{where2} |
|||
{where3} |
|||
AND l.company_id = {company_id} |
|||
{where4} |
|||
{where5} |
|||
GROUP BY {group_by1} a.id, a.name, a.code {group_by2} |
|||
{order_by} |
|||
) as s |
|||
{outer_where} |
|||
""".format( |
|||
select=is_partner and "partner_id, partner_name, to_char(last_time_entries_checked, 'YYYY-MM-DD') AS last_time_entries_checked," or ' ', |
|||
inner_select=is_partner and 'p.id AS partner_id, p.name AS partner_name, p.last_time_entries_checked AS last_time_entries_checked,' or ' ', |
|||
inner_from=is_partner and 'RIGHT JOIN res_partner p ON (l.partner_id = p.id)' or ' ', |
|||
where1=is_partner and ' ' or "AND ((at.type <> 'payable' AND at.type <> 'receivable') OR l.partner_id IS NULL)", |
|||
where2=account_type and "AND at.type = %(account_type)s" or '', |
|||
where3=res_ids and 'AND ' + res_alias + '.id in %(res_ids)s' or '', |
|||
company_id=self.env.company.id, |
|||
where4=aml_ids and 'AND l.id IN %(aml_ids)s' or ' ', |
|||
where5=all_entries and all_entries_query or only_dual_entries_query, |
|||
group_by1=is_partner and 'l.partner_id, p.id,' or ' ', |
|||
group_by2=is_partner and ', p.last_time_entries_checked' or ' ', |
|||
order_by=is_partner and 'ORDER BY p.last_time_entries_checked' or 'ORDER BY a.code', |
|||
outer_where=is_partner and 'WHERE (last_time_entries_checked IS NULL OR max_date > last_time_entries_checked)' or ' ', |
|||
)) |
|||
self.env['account.move.line'].flush() |
|||
self.env['account.account'].flush() |
|||
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] |
|||
|
|||
# Keep mode for future use in JS |
|||
if res_type == 'account': |
|||
mode = 'accounts' |
|||
else: |
|||
mode = 'customers' if account_type == 'receivable' else 'suppliers' |
|||
|
|||
# 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 = aml_ids and self.env['account.move.line'].browse(aml_ids) or self._get_move_line_reconciliation_proposition(account.id, partner_id) |
|||
row['reconciliation_proposition'] = self._prepare_move_lines(rec_prop, target_currency=currency) |
|||
row['mode'] = mode |
|||
row['company_id'] = account.company_id.id |
|||
|
|||
# Return the partners with a reconciliation proposition first, since they are most likely to |
|||
# be reconciled. |
|||
return [r for r in rows if r['reconciliation_proposition']] + [r for r in rows if not r['reconciliation_proposition']] |
|||
|
|||
@api.model |
|||
def process_move_lines(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() |
|||
""" |
|||
|
|||
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() |
|||
|
|||
#################################################### |
|||
# Private |
|||
#################################################### |
|||
|
|||
def _str_domain_for_mv_line(self, search_str): |
|||
return [ |
|||
'|', ('account_id.code', 'ilike', search_str), |
|||
'|', ('move_id.name', 'ilike', search_str), |
|||
'|', ('move_id.ref', 'ilike', search_str), |
|||
'|', ('date_maturity', 'like', parse_date(self.env, search_str)), |
|||
'&', ('name', '!=', '/'), ('name', 'ilike', search_str) |
|||
] |
|||
|
|||
@api.model |
|||
def _domain_move_lines(self, search_str): |
|||
""" Returns the domain from the search_str search |
|||
:param search_str: search string |
|||
""" |
|||
if not search_str: |
|||
return [] |
|||
str_domain = self._str_domain_for_mv_line(search_str) |
|||
if search_str[0] in ['-', '+']: |
|||
try: |
|||
amounts_str = search_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(search_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 |
|||
def _domain_move_lines_for_reconciliation(self, st_line, aml_accounts, partner_id, excluded_ids=[], search_str=False, mode='rp'): |
|||
""" 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 search_str: |
|||
:param mode: 'rp' for receivable/payable or 'other' |
|||
""" |
|||
AccountMoveLine = self.env['account.move.line'] |
|||
|
|||
#Always exclude the journal items that have been marked as 'to be checked' in a former bank statement reconciliation |
|||
to_check_excluded = AccountMoveLine.search(AccountMoveLine._get_suspense_moves_domain()).ids |
|||
excluded_ids.extend(to_check_excluded) |
|||
|
|||
domain_reconciliation = [ |
|||
'&', '&', '&', |
|||
('statement_line_id', '=', False), |
|||
('account_id', 'in', aml_accounts), |
|||
('payment_id', '<>', False), |
|||
('balance', '!=', 0.0), |
|||
] |
|||
|
|||
# default domain matching |
|||
domain_matching = [ |
|||
'&', '&', |
|||
('reconciled', '=', False), |
|||
('account_id.reconcile', '=', True), |
|||
('balance', '!=', 0.0), |
|||
] |
|||
|
|||
domain = expression.OR([domain_reconciliation, domain_matching]) |
|||
if partner_id: |
|||
domain = expression.AND([domain, [('partner_id', '=', partner_id)]]) |
|||
if mode == 'rp': |
|||
domain = expression.AND([domain, |
|||
[('account_id.internal_type', 'in', ['receivable', 'payable', 'liquidity'])] |
|||
]) |
|||
else: |
|||
domain = expression.AND([domain, |
|||
[('account_id.internal_type', 'not in', ['receivable', 'payable', 'liquidity'])] |
|||
]) |
|||
|
|||
# Domain factorized for all reconciliation use cases |
|||
if search_str: |
|||
str_domain = self._domain_move_lines(search_str=search_str) |
|||
str_domain = expression.OR([ |
|||
str_domain, |
|||
[('partner_id.name', 'ilike', search_str)] |
|||
]) |
|||
domain = expression.AND([ |
|||
domain, |
|||
str_domain |
|||
]) |
|||
|
|||
if excluded_ids: |
|||
domain = expression.AND([ |
|||
[('id', 'not in', excluded_ids)], |
|||
domain |
|||
]) |
|||
# filter on account.move.line having the same company as the statement line |
|||
domain = expression.AND([domain, [('company_id', '=', st_line.company_id.id)]]) |
|||
|
|||
# take only moves in valid state. Draft is accepted only when "Post At" is set |
|||
# to "Bank Reconciliation" in the associated journal |
|||
domain_post_at = [ |
|||
|
|||
('move_id.state', 'not in', ['draft', 'cancel']), |
|||
] |
|||
domain = expression.AND([domain, domain_post_at]) |
|||
|
|||
# if st_line.company_id.account_bank_reconciliation_start: |
|||
# domain = expression.AND([domain, [('date', '>=', st_line.company_id.account_bank_reconciliation_start)]]) |
|||
return domain |
|||
|
|||
@api.model |
|||
def _domain_move_lines_for_manual_reconciliation(self, account_id, partner_id=False, excluded_ids=None, search_str=False): |
|||
""" Create domain criteria that are relevant to manual reconciliation. """ |
|||
domain = [ |
|||
('reconciled', '=', False), |
|||
('account_id', '=', account_id), |
|||
('move_id.state', '=', 'posted') |
|||
] |
|||
domain = expression.AND([domain, [('balance', '!=', 0.0)]]) |
|||
if partner_id: |
|||
domain = expression.AND([domain, [('partner_id', '=', partner_id)]]) |
|||
if excluded_ids: |
|||
domain = expression.AND([[('id', 'not in', excluded_ids)], domain]) |
|||
if search_str: |
|||
str_domain = self._domain_move_lines(search_str=search_str) |
|||
domain = expression.AND([domain, str_domain]) |
|||
# filter on account.move.line having the same company as the given account |
|||
account = self.env['account.account'].browse(account_id) |
|||
domain = expression.AND([domain, [('company_id', '=', account.company_id.id)]]) |
|||
return domain |
|||
|
|||
@api.model |
|||
def _prepare_move_lines(self, move_lines, target_currency=False, target_date=False, recs_count=0): |
|||
""" 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.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 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': format_date(self.env, line.date_maturity), |
|||
'date': format_date(self.env, 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 |
|||
|
|||
# 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 |
|||
company = line.account_id.company_id |
|||
date = target_date or line.date |
|||
amount = company_currency._convert(amount, target_currency, company, date) |
|||
total_amount = company_currency._convert((line.debit - line.credit), target_currency, company, date) |
|||
total_amount_currency = line.currency_id and line.amount_currency or (line.debit - line.credit) |
|||
|
|||
ret_line['recs_count'] = recs_count |
|||
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 _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.narration or "", |
|||
'name': st_line.name, |
|||
'date': format_date(self.env, 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_account_id.id, st_line.journal_id.default_account_id.display_name], |
|||
'account_code': st_line.journal_id.default_account_id.code, |
|||
'account_name': st_line.journal_id.default_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, |
|||
'company_id': st_line.company_id.id, |
|||
} |
|||
if st_line.partner_id: |
|||
data['open_balance_account_id'] = amount > 0 and st_line.partner_id.property_account_receivable_id.id or st_line.partner_id.property_account_payable_id.id |
|||
|
|||
return data |
|||
|
|||
@api.model |
|||
def _get_move_line_reconciliation_proposition(self, account_id, partner_id=None): |
|||
""" Returns two lines whose amount are opposite """ |
|||
|
|||
Account_move_line = self.env['account.move.line'] |
|||
|
|||
ir_rules_query = Account_move_line._where_calc([]) |
|||
Account_move_line._apply_ir_rules(ir_rules_query, 'read') |
|||
from_clause, where_clause, where_clause_params = ir_rules_query.get_sql() |
|||
where_str = where_clause and (" WHERE %s" % where_clause) or '' |
|||
|
|||
# Get pairs |
|||
query = """ |
|||
SELECT a.id, b.id |
|||
FROM account_move_line a, account_move_line b, |
|||
account_move move_a, account_move move_b, |
|||
account_journal journal_a, account_journal journal_b |
|||
WHERE a.id != b.id |
|||
AND move_a.id = a.move_id |
|||
AND move_a.state = 'posted' |
|||
AND move_a.journal_id = journal_a.id |
|||
AND move_b.id = b.move_id |
|||
AND move_b.journal_id = journal_b.id |
|||
AND move_b.state = 'posted' |
|||
AND a.amount_residual = -b.amount_residual |
|||
AND a.balance != 0.0 |
|||
AND b.balance != 0.0 |
|||
AND NOT a.reconciled |
|||
AND a.account_id = %s |
|||
AND (%s IS NULL AND b.account_id = %s) |
|||
AND (%s IS NULL AND NOT b.reconciled OR b.id = %s) |
|||
AND (%s is NULL OR (a.partner_id = %s AND b.partner_id = %s)) |
|||
AND a.id IN (SELECT "account_move_line".id FROM {0}) |
|||
AND b.id IN (SELECT "account_move_line".id FROM {0}) |
|||
ORDER BY a.date desc |
|||
LIMIT 1 |
|||
""".format(from_clause + where_str) |
|||
move_line_id = self.env.context.get('move_line_id') or None |
|||
params = [ |
|||
account_id, |
|||
move_line_id, account_id, |
|||
move_line_id, move_line_id, |
|||
partner_id, partner_id, partner_id, |
|||
] + where_clause_params + where_clause_params |
|||
self.env.cr.execute(query, params) |
|||
|
|||
pairs = self.env.cr.fetchall() |
|||
|
|||
if pairs: |
|||
return Account_move_line.browse(pairs[0]) |
|||
return Account_move_line |
|||
|
|||
@api.model |
|||
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 |
|||
same_currency = False |
|||
currencies = list(set([aml.currency_id or company_currency for aml in account_move_line])) |
|||
if len(currencies) == 1 and currencies[0] != company_currency: |
|||
same_currency = True |
|||
# We don't have to convert debit/credit to currency as all values in the reconciliation widget are displayed in company currency |
|||
# If all the lines are in the same currency, create writeoff entry with same currency also |
|||
for mv_line_dict in new_mv_line_dicts: |
|||
if not same_currency: |
|||
mv_line_dict['amount_currency'] = False |
|||
writeoff_lines += account_move_line._create_writeoff([mv_line_dict]) |
|||
|
|||
(account_move_line + writeoff_lines).reconcile() |
|||
else: |
|||
account_move_line.reconcile() |
|||
|
|||
|
|||
class AccountInvoiceLine(models.Model): |
|||
_inherit = 'account.move.line' |
|||
|
|||
def _create_writeoff(self, writeoff_vals): |
|||
def compute_writeoff_counterpart_vals(values): |
|||
line_values = values.copy() |
|||
line_values['debit'], line_values['credit'] = line_values['credit'], line_values['debit'] |
|||
if 'amount_currency' in values: |
|||
line_values['amount_currency'] = -line_values['amount_currency'] |
|||
return line_values |
|||
# Group writeoff_vals by journals |
|||
writeoff_dict = {} |
|||
for val in writeoff_vals: |
|||
journal_id = val.get('journal_id', False) |
|||
if not writeoff_dict.get(journal_id, False): |
|||
writeoff_dict[journal_id] = [val] |
|||
else: |
|||
writeoff_dict[journal_id].append(val) |
|||
|
|||
partner_id = self.env['res.partner']._find_accounting_partner(self[0].partner_id).id |
|||
company_currency = self[0].account_id.company_id.currency_id |
|||
writeoff_currency = self[0].account_id.currency_id or company_currency |
|||
line_to_reconcile = self.env['account.move.line'] |
|||
# Iterate and create one writeoff by journal |
|||
writeoff_moves = self.env['account.move'] |
|||
for journal_id, lines in writeoff_dict.items(): |
|||
total = 0 |
|||
total_currency = 0 |
|||
writeoff_lines = [] |
|||
date = fields.Date.today() |
|||
for vals in lines: |
|||
# Check and complete vals |
|||
if 'account_id' not in vals or 'journal_id' not in vals: |
|||
raise UserError(_("It is mandatory to specify an account and a journal to create a write-off.")) |
|||
if ('debit' in vals) ^ ('credit' in vals): |
|||
raise UserError(_("Either pass both debit and credit or none.")) |
|||
if 'date' not in vals: |
|||
vals['date'] = self._context.get('date_p') or fields.Date.today() |
|||
vals['date'] = fields.Date.to_date(vals['date']) |
|||
if vals['date'] and vals['date'] < date: |
|||
date = vals['date'] |
|||
if 'name' not in vals: |
|||
vals['name'] = self._context.get('comment') or _('Write-Off') |
|||
if 'analytic_account_id' not in vals: |
|||
vals['analytic_account_id'] = self.env.context.get('analytic_id', False) |
|||
#compute the writeoff amount if not given |
|||
if 'credit' not in vals and 'debit' not in vals: |
|||
amount = sum([r.amount_residual for r in self]) |
|||
vals['credit'] = amount > 0 and amount or 0.0 |
|||
vals['debit'] = amount < 0 and abs(amount) or 0.0 |
|||
vals['partner_id'] = partner_id |
|||
total += vals['debit']-vals['credit'] |
|||
if 'amount_currency' not in vals and writeoff_currency != company_currency: |
|||
vals['currency_id'] = writeoff_currency.id |
|||
sign = 1 if vals['debit'] > 0 else -1 |
|||
vals['amount_currency'] = sign * abs(sum([r.amount_residual_currency for r in self])) |
|||
total_currency += vals['amount_currency'] |
|||
|
|||
writeoff_lines.append(compute_writeoff_counterpart_vals(vals)) |
|||
|
|||
# Create balance line |
|||
writeoff_lines.append({ |
|||
'name': _('Write-Off'), |
|||
'debit': total > 0 and total or 0.0, |
|||
'credit': total < 0 and -total or 0.0, |
|||
'amount_currency': total_currency, |
|||
'currency_id': total_currency and writeoff_currency.id or False, |
|||
'journal_id': journal_id, |
|||
'account_id': self[0].account_id.id, |
|||
'partner_id': partner_id |
|||
}) |
|||
|
|||
# Create the move |
|||
writeoff_move = self.env['account.move'].create({ |
|||
'journal_id': journal_id, |
|||
'date': date, |
|||
'state': 'draft', |
|||
'line_ids': [(0, 0, line) for line in writeoff_lines], |
|||
}) |
|||
writeoff_moves += writeoff_move |
|||
line_to_reconcile += writeoff_move.line_ids.filtered(lambda r: r.account_id == self[0].account_id).sorted(key='id')[-1:] |
|||
|
|||
#post all the writeoff moves at once |
|||
if writeoff_moves: |
|||
writeoff_moves.action_post() |
|||
|
|||
# Return the writeoff move.line which is to be reconciled |
|||
return line_to_reconcile |
|||
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 112 KiB |
@ -0,0 +1,505 @@ |
|||
odoo.define('base_accounting_kit.ReconciliationClientAction', function (require) { |
|||
"use strict"; |
|||
|
|||
var AbstractAction = require('web.AbstractAction'); |
|||
var ReconciliationModel = require('base_accounting_kit.ReconciliationModel'); |
|||
var ReconciliationRenderer = require('base_accounting_kit.ReconciliationRenderer'); |
|||
var core = require('web.core'); |
|||
var QWeb = core.qweb; |
|||
|
|||
|
|||
/** |
|||
* Widget used as action for 'account.bank.statement' reconciliation |
|||
*/ |
|||
var StatementAction = AbstractAction.extend({ |
|||
hasControlPanel: true, |
|||
withSearchBar: true, |
|||
loadControlPanel: true, |
|||
title: core._t('Bank Reconciliation'), |
|||
contentTemplate: 'reconciliation', |
|||
custom_events: { |
|||
change_mode: '_onAction', |
|||
change_filter: '_onAction', |
|||
change_offset: '_onAction', |
|||
change_partner: '_onAction', |
|||
add_proposition: '_onAction', |
|||
remove_proposition: '_onAction', |
|||
update_proposition: '_onAction', |
|||
create_proposition: '_onAction', |
|||
getPartialAmount: '_onActionPartialAmount', |
|||
quick_create_proposition: '_onAction', |
|||
partial_reconcile: '_onAction', |
|||
validate: '_onValidate', |
|||
close_statement: '_onCloseStatement', |
|||
load_more: '_onLoadMore', |
|||
reload: 'reload', |
|||
search: '_onSearch', |
|||
navigation_move:'_onNavigationMove', |
|||
}, |
|||
config: _.extend({}, AbstractAction.prototype.config, { |
|||
// used to instantiate the model
|
|||
Model: ReconciliationModel.StatementModel, |
|||
// used to instantiate the action interface
|
|||
ActionRenderer: ReconciliationRenderer.StatementRenderer, |
|||
// used to instantiate each widget line
|
|||
LineRenderer: ReconciliationRenderer.LineRenderer, |
|||
// used context params
|
|||
params: ['statement_line_ids'], |
|||
// number of statements/partners/accounts to display
|
|||
defaultDisplayQty: 10, |
|||
// number of moves lines displayed in 'match' mode
|
|||
limitMoveLines: 15, |
|||
}), |
|||
|
|||
_onNavigationMove: function (ev) { |
|||
var non_reconciled_keys = _.keys(_.pick(this.model.lines, function(value, key, object) {return !value.reconciled})); |
|||
var currentIndex = _.indexOf(non_reconciled_keys, ev.data.handle); |
|||
var widget = false; |
|||
switch (ev.data.direction) { |
|||
case 'up': |
|||
ev.stopPropagation(); |
|||
widget = this._getWidget(non_reconciled_keys[currentIndex-1]); |
|||
break; |
|||
case 'down': |
|||
ev.stopPropagation(); |
|||
widget = this._getWidget(non_reconciled_keys[currentIndex+1]); |
|||
break; |
|||
case 'validate': |
|||
ev.stopPropagation(); |
|||
widget = this._getWidget(non_reconciled_keys[currentIndex]); |
|||
widget.$('caption .o_buttons button:visible').click(); |
|||
break; |
|||
} |
|||
if (widget) widget.$el.focus(); |
|||
}, |
|||
|
|||
/** |
|||
* @override |
|||
* @param {Object} params |
|||
* @param {Object} params.context |
|||
* |
|||
*/ |
|||
init: function (parent, params) { |
|||
this._super.apply(this, arguments); |
|||
this.action_manager = parent; |
|||
this.params = params; |
|||
this.searchModelConfig.modelName = 'account.bank.statement.line'; |
|||
this.controlPanelProps.cp_content = {}; |
|||
// this.controlPanelParams.modelName = 'account.bank.statement.line';
|
|||
this.model = new this.config.Model(this, { |
|||
modelName: "account.reconciliation.widget", |
|||
defaultDisplayQty: params.params && params.params.defaultDisplayQty || this.config.defaultDisplayQty, |
|||
limitMoveLines: params.params && params.params.limitMoveLines || this.config.limitMoveLines, |
|||
}); |
|||
this.widgets = []; |
|||
// Adding values from the context is necessary to put this information in the url via the action manager so that
|
|||
// you can retrieve it if the person shares his url or presses f5
|
|||
_.each(params.params, function (value, name) { |
|||
params.context[name] = name.indexOf('_ids') !== -1 ? _.map((value+'').split(','), parseFloat) : value; |
|||
}); |
|||
params.params = {}; |
|||
_.each(this.config.params, function (name) { |
|||
if (params.context[name]) { |
|||
params.params[name] = params.context[name]; |
|||
} |
|||
}); |
|||
}, |
|||
|
|||
/** |
|||
* instantiate the action renderer |
|||
* |
|||
* @override |
|||
*/ |
|||
willStart: function () { |
|||
var self = this; |
|||
var def = this.model.load(this.params.context).then(this._super.bind(this)); |
|||
return def.then(function () { |
|||
if (!self.model.context || !self.model.context.active_id) { |
|||
self.model.context = {'active_id': self.params.context.active_id, |
|||
'active_model': self.params.context.active_model}; |
|||
} |
|||
var journal_id = self.params.context.journal_id; |
|||
if (self.model.context.active_id && self.model.context.active_model === 'account.journal') { |
|||
journal_id = journal_id || self.model.context.active_id; |
|||
} |
|||
if (journal_id) { |
|||
var promise = self._rpc({ |
|||
model: 'account.journal', |
|||
method: 'read', |
|||
args: [journal_id, ['display_name']], |
|||
}); |
|||
} else { |
|||
var promise = Promise.resolve(); |
|||
} |
|||
return promise.then(function (result) { |
|||
var title = result && result[0] ? result[0]['display_name'] : self.params.display_name || '' |
|||
self._setTitle(title); |
|||
self.renderer = new self.config.ActionRenderer(self, self.model, { |
|||
'bank_statement_line_id': self.model.bank_statement_line_id, |
|||
'valuenow': self.model.valuenow, |
|||
'valuemax': self.model.valuemax, |
|||
'defaultDisplayQty': self.model.defaultDisplayQty, |
|||
'title': title, |
|||
}); |
|||
}); |
|||
}); |
|||
}, |
|||
|
|||
reload: function() { |
|||
// On reload destroy all rendered line widget, reload data and then rerender widget
|
|||
var self = this; |
|||
|
|||
self.$('.o_reconciliation_lines').addClass('d-none'); // prevent the browser from recomputing css after each destroy for HUGE perf improvement on a lot of lines
|
|||
_.each(this.widgets, function(widget) { |
|||
widget.destroy(); |
|||
}); |
|||
this.widgets = []; |
|||
self.$('.o_reconciliation_lines').removeClass('d-none'); |
|||
return this.model.reload().then(function() { |
|||
return self._renderLinesOrRainbow(); |
|||
}); |
|||
}, |
|||
|
|||
_renderLinesOrRainbow: function() { |
|||
var self = this; |
|||
return self._renderLines().then(function() { |
|||
var initialState = self.renderer._initialState; |
|||
var valuenow = self.model.statement ? self.model.statement.value_min : initialState.valuenow; |
|||
var valuemax = self.model.statement ? self.model.statement.value_max : initialState.valuemax; |
|||
// No more lines to reconcile, trigger the rainbowman.
|
|||
if(valuenow === valuemax){ |
|||
initialState.valuenow = valuenow; |
|||
initialState.context = self.model.getContext(); |
|||
self.renderer.showRainbowMan(initialState); |
|||
self.remove_cp(); |
|||
}else{ |
|||
// Create a notification if some lines have been reconciled automatically.
|
|||
if(initialState.valuenow > 0) |
|||
self.renderer._renderNotifications(self.model.statement.notifications); |
|||
self._openFirstLine(); |
|||
self.renderer.$('[data-toggle="tooltip"]').tooltip(); |
|||
self.do_show(); |
|||
} |
|||
}); |
|||
}, |
|||
|
|||
/** |
|||
* append the renderer and instantiate the line renderers |
|||
* |
|||
* @override |
|||
*/ |
|||
start: function () { |
|||
var self = this; |
|||
var args = arguments; |
|||
var sup = this._super; |
|||
|
|||
return this.renderer.prependTo(self.$('.o_form_sheet')).then(function() { |
|||
return self._renderLinesOrRainbow().then(function() { |
|||
self.do_show(); |
|||
return sup.apply(self, args); |
|||
}); |
|||
}); |
|||
}, |
|||
|
|||
/** |
|||
* update the control panel and breadcrumbs |
|||
* |
|||
* @override |
|||
*/ |
|||
do_show: function () { |
|||
this._super.apply(this, arguments); |
|||
if (this.action_manager) { |
|||
this.$pager = $(QWeb.render('reconciliation.control.pager', {widget: this.renderer})); |
|||
this.controlPanelProps.cp_content = {$pager: this.$pager}; |
|||
// this.updateControlPanel({
|
|||
// clear: true,
|
|||
// cp_content: {
|
|||
// $pager: this.$pager,
|
|||
// },
|
|||
// });
|
|||
this.renderer.$progress = this.$pager; |
|||
$(this.renderer.$progress).parent().css('width', '100%').css('padding-left', '0'); |
|||
} |
|||
}, |
|||
|
|||
remove_cp: function() { |
|||
this.controlPanelProps.cp_content = {}; |
|||
// this.updateControlPanel({
|
|||
// clear: true,
|
|||
// });
|
|||
}, |
|||
|
|||
//--------------------------------------------------------------------------
|
|||
// Private
|
|||
//--------------------------------------------------------------------------
|
|||
|
|||
/** |
|||
* @private |
|||
* @param {string} handle |
|||
* @returns {Widget} widget line |
|||
*/ |
|||
_getWidget: function (handle) { |
|||
return _.find(this.widgets, function (widget) {return widget.handle===handle;}); |
|||
}, |
|||
|
|||
/** |
|||
* |
|||
*/ |
|||
_loadMore: function(qty) { |
|||
var self = this; |
|||
return this.model.loadMore(qty).then(function () { |
|||
return self._renderLines(); |
|||
}); |
|||
}, |
|||
/** |
|||
* sitch to 'match' the first available line |
|||
* |
|||
* @private |
|||
*/ |
|||
_openFirstLine: function (previous_handle) { |
|||
var self = this; |
|||
previous_handle = previous_handle || 'rline0'; |
|||
var handle = _.compact(_.map(this.model.lines, function (line, handle) { |
|||
return (line.reconciled || (parseInt(handle.substr(5)) < parseInt(previous_handle.substr(5)))) ? null : handle; |
|||
}))[0]; |
|||
if (handle) { |
|||
var line = this.model.getLine(handle); |
|||
this.model.changeMode(handle, 'default').then(function () { |
|||
self._getWidget(handle).update(line); |
|||
}).guardedCatch(function(){ |
|||
self._getWidget(handle).update(line); |
|||
}).then(function() { |
|||
self._getWidget(handle).$el.focus(); |
|||
} |
|||
); |
|||
} |
|||
return handle; |
|||
}, |
|||
|
|||
_forceUpdate: function() { |
|||
var self = this; |
|||
_.each(this.model.lines, function(handle) { |
|||
var widget = self._getWidget(handle['handle']); |
|||
if (widget && handle.need_update) { |
|||
widget.update(handle); |
|||
widget.need_update = false; |
|||
} |
|||
}) |
|||
}, |
|||
/** |
|||
* render line widget and append to view |
|||
* |
|||
* @private |
|||
*/ |
|||
_renderLines: function () { |
|||
var self = this; |
|||
var linesToDisplay = this.model.getStatementLines(); |
|||
var linePromises = []; |
|||
_.each(linesToDisplay, function (line, handle) { |
|||
var widget = new self.config.LineRenderer(self, self.model, line); |
|||
widget.handle = handle; |
|||
self.widgets.push(widget); |
|||
linePromises.push(widget.appendTo(self.$('.o_reconciliation_lines'))); |
|||
}); |
|||
if (this.model.hasMoreLines() === false) { |
|||
this.renderer.hideLoadMoreButton(true); |
|||
} |
|||
else { |
|||
this.renderer.hideLoadMoreButton(false); |
|||
} |
|||
return Promise.all(linePromises); |
|||
}, |
|||
|
|||
//--------------------------------------------------------------------------
|
|||
// Handlers
|
|||
//--------------------------------------------------------------------------
|
|||
|
|||
/** |
|||
* dispatch on the camelcased event name to model method then update the |
|||
* line renderer with the new state. If the mode was switched from 'inactive' |
|||
* to 'create' or 'match_rp' or 'match_other', the other lines switch to |
|||
* 'inactive' mode |
|||
* |
|||
* @private |
|||
* @param {OdooEvent} event |
|||
*/ |
|||
_onAction: function (event) { |
|||
var self = this; |
|||
var handle = event.target.handle; |
|||
var current_line = this.model.getLine(handle); |
|||
this.model[_.str.camelize(event.name)](handle, event.data.data).then(function () { |
|||
var widget = self._getWidget(handle); |
|||
if (widget) { |
|||
widget.update(current_line); |
|||
} |
|||
if (current_line.mode !== 'inactive') { |
|||
_.each(self.model.lines, function (line, _handle) { |
|||
if (line.mode !== 'inactive' && _handle !== handle) { |
|||
self.model.changeMode(_handle, 'inactive'); |
|||
var widget = self._getWidget(_handle); |
|||
if (widget) { |
|||
widget.update(line); |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
}); |
|||
}, |
|||
|
|||
/** |
|||
* @private |
|||
* @param {OdooEvent} ev |
|||
*/ |
|||
_onSearch: function (ev) { |
|||
var self = this; |
|||
ev.stopPropagation(); |
|||
this.model.domain = ev.data.domain; |
|||
this.model.display_context = 'search'; |
|||
self.reload().then(function() { |
|||
self.renderer._updateProgressBar({ |
|||
'valuenow': self.model.valuenow, |
|||
'valuemax': self.model.valuemax, |
|||
}); |
|||
}); |
|||
}, |
|||
|
|||
_onActionPartialAmount: function(event) { |
|||
var self = this; |
|||
var handle = event.target.handle; |
|||
var line = this.model.getLine(handle); |
|||
var amount = this.model.getPartialReconcileAmount(handle, event.data); |
|||
self._getWidget(handle).updatePartialAmount(event.data.data, amount); |
|||
}, |
|||
|
|||
/** |
|||
* call 'closeStatement' model method |
|||
* |
|||
* @private |
|||
* @param {OdooEvent} event |
|||
*/ |
|||
_onCloseStatement: function (event) { |
|||
var self = this; |
|||
return this.model.closeStatement().then(function (result) { |
|||
self.do_action({ |
|||
name: 'Bank Statements', |
|||
res_model: 'account.bank.statement.line', |
|||
res_id: result, |
|||
views: [[false, 'form']], |
|||
type: 'ir.actions.act_window', |
|||
view_mode: 'form', |
|||
}); |
|||
$('.o_reward').remove(); |
|||
}); |
|||
}, |
|||
/** |
|||
* Load more statement and render them |
|||
* |
|||
* @param {OdooEvent} event |
|||
*/ |
|||
_onLoadMore: function (event) { |
|||
return this._loadMore(this.model.defaultDisplayQty); |
|||
}, |
|||
/** |
|||
* call 'validate' model method then destroy the |
|||
* validated lines and update the action renderer with the new status bar |
|||
* values and notifications then open the first available line |
|||
* |
|||
* @private |
|||
* @param {OdooEvent} event |
|||
*/ |
|||
_onValidate: function (event) { |
|||
var self = this; |
|||
var handle = event.target.handle; |
|||
this.model.validate(handle).then(function (result) { |
|||
self.renderer.update({ |
|||
'valuenow': self.model.valuenow, |
|||
'valuemax': self.model.valuemax, |
|||
'title': self.title, |
|||
'time': Date.now()-self.time, |
|||
'notifications': result.notifications, |
|||
'context': self.model.getContext(), |
|||
}); |
|||
self._forceUpdate(); |
|||
_.each(result.handles, function (handle) { |
|||
var widget = self._getWidget(handle); |
|||
if (widget) { |
|||
widget.destroy(); |
|||
var index = _.findIndex(self.widgets, function (widget) {return widget.handle===handle;}); |
|||
self.widgets.splice(index, 1); |
|||
} |
|||
}); |
|||
// Get number of widget and if less than constant and if there are more to laod, load until constant
|
|||
if (self.widgets.length < self.model.defaultDisplayQty |
|||
&& self.model.valuemax - self.model.valuenow >= self.model.defaultDisplayQty) { |
|||
var toLoad = self.model.defaultDisplayQty - self.widgets.length; |
|||
self._loadMore(toLoad); |
|||
} |
|||
self._openFirstLine(handle); |
|||
}); |
|||
}, |
|||
}); |
|||
|
|||
|
|||
/** |
|||
* Widget used as action for 'account.move.line' and 'res.partner' for the |
|||
* manual reconciliation and mark data as reconciliate |
|||
*/ |
|||
var ManualAction = StatementAction.extend({ |
|||
title: core._t('Journal Items to Reconcile'), |
|||
withSearchBar: false, |
|||
config: _.extend({}, StatementAction.prototype.config, { |
|||
Model: ReconciliationModel.ManualModel, |
|||
ActionRenderer: ReconciliationRenderer.ManualRenderer, |
|||
LineRenderer: ReconciliationRenderer.ManualLineRenderer, |
|||
params: ['company_ids', 'mode', 'partner_ids', 'account_ids'], |
|||
defaultDisplayQty: 30, |
|||
limitMoveLines: 15, |
|||
}), |
|||
|
|||
//--------------------------------------------------------------------------
|
|||
// Handlers
|
|||
//--------------------------------------------------------------------------
|
|||
|
|||
/** |
|||
* call 'validate' model method then destroy the |
|||
* reconcilied lines, update the not reconcilied and update the action |
|||
* renderer with the new status bar values and notifications then open the |
|||
* first available line |
|||
* |
|||
* @private |
|||
* @param {OdooEvent} event |
|||
*/ |
|||
_onValidate: function (event) { |
|||
var self = this; |
|||
var handle = event.target.handle; |
|||
var method = 'validate'; |
|||
this.model[method](handle).then(function (result) { |
|||
_.each(result.reconciled, function (handle) { |
|||
self._getWidget(handle).destroy(); |
|||
}); |
|||
_.each(result.updated, function (handle) { |
|||
self._getWidget(handle).update(self.model.getLine(handle)); |
|||
}); |
|||
self.renderer.update({ |
|||
valuenow: _.compact(_.invoke(self.widgets, 'isDestroyed')).length, |
|||
valuemax: self.widgets.length, |
|||
title: self.title, |
|||
time: Date.now()-self.time, |
|||
}); |
|||
if(!_.any(result.updated, function (handle) { |
|||
return self.model.getLine(handle).mode !== 'inactive'; |
|||
})) { |
|||
self._openFirstLine(handle); |
|||
} |
|||
}); |
|||
}, |
|||
}); |
|||
|
|||
core.action_registry.add('bank_statement_reconciliation_view', StatementAction); |
|||
core.action_registry.add('manual_reconciliation_view', ManualAction); |
|||
|
|||
return { |
|||
StatementAction: StatementAction, |
|||
ManualAction: ManualAction, |
|||
}; |
|||
}); |
File diff suppressed because it is too large
@ -0,0 +1,929 @@ |
|||
odoo.define('base_accounting_kit.ReconciliationRenderer', function (require) { |
|||
"use strict"; |
|||
|
|||
var Widget = require('web.Widget'); |
|||
var FieldManagerMixin = require('web.FieldManagerMixin'); |
|||
var relational_fields = require('web.relational_fields'); |
|||
var basic_fields = require('web.basic_fields'); |
|||
var core = require('web.core'); |
|||
var time = require('web.time'); |
|||
var session = require('web.session'); |
|||
var qweb = core.qweb; |
|||
var _t = core._t; |
|||
|
|||
|
|||
/** |
|||
* rendering of the bank statement action contains progress bar, title and |
|||
* auto reconciliation button |
|||
*/ |
|||
var StatementRenderer = Widget.extend(FieldManagerMixin, { |
|||
template: 'reconciliation.statement', |
|||
events: { |
|||
'click *[rel="do_action"]': '_onDoAction', |
|||
'click button.js_load_more': '_onLoadMore', |
|||
}, |
|||
/** |
|||
* @override |
|||
*/ |
|||
init: function (parent, model, state) { |
|||
this._super(parent); |
|||
this.model = model; |
|||
this._initialState = state; |
|||
}, |
|||
/** |
|||
* display iniial state and create the name statement field |
|||
* |
|||
* @override |
|||
*/ |
|||
start: function () { |
|||
var self = this; |
|||
var defs = [this._super.apply(this, arguments)]; |
|||
this.time = Date.now(); |
|||
this.$progress = $(''); |
|||
|
|||
return Promise.all(defs); |
|||
}, |
|||
|
|||
//--------------------------------------------------------------------------
|
|||
// Public
|
|||
//--------------------------------------------------------------------------
|
|||
/* |
|||
* hide the button to load more statement line |
|||
*/ |
|||
hideLoadMoreButton: function (show) { |
|||
if (!show) { |
|||
this.$('.js_load_more').show(); |
|||
} |
|||
else { |
|||
this.$('.js_load_more').hide(); |
|||
} |
|||
}, |
|||
showRainbowMan: function (state) { |
|||
if (this.model.display_context !== 'validate') { |
|||
return |
|||
} |
|||
var dt = Date.now()-this.time; |
|||
var $done = $(qweb.render("reconciliation.done", { |
|||
'duration': moment(dt).utc().format(time.getLangTimeFormat()), |
|||
'number': state.valuenow, |
|||
'timePerTransaction': Math.round(dt/1000/state.valuemax), |
|||
'context': state.context, |
|||
})); |
|||
$done.find('*').addClass('o_reward_subcontent'); |
|||
$done.find('.button_close_statement').click(this._onCloseBankStatement.bind(this)); |
|||
$done.find('.button_back_to_statement').click(this._onGoToBankStatement.bind(this)); |
|||
// display rainbowman after full reconciliation
|
|||
if (session.show_effect) { |
|||
this.trigger_up('show_effect', { |
|||
type: 'rainbow_man', |
|||
fadeout: 'no', |
|||
message: $done, |
|||
}); |
|||
this.$el.css('min-height', '450px'); |
|||
} else { |
|||
$done.appendTo(this.$el); |
|||
} |
|||
}, |
|||
/** |
|||
* update the statement rendering |
|||
* |
|||
* @param {object} state - statement data |
|||
* @param {integer} state.valuenow - for the progress bar |
|||
* @param {integer} state.valuemax - for the progress bar |
|||
* @param {string} state.title - for the progress bar |
|||
* @param {[object]} [state.notifications] |
|||
*/ |
|||
update: function (state) { |
|||
var self = this; |
|||
this._updateProgressBar(state); |
|||
|
|||
if (state.valuenow === state.valuemax && !this.$('.done_message').length) { |
|||
this.showRainbowMan(state); |
|||
} |
|||
|
|||
if (state.notifications) { |
|||
this._renderNotifications(state.notifications); |
|||
} |
|||
}, |
|||
_updateProgressBar: function(state) { |
|||
this.$progress.find('.valuenow').text(state.valuenow); |
|||
this.$progress.find('.valuemax').text(state.valuemax); |
|||
this.$progress.find('.progress-bar') |
|||
.attr('aria-valuenow', state.valuenow) |
|||
.attr('aria-valuemax', state.valuemax) |
|||
.css('width', (state.valuenow/state.valuemax*100) + '%'); |
|||
}, |
|||
|
|||
//--------------------------------------------------------------------------
|
|||
// Private
|
|||
//--------------------------------------------------------------------------
|
|||
/** |
|||
* render the notifications |
|||
* |
|||
* @param {[object]} notifications |
|||
*/ |
|||
_renderNotifications: function(notifications) { |
|||
this.$(".notification_area").empty(); |
|||
for (var i=0; i<notifications.length; i++) { |
|||
var $notification = $(qweb.render("reconciliation.notification", notifications[i])).hide(); |
|||
$notification.appendTo(this.$(".notification_area")).slideDown(300); |
|||
} |
|||
}, |
|||
|
|||
//--------------------------------------------------------------------------
|
|||
// Handlers
|
|||
//--------------------------------------------------------------------------
|
|||
|
|||
/** |
|||
* @private |
|||
* Click on close bank statement button, this will |
|||
* close and then open form view of bank statement |
|||
* @param {MouseEvent} event |
|||
*/ |
|||
_onCloseBankStatement: function (e) { |
|||
this.trigger_up('close_statement'); |
|||
}, |
|||
/** |
|||
* @private |
|||
* @param {MouseEvent} event |
|||
*/ |
|||
_onDoAction: function(e) { |
|||
e.preventDefault(); |
|||
var name = e.currentTarget.dataset.action_name; |
|||
var model = e.currentTarget.dataset.model; |
|||
if (e.currentTarget.dataset.ids) { |
|||
var ids = e.currentTarget.dataset.ids.split(",").map(Number); |
|||
var domain = [['id', 'in', ids]]; |
|||
} else { |
|||
var domain = e.currentTarget.dataset.domain; |
|||
} |
|||
var context = e.currentTarget.dataset.context; |
|||
var tag = e.currentTarget.dataset.tag; |
|||
if (tag) { |
|||
this.do_action({ |
|||
type: 'ir.actions.client', |
|||
tag: tag, |
|||
context: context, |
|||
}) |
|||
} else { |
|||
this.do_action({ |
|||
name: name, |
|||
res_model: model, |
|||
domain: domain, |
|||
context: context, |
|||
views: [[false, 'list'], [false, 'form']], |
|||
type: 'ir.actions.act_window', |
|||
view_mode: "list" |
|||
}); |
|||
} |
|||
}, |
|||
/** |
|||
* Open the list view for account.bank.statement model |
|||
* @private |
|||
* @param {MouseEvent} event |
|||
*/ |
|||
_onGoToBankStatement: function (e) { |
|||
var journalId = $(e.target).attr('data_journal_id'); |
|||
if (journalId) { |
|||
journalId = parseInt(journalId); |
|||
} |
|||
$('.o_reward').remove(); |
|||
this.do_action({ |
|||
name: 'Bank Statements', |
|||
res_model: 'account.bank.statement', |
|||
views: [[false, 'list'], [false, 'form']], |
|||
type: 'ir.actions.act_window', |
|||
context: {search_default_journal_id: journalId, 'journal_type':'bank'}, |
|||
view_mode: 'form', |
|||
}); |
|||
}, |
|||
/** |
|||
* Load more statement lines for reconciliation |
|||
* @private |
|||
* @param {MouseEvent} event |
|||
*/ |
|||
_onLoadMore: function (e) { |
|||
this.trigger_up('load_more'); |
|||
}, |
|||
}); |
|||
|
|||
|
|||
/** |
|||
* rendering of the bank statement line, contains line data, proposition and |
|||
* view for 'match' and 'create' mode |
|||
*/ |
|||
var LineRenderer = Widget.extend(FieldManagerMixin, { |
|||
template: "reconciliation.line", |
|||
events: { |
|||
'click .accounting_view caption .o_buttons button': '_onValidate', |
|||
'click .accounting_view tfoot': '_onChangeTab', |
|||
'click': '_onTogglePanel', |
|||
'click .o_field_widget': '_onStopPropagation', |
|||
'keydown .o_input, .edit_amount_input': '_onStopPropagation', |
|||
'click .o_notebook li a': '_onChangeTab', |
|||
'click .cell': '_onEditAmount', |
|||
'change input.filter': '_onFilterChange', |
|||
'click .match .load-more a': '_onLoadMore', |
|||
'click .match .mv_line td': '_onSelectMoveLine', |
|||
'click .accounting_view tbody .mv_line td': '_onSelectProposition', |
|||
'click .o_reconcile_models button': '_onQuickCreateProposition', |
|||
'click .create .add_line': '_onCreateProposition', |
|||
'click .reconcile_model_create': '_onCreateReconcileModel', |
|||
'click .reconcile_model_edit': '_onEditReconcileModel', |
|||
'keyup input': '_onInputKeyup', |
|||
'blur input': '_onInputKeyup', |
|||
'keydown': '_onKeydown', |
|||
}, |
|||
custom_events: _.extend({}, FieldManagerMixin.custom_events, { |
|||
'field_changed': '_onFieldChanged', |
|||
}), |
|||
_avoidFieldUpdate: {}, |
|||
MV_LINE_DEBOUNCE: 200, |
|||
|
|||
_onKeydown: function (ev) { |
|||
switch (ev.which) { |
|||
case $.ui.keyCode.ENTER: |
|||
this.trigger_up('navigation_move', {direction: 'validate', handle: this.handle}); |
|||
break; |
|||
case $.ui.keyCode.UP: |
|||
ev.stopPropagation(); |
|||
ev.preventDefault(); |
|||
this.trigger_up('navigation_move', {direction: 'up', handle: this.handle}); |
|||
break; |
|||
case $.ui.keyCode.DOWN: |
|||
ev.stopPropagation(); |
|||
ev.preventDefault(); |
|||
this.trigger_up('navigation_move', {direction: 'down', handle: this.handle}); |
|||
break; |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* create partner_id field in editable mode |
|||
* |
|||
* @override |
|||
*/ |
|||
init: function (parent, model, state) { |
|||
this._super(parent); |
|||
FieldManagerMixin.init.call(this); |
|||
|
|||
this.model = model; |
|||
this._initialState = state; |
|||
if (this.MV_LINE_DEBOUNCE) { |
|||
this._onSelectMoveLine = _.debounce(this._onSelectMoveLine, this.MV_LINE_DEBOUNCE, true); |
|||
} else { |
|||
this._onSelectMoveLine = this._onSelectMoveLine; |
|||
} |
|||
}, |
|||
/** |
|||
* @override |
|||
*/ |
|||
start: function () { |
|||
var self = this; |
|||
var def1 = this._makePartnerRecord(this._initialState.st_line.partner_id, this._initialState.st_line.partner_name).then(function (recordID) { |
|||
self.fields = { |
|||
partner_id : new relational_fields.FieldMany2One(self, |
|||
'partner_id', |
|||
self.model.get(recordID), { |
|||
mode: 'edit', |
|||
attrs: { |
|||
placeholder: self._initialState.st_line.communication_partner_name || _t('Select Partner'), |
|||
} |
|||
} |
|||
) |
|||
}; |
|||
self.fields.partner_id.insertAfter(self.$('.accounting_view caption .o_buttons')); |
|||
}); |
|||
var def3 = session.user_has_group('analytic.group_analytic_tags').then(function(has_group) { |
|||
self.group_tags = has_group; |
|||
}); |
|||
var def4 = session.user_has_group('analytic.group_analytic_accounting').then(function(has_group) { |
|||
self.group_acc = has_group; |
|||
}); |
|||
$('<span class="line_info_button fa fa-info-circle"/>') |
|||
.appendTo(this.$('thead .cell_info_popover')) |
|||
.attr("data-content", qweb.render('reconciliation.line.statement_line.details', {'state': this._initialState})); |
|||
this.$el.popover({ |
|||
'selector': '.line_info_button', |
|||
'placement': 'left', |
|||
'container': this.$el, |
|||
'html': true, |
|||
// disable bootstrap sanitizer because we use a table that has been
|
|||
// rendered using qweb.render so it is safe and also because sanitizer escape table by default.
|
|||
'sanitize': false, |
|||
'trigger': 'hover', |
|||
'animation': false, |
|||
'toggle': 'popover' |
|||
}); |
|||
var def2 = this._super.apply(this, arguments); |
|||
return Promise.all([def1, def2, def3, def4]); |
|||
}, |
|||
|
|||
//--------------------------------------------------------------------------
|
|||
// Public
|
|||
//--------------------------------------------------------------------------
|
|||
|
|||
/** |
|||
* update the statement line rendering |
|||
* |
|||
* @param {object} state - statement line |
|||
*/ |
|||
update: function (state) { |
|||
var self = this; |
|||
// isValid
|
|||
var to_check_checked = !!(state.to_check); |
|||
this.$('caption .o_buttons button.o_validate').toggleClass('d-none', !!state.balance.type && !to_check_checked); |
|||
this.$('caption .o_buttons button.o_reconcile').toggleClass('d-none', state.balance.type <= 0 || to_check_checked); |
|||
this.$('caption .o_buttons .o_no_valid').toggleClass('d-none', state.balance.type >= 0 || to_check_checked); |
|||
self.$('caption .o_buttons button.o_validate').toggleClass('text-warning', to_check_checked); |
|||
|
|||
// partner_id
|
|||
this._makePartnerRecord(state.st_line.partner_id, state.st_line.partner_name).then(function (recordID) { |
|||
self.fields.partner_id.reset(self.model.get(recordID)); |
|||
self.$el.attr('data-partner', state.st_line.partner_id); |
|||
}); |
|||
|
|||
// mode
|
|||
this.$el.data('mode', state.mode).attr('data-mode', state.mode); |
|||
this.$('.o_notebook li a').attr('aria-selected', false); |
|||
this.$('.o_notebook li a').removeClass('active'); |
|||
this.$('.o_notebook .tab-content .tab-pane').removeClass('active'); |
|||
this.$('.o_notebook li a[href*="notebook_page_' + state.mode + '"]').attr('aria-selected', true); |
|||
this.$('.o_notebook li a[href*="notebook_page_' + state.mode + '"]').addClass('active'); |
|||
this.$('.o_notebook .tab-content .tab-pane[id*="notebook_page_' + state.mode + '"]').addClass('active'); |
|||
this.$('.create, .match').each(function () { |
|||
$(this).removeAttr('style'); |
|||
}); |
|||
|
|||
// reconciliation_proposition
|
|||
var $props = this.$('.accounting_view tbody').empty(); |
|||
|
|||
// Search propositions that could be a partial credit/debit.
|
|||
var props = []; |
|||
var balance = state.balance.amount_currency; |
|||
_.each(state.reconciliation_proposition, function (prop) { |
|||
if (prop.display) { |
|||
props.push(prop); |
|||
} |
|||
}); |
|||
|
|||
_.each(props, function (line) { |
|||
var $line = $(qweb.render("reconciliation.line.mv_line", {'line': line, 'state': state, 'proposition': true})); |
|||
if (!isNaN(line.id)) { |
|||
$('<span class="line_info_button fa fa-info-circle"/>') |
|||
.appendTo($line.find('.cell_info_popover')) |
|||
.attr("data-content", qweb.render('reconciliation.line.mv_line.details', {'line': line})); |
|||
} |
|||
$props.append($line); |
|||
}); |
|||
|
|||
// mv_lines
|
|||
var matching_modes = self.model.modes.filter(x => x.startsWith('match')); |
|||
for (let i = 0; i < matching_modes.length; i++) { |
|||
var stateMvLines = state['mv_lines_'+matching_modes[i]] || []; |
|||
var recs_count = stateMvLines.length > 0 ? stateMvLines[0].recs_count : 0; |
|||
var remaining = state['remaining_' + matching_modes[i]]; |
|||
var $mv_lines = this.$('div[id*="notebook_page_' + matching_modes[i] + '"] .match table tbody').empty(); |
|||
this.$('.o_notebook li a[href*="notebook_page_' + matching_modes[i] + '"]').parent().toggleClass('d-none', stateMvLines.length === 0 && !state['filter_'+matching_modes[i]]); |
|||
|
|||
_.each(stateMvLines, function (line) { |
|||
var $line = $(qweb.render("reconciliation.line.mv_line", {'line': line, 'state': state})); |
|||
if (!isNaN(line.id)) { |
|||
$('<span class="line_info_button fa fa-info-circle"/>') |
|||
.appendTo($line.find('.cell_info_popover')) |
|||
.attr("data-content", qweb.render('reconciliation.line.mv_line.details', {'line': line})); |
|||
} |
|||
$mv_lines.append($line); |
|||
}); |
|||
this.$('div[id*="notebook_page_' + matching_modes[i] + '"] .match div.load-more').toggle(remaining > 0); |
|||
this.$('div[id*="notebook_page_' + matching_modes[i] + '"] .match div.load-more span').text(remaining); |
|||
} |
|||
|
|||
// balance
|
|||
this.$('.popover').remove(); |
|||
this.$('table tfoot').html(qweb.render("reconciliation.line.balance", {'state': state})); |
|||
|
|||
// create form
|
|||
if (state.createForm) { |
|||
var createPromise; |
|||
if (!this.fields.account_id) { |
|||
createPromise = this._renderCreate(state); |
|||
} |
|||
Promise.resolve(createPromise).then(function(){ |
|||
var data = self.model.get(self.handleCreateRecord).data; |
|||
return self.model.notifyChanges(self.handleCreateRecord, state.createForm) |
|||
.then(function () { |
|||
// FIXME can't it directly written REPLACE_WITH ids=state.createForm.analytic_tag_ids
|
|||
return self.model.notifyChanges(self.handleCreateRecord, {analytic_tag_ids: {operation: 'REPLACE_WITH', ids: []}}) |
|||
}) |
|||
.then(function (){ |
|||
var defs = []; |
|||
_.each(state.createForm.analytic_tag_ids, function (tag) { |
|||
defs.push(self.model.notifyChanges(self.handleCreateRecord, {analytic_tag_ids: {operation: 'ADD_M2M', ids: tag}})); |
|||
}); |
|||
return Promise.all(defs); |
|||
}) |
|||
.then(function () { |
|||
return self.model.notifyChanges(self.handleCreateRecord, {tax_ids: {operation: 'REPLACE_WITH', ids: []}}) |
|||
}) |
|||
.then(function (){ |
|||
var defs = []; |
|||
_.each(state.createForm.tax_ids, function (tag) { |
|||
defs.push(self.model.notifyChanges(self.handleCreateRecord, {tax_ids: {operation: 'ADD_M2M', ids: tag}})); |
|||
}); |
|||
return Promise.all(defs); |
|||
}) |
|||
.then(function () { |
|||
var record = self.model.get(self.handleCreateRecord); |
|||
_.each(self.fields, function (field, fieldName) { |
|||
if (self._avoidFieldUpdate[fieldName]) return; |
|||
if (fieldName === "partner_id") return; |
|||
if ((data[fieldName] || state.createForm[fieldName]) && !_.isEqual(state.createForm[fieldName], data[fieldName])) { |
|||
field.reset(record); |
|||
} |
|||
if (fieldName === 'tax_ids') { |
|||
if (!state.createForm[fieldName].length || state.createForm[fieldName].length > 1) { |
|||
$('.create_force_tax_included').addClass('d-none'); |
|||
} |
|||
else { |
|||
$('.create_force_tax_included').removeClass('d-none'); |
|||
var price_include = state.createForm[fieldName][0].price_include; |
|||
var force_tax_included = state.createForm[fieldName][0].force_tax_included; |
|||
self.$('.create_force_tax_included input').prop('checked', force_tax_included); |
|||
self.$('.create_force_tax_included input').prop('disabled', price_include); |
|||
} |
|||
} |
|||
}); |
|||
if (state.to_check) { |
|||
// Set the to_check field to true if global to_check is set
|
|||
self.$('.create_to_check input').prop('checked', state.to_check).change(); |
|||
} |
|||
return true; |
|||
}); |
|||
}); |
|||
} |
|||
this.$('.create .add_line').toggle(!!state.balance.amount_currency); |
|||
}, |
|||
|
|||
updatePartialAmount: function(line_id, amount) { |
|||
var $line = this.$('.mv_line[data-line-id='+line_id+']'); |
|||
$line.find('.edit_amount').addClass('d-none'); |
|||
$line.find('.edit_amount_input').removeClass('d-none'); |
|||
$line.find('.edit_amount_input').focus(); |
|||
$line.find('.edit_amount_input').val(amount); |
|||
$line.find('.line_amount').addClass('d-none'); |
|||
}, |
|||
|
|||
//--------------------------------------------------------------------------
|
|||
// Private
|
|||
//--------------------------------------------------------------------------
|
|||
|
|||
/** |
|||
* @private |
|||
* @param {jQueryElement} $el |
|||
*/ |
|||
_destroyPopover: function ($el) { |
|||
var popover = $el.data('bs.popover'); |
|||
if (popover) { |
|||
popover.dispose(); |
|||
} |
|||
}, |
|||
/** |
|||
* @private |
|||
* @param {integer} partnerID |
|||
* @param {string} partnerName |
|||
* @returns {string} local id of the dataPoint |
|||
*/ |
|||
_makePartnerRecord: function (partnerID, partnerName) { |
|||
var field = { |
|||
relation: 'res.partner', |
|||
type: 'many2one', |
|||
name: 'partner_id', |
|||
}; |
|||
if (partnerID) { |
|||
field.value = [partnerID, partnerName]; |
|||
} |
|||
return this.model.makeRecord('account.bank.statement.line', [field], { |
|||
partner_id: { |
|||
domain: ["|", ["is_company", "=", true], ["parent_id", "=", false]], |
|||
options: { |
|||
no_open: true |
|||
} |
|||
} |
|||
}); |
|||
}, |
|||
|
|||
/** |
|||
* create account_id, tax_ids, analytic_account_id, analytic_tag_ids, label and amount fields |
|||
* |
|||
* @private |
|||
* @param {object} state - statement line |
|||
* @returns {Promise} |
|||
*/ |
|||
_renderCreate: function (state) { |
|||
var self = this; |
|||
return this.model.makeRecord('account.bank.statement.line', [{ |
|||
relation: 'account.account', |
|||
type: 'many2one', |
|||
name: 'account_id', |
|||
domain: [['company_id', '=', state.st_line.company_id], ['deprecated', '=', false]], |
|||
}, { |
|||
relation: 'account.journal', |
|||
type: 'many2one', |
|||
name: 'journal_id', |
|||
domain: [['company_id', '=', state.st_line.company_id]], |
|||
}, { |
|||
relation: 'account.tax', |
|||
type: 'many2many', |
|||
name: 'tax_ids', |
|||
domain: [['company_id', '=', state.st_line.company_id]], |
|||
}, { |
|||
relation: 'account.analytic.account', |
|||
type: 'many2one', |
|||
name: 'analytic_account_id', |
|||
}, { |
|||
relation: 'account.analytic.tag', |
|||
type: 'many2many', |
|||
name: 'analytic_tag_ids', |
|||
}, { |
|||
type: 'boolean', |
|||
name: 'force_tax_included', |
|||
}, { |
|||
type: 'char', |
|||
name: 'label', |
|||
}, { |
|||
type: 'float', |
|||
name: 'amount', |
|||
}, { |
|||
type: 'char', //TODO is it a bug or a feature when type date exists ?
|
|||
name: 'date', |
|||
}, { |
|||
type: 'boolean', |
|||
name: 'to_check', |
|||
}], { |
|||
account_id: { |
|||
string: _t("Account"), |
|||
}, |
|||
label: {string: _t("Label")}, |
|||
amount: {string: _t("Account")}, |
|||
}).then(function (recordID) { |
|||
self.handleCreateRecord = recordID; |
|||
var record = self.model.get(self.handleCreateRecord); |
|||
|
|||
self.fields.account_id = new relational_fields.FieldMany2One(self, |
|||
'account_id', record, {mode: 'edit', attrs: {can_create:false}}); |
|||
|
|||
self.fields.journal_id = new relational_fields.FieldMany2One(self, |
|||
'journal_id', record, {mode: 'edit'}); |
|||
|
|||
self.fields.tax_ids = new relational_fields.FieldMany2ManyTags(self, |
|||
'tax_ids', record, {mode: 'edit', additionalContext: {append_type_to_tax_name: true}}); |
|||
|
|||
self.fields.analytic_account_id = new relational_fields.FieldMany2One(self, |
|||
'analytic_account_id', record, {mode: 'edit'}); |
|||
|
|||
self.fields.analytic_tag_ids = new relational_fields.FieldMany2ManyTags(self, |
|||
'analytic_tag_ids', record, {mode: 'edit'}); |
|||
|
|||
self.fields.force_tax_included = new basic_fields.FieldBoolean(self, |
|||
'force_tax_included', record, {mode: 'edit'}); |
|||
|
|||
self.fields.label = new basic_fields.FieldChar(self, |
|||
'label', record, {mode: 'edit'}); |
|||
|
|||
self.fields.amount = new basic_fields.FieldFloat(self, |
|||
'amount', record, {mode: 'edit'}); |
|||
|
|||
self.fields.date = new basic_fields.FieldDate(self, |
|||
'date', record, {mode: 'edit'}); |
|||
|
|||
self.fields.to_check = new basic_fields.FieldBoolean(self, |
|||
'to_check', record, {mode: 'edit'}); |
|||
|
|||
var $create = $(qweb.render("reconciliation.line.create", {'state': state, 'group_tags': self.group_tags, 'group_acc': self.group_acc})); |
|||
self.fields.account_id.appendTo($create.find('.create_account_id .o_td_field')) |
|||
.then(addRequiredStyle.bind(self, self.fields.account_id)); |
|||
self.fields.journal_id.appendTo($create.find('.create_journal_id .o_td_field')); |
|||
self.fields.tax_ids.appendTo($create.find('.create_tax_id .o_td_field')); |
|||
self.fields.analytic_account_id.appendTo($create.find('.create_analytic_account_id .o_td_field')); |
|||
self.fields.analytic_tag_ids.appendTo($create.find('.create_analytic_tag_ids .o_td_field')); |
|||
self.fields.force_tax_included.appendTo($create.find('.create_force_tax_included .o_td_field')); |
|||
self.fields.label.appendTo($create.find('.create_label .o_td_field')) |
|||
.then(addRequiredStyle.bind(self, self.fields.label)); |
|||
self.fields.amount.appendTo($create.find('.create_amount .o_td_field')) |
|||
.then(addRequiredStyle.bind(self, self.fields.amount)); |
|||
self.fields.date.appendTo($create.find('.create_date .o_td_field')); |
|||
self.fields.to_check.appendTo($create.find('.create_to_check .o_td_field')); |
|||
self.$('.create').append($create); |
|||
|
|||
function addRequiredStyle(widget) { |
|||
widget.$el.addClass('o_required_modifier'); |
|||
} |
|||
}); |
|||
}, |
|||
|
|||
//--------------------------------------------------------------------------
|
|||
// Handlers
|
|||
//--------------------------------------------------------------------------
|
|||
/** |
|||
* The event on the partner m2o widget was propagated to the bank statement |
|||
* line widget, causing it to expand and the others to collapse. This caused |
|||
* the dropdown to be poorly placed and an unwanted update of this widget. |
|||
* |
|||
* @private |
|||
*/ |
|||
_onStopPropagation: function(ev) { |
|||
ev.stopPropagation(); |
|||
}, |
|||
|
|||
/** |
|||
* @private |
|||
* @param {MouseEvent} event |
|||
*/ |
|||
_onCreateReconcileModel: function (event) { |
|||
event.preventDefault(); |
|||
var self = this; |
|||
this.do_action({ |
|||
type: 'ir.actions.act_window', |
|||
res_model: 'account.reconcile.model', |
|||
views: [[false, 'form']], |
|||
target: 'current' |
|||
}, |
|||
{ |
|||
on_reverse_breadcrumb: function() {self.trigger_up('reload');}, |
|||
}); |
|||
}, |
|||
_editAmount: function (event) { |
|||
event.stopPropagation(); |
|||
var $line = $(event.target); |
|||
var moveLineId = $line.closest('.mv_line').data('line-id'); |
|||
this.trigger_up('partial_reconcile', {'data': {mvLineId: moveLineId, 'amount': $line.val()}}); |
|||
}, |
|||
_onEditAmount: function (event) { |
|||
event.preventDefault(); |
|||
event.stopPropagation(); |
|||
// Don't call when clicking inside the input field
|
|||
if (! $(event.target).hasClass('edit_amount_input')){ |
|||
var $line = $(event.target); |
|||
this.trigger_up('getPartialAmount', {'data': $line.closest('.mv_line').data('line-id')}); |
|||
} |
|||
}, |
|||
/** |
|||
* @private |
|||
* @param {MouseEvent} event |
|||
*/ |
|||
_onEditReconcileModel: function (event) { |
|||
event.preventDefault(); |
|||
var self = this; |
|||
this.do_action({ |
|||
type: 'ir.actions.act_window', |
|||
res_model: 'account.reconcile.model', |
|||
views: [[false, 'list'], [false, 'form']], |
|||
view_mode: "list", |
|||
target: 'current' |
|||
}, |
|||
{ |
|||
on_reverse_breadcrumb: function() {self.trigger_up('reload');}, |
|||
}); |
|||
}, |
|||
/** |
|||
* @private |
|||
* @param {OdooEvent} event |
|||
*/ |
|||
_onFieldChanged: function (event) { |
|||
event.stopPropagation(); |
|||
var fieldName = event.target.name; |
|||
if (fieldName === 'partner_id') { |
|||
var partner_id = event.data.changes.partner_id; |
|||
this.trigger_up('change_partner', {'data': partner_id}); |
|||
} else { |
|||
if (event.data.changes.amount && isNaN(event.data.changes.amount)) { |
|||
return; |
|||
} |
|||
this.trigger_up('update_proposition', {'data': event.data.changes}); |
|||
} |
|||
}, |
|||
/** |
|||
* @private |
|||
*/ |
|||
_onTogglePanel: function () { |
|||
if (this.$el[0].getAttribute('data-mode') == 'inactive') |
|||
this.trigger_up('change_mode', {'data': 'default'}); |
|||
}, |
|||
/** |
|||
* @private |
|||
*/ |
|||
_onChangeTab: function(event) { |
|||
if (event.currentTarget.nodeName === 'TFOOT') { |
|||
this.trigger_up('change_mode', {'data': 'next'}); |
|||
} else { |
|||
var modes = this.model.modes; |
|||
var selected_mode = modes.find(function(e) {return event.target.getAttribute('href').includes(e)}); |
|||
if (selected_mode) { |
|||
this.trigger_up('change_mode', {'data': selected_mode}); |
|||
} |
|||
} |
|||
}, |
|||
/** |
|||
* @private |
|||
* @param {input event} event |
|||
*/ |
|||
_onFilterChange: function (event) { |
|||
this.trigger_up('change_filter', {'data': _.str.strip($(event.target).val())}); |
|||
}, |
|||
/** |
|||
* @private |
|||
* @param {keyup event} event |
|||
*/ |
|||
_onInputKeyup: function (event) { |
|||
var target_partner_id = $(event.target).parents('[name="partner_id"]'); |
|||
if (target_partner_id.length === 1) { |
|||
return; |
|||
} |
|||
if(event.keyCode === 13) { |
|||
if ($(event.target).hasClass('edit_amount_input')) { |
|||
$(event.target).blur(); |
|||
return; |
|||
} |
|||
var created_lines = _.findWhere(this.model.lines, {mode: 'create'}); |
|||
if (created_lines && created_lines.balance.amount) { |
|||
this._onCreateProposition(); |
|||
} |
|||
return; |
|||
} |
|||
if ($(event.target).hasClass('edit_amount_input')) { |
|||
if (event.type === 'keyup') { |
|||
return; |
|||
} |
|||
else { |
|||
return this._editAmount(event); |
|||
} |
|||
} |
|||
|
|||
var self = this; |
|||
for (var fieldName in this.fields) { |
|||
var field = this.fields[fieldName]; |
|||
if (!field.$el.is(event.target)) { |
|||
continue; |
|||
} |
|||
this._avoidFieldUpdate[field.name] = event.type !== 'focusout'; |
|||
field.value = false; |
|||
field._setValue($(event.target).val()).then(function () { |
|||
self._avoidFieldUpdate[field.name] = false; |
|||
}); |
|||
break; |
|||
} |
|||
}, |
|||
/** |
|||
* @private |
|||
*/ |
|||
_onLoadMore: function (ev) { |
|||
ev.preventDefault(); |
|||
this.trigger_up('change_offset'); |
|||
}, |
|||
/** |
|||
* @private |
|||
* @param {MouseEvent} event |
|||
*/ |
|||
_onSelectMoveLine: function (event) { |
|||
var $el = $(event.target); |
|||
$el.prop('disabled', true); |
|||
this._destroyPopover($el); |
|||
var moveLineId = $el.closest('.mv_line').data('line-id'); |
|||
this.trigger_up('add_proposition', {'data': moveLineId}); |
|||
}, |
|||
/** |
|||
* @private |
|||
* @param {MouseEvent} event |
|||
*/ |
|||
_onSelectProposition: function (event) { |
|||
var $el = $(event.target); |
|||
this._destroyPopover($el); |
|||
var moveLineId = $el.closest('.mv_line').data('line-id'); |
|||
this.trigger_up('remove_proposition', {'data': moveLineId}); |
|||
}, |
|||
/** |
|||
* @private |
|||
* @param {MouseEvent} event |
|||
*/ |
|||
_onQuickCreateProposition: function (event) { |
|||
document.activeElement && document.activeElement.blur(); |
|||
this.trigger_up('quick_create_proposition', {'data': $(event.target).data('reconcile-model-id')}); |
|||
}, |
|||
/** |
|||
* @private |
|||
*/ |
|||
_onCreateProposition: function () { |
|||
document.activeElement && document.activeElement.blur(); |
|||
var invalid = []; |
|||
_.each(this.fields, function (field) { |
|||
if (!field.isValid()) { |
|||
invalid.push(field.string); |
|||
} |
|||
}); |
|||
if (invalid.length) { |
|||
this.do_warn(_t("Some fields are undefined"), invalid.join(', ')); |
|||
return; |
|||
} |
|||
this.trigger_up('create_proposition'); |
|||
}, |
|||
/** |
|||
* @private |
|||
*/ |
|||
_onValidate: function () { |
|||
this.trigger_up('validate'); |
|||
} |
|||
}); |
|||
|
|||
|
|||
/** |
|||
* rendering of the manual reconciliation action contains progress bar, title |
|||
* and auto reconciliation button |
|||
*/ |
|||
var ManualRenderer = StatementRenderer.extend({ |
|||
template: "reconciliation.manual.statement", |
|||
|
|||
}); |
|||
|
|||
|
|||
/** |
|||
* rendering of the manual reconciliation, contains line data, proposition and |
|||
* view for 'match' mode |
|||
*/ |
|||
var ManualLineRenderer = LineRenderer.extend({ |
|||
template: "reconciliation.manual.line", |
|||
/** |
|||
* @override |
|||
* @param {string} handle |
|||
* @param {number} proposition id (move line id) |
|||
* @returns {Promise} |
|||
*/ |
|||
removeProposition: function (handle, id) { |
|||
if (!id) { |
|||
return Promise.resolve(); |
|||
} |
|||
return this._super(handle, id); |
|||
}, |
|||
/** |
|||
* move the partner field |
|||
* |
|||
* @override |
|||
*/ |
|||
start: function () { |
|||
var self = this; |
|||
return this._super.apply(this, arguments).then(function () { |
|||
return self.model.makeRecord('account.move.line', [{ |
|||
relation: 'account.account', |
|||
type: 'many2one', |
|||
name: 'account_id', |
|||
value: [self._initialState.account_id.id, self._initialState.account_id.display_name], |
|||
}]).then(function (recordID) { |
|||
self.fields.title_account_id = new relational_fields.FieldMany2One(self, |
|||
'account_id', |
|||
self.model.get(recordID), |
|||
{mode: 'readonly'} |
|||
); |
|||
}).then(function () { |
|||
return self.fields.title_account_id.appendTo(self.$('.accounting_view thead td:eq(0) span:first')); |
|||
}); |
|||
}); |
|||
}, |
|||
/** |
|||
* @override |
|||
*/ |
|||
update: function (state) { |
|||
this._super(state); |
|||
var props = _.filter(state.reconciliation_proposition, {'display': true}); |
|||
if (!props.length) { |
|||
var $line = $(qweb.render("reconciliation.line.mv_line", {'line': {}, 'state': state})); |
|||
this.$('.accounting_view tbody').append($line); |
|||
} |
|||
}, |
|||
//--------------------------------------------------------------------------
|
|||
// Handlers
|
|||
//--------------------------------------------------------------------------
|
|||
/** |
|||
* display journal_id field |
|||
* |
|||
* @override |
|||
*/ |
|||
_renderCreate: function (state) { |
|||
var self = this; |
|||
var parentPromise = this._super(state).then(function() { |
|||
self.$('.create .create_journal_id').show(); |
|||
self.$('.create .create_date').removeClass('d-none'); |
|||
self.$('.create .create_journal_id .o_input').addClass('o_required_modifier'); |
|||
}); |
|||
return parentPromise; |
|||
}, |
|||
|
|||
}); |
|||
|
|||
|
|||
return { |
|||
StatementRenderer: StatementRenderer, |
|||
ManualRenderer: ManualRenderer, |
|||
LineRenderer: LineRenderer, |
|||
ManualLineRenderer: ManualLineRenderer, |
|||
}; |
|||
}); |
@ -0,0 +1,402 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
|
|||
<templates xml:space="preserve"> |
|||
|
|||
<div t-name="reconciliation" class="o_reconciliation"> |
|||
<div class="o_form_view"> |
|||
<div class="o_form_sheet_bg"> |
|||
<div class="o_form_sheet"/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<t t-name="reconciliation.control.pager"> |
|||
<div class="progress progress-reconciliation"> |
|||
<div aria-valuemin="0" t-att-aria-valuenow="widget._initialState.valuenow" t-att-aria-valuemax="widget._initialState.valuemax" class="progress-bar" role="progressbar" style="width: 0%;"><span class="valuenow"><t t-esc="widget._initialState.valuenow"/></span> / <span class="valuemax"><t t-esc="widget._initialState.valuemax"/></span></div> |
|||
</div> |
|||
</t> |
|||
|
|||
<t t-name="reconciliation.statement"> |
|||
<div t-if="widget._initialState.valuemax"> |
|||
<div class="notification_area"/> |
|||
<div class="o_reconciliation_lines"/> |
|||
<div t-if="widget._initialState.valuemax > widget._initialState.defaultDisplayQty"> |
|||
<button class="btn btn-secondary js_load_more">Load more</button> |
|||
</div> |
|||
</div> |
|||
<div t-else="" class="o_view_noreconciliation"> |
|||
<p>Nothing to do!</p> |
|||
<p>This page displays all the bank transactions that are to be reconciled and provides with a neat interface to do so.</p> |
|||
</div> |
|||
</t> |
|||
|
|||
<t t-name="reconciliation.manual.statement" t-extend="reconciliation.statement"> |
|||
<t t-jquery="div:first" t-operation="attributes"> |
|||
<attribute name="class" value="o_manual_statement" /> |
|||
</t> |
|||
<t t-jquery=".o_view_noreconciliation p" t-operation="replace"></t> |
|||
<!-- <t t-jquery=".o_filter_input_wrapper" t-operation="replace"></t>--> |
|||
<t t-jquery=".o_view_noreconciliation" t-operation="append"> |
|||
<p><b>Good Job!</b> There is nothing to reconcile.</p> |
|||
<p>All invoices and payments have been matched, your accounts' balances are clean.</p> |
|||
<p> |
|||
From now on, you may want to: |
|||
<ul> |
|||
<li>Check that you have no bank statement lines to <a href="#" |
|||
rel="do_action" |
|||
data-tag="bank_statement_reconciliation_view">reconcile</a></li> |
|||
<li>Verify <a href="#" |
|||
rel="do_action" |
|||
data-action_name="Unpaid Customer Invoices" |
|||
data-model="account.move" |
|||
data-domain="[('move_type', 'in', ('out_invoice', 'out_refund'))]" |
|||
data-context="{'search_default_unpaid': 1}">unpaid invoices</a> and follow-up customers</li> |
|||
<li>Pay your <a href="#" |
|||
rel="do_action" |
|||
data-action_name="Unpaid Vendor Bills" |
|||
data-model="account.move" |
|||
data-domain="[('move_type', 'in', ('in_invoice', 'in_refund'))]" |
|||
data-context="{'search_default_unpaid': 1}">vendor bills</a></li> |
|||
<li>Check all <a href="#" |
|||
rel="do_action" |
|||
data-action_name="Unreconciled Entries" |
|||
data-model="account.move.line" |
|||
data-context="{'search_default_unreconciled': 1}">unreconciled entries</a></li> |
|||
</ul> |
|||
</p> |
|||
</t> |
|||
</t> |
|||
|
|||
<div t-name="reconciliation.done" class="done_message"> |
|||
<h2>Congrats, you're all done!</h2> |
|||
<p>You reconciled <strong><t t-esc="number"/></strong> transactions in <strong><t t-esc="duration"/></strong>. |
|||
<t t-if="number > 1"> |
|||
<br/>That's on average <t t-esc="timePerTransaction"/> seconds per transaction. |
|||
</t> |
|||
</p> |
|||
<t t-if="context && context.active_model"> |
|||
<p t-if="context['active_model'] === 'account.journal' || context['active_model'] === 'account.bank.statement' || context['active_model'] === 'account.bank.statement.import'" class="actions_buttons"> |
|||
<t t-if="context.journal_id"> |
|||
<button class="button_back_to_statement btn btn-secondary" t-att-data_journal_id='context.journal_id'>Go to bank statement(s)</button> |
|||
</t> |
|||
<t t-if="context['active_model'] === 'account.bank.statement'"> |
|||
<button class="button_close_statement btn btn-primary" style="display: inline-block;">Close statement</button> |
|||
</t> |
|||
</p> |
|||
</t> |
|||
</div> |
|||
|
|||
<t t-name="reconciliation.line"> |
|||
<t t-set="state" t-value="widget._initialState"/> |
|||
<div class="o_reconciliation_line" t-att-data-mode="state.mode" tabindex="0"> |
|||
<table class="accounting_view"> |
|||
<caption style="caption-side: top;"> |
|||
<div class="float-right o_buttons"> |
|||
<button t-attf-class="o_no_valid btn btn-secondary #{state.balance.type < 0 ? '' : 'd-none'}" disabled="disabled" data-toggle="tooltip" title="Select a partner or choose a counterpart" accesskey="">Validate</button> |
|||
<button t-attf-class="o_validate btn btn-secondary #{!state.balance.type ? '' : 'd-none'}">Validate</button> |
|||
<button t-attf-class="o_reconcile btn btn-primary #{state.balance.type > 0 ? '' : 'd-none'}">Validate</button> |
|||
</div> |
|||
</caption> |
|||
<thead> |
|||
<tr> |
|||
<td class="cell_account_code"><t t-esc="state.st_line.account_code"/></td> |
|||
<td class="cell_due_date"><t t-esc="state.st_line.date"/></td> |
|||
<td class="cell_label"><t t-if="state.st_line.name" t-esc="state.st_line.name"/> <t t-if="state.st_line.amount_currency_str"> (<t t-esc="state.st_line.amount_currency_str"/>)</t></td> |
|||
<td class="cell_left"><t t-if="state.st_line.amount > 0"><t t-raw="state.st_line.amount_str"/></t></td> |
|||
<td class="cell_right"><t t-if="state.st_line.amount < 0"><t t-raw="state.st_line.amount_str"/></t></td> |
|||
<td class="cell_info_popover"></td> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
<t t-foreach="state.reconciliation_proposition" t-as="line"><t t-call="reconciliation.line.mv_line"/></t> |
|||
</tbody> |
|||
<tfoot> |
|||
<t t-call="reconciliation.line.balance"/> |
|||
</tfoot> |
|||
</table> |
|||
<div class="o_notebook"> |
|||
<div class="o_notebook_headers"> |
|||
<ul class="nav nav-tabs ml-0 mr-0"> |
|||
<li class="nav-item" t-attf-title="{{'Match statement with existing lines on receivable/payable accounts<br>* Black line: existing journal entry that should be matched<br>* Blue lines: existing payment that should be matched'}}" data-toggle="tooltip"><a data-toggle="tab" disable_anchor="true" t-attf-href="#notebook_page_match_rp_#{state.st_line.id}" class="nav-link active nav-match_rp" role="tab" aria-selected="true">Customer/Vendor Matching</a></li> |
|||
<li class="nav-item" title="Match with entries that are not from receivable/payable accounts" data-toggle="tooltip"><a data-toggle="tab" disable_anchor="true" t-attf-href="#notebook_page_match_other_#{state.st_line.id}" class="nav-link nav-match_other" role="tab" aria-selected="false">Miscellaneous Matching</a></li> |
|||
<li class="nav-item" title="Create a counterpart" data-toggle="tooltip"><a data-toggle="tab" disable_anchor="true" t-attf-href="#notebook_page_create_#{state.st_line.id}" class="nav-link nav-create" role="tab" aria-selected="false">Manual Operations</a></li> |
|||
</ul> |
|||
</div> |
|||
<div class="tab-content"> |
|||
<div class="tab-pane active" t-attf-id="notebook_page_match_rp_#{state.st_line.id}"> |
|||
<div class="match"> |
|||
<t t-call="reconciliation.line.match"/> |
|||
</div> |
|||
</div> |
|||
<div class="tab-pane" t-attf-id="notebook_page_match_other_#{state.st_line.id}"> |
|||
<div class="match"> |
|||
<t t-call="reconciliation.line.match"/> |
|||
</div> |
|||
</div> |
|||
<div class="tab-pane" t-attf-id="notebook_page_create_#{state.st_line.id}"> |
|||
<div class="create"></div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</t> |
|||
|
|||
<t t-name="reconciliation.manual.line" t-extend="reconciliation.line"> |
|||
<t t-jquery=".o_buttons" t-operation="replace"> |
|||
<div class="float-right o_buttons"> |
|||
<button t-attf-class="o_validate btn btn-secondary #{!state.balance.type ? '' : 'd-none'}">Reconcile</button> |
|||
<button t-attf-class="o_reconcile btn btn-primary #{state.balance.type > 0 ? '' : 'd-none'}">Reconcile</button> |
|||
<button t-attf-class="o_no_valid btn btn-secondary #{state.balance.type < 0 ? '' : 'd-none'}">Skip</button> |
|||
</div> |
|||
</t> |
|||
<t t-jquery=".accounting_view tbody" t-operation="append"> |
|||
<t t-if='!_.filter(state.reconciliation_proposition, {"display": true}).length'> |
|||
<t t-set="line" t-value='{}'/> |
|||
<t t-call="reconciliation.line.mv_line"/> |
|||
</t> |
|||
</t> |
|||
<t t-jquery=".accounting_view thead tr" t-operation="replace"> |
|||
<tr> |
|||
<td colspan="3"><span/><span t-if="state.last_time_entries_checked">Last Reconciliation: <t t-esc="state.last_time_entries_checked"/></span></td> |
|||
<td colspan="2"><t t-esc="state.st_line.account_code"/></td> |
|||
<td class="cell_info_popover"></td> |
|||
</tr> |
|||
</t> |
|||
<t t-jquery='div[t-attf-id*="notebook_page_match_rp"]' t-operation="replace"/> |
|||
<t t-jquery='a[t-attf-href*="notebook_page_match_rp"]' t-operation="replace"/> |
|||
</t> |
|||
|
|||
<t t-name="reconciliation.line.balance"> |
|||
<tr t-if="state.balance.show_balance"> |
|||
<td class="cell_account_code"><t t-esc="state.balance.account_code"/></td> |
|||
<td class="cell_due_date"></td> |
|||
<td class="cell_label"><t t-if="state.st_line.partner_id">Open balance</t><t t-else="">Choose counterpart or Create Write-off</t></td> |
|||
<td class="cell_left"><t t-if="state.balance.amount_currency < 0"><span role="img" t-if="state.balance.amount_currency_str" t-attf-class="o_multi_currency o_multi_currency_color_#{state.balance.currency_id%8} line_info_button fa fa-money" t-att-data-content="state.balance.amount_currency_str" t-att-aria-label="state.balance.amount_currency_str" t-att-title="state.balance.amount_currency_str"/><t t-raw="state.balance.amount_str"/></t></td> |
|||
<td class="cell_right"><t t-if="state.balance.amount_currency > 0"><span role="img" t-if="state.balance.amount_currency_str" t-attf-class="o_multi_currency o_multi_currency_color_#{state.balance.currency_id%8} line_info_button fa fa-money" t-att-data-content="state.balance.amount_currency_str" t-att-aria-label="state.balance.amount_currency_str" t-att-title="state.balance.amount_currency_str"/><t t-raw="state.balance.amount_str"/></t></td> |
|||
<td class="cell_info_popover"></td> |
|||
</tr> |
|||
</t> |
|||
|
|||
|
|||
<div t-name="reconciliation.line.match"> |
|||
<div class="match_controls"> |
|||
<span><input class="filter o_input" placeholder="Filter on account, label, partner, amount,..." type="text" t-att-value="state['filter_{{state.mode}}']"/></span> |
|||
<button class="btn btn-secondary btn-sm fa fa-search" type="button"></button> |
|||
</div> |
|||
<table> |
|||
<tbody> |
|||
</tbody> |
|||
</table> |
|||
<div class="load-more text-center"> |
|||
<a href="#">Load more... (<span></span> remaining)</a> |
|||
</div> |
|||
</div> |
|||
|
|||
|
|||
<div t-name="reconciliation.line.create"> |
|||
<div class="quick_add"> |
|||
<div class="btn-group o_reconcile_models" t-if="state.reconcileModels"> |
|||
<t t-foreach="state.reconcileModels" t-as="reconcileModel"> |
|||
<button class="btn btn-primary" |
|||
t-if="reconcileModel.rule_type === 'writeoff_button' && (reconcileModel.match_journal_ids.length == 0 || reconcileModel.match_journal_ids.includes(state.st_line.journal_id) || state.st_line.journal_id === undefined)" |
|||
t-att-data-reconcile-model-id="reconcileModel.id"> |
|||
<t t-esc="reconcileModel.name"/> |
|||
</button> |
|||
</t> |
|||
<p t-if="!state.reconcileModels.length" style="color: #bbb;">To speed up reconciliation, define <a style="cursor: pointer;" class="reconcile_model_create">reconciliation models</a>.</p> |
|||
</div> |
|||
<div class="dropdown float-right"> |
|||
<a data-toggle="dropdown" href="#"><span class="fa fa-cog" role="img" aria-label="Settings"/></a> |
|||
<div class="dropdown-menu dropdown-menu-right" role="menu" aria-label="Presets config"> |
|||
<a role="menuitem" class="dropdown-item reconcile_model_create" href="#">Create model</a> |
|||
<a role="menuitem" class="dropdown-item reconcile_model_edit" href="#">Modify models</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="clearfix o_form_sheet"> |
|||
<div class="o_group"> |
|||
<table class="o_group o_inner_group o_group_col_6"> |
|||
<tbody> |
|||
<tr class="create_account_id"> |
|||
<td class="o_td_label"><label class="o_form_label">Account</label></td> |
|||
<td class="o_td_field"></td> |
|||
</tr> |
|||
<tr class="create_tax_id"> |
|||
<td class="o_td_label"><label class="o_form_label">Taxes</label></td> |
|||
<td class="o_td_field"></td> |
|||
</tr> |
|||
<tr class="create_analytic_account_id" t-if="group_acc"> |
|||
<td class="o_td_label"><label class="o_form_label">Analytic Acc.</label></td> |
|||
<td class="o_td_field"></td> |
|||
</tr> |
|||
<tr class="create_analytic_tag_ids" t-if="group_tags"> |
|||
<td class="o_td_label"><label class="o_form_label">Analytic Tags.</label></td> |
|||
<td class="o_td_field"></td> |
|||
</tr> |
|||
</tbody> |
|||
</table> |
|||
<table class="o_group o_inner_group o_group_col_6"> |
|||
<tbody> |
|||
<tr class="create_journal_id" style="display: none;"> |
|||
<td class="o_td_label"><label class="o_form_label">Journal</label></td> |
|||
<td class="o_td_field"></td> |
|||
</tr> |
|||
<tr class="create_label"> |
|||
<td class="o_td_label"><label class="o_form_label">Label</label></td> |
|||
<td class="o_td_field"></td> |
|||
</tr> |
|||
<tr class="create_amount"> |
|||
<td class="o_td_label"><label class="o_form_label">Amount</label></td> |
|||
<td class="o_td_field"></td> |
|||
</tr> |
|||
<tr class="create_force_tax_included d-none"> |
|||
<td class="o_td_label"><label class="o_form_label">Tax Included in Price</label></td> |
|||
<td class="o_td_field"></td> |
|||
</tr> |
|||
<tr class="create_date d-none"> |
|||
<td class="o_td_label"><label class="o_form_label">Writeoff Date</label></td> |
|||
<td class="o_td_field"></td> |
|||
</tr> |
|||
<tr class="create_to_check"> |
|||
<td class="o_td_label"><label class="o_form_label">To Check</label></td> |
|||
<td class="o_td_field"></td> |
|||
</tr> |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
</div> |
|||
<div class="add_line_container"> |
|||
<a class="add_line" t-att-style="!state.balance.amout ? 'display: none;' : null"><i class="fa fa-plus-circle"/> Save and New</a> |
|||
</div> |
|||
</div> |
|||
|
|||
|
|||
<t t-name="reconciliation.line.mv_line.amount"> |
|||
<span t-att-class="(line.is_move_line && proposition == true) ? 'cell' : ''"> |
|||
<span class="line_amount"> |
|||
<span t-if="line.amount_currency_str" |
|||
t-attf-class="o_multi_currency o_multi_currency_color_#{line.currency_id%8} line_info_button fa fa-money" |
|||
t-att-data-content="line.amount_currency_str"/> |
|||
<span t-if="line.partial_amount && line.partial_amount != line.amount" class="strike_amount text-muted"> |
|||
<t t-raw="line.amount_str"/> |
|||
<br/> |
|||
</span> |
|||
</span> |
|||
<t t-if="line.is_move_line && proposition == true"> |
|||
<i class="fa fa-pencil edit_amount"></i> |
|||
<input class="edit_amount_input text-right d-none"/> |
|||
</t> |
|||
<span class="line_amount"> |
|||
<t t-if="!line.partial_amount_str" t-raw="line.amount_str"/> |
|||
<t t-if="line.partial_amount_str && line.partial_amount != line.amount" t-raw="line.partial_amount_str"/> |
|||
</span> |
|||
</span> |
|||
</t> |
|||
|
|||
|
|||
<t t-name="reconciliation.line.mv_line"> |
|||
<tr t-if="line.display !== false" t-attf-class="mv_line #{line.already_paid ? ' already_reconciled' : ''} #{line.__invalid ? 'invalid' : ''} #{line.is_tax ? 'is_tax' : ''}" t-att-data-line-id="line.id" t-att-data-selected="selected"> |
|||
<td class="cell_account_code"><t t-esc="line.account_code"/>​</td> <!-- zero width space to make empty lines the height of the text --> |
|||
<td class="cell_due_date"> |
|||
<t t-if="typeof(line.id) != 'number' && line.id"> |
|||
<span class="badge badge-secondary">New</span> |
|||
</t> |
|||
<t t-else="" t-esc="line.date_maturity || line.date"/> |
|||
</td> |
|||
<td class="cell_label"> |
|||
<t t-if="line.partner_id && line.partner_id !== state.st_line.partner_id"> |
|||
<t t-if="line.partner_name.length"> |
|||
<span class="font-weight-bold" t-esc="line.partner_name"/>: |
|||
</t> |
|||
</t> |
|||
<t t-esc="line.label || line.name"/> |
|||
<t t-if="line.ref && line.ref.length"> : </t> |
|||
<t t-esc="line.ref"/> |
|||
</td> |
|||
<td class="cell_left"> |
|||
<t t-if="line.amount < 0"> |
|||
<t t-call="reconciliation.line.mv_line.amount"/> |
|||
</t> |
|||
</td> |
|||
<td class="cell_right"> |
|||
<t t-if="line.amount > 0"> |
|||
<t t-call="reconciliation.line.mv_line.amount"/> |
|||
</t> |
|||
</td> |
|||
<td class="cell_info_popover"></td> |
|||
</tr> |
|||
</t> |
|||
|
|||
|
|||
<t t-name="reconciliation.line.mv_line.details"> |
|||
<table class='details'> |
|||
<tr t-if="line.account_code"><td>Account</td><td><t t-esc="line.account_code"/> <t t-esc="line.account_name"/></td></tr> |
|||
<tr><td>Date</td><td><t t-esc="line.date"/></td></tr> |
|||
<tr><td>Due Date</td><td><t t-esc="line.date_maturity || line.date"/></td></tr> |
|||
<tr><td>Journal</td><td><t t-esc="line.journal_id.display_name"/></td></tr> |
|||
<tr t-if="line.partner_id"><td>Partner</td><td><t t-esc="line.partner_name"/></td></tr> |
|||
<tr><td>Label</td><td><t t-esc="line.label"/></td></tr> |
|||
<tr t-if="line.ref"><td>Ref</td><td><t t-esc="line.ref"/></td></tr> |
|||
<tr><td>Amount</td><td><t t-raw="line.total_amount_str"/><t t-if="line.total_amount_currency_str"> (<t t-esc="line.total_amount_currency_str"/>)</t></td></tr> |
|||
<tr t-if="line.is_partially_reconciled"><td>Residual</td><td> |
|||
<t t-raw="line.amount_str"/><t t-if="line.amount_currency_str"> (<t t-esc="line.amount_currency_str"/>)</t> |
|||
</td></tr> |
|||
<tr class="one_line_info" t-if='line.already_paid'> |
|||
<td colspan="2">This payment is registered but not reconciled.</td> |
|||
</tr> |
|||
</table> |
|||
</t> |
|||
|
|||
|
|||
<t t-name="reconciliation.line.statement_line.details"> |
|||
<table class='details'> |
|||
<tr><td>Date</td><td><t t-esc="state.st_line.date"/></td></tr> |
|||
<tr t-if="state.st_line.partner_name"><td>Partner</td><td><t t-esc="state.st_line.partner_name"/></td></tr> |
|||
<tr t-if="state.st_line.ref"><td>Transaction</td><td><t t-esc="state.st_line.ref"/></td></tr> |
|||
<tr><td>Description</td><td><t t-esc="state.st_line.name"/></td></tr> |
|||
<tr><td>Amount</td><td><t t-raw="state.st_line.amount_str"/><t t-if="state.st_line.amount_currency_str"> (<t t-esc="state.st_line.amount_currency_str"/>)</t></td></tr> |
|||
<tr><td>Account</td><td><t t-esc="state.st_line.account_code"/> <t t-esc="state.st_line.account_name"/></td></tr> |
|||
<tr t-if="state.st_line.note"><td>Note</td><td style="white-space: pre;"><t t-esc="state.st_line.note"/></td></tr> |
|||
</table> |
|||
</t> |
|||
|
|||
|
|||
<t t-name="reconciliation.notification.reconciled"> |
|||
<t t-if="details !== undefined"> |
|||
<a rel="do_action" href="#" aria-label="External link" title="External link" |
|||
t-att-data-action_name="details.name" |
|||
t-att-data-model="details.model" |
|||
t-att-data-ids="details.ids"> |
|||
<t t-esc="nb_reconciled_lines"/> |
|||
statement lines |
|||
</a> |
|||
have been reconciled automatically. |
|||
</t> |
|||
</t> |
|||
|
|||
|
|||
<t t-name="reconciliation.notification.default"> |
|||
<t t-esc="message"/> |
|||
<t t-if="details !== undefined"> |
|||
<a class="fa fa-external-link" rel="do_action" href="#" aria-label="External link" title="External link" |
|||
t-att-data-action_name="details.name" |
|||
t-att-data-model="details.model" |
|||
t-att-data-ids="details.ids"> |
|||
</a> |
|||
</t> |
|||
</t> |
|||
|
|||
|
|||
<t t-name="reconciliation.notification"> |
|||
<div t-att-class="'notification alert-dismissible alert alert-' + type" role="alert"> |
|||
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span title="Close" class="fa fa-times"></span></button> |
|||
<t t-if="template"> |
|||
<t t-call="{{template}}"/> |
|||
</t> |
|||
<t t-else=""> |
|||
<t t-call="reconciliation.notification.default"/> |
|||
</t> |
|||
</div> |
|||
</t> |
|||
|
|||
</templates> |
@ -0,0 +1,34 @@ |
|||
<?xml version="1.0" encoding="utf-8" ?> |
|||
<odoo> |
|||
<record id="payment_matching_view" model="ir.ui.view"> |
|||
<field name="name">account.journal.dashboard.payment</field> |
|||
<field name="model">account.journal</field> |
|||
<field name="inherit_id" ref="account.account_journal_dashboard_kanban_view"/> |
|||
<field name="arch" type="xml"> |
|||
<xpath expr="//div[@id='sale_purchase_refund']" position="after"> |
|||
<div> |
|||
<a type="object" name="action_open_reconcile" groups="account.group_account_user">Payments Matching</a> |
|||
</div> |
|||
</xpath> |
|||
<xpath expr="//div[@id='dashboard_bank_cash_left']/*[1]" position="before"> |
|||
<t t-if="dashboard.number_to_reconcile > 0"> |
|||
<button type="object" name="action_open_reconcile" class="btn btn-primary" groups="account.group_account_user"> Reconcile <t t-esc="dashboard.number_to_reconcile"/> Items</button> |
|||
</t> |
|||
</xpath> |
|||
</field> |
|||
</record> |
|||
<record id="matching_account_payment" model="ir.ui.view"> |
|||
<field name="name">account.payment.matching</field> |
|||
<field name="model">account.payment</field> |
|||
<field name="inherit_id" ref="account.view_account_payment_form" /> |
|||
<field name="arch" type="xml"> |
|||
<xpath expr="//button[@name='button_open_statements']" position="after"> |
|||
<button class="oe_stat_button" name="open_payment_matching_screen" |
|||
string="Payment Matching" type="object" |
|||
groups="account.group_account_user" |
|||
attrs="{'invisible':[('is_reconciled','=',True)]}" icon="fa-dollar"/> |
|||
<field name="is_reconciled" invisible="1"/> |
|||
</xpath> |
|||
</field> |
|||
</record> |
|||
</odoo> |
Loading…
Reference in new issue