diff --git a/addons/account/models/account.py b/addons/account/models/account.py index da2896af..5ec4c3de 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -391,7 +391,7 @@ class AccountJournal(models.Model): belongs_to_company = fields.Boolean('Belong to the user\'s current company', compute="_belong_to_company", search="_search_company_journals",) # Bank journals fields - bank_account_id = fields.Many2one('res.partner.bank', string="Bank Account", ondelete='restrict', copy=False, domain="[('partner_id','=', company_id)]") + bank_account_id = fields.Many2one('res.partner.bank', string="Bank Account", ondelete='restrict', copy=False) bank_statements_source = fields.Selection([('undefined', 'Undefined Yet'),('manual', 'Record Manually')], string='Bank Feeds', default='undefined') bank_acc_number = fields.Char(related='bank_account_id.acc_number') bank_id = fields.Many2one('res.bank', related='bank_account_id.bank_id') @@ -433,7 +433,7 @@ class AccountJournal(models.Model): for journal in self: if journal.sequence_id and journal.sequence_number_next: sequence = journal.sequence_id._get_current_sequence() - sequence.number_next = journal.sequence_number_next + sequence.sudo().number_next = journal.sequence_number_next @api.multi # do not depend on 'refund_sequence_id.date_range_ids', because @@ -512,9 +512,16 @@ class AccountJournal(models.Model): @api.multi def write(self, vals): for journal in self: + company = journal.company_id if ('company_id' in vals and journal.company_id.id != vals['company_id']): if self.env['account.move'].search([('journal_id', 'in', self.ids)], limit=1): raise UserError(_('This journal already contains items, therefore you cannot modify its company.')) + company = self.env['res.company'].browse(vals['company_id']) + if self.bank_account_id.company_id and self.bank_account_id.company_id != company: + self.bank_account_id.write({ + 'company_id': company.id, + 'partner_id': company.partner_id.id, + }) if ('code' in vals and journal.code != vals['code']): if self.env['account.move'].search([('journal_id', 'in', self.ids)], limit=1): raise UserError(_('This journal already contains items, therefore you cannot modify its short name.')) @@ -528,8 +535,16 @@ class AccountJournal(models.Model): self.default_debit_account_id.currency_id = vals['currency_id'] if not 'default_credit_account_id' in vals and self.default_credit_account_id: self.default_credit_account_id.currency_id = vals['currency_id'] - if 'bank_account_id' in vals and not vals.get('bank_account_id'): - raise UserError(_('You cannot empty the bank account once set.')) + if self.bank_account_id: + self.bank_account_id.currency_id = vals['currency_id'] + if 'bank_account_id' in vals: + if not vals.get('bank_account_id'): + raise UserError(_('You cannot empty the bank account once set.')) + else: + bank_account = self.env['res.partner.bank'].browse(vals['bank_account_id']) + if bank_account.partner_id != company.partner_id: + raise UserError(_("The partners of the journal's company and the related bank account mismatch.")) + result = super(AccountJournal, self).write(vals) # Create the bank_account_id if necessary diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index acd22989..a9ab1ab1 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -341,7 +341,7 @@ class AccountBankStatement(models.Model): def link_bank_to_partner(self): for statement in self: for st_line in statement.line_ids: - if st_line.bank_account_id and st_line.partner_id and st_line.bank_account_id.partner_id != st_line.partner_id: + if st_line.bank_account_id and st_line.partner_id and not st_line.bank_account_id.partner_id: st_line.bank_account_id.partner_id = st_line.partner_id @@ -938,7 +938,7 @@ class AccountBankStatementLine(models.Model): 'currency_id': currency.id, 'amount': abs(total), 'communication': self._get_communication(payment_methods[0] if payment_methods else False), - 'name': self.statement_id.name, + 'name': self.statement_id.name or _("Bank Statement %s") % self.date, }) # Complete dicts to create both counterpart move lines and write-offs diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 5da26b99..0d146d5e 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -221,7 +221,7 @@ class AccountInvoice(models.Model): @api.depends('move_id.line_ids.amount_residual') def _compute_payments(self): payment_lines = set() - for line in self.move_id.line_ids: + for line in self.move_id.line_ids.filtered(lambda l: l.account_id.id == self.account_id.id): payment_lines.update(line.mapped('matched_credit_ids.credit_move_id.id')) payment_lines.update(line.mapped('matched_debit_ids.debit_move_id.id')) self.payment_move_line_ids = self.env['account.move.line'].browse(list(payment_lines)) @@ -347,7 +347,7 @@ class AccountInvoice(models.Model): payment_move_line_ids = fields.Many2many('account.move.line', string='Payment Move Lines', compute='_compute_payments', store=True) user_id = fields.Many2one('res.users', string='Salesperson', track_visibility='onchange', readonly=True, states={'draft': [('readonly', False)]}, - default=lambda self: self.env.user) + default=lambda self: self.env.user, copy=False) fiscal_position_id = fields.Many2one('account.fiscal.position', string='Fiscal Position', oldname='fiscal_position', readonly=True, states={'draft': [('readonly', False)]}) commercial_partner_id = fields.Many2one('res.partner', string='Commercial Entity', compute_sudo=True, @@ -1195,25 +1195,7 @@ class AccountInvoice(models.Model): @api.model def line_get_convert(self, line, part): - return { - 'date_maturity': line.get('date_maturity', False), - 'partner_id': part, - 'name': line['name'], - 'debit': line['price'] > 0 and line['price'], - 'credit': line['price'] < 0 and -line['price'], - 'account_id': line['account_id'], - 'analytic_line_ids': line.get('analytic_line_ids', []), - 'amount_currency': line['price'] > 0 and abs(line.get('amount_currency', False)) or -abs(line.get('amount_currency', False)), - 'currency_id': line.get('currency_id', False), - 'quantity': line.get('quantity', 1.00), - 'product_id': line.get('product_id', False), - 'product_uom_id': line.get('uom_id', False), - 'analytic_account_id': line.get('account_analytic_id', False), - 'invoice_id': line.get('invoice_id', False), - 'tax_ids': line.get('tax_ids', False), - 'tax_line_id': line.get('tax_line_id', False), - 'analytic_tag_ids': line.get('analytic_tag_ids', False), - } + return self.env['product.product']._convert_prepared_anglosaxon_line(line, part) @api.multi def action_cancel(self): @@ -1343,6 +1325,7 @@ class AccountInvoice(models.Model): values['state'] = 'draft' values['number'] = False values['origin'] = invoice.number + values['payment_term_id'] = False values['refund_invoice_id'] = invoice.id if date: @@ -1735,6 +1718,7 @@ class AccountPaymentTerm(models.Model): def compute(self, value, date_ref=False): date_ref = date_ref or fields.Date.today() amount = value + sign = value < 0 and -1 or 1 result = [] if self.env.context.get('currency_id'): currency = self.env['res.currency'].browse(self.env.context['currency_id']) @@ -1742,7 +1726,7 @@ class AccountPaymentTerm(models.Model): currency = self.env.user.company_id.currency_id for line in self.line_ids: if line.value == 'fixed': - amt = currency.round(line.value_amount) + amt = sign * currency.round(line.value_amount) elif line.value == 'percent': amt = currency.round(value * (line.value_amount / 100.0)) elif line.value == 'balance': diff --git a/addons/account/models/account_journal_dashboard.py b/addons/account/models/account_journal_dashboard.py index ebad4b50..600c162d 100644 --- a/addons/account/models/account_journal_dashboard.py +++ b/addons/account/models/account_journal_dashboard.py @@ -224,7 +224,7 @@ class account_journal(models.Model): data as its first element, and the arguments dictionary to use to run it as its second. """ - return ("""SELECT state, amount_total, currency_id AS currency + return ("""SELECT state, amount_total, currency_id AS currency, type FROM account_invoice WHERE journal_id = %(journal_id)s AND state = 'open';""", {'journal_id':self.id}) @@ -234,7 +234,7 @@ class account_journal(models.Model): gather the bills in draft state data, and the arguments dictionary to use to run it as its second. """ - return ("""SELECT state, amount_total, currency_id AS currency + return ("""SELECT state, amount_total, currency_id AS currency, type FROM account_invoice WHERE journal_id = %(journal_id)s AND state = 'draft';""", {'journal_id':self.id}) @@ -247,7 +247,9 @@ class account_journal(models.Model): for result in results_dict: cur = self.env['res.currency'].browse(result.get('currency')) rslt_count += 1 - rslt_sum += cur.compute(result.get('amount_total'), target_currency) + + type_factor = result.get('type') in ('in_refund', 'out_refund') and -1 or 1 + rslt_sum += type_factor * cur.compute(result.get('amount_total'), target_currency) return (rslt_count, rslt_sum) @api.multi @@ -352,6 +354,8 @@ class account_journal(models.Model): }) [action] = self.env.ref('account.%s' % action_name).read() + if not self.env.context.get('use_domain'): + ctx['search_default_journal_id'] = self.id action['context'] = ctx action['domain'] = self._context.get('use_domain', []) account_invoice_filter = self.env.ref('account.view_account_invoice_filter', False) diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index f2f618d6..6779ba18 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -107,7 +107,7 @@ class AccountMove(models.Model): default=lambda self: self.env.user.company_id) matched_percentage = fields.Float('Percentage Matched', compute='_compute_matched_percentage', digits=0, store=True, readonly=True, help="Technical field used in cash basis method") # Dummy Account field to search on account.move by account_id - dummy_account_id = fields.Many2one('account.account', related='line_ids.account_id', string='Account', store=False) + dummy_account_id = fields.Many2one('account.account', related='line_ids.account_id', string='Account', store=False, readonly=True) tax_cash_basis_rec_id = fields.Many2one( 'account.partial.reconcile', string='Tax Cash Basis Entry of', @@ -291,7 +291,7 @@ class AccountMoveLine(models.Model): if not cr.fetchone(): cr.execute('CREATE INDEX account_move_line_partner_id_ref_idx ON account_move_line (partner_id, ref)') - @api.depends('debit', 'credit', 'amount_currency', 'currency_id', 'matched_debit_ids', 'matched_credit_ids', 'matched_debit_ids.amount', 'matched_credit_ids.amount', 'account_id.currency_id', 'move_id.state') + @api.depends('debit', 'credit', 'amount_currency', 'currency_id', 'matched_debit_ids', 'matched_credit_ids', 'matched_debit_ids.amount', 'matched_credit_ids.amount', 'move_id.state') def _amount_residual(self): """ Computes the residual amount of a move line from a reconciliable account in the company currency and the line's currency. This amount will be 0 for fully reconciled lines or lines from a non-reconciliable account, the original line amount @@ -525,7 +525,7 @@ class AccountMoveLine(models.Model): raise ValidationError(_("You cannot create journal items with a secondary currency without filling both 'currency' and 'amount currency' field.")) @api.multi - @api.constrains('amount_currency') + @api.constrains('amount_currency', 'debit', 'credit') def _check_currency_amount(self): for line in self: if line.amount_currency: @@ -936,7 +936,9 @@ class AccountMoveLine(models.Model): def _get_pair_to_reconcile(self): #field is either 'amount_residual' or 'amount_residual_currency' (if the reconciled account has a secondary currency set) - field = self[0].account_id.currency_id and 'amount_residual_currency' or 'amount_residual' + company_currency_id = self[0].account_id.company_id.currency_id + account_curreny_id = self[0].account_id.currency_id + field = (account_curreny_id and company_currency_id != account_curreny_id) and 'amount_residual_currency' or 'amount_residual' #reconciliation on bank accounts are special cases as we don't want to set them as reconciliable #but we still want to reconcile entries that are reversed together in order to clear those lines #in the bank reconciliation report. @@ -952,7 +954,7 @@ class AccountMoveLine(models.Model): elif self._context.get('skip_full_reconcile_check') == 'amount_currency_only': field = 'amount_residual_currency' #target the pair of move in self that are the oldest - sorted_moves = sorted(self, key=lambda a: a.date) + sorted_moves = sorted(self, key=lambda a: a.date_maturity or a.date) debit = credit = False for aml in sorted_moves: if credit and debit: @@ -974,8 +976,9 @@ class AccountMoveLine(models.Model): #there is no more pair to reconcile so return what move_line are left if not sm_credit_move or not sm_debit_move: return self - - field = self[0].account_id.currency_id and 'amount_residual_currency' or 'amount_residual' + company_currency_id = self[0].account_id.company_id.currency_id + account_curreny_id = self[0].account_id.currency_id + field = (account_curreny_id and company_currency_id != account_curreny_id) and 'amount_residual_currency' or 'amount_residual' if not sm_debit_move.debit and not sm_debit_move.credit: #both debit and credit field are 0, consider the amount_residual_currency field because it's an exchange difference entry field = 'amount_residual_currency' @@ -1198,6 +1201,11 @@ class AccountMoveLine(models.Model): account_move_line.payment_id.write({'invoice_ids': [(3, invoice.id, None)]}) rec_move_ids += account_move_line.matched_debit_ids rec_move_ids += account_move_line.matched_credit_ids + if self.env.context.get('invoice_id'): + current_invoice = self.env['account.invoice'].browse(self.env.context['invoice_id']) + rec_move_ids = rec_move_ids.filtered( + lambda r: (r.debit_move_id + r.credit_move_id) & current_invoice.move_id.line_ids + ) return rec_move_ids.unlink() #################################################### @@ -1361,7 +1369,7 @@ class AccountMoveLine(models.Model): record.payment_id.state = 'reconciled' result = super(AccountMoveLine, self).write(vals) - if self._context.get('check_move_validity', True): + if self._context.get('check_move_validity', True) and any(key in vals for key in ('account_id', 'journal_id', 'date', 'move_id', 'debit', 'credit')): move_ids = set() for line in self: if line.move_id.id not in move_ids: @@ -1381,7 +1389,7 @@ class AccountMoveLine(models.Model): raise UserError(_('You cannot do this modification on a reconciled entry. You can just change some non legal fields or you must unreconcile first.\n%s.') % err_msg) if line.move_id.id not in move_ids: move_ids.add(line.move_id.id) - self.env['account.move'].browse(list(move_ids))._check_lock_date() + self.env['account.move'].browse(list(move_ids))._check_lock_date() return True #################################################### diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index 989ee375..ae879cce 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -102,7 +102,7 @@ class account_abstract_payment(models.AbstractModel): class account_register_payments(models.TransientModel): _name = "account.register.payments" - _inherit = ['account.abstract.payment'] + _inherit = 'account.abstract.payment' _description = "Register payments on multiple invoices" invoice_ids = fields.Many2many('account.invoice', string='Invoices', copy=False) @@ -332,7 +332,7 @@ class account_payment(models.Model): def _compute_journal_domain_and_types(self): journal_type = ['bank', 'cash'] domain = [] - if self.currency_id.is_zero(self.amount): + if self.currency_id.is_zero(self.amount) and self.has_invoices: # In case of payment with 0 amount, allow to select a journal of type 'general' like # 'Miscellaneous Operations' and set this journal by default. journal_type = ['general'] @@ -340,7 +340,7 @@ class account_payment(models.Model): else: if self.payment_type == 'inbound': domain.append(('at_least_one_inbound', '=', True)) - else: + elif self.payment_type == 'outbound': domain.append(('at_least_one_outbound', '=', True)) return {'domain': domain, 'journal_types': set(journal_type)} @@ -349,9 +349,16 @@ class account_payment(models.Model): jrnl_filters = self._compute_journal_domain_and_types() journal_types = jrnl_filters['journal_types'] domain_on_types = [('type', 'in', list(journal_types))] - if self.journal_id.type not in journal_types: - self.journal_id = self.env['account.journal'].search(domain_on_types, limit=1) - return {'domain': {'journal_id': jrnl_filters['domain'] + domain_on_types}} + + journal_domain = jrnl_filters['domain'] + domain_on_types + default_journal_id = self.env.context.get('default_journal_id') + if not default_journal_id: + if self.journal_id.type not in journal_types: + self.journal_id = self.env['account.journal'].search(domain_on_types, limit=1) + else: + journal_domain = journal_domain.append(('id', '=', default_journal_id)) + + return {'domain': {'journal_id': journal_domain}} @api.one @api.depends('invoice_ids', 'payment_type', 'partner_type', 'partner_id') @@ -382,6 +389,8 @@ class account_payment(models.Model): self.partner_type = 'customer' elif self.payment_type == 'outbound': self.partner_type = 'supplier' + else: + self.partner_type = False # Set payment method domain res = self._onchange_journal() if not res.get('domain', {}): @@ -522,7 +531,7 @@ class account_payment(models.Model): if any(len(record.invoice_ids) != 1 for record in self): # For multiple invoices, there is account.register.payments wizard raise UserError(_("This method should only be called to process a single invoice's payment.")) - self.post(); + return self.post() def _create_payment_entry(self, amount): """ Create a journal entry corresponding to a payment, if the payment references invoice(s) they are reconciled. @@ -571,9 +580,9 @@ class account_payment(models.Model): writeoff_line['amount_currency'] = amount_currency_wo writeoff_line['currency_id'] = currency_id writeoff_line = aml_obj.create(writeoff_line) - if counterpart_aml['debit'] or writeoff_line['credit']: + if counterpart_aml['debit'] or (writeoff_line['credit'] and not counterpart_aml['credit']): counterpart_aml['debit'] += credit_wo - debit_wo - if counterpart_aml['credit'] or writeoff_line['debit']: + if counterpart_aml['credit'] or (writeoff_line['debit'] and not counterpart_aml['debit']): counterpart_aml['credit'] += debit_wo - credit_wo counterpart_aml['amount_currency'] -= amount_currency_wo diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py index f1e3bc07..94a6cfb3 100644 --- a/addons/account/models/chart_template.py +++ b/addons/account/models/chart_template.py @@ -745,6 +745,9 @@ class WizardMultiChartsAccounts(models.TransientModel): res.setdefault('domain', {}) res['domain']['sale_tax_id'] = repr(sale_tax_domain) res['domain']['purchase_tax_id'] = repr(purchase_tax_domain) + else: + self.sale_tax_id = False + self.purchase_tax_id = False if self.chart_template_id.transfer_account_id: self.transfer_account_id = self.chart_template_id.transfer_account_id.id if self.chart_template_id.code_digits: @@ -775,7 +778,7 @@ class WizardMultiChartsAccounts(models.TransientModel): if company_id: company = self.env['res.company'].browse(company_id) currency_id = company.on_change_country(company.country_id.id)['value']['currency_id'] - res.update({'currency_id': currency_id.id}) + res.update({'currency_id': currency_id}) chart_templates = account_chart_template.search([('visible', '=', True)]) if chart_templates: diff --git a/addons/account/models/company.py b/addons/account/models/company.py index 64be319d..d9b9fe62 100644 --- a/addons/account/models/company.py +++ b/addons/account/models/company.py @@ -136,6 +136,12 @@ Best Regards,''')) company.reflect_code_prefix_change(company.cash_account_code_prefix, new_cash_code, digits) if values.get('accounts_code_digits'): company.reflect_code_digits_change(digits) + + #forbid the change of currency_id if there are already some accounting entries existing + if 'currency_id' in values and values['currency_id'] != company.currency_id.id: + if self.env['account.move.line'].search([('company_id', '=', company.id)]): + raise UserError(_('You cannot change the currency of the company since some journal items already exist')) + return super(ResCompany, self).write(values) @api.model @@ -271,7 +277,7 @@ Best Regards,''')) default_journal = self.env['account.journal'].search([('type', '=', 'general'), ('company_id', '=', self.id)], limit=1) if not default_journal: - raise UserError(_("No miscellaneous journal could be found. Please create one before proceeding.")) + raise UserError(_("Please install a chart of accounts or create a miscellaneous journal before proceeding.")) self.account_opening_move_id = self.env['account.move'].create({ 'name': _('Opening Journal Entry'), diff --git a/addons/account/models/product.py b/addons/account/models/product.py index b5556558..a5a6afb5 100644 --- a/addons/account/models/product.py +++ b/addons/account/models/product.py @@ -71,3 +71,28 @@ class ProductTemplate(models.Model): if not fiscal_pos: fiscal_pos = self.env['account.fiscal.position'] return fiscal_pos.map_accounts(accounts) + +class ProductProduct(models.Model): + _inherit = "product.product" + + @api.model + def _convert_prepared_anglosaxon_line(self, line, partner): + return { + 'date_maturity': line.get('date_maturity', False), + 'partner_id': partner, + 'name': line['name'], + 'debit': line['price'] > 0 and line['price'], + 'credit': line['price'] < 0 and -line['price'], + 'account_id': line['account_id'], + 'analytic_line_ids': line.get('analytic_line_ids', []), + 'amount_currency': line['price'] > 0 and abs(line.get('amount_currency', False)) or -abs(line.get('amount_currency', False)), + 'currency_id': line.get('currency_id', False), + 'quantity': line.get('quantity', 1.00), + 'product_id': line.get('product_id', False), + 'product_uom_id': line.get('uom_id', False), + 'analytic_account_id': line.get('account_analytic_id', False), + 'invoice_id': line.get('invoice_id', False), + 'tax_ids': line.get('tax_ids', False), + 'tax_line_id': line.get('tax_line_id', False), + 'analytic_tag_ids': line.get('analytic_tag_ids', False), + } diff --git a/addons/account/report/account_aged_partner_balance.py b/addons/account/report/account_aged_partner_balance.py index 3087c502..d7ce4aac 100644 --- a/addons/account/report/account_aged_partner_balance.py +++ b/addons/account/report/account_aged_partner_balance.py @@ -27,7 +27,7 @@ class ReportAgedPartnerBalance(models.AbstractModel): res = [] total = [] cr = self.env.cr - user_company = self.env.user.company_id.id + company_ids = self.env.context.get('company_ids', (self.env.user.company_id.id,)) move_state = ['draft', 'posted'] branch = '' if branch_id: @@ -44,7 +44,7 @@ class ReportAgedPartnerBalance(models.AbstractModel): if reconciled_after_date: reconciliation_clause = '(l.reconciled IS FALSE OR l.id IN %s)' arg_list += (tuple(reconciled_after_date),) - arg_list += (date_from, user_company) + arg_list += (date_from, tuple(company_ids)) query = ''' SELECT DISTINCT l.partner_id, UPPER(res_partner.name) FROM account_move_line AS l left join res_partner on l.partner_id = res_partner.id, account_account, account_move am @@ -54,7 +54,7 @@ class ReportAgedPartnerBalance(models.AbstractModel): AND (account_account.internal_type IN %s) AND ''' + reconciliation_clause + branch +''' AND (l.date <= %s) - AND l.company_id = %s + AND l.company_id IN %s ORDER BY UPPER(res_partner.name)''' cr.execute(query, arg_list) @@ -79,8 +79,8 @@ class ReportAgedPartnerBalance(models.AbstractModel): AND (COALESCE(l.date_maturity,l.date) > %s)\ AND ((l.partner_id IN %s) OR (l.partner_id IS NULL)) AND (l.date <= %s) ''' + branch + ''' - AND l.company_id = %s''' - cr.execute(query, (tuple(move_state), tuple(account_type), date_from, tuple(partner_ids), date_from, user_company)) + AND l.company_id IN %s''' + cr.execute(query, (tuple(move_state), tuple(account_type), date_from, tuple(partner_ids), date_from, tuple(company_ids))) aml_ids = cr.fetchall() aml_ids = aml_ids and [x[0] for x in aml_ids] or [] for line in self.env['account.move.line'].browse(aml_ids): @@ -120,7 +120,7 @@ class ReportAgedPartnerBalance(models.AbstractModel): else: dates_query += ' <= %s)' args_list += (periods[str(i)]['stop'],) - args_list += (date_from, user_company) + args_list += (date_from, tuple(company_ids)) query = '''SELECT l.id FROM account_move_line AS l, account_account, account_move am @@ -130,7 +130,7 @@ class ReportAgedPartnerBalance(models.AbstractModel): AND ((l.partner_id IN %s) OR (l.partner_id IS NULL)) AND ''' + dates_query + branch +''' AND (l.date <= %s) - AND l.company_id = %s''' + AND l.company_id IN %s''' cr.execute(query, args_list) partners_amount = {} aml_ids = cr.fetchall() @@ -193,7 +193,7 @@ class ReportAgedPartnerBalance(models.AbstractModel): values['name'] = _('Unknown Partner') values['trust'] = False - if at_least_one_amount: + if at_least_one_amount or self._context.get('include_nullified_amount'): res.append(values) return res, total, lines diff --git a/addons/account/report/account_partner_ledger.py b/addons/account/report/account_partner_ledger.py index de33ee50..514dc88e 100644 --- a/addons/account/report/account_partner_ledger.py +++ b/addons/account/report/account_partner_ledger.py @@ -14,7 +14,7 @@ class ReportPartnerLedger(models.AbstractModel): full_account = [] currency = self.env['res.currency'] query_get_data = self.env['account.move.line'].with_context(data['form'].get('used_context', {}))._query_get() - reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".reconciled = false ' + reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".full_reconcile_id IS NULL ' params = [partner.id, tuple(data['computed']['move_state']), tuple(data['computed']['account_ids'])] + query_get_data[2] query = """ SELECT "account_move_line".id, "account_move_line".date, j.code, acc.code as a_code, acc.name as a_name, "account_move_line".ref, m.name as move_name, "account_move_line".name, "account_move_line".debit, "account_move_line".credit, "account_move_line".amount_currency,"account_move_line".currency_id, c.symbol AS currency_code @@ -51,7 +51,7 @@ class ReportPartnerLedger(models.AbstractModel): return result = 0.0 query_get_data = self.env['account.move.line'].with_context(data['form'].get('used_context', {}))._query_get() - reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".reconciled = false ' + reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".full_reconcile_id IS NULL ' params = [partner.id, tuple(data['computed']['move_state']), tuple(data['computed']['account_ids'])] + query_get_data[2] query = """SELECT sum(""" + field + """) @@ -95,7 +95,7 @@ class ReportPartnerLedger(models.AbstractModel): AND NOT a.deprecated""", (tuple(data['computed']['ACCOUNT_TYPE']),)) data['computed']['account_ids'] = [a for (a,) in self.env.cr.fetchall()] params = [tuple(data['computed']['move_state']), tuple(data['computed']['account_ids'])] + query_get_data[2] - reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".reconciled = false ' + reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".full_reconcile_id IS NULL ' query = """ SELECT DISTINCT "account_move_line".partner_id FROM """ + query_get_data[0] + """, account_account AS account, account_move AS am diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js index 8c6595a4..c7fd2779 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_model.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -524,17 +524,37 @@ var StatementModel = BasicModel.extend({ */ togglePartialReconcile: function (handle) { var line = this.getLine(handle); - var props = _.filter(line.reconciliation_proposition, {'invalid': false}); - var prop = props[0]; - if (props.length !== 1 || Math.abs(line.st_line.amount) >= Math.abs(prop.amount)) { + + // Retrieve the toggle proposition + var selected; + _.each(line.reconciliation_proposition, function (prop) { + if (!prop.invalid) { + if (((line.balance.amount < 0 || !line.partial_reconcile) && prop.amount > 0 && line.st_line.amount > 0 && line.st_line.amount < prop.amount) || + ((line.balance.amount > 0 || !line.partial_reconcile) && prop.amount < 0 && line.st_line.amount < 0 && line.st_line.amount > prop.amount)) { + selected = prop; + return false; + } + } + }); + + // If no toggled proposition found, reject it + if (selected == null) return $.Deferred().reject(); - } - prop.partial_reconcile = !prop.partial_reconcile; - if (!prop.partial_reconcile) { + + // Inverse partial_reconcile value + selected.partial_reconcile = !selected.partial_reconcile; + if (!selected.partial_reconcile) { return this._computeLine(line); } + + // Compute the write_off + var format_options = { currency_id: line.st_line.currency_id }; + selected.write_off_amount = selected.amount + line.balance.amount; + selected.write_off_amount_str = field_utils.format.monetary(Math.abs(selected.write_off_amount), {}, format_options); + selected.write_off_amount_str = selected.write_off_amount_str.replace(' ', ' '); + return this._computeLine(line).then(function () { - if (prop.partial_reconcile) { + if (selected.partial_reconcile) { line.balance.amount = 0; line.balance.type = 1; line.mode = 'inactive'; @@ -594,7 +614,7 @@ var StatementModel = BasicModel.extend({ handles = [handle]; } else { _.each(this.lines, function (line, handle) { - if (!line.reconciled && !line.balance.amount && line.reconciliation_proposition.length) { + if (!line.reconciled && line.balance && !line.balance.amount && line.reconciliation_proposition.length) { handles.push(handle); } }); @@ -1051,7 +1071,7 @@ var StatementModel = BasicModel.extend({ // Do not forward port in master. @CSN will change this var amount = prop.computed_with_tax && -prop.base_amount || -prop.amount; if (prop.partial_reconcile === true) { - amount = -line.st_line.amount; + amount = -prop.write_off_amount; } var result = { name : prop.label, diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js index 9b21da9b..58ee890f 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js @@ -354,7 +354,21 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { // reconciliation_proposition var $props = this.$('.accounting_view tbody').empty(); - var props = _.filter(state.reconciliation_proposition, {'display': true}); + + // loop state propositions + var props = []; + var nb_debit_props = 0; + var nb_credit_props = 0; + _.each(state.reconciliation_proposition, function (prop) { + if (prop.display) { + props.push(prop); + if (prop.amount < 0) + nb_debit_props += 1; + else if (prop.amount > 0) + nb_credit_props += 1; + } + }); + _.each(props, function (line) { var $line = $(qweb.render("reconciliation.line.mv_line", {'line': line, 'state': state})); if (!isNaN(line.id)) { @@ -362,18 +376,16 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { .appendTo($line.find('.cell_info_popover')) .attr("data-content", qweb.render('reconciliation.line.mv_line.details', {'line': line})); } - - if ((state.balance.amount_currency !== 0 || line.partial_reconcile) && props.length === 1 && - line.already_paid === false && - ( - (state.st_line.amount > 0 && state.st_line.amount < props[0].amount) || - (state.st_line.amount < 0 && state.st_line.amount > props[0].amount)) - ) { + if (line.already_paid === false && + ((state.balance.amount_currency < 0 || line.partial_reconcile) && nb_credit_props == 1 + && line.amount > 0 && state.st_line.amount > 0 && state.st_line.amount < line.amount) || + ((state.balance.amount_currency > 0 || line.partial_reconcile) && nb_debit_props == 1 + && line.amount < 0 && state.st_line.amount < 0 && state.st_line.amount > line.amount)) { var $cell = $line.find(line.amount > 0 ? '.cell_right' : '.cell_left'); var text; if (line.partial_reconcile) { text = _t("Undo the partial reconciliation."); - $cell.text(state.st_line.amount_str); + $cell.text(line.write_off_amount_str); } else { text = _t("This move's amount is higher than the transaction's amount. Click to register a partial payment and keep the payment balance open."); } @@ -415,14 +427,15 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { this._renderCreate(state); } var data = this.model.get(this.handleCreateRecord).data; - this.model.notifyChanges(this.handleCreateRecord, state.createForm); - var record = this.model.get(this.handleCreateRecord); - _.each(this.fields, function (field, fieldName) { - if (self._avoidFieldUpdate[fieldName]) return; - if (fieldName === "partner_id") return; - if ((data[fieldName] || state.createForm[fieldName]) && !_.isEqual(state.createForm[fieldName], data[fieldName])) { - field.reset(record); - } + this.model.notifyChanges(this.handleCreateRecord, state.createForm).then(function () { + var record = self.model.get(self.handleCreateRecord); + _.each(self.fields, function (field, fieldName) { + if (self._avoidFieldUpdate[fieldName]) return; + if (fieldName === "partner_id") return; + if ((data[fieldName] || state.createForm[fieldName]) && !_.isEqual(state.createForm[fieldName], data[fieldName])) { + field.reset(record); + } + }); }); } this.$('.create .add_line').toggle(!!state.balance.amount_currency); @@ -626,7 +639,8 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { return; } if(event.keyCode === 13) { - if (_.findWhere(this.model.lines, {mode: 'create'}).balance.amount) { + var created_lines = _.findWhere(this.model.lines, {mode: 'create'}); + if (created_lines && created_lines.balance.amount) { this._onCreateProposition(); } return; diff --git a/addons/account/static/tests/reconciliation_tests.js b/addons/account/static/tests/reconciliation_tests.js index ddf2c57d..0cbb4f9a 100644 --- a/addons/account/static/tests/reconciliation_tests.js +++ b/addons/account/static/tests/reconciliation_tests.js @@ -34,17 +34,22 @@ var db = { fields: { id: {string: "ID", type: 'integer'}, code: {string: "code", type: 'integer'}, - display_name: {string: "Displayed name", type: 'char'}, + name: {string: "Displayed name", type: 'char'}, }, records: [ - {id: 282, code: 100000, display_name: "100000 Fixed Asset Account"}, - {id: 283, code: 101000, display_name: "101000 Current Assets"}, - {id: 284, code: 101110, display_name: "101110 Stock Valuation Account"}, - {id: 285, code: 101120, display_name: "101120 Stock Interim Account (Received)"}, - {id: 286, code: 101130, display_name: "101130 Stock Interim Account (Delivered)"}, - {id: 287, code: 101200, display_name: "101200 Account Receivable"}, - {id: 288, code: 101300, display_name: "101300 Tax Paid"}, - {id: 308, code: 101401, display_name: "101401 Bank"}, + {id: 282, code: 100000, name: "100000 Fixed Asset Account"}, + {id: 283, code: 101000, name: "101000 Current Assets"}, + {id: 284, code: 101110, name: "101110 Stock Valuation Account"}, + {id: 285, code: 101120, name: "101120 Stock Interim Account (Received)"}, + {id: 286, code: 101130, name: "101130 Stock Interim Account (Delivered)"}, + {id: 287, code: 101200, name: "101200 Account Receivable"}, + {id: 288, code: 101300, name: "101300 Tax Paid"}, + {id: 308, code: 101401, name: "101401 Bank"}, + {id: 500, code: 500, name: "500 Account"}, + {id: 501, code: 501, name: "501 Account"}, + {id: 502, code: 502, name: "502 Account"}, + {id: 503, code: 503, name: "503 Account"}, + {id: 504, code: 504, name: "504 Account"}, ], mark_as_reconciled: function () { return $.when(); @@ -767,13 +772,7 @@ QUnit.module('account', { partner_id: false, counterpart_aml_dicts:[], payment_aml_ids: [392], - new_aml_dicts: [ - { - "credit": 343.42, - "debit": 0, - "name": "Bank fees : Open balance" - } - ], + new_aml_dicts: [], }] ], "should call process_reconciliations with partial reconcile values"); } @@ -802,14 +801,14 @@ QUnit.module('account', { assert.notOk( widget.$('.cell_left .line_info_button').length, "should not display the partial reconciliation alert"); widget.$('.accounting_view thead td:first').trigger('click'); widget.$('.match .cell_account_code:first').trigger('click'); - assert.equal( widget.$('.accounting_view tbody .cell_left .line_info_button').length, 0, "should not display the partial reconciliation alert"); + assert.equal( widget.$('.accounting_view tbody .cell_left .line_info_button').length, 1, "should display the partial reconciliation alert"); assert.ok( widget.$('button.btn-primary:not(hidden)').length, "should not display the reconcile button"); assert.ok( widget.$('.text-danger:not(hidden)').length, "should display counterpart alert"); widget.$('.accounting_view .cell_left .line_info_button').trigger('click'); - assert.strictEqual(widget.$('.accounting_view .cell_left .line_info_button').length, 0, "should not display a partial reconciliation alert"); - assert.notOk(widget.$('.accounting_view .cell_left .line_info_button').hasClass('do_partial_reconcile_false'), "should not display the partial reconciliation information"); + assert.strictEqual(widget.$('.accounting_view .cell_left .line_info_button').length, 1, "should display a partial reconciliation alert"); + assert.notOk(widget.$('.accounting_view .cell_left .line_info_button').hasClass('do_partial_reconcile_true'), "should display the partial reconciliation information"); assert.ok( widget.$('button.btn-default:not(hidden)').length, "should display the validate button"); - assert.strictEqual( widget.$el.data('mode'), "match", "should be inactive mode"); + assert.strictEqual( widget.$el.data('mode'), "inactive", "should be inactive mode"); widget.$('button.btn-default:not(hidden)').trigger('click'); clientAction.destroy(); @@ -1027,13 +1026,77 @@ QUnit.module('account', { clientAction.destroy(); }); + QUnit.test('Reconciliation create line (many2one test)', function (assert) { + assert.expect(5); + + var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options); + var def = $.Deferred(); + + testUtils.addMockEnvironment(clientAction, { + data: this.params.data, + session: { + currencies: { + 3: { + digits: [69, 2], + position: "before", + symbol: "$" + } + } + }, + archs: { + "account.account,false,list": '', + "account.account,false,search": '', + }, + mockRPC: function (route, args) { + if (args.method === 'name_get') { + return def.then(this._super.bind(this, route, args)); + } + return this._super(route, args); + }, + }); + + clientAction.prependTo($('#qunit-fixture')); + + var widget = clientAction.widgets[0]; + + // open the first line in write-off mode + widget.$('.accounting_view tfoot td:first').trigger('click'); + + // select an account with the many2one (drop down) + widget.$('.create .create_account_id input').trigger('click'); + $('.ui-autocomplete .ui-menu-item a:contains(101200)').trigger('mouseenter').trigger('click'); + assert.strictEqual(widget.$('.create .create_account_id input').val(), "101200 Account Receivable", "Display the selected account"); + assert.strictEqual(widget.$('tbody:first .cell_account_code').text(), "101200", "Display the code of the selected account"); + + // use the many2one select dialog to change the account + widget.$('.create .create_account_id input').trigger('click'); + $('.ui-autocomplete .ui-menu-item a:contains(Search)').trigger('mouseenter').trigger('click'); + // select the account who does not appear in the drop drown + $('.modal tr.o_data_row:contains(502)').click(); + assert.strictEqual(widget.$('.create .create_account_id input').val(), "101200 Account Receivable", "Selected account does not change"); + // wait the name_get to render the changes + def.resolve(); + assert.strictEqual(widget.$('.create .create_account_id input').val(), "502 Account", "Display the selected account"); + assert.strictEqual(widget.$('tbody:first .cell_account_code').text(), "502", "Display the code of the selected account"); + clientAction.destroy(); + }); + QUnit.test('Reconciliation create line with taxes', function (assert) { assert.expect(13); var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options); testUtils.addMockEnvironment(clientAction, { - 'data': this.params.data, + data: this.params.data, + session: { + currencies: { + 3: { + digits: [69, 2], + position: "before", + symbol: "$" + } + } + }, }); clientAction.appendTo($('#qunit-fixture')); @@ -1045,26 +1108,26 @@ QUnit.module('account', { widget.$('.create .create_label input').val('test1').trigger('input'); widget.$('.create .create_amount input').val('1100').trigger('input'); - assert.strictEqual(widget.$('.accounting_view tbody .cell_right:last').text(), "1100.00", "should display the value 1100.00 in left column"); + assert.strictEqual(widget.$('.accounting_view tbody .cell_right:last').text(), "$\u00a01100.00", "should display the value 1100.00 in left column"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open Balance'"); - assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "75.00", "should display 'Open Balance' with 75.00 in right column"); + assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "$\u00a075.00", "should display 'Open Balance' with 75.00 in right column"); assert.strictEqual(widget.$('.accounting_view tbody tr').length, 1, "should have 1 created reconcile lines"); widget.$('.create .create_tax_id input').trigger('click'); $('.ui-autocomplete .ui-menu-item a:contains(10.00%)').trigger('mouseenter').trigger('click'); - assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().replace('$_', ''), "1000.00100.00", "should have 2 created reconcile lines with right column values"); + assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().replace('$_', ''), "$\u00a01000.00$\u00a0100.00", "should have 2 created reconcile lines with right column values"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open Balance'"); - assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "75.00", "should display 'Open Balance' with 75.00 in right column"); + assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "$\u00a075.00", "should display 'Open Balance' with 75.00 in right column"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "", "should display 'Open Balance' without any value in left column"); assert.strictEqual(widget.$('.accounting_view tbody tr').length, 2, "should have 2 created reconcile lines"); widget.$('.create .create_tax_id input').trigger('click'); $('.ui-autocomplete .ui-menu-item a:contains(20.00%)').trigger('mouseenter').trigger('click'); - assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().replace('$_', ''), "1100.00220.00", "should have 2 created reconcile lines with right column values"); + assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().replace('$_', ''), "$\u00a01100.00$\u00a0220.00", "should have 2 created reconcile lines with right column values"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Create Write-off", "should display 'Create Write-off'"); - assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "145.00", "should display 'Create Write-off' with 145.00 in right column"); + assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "$\u00a0145.00", "should display 'Create Write-off' with 145.00 in right column"); assert.strictEqual(widget.$('.accounting_view tbody tr').length, 2, "should have 2 created reconcile lines"); clientAction.destroy(); @@ -1076,7 +1139,7 @@ QUnit.module('account', { var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options); testUtils.addMockEnvironment(clientAction, { - 'data': this.params.data, + data: this.params.data, }); clientAction.appendTo($('#qunit-fixture')); diff --git a/addons/account/tests/test_payment.py b/addons/account/tests/test_payment.py index a7c5a528..6abc23ae 100644 --- a/addons/account/tests/test_payment.py +++ b/addons/account/tests/test_payment.py @@ -17,7 +17,9 @@ class TestPayment(AccountingTestCase): self.currency_chf_id = self.env.ref("base.CHF").id self.currency_usd_id = self.env.ref("base.USD").id self.currency_eur_id = self.env.ref("base.EUR").id - self.env.ref('base.main_company').write({'currency_id': self.currency_eur_id}) + + company = self.env.ref('base.main_company') + self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", [self.currency_eur_id, company.id]) self.product = self.env.ref("product.product_product_4") self.payment_method_manual_in = self.env.ref("account.account_payment_method_manual_in") self.payment_method_manual_out = self.env.ref("account.account_payment_method_manual_out") @@ -318,3 +320,53 @@ class TestPayment(AccountingTestCase): self.assertEqual(payment_id.payment_type, 'outbound') self.assertEqual(payment_id.partner_id, self.partner_china_exp) self.assertEqual(payment_id.partner_type, 'supplier') + + def test_payment_and_writeoff_in_other_currency(self): + # Use case: + # Company is in EUR, create a customer invoice for 25 EUR and register payment of 25 USD. + # Mark invoice as fully paid with a write_off + # Check that all the aml are correctly created. + invoice = self.create_invoice(amount=25, type='out_invoice', currency_id=self.currency_eur_id, partner=self.partner_agrolait.id) + # register payment on invoice + payment = self.payment_model.create({'payment_type': 'inbound', + 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id, + 'partner_type': 'customer', + 'partner_id': self.partner_agrolait.id, + 'amount': 25, + 'currency_id': self.currency_usd_id, + 'payment_date': time.strftime('%Y') + '-07-15', + 'payment_difference_handling': 'reconcile', + 'writeoff_account_id': self.account_payable.id, + 'journal_id': self.bank_journal_euro.id, + 'invoice_ids': [(4, invoice.id, None)] + }) + payment.post() + self.check_journal_items(payment.move_line_ids, [ + {'account_id': self.account_eur.id, 'debit': 16.35, 'credit': 0.0, 'amount_currency': 25.0, 'currency_id': self.currency_usd_id}, + {'account_id': self.account_payable.id, 'debit': 8.65, 'credit': 0.0, 'amount_currency': 13.22, 'currency_id': self.currency_usd_id}, + {'account_id': self.account_receivable.id, 'debit': 0.0, 'credit': 25.0, 'amount_currency': -38.22, 'currency_id': self.currency_usd_id}, + ]) + # Use case: + # Company is in EUR, create a vendor bill for 25 EUR and register payment of 25 USD. + # Mark invoice as fully paid with a write_off + # Check that all the aml are correctly created. + invoice = self.create_invoice(amount=25, type='in_invoice', currency_id=self.currency_eur_id, partner=self.partner_agrolait.id) + # register payment on invoice + payment = self.payment_model.create({'payment_type': 'inbound', + 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id, + 'partner_type': 'supplier', + 'partner_id': self.partner_agrolait.id, + 'amount': 25, + 'currency_id': self.currency_usd_id, + 'payment_date': time.strftime('%Y') + '-07-15', + 'payment_difference_handling': 'reconcile', + 'writeoff_account_id': self.account_payable.id, + 'journal_id': self.bank_journal_euro.id, + 'invoice_ids': [(4, invoice.id, None)] + }) + payment.post() + self.check_journal_items(payment.move_line_ids, [ + {'account_id': self.account_eur.id, 'debit': 16.35, 'credit': 0.0, 'amount_currency': 25.0, 'currency_id': self.currency_usd_id}, + {'account_id': self.account_payable.id, 'debit': 0.0, 'credit': 8.65, 'amount_currency': -13.22, 'currency_id': self.currency_usd_id}, + {'account_id': self.account_receivable.id, 'debit': 0.0, 'credit': 7.7, 'amount_currency': -11.78, 'currency_id': self.currency_usd_id}, + ]) diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py index 7782f885..aeacd3e6 100644 --- a/addons/account/tests/test_reconciliation.py +++ b/addons/account/tests/test_reconciliation.py @@ -25,7 +25,8 @@ class TestReconciliation(AccountingTestCase): self.currency_swiss_id = self.env.ref("base.CHF").id self.currency_usd_id = self.env.ref("base.USD").id self.currency_euro_id = self.env.ref("base.EUR").id - self.env.ref('base.main_company').write({'currency_id': self.currency_euro_id}) + company = self.env.ref('base.main_company') + self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", [self.currency_euro_id, company.id]) self.account_rcv = partner_agrolait.property_account_receivable_id or self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_receivable').id)], limit=1) self.account_rsa = partner_agrolait.property_account_payable_id or self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_payable').id)], limit=1) self.product = self.env.ref("product.product_product_4") @@ -620,3 +621,39 @@ class TestReconciliation(AccountingTestCase): # Checking if the direction of the move is correct full_rec_payable = full_rec_move.line_ids.filtered(lambda l: l.account_id == self.account_rsa) self.assertEqual(full_rec_payable.balance, 18.75) + + def test_unreconcile(self): + # Use case: + # 2 invoices paid with a single payment. Unreconcile the payment with one invoice, the + # other invoice should remain reconciled. + inv1 = self.create_invoice(invoice_amount=10, currency_id=self.currency_usd_id) + inv2 = self.create_invoice(invoice_amount=20, currency_id=self.currency_usd_id) + payment = self.env['account.payment'].create({ + 'payment_type': 'inbound', + 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id, + 'partner_type': 'customer', + 'partner_id': self.partner_agrolait_id, + 'amount': 100, + 'currency_id': self.currency_usd_id, + 'journal_id': self.bank_journal_usd.id, + }) + payment.post() + credit_aml = payment.move_line_ids.filtered('credit') + + # Check residual before assignation + self.assertAlmostEquals(inv1.residual, 10) + self.assertAlmostEquals(inv2.residual, 20) + + # Assign credit and residual + inv1.assign_outstanding_credit(credit_aml.id) + inv2.assign_outstanding_credit(credit_aml.id) + self.assertAlmostEquals(inv1.residual, 0) + self.assertAlmostEquals(inv2.residual, 0) + + # Unreconcile one invoice at a time and check residual + credit_aml.with_context(invoice_id=inv1.id).remove_move_reconcile() + self.assertAlmostEquals(inv1.residual, 10) + self.assertAlmostEquals(inv2.residual, 0) + credit_aml.with_context(invoice_id=inv2.id).remove_move_reconcile() + self.assertAlmostEquals(inv1.residual, 10) + self.assertAlmostEquals(inv2.residual, 20) diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml index e1ff2f9b..ffeaf4e7 100644 --- a/addons/account/views/account_invoice_view.xml +++ b/addons/account/views/account_invoice_view.xml @@ -251,7 +251,7 @@ - + @@ -297,7 +297,7 @@ - + @@ -396,7 +396,7 @@ - +