diff --git a/addons/account/account.py b/addons/account/account.py index ddc3d12f4bb..c0c0f76cffa 100644 --- a/addons/account/account.py +++ b/addons/account/account.py @@ -55,6 +55,19 @@ def check_cycle(self, cr, uid, ids, context=None): level -= 1 return True +class res_company(osv.osv): + _inherit = "res.company" + _columns = { + 'income_currency_exchange_account_id': fields.many2one( + 'account.account', + string="Gain Exchange Rate Account", + domain="[('type', '=', 'other')]",), + 'expense_currency_exchange_account_id': fields.many2one( + 'account.account', + string="Loss Exchange Rate Account", + domain="[('type', '=', 'other')]",), + } + class account_payment_term(osv.osv): _name = "account.payment.term" _description = "Payment Term" diff --git a/addons/account/account_bank_statement.py b/addons/account/account_bank_statement.py index a8727f5f077..f3d6ce94323 100644 --- a/addons/account/account_bank_statement.py +++ b/addons/account/account_bank_statement.py @@ -405,31 +405,9 @@ class account_bank_statement(osv.osv): 'context':ctx, } - def number_of_lines_reconciled(self, cr, uid, id, context=None): + def number_of_lines_reconciled(self, cr, uid, ids, context=None): bsl_obj = self.pool.get('account.bank.statement.line') - return bsl_obj.search_count(cr, uid, [('statement_id', '=', id), ('journal_entry_id', '!=', False)], context=context) - - def get_format_currency_js_function(self, cr, uid, id, context=None): - """ Returns a string that can be used to instanciate a javascript function. - That function formats a number according to the statement line's currency or the statement currency""" - company_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id - st = id and self.browse(cr, uid, id, context=context) - if not st: - return - statement_currency = st.journal_id.currency or company_currency - digits = 2 # TODO : from currency_obj - function = "" - done_currencies = [] - for st_line in st.line_ids: - st_line_currency = st_line.currency_id or statement_currency - if st_line_currency.id not in done_currencies: - if st_line_currency.position == 'after': - return_str = "return amount.toFixed(" + str(digits) + ") + ' " + st_line_currency.symbol + "';" - else: - return_str = "return '" + st_line_currency.symbol + " ' + amount.toFixed(" + str(digits) + ");" - function += "if (currency_id === " + str(st_line_currency.id) + "){ " + return_str + " }" - done_currencies.append(st_line_currency.id) - return function + return bsl_obj.search_count(cr, uid, [('statement_id', 'in', ids), ('journal_entry_id', '!=', False)], context=context) def link_bank_to_partner(self, cr, uid, ids, context=None): for statement in self.browse(cr, uid, ids, context=context): @@ -465,98 +443,102 @@ class account_bank_statement_line(osv.osv): account_move_obj.button_cancel(cr, uid, move_ids, context=context) account_move_obj.unlink(cr, uid, move_ids, context) - def get_data_for_reconciliations(self, cr, uid, ids, context=None): - """ Used to instanciate a batch of reconciliations in a single request """ - # Build a list of reconciliations data + def get_data_for_reconciliations(self, cr, uid, ids, excluded_ids=None, search_reconciliation_proposition=True, context=None): + """ Returns the data required to display a reconciliation, for each statement line id in ids """ ret = [] - statement_line_done = {} - mv_line_ids_selected = [] - for st_line in self.browse(cr, uid, ids, context=context): - # look for structured communication first - exact_match_id = self.search_structured_com(cr, uid, st_line, context=context) - if exact_match_id: - reconciliation_data = { - 'st_line': self.get_statement_line_for_reconciliation(cr, uid, st_line.id, context), - 'reconciliation_proposition': self.make_counter_part_lines(cr, uid, st_line, [exact_match_id], context=context) - } - for mv_line in reconciliation_data['reconciliation_proposition']: - mv_line_ids_selected.append(mv_line['id']) - statement_line_done[st_line.id] = reconciliation_data - - for st_line_id in ids: - if statement_line_done.get(st_line_id): - ret.append(statement_line_done.get(st_line_id)) - else: - reconciliation_data = { - 'st_line': self.get_statement_line_for_reconciliation(cr, uid, st_line_id, context), - 'reconciliation_proposition': self.get_reconciliation_proposition(cr, uid, st_line_id, mv_line_ids_selected, context) - } - for mv_line in reconciliation_data['reconciliation_proposition']: - mv_line_ids_selected.append(mv_line['id']) - ret.append(reconciliation_data) + if excluded_ids is None: + excluded_ids = [] + + for st_line in self.browse(cr, uid, ids, context=context): + reconciliation_data = {} + if search_reconciliation_proposition: + reconciliation_proposition = self.get_reconciliation_proposition(cr, uid, st_line, excluded_ids=excluded_ids, context=context) + for mv_line in reconciliation_proposition: + excluded_ids.append(mv_line['id']) + reconciliation_data['reconciliation_proposition'] = reconciliation_proposition + else: + reconciliation_data['reconciliation_proposition'] = [] + st_line = self.get_statement_line_for_reconciliation(cr, uid, st_line, context=context) + reconciliation_data['st_line'] = st_line + ret.append(reconciliation_data) - # Check if, now that 'candidate' move lines were selected, there are moves left for statement lines - #for reconciliation_data in ret: - # if not reconciliation_data['st_line']['has_no_partner']: - # st_line = self.browse(cr, uid, reconciliation_data['st_line']['id'], context=context) - # if not self.get_move_lines_counterparts(cr, uid, st_line, excluded_ids=mv_line_ids_selected, count=True, context=context): - # reconciliation_data['st_line']['no_match'] = True return ret - def get_statement_line_for_reconciliation(self, cr, uid, id, context=None): - """ Returns the data required by the bank statement reconciliation use case """ - line = self.browse(cr, uid, id, context=context) - statement_currency = line.journal_id.currency or line.journal_id.company_id.currency_id - amount = line.amount - rml_parser = report_sxw.rml_parse(cr, uid, 'statement_line_widget', context=context) - amount_str = line.amount > 0 and line.amount or -line.amount - amount_str = rml_parser.formatLang(amount_str, currency_obj=statement_currency) - amount_currency_str = "" - if line.amount_currency and line.currency_id: - amount_currency_str = amount_str - amount_str = rml_parser.formatLang(line.amount_currency, currency_obj=line.currency_id) - amount = line.amount_currency + def get_statement_line_for_reconciliation(self, cr, uid, st_line, context=None): + """ Returns the data required by the bank statement reconciliation widget to display a statement line """ + if context is None: + context = {} + statement_currency = st_line.journal_id.currency or st_line.journal_id.company_id.currency_id + rml_parser = report_sxw.rml_parse(cr, uid, 'reconciliation_widget_asl', context=context) + + if st_line.amount_currency and st_line.currency_id: + amount = st_line.amount_currency + amount_currency = st_line.amount + amount_currency_str = amount_currency > 0 and amount_currency or -amount_currency + amount_currency_str = rml_parser.formatLang(amount_currency_str, currency_obj=statement_currency) + else: + amount = st_line.amount + amount_currency_str = "" + amount_str = amount > 0 and amount or -amount + amount_str = rml_parser.formatLang(amount_str, currency_obj=st_line.currency_id or statement_currency) data = { - 'id': line.id, - 'ref': line.ref, - 'note': line.note or "", - 'name': line.name, - 'date': line.date, + '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, - 'currency_id': line.currency_id.id or statement_currency.id, - 'no_match': self.get_move_lines_counterparts(cr, uid, line, count=True, context=context) == 0, - 'partner_id': line.partner_id.id, - 'statement_id': line.statement_id.id, - 'account_code': line.journal_id.default_debit_account_id.code, - 'account_name': line.journal_id.default_debit_account_id.name, - 'partner_name': line.partner_id and line.partner_id.name or line.partner_name, - 'amount_currency_str': amount_currency_str, - 'has_no_partner': not line.partner_id.id, + '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, + 'statement_id': st_line.statement_id.id, + '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, + 'amount_currency_str': amount_currency_str, # Amount in the statement currency + 'has_no_partner': not st_line.partner_id.id, } - if line.partner_id.id: - data['open_balance_account_id'] = line.partner_id.property_account_payable.id + if st_line.partner_id.id: if amount > 0: - data['open_balance_account_id'] = line.partner_id.property_account_receivable.id + data['open_balance_account_id'] = st_line.partner_id.property_account_receivable.id + else: + data['open_balance_account_id'] = st_line.partner_id.property_account_payable.id + return data - def search_structured_com(self, cr, uid, st_line, context=None): - domain = [('ref', '=', st_line.name.replace('/', ''))] - if st_line.partner_id: - domain += [('partner_id', '=', st_line.partner_id.id)] - ids = self.pool.get('account.move.line').search(cr, uid, domain, limit=1, context=context) - return ids and ids[0] or False - - def get_reconciliation_proposition(self, cr, uid, id, excluded_ids=[], context=None): + def get_reconciliation_proposition(self, cr, uid, st_line, excluded_ids=None, context=None): """ Returns move lines that constitute the best guess to reconcile a statement line. """ - st_line = self.browse(cr, uid, id, context=context) + if excluded_ids is None: + excluded_ids = [] + mv_line_pool = self.pool.get('account.move.line') + + # Look for structured communication + if st_line.name: + structured_com_match_domain = [('ref', '=', st_line.name),('reconcile_id', '=', False),('state', '=', 'valid'),('account_id.reconcile', '=', True),('id', 'not in', excluded_ids)] + match_id = mv_line_pool.search(cr, uid, structured_com_match_domain, offset=0, limit=1, context=context) + if match_id: + mv_line_br = mv_line_pool.browse(cr, uid, match_id, context=context) + target_currency = st_line.currency_id or st_line.journal_id.currency or st_line.journal_id.company_id.currency_id + mv_line = mv_line_pool.prepare_move_lines_for_reconciliation_widget(cr, uid, mv_line_br, target_currency=target_currency, target_date=st_line.date, context=context)[0] + mv_line['has_no_partner'] = not bool(st_line.partner_id.id) + # If the structured communication matches a move line that is associated with a partner, we can safely associate the statement line with the partner + if (mv_line['partner_id']): + self.write(cr, uid, st_line.id, {'partner_id': mv_line['partner_id']}, context=context) + mv_line['has_no_partner'] = False + return [mv_line] + + # If there is no identified partner or structured communication, don't look further + if not st_line.partner_id.id: + return [] + + # Look for a move line whose amount matches the statement line's amount company_currency = st_line.journal_id.company_id.currency_id.id statement_currency = st_line.journal_id.currency.id or company_currency - # either use the unsigned debit/credit fields or the signed amount_currency field sign = 1 if statement_currency == company_currency: amount_field = 'credit' + sign = -1 if st_line.amount > 0: amount_field = 'debit' else: @@ -564,124 +546,74 @@ class account_bank_statement_line(osv.osv): if st_line.amount < 0: sign = -1 - #we don't propose anything if there is no partner detected - if not st_line.partner_id.id: - return [] - # look for exact match - exact_match_id = self.get_move_lines_counterparts(cr, uid, st_line, excluded_ids=excluded_ids, additional_domain=[(amount_field, '=', (sign * st_line.amount))]) - if exact_match_id: - return exact_match_id + match_id = self.get_move_lines_for_reconciliation(cr, uid, st_line, excluded_ids=excluded_ids, offset=0, limit=1, additional_domain=[(amount_field, '=', (sign * st_line.amount))]) + if match_id: + return [match_id[0]] - # select oldest move lines - if sign == -1: - mv_lines = self.get_move_lines_counterparts(cr, uid, st_line, excluded_ids=excluded_ids, additional_domain=[(amount_field, '<', 0)]) - else: - mv_lines = self.get_move_lines_counterparts(cr, uid, st_line, excluded_ids=excluded_ids, additional_domain=[(amount_field, '>', 0)]) - ret = [] - total = 0 - # get_move_lines_counterparts inverts debit and credit - amount_field = 'debit' if amount_field == 'credit' else 'credit' - for line in mv_lines: - if total + line[amount_field] <= abs(st_line.amount): - ret.append(line) - total += line[amount_field] - if total >= abs(st_line.amount): - break - return ret + return [] - def get_move_lines_counterparts_id(self, cr, uid, st_line_id, excluded_ids=[], additional_domain=[], count=False, context=None): + def get_move_lines_for_reconciliation_by_statement_line_id(self, cr, uid, st_line_id, excluded_ids=None, str=False, offset=0, limit=None, count=False, additional_domain=None, context=None): + """ Bridge between the web client reconciliation widget and get_move_lines_for_reconciliation (which expects a browse record) """ + if excluded_ids is None: + excluded_ids = [] + if additional_domain is None: + additional_domain = [] st_line = self.browse(cr, uid, st_line_id, context=context) - return self.get_move_lines_counterparts(cr, uid, st_line, excluded_ids, additional_domain, count, context=context) + return self.get_move_lines_for_reconciliation(cr, uid, st_line, excluded_ids, str, offset, limit, count, additional_domain, context=context) - def get_move_lines_counterparts(self, cr, uid, st_line, excluded_ids=[], additional_domain=[], count=False, context=None): - """ Find the move lines that could be used to reconcile a statement line and returns the counterpart that could be created to reconcile them - If count is true, only returns the count. + def get_move_lines_for_reconciliation(self, cr, uid, st_line, excluded_ids=None, str=False, offset=0, limit=None, count=False, additional_domain=None, context=None): + """ Find the move lines that could be used to reconcile a statement line. If count is true, only returns the count. :param st_line: the browse record of the statement line :param integers list excluded_ids: ids of move lines that should not be fetched - :param string filter_str: string to filter lines - :param integer offset: offset of the request - :param integer limit: number of lines to fetch :param boolean count: just return the number of records - :param tuples list domain: additional domain restrictions + :param tuples list additional_domain: additional domain restrictions """ + if excluded_ids is None: + excluded_ids = [] + if additional_domain is None: + additional_domain = [] mv_line_pool = self.pool.get('account.move.line') - domain = additional_domain + [('reconcile_id', '=', False), ('state', '=', 'valid')] + # Make domain + domain = additional_domain + [('reconcile_id', '=', False),('state', '=', 'valid')] if st_line.partner_id.id: domain += [('partner_id', '=', st_line.partner_id.id), '|', ('account_id.type', '=', 'receivable'), ('account_id.type', '=', 'payable')] else: domain += [('account_id.reconcile', '=', True), ('account_id.type', '=', 'other')] + if str: + domain += [('partner_id.name', 'ilike', str)] if excluded_ids: domain.append(('id', 'not in', excluded_ids)) - line_ids = mv_line_pool.search(cr, uid, domain, order="date_maturity asc, id asc", context=context) - return self.make_counter_part_lines(cr, uid, st_line, line_ids, count=count, context=context) + if str: + domain += ['|', ('move_id.name', 'ilike', str), ('move_id.ref', 'ilike', str)] - def make_counter_part_lines(self, cr, uid, st_line, line_ids, count=False, context=None): - if context is None: - context = {} - mv_line_pool = self.pool.get('account.move.line') - currency_obj = self.pool.get('res.currency') - company_currency = st_line.journal_id.company_id.currency_id - statement_currency = st_line.journal_id.currency or company_currency - rml_parser = report_sxw.rml_parse(cr, uid, 'statement_line_counterpart_widget', context=context) - #partially reconciled lines can be displayed only once - reconcile_partial_ids = [] + # Get move lines + line_ids = mv_line_pool.search(cr, uid, domain, offset=offset, limit=limit, order="date_maturity asc, id asc", context=context) + lines = mv_line_pool.browse(cr, uid, line_ids, context=context) + + # Either return number of lines if count: nb_lines = 0 - for line in mv_line_pool.browse(cr, uid, line_ids, context=context): + reconcile_partial_ids = [] # for a partial reconciliation, take only one line + for line in lines: if line.reconcile_partial_id and line.reconcile_partial_id.id in reconcile_partial_ids: continue nb_lines += 1 if line.reconcile_partial_id: reconcile_partial_ids.append(line.reconcile_partial_id.id) return nb_lines + + # Or return list of dicts representing the formatted move lines else: - ret = [] - for line in mv_line_pool.browse(cr, uid, line_ids, context=context): - if line.reconcile_partial_id and line.reconcile_partial_id.id in reconcile_partial_ids: - continue - amount_currency_str = "" - if line.currency_id and line.amount_currency: - amount_currency_str = rml_parser.formatLang(line.amount_currency, currency_obj=line.currency_id) - ret_line = { - 'id': line.id, - 'name': line.move_id.name, - 'ref': line.move_id.ref, - 'account_code': line.account_id.code, - 'account_name': line.account_id.name, - 'account_type': line.account_id.type, - 'date_maturity': line.date_maturity, - 'date': line.date, - 'period_name': line.period_id.name, - 'journal_name': line.journal_id.name, - 'amount_currency_str': amount_currency_str, - 'partner_id': line.partner_id.id, - 'partner_name': line.partner_id.name, - 'has_no_partner': not bool(st_line.partner_id.id), - } - st_line_currency = st_line.currency_id or statement_currency - line_currency = line.currency_id or company_currency - if line_currency == st_line_currency and line_currency != company_currency: - ret_line['debit'] = line.credit > 0 and -line.amount_residual_currency or 0.0 - ret_line['credit'] = line.debit > 0 and line.amount_residual_currency or 0.0 - ret_line['amount_currency_str'] = rml_parser.formatLang(line.amount_residual, currency_obj=company_currency) - else: - ret_line['debit'] = line.credit > 0 and -line.amount_residual or 0.0 - ret_line['credit'] = line.debit > 0 and line.amount_residual or 0.0 - if st_line_currency != company_currency: - ctx = context.copy() - ctx.update({'date': st_line.date}) - ret_line['debit'] = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, ret_line['debit'], context=ctx) - ret_line['credit'] = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, ret_line['credit'], context=ctx) - ret_line['debit_str'] = rml_parser.formatLang(ret_line['debit'], currency_obj=st_line_currency) - ret_line['credit_str'] = rml_parser.formatLang(ret_line['credit'], currency_obj=st_line_currency) - ret.append(ret_line) - if line.reconcile_partial_id: - reconcile_partial_ids.append(line.reconcile_partial_id.id) - return ret + target_currency = st_line.currency_id or st_line.journal_id.currency or st_line.journal_id.company_id.currency_id + mv_lines = mv_line_pool.prepare_move_lines_for_reconciliation_widget(cr, uid, lines, target_currency=target_currency, target_date=st_line.date, context=context) + has_no_partner = not bool(st_line.partner_id.id) + for line in mv_lines: + line['has_no_partner'] = has_no_partner + return mv_lines def get_currency_rate_line(self, cr, uid, st_line, currency_diff, move_id, context=None): if currency_diff < 0: @@ -769,20 +701,21 @@ class account_bank_statement_line(osv.osv): mv_line = aml_obj.browse(cr, uid, mv_line_dict['counterpart_move_line_id'], context=context) mv_line_dict['account_id'] = mv_line.account_id.id if st_line_currency.id != company_currency.id: + ctx = context.copy() + ctx['date'] = st_line.date mv_line_dict['amount_currency'] = mv_line_dict['debit'] - mv_line_dict['credit'] mv_line_dict['currency_id'] = st_line_currency.id if st_line.currency_id and statement_currency.id == company_currency.id and st_line_currency_rate: debit_at_current_rate = self.pool.get('res.currency').round(cr, uid, company_currency, mv_line_dict['debit'] / st_line_currency_rate) credit_at_current_rate = self.pool.get('res.currency').round(cr, uid, company_currency, mv_line_dict['credit'] / st_line_currency_rate) elif st_line.currency_id and st_line_currency_rate: - debit_at_current_rate = currency_obj.compute(cr, uid, statement_currency.id, company_currency.id, mv_line_dict['debit'] / st_line_currency_rate, context=context) - credit_at_current_rate = currency_obj.compute(cr, uid, statement_currency.id, company_currency.id, mv_line_dict['credit'] / st_line_currency_rate, context=context) + debit_at_current_rate = currency_obj.compute(cr, uid, statement_currency.id, company_currency.id, mv_line_dict['debit'] / st_line_currency_rate, context=ctx) + credit_at_current_rate = currency_obj.compute(cr, uid, statement_currency.id, company_currency.id, mv_line_dict['credit'] / st_line_currency_rate, context=ctx) else: - debit_at_current_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['debit'], context=context) - credit_at_current_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['credit'], context=context) + debit_at_current_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['debit'], context=ctx) + credit_at_current_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['credit'], context=ctx) if mv_line_dict.get('counterpart_move_line_id'): #post an account line that use the same currency rate than the counterpart (to balance the account) and post the difference in another line - ctx = context.copy() ctx['date'] = mv_line.date debit_at_old_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['debit'], context=ctx) credit_at_old_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['credit'], context=ctx) @@ -790,7 +723,7 @@ class account_bank_statement_line(osv.osv): mv_line_dict['debit'] = debit_at_old_rate if debit_at_old_rate - debit_at_current_rate: currency_diff = debit_at_current_rate - debit_at_old_rate - to_create.append(self.get_currency_rate_line(cr, uid, st_line, currency_diff, move_id, context=context)) + to_create.append(self.get_currency_rate_line(cr, uid, st_line, -currency_diff, move_id, context=context)) if credit_at_old_rate - credit_at_current_rate: currency_diff = credit_at_current_rate - credit_at_old_rate to_create.append(self.get_currency_rate_line(cr, uid, st_line, currency_diff, move_id, context=context)) @@ -812,12 +745,9 @@ class account_bank_statement_line(osv.osv): new_aml_id = aml_obj.create(cr, uid, mv_line_dict, context=context) if counterpart_move_line_id != None: move_line_pairs_to_reconcile.append([new_aml_id, counterpart_move_line_id]) - # Reconcile for pair in move_line_pairs_to_reconcile: - # TODO : too slow aml_obj.reconcile_partial(cr, uid, pair, context=context) - # Mark the statement line as reconciled self.write(cr, uid, id, {'journal_entry_id': move_id}, context=context) @@ -826,7 +756,7 @@ class account_bank_statement_line(osv.osv): # Unfortunately, that spawns a "no access rights" error ; it shouldn't. def _needaction_domain_get(self, cr, uid, context=None): user = self.pool.get("res.users").browse(cr, uid, uid) - return ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id]),('journal_entry_id', '=', False)] + return ['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id]), ('journal_entry_id', '=', False)] _order = "statement_id desc, sequence" _name = "account.bank.statement.line" @@ -864,13 +794,13 @@ class account_statement_operation_template(osv.osv): 'label': fields.char('Label'), 'amount_type': fields.selection([('fixed', 'Fixed'),('percentage_of_total','Percentage of total amount'),('percentage_of_balance', 'Percentage of open balance')], 'Amount type', required=True), - 'amount': fields.float('Amount', digits_compute=dp.get_precision('Account'), help="Leave to 0 to ignore."), + 'amount': fields.float('Amount', digits_compute=dp.get_precision('Account'), help="The amount will count as a debit if it is negative, as a credit if it is positive (except if amount type is 'Percentage of open balance').", required=True), 'tax_id': fields.many2one('account.tax', 'Tax', ondelete='cascade'), 'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account', ondelete='cascade'), } _defaults = { - 'amount_type': 'fixed', - 'amount': 0.0 + 'amount_type': 'percentage_of_balance', + 'amount': 100.0 } # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/account/account_invoice.py b/addons/account/account_invoice.py index 7de7d1adbe9..cdba622b9b4 100644 --- a/addons/account/account_invoice.py +++ b/addons/account/account_invoice.py @@ -645,10 +645,6 @@ class account_invoice(models.Model): invoice.check_total = invoice.amount_total return True - @staticmethod - def _convert_ref(ref): - return (ref or '').replace('/','') - @api.multi def _get_analytic_lines(self): """ Return a list of dict for creating analytic lines for self[0] """ @@ -661,7 +657,7 @@ class account_invoice(models.Model): if self.type in ('in_invoice', 'in_refund'): ref = self.reference else: - ref = self._convert_ref(self.number) + ref = self.number if not self.journal_id.analytic_journal_id: raise except_orm(_('No Analytic Journal!'), _("You have to define an analytic journal on the '%s' journal!") % (self.journal_id.name,)) @@ -827,7 +823,7 @@ class account_invoice(models.Model): if inv.type in ('in_invoice', 'in_refund'): ref = inv.reference else: - ref = self._convert_ref(inv.number) + ref = inv.number diff_currency = inv.currency_id != company_currency # create one move line for the total and possibly adjust the other lines amount @@ -955,11 +951,11 @@ class account_invoice(models.Model): if inv.type in ('in_invoice', 'in_refund'): if not inv.reference: - ref = self._convert_ref(inv.number) + ref = inv.number else: ref = inv.reference else: - ref = self._convert_ref(inv.number) + ref = inv.number self._cr.execute(""" UPDATE account_move SET ref=%s WHERE id=%s AND (ref IS NULL OR ref = '')""", @@ -1131,7 +1127,7 @@ class account_invoice(models.Model): if self.type in ('in_invoice', 'in_refund'): ref = self.reference else: - ref = self._convert_ref(self.number) + ref = self.number partner = self.partner_id._find_accounting_partner(self.partner_id) name = name or self.invoice_line.name or self.number # Pay attention to the sign for both debit/credit AND amount_currency diff --git a/addons/account/account_move_line.py b/addons/account/account_move_line.py index 5cac88c7274..0f3e1322aaf 100644 --- a/addons/account/account_move_line.py +++ b/addons/account/account_move_line.py @@ -22,12 +22,12 @@ import time from datetime import datetime - from openerp import workflow from openerp.osv import fields, osv from openerp.tools.translate import _ import openerp.addons.decimal_precision as dp from openerp import tools +from openerp.report import report_sxw import openerp class account_move_line(osv.osv): @@ -752,6 +752,74 @@ class account_move_line(osv.osv): args.append(('partner_id', '=', partner[0])) return super(account_move_line, self).search(cr, uid, args, offset, limit, order, context, count) + def prepare_move_lines_for_reconciliation_widget(self, cr, uid, lines, target_currency=False, target_date=False, context=None): + """ Returns move lines formatted for the manual/bank reconciliation widget + + :param target_currency: curreny you want the move line debit/credit converted into + :param target_date: date to use for the monetary conversion + """ + if not lines: + return [] + if context is None: + context = {} + ctx = context.copy() + currency_obj = self.pool.get('res.currency') + company_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id + rml_parser = report_sxw.rml_parse(cr, uid, 'reconciliation_widget_aml', context=context) + reconcile_partial_ids = [] # for a partial reconciliation, take only one line + ret = [] + + for line in lines: + if line.reconcile_partial_id and line.reconcile_partial_id.id in reconcile_partial_ids: + continue + if line.reconcile_partial_id: + reconcile_partial_ids.append(line.reconcile_partial_id.id) + + ret_line = { + 'id': line.id, + 'name': line.move_id.name, + 'ref': line.move_id.ref, + 'account_code': line.account_id.code, + 'account_name': line.account_id.name, + 'account_type': line.account_id.type, + 'date_maturity': line.date_maturity, + 'date': line.date, + 'period_name': line.period_id.name, + 'journal_name': line.journal_id.name, + 'partner_id': line.partner_id.id, + 'partner_name': line.partner_id.name, + } + + # Get right debit / credit: + line_currency = line.currency_id or company_currency + amount_currency_str = "" + if line.currency_id and line.amount_currency: + amount_currency_str = rml_parser.formatLang(line.amount_currency, currency_obj=line.currency_id) + if target_currency and line_currency == target_currency and target_currency != company_currency: + debit = line.debit > 0 and line.amount_residual_currency or 0.0 + credit = line.credit > 0 and line.amount_residual_currency or 0.0 + amount_currency_str = rml_parser.formatLang(line.amount_residual, currency_obj=company_currency) + amount_str = rml_parser.formatLang(debit or credit, currency_obj=target_currency) + else: + debit = line.debit > 0 and line.amount_residual or 0.0 + credit = line.credit > 0 and line.amount_residual or 0.0 + amount_str = rml_parser.formatLang(debit or credit, currency_obj=company_currency) + if target_currency and target_currency != company_currency: + amount_currency_str = rml_parser.formatLang(debit or credit, currency_obj=line_currency) + ctx = context.copy() + if target_date: + ctx.update({'date': target_date}) + debit = currency_obj.compute(cr, uid, target_currency.id, company_currency.id, debit, context=ctx) + credit = currency_obj.compute(cr, uid, target_currency.id, company_currency.id, credit, context=ctx) + amount_str = rml_parser.formatLang(debit or credit, currency_obj=target_currency) + + ret_line['credit'] = credit + ret_line['debit'] = debit + ret_line['amount_str'] = amount_str + ret_line['amount_currency_str'] = amount_currency_str + ret.append(ret_line) + return ret + def list_partners_to_reconcile(self, cr, uid, context=None): cr.execute( """SELECT partner_id FROM ( diff --git a/addons/account/account_view.xml b/addons/account/account_view.xml index 47f21266c18..a14934a0a8a 100644 --- a/addons/account/account_view.xml +++ b/addons/account/account_view.xml @@ -493,7 +493,7 @@ Reconciliation on Bank Statements bank_statement_reconciliation_view - {'statement_id': active_id} + {'statement_ids': [active_id]} diff --git a/addons/account/demo/account_bank_statement.xml b/addons/account/demo/account_bank_statement.xml index aec70da0097..18cc4518278 100644 --- a/addons/account/demo/account_bank_statement.xml +++ b/addons/account/demo/account_bank_statement.xml @@ -30,7 +30,7 @@ - SAJ2014002 + SAJ/2014/002 @@ -70,15 +70,15 @@ Bank Fees Bank Fees - fixed - + percentage_of_balance + 100.0 Profit / Loss Profit / Loss - fixed - + percentage_of_balance + 100.0 diff --git a/addons/account/demo/account_invoice_demo.xml b/addons/account/demo/account_invoice_demo.xml index 4f329eb32d1..02fd72417e3 100644 --- a/addons/account/demo/account_invoice_demo.xml +++ b/addons/account/demo/account_invoice_demo.xml @@ -44,7 +44,7 @@ Zed+ Antivirus - + diff --git a/addons/account/res_config.py b/addons/account/res_config.py index 5fd111a8fe0..1cc814d8f75 100644 --- a/addons/account/res_config.py +++ b/addons/account/res_config.py @@ -125,7 +125,29 @@ class account_config_settings(osv.osv_memory): help="Allows you to use the analytic accounting."), 'group_check_supplier_invoice_total': fields.boolean('Check the total of supplier invoices', implied_group="account.group_supplier_inv_check_total"), + 'income_currency_exchange_account_id': fields.related( + 'company_id', 'income_currency_exchange_account_id', + type='many2one', + relation='account.account', + string="Gain Exchange Rate Account", + domain="[('type', '=', 'other')]"), + 'expense_currency_exchange_account_id': fields.related( + 'company_id', 'expense_currency_exchange_account_id', + type="many2one", + relation='account.account', + string="Loss Exchange Rate Account", + domain="[('type', '=', 'other')]"), } + def onchange_company_id(self, cr, uid, ids, company_id, context=None): + res = super(account_config_settings, self).onchange_company_id(cr, uid, ids, company_id, context=context) + if company_id: + company = self.pool.get('res.company').browse(cr, uid, company_id, context=context) + res['value'].update({'income_currency_exchange_account_id': company.income_currency_exchange_account_id and company.income_currency_exchange_account_id.id or False, + 'expense_currency_exchange_account_id': company.expense_currency_exchange_account_id and company.expense_currency_exchange_account_id.id or False}) + else: + res['value'].update({'income_currency_exchange_account_id': False, + 'expense_currency_exchange_account_id': False}) + return res def _default_company(self, cr, uid, context=None): user = self.pool.get('res.users').browse(cr, uid, uid, context=context) diff --git a/addons/account/res_config_view.xml b/addons/account/res_config_view.xml index b33264fa452..98031834ce4 100644 --- a/addons/account/res_config_view.xml +++ b/addons/account/res_config_view.xml @@ -124,6 +124,14 @@
diff --git a/addons/account/static/src/css/account_bank_statement_reconciliation.css b/addons/account/static/src/css/account_bank_statement_reconciliation.css index 05caeab2655..040e55eb45f 100644 --- a/addons/account/static/src/css/account_bank_statement_reconciliation.css +++ b/addons/account/static/src/css/account_bank_statement_reconciliation.css @@ -6,18 +6,29 @@ -o-user-select: none; user-select: none; cursor: default; + height: 100%; /* icons */ } + .openerp .oe_bank_statement_reconciliation .oe_form_sheetbg { + border-bottom: 0; + padding: 0; + height: 100%; } + .openerp .oe_bank_statement_reconciliation .oe_form_sheetbg .oe_form_sheet { + position: relative; + padding: 20px 15px 30px 15px; + border-top: 0; + border-bottom: 0; + height: 100%; } .openerp .oe_bank_statement_reconciliation h1 { width: 48%; padding: 0 0 0 15px; - margin: 0 0 35px 0; + margin: 0 0 25px 0; float: left; - font-size: 2.3em; } + font-size: 2em; } .openerp .oe_bank_statement_reconciliation h2 { font-size: 1.8em; } .openerp .oe_bank_statement_reconciliation .progress { width: 49%; - margin: 6px 15px 0 0; + margin: 4px 15px 0 0; float: right; position: relative; display: inline-block; } @@ -29,9 +40,6 @@ top: 2px; z-index: 10; text-shadow: -1px -1px 0 #f5f5f5, 1px -1px 0 #f5f5f5, -1px 1px 0 #f5f5f5, 1px 1px 0 #f5f5f5; } - .openerp .oe_bank_statement_reconciliation .oe_form_sheet { - position: relative; - padding-bottom: 30px; } .openerp .oe_bank_statement_reconciliation .protip { margin: 0; position: absolute; @@ -100,7 +108,12 @@ -moz-transform: rotate(0deg); -ms-transform: rotate(0deg); -o-transform: rotate(0deg); - transform: rotate(0deg); } + transform: rotate(0deg); + -webkit-transition-duration: 300ms; + -moz-transition-duration: 300ms; + -ms-transition-duration: 300ms; + -o-transition-duration: 300ms; + transition-duration: 300ms; } .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .visible_toggle, .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line[data-mode="match"] .toggle_match, .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line[data-mode="create"] .toggle_create { visibility: visible !important; -webkit-transform: rotate(90deg); @@ -136,24 +149,24 @@ .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .accounting_view td, .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .match table td { padding: 1px 8px; vertical-align: middle; } - .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .accounting_view td:nth-child(1), .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .accounting_view td:nth-child(7), .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .match table td:nth-child(1), .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .match table td:nth-child(7) { + .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .accounting_view td.cell_action, .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .accounting_view td.cell_info_popover, .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .match table td.cell_action, .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .match table td.cell_info_popover { width: 15px; padding: 0; } - .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .accounting_view td:nth-child(1), .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .match table td:nth-child(1) { + .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .accounting_view td.cell_action, .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .match table td.cell_action { text-align: left; } - .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .accounting_view td:nth-child(2), .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .match table td:nth-child(2) { + .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .accounting_view td.cell_account_code, .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .match table td.cell_account_code { width: 80px; padding-left: 3px; } - .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .accounting_view td:nth-child(3), .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .match table td:nth-child(3) { + .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .accounting_view td.cell_due_date, .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .match table td.cell_due_date { width: 100px; } - .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .accounting_view td:nth-child(5), .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .match table td:nth-child(5) { + .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .accounting_view td.cell_debit, .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .match table td.cell_debit { text-align: right; width: 15%; } - .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .accounting_view td:nth-child(6), .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .match table td:nth-child(6) { + .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .accounting_view td.cell_credit, .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .match table td.cell_credit { width: 15%; text-align: right; padding-right: 3px; } - .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .accounting_view td:nth-child(7), .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .match table td:nth-child(7) { + .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .accounting_view td.cell_info_popover, .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .match table td.cell_info_popover { text-align: right; } .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .accounting_view tr.line_open_balance, .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .match table tr.line_open_balance { color: #bbb; } @@ -176,8 +189,13 @@ -webkit-transition-property: background-color; -moz-transition-property: background-color; -ms-transition-property: background-color; - transition-property: background-color; } - .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .accounting_view .initial_line > td:nth-child(1), .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .accounting_view .initial_line > td:nth-child(7) { + transition-property: background-color; + -webkit-transition-duration: 300ms; + -moz-transition-duration: 300ms; + -ms-transition-duration: 300ms; + -o-transition-duration: 300ms; + transition-duration: 300ms; } + .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .accounting_view .initial_line > td.cell_action, .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .accounting_view .initial_line > td.cell_info_popover { border-top: none; background: white !important; padding-top: 6px; @@ -188,9 +206,9 @@ font-weight: bold; height: 26px; margin: 0 15px 4px 15px; } - .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .accounting_view caption .button_ok { + .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .accounting_view caption button { float: right; } - .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .accounting_view caption .button_ok:disabled { + .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .accounting_view caption button:disabled { opacity: 0.5; } .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .accounting_view caption > span, .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .accounting_view caption > input { position: relative; @@ -198,7 +216,7 @@ /* meh */ font-weight: bold; cursor: pointer; } - .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .accounting_view td:nth-child(6) { + .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .accounting_view td.cell_credit { border-left: 1px solid black; } .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .match .match_controls { padding: 0 0 5px 18px; } diff --git a/addons/account/static/src/css/account_bank_statement_reconciliation.scss b/addons/account/static/src/css/account_bank_statement_reconciliation.scss index 248536b7a9b..92788e3bb51 100644 --- a/addons/account/static/src/css/account_bank_statement_reconciliation.scss +++ b/addons/account/static/src/css/account_bank_statement_reconciliation.scss @@ -3,6 +3,8 @@ $mainTableBordersPadding: 3px; $lightBorder: 1px solid #bbb; $accountingBorder: 1px solid #000; $initialLineBackground: #f0f0f0; +// Warning, this value is also specified in the instance.web.account.abstractReconciliation widget +$aestetic_animation_speed: 300ms; .openerp .oe_bank_statement_reconciliation { @@ -13,13 +15,28 @@ $initialLineBackground: #f0f0f0; -o-user-select: none; user-select: none; cursor: default; + height: 100%; + + .oe_form_sheetbg { + border-bottom: 0; + padding: 0; + height: 100%; + + .oe_form_sheet { + position: relative; + padding: 20px 15px 30px 15px; + border-top: 0; + border-bottom: 0; + height: 100%; + } + } h1 { width: 48%; padding: 0 0 0 $actionColWidth; - margin: 0 0 35px 0; + margin: 0 0 25px 0; float: left; - font-size: 2.3em; + font-size: 2em; } h2 { @@ -28,7 +45,7 @@ $initialLineBackground: #f0f0f0; .progress { width: 49%; - margin: 6px $actionColWidth 0 0; + margin: 4px $actionColWidth 0 0; float: right; position: relative; display: inline-block; @@ -48,11 +65,6 @@ $initialLineBackground: #f0f0f0; } } - .oe_form_sheet { - position: relative; - padding-bottom: 30px; - } - .protip { margin: 0; position: absolute; @@ -145,6 +157,11 @@ $initialLineBackground: #f0f0f0; -ms-transform: rotate(0deg); -o-transform: rotate(0deg); transform: rotate(0deg); + -webkit-transition-duration: $aestetic_animation_speed; + -moz-transition-duration: $aestetic_animation_speed; + -ms-transition-duration: $aestetic_animation_speed; + -o-transition-duration: $aestetic_animation_speed; + transition-duration: $aestetic_animation_speed; } .visible_toggle { @@ -228,40 +245,40 @@ $initialLineBackground: #f0f0f0; vertical-align: middle; } - td:nth-child(1), td:nth-child(7) { + td.cell_action, td.cell_info_popover { width: $actionColWidth; padding: 0; } - td:nth-child(1) { + td.cell_action { text-align: left; } - td:nth-child(2) { + td.cell_account_code { width: 80px; padding-left: $mainTableBordersPadding; } - td:nth-child(3) { + td.cell_due_date { width: 100px; } - td:nth-child(4) { + td.cell_label { } - td:nth-child(5) { + td.cell_debit { text-align: right; width: 15%; } - td:nth-child(6) { + td.cell_credit { width: 15%; text-align: right; padding-right: $mainTableBordersPadding; } - td:nth-child(7) { + td.cell_info_popover { text-align: right; } @@ -301,8 +318,13 @@ $initialLineBackground: #f0f0f0; -moz-transition-property: background-color; -ms-transition-property: background-color; transition-property: background-color; + -webkit-transition-duration: $aestetic_animation_speed; + -moz-transition-duration: $aestetic_animation_speed; + -ms-transition-duration: $aestetic_animation_speed; + -o-transition-duration: $aestetic_animation_speed; + transition-duration: $aestetic_animation_speed; - &:nth-child(1), &:nth-child(7) { + &.cell_action, &.cell_info_popover { border-top: none; background: white !important; // Hack pour l'alignement au px près @@ -318,7 +340,7 @@ $initialLineBackground: #f0f0f0; height: 26px; margin: 0 $actionColWidth 4px $actionColWidth; - .button_ok { + button { float: right; &:disabled { @@ -334,7 +356,7 @@ $initialLineBackground: #f0f0f0; } // accounting "T" - td:nth-child(6) { border-left: $accountingBorder; } + td.cell_credit { border-left: $accountingBorder; } } @@ -434,4 +456,4 @@ $initialLineBackground: #f0f0f0; } } } -} +} \ No newline at end of file diff --git a/addons/account/static/src/js/account_tour_bank_statement_reconciliation.js b/addons/account/static/src/js/account_tour_bank_statement_reconciliation.js new file mode 100644 index 00000000000..fbc55346772 --- /dev/null +++ b/addons/account/static/src/js/account_tour_bank_statement_reconciliation.js @@ -0,0 +1,147 @@ +(function () { + 'use strict'; + + var _t = openerp._t; + + openerp.Tour.register({ + id: 'bank_statement_reconciliation', + name: _t("Reconcile the demo bank statement"), + path: '/web', + mode: 'test', + // TODO : identify menu by data-menu attr or text node ? + 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/src/js/account_widgets.js b/addons/account/static/src/js/account_widgets.js index 889538cedb4..e8f364127a1 100644 --- a/addons/account/static/src/js/account_widgets.js +++ b/addons/account/static/src/js/account_widgets.js @@ -13,7 +13,8 @@ openerp.account = function (instance) { init: function(parent, context) { this._super(parent); this.max_reconciliations_displayed = 10; - this.statement_id = context.context.statement_id; + if (context.context.statement_id) this.statement_ids = [context.context.statement_id]; + if (context.context.statement_ids) this.statement_ids = context.context.statement_ids; this.title = context.context.title || _t("Reconciliation"); this.st_lines = []; this.last_displayed_reconciliation_index = undefined; // Flow control @@ -37,7 +38,7 @@ openerp.account = function (instance) { // We'll need to get the code of an account selected in a many2one (whose value is the id) this.map_account_id_code = {}; // The same move line cannot be selected for multiple resolutions - this.excluded_move_lines_ids = []; + this.excluded_move_lines_ids = {}; // Description of the fields to initialize in the "create new line" form // NB : for presets to work correctly, a field id must be the same string as a preset field this.create_form_fields = { @@ -118,48 +119,35 @@ openerp.account = function (instance) { start: function() { this._super(); var self = this; - - // Inject variable styles - var style = document.createElement("style"); - style.appendChild(document.createTextNode("")); - document.head.appendChild(style); - var css_selector = ".oe_bank_statement_reconciliation_line .toggle_match, .oe_bank_statement_reconciliation_line .toggle_create, .oe_bank_statement_reconciliation_line .initial_line > td"; - if(style.sheet.insertRule) { - style.sheet.insertRule(css_selector + " { -webkit-transition-duration: "+self.aestetic_animation_speed+"ms; }", 0); - style.sheet.insertRule(css_selector + " { -moz-transition-duration: "+self.aestetic_animation_speed+"ms; }", 0); - style.sheet.insertRule(css_selector + " { -ms-transition-duration: "+self.aestetic_animation_speed+"ms; }", 0); - style.sheet.insertRule(css_selector + " { -o-transition-duration: "+self.aestetic_animation_speed+"ms; }", 0); - style.sheet.insertRule(css_selector + " { transition-duration: "+self.aestetic_animation_speed+"ms; }", 0); - } else { - style.sheet.addRule(css_selector, "-webkit-transition-duration: "+self.aestetic_animation_speed+"ms;"); - style.sheet.addRule(css_selector, "-moz-transition-duration: "+self.aestetic_animation_speed+"ms;"); - style.sheet.addRule(css_selector, "-ms-transition-duration: "+self.aestetic_animation_speed+"ms;"); - style.sheet.addRule(css_selector, "-o-transition-duration: "+self.aestetic_animation_speed+"ms;"); - style.sheet.addRule(css_selector, "-webkit-transition-duration: "+self.aestetic_animation_speed+"ms;"); - } - // Retreive statement infos and reconciliation data from the model var lines_filter = [['journal_entry_id', '=', false], ['account_id', '=', false]]; var deferred_promises = []; - - if (self.statement_id) { - lines_filter.push(['statement_id', '=', self.statement_id]); + + // Working on specified statement(s) + if (self.statement_ids && self.statement_ids.length > 0) { + lines_filter.push(['statement_id', 'in', self.statement_ids]); + + // If only one statement, retreive its name + if (self.statement_ids.length === 1) { + deferred_promises.push(self.model_bank_statement + .query(["name"]) + .filter([['id', '=', self.statement_ids[0]]]) + .first() + .then(function(title){ + self.title = title.name; + }) + ); + } + // Anyway, find out how many statement lines are reconciled (for the progressbar) deferred_promises.push(self.model_bank_statement - .query(["name"]) - .filter([['id', '=', self.statement_id]]) - .first() - .then(function(title){ - self.title = title.name; - }) - ); - deferred_promises.push(self.model_bank_statement - .call("number_of_lines_reconciled", [self.statement_id]) + .call("number_of_lines_reconciled", [self.statement_ids]) .then(function(num) { self.already_reconciled_lines = num; }) ); } - + + // Get operation templates deferred_promises.push(new instance.web.Model("account.statement.operation.template") .query(['id','name','account_id','label','amount_type','amount','tax_id','analytic_account_id']) .all().then(function (data) { @@ -169,17 +157,19 @@ openerp.account = function (instance) { }) ); - deferred_promises.push(self.model_bank_statement - .call("get_format_currency_js_function", [self.statement_id]) - .then(function(data){ + // Get the function to format currencies + deferred_promises.push(new instance.web.Model("res.currency") + .call("get_format_currencies_js_function") + .then(function(data) { self.formatCurrency = new Function("amount, currency_id", data); }) ); + // Get statement lines deferred_promises.push(self.model_bank_statement_line .query(['id']) .filter(lines_filter) - .order_by('id') + .order_by('statement_id, id') .all().then(function (data) { self.st_lines = _(data).map(function(o){ return o.id }); }) @@ -200,21 +190,21 @@ openerp.account = function (instance) { .all().then(function(data) { _.each(data, function(o) { self.map_account_id_code[o.id] = o.code }); }); - + // Create a dict tax id -> amount new instance.web.Model("account.tax") .query(['id', 'amount']) .all().then(function(data) { _.each(data, function(o) { self.map_tax_id_amount[o.id] = o.amount }); }); - + new instance.web.Model("ir.model.data") .call("xmlid_to_res_id", ["account.menu_bank_reconcile_bank_statements"]) .then(function(data) { self.reconciliation_menu_id = data; self.doReloadMenuReconciliation(); }); - + // Bind keyboard events TODO : méthode standard ? $("body").on("keypress", function (e) { self.keyboardShortcutsHandler(e); @@ -255,33 +245,58 @@ openerp.account = function (instance) { } }, - excludeMoveLines: function(line_ids) { + // Adds move line ids to the list of move lines not to fetch for a given partner + // This is required because the same move line cannot be selected for multiple reconciliation + excludeMoveLines: function(source_child, partner_id, line_ids) { var self = this; + + var excluded_ids = this.excluded_move_lines_ids[partner_id]; + var excluded_move_lines_changed = false; _.each(line_ids, function(line_id){ - line_id = parseInt(line_id); - if (self.excluded_move_lines_ids.indexOf(line_id) === -1) { - self.excluded_move_lines_ids.push(line_id); + if (excluded_ids.indexOf(line_id) === -1) { + excluded_ids.push(line_id); + excluded_move_lines_changed = true; } }); - //update all children view + if (! excluded_move_lines_changed) + return; + + // Function that finds if an array of line objects contains at least a line identified by its id + var contains_lines = function(lines_array, line_ids) { + for (var i = 0; i < lines_array.length; i++) + for (var j = 0; j < line_ids.length; j++) + if (lines_array[i].id === line_ids[j]) + return true; + return false; + }; + + // Update children if needed _.each(self.getChildren(), function(child){ - child.render(); + if (child.partner_id === partner_id && child !== source_child) { + if (contains_lines(child.get("mv_lines_selected"), line_ids)) { + child.set("mv_lines_selected", _.filter(child.get("mv_lines_selected"), function(o){ return line_ids.indexOf(o.id) === -1 })); + } else if (contains_lines(child.mv_lines_deselected, line_ids)) { + child.mv_lines_deselected = _.filter(child.mv_lines_deselected, function(o){ return line_ids.indexOf(o.id) === -1 }); + child.updateMatches(); + } else if (contains_lines(child.get("mv_lines"), line_ids)) { + child.updateMatches(); + } + } }); }, - - unexcludeMoveLines: function(line_ids) { + + unexcludeMoveLines: function(source_child, partner_id, line_ids) { var self = this; - var index = -1; - _.each(line_ids, function(line_id){ - line_id = parseInt(line_id); - index = self.excluded_move_lines_ids.indexOf(line_id); - if (index > -1) { - self.excluded_move_lines_ids.splice(index,1); - } - }); - //update all children view + + var initial_excluded_lines_num = this.excluded_move_lines_ids[partner_id].length; + this.excluded_move_lines_ids[partner_id] = _.difference(this.excluded_move_lines_ids[partner_id], line_ids); + if (this.excluded_move_lines_ids[partner_id].length === initial_excluded_lines_num) + return; + + // Update children if needed _.each(self.getChildren(), function(child){ - child.render(); + if (child.partner_id === partner_id && child !== source_child && (child.get("mode") === "match" || child.$el.hasClass("no_match"))) + child.updateMatches(); }); }, @@ -313,6 +328,11 @@ openerp.account = function (instance) { if (self.last_displayed_reconciliation_index < self.st_lines.length) { self.displayReconciliation(self.st_lines[self.last_displayed_reconciliation_index++], 'inactive'); } + // Congratulate the user if the work is done + if (self.reconciled_lines === self.st_lines.length) { + self.displayDoneMessage(); + } + // Put the first line in match mode if (self.reconciled_lines !== self.st_lines.length) { var first_child = self.getChildren()[0]; @@ -320,15 +340,12 @@ openerp.account = function (instance) { first_child.set("mode", "match"); } } - // Congratulate the user if the work is done - if (self.reconciled_lines === self.st_lines.length) { - self.displayDoneMessage(); - } }, displayDoneMessage: function() { var self = this; + var is_single_statement = self.statement_ids !== undefined && self.statement_ids.length === 1; var sec_taken = Math.round((Date.now()-self.time_widget_loaded)/1000); var sec_per_item = Math.round(sec_taken/self.reconciled_lines); var achievements = []; @@ -364,7 +381,7 @@ openerp.account = function (instance) { transactions_done: self.reconciled_lines, done_with_ctrl_enter: self.lines_reconciled_with_ctrl_enter, achievements: achievements, - has_statement_id: self.statement_id !== undefined, + has_statement_id: is_single_statement, })); // Animate it @@ -383,11 +400,11 @@ openerp.account = function (instance) { }); }); - if (self.$(".button_close_statement").length !== 0) { + if (is_single_statement && self.$(".button_close_statement").length !== 0) { self.$(".button_close_statement").hide(); self.model_bank_statement .query(["balance_end_real", "balance_end"]) - .filter([['id', '=', self.statement_id]]) + .filter([['id', '=', self.statement_ids[0]]]) .first() .then(function(data){ if (data.balance_end_real === data.balance_end) { @@ -395,7 +412,7 @@ openerp.account = function (instance) { self.$(".button_close_statement").click(function() { self.$(".button_close_statement").attr("disabled", "disabled"); self.model_bank_statement - .call("button_confirm_bank", [[self.statement_id]]) + .call("button_confirm_bank", [[self.statement_ids[0]]]) .then(function () { self.do_action({ type: 'ir.actions.client', @@ -456,9 +473,12 @@ openerp.account = function (instance) { init: function(parent, context) { this._super(parent); + this.formatCurrency = this.getParent().formatCurrency; if (context.initial_data_provided) { // Process data - _(context.reconciliation_proposition).each(this.decorateMoveLine.bind(this)); + _.each(context.reconciliation_proposition, function(line) { + this.decorateMoveLine(line, context.st_line.currency_id); + }, this); this.set("mv_lines_selected", context.reconciliation_proposition); this.st_line = context.st_line; this.partner_id = context.st_line.partner_id; @@ -466,7 +486,9 @@ openerp.account = function (instance) { // Exclude selected move lines var selected_line_ids = _(context.reconciliation_proposition).map(function(o){ return o.id }); - this.getParent().excludeMoveLines(selected_line_ids); + if (this.getParent().excluded_move_lines_ids[this.partner_id] === undefined) + this.getParent().excluded_move_lines_ids[this.partner_id] = []; + this.getParent().excludeMoveLines(this, this.partner_id, selected_line_ids); } else { this.set("mv_lines_selected", []); this.st_line = undefined; @@ -483,29 +505,31 @@ openerp.account = function (instance) { this.model_tax = new instance.web.Model("account.tax"); this.map_account_id_code = this.getParent().map_account_id_code; this.map_tax_id_amount = this.getParent().map_tax_id_amount; - this.formatCurrency = this.getParent().formatCurrency; this.presets = this.getParent().presets; this.is_valid = true; this.is_consistent = true; // Used to prevent bad server requests + this.total_move_lines_num = undefined; // Used for pagers this.filter = ""; + // In rare cases like when deleting a statement line's partner we don't want the server to + // look for a reconciliation proposition (in this particular case it might find a move line + // matching the statement line and decide to set the statement line's partner accordingly) + this.do_load_reconciliation_proposition = true; - this.set("balance", undefined); // Debit is +, credit is - - this.on("change:balance", this, this.balanceChanged); this.set("mode", undefined); this.on("change:mode", this, this.modeChanged); + this.set("balance", undefined); // Debit is +, credit is - + this.on("change:balance", this, this.balanceChanged); this.set("pager_index", 0); this.on("change:pager_index", this, this.pagerChanged); // NB : mv_lines represent the counterpart that will be created to reconcile existing move lines, so debit and credit are inverted this.set("mv_lines", []); this.on("change:mv_lines", this, this.mvLinesChanged); + this.mv_lines_deselected = []; // deselected lines are displayed on top of the match table this.on("change:mv_lines_selected", this, this.mvLinesSelectedChanged); this.set("lines_created", []); this.set("line_created_being_edited", [{'id': 0}]); this.on("change:lines_created", this, this.createdLinesChanged); this.on("change:line_created_being_edited", this, this.createdLinesChanged); - - //all lines associated to current reconciliation - this.propositions_lines = undefined; }, start: function() { @@ -516,85 +540,97 @@ openerp.account = function (instance) { self.aestetic_animation_speed = 0; self.is_consistent = false; - if (self.context.animate_entrance) self.$el.css("opacity", "0"); - - // Fetch data - var deferred_fetch_data = new $.Deferred(); - if (! self.context.initial_data_provided) { - // Load statement line - self.model_bank_statement_line - .call("get_statement_line_for_reconciliation", [self.st_line_id]) - .then(function (data) { - self.st_line = data; - self.decorateStatementLine(self.st_line); - self.partner_id = data.partner_id; - $.when(self.loadReconciliationProposition()).then(function(){ - deferred_fetch_data.resolve(); - }); - }); - } else { - deferred_fetch_data.resolve(); + if (self.context.animate_entrance) { + self.$el.fadeOut(0); + self.$el.slideUp(0); } - - // Display the widget - return $.when(deferred_fetch_data).then(function(){ - //load all lines that can be usefull for counterparts - var deferred_total_move_lines_num = self.model_bank_statement_line - .call("get_move_lines_counterparts_id", [self.st_line.id, []]) - .then(function(lines){ - _(lines).each(self.decorateMoveLine.bind(self)); - self.propositions_lines = lines; - }); - return deferred_total_move_lines_num; - }).then(function(){ - // Render template - var presets_array = []; - for (var id in self.presets) - if (self.presets.hasOwnProperty(id)) - presets_array.push(self.presets[id]); - self.$el.prepend(QWeb.render("bank_statement_reconciliation_line", {line: self.st_line, mode: self.context.mode, presets: presets_array})); - - // Stuff that require the template to be rendered - self.$(".match").slideUp(0); - self.$(".create").slideUp(0); - if (self.st_line.no_match) self.$el.addClass("no_match"); - if (self.context.mode !== "match") self.render(); - self.bindPopoverTo(self.$(".line_info_button")); - self.createFormWidgets(); - // Special case hack : no identified partner - if (self.st_line.has_no_partner) { - self.$el.css("opacity", "0"); - self.updateBalance(); - self.$(".change_partner_container").show(0); - self.change_partner_field.$el.find("input").attr("placeholder", _t("Select Partner")); - self.$(".match").slideUp(0); - self.$el.addClass("no_partner"); - self.set("mode", self.context.mode); - self.animation_speed = self.getParent().animation_speed; - self.aestetic_animation_speed = self.getParent().aestetic_animation_speed; - self.$el.animate({opacity: 1}, self.aestetic_animation_speed); + return $.when(self.loadData()).then(function(){ + return $.when(self.render()).then(function(){ self.is_consistent = true; - return; - } - - // TODO : the .on handler's returned deferred is lost - return $.when(self.set("mode", self.context.mode)).then(function(){ - self.is_consistent = true; - - // Make sure the display is OK - self.balanceChanged(); - self.createdLinesChanged(); - self.updateAccountingViewMatchedLines(); - // Make an entrance self.animation_speed = self.getParent().animation_speed; self.aestetic_animation_speed = self.getParent().aestetic_animation_speed; - if (self.context.animate_entrance) return self.$el.animate({opacity: 1}, self.aestetic_animation_speed); + if (self.context.animate_entrance) { + return self.$el.stop(true, true).fadeIn({ duration: self.aestetic_animation_speed, queue: false }).css('display', 'none').slideDown(self.aestetic_animation_speed); + } }); }); }); }, - + + loadData: function() { + var self = this; + if (self.context.initial_data_provided) + return; + + // Get ids of selected move lines (to exclude them from reconciliation proposition) + var excluded_move_lines_ids = []; + if (self.do_load_reconciliation_proposition) { + _.each(self.getParent().excluded_move_lines_ids, function(o){ + excluded_move_lines_ids = excluded_move_lines_ids.concat(o); + }); + } + // Load statement line + return self.model_bank_statement_line + .call("get_data_for_reconciliations", [[self.st_line_id], excluded_move_lines_ids, self.do_load_reconciliation_proposition]) + .then(function (data) { + self.st_line = data[0].st_line; + self.decorateStatementLine(self.st_line); + self.partner_id = data[0].st_line.partner_id; + if (self.getParent().excluded_move_lines_ids[self.partner_id] === undefined) + self.getParent().excluded_move_lines_ids[self.partner_id] = []; + var mv_lines = []; + _.each(data[0].reconciliation_proposition, function(line) { + self.decorateMoveLine(line, self.st_line.currency_id); + mv_lines.push(line); + }, self); + self.set("mv_lines_selected", self.get("mv_lines_selected").concat(mv_lines)); + }); + }, + + render: function() { + var self = this; + var presets_array = []; + for (var id in self.presets) + if (self.presets.hasOwnProperty(id)) + presets_array.push(self.presets[id]); + self.$el.prepend(QWeb.render("bank_statement_reconciliation_line", { + line: self.st_line, + mode: self.context.mode, + presets: presets_array + })); + + // Stuff that require the template to be rendered + self.$(".match").slideUp(0); + self.$(".create").slideUp(0); + if (self.st_line.no_match) self.$el.addClass("no_match"); + self.bindPopoverTo(self.$(".line_info_button")); + self.createFormWidgets(); + // Special case hack : no identified partner + if (self.st_line.has_no_partner) { + self.$el.css("opacity", "0"); + self.updateBalance(); + self.$(".change_partner_container").show(0); + self.$(".match").slideUp(0); + self.$el.addClass("no_partner"); + self.set("mode", self.context.mode); + self.balanceChanged(); + self.updateAccountingViewMatchedLines(); + self.animation_speed = self.getParent().animation_speed; + self.aestetic_animation_speed = self.getParent().aestetic_animation_speed; + self.$el.animate({opacity: 1}, self.aestetic_animation_speed); + return; + } + + // TODO : the .on handler's returned deferred is lost + return $.when(self.set("mode", self.context.mode)).then(function(){ + // Make sure the display is OK + self.balanceChanged(); + self.createdLinesChanged(); + self.updateAccountingViewMatchedLines(); + }); + }, + restart: function(mode) { var self = this; mode = (mode === undefined ? 'inactive' : mode); @@ -603,7 +639,7 @@ openerp.account = function (instance) { _.each(self.getChildren(), function(o){ o.destroy() }); self.is_consistent = false; return $.when(self.$el.animate({opacity: 0}, self.animation_speed)).then(function() { - self.getParent().unexcludeMoveLines(_.map(self.get("mv_lines_selected"), function(o){ return o.id })); + self.getParent().unexcludeMoveLines(self, self.partner_id, _.map(self.get("mv_lines_selected"), function(o){ return o.id })); $.each(self.$(".bootstrap_popover"), function(){ $(this).popover('destroy') }); self.$el.empty(); self.$el.removeClass("no_partner"); @@ -617,6 +653,7 @@ openerp.account = function (instance) { self.set("pager_index", 0, {silent: true}); self.set("mv_lines", [], {silent: true}); self.set("mv_lines_selected", [], {silent: true}); + self.mv_lines_deselected = []; self.set("lines_created", [], {silent: true}); self.set("line_created_being_edited", [{'id': 0}], {silent: true}); // Rebirth @@ -749,6 +786,7 @@ openerp.account = function (instance) { self.change_partner_field.on("change:value", self.change_partner_field, function() { self.changePartner(this.get_value()); }); + self.change_partner_field.$el.find("input").attr("placeholder", _t("Select Partner")); field_manager.do_show(); }, @@ -761,20 +799,19 @@ openerp.account = function (instance) { }, // adds fields, prefixed with q_, to the move line for qweb rendering - decorateMoveLine: function(line){ + decorateMoveLine: function(line, currency_id) { line.partial_reconcile = false; line.propose_partial_reconcile = false; + line['credit'] = [line['debit'], line['debit'] = line['credit']][0]; line.q_due_date = (line.date_maturity === false ? line.date : line.date_maturity); line.q_amount = (line.debit !== 0 ? "- "+line.q_debit : "") + (line.credit !== 0 ? line.q_credit : ""); - line.q_popover = QWeb.render("bank_statement_reconciliation_move_line_details", {line: line}); line.q_label = line.name; - - if (line.has_no_partner){ - line.q_label = line.partner_name + ': ' +line.q_label; - } - // WARNING : pretty much of a ugly hack - // The value of account_move.ref is either the move's communication or it's name without the slashes - if (line.ref && line.ref !== line.name.replace(/\//g,'')) + line.debit_str = this.formatCurrency(line.debit, currency_id); + line.credit_str = this.formatCurrency(line.credit, currency_id); + line.q_popover = QWeb.render("bank_statement_reconciliation_move_line_details", {line: line}); + if (line.has_no_partner) + line.q_label = line.partner_name + ': ' + line.q_label; + if (line.ref && line.ref !== line.name) line.q_label += " : " + line.ref; }, @@ -819,31 +856,77 @@ openerp.account = function (instance) { selectMoveLine: function(mv_line) { var self = this; var line_id = mv_line.dataset.lineid; - var line = _.find(self.propositions_lines, function(o){ return o.id == line_id}); - $(mv_line).attr('data-selected','true'); - self.getParent().excludeMoveLines([line_id]); - self.set("mv_lines_selected", self.get("mv_lines_selected").concat(line)); + + // find the line in mv_lines or mv_lines_deselected + var line = _.find(self.get("mv_lines"), function(o){ return o.id == line_id}); + if (! line) { + line = _.find(self.mv_lines_deselected, function(o){ return o.id == line_id }); + self.mv_lines_deselected = _.filter(self.mv_lines_deselected, function(o) { return o.id != line_id }); + } + if (! line) return; // If no line found, we've got a syncing problem (let's turn a deaf ear) + + // Warn the user if he's selecting lines from both a payable and a receivable account + var last_selected_line = _.last(self.get("mv_lines_selected")); + if (last_selected_line && last_selected_line.account_type != line.account_type) { + new instance.web.Dialog(this, { + title: _t("Warning"), + size: 'medium', + }, $("
").text(_.str.sprintf(_t("You are selecting transactions from both a payable and a receivable account.\n\nIn order to proceed, you first need to deselect the %s transactions."), last_selected_line.account_type))).open(); + return; + } + + // If statement line has no partner, give it the partner of the selected move line + if (!this.st_line.partner_id && line.partner_id) { + self.changePartner(line.partner_id, function() { + self.selectMoveLine(mv_line); + }); + } else { + self.set("mv_lines_selected", self.get("mv_lines_selected").concat(line)); + // $(mv_line).attr('data-selected','true'); + // self.set("mv_lines_selected", self.get("mv_lines_selected").concat(line)); + // this.set("mv_lines", _.reject(this.get("mv_lines"), function(o){return o.id == line_id})); + // this.getParent().excludeMoveLines([line_id]); + } }, deselectMoveLine: function(mv_line) { var self = this; var line_id = mv_line.dataset.lineid; - var line = _.find(self.propositions_lines, function(o){ return o.id == line_id}); - $(mv_line).attr('data-selected','false'); - self.getParent().unexcludeMoveLines([line_id]); - self.set("mv_lines_selected",_.filter(self.get("mv_lines_selected"), function(o) { return o.id != line_id })); + var line = _.find(self.get("mv_lines_selected"), function(o){ return o.id == line_id}); + if (! line) return; // If no line found, we've got a syncing problem (let's turn a deaf ear) + + // add the line to mv_lines_deselected and remove it from mv_lines_selected + self.mv_lines_deselected.unshift(line); + var mv_lines_selected = _.filter(self.get("mv_lines_selected"), function(o) { return o.id != line_id }); + + // remove partial reconciliation stuff if necessary + if (line.partial_reconcile === true) self.unpartialReconcileLine(line); + if (line.propose_partial_reconcile === true) line.propose_partial_reconcile = false; + + self.$el.removeClass("no_match"); + self.set("mode", "match"); + self.set("mv_lines_selected", mv_lines_selected); + + + // $(mv_line).attr('data-selected','false'); + // this.set("mv_lines", this.get("mv_lines").concat(line)); + // this.getParent().unexcludeMoveLines([line_id]); }, /** Matches pagination */ pagerControlLeftHandler: function() { var self = this; + if (self.$(".pager_control_left").hasClass("disabled")) { return; /* shouldn't happen, anyway*/ } + if (self.total_move_lines_num < 0) { return; } self.set("pager_index", self.get("pager_index")-1 ); }, - + pagerControlRightHandler: function() { var self = this; var new_index = self.get("pager_index")+1; + if (self.$(".pager_control_right").hasClass("disabled")) { return; /* shouldn't happen, anyway*/ } + if ((new_index * self.max_move_lines_displayed) >= self.total_move_lines_num) { return; } self.set("pager_index", new_index ); }, @@ -851,8 +934,10 @@ openerp.account = function (instance) { var self = this; self.set("pager_index", 0); self.filter = self.$(".filter").val(); - self.render(); + window.clearTimeout(self.apply_filter_timeout); + self.apply_filter_timeout = window.setTimeout(self.proxy('updateMatches'), 200); }, + /** Creating */ @@ -862,6 +947,7 @@ openerp.account = function (instance) { _.each(self.create_form, function(field) { field.set("value", false); }); + self.label_field.set("value", self.st_line.name); self.amount_field.set("value", -1*self.get("balance")); self.account_id_field.focus(); }, @@ -894,26 +980,31 @@ openerp.account = function (instance) { var self = this; self.initializeCreateForm(); var preset = self.presets[e.currentTarget.dataset.presetid]; + // Hack : set_value of a field calls a handler that returns a deferred because it could make a RPC call + // to compute the tax before it updates the line being edited. Unfortunately this deferred is lost. + // Hence this ugly hack to avoid concurrency problem that arose when setting amount (in initializeCreateForm), then tax, then another amount + if (preset.tax && self.tax_field) self.tax_field.set_value(false); + if (preset.amount && self.amount_field) self.amount_field.set_value(false); + for (var key in preset) { if (! preset.hasOwnProperty(key) || key === "amount") continue; - if (self.hasOwnProperty(key+"_field")) + if (preset[key] && self.hasOwnProperty(key+"_field")) self[key+"_field"].set_value(preset[key]); } - var sign = self.amount_field.get_value() < 0 ? -1 : 1; if (preset.amount && self.amount_field) { if (preset.amount_type === "fixed") - self.amount_field.set_value(sign * preset.amount); + self.amount_field.set_value(preset.amount); else if (preset.amount_type === "percentage_of_total") - self.amount_field.set_value(sign * self.st_line.amount * preset.amount / 100); + self.amount_field.set_value(self.st_line.amount * preset.amount / 100); else if (preset.amount_type === "percentage_of_balance") { self.amount_field.set_value(0); self.updateBalance(); - self.amount_field.set_value(sign * Math.abs(self.get("balance")) * preset.amount / 100); + self.amount_field.set_value(-1 * self.get("balance") * preset.amount / 100); } } }, - + /** Display */ initialLineClickHandler: function() { @@ -937,9 +1028,11 @@ openerp.account = function (instance) { partnerNameClickHandler: function() { var self = this; - self.$(".partner_name").hide(); - self.change_partner_field.$el.find("input").attr("placeholder", self.st_line.partner_name); - self.$(".change_partner_container").show(); + // Delete statement line's partner + return self.changePartner('', function() { + self.$(".partner_name").hide(); + self.$(".change_partner_container").show(); + }); }, @@ -979,40 +1072,43 @@ openerp.account = function (instance) { var self = this; var table = self.$(".match table"); var nothing_displayed = true; - + // Display move lines $.each(self.$(".match table .bootstrap_popover"), function(){ $(this).popover('destroy') }); table.empty(); var slice_start = self.get("pager_index") * self.max_move_lines_displayed; var slice_end = (self.get("pager_index")+1) * self.max_move_lines_displayed; - - var visible = 0 - _(self.get("mv_lines")).each(function(line){ - if (visible >= slice_start && visible < slice_end) { - var $line = $(QWeb.render("bank_statement_reconciliation_move_line", {line: line, selected: false})); - self.bindPopoverTo($line.find(".line_info_button")); - table.append($line); - nothing_displayed = false; - } - visible = visible + 1; + _( _.filter(self.mv_lines_deselected, function(o){ + return o.name.indexOf(self.filter) !== -1 || o.ref.indexOf(self.filter) !== -1 }) + .slice(slice_start, slice_end)).each(function(line){ + var $line = $(QWeb.render("bank_statement_reconciliation_move_line", {line: line, selected: false})); + self.bindPopoverTo($line.find(".line_info_button")); + table.append($line); + nothing_displayed = false; }); - if (nothing_displayed) + _(self.get("mv_lines")).each(function(line){ + var $line = $(QWeb.render("bank_statement_reconciliation_move_line", {line: line, selected: false})); + self.bindPopoverTo($line.find(".line_info_button")); + table.append($line); + nothing_displayed = false; + }); + if (nothing_displayed && this.filter !== "") table.append(QWeb.render("filter_no_match", {filter_str: self.filter})); }, updatePagerControls: function() { var self = this; + if (self.get("pager_index") === 0) self.$(".pager_control_left").addClass("disabled"); else self.$(".pager_control_left").removeClass("disabled"); - if (self.get('mv_lines').length <= ((self.get("pager_index")+1) * self.max_move_lines_displayed)) + if (self.total_move_lines_num <= ((self.get("pager_index")+1) * self.max_move_lines_displayed)) self.$(".pager_control_right").addClass("disabled"); else self.$(".pager_control_right").removeClass("disabled"); }, - /** Properties changed */ // Updates the validation button and the "open balance" line @@ -1034,8 +1130,12 @@ openerp.account = function (instance) { self.is_valid = false; var debit = (balance > 0 ? self.formatCurrency(balance, self.st_line.currency_id) : ""); var credit = (balance < 0 ? self.formatCurrency(-1*balance, self.st_line.currency_id) : ""); - var $line = $(QWeb.render("bank_statement_reconciliation_line_open_balance", {debit: debit, credit: credit, account_code: self.map_account_id_code[self.st_line.open_balance_account_id]})); - $line.find('.js_open_balance')[0].innerHTML = "Choose counterpart"; + var $line = $(QWeb.render("bank_statement_reconciliation_line_open_balance", { + debit: debit, + credit: credit, + account_code: self.map_account_id_code[self.st_line.open_balance_account_id] + })); + $line.find('.js_open_balance')[0].innerHTML = _t("Choose counterpart"); self.$(".tbody_open_balance").append($line); } return; @@ -1049,7 +1149,11 @@ openerp.account = function (instance) { self.$(".button_ok").text("Keep open"); var debit = (balance > 0 ? self.formatCurrency(balance, self.st_line.currency_id) : ""); var credit = (balance < 0 ? self.formatCurrency(-1*balance, self.st_line.currency_id) : ""); - var $line = $(QWeb.render("bank_statement_reconciliation_line_open_balance", {debit: debit, credit: credit, account_code: self.map_account_id_code[self.st_line.open_balance_account_id]})); + var $line = $(QWeb.render("bank_statement_reconciliation_line_open_balance", { + debit: debit, + credit: credit, + account_code: self.map_account_id_code[self.st_line.open_balance_account_id] + })); self.$(".tbody_open_balance").append($line); } }, @@ -1059,15 +1163,21 @@ openerp.account = function (instance) { self.$(".action_pane.active").removeClass("active"); - // Special case hack : if no_partner and mode == inactive + // Special case hack : if no_partner, either inactive or create if (self.st_line.has_no_partner) { if (self.get("mode") === "inactive") { self.$(".match").slideUp(self.animation_speed); self.$(".create").slideUp(self.animation_speed); self.$(".toggle_match").removeClass("visible_toggle"); self.el.dataset.mode = "inactive"; - return; - } + } else { + self.initializeCreateForm(); + self.$(".match").slideUp(self.animation_speed); + self.$(".create").slideDown(self.animation_speed); + self.$(".toggle_match").addClass("visible_toggle"); + self.el.dataset.mode = "create"; + } + return; } if (self.get("mode") === "inactive") { @@ -1076,7 +1186,7 @@ openerp.account = function (instance) { self.el.dataset.mode = "inactive"; } else if (self.get("mode") === "match") { - return $.when(self.render()).then(function() { + return $.when(self.updateMatches()).then(function() { if (self.$el.hasClass("no_match")) { self.set("mode", "inactive"); return; @@ -1095,15 +1205,18 @@ openerp.account = function (instance) { }, pagerChanged: function() { - var self = this; - self.render(); + this.updateMatches(); }, mvLinesChanged: function() { var self = this; - + // If pager_index is out of range, set it to display the last page + if (self.get("pager_index") !== 0 && self.total_move_lines_num <= (self.get("pager_index") * self.max_move_lines_displayed)) { + self.set("pager_index", Math.ceil(self.total_move_lines_num/self.max_move_lines_displayed)-1); + } + // If there is no match to display, disable match view and pass in mode inactive - if (self.get("mv_lines").length === 0 && self.filter === "") { + if (self.total_move_lines_num + self.mv_lines_deselected.length === 0 && self.filter === "") { self.$el.addClass("no_match"); if (self.get("mode") === "match") { self.set("mode", "inactive"); @@ -1118,23 +1231,24 @@ openerp.account = function (instance) { mvLinesSelectedChanged: function(elt, val) { var self = this; - + var added_lines_ids = _.map(_.difference(val.newValue, val.oldValue), function(o){ return o.id }); var removed_lines_ids = _.map(_.difference(val.oldValue, val.newValue), function(o){ return o.id }); - - self.getParent().excludeMoveLines(added_lines_ids); - self.getParent().unexcludeMoveLines(removed_lines_ids); - - self.updateAccountingViewMatchedLines(); - self.updateBalance(); + + self.getParent().excludeMoveLines(self, self.partner_id, added_lines_ids); + self.getParent().unexcludeMoveLines(self, self.partner_id, removed_lines_ids); + + $.when(self.updateMatches()).then(function(){ + self.updateAccountingViewMatchedLines(); + self.updateBalance(); + }); }, - + // Generic function for updating the line_created_being_edited formCreateInputChanged: function(elt, val) { var self = this; var line_created_being_edited = self.get("line_created_being_edited"); line_created_being_edited[0][elt.corresponding_property] = val.newValue; - line_created_being_edited[0].currency_id = self.st_line.currency_id; // Specific cases @@ -1155,17 +1269,26 @@ openerp.account = function (instance) { var current_line_cursor = 1; $.each(data.taxes, function(index, tax){ if (tax.amount !== 0.0) { - var tax_account_id = (amount > 0 ? tax.account_collected_id : tax.account_paid_id) - tax_account_id = tax_account_id !== false ? tax_account_id: line_created_being_edited[0].account_id - line_created_being_edited[current_line_cursor] = {id: line_created_being_edited[0].id, account_id: tax_account_id, account_num: self.map_account_id_code[tax_account_id], label: tax.name, amount: tax.amount, no_remove_action: true, currency_id: self.st_line.currency_id, is_tax_line: true}; + var tax_account_id = (amount > 0 ? tax.account_collected_id : tax.account_paid_id); + tax_account_id = tax_account_id !== false ? tax_account_id: line_created_being_edited[0].account_id; + line_created_being_edited[current_line_cursor] = { + id: line_created_being_edited[0].id, + account_id: tax_account_id, + account_num: self.map_account_id_code[tax_account_id], + label: tax.name, + amount: tax.amount, + no_remove_action: true, + currency_id: self.st_line.currency_id, + is_tax_line: true + }; current_line_cursor = current_line_cursor + 1; - }; + } }); } ); } else { line_created_being_edited[0].amount = amount; - delete line_created_being_edited[1]; + line_created_being_edited.length = 1; deferred_tax.resolve(); } } else { deferred_tax.resolve(); } @@ -1211,7 +1334,7 @@ openerp.account = function (instance) { var balance = self.get("balance"); line.initial_amount = line.debit !== 0 ? line.debit : -1 * line.credit; if (balance < 0) { - line.debit -= balance; + line.debit += balance; line.debit_str = self.formatCurrency(line.debit, self.st_line.currency_id); } else { line.credit -= balance; @@ -1250,6 +1373,19 @@ openerp.account = function (instance) { updateBalance: function() { var self = this; var mv_lines_selected = self.get("mv_lines_selected"); + var lines_selected_num = mv_lines_selected.length; + var lines_created_num = self.getCreatedLines().length; + + // Undo partial reconciliation if necessary + if (lines_selected_num !== 1 || lines_created_num !== 0) { + _.each(mv_lines_selected, function(line) { + if (line.partial_reconcile === true) self.unpartialReconcileLine(line); + if (line.propose_partial_reconcile === true) line.propose_partial_reconcile = false; + }); + self.updateAccountingViewMatchedLines(); + } + + // Compute balance var balance = 0; balance -= self.st_line.amount; _.each(mv_lines_selected, function(o) { @@ -1258,57 +1394,71 @@ openerp.account = function (instance) { _.each(self.getCreatedLines(), function(o) { balance += o.amount; }); + // Should work as long as currency's rounding factor is > 0.001 (ie: don't use gold kilos as a currency) + balance = Math.round(balance*1000)/1000; self.set("balance", balance); // Propose partial reconciliation if necessary - var lines_selected_num = mv_lines_selected.length; - var lines_created_num = self.getCreatedLines().length; if (lines_selected_num === 1 && lines_created_num === 0 && self.st_line.amount * balance > 0) { mv_lines_selected[0].propose_partial_reconcile = true; self.updateAccountingViewMatchedLines(); } - if (lines_selected_num !== 1 || lines_created_num !== 0) { - // remove partial reconciliation stuff if necessary - _.each(mv_lines_selected, function(line) { - if (line.partial_reconcile === true) self.unpartialReconcileLine(line); - if (line.propose_partial_reconcile === true) line.propose_partial_reconcile = false; - }); - self.updateAccountingViewMatchedLines(); - } - }, - - loadReconciliationProposition: function() { - var self = this; - return self.model_bank_statement_line - .call("get_reconciliation_proposition", [self.st_line.id, self.getParent().excluded_move_lines_ids]) - .then(function (lines) { - _(lines).each(self.decorateMoveLine.bind(self)); - self.set("mv_lines_selected", self.get("mv_lines_selected").concat(lines)); - }); }, - render: function() { + // Loads move lines according to the widget's state + updateMatches: function() { + if (this.st_line.has_no_partner) return; var self = this; - var lines_to_show = []; - _.each(self.propositions_lines, function(line){ - var filter = (line.q_label.toLowerCase().indexOf(self.filter.toLowerCase()) > -1 || line.account_code.toLowerCase().indexOf(self.filter.toLowerCase()) > -1); - if (self.getParent().excluded_move_lines_ids.indexOf(line.id) === -1 && filter) { - lines_to_show.push(line); - } + var deselected_lines_num = self.mv_lines_deselected.length; + var move_lines_num = 0; + var offset = self.get("pager_index") * self.max_move_lines_displayed - deselected_lines_num; + if (offset < 0) offset = 0; + var limit = (self.get("pager_index")+1) * self.max_move_lines_displayed - deselected_lines_num; + if (limit > self.max_move_lines_displayed) limit = self.max_move_lines_displayed; + var excluded_ids = _.collect(self.get("mv_lines_selected").concat(self.mv_lines_deselected), function(o){ return o.id }); + excluded_ids = excluded_ids.concat(self.getParent().excluded_move_lines_ids[self.partner_id]); + + var deferred_move_lines; + var move_lines = []; + if (limit > 0) { + // Load move lines + deferred_move_lines = self.model_bank_statement_line + .call("get_move_lines_for_reconciliation_by_statement_line_id", [self.st_line.id, excluded_ids, self.filter, offset, limit]) + .then(function (lines) { + _.each(lines, function(line) { + self.decorateMoveLine(line, self.st_line.currency_id); + move_lines.push(line); + }, self); + }); + } + + // Fetch the number of move lines corresponding to this statement line and this filter + var deferred_total_move_lines_num = self.model_bank_statement_line + .call("get_move_lines_for_reconciliation_by_statement_line_id", [self.st_line.id, excluded_ids, self.filter, 0, undefined, true]) + .then(function(num){ + move_lines_num = num; + }); + + return $.when(deferred_move_lines, deferred_total_move_lines_num).then(function(){ + self.total_move_lines_num = move_lines_num + deselected_lines_num; + self.set("mv_lines", move_lines); }); - self.set("mv_lines", lines_to_show); }, - + // Changes the partner_id of the statement_line in the DB and reloads the widget - changePartner: function(partner_id) { + changePartner: function(partner_id, callback) { var self = this; self.is_consistent = false; return self.model_bank_statement_line // Update model .call("write", [[self.st_line_id], {'partner_id': partner_id}]) .then(function () { + self.do_load_reconciliation_proposition = false; // of the server might set the statement line's partner return $.when(self.restart(self.get("mode"))).then(function(){ + self.do_load_reconciliation_proposition = true; self.is_consistent = true; + self.set("mode", "match"); + if (callback) callback(); }); }); }, @@ -1353,16 +1503,13 @@ openerp.account = function (instance) { }, // Persist data, notify parent view and terminate widget - persistAndDestroy: function() { + persistAndDestroy: function(speed) { var self = this; + speed = (isNaN(speed) ? self.animation_speed : speed); if (! self.is_consistent) return; - - // Prepare data - var mv_line_dicts = []; - _.each(self.get("mv_lines_selected"), function(o) { mv_line_dicts.push(self.prepareSelectedMoveLineForPersisting(o)) }); - _.each(self.getCreatedLines(), function(o) { mv_line_dicts.push(self.prepareCreatedMoveLineForPersisting(o)) }); - if (Math.abs(self.get("balance")).toFixed(3) !== "0.000") mv_line_dicts.push(self.prepareOpenBalanceForPersisting()); - + + self.getParent().unexcludeMoveLines(self, self.partner_id, _.map(self.get("mv_lines_selected"), function(o){ return o.id })); + // Sliding animation var height = self.$el.outerHeight(); var container = $("
"); @@ -1370,11 +1517,10 @@ openerp.account = function (instance) { .css("marginTop", self.$el.css("marginTop")) .css("marginBottom", self.$el.css("marginBottom")); self.$el.wrap(container); - var deferred_animation = self.$el.parent().slideUp(self.animation_speed*height/150); + var deferred_animation = self.$el.parent().slideUp(speed*height/150); // RPC - return self.model_bank_statement_line - .call("process_reconciliation", [self.st_line_id, mv_line_dicts]) + return $.when(self.makeRPCForPersisting()) .then(function () { $.each(self.$(".bootstrap_popover"), function(){ $(this).popover('destroy') }); return $.when(deferred_animation).then(function(){ @@ -1385,11 +1531,20 @@ openerp.account = function (instance) { }); }); }, function(){ - self.$el.parent().slideDown(self.animation_speed*height/150, function(){ + self.$el.parent().slideDown(speed*height/150, function(){ self.$el.unwrap(); }); }); - + }, + + makeRPCForPersisting: function() { + var self = this; + var mv_line_dicts = []; + _.each(self.get("mv_lines_selected"), function(o) { mv_line_dicts.push(self.prepareSelectedMoveLineForPersisting(o)) }); + _.each(self.getCreatedLines(), function(o) { mv_line_dicts.push(self.prepareCreatedMoveLineForPersisting(o)) }); + if (Math.abs(self.get("balance")).toFixed(3) !== "0.000") mv_line_dicts.push(self.prepareOpenBalanceForPersisting()); + return self.model_bank_statement_line + .call("process_reconciliation", [self.st_line_id, mv_line_dicts]); }, }); diff --git a/addons/account/static/src/xml/account_bank_statement_reconciliation.xml b/addons/account/static/src/xml/account_bank_statement_reconciliation.xml index 83680f005e3..4e32d97eceb 100644 --- a/addons/account/static/src/xml/account_bank_statement_reconciliation.xml +++ b/addons/account/static/src/xml/account_bank_statement_reconciliation.xml @@ -53,22 +53,21 @@ - - - - + + + - - - + @@ -115,7 +114,7 @@
- +
: - + () + +
- + @@ -123,40 +122,26 @@
Date
Partner
Partner
Transaction
Description
Amount ()
- - - - - - - - - - - - - - - - - + + + + + + + - - - + - - - + - - + + @@ -168,28 +153,27 @@
ID
Account
id
Account
Journal
Period
Date
- - - - - - - - + + + + + + + - - - - Open balance - - - + + + + Open balance + + + diff --git a/addons/account/tests/__init__.py b/addons/account/tests/__init__.py index 02e9677ae03..e09bcc6ee99 100644 --- a/addons/account/tests/__init__.py +++ b/addons/account/tests/__init__.py @@ -1,7 +1,9 @@ from . import test_tax from . import test_search +from . import test_reconciliation fast_suite = [ test_tax, test_search, + test_reconciliation, ] diff --git a/addons/account/tests/test_bank_stmt_reconciliation_widget_ui.py b/addons/account/tests/test_bank_stmt_reconciliation_widget_ui.py new file mode 100644 index 00000000000..fe3a84ffd6c --- /dev/null +++ b/addons/account/tests/test_bank_stmt_reconciliation_widget_ui.py @@ -0,0 +1,8 @@ +import openerp.tests + +@openerp.tests.common.at_install(False) +@openerp.tests.common.post_install(True) + +class TestUi(openerp.tests.HttpCase): + def test_01_admin_bank_statement_reconciliation(self): + self.phantom_js("/", "openerp.Tour.run('bank_statement_reconciliation', 'test')", "openerp.Tour.tours.bank_statement_reconciliation", login="admin") \ No newline at end of file diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py new file mode 100644 index 00000000000..62d44198153 --- /dev/null +++ b/addons/account/tests/test_reconciliation.py @@ -0,0 +1,162 @@ +from openerp.tests.common import TransactionCase + +class TestReconciliation(TransactionCase): + """Tests for reconciliation (account.tax) + + Test used to check that when doing a sale or purchase invoice in a different currency, + the result will be balanced. + """ + + def setUp(self): + super(TestReconciliation, self).setUp() + self.account_invoice_model = self.registry('account.invoice') + self.account_invoice_line_model = self.registry('account.invoice.line') + self.acc_bank_stmt_model = self.registry('account.bank.statement') + self.acc_bank_stmt_line_model = self.registry('account.bank.statement.line') + + self.partner_agrolait_id = self.registry("ir.model.data").get_object_reference(self.cr, self.uid, "base", "res_partner_2")[1] + self.currency_swiss_id = self.registry("ir.model.data").get_object_reference(self.cr, self.uid, "base", "CHF")[1] + self.currency_usd_id = self.registry("ir.model.data").get_object_reference(self.cr, self.uid, "base", "USD")[1] + self.account_rcv_id = self.registry("ir.model.data").get_object_reference(self.cr, self.uid, "account", "a_recv")[1] + self.account_rsa_id = self.registry("ir.model.data").get_object_reference(self.cr, self.uid, "account", "rsa")[1] + self.product_id = self.registry("ir.model.data").get_object_reference(self.cr, self.uid, "product", "product_product_4")[1] + + self.bank_journal_usd_id = self.registry("ir.model.data").get_object_reference(self.cr, self.uid, "account", "bank_journal_usd")[1] + self.account_usd_id = self.registry("ir.model.data").get_object_reference(self.cr, self.uid, "account", "usd_bnk")[1] + + self.company_id = self.registry("ir.model.data").get_object_reference(self.cr, self.uid, "base", "main_company")[1] + #set expense_currency_exchange_account_id and income_currency_exchange_account_id to a random account + self.registry("res.company").write(self.cr, self.uid, [self.company_id], {'expense_currency_exchange_account_id': self.account_rsa_id, 'income_currency_exchange_account_id':self.account_rsa_id}) + + def test_balanced_customer_invoice(self): + cr, uid = self.cr, self.uid + #we create an invoice in CHF + invoice_id = self.account_invoice_model.create(cr, uid, {'partner_id': self.partner_agrolait_id, + 'reference_type': 'none', + 'currency_id': self.currency_swiss_id, + 'name': 'invoice to client', + 'account_id': self.account_rcv_id, + 'type': 'out_invoice' + }) + self.account_invoice_line_model.create(cr, uid, {'product_id': self.product_id, + 'quantity': 1, + 'price_unit': 100, + 'invoice_id': invoice_id, + 'name': 'product that cost 100',}) + + #validate purchase + self.registry('account.invoice').signal_workflow(cr, uid, [invoice_id], 'invoice_open') + invoice_record = self.account_invoice_model.browse(cr, uid, [invoice_id]) + + #we pay half of it on a journal with currency in dollar (bank statement) + bank_stmt_id = self.acc_bank_stmt_model.create(cr, uid, {'journal_id': self.bank_journal_usd_id,}) + + bank_stmt_line_id = self.acc_bank_stmt_line_model.create(cr, uid, {'name': 'half payment', + 'statement_id': bank_stmt_id, + 'partner_id': self.partner_agrolait_id, + 'amount': 42, + 'amount_currency': 50, + 'currency_id': self.currency_swiss_id,}) + + #reconcile the payment with the invoice + for l in invoice_record.move_id.line_id: + if l.account_id.id == self.account_rcv_id: + line_id = l + break + self.acc_bank_stmt_line_model.process_reconciliation(cr, uid, bank_stmt_line_id, [ + {'counterpart_move_line_id': line_id.id, 'credit':50, 'debit':0, 'name': line_id.name,}]) + + #we check that the line is balanced (bank statement line) + move_line_ids = self.acc_bank_stmt_model.browse(cr,uid,bank_stmt_id).move_line_ids + + self.assertEquals(len(move_line_ids), 3) + checked_line = 0 + for move_line in move_line_ids: + if move_line.account_id.id == self.account_usd_id: + self.assertEquals(move_line.debit, 27.47) + self.assertEquals(move_line.credit, 0.0) + self.assertEquals(move_line.amount_currency, 42) + self.assertEquals(move_line.currency_id.id, self.currency_usd_id) + checked_line += 1 + continue + if move_line.account_id.id == self.account_rcv_id: + self.assertEquals(move_line.debit, 0.0) + self.assertEquals(move_line.credit, 38.21) + self.assertEquals(move_line.amount_currency, -50) + self.assertEquals(move_line.currency_id.id, self.currency_swiss_id) + checked_line += 1 + continue + if move_line.account_id.id == self.account_rsa_id: + self.assertEquals(move_line.debit, 10.74) + self.assertEquals(move_line.credit, 0.0) + checked_line += 1 + continue + self.assertEquals(checked_line, 3) + + + + def test_balanced_supplier_invoice(self): + cr, uid = self.cr, self.uid + #we create a supplier invoice in CHF + invoice_id = self.account_invoice_model.create(cr, uid, {'partner_id': self.partner_agrolait_id, + 'reference_type': 'none', + 'currency_id': self.currency_swiss_id, + 'name': 'invoice to client', + 'account_id': self.account_rcv_id, + 'type': 'in_invoice' + }) + self.account_invoice_line_model.create(cr, uid, {'product_id': self.product_id, + 'quantity': 1, + 'price_unit': 100, + 'invoice_id': invoice_id, + 'name': 'product that cost 100',}) + + #validate purchase + self.registry('account.invoice').signal_workflow(cr, uid, [invoice_id], 'invoice_open') + invoice_record = self.account_invoice_model.browse(cr, uid, [invoice_id]) + + #we pay half of it on a journal with currency in dollar (bank statement) + bank_stmt_id = self.acc_bank_stmt_model.create(cr, uid, {'journal_id': self.bank_journal_usd_id,}) + + bank_stmt_line_id = self.acc_bank_stmt_line_model.create(cr, uid, {'name': 'half payment', + 'statement_id': bank_stmt_id, + 'partner_id': self.partner_agrolait_id, + 'amount': -42, + 'amount_currency': -50, + 'currency_id': self.currency_swiss_id,}) + + #reconcile the payment with the invoice + for l in invoice_record.move_id.line_id: + if l.account_id.id == self.account_rcv_id: + line_id = l + break + self.acc_bank_stmt_line_model.process_reconciliation(cr, uid, bank_stmt_line_id, [ + {'counterpart_move_line_id': line_id.id, 'credit':0, 'debit':50, 'name': line_id.name,}]) + + #we check that the line is balanced (bank statement line) + move_line_ids = self.acc_bank_stmt_model.browse(cr,uid,bank_stmt_id).move_line_ids + + self.assertEquals(len(move_line_ids), 3) + checked_line = 0 + for move_line in move_line_ids: + if move_line.account_id.id == self.account_usd_id: + self.assertEquals(move_line.debit, 0.0) + self.assertEquals(move_line.credit, 27.47) + self.assertEquals(move_line.amount_currency, -42) + self.assertEquals(move_line.currency_id.id, self.currency_usd_id) + checked_line += 1 + continue + if move_line.account_id.id == self.account_rcv_id: + self.assertEquals(move_line.debit, 38.21) + self.assertEquals(move_line.credit, 0.0) + self.assertEquals(move_line.amount_currency, 50) + self.assertEquals(move_line.currency_id.id, self.currency_swiss_id) + checked_line += 1 + continue + if move_line.account_id.id == self.account_rsa_id: + self.assertEquals(move_line.debit, 0.0) + self.assertEquals(move_line.credit, 10.74) + checked_line += 1 + continue + self.assertEquals(checked_line, 3) + diff --git a/addons/account/views/account.xml b/addons/account/views/account.xml index 8a9c209baa0..0718615e312 100644 --- a/addons/account/views/account.xml +++ b/addons/account/views/account.xml @@ -11,6 +11,7 @@ + diff --git a/addons/account_voucher/account_voucher.py b/addons/account_voucher/account_voucher.py index a5f1621de98..1bfd5dd1224 100644 --- a/addons/account_voucher/account_voucher.py +++ b/addons/account_voucher/account_voucher.py @@ -41,47 +41,6 @@ class res_currency(osv.osv): return res -class res_company(osv.osv): - _inherit = "res.company" - _columns = { - 'income_currency_exchange_account_id': fields.many2one( - 'account.account', - string="Gain Exchange Rate Account", - domain="[('type', '=', 'other')]",), - 'expense_currency_exchange_account_id': fields.many2one( - 'account.account', - string="Loss Exchange Rate Account", - domain="[('type', '=', 'other')]",), - } - - -class account_config_settings(osv.osv_memory): - _inherit = 'account.config.settings' - _columns = { - 'income_currency_exchange_account_id': fields.related( - 'company_id', 'income_currency_exchange_account_id', - type='many2one', - relation='account.account', - string="Gain Exchange Rate Account", - domain="[('type', '=', 'other')]"), - 'expense_currency_exchange_account_id': fields.related( - 'company_id', 'expense_currency_exchange_account_id', - type="many2one", - relation='account.account', - string="Loss Exchange Rate Account", - domain="[('type', '=', 'other')]"), - } - def onchange_company_id(self, cr, uid, ids, company_id, context=None): - res = super(account_config_settings, self).onchange_company_id(cr, uid, ids, company_id, context=context) - if company_id: - company = self.pool.get('res.company').browse(cr, uid, company_id, context=context) - res['value'].update({'income_currency_exchange_account_id': company.income_currency_exchange_account_id and company.income_currency_exchange_account_id.id or False, - 'expense_currency_exchange_account_id': company.expense_currency_exchange_account_id and company.expense_currency_exchange_account_id.id or False}) - else: - res['value'].update({'income_currency_exchange_account_id': False, - 'expense_currency_exchange_account_id': False}) - return res - class account_voucher(osv.osv): def _check_paid(self, cr, uid, ids, name, args, context=None): res = {} diff --git a/addons/account_voucher/account_voucher_view.xml b/addons/account_voucher/account_voucher_view.xml index ffcc8fc4b1b..2b0508a7542 100644 --- a/addons/account_voucher/account_voucher_view.xml +++ b/addons/account_voucher/account_voucher_view.xml @@ -193,26 +193,6 @@ {'state':'posted'} - - - - account.config.settings.inherit - - account.config.settings - 20 - - - - - - - - - - - - - diff --git a/addons/l10n_be_coda/wizard/account_coda_import.py b/addons/l10n_be_coda/wizard/account_coda_import.py index 76f07d2b400..3dec320323f 100644 --- a/addons/l10n_be_coda/wizard/account_coda_import.py +++ b/addons/l10n_be_coda/wizard/account_coda_import.py @@ -286,7 +286,7 @@ class account_coda_import(osv.osv_memory): if 'counterpartyAddress' in line and line['counterpartyAddress'] != '': note.append(_('Counter Party Address') + ': ' + line['counterpartyAddress']) partner_id = None - structured_com = "" + structured_com = False bank_account_id = False if line['communication_struct'] and 'communication_type' in line and line['communication_type'] == '101': structured_com = line['communication'] @@ -322,21 +322,16 @@ class account_coda_import(osv.osv_memory): self.pool.get('account.bank.statement.line').create(cr, uid, data, context=context) if statement['coda_note'] != '': self.pool.get('account.bank.statement').write(cr, uid, [statement['id']], {'coda_note': statement['coda_note']}, context=context) - model, action_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account', 'action_bank_statement_tree') + model, action_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account', 'action_bank_reconcile_bank_statements') action = self.pool[model].browse(cr, uid, action_id, context=context) + statements_ids = [statement['id'] for statement in statements] return { 'name': action.name, - 'view_type': action.view_type, - 'view_mode': action.view_mode, - 'res_model': action.res_model, - 'domain': action.domain, - 'context': action.context, - 'type': 'ir.actions.act_window', - 'search_view_id': action.search_view_id.id, - 'views': [(v.view_id.id, v.view_mode) for v in action.view_ids] + 'tag': action.tag, + 'context': {'statement_ids': statements_ids}, + 'type': 'ir.actions.client', } - def rmspaces(s): return " ".join(s.split()) diff --git a/openerp/addons/base/res/res_currency.py b/openerp/addons/base/res/res_currency.py index 697d5ef2d2f..8a155d9a35e 100644 --- a/openerp/addons/base/res/res_currency.py +++ b/openerp/addons/base/res/res_currency.py @@ -21,6 +21,7 @@ import re import time +import math from openerp import api, fields as fields2 from openerp import tools @@ -270,6 +271,22 @@ class res_currency(osv.osv): # apply rounding return to_currency.round(to_amount) if round else to_amount + def get_format_currencies_js_function(self, cr, uid, context=None): + """ Returns a string that can be used to instanciate a javascript function that formats numbers as currencies. + That function expects the number as first parameter and the currency id as second parameter. In case of failure it returns undefined.""" + function = "" + for row in self.search_read(cr, uid, domain=[], fields=['id', 'name', 'symbol', 'rounding', 'position'], context=context): + digits = int(math.log10(1 / row['rounding'])) + symbol = row['symbol'] or row['name'] + + format_number_str = "openerp.web.format_value(arguments[0], {type: 'float', digits: [69," + str(digits) + "]}, 0.00)" + if row['position'] == 'after': + return_str = "return " + format_number_str + " + '\\xA0" + symbol + "';" + else: + return_str = "return '" + symbol + "\\xA0' + " + format_number_str + ";" + function += "if (arguments[1] === " + str(row['id']) + ") { " + return_str + " }" + return function + class res_currency_rate(osv.osv): _name = "res.currency.rate" _description = "Currency Rate" @@ -285,4 +302,3 @@ class res_currency_rate(osv.osv): _order = "name desc" # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: -