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