You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
615 lines
31 KiB
Python
615 lines
31 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
from collections import defaultdict
|
|
import re
|
|
|
|
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, frozendict, get_lang
|
|
from odoo.tools import html2plaintext
|
|
|
|
|
|
class AccountReconciliation(models.AbstractModel):
|
|
_name = 'account.reconciliation.widget'
|
|
_description = 'Account Reconciliation widget'
|
|
|
|
####################################################
|
|
# Public
|
|
####################################################
|
|
|
|
@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 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_reconcile_modelds_for_manual_reconciliation(self, company_ids):
|
|
""" Collect the available account.reconcile.model records for the manual reconciliation widget.
|
|
|
|
:param company_ids: A list of res.company ids.
|
|
:return: The result of read for each available record.
|
|
"""
|
|
reco_models = self.env['account.reconcile.model'] \
|
|
.search([
|
|
('rule_type', '=', 'writeoff_button'),
|
|
('company_id', 'in', company_ids),
|
|
]) \
|
|
.filtered(lambda x: not set(x.mapped('line_ids.amount_type')).intersection({'percentage_st_line', 'regex'}))
|
|
return reco_models.read()
|
|
|
|
@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)
|
|
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:
|
|
accounts_data = self.get_data_for_manual_reconciliation('account', account_ids)
|
|
return {
|
|
'customers': self.get_data_for_manual_reconciliation('partner', partner_ids, 'asset_receivable'),
|
|
'suppliers': self.get_data_for_manual_reconciliation('partner', partner_ids, 'liability_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 'liability_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 ('liability_payable', 'asset_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 ' ')
|
|
lang = self.env.user.lang or get_lang(self.env).code
|
|
query = ("""
|
|
SELECT {select} account_id, account_name, account_code, max_date
|
|
FROM (
|
|
SELECT {inner_select}
|
|
a.id AS account_id,
|
|
{account_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)
|
|
{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 ((a.account_type <> 'liability_payable' AND a.account_type <> 'asset_receivable') OR l.partner_id IS NULL)",
|
|
where2=account_type and "AND a.account_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 ' ',
|
|
account_name=f"COALESCE(a.name->>'{lang}', a.name->>'en_US')" if self.pool['account.account'].name.translate else 'a.name'
|
|
))
|
|
self.env['account.move.line'].flush_model()
|
|
self.env['account.account'].flush_model()
|
|
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 == 'asset_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()
|
|
"""
|
|
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 = self.env['res.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 _get_search_domain(self, search_str=''):
|
|
''' Get the domain to be applied on the account.move.line when the user is typing
|
|
something on the search bar.
|
|
:param search_str: The search bar content as a string.
|
|
:return: A applicable domain on the account.move.line model.
|
|
'''
|
|
search_str = search_str.strip()
|
|
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.account_type', 'in', ('asset_cash', 'liability_credit_card')),
|
|
'|', '|', '|', ('debit', '=', amount), ('credit', '=', amount), ('amount_currency', '=', amount), ('amount_currency', '=', -amount),
|
|
]
|
|
str_domain = expression.OR([str_domain, amount_domain])
|
|
except:
|
|
pass
|
|
|
|
return expression.OR([str_domain, [('partner_id.name', 'ilike', search_str)]])
|
|
|
|
@api.model
|
|
def _domain_move_lines_for_manual_reconciliation(self, account_id, partner_id=False, excluded_ids=None, search_str=''):
|
|
""" Create domain criteria that are relevant to manual reconciliation. """
|
|
domain = ['&', '&', ('reconciled', '=', False), ('account_id', '=', account_id), ('move_id.state', '=', 'posted')]
|
|
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._get_search_domain(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
|
|
"""
|
|
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': line.name and line.name != '/' and line.move_id.name != line.name and ': ' + line.name or '',
|
|
'move_name': 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],
|
|
'is_liquidity_line': line.account_id.account_type in ('asset_cash', 'liability_credit_card'),
|
|
'account_code': line.account_id.code,
|
|
'account_name': line.account_id.name,
|
|
'account_type': line.account_id.account_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.account_type in ('asset_cash', 'liability_credit_card'):
|
|
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['balance'] = amount
|
|
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_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
|
|
|
|
def _prepare_writeoff_move_vals(self, move_lines, vals_list):
|
|
aggr = defaultdict(list)
|
|
for vals in vals_list:
|
|
# detect if it is a tax case (the balance still exists) and manage the 0% tax case
|
|
if vals.get('tax_repartition_line_id') and self.env.company.currency_id.is_zero(vals['balance']):
|
|
continue
|
|
move_vals = self._prepare_writeoff_moves(move_lines, vals)
|
|
grouping = frozendict({k: v for k, v in move_vals.items() if k != 'line_ids'})
|
|
aggr[grouping].extend(move_vals['line_ids'])
|
|
return [{**grouping, 'line_ids': line_ids} for grouping, line_ids in aggr.items()]
|
|
|
|
@api.model
|
|
def _prepare_writeoff_moves(self, move_lines, 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."))
|
|
|
|
move_fields = {'journal_id', 'date'}
|
|
move_vals = {k: v for k, v in vals.items() if k in move_fields}
|
|
|
|
company_currency = move_lines.company_id.currency_id
|
|
currencies = set(line.currency_id for line in move_lines)
|
|
currency = list(currencies)[0] if len(currencies) == 1 else company_currency
|
|
|
|
line_vals = {
|
|
**{k: v for k, v in vals.items() if k not in move_fields},
|
|
'partner_id': move_lines[0].partner_id.id,
|
|
'sequence': 10,
|
|
}
|
|
|
|
if 'debit' not in vals and 'credit' not in vals:
|
|
balance = -vals.get('balance', 0.0) or sum(move_lines.mapped('amount_residual'))
|
|
else:
|
|
balance = vals.get('credit', 0.0) - vals.get('debit', 0.0)
|
|
line_vals['balance'] = balance
|
|
|
|
if currency == company_currency:
|
|
line_vals['amount_currency'] = balance
|
|
line_vals['currency_id'] = company_currency.id
|
|
else:
|
|
if 'amount_currency' in vals:
|
|
line_vals['amount_currency'] = -vals['amount_currency']
|
|
else:
|
|
line_vals['amount_currency'] = sum(move_lines.mapped('amount_residual_currency'))
|
|
line_vals['currency_id'] = currency.id
|
|
|
|
move_vals['line_ids'] = [
|
|
(0, 0, line_vals),
|
|
(0, 0, {
|
|
'name': _('Write-Off'),
|
|
'balance': -line_vals['balance'],
|
|
'amount_currency': -line_vals['amount_currency'],
|
|
'currency_id': currency.id,
|
|
'account_id': move_lines[0].account_id.id,
|
|
'partner_id': move_lines[0].partner_id.id,
|
|
'sequence': 20,
|
|
}),
|
|
]
|
|
return move_vals
|
|
|
|
@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.'))
|
|
|
|
move_lines = self.env['account.move.line'].browse(move_line_ids)
|
|
|
|
# Create writeoff move lines
|
|
if len(new_mv_line_dicts) > 0:
|
|
move_vals_list = self._prepare_writeoff_move_vals(move_lines, new_mv_line_dicts)
|
|
moves = self.env['account.move'].create(move_vals_list)
|
|
moves.action_post()
|
|
account = move_lines[0].account_id
|
|
move_lines |= moves.line_ids.filtered(lambda line: line.account_id == account and not line.reconciled)
|
|
move_lines.reconcile()
|
|
|
|
@api.model
|
|
def get_reconciliation_dict_from_model(self, model_id, residual_balance, widget_partner_id):
|
|
""" Returns the values to be used by the reconciliation widget widget in order
|
|
to apply the provided reconciliation model to the provided statement line
|
|
(hence the values used to create the lines in the widget).
|
|
"""
|
|
model = self.env['account.reconcile.model'].browse(model_id)
|
|
new_aml_dicts = model._get_write_off_move_lines_dict(residual_balance, widget_partner_id)
|
|
self._complete_write_off_vals_for_widget(new_aml_dicts)
|
|
return new_aml_dicts
|
|
|
|
@api.model
|
|
def _complete_write_off_vals_for_widget(self, new_aml_dicts):
|
|
for line in new_aml_dicts:
|
|
balance = line.get('balance', 0.0)
|
|
line.update({
|
|
'debit': balance if balance > 0.0 else 0.0,
|
|
'credit': -balance if balance < 0.0 else 0.0,
|
|
})
|
|
for m2o_name in ('account_id', 'journal_id', 'partner_id', 'analytic_account_id'):
|
|
if line.get(m2o_name) and not isinstance(line[m2o_name], dict):
|
|
m2o_record = self.env[self.env['account.move.line']._fields[m2o_name].comodel_name].browse(line[m2o_name])
|
|
if m2o_name == 'analytic_account_id':
|
|
line['analytic_distribution'] = {m2o_record.id: 100}
|
|
else:
|
|
line[m2o_name] = {'display_name': m2o_record.display_name, 'id': m2o_record.id}
|
|
if m2o_name == 'account_id':
|
|
line['account_code'] = m2o_record.code
|
|
for x2m_name in ('tax_ids', 'tax_tag_ids'):
|
|
if line.get(x2m_name) and not isinstance(line[x2m_name][0], dict):
|
|
x2m_records = self.env[self.env['account.move.line']._fields[x2m_name].comodel_name].browse(line[x2m_name][0][2])
|
|
line[x2m_name] = [{'display_name': r.display_name, 'id': r.id} for r in x2m_records]
|
|
if 'reconcile_model_id' in line:
|
|
line['to_check'] = self.env['account.reconcile.model'].browse(line['reconcile_model_id']).to_check
|
|
|
|
@api.model
|
|
def open_rec_model_creation_widget(self, rec_propositions, st_line_amount, to_check):
|
|
""" Called by the reconciliation widget in order to open the wizard allowing
|
|
automatically creating a reconciliation model from the reconciliation
|
|
propositions manually matched with a statement line in the widget.
|
|
"""
|
|
if st_line_amount:
|
|
# No proposition to create. We open an empty wizard.
|
|
line_vals = [(0, 0, {
|
|
'account_id': proposition['account_id'],
|
|
'tax_ids': [(6, 0, proposition['tax_ids'])],
|
|
'amount_type': 'percentage',
|
|
'amount_string': str(round(100 * proposition['amount'] / st_line_amount, 5)),
|
|
'label': proposition.get('label', ''),
|
|
}) for proposition in rec_propositions]
|
|
else:
|
|
line_vals = []
|
|
|
|
view_id = self.env['ir.model.data']._xmlid_to_res_id('account_accountant.view_account_reconcile_model_widget_wizard')
|
|
return {
|
|
'type': 'ir.actions.act_window',
|
|
'name': _("Create Reconciliation Model"),
|
|
'view_mode': 'form',
|
|
'res_model': 'account.reconcile.model',
|
|
'views': [[view_id, 'form']],
|
|
'target': 'new',
|
|
'context': {
|
|
'default_line_ids': line_vals,
|
|
'default_to_check': to_check,
|
|
}
|
|
}
|