[IMP] account,*: Improve bank statements/payments workflow

- Create journal entries as soon as bank/cash statement lines are created, temporary booked on a suspense account set on the journal.
- Simplify the management of "blue" lines in the reconciliation widget. A "blue" line is now a journal item using a temporary liquidity account (outstanding payment/receipt accounts, set on the journal).
- Adapt and simplify the bank reconciliation report.
- Remove the bank reconciliation threshold date. The reconciliation report will show the not already reconciled journal entries using a liquidity account and the not already reconciled journal entries using a temporary liquidity account. Without accounting, an account.payment will involve directly the liquidity account and then, will be considered as a statement line directly.
- Remove the post_at bank reconciliation feature. The "paid" state will be set on the invoices only if reconciled with a journal entry involving the journal's liquidity account.
    With invoicing, the payment will do that so the "in_payment" state should never be shown up.
    With accounting, only the statement lines have the power to move an invoice to the "paid" state.
- Fix various corner cases about the management of multi-currency in bank statement lines.
- Fix the conversion dates in multi-currency: Since the bank/cash is always used on the statement lines, it will use always the real "bank" date instead of the fictive payment one.
- Ensure the 'reconcile' method will raise an error if the involved moves are not posted.

related enterprise PR odoo/enterprise#7019

closes odoo/odoo#41301

--task: 2092096
Related: odoo/upgrade#1018
Signed-off-by: Quentin De Paoli (qdp) <qdp@openerp.com>
This commit is contained in:
Laurent Smet
2020-04-10 09:47:24 +00:00
committed by qdp-odoo
parent b4d1360ed1
commit caeb782841
90 changed files with 5621 additions and 3718 deletions
+1
View File
@@ -27,6 +27,7 @@ You could use this simplified accounting in case you work with an (external) acc
'wizard/account_unreconcile_view.xml',
'wizard/account_move_reversal_view.xml',
'wizard/account_resequence_views.xml',
'wizard/account_payment_register_views.xml',
'views/account_move_views.xml',
'wizard/setup_wizards_view.xml',
'wizard/pos_box.xml',
-52
View File
@@ -127,58 +127,6 @@
<field name="payment_type">outbound</field>
</record>
<!-- Payment sequences -->
<record id="sequence_payment_customer_invoice" model="ir.sequence">
<field name="name">Payments customer invoices sequence</field>
<field name="code">account.payment.customer.invoice</field>
<field name="prefix">CUST.IN/%(range_year)s/</field>
<field eval="1" name="number_next"/>
<field eval="1" name="number_increment"/>
<field eval="True" name="use_date_range"/>
<field eval="False" name="company_id"/>
<field name="padding">4</field>
</record>
<record id="sequence_payment_customer_refund" model="ir.sequence">
<field name="name">Payments customer credit notes sequence</field>
<field name="code">account.payment.customer.refund</field>
<field name="prefix">CUST.OUT/%(range_year)s/</field>
<field eval="1" name="number_next"/>
<field eval="1" name="number_increment"/>
<field eval="True" name="use_date_range"/>
<field eval="False" name="company_id"/>
<field name="padding">4</field>
</record>
<record id="sequence_payment_supplier_invoice" model="ir.sequence">
<field name="name">Payments supplier invoices sequence</field>
<field name="code">account.payment.supplier.invoice</field>
<field name="prefix">SUPP.OUT/%(range_year)s/</field>
<field eval="1" name="number_next"/>
<field eval="1" name="number_increment"/>
<field eval="True" name="use_date_range"/>
<field eval="False" name="company_id"/>
<field name="padding">4</field>
</record>
<record id="sequence_payment_supplier_refund" model="ir.sequence">
<field name="name">Payments supplier credit notes sequence</field>
<field name="code">account.payment.supplier.refund</field>
<field name="prefix">SUPP.IN/%(range_year)s/</field>
<field eval="1" name="number_next"/>
<field eval="1" name="number_increment"/>
<field eval="True" name="use_date_range"/>
<field eval="False" name="company_id"/>
<field name="padding">4</field>
</record>
<record id="sequence_payment_transfer" model="ir.sequence">
<field name="name">Payments transfer sequence</field>
<field name="code">account.payment.transfer</field>
<field name="prefix">TRANS/%(range_year)s/</field>
<field eval="1" name="number_next"/>
<field eval="1" name="number_increment"/>
<field eval="True" name="use_date_range"/>
<field eval="False" name="company_id"/>
<field name="padding">4</field>
</record>
<!-- Account Tax Group -->
<record id="tax_group_taxes" model="account.tax.group">
<field name="name">Taxes</field>
+1 -3
View File
@@ -4,11 +4,9 @@ from . import sequence_mixin
from . import partner
from . import account
from . import account_reconcile_model
from . import account_payment
from . import account_payment_term
from . import account_bank_statement
from . import account_move
from . import account_payment_term
from . import account_payment
from . import account_bank_statement
from . import chart_template
from . import account_analytic_line
+309 -84
View File
@@ -1,15 +1,12 @@
# -*- coding: utf-8 -*-
import time
import math
import re
import logging
from odoo.osv import expression
from odoo.tools.float_utils import float_round as round, float_compare
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
from odoo.exceptions import UserError, ValidationError
from odoo import api, fields, models, _, tools
from odoo.tests.common import Form
TYPE_TAX_USE = [
('sale', 'Sales'),
@@ -17,6 +14,8 @@ TYPE_TAX_USE = [
('none', 'None'),
]
_logger = logging.getLogger(__name__)
class AccountAccountType(models.Model):
_name = "account.account.type"
@@ -415,6 +414,43 @@ class AccountAccount(models.Model):
if ids:
raise ValidationError(_('Some journal items already exist with this account but in other journals than the allowed ones.'))
@api.constrains('currency_id')
def _check_journal_consistency(self):
''' Ensure the currency set on the journal is the same as the currency set on the
linked accounts.
'''
if not self:
return
self.env['account.account'].flush(['currency_id'])
self.env['account.journal'].flush([
'currency_id',
'default_debit_account_id',
'default_credit_account_id',
'payment_debit_account_id',
'payment_credit_account_id',
'suspense_account_id',
])
self._cr.execute('''
SELECT account.id, journal.id
FROM account_account account
JOIN res_company company ON company.id = account.company_id
JOIN account_journal journal ON
journal.default_debit_account_id = account.id
OR
journal.default_credit_account_id = account.id
WHERE account.id IN %s
AND journal.type IN ('bank', 'cash')
AND journal.currency_id IS NOT NULL
AND journal.currency_id != company.currency_id
AND account.currency_id != journal.currency_id
''', [tuple(self.ids)])
res = self._cr.fetchone()
if res:
account = self.env['account.account'].browse(res[0])
journal = self.env['account.journal'].browse(res[1])
raise ValidationError(_("The foreign currency set on the journal '%s' and the account '%s' must be the same.") % (journal.display_name, account.display_name))
@api.constrains('company_id')
def _check_company_consistency(self):
if not self:
@@ -949,14 +985,33 @@ class AccountJournal(models.Model):
account_control_ids = fields.Many2many('account.account', 'journal_account_control_rel', 'journal_id', 'account_id', string='Accounts Allowed',
check_company=True,
domain="[('deprecated', '=', False), ('company_id', '=', company_id)]")
default_credit_account_id = fields.Many2one('account.account', string='Default Credit Account',
domain="[('deprecated', '=', False), ('company_id', '=', company_id)]",
help="It acts as a default account for credit amount",
default_credit_account_id = fields.Many2one('account.account', string='Default Credit Account', copy=False,
domain=[('deprecated', '=', False)], help="It acts as a default account for credit amount",
check_company=True,
ondelete='restrict')
default_debit_account_id = fields.Many2one('account.account', string='Default Debit Account',
default_debit_account_id = fields.Many2one('account.account', string='Default Debit Account', copy=False,
check_company=True,
domain="[('deprecated', '=', False), ('company_id', '=', company_id)]", help="It acts as a default account for debit amount", ondelete='restrict')
payment_debit_account_id = fields.Many2one('account.account', string='Outstanding Receipts Account', copy=False,
ondelete='restrict',
domain=[('deprecated', '=', False)],
help="Incoming payments entries triggered by invoices/refunds will be posted on the Outstanding Receipts Account "
"and displayed as blue lines in the bank reconciliation widget. During the reconciliation process, concerned "
"transactions will be reconciled with entries on the Outstanding Receipts Account instead of the "
"receivable account.")
payment_credit_account_id = fields.Many2one('account.account', string='Outstanding Payments Account', copy=False,
ondelete='restrict',
domain=[('deprecated', '=', False)],
help="Outgoing payments entries triggered by bills/credit notes will be posted on the Outstanding Payments Account "
"and displayed as blue lines in the bank reconciliation widget. During the reconciliation process, concerned "
"transactions will be reconciled with entries on the Outstanding Payments Account instead of the "
"payable account.")
suspense_account_id = fields.Many2one('account.account', string='Bank Suspense Account',
ondelete='restrict', readonly=False, store=True, require=True,
compute='_compute_suspense_account_id',
domain=[('deprecated', '=', False), ('reconcile', '=', True)],
help="Bank statements transactions will be posted on the suspense account until the final reconciliation "
"allowing finding the right account.")
restrict_mode_hash_table = fields.Boolean(string="Lock Posted Entries with Hash",
help="If ticked, the accounting entry or invoice receives a hash as soon as it is posted and cannot be modified anymore.")
sequence = fields.Integer(help='Used to order Journals in the dashboard view', default=10)
@@ -1009,7 +1064,6 @@ class AccountJournal(models.Model):
bank_statements_source = fields.Selection(selection=_get_bank_statements_available_sources, string='Bank Feeds', default='undefined', help="Defines how the bank statements will be registered")
bank_acc_number = fields.Char(related='bank_account_id.acc_number', readonly=False)
bank_id = fields.Many2one('res.bank', related='bank_account_id.bank_id', readonly=False)
post_at = fields.Selection([('pay_val', 'Payment Validation'), ('bank_rec', 'Bank Reconciliation')], string="Post At", default='pay_val')
# Sale journals fields
sale_activity_type_id = fields.Many2one('mail.activity.type', string='Schedule Activity', default=False, help="Activity will be automatically scheduled on payment due date, improving collection process.")
@@ -1035,6 +1089,18 @@ class AccountJournal(models.Model):
('code_company_uniq', 'unique (code, name, company_id)', 'The code and name of the journal must be unique per company !'),
]
@api.depends('company_id', 'type')
def _compute_suspense_account_id(self):
for journal in self:
if journal.type not in ('bank', 'cash'):
journal.suspense_account_id = False
elif journal.suspense_account_id:
journal.suspense_account_id = journal.suspense_account_id
elif journal.company_id.account_journal_suspense_account_id:
journal.suspense_account_id = journal.company_id.account_journal_suspense_account_id
else:
journal.suspense_account_id = False
def _compute_alias_domain(self):
alias_domain = self.env["ir.config_parameter"].sudo().get_param("mail.catchall.domain")
for record in self:
@@ -1070,15 +1136,6 @@ class AccountJournal(models.Model):
if self._cr.fetchone():
raise ValidationError(_('Some journal items already exist in this journal but with other accounts than the allowed ones.'))
@api.constrains('currency_id', 'default_credit_account_id', 'default_debit_account_id')
def _check_currency(self):
for journal in self:
if journal.currency_id:
if journal.default_credit_account_id and not journal.default_credit_account_id.currency_id.id == journal.currency_id.id:
raise ValidationError(_('The currency of the journal should be the same than the default credit account.'))
if journal.default_debit_account_id and not journal.default_debit_account_id.currency_id.id == journal.currency_id.id:
raise ValidationError(_('The currency of the journal should be the same than the default debit account.'))
@api.constrains('type', 'bank_account_id')
def _check_bank_account(self):
for journal in self:
@@ -1106,6 +1163,43 @@ class AccountJournal(models.Model):
if self._cr.fetchone():
raise UserError(_("You can't change the company of your journal since there are some journal entries linked to it."))
@api.constrains('default_debit_account_id', 'default_credit_account_id', 'payment_debit_account_id', 'payment_credit_account_id')
def _check_journal_not_shared_accounts(self):
accounts = self.default_debit_account_id \
+ self.default_credit_account_id \
+ self.payment_debit_account_id \
+ self.payment_credit_account_id
if not accounts:
return
self.env['account.journal'].flush([
'default_debit_account_id',
'default_credit_account_id',
'payment_debit_account_id',
'payment_credit_account_id',
])
self._cr.execute('''
SELECT
account.name,
ARRAY_AGG(DISTINCT journal.name) AS journal_names
FROM account_account account
LEFT JOIN account_journal journal ON
journal.default_debit_account_id = account.id
OR
journal.default_credit_account_id = account.id
OR
journal.payment_debit_account_id = account.id
OR
journal.payment_credit_account_id = account.id
WHERE account.id IN %s
GROUP BY account.name
HAVING COUNT(DISTINCT journal.id) > 1
''', [tuple(accounts.ids)])
res = self._cr.fetchone()
if res:
raise ValidationError(_("The account %s can't be shared between multiple journals: %s") % (res[0], ', '.join(res[1])))
@api.constrains('type', 'default_credit_account_id', 'default_debit_account_id')
def _check_type_default_credit_account_id_type(self):
journals_to_check = self.filtered(lambda journal: journal.type in ('sale', 'purchase'))
@@ -1183,10 +1277,6 @@ class AccountJournal(models.Model):
'partner_id': company.partner_id.id,
})
if 'currency_id' in vals:
if not 'default_debit_account_id' in vals and journal.default_debit_account_id:
journal.default_debit_account_id.currency_id = vals['currency_id']
if not 'default_credit_account_id' in vals and journal.default_credit_account_id:
journal.default_credit_account_id.currency_id = vals['currency_id']
if journal.bank_account_id:
journal.bank_account_id.currency_id = vals['currency_id']
if 'bank_account_id' in vals:
@@ -1205,16 +1295,15 @@ class AccountJournal(models.Model):
raise UserError(_("You cannot modify the field %s of a journal that already has accounting entries.") % field_string)
result = super(AccountJournal, self).write(vals)
for journal in self:
# Ensure the liquidity accounts are sharing the same foreign currency.
accounts = journal.default_debit_account_id + journal.default_credit_account_id
accounts.write({'currency_id': journal.currency_id.id})
# Create the bank_account_id if necessary
if 'bank_acc_number' in vals:
for journal in self.filtered(lambda r: r.type == 'bank' and not r.bank_account_id):
journal.set_bank_account(vals.get('bank_acc_number'), vals.get('bank_id'))
# Changing the 'post_at' option will post the draft payment moves and change the related invoices' state.
if 'post_at' in vals and vals['post_at'] != 'bank_rec':
draft_moves = self.env['account.move'].search([('journal_id', 'in', self.ids), ('state', '=', 'draft')])
pending_payments = draft_moves.mapped('line_ids.payment_id')
pending_payments.mapped('move_line_ids.move_id').post()
pending_payments.mapped('reconciled_invoice_ids').filtered(lambda x: x.state == 'in_payment').write({'state': 'paid'})
for record in self:
if record.restrict_mode_hash_table and not record.secure_sequence_id:
record._create_secure_sequence(['secure_sequence_id'])
@@ -1222,41 +1311,9 @@ class AccountJournal(models.Model):
return result
@api.model
def _prepare_liquidity_account(self, name, company, currency_id, type):
'''
This function prepares the value to use for the creation of the default debit and credit accounts of a
liquidity journal (created through the wizard of generating COA from templates for example).
:param name: name of the bank account
:param company: company for which the wizard is running
:param currency_id: ID of the currency in which is the bank account
:param type: either 'cash' or 'bank'
:return: mapping of field names and values
:rtype: dict
'''
digits = 6
acc = self.env['account.account'].search([('company_id', '=', company.id)], limit=1)
if acc:
digits = len(acc.code)
# Seek the next available number for the account code
if type == 'bank':
account_code_prefix = company.bank_account_code_prefix or ''
else:
account_code_prefix = company.cash_account_code_prefix or company.bank_account_code_prefix or ''
liquidity_type = self.env.ref('account.data_account_type_liquidity')
return {
'name': name,
'currency_id': currency_id or False,
'code': self.env['account.account']._search_new_account_code(company, digits, account_code_prefix),
'user_type_id': liquidity_type and liquidity_type.id or False,
'company_id': company.id,
}
@api.model
def get_next_bank_cash_default_code(self, journal_type, company_id):
def get_next_bank_cash_default_code(self, journal_type, company):
journal_code_base = (journal_type == 'cash' and 'CSH' or 'BNK')
journals = self.env['account.journal'].search([('code', 'like', journal_code_base + '%'), ('company_id', '=', company_id)])
journals = self.env['account.journal'].search([('code', 'like', journal_code_base + '%'), ('company_id', '=', company.id)])
for num in range(1, 100):
# journal_code has a maximal size of 5, hence we can enforce the boundary num < 100
journal_code = journal_code_base + str(num)
@@ -1264,38 +1321,89 @@ class AccountJournal(models.Model):
return journal_code
@api.model
def create(self, vals):
company_id = vals.get('company_id', self.env.company.id)
if vals.get('type') in ('bank', 'cash'):
# For convenience, the name can be inferred from account number
if not vals.get('name') and 'bank_acc_number' in vals:
vals['name'] = vals['bank_acc_number']
def _fill_missing_values(self, vals):
journal_type = vals.get('type')
# If no code provided, loop to find next available journal code
if not vals.get('code'):
vals['code'] = self.get_next_bank_cash_default_code(vals['type'], company_id)
# 'type' field is required.
if not journal_type:
return
# === Fill missing company ===
company = self.env['res.company'].browse(vals['company_id']) if vals.get('company_id') else self.env.company
vals['company_id'] = company.id
# Don't get the digits on 'chart_template_id' since the chart template could be a custom one.
random_account = self.env['account.account'].search([('company_id', '=', company.id)], limit=1)
digits = len(random_account.code) if random_account else 6
liquidity_type = self.env.ref('account.data_account_type_liquidity')
current_assets_type = self.env.ref('account.data_account_type_current_assets')
if journal_type in ('bank', 'cash'):
has_liquidity_accounts = vals.get('default_debit_account_id') or vals.get('default_credit_account_id')
has_payment_accounts = vals.get('payment_debit_account_id') or vals.get('payment_credit_account_id')
has_profit_account = vals.get('profit_account_id')
has_loss_account = vals.get('loss_account_id')
if journal_type == 'bank':
liquidity_account_prefix = company.bank_account_code_prefix or ''
else:
liquidity_account_prefix = company.cash_account_code_prefix or company.bank_account_code_prefix or ''
# === Fill missing name ===
vals['name'] = vals.get('name') or vals.get('bank_acc_number')
# === Fill missing code ===
if 'code' not in vals:
vals['code'] = self.get_next_bank_cash_default_code(journal_type, company)
if not vals['code']:
raise UserError(_("Cannot generate an unused journal code. Please fill the 'Shortcode' field."))
# === Fill missing accounts ===
if not has_liquidity_accounts:
liquidity_account = self.env['account.account'].create({
'name': vals.get('name'),
'code': self.env['account.account']._search_new_account_code(company, digits, liquidity_account_prefix),
'user_type_id': liquidity_type.id,
'currency_id': vals.get('currency_id'),
'company_id': company.id,
})
# Create a default debit/credit account if not given
default_account = vals.get('default_debit_account_id') or vals.get('default_credit_account_id')
company = self.env['res.company'].browse(company_id)
if not default_account:
account_vals = self._prepare_liquidity_account(vals.get('name'), company, vals.get('currency_id'), vals.get('type'))
default_account = self.env['account.account'].create(account_vals)
vals['default_debit_account_id'] = default_account.id
vals['default_credit_account_id'] = default_account.id
if vals['type'] == 'cash':
if not vals.get('profit_account_id'):
vals['profit_account_id'] = company.default_cash_difference_income_account_id.id
if not vals.get('loss_account_id'):
vals['loss_account_id'] = company.default_cash_difference_expense_account_id.id
vals.update({
'default_debit_account_id': liquidity_account.id,
'default_credit_account_id': liquidity_account.id,
})
if not has_payment_accounts:
vals['payment_debit_account_id'] = self.env['account.account'].create({
'name': _("Outstanding Receipts"),
'code': self.env['account.account']._search_new_account_code(company, digits, liquidity_account_prefix),
'reconcile': True,
'user_type_id': current_assets_type.id,
'company_id': company.id,
}).id
vals['payment_credit_account_id'] = self.env['account.account'].create({
'name': _("Outstanding Payments"),
'code': self.env['account.account']._search_new_account_code(company, digits, liquidity_account_prefix),
'reconcile': True,
'user_type_id': current_assets_type.id,
'company_id': company.id,
}).id
if journal_type == 'cash' and not has_profit_account:
vals['profit_account_id'] = company.default_cash_difference_income_account_id.id
if journal_type == 'cash' and not has_loss_account:
vals['loss_account_id'] = company.default_cash_difference_expense_account_id.id
# === Fill missing refund_sequence ===
if 'refund_sequence' not in vals:
vals['refund_sequence'] = vals['type'] in ('sale', 'purchase')
@api.model
def create(self, vals):
# OVERRIDE
self._fill_missing_values(vals)
journal = super(AccountJournal, self.with_context(mail_create_nolog=True)).create(vals)
if 'alias_name' in vals:
journal._update_mail_alias(vals)
@@ -1401,6 +1509,123 @@ class AccountJournal(models.Model):
if vals_write:
journal.write(vals_write)
# -------------------------------------------------------------------------
# REPORTING METHODS
# -------------------------------------------------------------------------
def _get_journal_bank_account_balance(self, domain=None):
''' Get the bank balance of the current journal by filtering the journal items using the journal's accounts.
/!\ The current journal is not part of the applied domain. This is the expected behavior since we only want
a logic based on accounts.
:param domain: An additional domain to be applied on the account.move.line model.
:return: The balance expressed in the journal's currency.
'''
self.ensure_one()
self.env['account.move.line'].check_access_rights('read')
accounts = self.default_debit_account_id + self.default_credit_account_id
if not accounts:
return 0.0
domain = (domain or []) + [
('account_id', 'in', tuple(accounts.ids)),
('display_type', 'not in', ('line_section', 'line_note')),
('move_id.state', '!=', 'cancel'),
]
query = self.env['account.move.line']._where_calc(domain)
tables, where_clause, where_params = query.get_sql()
query = '''
SELECT
COUNT(account_move_line.id) AS nb_lines,
COALESCE(SUM(account_move_line.balance), 0.0),
COALESCE(SUM(account_move_line.amount_currency), 0.0)
FROM ''' + tables + '''
WHERE ''' + where_clause + '''
'''
self._cr.execute(query, where_params)
company_currency = self.company_id.currency_id
journal_currency = self.currency_id if self.currency_id and self.currency_id != company_currency else False
nb_lines, balance, amount_currency = self._cr.fetchone()
return amount_currency if journal_currency else balance, nb_lines
def _get_journal_outstanding_payments_account_balance(self, domain=None, date=None):
''' Get the outstanding payments balance of the current journal by filtering the journal items using the
journal's accounts.
/!\ The current journal is not part of the applied domain. This is the expected behavior since we only want
a logic based on accounts.
:param domain: An additional domain to be applied on the account.move.line model.
:param date: The date to be used when performing the currency conversions.
:return: The balance expressed in the journal's currency.
'''
self.ensure_one()
self.env['account.move.line'].check_access_rights('read')
conversion_date = date or fields.Date.context_today(self)
accounts = self.payment_debit_account_id + self.payment_credit_account_id
if not accounts:
return 0.0
domain = (domain or []) + [
('account_id', 'in', tuple(accounts.ids)),
('display_type', 'not in', ('line_section', 'line_note')),
('move_id.state', '!=', 'cancel'),
('reconciled', '=', False),
]
query = self.env['account.move.line']._where_calc(domain)
tables, where_clause, where_params = query.get_sql()
self._cr.execute('''
SELECT
COUNT(account_move_line.id) AS nb_lines,
account_move_line.currency_id,
account.reconcile AS is_account_reconcile,
SUM(account_move_line.amount_residual) AS amount_residual,
SUM(account_move_line.balance) AS balance,
SUM(account_move_line.amount_residual_currency) AS amount_residual_currency,
SUM(account_move_line.amount_currency) AS amount_currency
FROM ''' + tables + '''
JOIN account_account account ON account.id = account_move_line.account_id
WHERE ''' + where_clause + '''
GROUP BY account_move_line.currency_id, account.reconcile
''', where_params)
company_currency = self.company_id.currency_id
journal_currency = self.currency_id if self.currency_id and self.currency_id != company_currency else False
balance_currency = journal_currency or company_currency
total_balance = 0.0
nb_lines = 0
for res in self._cr.dictfetchall():
nb_lines += res['nb_lines']
amount_currency = res['amount_residual_currency'] if res['is_account_reconcile'] else res['amount_currency']
balance = res['amount_residual'] if res['is_account_reconcile'] else res['balance']
if res['currency_id'] and journal_currency and res['currency_id'] == journal_currency.id:
total_balance += amount_currency
elif journal_currency:
total_balance += company_currency._convert(balance, balance_currency, self.company_id, conversion_date)
else:
total_balance += balance
return total_balance, nb_lines
def _get_last_bank_statement(self, domain=None):
''' Retrieve the last bank statement created using this journal.
:param domain: An additional domain to be applied on the account.bank.statement model.
:return: An account.bank.statement record or an empty recordset.
'''
self.ensure_one()
last_statement_domain = (domain or []) + [('journal_id', '=', self.id)]
last_st_line = self.env['account.bank.statement.line'].search(last_statement_domain, order='date desc, id desc', limit=1)
return last_st_line.statement_id
class ResPartnerBank(models.Model):
_inherit = "res.partner.bank"
File diff suppressed because it is too large Load Diff
@@ -100,7 +100,7 @@ class account_journal(models.Model):
locale = get_lang(self.env).code
#starting point of the graph is the last statement
last_stmt = BankStatement.search([('journal_id', '=', self.id), ('date', '<=', today.strftime(DF))], order='date desc, id desc', limit=1)
last_stmt = self._get_last_bank_statement(domain=[('move_id.state', '=', 'posted')])
last_balance = last_stmt and last_stmt.balance_end_real or 0
data.append(build_graph_data(today, last_balance))
@@ -109,15 +109,16 @@ class account_journal(models.Model):
#(graph is drawn backward)
date = today
amount = last_balance
query = """SELECT l.date, sum(l.amount) as amount
FROM account_bank_statement_line l
RIGHT JOIN account_bank_statement st ON l.statement_id = st.id
WHERE st.journal_id = %s
AND l.date > %s
AND l.date <= %s
GROUP BY l.date
ORDER BY l.date desc
"""
query = '''
SELECT move.date, sum(st_line.amount) as amount
FROM account_bank_statement_line st_line
JOIN account_move move ON move.id = st_line.move_id
WHERE move.journal_id = %s
AND move.date > %s
AND move.date <= %s
GROUP BY move.date
ORDER BY move.date desc
'''
self.env.cr.execute(query, (self.id, last_month, today))
query_result = self.env.cr.dictfetchall()
for val in query_result:
@@ -218,37 +219,37 @@ class account_journal(models.Model):
def get_journal_dashboard_datas(self):
currency = self.currency_id or self.company_id.currency_id
number_to_reconcile = number_to_check = last_balance = account_sum = 0
number_to_reconcile = number_to_check = last_balance = 0
has_at_least_one_statement = False
bank_account_balance = nb_lines_bank_account_balance = 0
outstanding_pay_account_balance = nb_lines_outstanding_pay_account_balance = 0
title = ''
number_draft = number_waiting = number_late = to_check_balance = 0
sum_draft = sum_waiting = sum_late = 0.0
if self.type in ['bank', 'cash']:
last_bank_stmt = self.env['account.bank.statement'].search([('journal_id', 'in', self.ids)], order="date desc, id desc", limit=1)
last_balance = last_bank_stmt and last_bank_stmt[0].balance_end or 0
#Get the number of items to reconcile for that bank journal
self.env.cr.execute("""SELECT COUNT(DISTINCT(line.id))
FROM account_bank_statement_line AS line
LEFT JOIN account_bank_statement AS st
ON line.statement_id = st.id
WHERE st.journal_id IN %s AND st.state = 'open' AND line.amount != 0.0 AND line.account_id IS NULL
AND not exists (select 1 from account_move_line aml where aml.statement_line_id = line.id)
""", (tuple(self.ids),))
if self.type in ('bank', 'cash'):
last_statement = self._get_last_bank_statement(
domain=[('move_id.state', '=', 'posted')])
last_balance = last_statement.balance_end
has_at_least_one_statement = bool(last_statement)
bank_account_balance, nb_lines_bank_account_balance = self._get_journal_bank_account_balance(
domain=[('move_id.state', '=', 'posted')])
outstanding_pay_account_balance, nb_lines_outstanding_pay_account_balance = self._get_journal_outstanding_payments_account_balance(
domain=[('move_id.state', '=', 'posted')])
self._cr.execute('''
SELECT COUNT(st_line.id)
FROM account_bank_statement_line st_line
JOIN account_move st_line_move ON st_line_move.id = st_line.move_id
JOIN account_bank_statement st ON st_line.statement_id = st.id
WHERE st_line_move.journal_id IN %s
AND st.state = 'posted'
AND NOT st_line.is_reconciled
''', [tuple(self.ids)])
number_to_reconcile = self.env.cr.fetchone()[0]
to_check_ids = self.to_check_ids()
number_to_check = len(to_check_ids)
to_check_balance = sum([r.amount for r in to_check_ids])
# optimization to read sum of balance from account_move_line
account_ids = tuple(ac for ac in [self.default_debit_account_id.id, self.default_credit_account_id.id] if ac)
if account_ids:
amount_field = 'aml.balance' if (not self.currency_id or self.currency_id == self.company_id.currency_id) else 'aml.amount_currency'
query = """SELECT sum(%s) FROM account_move_line aml
LEFT JOIN account_move move ON aml.move_id = move.id
WHERE aml.account_id in %%s
AND move.date <= %%s AND move.state = 'posted';""" % (amount_field,)
self.env.cr.execute(query, (account_ids, fields.Date.context_today(self),))
query_results = self.env.cr.dictfetchall()
if query_results and query_results[0].get('sum') != None:
account_sum = query_results[0].get('sum')
#TODO need to check if all invoices are in the same currency than the journal!!!!
elif self.type in ['sale', 'purchase']:
title = _('Bills to pay') if self.type == 'purchase' else _('Invoices owed to you')
@@ -293,17 +294,18 @@ class account_journal(models.Model):
number_to_check = read[0]['__count']
to_check_balance = read[0]['amount_total']
difference = currency.round(last_balance-account_sum) + 0.0
is_sample_data = self.kanban_dashboard_graph and any(data.get('is_sample_data', False) for data in json.loads(self.kanban_dashboard_graph))
return {
'number_to_check': number_to_check,
'to_check_balance': formatLang(self.env, to_check_balance, currency_obj=currency),
'number_to_reconcile': number_to_reconcile,
'account_balance': formatLang(self.env, currency.round(account_sum) + 0.0, currency_obj=currency),
'account_balance': formatLang(self.env, currency.round(bank_account_balance), currency_obj=currency),
'has_at_least_one_statement': has_at_least_one_statement,
'nb_lines_bank_account_balance': nb_lines_bank_account_balance,
'outstanding_pay_account_balance': formatLang(self.env, currency.round(outstanding_pay_account_balance), currency_obj=currency),
'nb_lines_outstanding_pay_account_balance': nb_lines_outstanding_pay_account_balance,
'last_balance': formatLang(self.env, currency.round(last_balance) + 0.0, currency_obj=currency),
'difference': formatLang(self.env, difference, currency_obj=currency) if difference else False,
'number_draft': number_draft,
'number_waiting': number_waiting,
'number_late': number_late,
@@ -494,7 +496,10 @@ class account_journal(models.Model):
[action] = self.env.ref(action_ref).read()
action['context'] = dict(ast.literal_eval(action.get('context')), default_journal_id=self.id, search_default_journal_id=self.id)
if payment_type == 'transfer':
action['context']['default_partner_id'] = self.company_id.partner_id.id
action['context'].update({
'default_partner_id': self.company_id.partner_id.id,
'default_is_internal_transfer': True,
})
if mode == 'form':
action['views'] = [[False, 'form']]
return action
+239 -162
View File
@@ -49,6 +49,30 @@ class AccountMove(models.Model):
def _sequence_fixed_regex(self):
return self.journal_id.sequence_override_regex or super()._sequence_fixed_regex
@api.model
def _search_default_journal(self, journal_types):
company_id = self._context.get('default_company_id', self.env.company.id)
domain = [('company_id', '=', company_id), ('type', 'in', journal_types)]
journal = None
if self._context.get('default_currency_id'):
currency_domain = domain + [('currency_id', '=', self._context['default_currency_id'])]
journal = self.env['account.journal'].search(currency_domain, limit=1)
if not journal:
journal = self.env['account.journal'].search(domain, limit=1)
if not journal:
company = self.env['res.company'].browse(company_id)
error_msg = _("No journal could be found in company %s for any of those types: %s") % (
company.display_name,
', '.join(journal_types),
)
raise UserError(error_msg)
return journal
@api.model
def _get_default_journal(self):
''' Get the default journal.
@@ -56,36 +80,21 @@ class AccountMove(models.Model):
either be determined by the default type.
'''
move_type = self._context.get('default_move_type', 'entry')
journal_type = 'general'
if move_type in self.get_sale_types(include_receipts=True):
journal_type = 'sale'
journal_types = ['sale']
elif move_type in self.get_purchase_types(include_receipts=True):
journal_type = 'purchase'
journal_types = ['purchase']
else:
journal_types = self._context.get('default_move_journal_types', ['general'])
if self._context.get('default_journal_id'):
journal = self.env['account.journal'].browse(self._context['default_journal_id'])
if move_type != 'entry' and journal.type != journal_type:
if move_type != 'entry' and journal.type not in journal_types:
raise UserError(_("Cannot create an invoice of type %s with a journal having %s as type.") % (move_type, journal.type))
else:
company_id = self._context.get('force_company', self._context.get('default_company_id', self.env.company.id))
domain = [('company_id', '=', company_id), ('type', '=', journal_type)]
journal = self._search_default_journal(journal_types)
journal = None
if self._context.get('default_currency_id'):
currency_domain = domain + [('currency_id', '=', self._context['default_currency_id'])]
journal = self.env['account.journal'].search(currency_domain, limit=1)
if not journal:
journal = self.env['account.journal'].search(domain, limit=1)
if not journal:
error_msg = _('Please define an accounting miscellaneous journal in your company')
if journal_type == 'sale':
error_msg = _('Please define an accounting sale journal in your company')
elif journal_type == 'purchase':
error_msg = _('Please define an accounting purchase journal in your company')
raise UserError(error_msg)
return journal
@api.model
@@ -154,6 +163,21 @@ class AccountMove(models.Model):
commercial_partner_id = fields.Many2one('res.partner', string='Commercial Entity', store=True, readonly=True,
compute='_compute_commercial_partner_id')
country_code = fields.Char(related='company_id.country_id.code', readonly=True)
user_id = fields.Many2one(string='User', related='invoice_user_id',
help='Technical field used to fit the generic behavior in mail templates.')
is_move_sent = fields.Boolean(readonly=True, default=False, copy=False,
help="It indicates that the invoice/payment has been sent.")
partner_bank_id = fields.Many2one('res.partner.bank', string='Bank Account',
help='Bank Account Number to which the invoice will be paid. A Company bank account if this is a Customer Invoice or Vendor Credit Note, otherwise a Partner bank account number.',
check_company=True)
payment_reference = fields.Char(string='Payment Reference', index=True, copy=False,
help="The payment reference to set on journal items.")
payment_id = fields.Many2one(
comodel_name='account.payment',
string="Payment", copy=False, check_company=True)
statement_line_id = fields.Many2one(
comodel_name='account.bank.statement.line',
string="Statement Line", copy=False, check_company=True)
# === Amount fields ===
amount_untaxed = fields.Monetary(string='Untaxed Amount', store=True, readonly=True, tracking=True,
@@ -183,7 +207,7 @@ class AccountMove(models.Model):
('partial', 'Partially Paid'),
('reversed', 'Reversed'),
('invoicing_legacy', 'Invoicing App Legacy')],
string='Payment', store=True, readonly=True, copy=False, tracking=True,
string="Payment State", store=True, readonly=True, copy=False, tracking=True,
compute='_compute_amount')
# ==== Cash basis feature fields ====
@@ -215,17 +239,11 @@ class AccountMove(models.Model):
invoice_user_id = fields.Many2one('res.users', copy=False, tracking=True,
string='Salesperson',
default=lambda self: self.env.user)
user_id = fields.Many2one(string='User', related='invoice_user_id',
help='Technical field used to fit the generic behavior in mail templates.')
invoice_date = fields.Date(string='Invoice/Bill Date', readonly=True, index=True, copy=False,
states={'draft': [('readonly', False)]},
default=_get_default_invoice_date)
invoice_date_due = fields.Date(string='Due Date', readonly=True, index=True, copy=False,
states={'draft': [('readonly', False)]})
invoice_payment_ref = fields.Char(string='Payment Reference', index=True, copy=False,
help="The payment reference to set on journal items.")
invoice_sent = fields.Boolean(readonly=True, default=False, copy=False,
help="It indicates that the invoice has been sent.")
invoice_origin = fields.Char(string='Origin', readonly=True, tracking=True,
help="The document(s) that generated the invoice.")
invoice_payment_term_id = fields.Many2one('account.payment.term', string='Payment Terms',
@@ -236,9 +254,6 @@ class AccountMove(models.Model):
copy=False, readonly=True,
domain=[('exclude_from_invoice_tab', '=', False)],
states={'draft': [('readonly', False)]})
invoice_partner_bank_id = fields.Many2one('res.partner.bank', string='Bank Account',
help='Bank Account Number to which the invoice will be paid. A Company bank account if this is a Customer Invoice or Vendor Credit Note, otherwise a Partner bank account number.',
check_company=True)
invoice_incoterm_id = fields.Many2one('account.incoterms', string='Incoterm',
default=_get_default_invoice_incoterm,
help='International Commercial Terms are a series of predefined commercial terms used in international transactions.')
@@ -280,12 +295,38 @@ class AccountMove(models.Model):
help="Technical field used to display a message when the invoice's accounting date is prior of the tax lock date.")
# Technical field to hide Reconciled Entries stat button
has_reconciled_entries = fields.Boolean(compute="_compute_has_reconciled_entries")
# ==== Hash Fields ====
restrict_mode_hash_table = fields.Boolean(related='journal_id.restrict_mode_hash_table')
secure_sequence_number = fields.Integer(string="Inalteralbility No Gap Sequence #", readonly=True, copy=False)
inalterable_hash = fields.Char(string="Inalterability Hash", readonly=True, copy=False)
string_to_hash = fields.Char(compute='_compute_string_to_hash', readonly=True)
@api.model
def _field_will_change(self, record, vals, field_name):
if field_name not in vals:
return False
field = record._fields[field_name]
if field.type == 'many2one':
return record[field_name].id != vals[field_name]
if field.type == 'many2many':
current_ids = set(record[field_name].ids)
after_write_ids = set(record.new({field_name: vals[field_name]})[field_name].ids)
return current_ids != after_write_ids
if field.type == 'one2many':
return True
if field.type == 'monetary' and record[field.currency_field]:
return not record[field.currency_field].is_zero(record[field_name] - vals[field_name])
return record[field_name] != vals[field_name]
@api.model
def _cleanup_write_orm_values(self, record, vals):
cleaned_vals = dict(vals)
for field_name, value in vals.items():
if not self._field_will_change(record, vals, field_name):
del cleaned_vals[field_name]
return cleaned_vals
# -------------------------------------------------------------------------
# ONCHANGE METHODS
# -------------------------------------------------------------------------
@@ -349,7 +390,7 @@ class AccountMove(models.Model):
line.account_id = new_term_account
self._compute_bank_partner_id()
self.invoice_partner_bank_id = self.bank_partner_id.bank_ids and self.bank_partner_id.bank_ids[0]
self.partner_bank_id = self.bank_partner_id.bank_ids and self.bank_partner_id.bank_ids[0]
# Find the new fiscal position.
delivery_partner_id = self._get_invoice_delivery_partner_id()
@@ -376,10 +417,10 @@ class AccountMove(models.Model):
self._recompute_dynamic_lines(recompute_tax_base_amount=True)
@api.onchange('invoice_payment_ref')
def _onchange_invoice_payment_ref(self):
@api.onchange('payment_reference')
def _onchange_payment_reference(self):
for line in self.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable')):
line.name = self.invoice_payment_ref
line.name = self.payment_reference
@api.onchange('invoice_vendor_bill_id')
def _onchange_invoice_vendor_bill(self):
@@ -854,7 +895,7 @@ class AccountMove(models.Model):
# Create new line.
create_method = in_draft_mode and self.env['account.move.line'].new or self.env['account.move.line'].create
candidate = create_method({
'name': self.invoice_payment_ref or '',
'name': self.payment_reference or '',
'debit': balance < 0.0 and -balance or 0.0,
'credit': balance > 0.0 and balance or 0.0,
'quantity': 1.0,
@@ -890,7 +931,7 @@ class AccountMove(models.Model):
self.line_ids -= existing_terms_lines - new_terms_lines
if new_terms_lines:
self.invoice_payment_ref = new_terms_lines[-1].name or ''
self.payment_reference = new_terms_lines[-1].name or ''
self.invoice_date_due = new_terms_lines[-1].date_maturity
def _recompute_dynamic_lines(self, recompute_all_taxes=False, recompute_tax_base_amount=False):
@@ -1056,7 +1097,18 @@ class AccountMove(models.Model):
else:
move.bank_partner_id = move.company_id.partner_id
@api.model
def _get_invoice_in_payment_state(self):
''' Hook to give the state when the invoice becomes fully paid. This is necessary because the users working
with only invoicing don't want to see the 'in_payment' state. Then, this method will be overridden in the
accountant module to enable the 'in_payment' state. '''
return 'paid'
@api.depends(
'line_ids.matched_debit_ids.debit_move_id.move_id.line_ids.amount_residual',
'line_ids.matched_debit_ids.debit_move_id.move_id.line_ids.amount_residual_currency',
'line_ids.matched_credit_ids.credit_move_id.move_id.line_ids.amount_residual',
'line_ids.matched_credit_ids.credit_move_id.move_id.line_ids.amount_residual_currency',
'line_ids.debit',
'line_ids.credit',
'line_ids.currency_id',
@@ -1066,30 +1118,6 @@ class AccountMove(models.Model):
'line_ids.payment_id.state',
'line_ids.full_reconcile_id')
def _compute_amount(self):
invoice_ids = [move.id for move in self if move.id and move.is_invoice(include_receipts=True)]
self.env['account.payment'].flush(['state'])
if invoice_ids:
self._cr.execute(
'''
SELECT move.id
FROM account_move move
JOIN account_move_line line ON line.move_id = move.id
JOIN account_partial_reconcile part ON part.debit_move_id = line.id OR part.credit_move_id = line.id
JOIN account_move_line rec_line ON
(rec_line.id = part.credit_move_id AND line.id = part.debit_move_id)
OR
(rec_line.id = part.debit_move_id AND line.id = part.credit_move_id)
JOIN account_payment payment ON payment.id = rec_line.payment_id
JOIN account_journal journal ON journal.id = rec_line.journal_id
WHERE payment.state IN ('posted', 'sent')
AND journal.post_at = 'bank_rec'
AND move.id IN %s
''', [tuple(invoice_ids)]
)
in_payment_set = set(res[0] for res in self._cr.fetchall())
else:
in_payment_set = {}
for move in self:
if move.payment_state == 'invoicing_legacy':
@@ -1162,10 +1190,10 @@ class AccountMove(models.Model):
if move.is_invoice(include_receipts=True) and move.state == 'posted':
if currency.is_zero(move.amount_residual):
if move.id in in_payment_set:
new_pmt_state = 'in_payment'
else:
if all(payment.is_matched for payment in move._get_reconciled_payments()):
new_pmt_state = 'paid'
else:
new_pmt_state = move._get_invoice_in_payment_state()
elif currency.compare_amounts(total_to_pay, total_residual) != 0:
new_pmt_state = 'partial'
@@ -1252,7 +1280,7 @@ class AccountMove(models.Model):
pay_term_line_ids = move.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
domain = [('account_id', 'in', pay_term_line_ids.mapped('account_id').ids),
'|', ('move_id.state', '=', 'posted'), '&', ('move_id.state', '=', 'draft'), ('journal_id.post_at', '=', 'bank_rec'),
('move_id.state', '=', 'posted'),
('partner_id', '=', move.commercial_partner_id.id),
('reconciled', '=', False), '|', ('amount_residual', '!=', 0.0),
('amount_residual_currency', '!=', 0.0)]
@@ -1320,7 +1348,7 @@ class AccountMove(models.Model):
'currency': self.currency_id.symbol,
'digits': [69, self.currency_id.decimal_places],
'position': self.currency_id.position,
'date': counterpart_line.date,
'date': counterpart_line.payment_id.date or counterpart_line.date,
'payment_id': counterpart_line.id,
'account_payment_id': counterpart_line.payment_id.id,
'payment_method_name': counterpart_line.payment_id.payment_method_id.name if counterpart_line.journal_id.type == 'bank' else None,
@@ -1408,13 +1436,30 @@ class AccountMove(models.Model):
move.tax_lock_date_message = False
# -------------------------------------------------------------------------
# CONSTRAINS METHODS
# BUSINESS MODELS SYNCHRONIZATION
# -------------------------------------------------------------------------
@api.constrains('line_ids', 'journal_id')
def _validate_move_modification(self):
if 'posted' in self.mapped('line_ids.payment_id.state'):
raise ValidationError(_("You cannot modify a journal entry linked to a posted payment."))
def _synchronize_business_models(self, changed_fields):
''' Ensure the consistency between:
account.payment & account.move
account.bank.statement.line & account.move
The idea is to call the method performing the synchronization of the business
models regarding their related journal entries. To avoid cycling, the
'skip_account_move_synchronization' key is used through the context.
:param changed_fields: A set containing all modified fields on account.move.
'''
if self._context.get('skip_account_move_synchronization'):
return
self_sudo = self.sudo()
self_sudo.payment_id._synchronize_from_moves(changed_fields)
self_sudo.statement_line_id._synchronize_from_moves(changed_fields)
# -------------------------------------------------------------------------
# CONSTRAINT METHODS
# -------------------------------------------------------------------------
@api.constrains('name', 'journal_id', 'state')
def _check_unique_sequence_number(self):
@@ -1683,7 +1728,7 @@ class AccountMove(models.Model):
res = True
else:
vals.pop('invoice_line_ids', None)
res = super(AccountMove, self.with_context(check_move_validity=False)).write(vals)
res = super(AccountMove, self.with_context(check_move_validity=False, skip_account_move_synchronization=True)).write(vals)
# You can't change the date of a not-locked move to a locked period.
# You can't post a new journal entry inside a locked period.
@@ -1702,13 +1747,15 @@ class AccountMove(models.Model):
if 'line_ids' in vals and self._context.get('check_move_validity', True):
self._check_balanced()
self._synchronize_business_models(set(vals.keys()))
return res
def unlink(self):
for move in self:
if move.posted_before and not self._context.get('force_delete'):
raise UserError(_("You cannot delete an entry which has been posted once."))
move.line_ids.unlink()
self.line_ids.unlink()
return super(AccountMove, self).unlink()
@api.depends('name', 'state')
@@ -1929,10 +1976,54 @@ class AccountMove(models.Model):
def _get_reconciled_payments(self):
"""Helper used to retrieve the reconciled payments on this journal entry"""
pay_term_line_ids = self.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
reconciled_amls = pay_term_line_ids.mapped('matched_debit_ids.debit_move_id') + \
pay_term_line_ids.mapped('matched_credit_ids.credit_move_id')
return reconciled_amls.mapped('payment_id')
reconciled_lines = self.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
reconciled_amls = reconciled_lines.mapped('matched_debit_ids.debit_move_id') + \
reconciled_lines.mapped('matched_credit_ids.credit_move_id')
return reconciled_amls.move_id.payment_id
def _get_reconciled_statement_lines(self):
"""Helper used to retrieve the reconciled payments on this journal entry"""
reconciled_lines = self.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
reconciled_amls = reconciled_lines.mapped('matched_debit_ids.debit_move_id') + \
reconciled_lines.mapped('matched_credit_ids.credit_move_id')
return reconciled_amls.move_id.statement_line_id
def _get_reconciled_invoices(self):
"""Helper used to retrieve the reconciled payments on this journal entry"""
reconciled_lines = self.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
reconciled_amls = reconciled_lines.mapped('matched_debit_ids.debit_move_id') + \
reconciled_lines.mapped('matched_credit_ids.credit_move_id')
return reconciled_amls.move_id.filtered(lambda move: move.is_invoice(include_receipts=True))
def _get_reconciled_invoices_partials(self):
''' Helper to retrieve the details about reconciled invoices.
:return A list of tuple (amount, rate, invoice_line).
'''
self.ensure_one()
def compute_tuple(partial, payment_line, invoice_line):
if payment_line.currency_id == invoice_line.currency_id:
# Same currency.
exchange_rate = None
amount_paid = partial.amount_currency
elif invoice_line.currency_id:
# Different currency on the payment.
exchange_rate = payment_line.amount_currency / payment_line.balance
amount_paid = partial.company_currency_id._convert(partial.amount, invoice_line.currency_id, invoice_line.company_id, invoice_line.date)
else:
# No foreign currency on the payment, don't care about the invoice's currency.
exchange_rate = None
amount_paid = partial.amount
return (amount_paid, exchange_rate, invoice_line)
receivable_lines = self.line_ids.filtered(lambda line: line.account_internal_type == 'receivable')
invoice_partials = []
for partial in receivable_lines.matched_debit_ids:
invoice_partials.append(compute_tuple(partial, partial.credit_move_id, partial.debit_move_id))
for partial in receivable_lines.matched_credit_ids:
invoice_partials.append(compute_tuple(partial, partial.debit_move_id, partial.credit_move_id))
return invoice_partials
def _reverse_move_vals(self, default_values, cancel=True):
''' Reverse values passed as parameter being the copied values of the original journal entry.
@@ -2177,6 +2268,7 @@ class AccountMove(models.Model):
'state': 'posted',
'posted_before': True,
})
for move in self:
if move.auto_post and move.date > fields.Date.today():
raise UserError(_("This move is configured to be auto-posted on {}".format(move.date.strftime(get_lang(self.env).date_format))))
@@ -2186,21 +2278,17 @@ class AccountMove(models.Model):
# Compute 'ref' for 'out_invoice'.
if move._auto_compute_invoice_reference():
to_write = {
'invoice_payment_ref': move._get_invoice_computed_reference(),
'payment_reference': move._get_invoice_computed_reference(),
'line_ids': []
}
for line in move.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable')):
to_write['line_ids'].append((1, line.id, {'name': to_write['invoice_payment_ref']}))
to_write['line_ids'].append((1, line.id, {'name': to_write['payment_reference']}))
move.write(to_write)
if move == move.company_id.account_opening_move_id and not move.company_id.account_bank_reconciliation_start:
# For opening moves, we set the reconciliation date threshold
# to the move's date if it wasn't already set (we don't want
# to have to reconcile all the older payments -made before
# installing Accounting- with bank statements)
move.company_id.account_bank_reconciliation_start = move.date
if move.is_sale_document() and move.journal_id.sale_activity_type_id and (move.journal_id.sale_activity_user_id or move.invoice_user_id).id not in (self.env.ref('base.user_root').id, False):
for move in self:
if move.is_sale_document() \
and move.journal_id.sale_activity_type_id \
and (move.journal_id.sale_activity_user_id or move.invoice_user_id).id not in (self.env.ref('base.user_root').id, False):
move.activity_schedule(
date_deadline=min((date for date in move.line_ids.mapped('date_maturity') if date), default=move.date),
activity_type_id=move.journal_id.sale_activity_type_id.id,
@@ -2219,14 +2307,13 @@ class AccountMove(models.Model):
lambda m: m.is_invoice(include_receipts=True) and m.currency_id.is_zero(m.amount_total)
).action_invoice_paid()
def _auto_compute_invoice_reference(self):
''' Hook to be overridden to set custom conditions for auto-computed invoice references.
:return True if the move should get a auto-computed reference else False
:rtype bool
'''
self.ensure_one()
return self.move_type == 'out_invoice' and not self.invoice_payment_ref
return self.move_type == 'out_invoice' and not self.payment_reference
def action_reverse(self):
action = self.env.ref('account.action_view_account_move_reversal').read()[0]
@@ -2237,8 +2324,6 @@ class AccountMove(models.Model):
return action
def action_post(self):
if self.mapped('line_ids.payment_id') and any(post_at == 'bank_rec' for post_at in self.mapped('journal_id.post_at')):
raise UserError(_("A payment journal entry generated in a journal configured to post entries only when payments are reconciled with a bank statement cannot be manually posted. Those will be posted automatically after performing the bank reconciliation."))
return self.post()
def js_assign_outstanding_line(self, line_id):
@@ -2265,7 +2350,7 @@ class AccountMove(models.Model):
move.mapped('line_ids.analytic_line_ids').unlink()
self.mapped('line_ids').remove_move_reconcile()
self.write({'state': 'draft'})
self.write({'state': 'draft', 'is_move_sent': False})
def button_cancel(self):
self.write({'auto_post': False, 'state': 'cancel'})
@@ -2357,7 +2442,7 @@ class AccountMove(models.Model):
if any(not move.is_invoice(include_receipts=True) for move in self):
raise UserError(_("Only invoices could be printed."))
self.filtered(lambda inv: not inv.invoice_sent).write({'invoice_sent': True})
self.filtered(lambda inv: not inv.is_move_sent).write({'is_move_sent': True})
if self.user_has_groups('account.group_account_invoice'):
return self.env.ref('account.account_invoices').report_action(self)
else:
@@ -2367,10 +2452,21 @@ class AccountMove(models.Model):
''' Hook to be overrided called when the invoice moves to the paid state. '''
pass
def action_invoice_register_payment(self):
return self.env['account.payment']\
.with_context(active_ids=self.ids, active_model='account.move', active_id=self.id)\
.action_register_payment()
def action_register_payment(self):
''' Open the account.payment.register wizard to pay the selected journal entries.
:return: An action opening the account.payment.register wizard.
'''
return {
'name': _('Register Payment'),
'res_model': 'account.payment.register',
'view_mode': 'form',
'context': {
'active_model': 'account.move',
'active_ids': self.ids,
},
'target': 'new',
'type': 'ir.actions.act_window',
}
def action_switch_invoice_into_refund_credit_note(self):
if any(move.move_type not in ('in_invoice', 'out_invoice') for move in self):
@@ -2394,7 +2490,7 @@ class AccountMove(models.Model):
move.write({
'move_type': move.move_type.replace('invoice', 'refund'),
'invoice_line_ids' : [(5, 0, 0)],
'invoice_partner_bank_id': False,
'partner_bank_id': False,
})
move.write({'invoice_line_ids' : new_invoice_line_ids})
@@ -2498,12 +2594,12 @@ class AccountMove(models.Model):
qr_code_method = self.qr_code_method
if qr_code_method:
# If the user set a qr code generator manually, we check that we can use it
if not self.invoice_partner_bank_id._eligible_for_qr_code(self.qr_code_method, self.partner_id, self.currency_id):
if not self.partner_bank_id._eligible_for_qr_code(self.qr_code_method, self.partner_id, self.currency_id):
raise UserError(_("The chosen QR-code type is not eligible for this invoice."))
else:
# Else we find one that's eligible and assign it to the invoice
for candidate_method, candidate_name in self.env['res.partner.bank'].get_available_qr_methods_in_sequence():
if self.invoice_partner_bank_id._eligible_for_qr_code(candidate_method, self.partner_id, self.currency_id):
if self.partner_bank_id._eligible_for_qr_code(candidate_method, self.partner_id, self.currency_id):
qr_code_method = candidate_method
break
@@ -2512,7 +2608,7 @@ class AccountMove(models.Model):
return None
unstruct_ref = self.ref if self.ref else self.name
rslt = self.invoice_partner_bank_id.build_qr_code_url(self.amount_residual, unstruct_ref, self.invoice_payment_ref, self.currency_id, self.partner_id, qr_code_method, silent_errors=False)
rslt = self.partner_bank_id.build_qr_code_url(self.amount_residual, unstruct_ref, self.payment_reference, self.currency_id, self.partner_id, qr_code_method, silent_errors=False)
# We only set qr_code_method after generating the url; otherwise, it
# could be set even in case of a failure in the QR code generation
@@ -2586,11 +2682,14 @@ class AccountMoveLine(models.Model):
# ==== Origin fields ====
reconcile_model_id = fields.Many2one('account.reconcile.model', string="Reconciliation Model", copy=False, readonly=True, check_company=True)
payment_id = fields.Many2one('account.payment', string="Originator Payment", copy=False, check_company=True,
help="Payment that created this entry")
statement_line_id = fields.Many2one('account.bank.statement.line',
string='Bank statement line reconciled with this entry',
index=True, copy=False, readonly=True, check_company=True)
payment_id = fields.Many2one('account.payment', index=True, store=True,
string="Originator Payment",
related='move_id.payment_id',
help="The payment that created this entry")
statement_line_id = fields.Many2one('account.bank.statement.line', index=True, store=True,
string="Originator Statement Line",
related='move_id.statement_line_id',
help="The statement line that created this entry")
statement_id = fields.Many2one(related='statement_line_id.statement_id', store=True, index=True, copy=False,
help="The bank statement used for bank reconciliation")
@@ -2823,12 +2922,12 @@ class AccountMoveLine(models.Model):
res = {}
# Compute 'price_subtotal'.
price_unit_wo_discount = price_unit * (1 - (discount / 100.0))
subtotal = quantity * price_unit_wo_discount
line_discount_price_unit = price_unit * (1 - (discount / 100.0))
subtotal = quantity * line_discount_price_unit
# Compute 'price_total'.
if taxes:
taxes_res = taxes._origin.compute_all(price_unit_wo_discount,
taxes_res = taxes._origin.compute_all(line_discount_price_unit,
quantity=quantity, currency=currency, product=product, partner=partner, is_refund=move_type in ('out_refund', 'in_refund'))
res['price_subtotal'] = taxes_res['total_excluded']
res['price_total'] = taxes_res['total_included']
@@ -3269,7 +3368,7 @@ class AccountMoveLine(models.Model):
def _check_constrains_account_id_journal_id(self):
for line in self:
account = line.account_id
journal = line.journal_id
journal = line.move_id.journal_id
if account.deprecated:
raise UserError(_('The account %s (%s) is deprecated.') % (account.name, account.code))
@@ -3423,25 +3522,12 @@ class AccountMoveLine(models.Model):
moves._check_balanced()
moves._check_fiscalyear_lock_date()
lines._check_tax_lock_date()
moves._synchronize_business_models({'line_ids'})
return lines
def write(self, vals):
# OVERRIDE
def field_will_change(line, field_name):
if field_name not in vals:
return False
field = line._fields[field_name]
if field.type == 'many2one':
return line[field_name].id != vals[field_name]
if field.type in ('one2many', 'many2many'):
current_ids = set(line[field_name].ids)
after_write_ids = set(line.new({field_name: vals[field_name]})[field_name].ids)
return current_ids != after_write_ids
if field.type == 'monetary' and line[field.currency_field]:
return not line[field.currency_field].is_zero(line[field_name] - vals[field_name])
return line[field_name] != vals[field_name]
ACCOUNTING_FIELDS = ('debit', 'credit', 'amount_currency')
BUSINESS_FIELDS = ('price_unit', 'quantity', 'discount', 'tax_ids')
PROTECTED_FIELDS_TAX_LOCK_DATE = ['debit', 'credit', 'tax_line_id', 'tax_ids', 'tax_tag_ids']
@@ -3460,23 +3546,17 @@ class AccountMoveLine(models.Model):
raise UserError(_("You cannot edit the following fields due to restrict mode being activated on the journal: %s.") % ', '.join(INTEGRITY_HASH_LINE_FIELDS))
if any(key in vals for key in ('tax_ids', 'tax_line_ids')):
raise UserError(_('You cannot modify the taxes related to a posted journal item, you should reset the journal entry to draft to do so.'))
# When making a reconciliation on an existing liquidity journal item, mark the payment as reconciled
if 'statement_line_id' in vals and line.payment_id:
# In case of an internal transfer, there are 2 liquidity move lines to match with a bank statement
if all(line.statement_id for line in line.payment_id.move_line_ids.filtered(
lambda r: r.id != line.id and r.account_id.internal_type == 'liquidity')):
line.payment_id.state = 'reconciled'
# Check the lock date.
if any(field_will_change(line, field_name) for field_name in PROTECTED_FIELDS_LOCK_DATE):
if any(self.env['account.move']._field_will_change(line, vals, field_name) for field_name in PROTECTED_FIELDS_LOCK_DATE):
line.move_id._check_fiscalyear_lock_date()
# Check the tax lock date.
if any(field_will_change(line, field_name) for field_name in PROTECTED_FIELDS_TAX_LOCK_DATE):
if any(self.env['account.move']._field_will_change(line, vals, field_name) for field_name in PROTECTED_FIELDS_TAX_LOCK_DATE):
line._check_tax_lock_date()
# Check the reconciliation.
if any(field_will_change(line, field_name) for field_name in PROTECTED_FIELDS_RECONCILIATION):
if any(self.env['account.move']._field_will_change(line, vals, field_name) for field_name in PROTECTED_FIELDS_RECONCILIATION):
line._check_reconciliation()
# Check switching receivable / payable accounts.
@@ -3576,6 +3656,8 @@ class AccountMoveLine(models.Model):
if self._context.get('check_move_validity', True):
self.mapped('move_id')._check_balanced()
self.mapped('move_id')._synchronize_business_models({'line_ids'})
return result
def _valid_field_parameter(self, field, name):
@@ -3910,6 +3992,8 @@ class AccountMoveLine(models.Model):
all_accounts.append(line.account_id)
if line.reconciled:
raise UserError(_('You are trying to reconcile some entries that are already reconciled.'))
if line.move_id.state != 'posted':
raise UserError(_('You can only reconcile posted entries.'))
if len(company_ids) > 1:
raise UserError(_('To reconcile the entries company should be the same for all entries.'))
if len(set(all_accounts)) > 1:
@@ -3923,11 +4007,6 @@ class AccountMoveLine(models.Model):
if not self:
return
# List unpaid invoices
not_paid_invoices = self.mapped('move_id').filtered(
lambda m: m.is_invoice(include_receipts=True) and m.payment_state not in ('paid', 'in_payment')
)
self._check_reconcile_validity()
#reconcile everything that can be
remaining_moves = self.auto_reconcile_lines()
@@ -3948,11 +4027,6 @@ class AccountMoveLine(models.Model):
# Check if reconciliation is total or needs an exchange rate entry to be created
(self + writeoff_to_reconcile).check_full_reconcile()
# Trigger action for paid invoices
not_paid_invoices.filtered(
lambda m: m.payment_state in ('paid', 'in_payment')
).action_invoice_paid()
return True
def _create_writeoff(self, writeoff_vals):
@@ -4048,7 +4122,7 @@ class AccountMoveLine(models.Model):
def remove_move_reconcile(self):
""" Undo a reconciliation """
(self.mapped('matched_debit_ids') + self.mapped('matched_credit_ids')).unlink()
(self.matched_debit_ids + self.matched_credit_ids).unlink()
def _copy_data_extend_business_fields(self, values):
''' Hook allowing copying business fields under certain conditions.
@@ -4454,6 +4528,7 @@ class AccountPartialReconcile(models.Model):
self.ensure_one()
move_date = self.debit_move_id.date
newly_created_move = self.env['account.move']
to_reconcile = []
# We use a set here in case the reconciled lines belong to the same move (it happens with POS)
for move in {self.debit_move_id.move_id, self.credit_move_id.move_id}:
#move_date is the max of the 2 reconciled items
@@ -4509,9 +4584,7 @@ class AccountPartialReconcile(models.Model):
'tax_tag_ids': [(6, 0, line.tax_tag_ids.ids)],
})
if line.account_id.reconcile and not line.reconciled:
#setting the account to allow reconciliation will help to fix rounding errors
to_clear_aml |= line
to_clear_aml.reconcile()
to_reconcile.append(to_clear_aml + line)
taxes_payment_exigible = line.tax_ids.flatten_taxes_hierarchy().filtered(lambda tax: tax.tax_exigibility == 'on_payment')
if taxes_payment_exigible:
@@ -4552,6 +4625,8 @@ class AccountPartialReconcile(models.Model):
self._set_tax_cash_basis_entry_date(move_date, newly_created_move)
# post move
newly_created_move.post()
for lines in to_reconcile:
lines.reconcile()
def _create_tax_basis_move(self):
# Check if company_journal for cash basis is set if not, raise exception
@@ -4578,9 +4653,13 @@ class AccountPartialReconcile(models.Model):
move._reverse_moves([{'ref': _('Reversal of %s') % move.name}], cancel=True)
else:
move._reverse_moves([{'date': fields.Date.today(), 'ref': _('Reversal of %s') % move.name}], cancel=True)
# Unlink partials then the full in this order to avoid a recursive call to the same partials.
res = super(AccountPartialReconcile, self).unlink()
if full_to_unlink:
full_to_unlink.unlink()
return res
@@ -4600,21 +4679,19 @@ class AccountFullReconcile(models.Model):
cancel the currency difference entry on the partner account (otherwise it will still appear on the aged balance
for example).
"""
for rec in self:
if rec.exists() and rec.exchange_move_id:
# reverse the exchange rate entry after de-referencing it to avoid looping
# (reversing will cause a nested attempt to drop the full reconciliation)
to_reverse = rec.exchange_move_id
rec.exchange_move_id = False
if to_reverse.date > (to_reverse.company_id.period_lock_date or date.min):
reverse_date = to_reverse.date
else:
reverse_date = fields.Date.today()
to_reverse._reverse_moves([{
'date': reverse_date,
'ref': _('Reversal of: %s') % to_reverse.name,
}], cancel=True)
return super(AccountFullReconcile, self).unlink()
moves_to_reverse = self.exchange_move_id
res = super(AccountFullReconcile, self).unlink()
# Reverse all exchange moves at once.
today = fields.Date.today()
default_values_list = [{
'date': today,
'ref': _('Reversal of: %s') % move.name,
} for move in moves_to_reverse]
moves_to_reverse._reverse_moves(default_values_list, cancel=True)
return res
@api.model
def _prepare_exchange_diff_move(self, move_date, company):
File diff suppressed because it is too large Load Diff
+108 -150
View File
@@ -218,10 +218,9 @@ class AccountReconcileModel(models.Model):
:return: A list of dict representing move.lines to be created corresponding to the tax.
'''
self.ensure_one()
balance = base_line_dict['debit'] - base_line_dict['credit']
currency = base_line_dict.get('currency_id') and self.env['res.currency'].browse(base_line_dict['currency_id'])
balance = base_line_dict['balance']
res = tax.compute_all(balance, currency=currency)
res = tax.compute_all(balance)
new_aml_dicts = []
for tax_res in res['taxes']:
@@ -231,27 +230,26 @@ class AccountReconcileModel(models.Model):
'account_id': tax_res['account_id'] or base_line_dict['account_id'],
'name': tax_res['name'],
'partner_id': base_line_dict.get('partner_id'),
'debit': tax_res['amount'] > 0 and tax_res['amount'] or 0,
'credit': tax_res['amount'] < 0 and -tax_res['amount'] or 0,
'balance': tax_res['amount'],
'analytic_account_id': tax.analytic and base_line_dict['analytic_account_id'],
'analytic_tag_ids': tax.analytic and base_line_dict['analytic_tag_ids'],
'tax_exigible': tax_res['tax_exigibility'],
'tax_repartition_line_id': tax_res['tax_repartition_line_id'],
'tax_ids': [(6, 0, tax_res['tax_ids'])],
'tax_tag_ids': [(6, 0, tax_res['tag_ids'])],
'currency_id': False,
'reconcile_model_id': self.id,
})
# Handle price included taxes.
base_line_dict['debit'] = tax_res['base'] > 0 and tax_res['base'] or base_line_dict['debit']
base_line_dict['credit'] = tax_res['base'] < 0 and -tax_res['base'] or base_line_dict['credit']
base_line_dict['balance'] = tax_res['base']
base_line_dict['tax_tag_ids'] = [(6, 0, res['base_tags'])]
return new_aml_dicts
def _get_write_off_move_lines_dict(self, st_line, move_lines=None, residual_balance=None):
def _get_write_off_move_lines_dict(self, st_line, residual_balance):
''' Get move.lines dict (to be passed to the create()) corresponding to the reconciliation model's write-off lines.
:param st_line: An account.bank.statement.line record.(possibly empty, if performing manual reconciliation)
:param move_lines: An account.move.line recordset.
:param st_line: An account.bank.statement.line record.(possibly empty, if performing manual reconciliation)
:param residual_balance: The residual balance of the statement line.
:return: A list of dict representing move.lines to be created corresponding to the write-off lines.
'''
self.ensure_one()
@@ -259,42 +257,39 @@ class AccountReconcileModel(models.Model):
if self.rule_type == 'invoice_matching' and (not self.match_total_amount or (self.match_total_amount_param == 100)):
return []
line_residual = st_line.currency_id and st_line.amount_currency or st_line.amount
line_currency = st_line.currency_id or st_line.journal_id.currency_id or self.company_id.currency_id
lines_vals_list = []
total_residual = move_lines and sum(aml.currency_id and aml.amount_residual_currency or aml.amount_residual for aml in move_lines) or 0.0
balance = total_residual - line_residual
new_aml_dicts = []
if residual_balance is None:
residual_balance = balance
for line in self.line_ids:
if not line.account_id or float_is_zero(residual_balance, precision_rounding=line_currency.rounding):
continue
if not line.account_id or st_line.company_currency_id.is_zero(residual_balance):
return []
if line.amount_type == 'percentage':
line_balance = residual_balance * (line.amount / 100.0)
balance = residual_balance * (line.amount / 100.0)
elif line.amount_type == "regex":
match = re.search(line.amount_string, st_line.name)
match = re.search(line.amount_string, st_line.payment_ref)
if match:
line_balance = copysign(float(re.sub(r'\D' + self.decimal_separator, '', match.group(1)).replace(self.decimal_separator, '.')) * (1 if balance > 0.0 else -1), balance)
sign = 1 if residual_balance > 0.0 else -1
extracted_balance = float(re.sub(r'\D' + self.decimal_separator, '', match.group(1)).replace(self.decimal_separator, '.'))
balance = copysign(extracted_balance * sign, residual_balance)
else:
line_balance = 0
balance = 0
else:
line_balance = line.amount * (1 if residual_balance > 0.0 else -1)
balance = line.amount * (1 if residual_balance > 0.0 else -1)
writeoff_line = {
'name': line.label or st_line.name,
'name': line.label or st_line.payment_ref,
'balance': balance,
'account_id': line.account_id.id,
'currency_id': False,
'analytic_account_id': line.analytic_account_id.id,
'analytic_tag_ids': [(6, 0, line.analytic_tag_ids.ids)],
'debit': line_balance > 0 and line_balance or 0,
'credit': line_balance < 0 and -line_balance or 0,
'reconcile_model_id': self.id,
}
new_aml_dicts.append(writeoff_line)
lines_vals_list.append(writeoff_line)
residual_balance -= balance
residual_balance -= line_balance
if line.tax_ids:
writeoff_line['tax_ids'] = [(6, None, line.tax_ids.ids)]
tax = line.tax_ids
@@ -302,63 +297,59 @@ class AccountReconcileModel(models.Model):
# only allow to set the force_tax_included field if we have one tax selected
if line.force_tax_included:
tax = tax[0].with_context(force_price_include=True)
new_aml_dicts += self._get_taxes_move_lines_dict(tax, writeoff_line)
lines_vals_list += self._get_taxes_move_lines_dict(tax, writeoff_line)
return new_aml_dicts
return lines_vals_list
def _prepare_reconciliation(self, st_line, move_lines=None, partner=None):
''' Reconcile the statement line with some move lines using this reconciliation model.
:param st_line: An account.bank.statement.line record.
:param move_lines: An account.move.line recordset.
:param partner_id: An optional res.partner record. If not set, st_line.partner_id will be used.
:return: Counterpart account.moves.
def _prepare_reconciliation(self, st_line, aml_ids=[], partner=None):
''' Prepare the reconciliation of the statement line with some counterpart line but
also with some auto-generated write-off lines.
The complexity of this method comes from the fact the reconciliation will be soft meaning
it will be done only if the reconciliation will not trigger an error.
For example, the reconciliation will be skipped if we need to create an open balance but we
don't have a partner to get the receivable/payable account.
This method works in two major steps. First, simulate the reconciliation of the account.move.line.
Then, add some write-off lines depending the rule's fields.
:param st_line: An account.bank.statement.line record.
:param aml_ids: The ids of some account.move.line to reconcile.
:param partner: An optional res.partner record. If not specified, fallback on the statement line's partner.
:return: A list of dictionary to be passed to the account.bank.statement.line's 'reconcile' method.
'''
self.ensure_one()
liquidity_lines, suspense_lines, other_lines = st_line._seek_for_lines()
# Create counterpart_aml_dicts + payment_aml_rec.
counterpart_aml_dicts = []
payment_aml_rec = self.env['account.move.line']
if move_lines:
for aml in move_lines:
if aml.account_id.internal_type == 'liquidity':
payment_aml_rec |= aml
else:
amount = aml.currency_id and aml.amount_residual_currency or aml.amount_residual
counterpart_aml_dicts.append({
'name': aml.name if aml.name != '/' else aml.move_id.name,
'debit': amount < 0 and -amount or 0,
'credit': amount > 0 and amount or 0,
'move_line': aml,
})
if st_line.to_check:
residual_balance = -liquidity_lines.balance
elif suspense_lines.account_id.reconcile:
residual_balance = sum(suspense_lines.mapped('amount_residual'))
else:
residual_balance = sum(suspense_lines.mapped('balance'))
# Create new_aml_dicts.
new_aml_dicts = self._get_write_off_move_lines_dict(st_line, move_lines=move_lines)
partner = partner or st_line.partner_id
lines_vals_list = [{'id': aml_id} for aml_id in aml_ids]
reconciliation_overview, open_balance_vals = st_line._prepare_reconciliation(lines_vals_list)
line_residual = st_line.currency_id and st_line.amount_currency or st_line.amount
line_currency = st_line.currency_id or st_line.journal_id.currency_id or st_line.company_id.currency_id
total_residual = move_lines and sum(aml.currency_id and aml.amount_residual_currency or aml.amount_residual for aml in move_lines) or 0.0
total_residual -= sum(aml['debit'] - aml['credit'] for aml in new_aml_dicts)
for reconciliation_vals in reconciliation_overview:
residual_balance -= reconciliation_vals['line_vals']['debit'] - reconciliation_vals['line_vals']['credit']
# Create open_balance_dict
open_balance_dict = None
if float_compare(line_residual, total_residual, precision_rounding=line_currency.rounding) != 0:
if not partner and not st_line.partner_id:
open_balance_dict = False
writeoff_vals_list = self._get_write_off_move_lines_dict(st_line, residual_balance)
for line_vals in writeoff_vals_list:
residual_balance -= st_line.company_currency_id.round(line_vals['balance'])
# Check we have enough information to create an open balance.
if not st_line.company_currency_id.is_zero(residual_balance):
if st_line.amount > 0:
open_balance_account = partner.property_account_receivable_id
else:
balance = total_residual - line_residual
partner = (partner or st_line.partner_id).with_company(st_line.company_id)
open_balance_dict = {
'name': '%s : %s' % (st_line.name, _('Open Balance')),
'account_id': balance < 0 and partner.property_account_payable_id.id or partner.property_account_receivable_id.id,
'debit': balance > 0 and balance or 0,
'credit': balance < 0 and -balance or 0,
}
return {
'counterpart_aml_dicts': counterpart_aml_dicts,
'payment_aml_rec': payment_aml_rec,
'new_aml_dicts': new_aml_dicts,
'open_balance_dict': open_balance_dict
}
open_balance_account = partner.property_account_payable_id
if not open_balance_account:
return []
return lines_vals_list + writeoff_vals_list
####################################################
# RECONCILIATION CRITERIA
@@ -369,7 +360,7 @@ class AccountReconcileModel(models.Model):
rule = self
# Filter on journals.
if rule.match_journal_ids:
query += ' AND st_line.journal_id IN %s'
query += ' AND st_line_move.journal_id IN %s'
params += [tuple(rule.match_journal_ids.ids)]
# Filter on amount nature.
@@ -393,15 +384,15 @@ class AccountReconcileModel(models.Model):
params += [rule.match_amount_min, rule.match_amount_max]
# Filter on label, note and transaction_type
for field, column in [('label', 'name'), ('note', 'note'), ('transaction_type', 'transaction_type')]:
for table, field, column in [('st_line', 'label', 'payment_ref'), ('st_line_move', 'note', 'narration'), ('st_line', 'transaction_type', 'transaction_type')]:
if rule['match_' + field] == 'contains':
query += ' AND st_line.{} ILIKE %s'.format(column)
query += ' AND {}.{} ILIKE %s'.format(table, column)
params += ['%%%s%%' % rule['match_' + field + '_param']]
elif rule['match_' + field] == 'not_contains':
query += ' AND st_line.{} NOT ILIKE %s'.format(column)
query += ' AND {}.{} NOT ILIKE %s'.format(table, column)
params += ['%%%s%%' % rule['match_' + field + '_param']]
elif rule['match_' + field] == 'match_regex':
query += ' AND st_line.{} ~* %s'.format(column)
query += ' AND {}.{} ~* %s'.format(table, column)
params += [rule['match_' + field + '_param']]
# Filter on partners.
@@ -478,11 +469,11 @@ class AccountReconcileModel(models.Model):
substring(REGEXP_REPLACE(aml.name, '[^0-9|^\s]', '', 'g'), '\S(?:.*\S)*') != ''
AND
regexp_split_to_array(substring(REGEXP_REPLACE(aml.name, '[^0-9|^\s]', '', 'g'), '\S(?:.*\S)*'),'\s+')
&& regexp_split_to_array(substring(REGEXP_REPLACE(st_line.name, '[^0-9|^\s]', '', 'g'), '\S(?:.*\S)*'), '\s+')
&& regexp_split_to_array(substring(REGEXP_REPLACE(st_line.payment_ref, '[^0-9|^\s]', '', 'g'), '\S(?:.*\S)*'), '\s+')
)
OR
regexp_split_to_array(substring(REGEXP_REPLACE(move.name, '[^0-9|^\s]', '', 'g'), '\S(?:.*\S)*'),'\s+')
&& regexp_split_to_array(substring(REGEXP_REPLACE(st_line.name, '[^0-9|^\s]', '', 'g'), '\S(?:.*\S)*'), '\s+')
&& regexp_split_to_array(substring(REGEXP_REPLACE(st_line.payment_ref, '[^0-9|^\s]', '', 'g'), '\S(?:.*\S)*'), '\s+')
OR
(
move.ref IS NOT NULL
@@ -490,24 +481,25 @@ class AccountReconcileModel(models.Model):
substring(REGEXP_REPLACE(move.ref, '[^0-9|^\s]', '', 'g'), '\S(?:.*\S)*') != ''
AND
regexp_split_to_array(substring(REGEXP_REPLACE(move.ref, '[^0-9|^\s]', '', 'g'), '\S(?:.*\S)*'),'\s+')
&& regexp_split_to_array(substring(REGEXP_REPLACE(st_line.name, '[^0-9|^\s]', '', 'g'), '\S(?:.*\S)*'), '\s+')
&& regexp_split_to_array(substring(REGEXP_REPLACE(st_line.payment_ref, '[^0-9|^\s]', '', 'g'), '\S(?:.*\S)*'), '\s+')
) AS communication_flag,
-- Determine a matching or not with the statement line communication using the move.invoice_payment_ref.
-- Determine a matching or not with the statement line communication using the move.payment_reference.
(
move.invoice_payment_ref IS NOT NULL
move.payment_reference IS NOT NULL
AND
regexp_replace(move.invoice_payment_ref, '\s+', '', 'g') = regexp_replace(st_line.name, '\s+', '', 'g')
regexp_replace(move.payment_reference, '\s+', '', 'g') = regexp_replace(st_line.payment_ref, '\s+', '', 'g')
) AS payment_reference_flag
FROM account_bank_statement_line st_line
LEFT JOIN account_journal journal ON journal.id = st_line.journal_id
JOIN account_move st_line_move ON st_line_move.id = st_line.move_id
JOIN account_journal journal ON journal.id = st_line_move.journal_id
LEFT JOIN jnl_precision ON jnl_precision.journal_id = journal.id
LEFT JOIN res_company company ON company.id = st_line.company_id
JOIN res_company company ON company.id = st_line_move.company_id
LEFT JOIN partners_table line_partner ON line_partner.line_id = st_line.id
, account_move_line aml
LEFT JOIN account_move move ON move.id = aml.move_id AND move.state = 'posted'
LEFT JOIN account_account account ON account.id = aml.account_id
WHERE st_line.id IN %s
AND aml.company_id = st_line.company_id
AND aml.company_id = st_line_move.company_id
AND move.state = 'posted'
AND (
-- the field match_partner of the rule might enforce the second part of
@@ -534,7 +526,7 @@ class AccountReconcileModel(models.Model):
(
line_partner.partner_id = 0
AND
substring(REGEXP_REPLACE(st_line.name, '[^0-9|^\s]', '', 'g'), '\S(?:.*\S)*') != ''
substring(REGEXP_REPLACE(st_line.payment_ref, '[^0-9|^\s]', '', 'g'), '\S(?:.*\S)*') != ''
AND
(
(
@@ -543,11 +535,11 @@ class AccountReconcileModel(models.Model):
substring(REGEXP_REPLACE(aml.name, '[^0-9|^\s]', '', 'g'), '\S(?:.*\S)*') != ''
AND
regexp_split_to_array(substring(REGEXP_REPLACE(aml.name, '[^0-9|^\s]', '', 'g'), '\S(?:.*\S)*'),'\s+')
&& regexp_split_to_array(substring(REGEXP_REPLACE(st_line.name, '[^0-9|^\s]', '', 'g'), '\S(?:.*\S)*'), '\s+')
&& regexp_split_to_array(substring(REGEXP_REPLACE(st_line.payment_ref, '[^0-9|^\s]', '', 'g'), '\S(?:.*\S)*'), '\s+')
)
OR
regexp_split_to_array(substring(REGEXP_REPLACE(move.name, '[^0-9|^\s]', '', 'g'), '\S(?:.*\S)*'),'\s+')
&& regexp_split_to_array(substring(REGEXP_REPLACE(st_line.name, '[^0-9|^\s]', '', 'g'), '\S(?:.*\S)*'), '\s+')
&& regexp_split_to_array(substring(REGEXP_REPLACE(st_line.payment_ref, '[^0-9|^\s]', '', 'g'), '\S(?:.*\S)*'), '\s+')
OR
(
move.ref IS NOT NULL
@@ -555,41 +547,24 @@ class AccountReconcileModel(models.Model):
substring(REGEXP_REPLACE(move.ref, '[^0-9|^\s]', '', 'g'), '\S(?:.*\S)*') != ''
AND
regexp_split_to_array(substring(REGEXP_REPLACE(move.ref, '[^0-9|^\s]', '', 'g'), '\S(?:.*\S)*'),'\s+')
&& regexp_split_to_array(substring(REGEXP_REPLACE(st_line.name, '[^0-9|^\s]', '', 'g'), '\S(?:.*\S)*'), '\s+')
&& regexp_split_to_array(substring(REGEXP_REPLACE(st_line.payment_ref, '[^0-9|^\s]', '', 'g'), '\S(?:.*\S)*'), '\s+')
)
OR
(
move.invoice_payment_ref IS NOT NULL
move.payment_reference IS NOT NULL
AND
regexp_replace(move.invoice_payment_ref, '\s+', '', 'g') = regexp_replace(st_line.name, '\s+', '', 'g')
regexp_replace(move.payment_reference, '\s+', '', 'g') = regexp_replace(st_line.payment_ref, '\s+', '', 'g')
)
)
)
)
AND
(
(
-- blue lines appearance conditions
aml.account_id IN (journal.default_credit_account_id, journal.default_debit_account_id)
AND aml.statement_id IS NULL
AND (
company.account_bank_reconciliation_start IS NULL
OR
aml.date > company.account_bank_reconciliation_start
)
)
OR
(
-- black lines appearance conditions
account.reconcile IS TRUE
AND aml.reconciled IS FALSE
)
)
AND account.reconcile IS TRUE
AND aml.reconciled IS FALSE
'''
# Filter on the same currency.
if rule.match_same_currency:
query += '''
AND COALESCE(st_line.currency_id, journal.currency_id, company.currency_id) = COALESCE(aml.currency_id, company.currency_id)
AND COALESCE(st_line.foreign_currency_id, st_line_move.currency_id) = COALESCE(aml.currency_id, company.currency_id)
'''
params = [rule.sequence, rule.id, tuple(st_lines.ids)]
@@ -625,9 +600,10 @@ class AccountReconcileModel(models.Model):
%s AS model_id,
st_line.id AS id
FROM account_bank_statement_line st_line
LEFT JOIN account_journal journal ON journal.id = st_line.journal_id
JOIN account_move st_line_move ON st_line_move.id = st_line.move_id
LEFT JOIN account_journal journal ON journal.id = st_line_move.journal_id
LEFT JOIN jnl_precision ON jnl_precision.journal_id = journal.id
LEFT JOIN res_company company ON company.id = st_line.company_id
LEFT JOIN res_company company ON company.id = st_line_move.company_id
LEFT JOIN partners_table line_partner ON line_partner.line_id = st_line.id
WHERE st_line.id IN %s
'''
@@ -803,30 +779,20 @@ class AccountReconcileModel(models.Model):
# Create write-off lines.
move_lines = self.env['account.move.line'].browse(results[line.id]['aml_ids'])
partner = partner_map and partner_map.get(line.id) and self.env['res.partner'].browse(partner_map[line.id])
reconciliation_results = model._prepare_reconciliation(line, move_lines, partner=partner)
lines_vals_list = model._prepare_reconciliation(line, aml_ids=results[line.id]['aml_ids'], partner=partner)
# A write-off must be applied.
if reconciliation_results['new_aml_dicts']:
# A write-off must be applied since there is some 'new' lines to propose.
if not lines_vals_list or any(not line_vals.get('id') for line_vals in lines_vals_list):
results[line.id]['status'] = 'write_off'
# Process auto-reconciliation.
if (first_batch_candidates or second_batch_candidates) and model.auto_reconcile:
# An open balance is needed but no partner has been found.
if reconciliation_results['open_balance_dict'] is False:
break
new_aml_dicts = reconciliation_results['new_aml_dicts']
if reconciliation_results['open_balance_dict']:
new_aml_dicts.append(reconciliation_results['open_balance_dict'])
if lines_vals_list and (first_batch_candidates or second_batch_candidates) and model.auto_reconcile:
if not line.partner_id and partner:
line.partner_id = partner
counterpart_moves = line.process_reconciliation(
counterpart_aml_dicts=reconciliation_results['counterpart_aml_dicts'],
payment_aml_rec=reconciliation_results['payment_aml_rec'],
new_aml_dicts=new_aml_dicts,
)
line.reconcile(lines_vals_list)
results[line.id]['status'] = 'reconciled'
results[line.id]['reconciled_lines'] = counterpart_moves.mapped('line_ids')
results[line.id]['reconciled_lines'] = line.line_ids
# The reconciled move lines are no longer candidates for another rule.
reconciled_amls_ids.update(move_lines.ids)
@@ -840,26 +806,18 @@ class AccountReconcileModel(models.Model):
# Create write-off lines.
partner = partner_map and partner_map.get(line.id) and self.env['res.partner'].browse(partner_map[line.id])
reconciliation_results = model._prepare_reconciliation(line, partner=partner)
lines_vals_list = model._prepare_reconciliation(line, partner=partner)
# An open balance is needed but no partner has been found.
if reconciliation_results['open_balance_dict'] is False:
if not lines_vals_list:
break
# Process auto-reconciliation.
if model.auto_reconcile:
new_aml_dicts = reconciliation_results['new_aml_dicts']
if reconciliation_results['open_balance_dict']:
new_aml_dicts.append(reconciliation_results['open_balance_dict'])
if not line.partner_id and partner:
line.partner_id = partner
counterpart_moves = line.process_reconciliation(
counterpart_aml_dicts=reconciliation_results['counterpart_aml_dicts'],
payment_aml_rec=reconciliation_results['payment_aml_rec'],
new_aml_dicts=new_aml_dicts,
)
line.reconcile(lines_vals_list)
results[line.id]['status'] = 'reconciled'
results[line.id]['reconciled_lines'] = counterpart_moves.mapped('line_ids')
results[line.id]['reconciled_lines'] = line.line_ids
# Break models loop.
break
+17 -11
View File
@@ -105,6 +105,7 @@ class AccountChartTemplate(models.Model):
string="Gain Exchange Rate Account", domain=[('internal_type', '=', 'other'), ('deprecated', '=', False)])
expense_currency_exchange_account_id = fields.Many2one('account.account.template',
string="Loss Exchange Rate Account", domain=[('internal_type', '=', 'other'), ('deprecated', '=', False)])
account_journal_suspense_account_id = fields.Many2one('account.account.template', string='Journal Suspense Account')
default_cash_difference_income_account_id = fields.Many2one('account.account.template', string="Cash Difference Income Account")
default_cash_difference_expense_account_id = fields.Many2one('account.account.template', string="Cash Difference Expense Account")
default_pos_receivable_account_id = fields.Many2one('account.account.template', string="PoS receivable account")
@@ -122,14 +123,14 @@ class AccountChartTemplate(models.Model):
property_advance_tax_payment_account_id = fields.Many2one('account.account.template', string="Advance tax payment account")
@api.model
def _prepare_transfer_account_template(self):
def _prepare_transfer_account_template(self, prefix=None):
''' Prepare values to create the transfer account that is an intermediary account used when moving money
from a liquidity account to another.
:return: A dictionary of values to create a new account.account.
'''
digits = self.code_digits
prefix = self.transfer_account_code_prefix or ''
prefix = prefix or self.transfer_account_code_prefix or ''
# Flatten the hierarchy of chart templates.
chart_template = self
chart_templates = self
@@ -154,6 +155,15 @@ class AccountChartTemplate(models.Model):
'chart_template_id': self.id,
}
@api.model
def _create_liquidity_journal_suspense_account(self, company, code_digits):
return self.env['account.account'].create({
'name': _("Bank Suspense Account"),
'code': self.env['account.account']._search_new_account_code(company, code_digits, company.transfer_account_code_prefix or ''),
'user_type_id': self.env.ref('account.data_account_type_current_assets').id,
'company_id': company.id,
})
def try_loading(self, company=False):
""" Installs this chart of accounts for the current company if not chart
of accounts had been created for it yet.
@@ -239,8 +249,12 @@ class AccountChartTemplate(models.Model):
company.write({
'default_cash_difference_income_account_id': acc_template_ref.get(self.default_cash_difference_income_account_id.id, False),
'default_cash_difference_expense_account_id': acc_template_ref.get(self.default_cash_difference_expense_account_id.id, False),
'account_journal_suspense_account_id': acc_template_ref.get(self.account_journal_suspense_account_id.id),
})
if not company.account_journal_suspense_account_id:
company.account_journal_suspense_account_id = self._create_liquidity_journal_suspense_account(company, self.code_digits)
# Set default PoS receivable account in company
default_pos_receivable = self.default_pos_receivable_account_id.id
if not default_pos_receivable and self.parent_id:
@@ -333,19 +347,11 @@ class AccountChartTemplate(models.Model):
'type': acc['account_type'],
'company_id': company.id,
'currency_id': acc.get('currency_id', self.env['res.currency']).id,
'sequence': 10
'sequence': 10,
})
return bank_journals
def get_countries_posting_at_bank_rec(self):
""" Returns the list of the country codes of the countries for which, by default,
payments made on bank journals should be creating draft account.move objects,
which get in turn posted when their payment gets reconciled with a bank statement line.
This function is an extension hook for localization modules.
"""
return []
@api.model
def _get_default_bank_journals_data(self):
""" Returns the data needed to create the default bank journals when
+2 -4
View File
@@ -45,6 +45,7 @@ class ResCompany(models.Model):
cash_account_code_prefix = fields.Char(string='Prefix of the cash accounts')
default_cash_difference_income_account_id = fields.Many2one('account.account', string="Cash Difference Income Account")
default_cash_difference_expense_account_id = fields.Many2one('account.account', string="Cash Difference Expense Account")
account_journal_suspense_account_id = fields.Many2one('account.account', string='Journal Suspense Account')
transfer_account_code_prefix = fields.Char(string='Prefix of the transfer accounts')
account_sale_tax_id = fields.Many2one('account.tax', string="Default Sale Tax")
account_purchase_tax_id = fields.Many2one('account.tax', string="Default Purchase Tax")
@@ -64,9 +65,6 @@ class ResCompany(models.Model):
property_stock_valuation_account_id = fields.Many2one('account.account', string="Account Template for Stock Valuation")
bank_journal_ids = fields.One2many('account.journal', 'company_id', domain=[('type', '=', 'bank')], string='Bank Journals')
tax_exigibility = fields.Boolean(string='Use Cash Basis')
account_bank_reconciliation_start = fields.Date(string="Bank Reconciliation Threshold", help="""The bank reconciliation widget won't ask to reconcile payments older than this date.
This is useful if you install accounting after having used invoicing for some time and
don't want to reconcile all the past payments with bank statements.""")
incoterm_id = fields.Many2one('account.incoterms', string='Default incoterm',
help='International Commercial Terms are a series of predefined commercial terms used in international transactions.')
@@ -456,7 +454,7 @@ class ResCompany(models.Model):
raise RedirectWarning(msg, action.id, _("Go to the journal configuration"))
sample_invoice = self.env['account.move'].with_context(default_move_type='out_invoice', default_journal_id=journal.id).create({
'invoice_payment_ref': _('Sample invoice'),
'payment_reference': _('Sample invoice'),
'partner_id': partner.id,
'invoice_line_ids': [
(0, 0, {
@@ -74,11 +74,6 @@ class ResConfigSettings(models.TransientModel):
module_snailmail_account = fields.Boolean(string="Snailmail")
tax_exigibility = fields.Boolean(string='Cash Basis', related='company_id.tax_exigibility', readonly=False)
tax_cash_basis_journal_id = fields.Many2one('account.journal', related='company_id.tax_cash_basis_journal_id', string="Tax Cash Basis Journal", readonly=False)
account_bank_reconciliation_start = fields.Date(string="Bank Reconciliation Threshold",
related='company_id.account_bank_reconciliation_start', readonly=False,
help="""The bank reconciliation widget won't ask to reconcile payments older than this date.
This is useful if you install accounting after having used invoicing for some time and
don't want to reconcile all the past payments with bank statements.""")
qr_code = fields.Boolean(string='Display SEPA QR-code', related='company_id.qr_code', readonly=False)
invoice_is_print = fields.Boolean(string='Print', related='company_id.invoice_is_print', readonly=False)
@@ -55,7 +55,7 @@ class AccountInvoiceReport(models.Model):
_depends = {
'account.move': [
'name', 'state', 'move_type', 'partner_id', 'invoice_user_id', 'fiscal_position_id',
'invoice_date', 'invoice_date_due', 'invoice_payment_term_id', 'invoice_partner_bank_id',
'invoice_date', 'invoice_date_due', 'invoice_payment_term_id', 'partner_bank_id',
],
'account.move.line': [
'quantity', 'price_subtotal', 'amount_residual', 'balance', 'amount_currency',
@@ -17,7 +17,7 @@
<a title="assign to invoice" role="button" class="oe_form_field btn btn-link outstanding_credit_assign" t-att-data-id="line.id" style="margin-right: 10px;" href="#" data-toggle="tooltip">Add</a>
</td>
<td style="max-width: 30em;">
<div class="oe_form_field" style="margin-right: 30px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap;" t-att-title="line.payment_date" data-toggle="tooltip"><t t-esc="line.journal_name"></t></div>
<div class="oe_form_field" style="margin-right: 30px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap;" t-att-title="line.date" data-toggle="tooltip"><t t-esc="line.journal_name"></t></div>
</td>
</t>
<t t-if="!outstanding">
+2 -4
View File
@@ -10,15 +10,13 @@ from . import test_account_account
from . import test_account_tax
from . import test_account_analytic
from . import test_account_payment
from . import test_account_bank_statement
from . import test_account_move_partner_count
from . import test_account_move_rounding
from . import test_account_invoice_report
from . import test_account_journal_dashboard
from . import test_account_bank_statement
from . import test_bank_statement_reconciliation
from . import test_control_journal_account
from . import test_fiscal_position
from . import test_payment
from . import test_reconciliation
from . import test_search
from . import test_settings
@@ -31,6 +29,6 @@ from . import test_reconciliation_matching_rules
from . import test_account_onboarding
from . import test_portal_attachment
from . import test_tax_report
from . import test_reconciliation_heavy_load
from . import test_transfer_wizard
from . import test_account_incoming_supplier_invoice
from . import test_account_payment_register
+64 -38
View File
@@ -133,6 +133,12 @@ class AccountTestCommon(SavepointCase):
'name': 'Opening Expense - (test)',
'user_type_id': cls.env.ref('account.data_account_type_expenses').id,
})
cls.sus = cls.env['account.account'].create({
'name': 'Suspense Account - (test)',
'code': 'X1115',
'user_type_id': cls.env.ref('account.data_account_type_current_assets').id,
})
cls.company.account_journal_suspense_account_id = cls.sus
# Profit and Loss
cls.income_fx_income = cls.env['account.account'].create({
@@ -238,16 +244,12 @@ class AccountTestCommon(SavepointCase):
'name': 'Vendor Bills - Test',
'code': 'TEXJ',
'type': 'purchase',
'default_credit_account_id': cls.a_expense.id,
'default_debit_account_id': cls.a_expense.id,
'refund_sequence': True,
})
cls.bank_journal = cls.env['account.journal'].create({
'name': 'Bank - Test',
'code': 'TBNK',
'type': 'bank',
'default_credit_account_id': cls.bnk.id,
'default_debit_account_id': cls.bnk.id,
})
cls.cash_journal = cls.env['account.journal'].create({
'name': 'Cash - Test',
@@ -367,6 +369,12 @@ class AccountTestNoChartCommon(SavepointCaseWithUserDemo):
'code': 'SJT0',
})
cls.general_journal0 = cls.env['account.journal'].create({
'name': 'General Journal',
'type': 'general',
'code': 'GJT0',
})
@classmethod
def setUpAdditionalAccounts(cls):
""" Set up some addionnal accounts: expenses, revenue, ... """
@@ -415,6 +423,12 @@ class AccountTestNoChartCommon(SavepointCaseWithUserDemo):
'type': 'general',
'company_id': cls.env.user.company_id.id,
})
cls.journal_bank = cls.env['account.journal'].create({
'name': 'Bank Journal - Test',
'code': 'AJ-BANK',
'type': 'bank',
'company_id': cls.env.user.company_id.id,
})
@classmethod
def setUpUsers(cls):
@@ -586,10 +600,24 @@ class AccountTestNoChartCommonMultiCompany(AccountTestNoChartCommon):
class AccountTestInvoicingCommon(SavepointCase):
@classmethod
def setUpClass(cls):
def copy_account(cls, account):
suffix_nb = 1
while True:
new_code = '%s (%s)' % (account.code, suffix_nb)
if account.search_count([('company_id', '=', account.company_id.id), ('code', '=', new_code)]):
suffix_nb += 1
else:
return account.copy(default={'code': new_code})
@classmethod
def setUpClass(cls, chart_template_ref=None):
super(AccountTestInvoicingCommon, cls).setUpClass()
chart_template = cls.env.user.company_id.chart_template_id
chart_template = None
if chart_template_ref:
chart_template = cls.env.ref(chart_template_ref)
if not chart_template:
chart_template = cls.env.user.company_id.chart_template_id
if not chart_template:
chart_template = cls.env.ref('l10n_generic_coa.configurable_chart_template', raise_if_not_found=False)
if not chart_template:
@@ -634,8 +662,8 @@ class AccountTestInvoicingCommon(SavepointCase):
'uom_id': cls.env.ref('uom.product_uom_dozen').id,
'lst_price': 200.0,
'standard_price': 160.0,
'property_account_income_id': cls.company_data['default_account_revenue'].copy().id,
'property_account_expense_id': cls.company_data['default_account_expense'].copy().id,
'property_account_income_id': cls.copy_account(cls.company_data['default_account_revenue']).id,
'property_account_expense_id': cls.copy_account(cls.company_data['default_account_expense']).id,
'taxes_id': [(6, 0, (cls.tax_sale_a + cls.tax_sale_b).ids)],
'supplier_taxes_id': [(6, 0, (cls.tax_purchase_a + cls.tax_purchase_b).ids)],
})
@@ -793,29 +821,30 @@ class AccountTestInvoicingCommon(SavepointCase):
}
@classmethod
def setup_multi_currency_data(cls):
gold_currency = cls.env['res.currency'].create({
def setup_multi_currency_data(cls, default_values={}, rate2016=3.0, rate2017=2.0):
foreign_currency = cls.env['res.currency'].create({
'name': 'Gold Coin',
'symbol': '☺',
'rounding': 0.001,
'position': 'after',
'currency_unit_label': 'Gold',
'currency_subunit_label': 'Silver',
**default_values,
})
rate1 = cls.env['res.currency.rate'].create({
'name': '2016-01-01',
'rate': 3.0,
'currency_id': gold_currency.id,
'rate': rate2016,
'currency_id': foreign_currency.id,
'company_id': cls.env.company.id,
})
rate2 = cls.env['res.currency.rate'].create({
'name': '2017-01-01',
'rate': 2.0,
'currency_id': gold_currency.id,
'rate': rate2017,
'currency_id': foreign_currency.id,
'company_id': cls.env.company.id,
})
return {
'currency': gold_currency,
'currency': foreign_currency,
'rates': rate1 + rate2,
}
@@ -931,9 +960,11 @@ class TestAccountReconciliationCommon(AccountTestCommon):
super(TestAccountReconciliationCommon, cls).setUpClass()
cls.company = cls.env['res.company'].create({
'name': 'A test company',
'currency_id': cls.env.ref('base.EUR').id
'currency_id': cls.env.ref('base.EUR').id,
})
cls.env.user.company_id = cls.company
cls.env.user.groups_id |= cls.env.ref('account.group_account_user')
# Generate minimal data for my new company
cls.create_accounting_minimal_data()
@@ -1116,41 +1147,36 @@ class TestAccountReconciliationCommon(AccountTestCommon):
auto_validate=True
)
def make_payment(self, invoice_record, bank_journal, amount=0.0, amount_currency=0.0, currency_id=None):
def make_payment(self, invoice_record, bank_journal, amount=0.0, amount_currency=0.0, currency_id=None, reconcile_param=[]):
bank_stmt = self.acc_bank_stmt_model.create({
'journal_id': bank_journal.id,
'date': time.strftime('%Y') + '-07-15',
'name': 'payment' + invoice_record.name
'name': 'payment' + invoice_record.name,
'line_ids': [(0, 0, {
'payment_ref': 'payment',
'partner_id': self.partner_agrolait_id,
'amount': amount,
'amount_currency': amount_currency,
'foreign_currency_id': currency_id,
})],
})
bank_stmt.button_post()
bank_stmt_line = self.acc_bank_stmt_line_model.create({'name': 'payment',
'statement_id': bank_stmt.id,
'partner_id': self.partner_agrolait_id,
'amount': amount,
'amount_currency': amount_currency,
'currency_id': currency_id,
'date': time.strftime('%Y') + '-07-15',
})
line_id = invoice_record.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
amount_in_widget = currency_id and amount_currency or amount
bank_stmt_line.process_reconciliation(counterpart_aml_dicts=[{
'move_line': line_id,
'debit': amount_in_widget < 0 and -amount_in_widget or 0.0,
'credit': amount_in_widget > 0 and amount_in_widget or 0.0,
'name': line_id.name,
}])
bank_stmt.line_ids[0].reconcile(reconcile_param)
return bank_stmt
def make_customer_and_supplier_flows(self, invoice_currency_id, invoice_amount, bank_journal, amount, amount_currency, transaction_currency_id):
#we create an invoice in given invoice_currency
invoice_record = self.create_invoice(type='out_invoice', invoice_amount=invoice_amount, currency_id=invoice_currency_id)
#we encode a payment on it, on the given bank_journal with amount, amount_currency and transaction_currency given
bank_stmt = self.make_payment(invoice_record, bank_journal, amount=amount, amount_currency=amount_currency, currency_id=transaction_currency_id)
customer_move_lines = bank_stmt.move_line_ids
line = invoice_record.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
bank_stmt = self.make_payment(invoice_record, bank_journal, amount=amount, amount_currency=amount_currency, currency_id=transaction_currency_id, reconcile_param=[{'id': line.id}])
customer_move_lines = bank_stmt.line_ids.line_ids
#we create a supplier bill in given invoice_currency
invoice_record = self.create_invoice(type='in_invoice', invoice_amount=invoice_amount, currency_id=invoice_currency_id)
#we encode a payment on it, on the given bank_journal with amount, amount_currency and transaction_currency given
bank_stmt = self.make_payment(invoice_record, bank_journal, amount=-amount, amount_currency=-amount_currency, currency_id=transaction_currency_id)
supplier_move_lines = bank_stmt.move_line_ids
line = invoice_record.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
bank_stmt = self.make_payment(invoice_record, bank_journal, amount=-amount, amount_currency=-amount_currency, currency_id=transaction_currency_id, reconcile_param=[{'id': line.id}])
supplier_move_lines = bank_stmt.line_ids.line_ids
return customer_move_lines, supplier_move_lines
File diff suppressed because it is too large Load Diff
+25 -19
View File
@@ -1,30 +1,36 @@
# -*- coding: utf-8 -*-
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.tests import tagged
from odoo.exceptions import UserError
from odoo.exceptions import UserError, ValidationError
@tagged('post_install', '-at_install')
class TestAccountJournal(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
def test_constraint_currency_consistency_with_accounts(self):
''' The accounts linked to a bank/cash journal must share the same foreign currency
if specified.
'''
journal_bank = self.company_data['default_journal_bank']
journal_bank.currency_id = self.currency_data['currency']
# Create another company.
cls.company_data_2 = cls.setup_company_data('company_2_data')
# Try to set a different currency on the 'debit' account.
with self.assertRaises(ValidationError), self.cr.savepoint():
journal_bank.default_debit_account_id.currency_id = self.company_data['currency']
# By default, tests are run with the current user set on the first company.
cls.env.user.company_id = cls.company_data['company']
def test_constraint_shared_accounts(self):
''' Ensure the bank/outstanding accounts are not shared between multiple journals. '''
journal_bank = self.company_data['default_journal_bank']
def test_changing_journal_company(self):
''' Ensure you can't change the company of an account.journal if there are some journal entries '''
self.env['account.move'].create({
'move_type': 'entry',
'date': '2019-01-01',
'journal_id': self.company_data['default_journal_sale'].id,
})
with self.assertRaises(UserError), self.cr.savepoint():
self.company_data['default_journal_sale'].company_id = self.company_data_2['company']
account_fields = (
'default_debit_account_id',
'default_credit_account_id',
'payment_debit_account_id',
'payment_credit_account_id',
)
for account_field in account_fields:
with self.assertRaises(ValidationError), self.cr.savepoint():
journal_bank.copy(default={
'name': 'test_constraint_shared_accounts %s' % account_field,
account_field: journal_bank[account_field].id,
})
@@ -280,6 +280,7 @@ class TestAccountMove(AccountTestInvoicingCommon):
# lines[1] = 'move 1 counterpart line'
# lines[2] = 'move 1 receivable line'
# lines[3] = 'move 2 counterpart line'
draft_moves.post()
lines = draft_moves.mapped('line_ids').sorted('balance')
(lines[0] + lines[2]).reconcile()
@@ -121,7 +121,7 @@ class TestAccountMoveInInvoiceOnchanges(AccountTestInvoicingCommon):
'journal_id': cls.company_data['default_journal_purchase'].id,
'date': fields.Date.from_string('2019-01-01'),
'fiscal_position_id': False,
'invoice_payment_ref': '',
'payment_reference': '',
'invoice_payment_term_id': cls.pay_terms_a.id,
'amount_untaxed': 960.0,
'amount_tax': 168.0,
@@ -601,7 +601,7 @@ class TestAccountMoveInInvoiceOnchanges(AccountTestInvoicingCommon):
def test_in_invoice_line_onchange_partner_1(self):
move_form = Form(self.invoice)
move_form.partner_id = self.partner_b
move_form.invoice_payment_ref = 'turlututu'
move_form.payment_reference = 'turlututu'
move_form.save()
self.assertInvoiceValues(self.invoice, [
@@ -645,7 +645,7 @@ class TestAccountMoveInInvoiceOnchanges(AccountTestInvoicingCommon):
], {
**self.move_vals,
'partner_id': self.partner_b.id,
'invoice_payment_ref': 'turlututu',
'payment_reference': 'turlututu',
'fiscal_position_id': self.fiscal_pos_a.id,
'invoice_payment_term_id': self.pay_terms_b.id,
'amount_untaxed': 960.0,
@@ -706,7 +706,7 @@ class TestAccountMoveInInvoiceOnchanges(AccountTestInvoicingCommon):
], {
**self.move_vals,
'partner_id': self.partner_b.id,
'invoice_payment_ref': 'turlututu',
'payment_reference': 'turlututu',
'fiscal_position_id': self.fiscal_pos_a.id,
'invoice_payment_term_id': self.pay_terms_b.id,
'amount_untaxed': 960.0,
@@ -121,7 +121,7 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
'journal_id': cls.company_data['default_journal_purchase'].id,
'date': fields.Date.from_string('2019-01-01'),
'fiscal_position_id': False,
'invoice_payment_ref': '',
'payment_reference': '',
'invoice_payment_term_id': cls.pay_terms_a.id,
'amount_untaxed': 960.0,
'amount_tax': 168.0,
@@ -313,7 +313,7 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
def test_in_refund_line_onchange_partner_1(self):
move_form = Form(self.invoice)
move_form.partner_id = self.partner_b
move_form.invoice_payment_ref = 'turlututu'
move_form.payment_reference = 'turlututu'
move_form.save()
self.assertInvoiceValues(self.invoice, [
@@ -357,7 +357,7 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
], {
**self.move_vals,
'partner_id': self.partner_b.id,
'invoice_payment_ref': 'turlututu',
'payment_reference': 'turlututu',
'fiscal_position_id': self.fiscal_pos_a.id,
'invoice_payment_term_id': self.pay_terms_b.id,
'amount_untaxed': 960.0,
@@ -418,7 +418,7 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
], {
**self.move_vals,
'partner_id': self.partner_b.id,
'invoice_payment_ref': 'turlututu',
'payment_reference': 'turlututu',
'fiscal_position_id': self.fiscal_pos_a.id,
'invoice_payment_term_id': self.pay_terms_b.id,
'amount_untaxed': 960.0,
@@ -123,7 +123,7 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon):
'journal_id': cls.company_data['default_journal_sale'].id,
'date': fields.Date.from_string('2019-01-01'),
'fiscal_position_id': False,
'invoice_payment_ref': '',
'payment_reference': '',
'invoice_payment_term_id': cls.pay_terms_a.id,
'amount_untaxed': 1200.0,
'amount_tax': 210.0,
@@ -603,7 +603,7 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon):
def test_out_invoice_line_onchange_partner_1(self):
move_form = Form(self.invoice)
move_form.partner_id = self.partner_b
move_form.invoice_payment_ref = 'turlututu'
move_form.payment_reference = 'turlututu'
move_form.save()
self.assertInvoiceValues(self.invoice, [
@@ -647,7 +647,7 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon):
], {
**self.move_vals,
'partner_id': self.partner_b.id,
'invoice_payment_ref': 'turlututu',
'payment_reference': 'turlututu',
'fiscal_position_id': self.fiscal_pos_a.id,
'invoice_payment_term_id': self.pay_terms_b.id,
'amount_untaxed': 1200.0,
@@ -708,7 +708,7 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon):
], {
**self.move_vals,
'partner_id': self.partner_b.id,
'invoice_payment_ref': 'turlututu',
'payment_reference': 'turlututu',
'fiscal_position_id': self.fiscal_pos_a.id,
'invoice_payment_term_id': self.pay_terms_b.id,
'amount_untaxed': 1200.0,
@@ -2006,7 +2006,7 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon):
},
], {
**self.move_vals,
'invoice_payment_ref': move.name,
'payment_reference': move.name,
'currency_id': self.currency_data['currency'].id,
'date': frozen_today,
'invoice_date': frozen_today,
@@ -2112,7 +2112,7 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon):
},
], {
**self.move_vals,
'invoice_payment_ref': move.name,
'payment_reference': move.name,
'currency_id': self.currency_data['currency'].id,
'date': fields.Date.from_string('2017-01-01'),
'amount_untaxed': 1200.0,
@@ -2392,7 +2392,7 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon):
**self.move_vals,
'currency_id': self.currency_data['currency'].id,
'date': fields.Date.from_string('2017-01-01'),
'invoice_payment_ref': 'INV/2017/01/0001',
'payment_reference': 'INV/2017/01/0001',
})
accrual_lines = self.env['account.move'].browse(wizard_res['domain'][0][2]).line_ids.sorted('date')
@@ -121,7 +121,7 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
'journal_id': cls.company_data['default_journal_sale'].id,
'date': fields.Date.from_string('2019-01-01'),
'fiscal_position_id': False,
'invoice_payment_ref': '',
'payment_reference': '',
'invoice_payment_term_id': cls.pay_terms_a.id,
'amount_untaxed': 1200.0,
'amount_tax': 210.0,
@@ -313,7 +313,7 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
def test_out_refund_line_onchange_partner_1(self):
move_form = Form(self.invoice)
move_form.partner_id = self.partner_b
move_form.invoice_payment_ref = 'turlututu'
move_form.payment_reference = 'turlututu'
move_form.save()
self.assertInvoiceValues(self.invoice, [
@@ -357,7 +357,7 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
], {
**self.move_vals,
'partner_id': self.partner_b.id,
'invoice_payment_ref': 'turlututu',
'payment_reference': 'turlututu',
'fiscal_position_id': self.fiscal_pos_a.id,
'invoice_payment_term_id': self.pay_terms_b.id,
'amount_untaxed': 1200.0,
@@ -418,7 +418,7 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
], {
**self.move_vals,
'partner_id': self.partner_b.id,
'invoice_payment_ref': 'turlututu',
'payment_reference': 'turlututu',
'fiscal_position_id': self.fiscal_pos_a.id,
'invoice_payment_term_id': self.pay_terms_b.id,
'amount_untaxed': 1200.0,
+429 -158
View File
@@ -2,221 +2,492 @@
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.tests import tagged, new_test_user
from odoo.tests.common import Form
from odoo import fields
@tagged('post_install', '-at_install')
class TestAccountMovePayment(AccountTestInvoicingCommon):
class TestAccountPayment(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls):
super(TestAccountMovePayment, cls).setUpClass()
super().setUpClass()
cls.bank_journal = cls.company_data['default_journal_bank']
cls.bank_journal.default_credit_account_id = cls.bank_journal.default_debit_account_id.copy()
cls.payment_debit_account_id = cls.copy_account(cls.company_data['default_journal_bank'].payment_debit_account_id)
cls.payment_credit_account_id = cls.copy_account(cls.company_data['default_journal_bank'].payment_credit_account_id)
cls.inbound_payment = cls._init_payment(cls, 'inbound', cls.partner_a, partner_type='customer')
cls.outbound_payment = cls._init_payment(cls, 'outbound', cls.partner_a, partner_type='supplier')
cls.inbound_internal_transfer = cls._init_payment(cls, 'inbound', cls.company_data['company'].partner_id)
cls.outbound_internal_transfer = cls._init_payment(cls, 'outbound', cls.company_data['company'].partner_id)
cls.company_data['default_journal_bank'].write({
'payment_debit_account_id': cls.payment_debit_account_id.id,
'payment_credit_account_id': cls.payment_credit_account_id.id,
'inbound_payment_method_ids': [(6, 0, cls.env.ref('account.account_payment_method_manual_in').ids)],
'outbound_payment_method_ids': [(6, 0, cls.env.ref('account.account_payment_method_manual_out').ids)],
})
@staticmethod
def _init_payment(cls, payment_type, partner_id, partner_type=None):
payment_form = Form(cls.env['account.payment'])
payment_form.journal_id = cls.bank_journal
payment_form.payment_date = fields.Date.from_string('2019-01-01')
payment_form.amount = 100
payment_form.partner_type = partner_type
payment_form.partner_id = partner_id
payment_form.payment_type = payment_type
return payment_form.save()
def test_payment_move_sync_create_write(self):
copy_receivable = self.copy_account(self.company_data['default_account_receivable'])
def assertPaymentValues(self, payment, expected_lines_values):
self.assertRecordValues(payment.move_line_ids.sorted(lambda line: (line.name, line.balance)), expected_lines_values)
payment = self.env['account.payment'].create({
'amount': 50.0,
'payment_type': 'inbound',
'partner_type': 'customer',
'destination_account_id': copy_receivable.id,
})
def test_inbound_payment_move_1(self):
''' Check created account.move for an account.payment having the 'inbound' type. '''
liquidity_account = self.bank_journal.default_credit_account_id
expected_bank_line = {
'account_id': liquidity_account.id,
'partner_id': self.partner_a.id,
'currency_id': False,
'amount_currency': 0.0,
'debit': 100.0,
expected_payment_values = {
'amount': 50.0,
'payment_type': 'inbound',
'partner_type': 'customer',
'payment_reference': False,
'is_reconciled': False,
'currency_id': self.company_data['currency'].id,
'partner_id': False,
'destination_account_id': copy_receivable.id,
'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
}
expected_move_values = {
'currency_id': self.company_data['currency'].id,
'partner_id': False,
}
expected_liquidity_line = {
'debit': 50.0,
'credit': 0.0,
}
expected_receivable_line = {
'account_id': self.inbound_payment.destination_account_id .id,
'partner_id': self.partner_a.id,
'currency_id': False,
'amount_currency': 0.0,
'currency_id': False,
'account_id': self.payment_debit_account_id.id,
}
expected_counterpart_line = {
'debit': 0.0,
'credit': 100.0,
'credit': 50.0,
'amount_currency': 0.0,
'currency_id': False,
'account_id': copy_receivable.id,
}
# Single currency.
self.inbound_payment.post()
self.assertPaymentValues(self.inbound_payment, [
expected_bank_line,
expected_receivable_line,
self.assertRecordValues(payment, [expected_payment_values])
self.assertRecordValues(payment.move_id, [expected_move_values])
self.assertRecordValues(payment.line_ids.sorted('balance'), [
expected_counterpart_line,
expected_liquidity_line,
])
# Multi currencies: Foreign currency on payment.
# ==== Check editing the account.payment ====
self.inbound_payment.action_draft()
self.inbound_payment.currency_id = self.currency_data['currency']
self.inbound_payment.post()
payment.write({
'partner_type': 'supplier',
'currency_id': self.currency_data['currency'].id,
'partner_id': self.partner_a.id,
})
self.assertPaymentValues(self.inbound_payment, [
self.assertRecordValues(payment, [{
**expected_payment_values,
'partner_type': 'supplier',
'destination_account_id': self.partner_a.property_account_payable_id.id,
'currency_id': self.currency_data['currency'].id,
'partner_id': self.partner_a.id,
}])
self.assertRecordValues(payment.move_id, [{
**expected_move_values,
'currency_id': self.currency_data['currency'].id,
'partner_id': self.partner_a.id,
}])
self.assertRecordValues(payment.line_ids.sorted('balance'), [
{
**expected_bank_line,
**expected_counterpart_line,
'debit': 0.0,
'credit': 25.0,
'amount_currency': -50.0,
'currency_id': self.currency_data['currency'].id,
'amount_currency': 100.0,
'debit': 50.0,
'account_id': self.partner_a.property_account_payable_id.id,
},
{
**expected_receivable_line,
**expected_liquidity_line,
'debit': 25.0,
'credit': 0.0,
'amount_currency': 50.0,
'currency_id': self.currency_data['currency'].id,
'amount_currency': -100.0,
'credit': 50.0,
},
])
self.inbound_payment.currency_id = self.company_data['currency']
# ==== Check editing the account.move.line ====
# Multi currencies: Foreign currency on journal.
liquidity_lines, counterpart_lines, writeoff_lines = payment._seek_for_lines()
payment.move_id.write({
'line_ids': [
(1, counterpart_lines.id, {
'debit': 0.0,
'credit': 75.0,
'amount_currency': 0.0,
'currency_id': False,
'account_id': copy_receivable.id,
'partner_id': self.partner_b.id,
}),
(1, liquidity_lines.id, {
'debit': 100.0,
'credit': 0.0,
'amount_currency': 0.0,
'currency_id': False,
'partner_id': self.partner_b.id,
}),
self.inbound_payment.action_draft()
self.inbound_payment.journal_id.currency_id = self.currency_data['currency']
self.inbound_payment.post()
# Additional write-off:
(0, 0, {
'debit': 0.0,
'credit': 25.0,
'amount_currency': 0.0,
'currency_id': False,
'account_id': self.company_data['default_account_revenue'].id,
'partner_id': self.partner_b.id,
}),
]
})
self.assertPaymentValues(self.inbound_payment, [
self.assertRecordValues(payment, [{
**expected_payment_values,
'amount': 100.0,
'partner_id': self.partner_b.id,
}])
self.assertRecordValues(payment.move_id, [{
**expected_move_values,
'partner_id': self.partner_b.id,
}])
self.assertRecordValues(payment.line_ids.sorted('balance'), [
{
**expected_bank_line,
'currency_id': self.currency_data['currency'].id,
'amount_currency': 200.0,
**expected_counterpart_line,
'debit': 0.0,
'credit': 75.0,
'partner_id': self.partner_b.id,
},
{
'debit': 0.0,
'credit': 25.0,
'amount_currency': 0.0,
'currency_id': False,
'account_id': self.company_data['default_account_revenue'].id,
'partner_id': self.partner_b.id,
},
{
**expected_liquidity_line,
'debit': 100.0,
'credit': 0.0,
'account_id': self.payment_debit_account_id.id,
'partner_id': self.partner_b.id,
},
expected_receivable_line,
])
def test_outbound_payment_move_1(self):
''' Check created account.move for an account.payment having the 'outbound' type. '''
liquidity_account = self.bank_journal.default_debit_account_id
def test_payment_move_sync_onchange(self):
copy_receivable = self.copy_account(self.company_data['default_account_receivable'])
expected_bank_line = {
'account_id': liquidity_account.id,
pay_form = Form(self.env['account.payment'].with_context(default_journal_id=self.company_data['default_journal_bank'].id))
pay_form.amount = 50.0
pay_form.payment_type = 'inbound'
pay_form.partner_type = 'customer'
pay_form.destination_account_id = copy_receivable
payment = pay_form.save()
expected_payment_values = {
'amount': 50.0,
'payment_type': 'inbound',
'partner_type': 'customer',
'payment_reference': False,
'is_reconciled': False,
'currency_id': self.company_data['currency'].id,
'partner_id': False,
'destination_account_id': copy_receivable.id,
'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
}
expected_move_values = {
'currency_id': self.company_data['currency'].id,
'partner_id': False,
}
expected_liquidity_line = {
'debit': 50.0,
'credit': 0.0,
'amount_currency': 0.0,
'currency_id': False,
'account_id': self.payment_debit_account_id.id,
}
expected_counterpart_line = {
'debit': 0.0,
'credit': 50.0,
'amount_currency': 0.0,
'currency_id': False,
'account_id': copy_receivable.id,
}
self.assertRecordValues(payment, [expected_payment_values])
self.assertRecordValues(payment.move_id, [expected_move_values])
self.assertRecordValues(payment.line_ids.sorted('balance'), [
expected_counterpart_line,
expected_liquidity_line,
])
# ==== Check editing the account.payment ====
pay_form = Form(payment)
pay_form.partner_type = 'supplier'
pay_form.currency_id = self.currency_data['currency']
pay_form.partner_id = self.partner_a
payment = pay_form.save()
self.assertRecordValues(payment, [{
**expected_payment_values,
'partner_type': 'supplier',
'destination_account_id': self.partner_a.property_account_payable_id.id,
'currency_id': self.currency_data['currency'].id,
'partner_id': self.partner_a.id,
'currency_id': False,
'amount_currency': 0.0,
'debit': 0.0,
'credit': 100.0,
}
expected_payable_line = {
'account_id': self.outbound_payment.destination_account_id.id,
}])
self.assertRecordValues(payment.move_id, [{
**expected_move_values,
'currency_id': self.currency_data['currency'].id,
'partner_id': self.partner_a.id,
'currency_id': False,
'amount_currency': 0.0,
'debit': 100.0,
'credit': 0.0,
}
# Single currency.
self.outbound_payment.post()
self.assertPaymentValues(self.outbound_payment, [
expected_bank_line,
expected_payable_line,
])
# Multi currencies: Foreign currency on payment.
self.outbound_payment.action_draft()
self.outbound_payment.currency_id = self.currency_data['currency']
self.outbound_payment.post()
self.assertPaymentValues(self.outbound_payment, [
}])
self.assertRecordValues(payment.line_ids.sorted('balance'), [
{
**expected_bank_line,
**expected_counterpart_line,
'debit': 0.0,
'credit': 25.0,
'amount_currency': -50.0,
'currency_id': self.currency_data['currency'].id,
'amount_currency': -100.0,
'credit': 50.0,
'account_id': self.partner_a.property_account_payable_id.id,
},
{
**expected_payable_line,
**expected_liquidity_line,
'debit': 25.0,
'credit': 0.0,
'amount_currency': 50.0,
'currency_id': self.currency_data['currency'].id,
'amount_currency': 100.0,
'debit': 50.0,
},
])
self.outbound_payment.currency_id = self.company_data['currency']
# ==== Check editing the account.move.line ====
# Multi currencies: Foreign currency on journal.
move_form = Form(payment.move_id)
with move_form.line_ids.edit(0) as line_form:
line_form.currency_id = self.env['res.currency']
line_form.amount_currency = 0.0
line_form.debit = 100.0
line_form.partner_id = self.partner_b
with move_form.line_ids.edit(1) as line_form:
line_form.currency_id = self.env['res.currency']
line_form.amount_currency = 0.0
line_form.credit = 75.0
line_form.account_id = copy_receivable
line_form.partner_id = self.partner_b
with move_form.line_ids.new() as line_form:
line_form.currency_id = self.env['res.currency']
line_form.amount_currency = 0.0
line_form.credit = 25.0
line_form.account_id = self.company_data['default_account_revenue']
line_form.partner_id = self.partner_b
move_form.save()
self.outbound_payment.action_draft()
self.outbound_payment.journal_id.currency_id = self.currency_data['currency']
self.outbound_payment.post()
self.assertPaymentValues(self.outbound_payment, [
self.assertRecordValues(payment, [{
**expected_payment_values,
'amount': 100.0,
'partner_id': self.partner_b.id,
}])
self.assertRecordValues(payment.line_ids.sorted('balance'), [
{
**expected_bank_line,
'currency_id': self.currency_data['currency'].id,
'amount_currency': -200.0,
'credit': 100.0,
**expected_counterpart_line,
'debit': 0.0,
'credit': 75.0,
'partner_id': self.partner_b.id,
},
{
'debit': 0.0,
'credit': 25.0,
'amount_currency': 0.0,
'currency_id': False,
'account_id': self.company_data['default_account_revenue'].id,
'partner_id': self.partner_b.id,
},
{
**expected_liquidity_line,
'debit': 100.0,
'credit': 0.0,
'account_id': self.payment_debit_account_id.id,
'partner_id': self.partner_b.id,
},
expected_payable_line,
])
def test_inbound_transfer_payment_1(self):
liquidity_bank_account = self.bank_journal.default_debit_account_id
self.inbound_internal_transfer.post()
transfer_line = {
'account_id': self.company_data['company'].transfer_account_id.id,
def test_internal_transfer(self):
copy_receivable = self.copy_account(self.company_data['default_account_receivable'])
payment = self.env['account.payment'].create({
'amount': 50.0,
'is_internal_transfer': True,
})
expected_payment_values = {
'amount': 50.0,
'payment_type': 'inbound',
'currency_id': self.company_data['currency'].id,
'partner_id': self.company_data['company'].partner_id.id,
'currency_id': False,
'destination_account_id': self.company_data['company'].transfer_account_id.id,
}
expected_move_values = {
'currency_id': self.company_data['currency'].id,
'partner_id': self.company_data['company'].partner_id.id,
}
expected_liquidity_line = {
'debit': 50.0,
'credit': 0.0,
'amount_currency': 0.0,
'currency_id': False,
'account_id': self.payment_debit_account_id.id,
}
expected_counterpart_line = {
'debit': 0.0,
'credit': 100.0,
}
liquidity_bank_line = {
'account_id': self.bank_journal.default_credit_account_id.id,
'partner_id': self.company_data['company'].partner_id.id,
'currency_id': False,
'credit': 50.0,
'amount_currency': 0.0,
'debit': 100.0,
'credit': 0.0,
}
self.assertPaymentValues(self.inbound_internal_transfer, [
transfer_line,
liquidity_bank_line,
])
self.assertTrue(self.inbound_internal_transfer.is_internal_transfer)
def test_outbound_transfer_payment_1(self):
self.outbound_internal_transfer.post()
transfer_line = {
'currency_id': False,
'account_id': self.company_data['company'].transfer_account_id.id,
'partner_id': self.company_data['company'].partner_id.id,
'currency_id': False,
'amount_currency': 0.0,
'debit': 100.0,
'credit': 0.0,
}
liquidity_bank_line = {
'account_id': self.bank_journal.default_debit_account_id.id,
'partner_id': self.company_data['company'].partner_id.id,
'currency_id': False,
'amount_currency': 0.0,
'debit': 0.0,
'credit': 100.0,
}
self.assertPaymentValues(self.outbound_internal_transfer, [
transfer_line,
liquidity_bank_line,
self.assertRecordValues(payment, [expected_payment_values])
self.assertRecordValues(payment.move_id, [expected_move_values])
self.assertRecordValues(payment.line_ids.sorted('balance'), [
expected_counterpart_line,
expected_liquidity_line,
])
self.assertTrue(self.outbound_internal_transfer.is_internal_transfer)
# ==== Check editing the account.payment ====
payment.write({
'partner_type': 'customer',
'partner_id': self.partner_a.id,
'destination_account_id': copy_receivable.id,
})
self.assertRecordValues(payment, [{
**expected_payment_values,
'partner_type': 'customer',
'destination_account_id': copy_receivable.id,
'partner_id': self.partner_a.id,
'is_internal_transfer': False,
}])
self.assertRecordValues(payment.move_id, [{
**expected_move_values,
'partner_id': self.partner_a.id,
}])
self.assertRecordValues(payment.line_ids.sorted('balance'), [
{
**expected_counterpart_line,
'account_id': copy_receivable.id,
},
expected_liquidity_line,
])
# ==== Check editing the account.move.line ====
liquidity_lines, counterpart_lines, writeoff_lines = payment._seek_for_lines()
payment.move_id.write({
'line_ids': [
(1, counterpart_lines.id, {
'account_id': self.company_data['company'].transfer_account_id.id,
'partner_id': self.company_data['company'].partner_id.id,
}),
(1, liquidity_lines.id, {
'partner_id': self.company_data['company'].partner_id.id,
}),
]
})
self.assertRecordValues(payment, [expected_payment_values])
self.assertRecordValues(payment.move_id, [expected_move_values])
self.assertRecordValues(payment.line_ids.sorted('balance'), [
expected_counterpart_line,
expected_liquidity_line,
])
def test_compute_currency_id(self):
''' When creating a new account.payment without specifying a currency, the default currency should be the one
set on the journal.
'''
self.company_data['default_journal_bank'].write({
'currency_id': self.currency_data['currency'].id,
})
payment = self.env['account.payment'].create({
'amount': 50.0,
'payment_type': 'inbound',
'partner_type': 'customer',
'partner_id': self.partner_a.id,
'journal_id': self.company_data['default_journal_bank'].id,
})
self.assertRecordValues(payment, [{
'currency_id': self.currency_data['currency'].id,
}])
self.assertRecordValues(payment.move_id, [{
'currency_id': self.currency_data['currency'].id,
}])
self.assertRecordValues(payment.line_ids.sorted('balance'), [
{
'debit': 0.0,
'credit': 25.0,
'amount_currency': -50.0,
'currency_id': self.currency_data['currency'].id,
},
{
'debit': 25.0,
'credit': 0.0,
'amount_currency': 50.0,
'currency_id': self.currency_data['currency'].id,
},
])
def test_reconciliation_payment_states(self):
payment = self.env['account.payment'].create({
'amount': 50.0,
'payment_type': 'inbound',
'partner_type': 'customer',
'destination_account_id': self.company_data['default_account_receivable'].id,
})
liquidity_lines, counterpart_lines, writeoff_lines = payment._seek_for_lines()
self.assertRecordValues(payment, [{
'is_reconciled': False,
'is_matched': False,
}])
invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'invoice_line_ids': [(0, 0, {
'name': '50 to pay',
'price_unit': 50.0,
'quantity': 1,
'account_id': self.company_data['default_account_revenue'].id,
})],
})
payment.action_post()
invoice.post()
(counterpart_lines + invoice.line_ids.filtered(lambda line: line.account_internal_type == 'receivable'))\
.reconcile()
self.assertRecordValues(payment, [{
'is_reconciled': True,
'is_matched': False,
}])
statement = self.env['account.bank.statement'].create({
'name': 'test_statement',
'journal_id': self.company_data['default_journal_bank'].id,
'line_ids': [
(0, 0, {
'payment_ref': '50 to pay',
'partner_id': self.partner_a.id,
'amount': 50.0,
}),
],
})
statement.button_post()
statement_line = statement.line_ids
statement_line.reconcile([{'id': liquidity_lines.id}])
self.assertRecordValues(payment, [{
'is_reconciled': True,
'is_matched': True,
}])
@@ -0,0 +1,424 @@
# -*- coding: utf-8 -*-
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.exceptions import UserError
from odoo.tests import tagged
@tagged('post_install', '-at_install')
class TestAccountPaymentRegister(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.payment_debit_account_id = cls.company_data['default_journal_bank'].payment_debit_account_id.copy()
cls.payment_credit_account_id = cls.company_data['default_journal_bank'].payment_credit_account_id.copy()
cls.custom_payment_method_in = cls.env['account.payment.method'].create({
'name': 'custom_payment_method_in',
'code': 'CUSTOMIN',
'payment_type': 'inbound',
})
cls.manual_payment_method_in = cls.env.ref('account.account_payment_method_manual_in')
cls.custom_payment_method_out = cls.env['account.payment.method'].create({
'name': 'custom_payment_method_out',
'code': 'CUSTOMOUT',
'payment_type': 'outbound',
})
cls.manual_payment_method_out = cls.env.ref('account.account_payment_method_manual_out')
cls.company_data['default_journal_bank'].write({
'payment_debit_account_id': cls.payment_debit_account_id.id,
'payment_credit_account_id': cls.payment_credit_account_id.id,
'inbound_payment_method_ids': [(6, 0, (
cls.manual_payment_method_in.id,
cls.custom_payment_method_in.id,
))],
'outbound_payment_method_ids': [(6, 0, (
cls.env.ref('account.account_payment_method_manual_out').id,
cls.custom_payment_method_out.id,
cls.manual_payment_method_out.id,
))],
})
# Customer invoices sharing the same batch.
cls.out_invoice_1 = cls.env['account.move'].create({
'move_type': 'out_invoice',
'date': '2017-01-01',
'invoice_date': '2017-01-01',
'partner_id': cls.partner_a.id,
'currency_id': cls.currency_data['currency'].id,
'invoice_line_ids': [(0, 0, {'product_id': cls.product_a.id, 'price_unit': 1000.0})],
})
cls.out_invoice_1.post()
cls.out_invoice_2 = cls.env['account.move'].create({
'move_type': 'out_invoice',
'date': '2017-01-01',
'invoice_date': '2017-01-01',
'partner_id': cls.partner_a.id,
'currency_id': cls.currency_data['currency'].id,
'invoice_line_ids': [(0, 0, {'product_id': cls.product_a.id, 'price_unit': 2000.0})],
})
(cls.out_invoice_1 + cls.out_invoice_2).post()
# Vendor bills, in_invoice_1 + in_invoice_2 are sharing the same batch but not in_invoice_3.
cls.in_invoice_1 = cls.env['account.move'].create({
'move_type': 'in_invoice',
'date': '2017-01-01',
'invoice_date': '2017-01-01',
'partner_id': cls.partner_a.id,
'invoice_line_ids': [(0, 0, {'product_id': cls.product_a.id, 'price_unit': 1000.0})],
})
cls.in_invoice_2 = cls.env['account.move'].create({
'move_type': 'in_invoice',
'date': '2017-01-01',
'invoice_date': '2017-01-01',
'partner_id': cls.partner_a.id,
'invoice_line_ids': [(0, 0, {'product_id': cls.product_a.id, 'price_unit': 2000.0})],
})
cls.in_invoice_3 = cls.env['account.move'].create({
'move_type': 'in_invoice',
'date': '2017-01-01',
'invoice_date': '2017-01-01',
'partner_id': cls.partner_b.id,
'currency_id': cls.currency_data['currency'].id,
'invoice_line_ids': [(0, 0, {'product_id': cls.product_a.id, 'price_unit': 3000.0})],
})
(cls.in_invoice_1 + cls.in_invoice_2 + cls.in_invoice_3).post()
def test_register_payment_single_batch_grouped_keep_open_lower_amount(self):
''' Pay 800.0 with 'open' as payment difference handling on two customer invoices (1000 + 2000). '''
active_ids = (self.out_invoice_1 + self.out_invoice_2).ids
payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=active_ids).create({
'amount': 800.0,
'group_payment': True,
'payment_difference_handling': 'open',
'currency_id': self.currency_data['currency'].id,
'payment_method_id': self.custom_payment_method_in.id,
})._create_payments()
self.assertRecordValues(payments, [{
'payment_method_id': self.custom_payment_method_in.id,
}])
self.assertRecordValues(payments.line_ids.sorted('balance'), [
# Receivable line:
{
'debit': 0.0,
'credit': 400.0,
'currency_id': self.currency_data['currency'].id,
'amount_currency': -800.0,
'reconciled': True,
},
# Liquidity line:
{
'debit': 400.0,
'credit': 0.0,
'currency_id': self.currency_data['currency'].id,
'amount_currency': 800.0,
'reconciled': False,
},
])
def test_register_payment_single_batch_grouped_keep_open_higher_amount(self):
''' Pay 3100.0 with 'open' as payment difference handling on two customer invoices (1000 + 2000). '''
active_ids = (self.out_invoice_1 + self.out_invoice_2).ids
payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=active_ids).create({
'amount': 3100.0,
'group_payment': True,
'payment_difference_handling': 'open',
'currency_id': self.currency_data['currency'].id,
'payment_method_id': self.custom_payment_method_in.id,
})._create_payments()
self.assertRecordValues(payments, [{
'payment_method_id': self.custom_payment_method_in.id,
}])
self.assertRecordValues(payments.line_ids.sorted('balance'), [
# Receivable line:
{
'debit': 0.0,
'credit': 1550.0,
'currency_id': self.currency_data['currency'].id,
'amount_currency': -3100.0,
'reconciled': False,
},
# Liquidity line:
{
'debit': 1550.0,
'credit': 0.0,
'currency_id': self.currency_data['currency'].id,
'amount_currency': 3100.0,
'reconciled': False,
},
])
def test_register_payment_single_batch_grouped_writeoff_lower_amount(self):
''' Pay 800.0 with 'reconcile' as payment difference handling on two customer invoices (1000 + 2000). '''
active_ids = (self.out_invoice_1 + self.out_invoice_2).ids
payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=active_ids).create({
'amount': 800.0,
'group_payment': True,
'payment_difference_handling': 'reconcile',
'writeoff_account_id': self.company_data['default_account_revenue'].id,
'writeoff_label': 'writeoff',
'currency_id': self.currency_data['currency'].id,
'payment_method_id': self.custom_payment_method_in.id,
})._create_payments()
self.assertRecordValues(payments, [{
'payment_method_id': self.custom_payment_method_in.id,
}])
self.assertRecordValues(payments.line_ids.sorted('balance'), [
# Receivable line:
{
'debit': 0.0,
'credit': 1500.0,
'currency_id': self.currency_data['currency'].id,
'amount_currency': -3000.0,
'reconciled': True,
},
# Liquidity line:
{
'debit': 400.0,
'credit': 0.0,
'currency_id': self.currency_data['currency'].id,
'amount_currency': 800.0,
'reconciled': False,
},
# Writeoff line:
{
'debit': 1100.0,
'credit': 0.0,
'currency_id': self.currency_data['currency'].id,
'amount_currency': 2200.0,
'reconciled': False,
},
])
def test_register_payment_single_batch_grouped_writeoff_higher_amount(self):
''' Pay 3100.0 with 'reconcile' as payment difference handling on two customer invoices (1000 + 2000). '''
active_ids = (self.out_invoice_1 + self.out_invoice_2).ids
payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=active_ids).create({
'amount': 3100.0,
'group_payment': True,
'payment_difference_handling': 'reconcile',
'writeoff_account_id': self.company_data['default_account_revenue'].id,
'writeoff_label': 'writeoff',
'currency_id': self.currency_data['currency'].id,
'payment_method_id': self.custom_payment_method_in.id,
})._create_payments()
self.assertRecordValues(payments, [{
'payment_method_id': self.custom_payment_method_in.id,
}])
self.assertRecordValues(payments.line_ids.sorted('balance'), [
# Receivable line:
{
'debit': 0.0,
'credit': 1500.0,
'currency_id': self.currency_data['currency'].id,
'amount_currency': -3000.0,
'reconciled': True,
},
# Writeoff line:
{
'debit': 0.0,
'credit': 50.0,
'currency_id': self.currency_data['currency'].id,
'amount_currency': -100.0,
'reconciled': False,
},
# Liquidity line:
{
'debit': 1550.0,
'credit': 0.0,
'currency_id': self.currency_data['currency'].id,
'amount_currency': 3100.0,
'reconciled': False,
},
])
def test_register_payment_single_batch_not_grouped(self):
''' Choose to pay two customer invoices with separated payments (1000 + 2000). '''
active_ids = (self.out_invoice_1 + self.out_invoice_2).ids
payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=active_ids).create({
'group_payment': False,
})._create_payments()
self.assertRecordValues(payments, [
{'payment_method_id': self.manual_payment_method_in.id},
{'payment_method_id': self.manual_payment_method_in.id},
])
self.assertRecordValues(payments[0].line_ids.sorted('balance') + payments[1].line_ids.sorted('balance'), [
# == Payment 1: to pay out_invoice_1 ==
# Receivable line:
{
'debit': 0.0,
'credit': 500.0,
'currency_id': self.currency_data['currency'].id,
'amount_currency': -1000.0,
'reconciled': True,
},
# Liquidity line:
{
'debit': 500.0,
'credit': 0.0,
'currency_id': self.currency_data['currency'].id,
'amount_currency': 1000.0,
'reconciled': False,
},
# == Payment 2: to pay out_invoice_2 ==
# Receivable line:
{
'debit': 0.0,
'credit': 1000.0,
'currency_id': self.currency_data['currency'].id,
'amount_currency': -2000.0,
'reconciled': True,
},
# Liquidity line:
{
'debit': 1000.0,
'credit': 0.0,
'currency_id': self.currency_data['currency'].id,
'amount_currency': 2000.0,
'reconciled': False,
},
])
def test_register_payment_multi_batches_grouped(self):
''' Choose to pay multiple batches, one with two customer invoices (1000 + 2000)
and one with a vendor bill of 600, by grouping payments.
'''
active_ids = (self.in_invoice_1 + self.in_invoice_2 + self.in_invoice_3).ids
payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=active_ids).create({
'group_payment': True,
})._create_payments()
self.assertRecordValues(payments, [
{'payment_method_id': self.manual_payment_method_out.id},
{'payment_method_id': self.manual_payment_method_out.id},
])
self.assertRecordValues(payments[0].line_ids.sorted('balance') + payments[1].line_ids.sorted('balance'), [
# == Payment 1: to pay in_invoice_1 & in_invoice_2 ==
# Liquidity line:
{
'debit': 0.0,
'credit': 3000.0,
'currency_id': False,
'amount_currency': 0.0,
'reconciled': False,
},
# Payable line:
{
'debit': 3000.0,
'credit': 0.0,
'currency_id': False,
'amount_currency': 0.0,
'reconciled': True,
},
# == Payment 2: to pay in_invoice_3 ==
# Liquidity line:
{
'debit': 0.0,
'credit': 1500.0,
'currency_id': self.currency_data['currency'].id,
'amount_currency': -3000.0,
'reconciled': False,
},
# Payable line:
{
'debit': 1500.0,
'credit': 0.0,
'currency_id': self.currency_data['currency'].id,
'amount_currency': 3000.0,
'reconciled': True,
},
])
def test_register_payment_multi_batches_not_grouped(self):
''' Choose to pay multiple batches, one with two customer invoices (1000 + 2000)
and one with a vendor bill of 600, by splitting payments.
'''
active_ids = (self.in_invoice_1 + self.in_invoice_2 + self.in_invoice_3).ids
payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=active_ids).create({
'group_payment': False,
})._create_payments()
self.assertRecordValues(payments, [
{'payment_method_id': self.manual_payment_method_out.id},
{'payment_method_id': self.manual_payment_method_out.id},
{'payment_method_id': self.manual_payment_method_out.id},
])
self.assertRecordValues(payments[0].line_ids.sorted('balance') + payments[1].line_ids.sorted('balance') + payments[2].line_ids.sorted('balance'), [
# == Payment 1: to pay in_invoice_1 ==
# Liquidity line:
{
'debit': 0.0,
'credit': 1000.0,
'currency_id': False,
'amount_currency': 0.0,
'reconciled': False,
},
# Payable line:
{
'debit': 1000.0,
'credit': 0.0,
'currency_id': False,
'amount_currency': 0.0,
'reconciled': True,
},
# == Payment 2: to pay in_invoice_2 ==
# Liquidity line:
{
'debit': 0.0,
'credit': 2000.0,
'currency_id': False,
'amount_currency': 0.0,
'reconciled': False,
},
# Payable line:
{
'debit': 2000.0,
'credit': 0.0,
'currency_id': False,
'amount_currency': 0.0,
'reconciled': True,
},
# == Payment 3: to pay in_invoice_3 ==
# Liquidity line:
{
'debit': 0.0,
'credit': 1500.0,
'currency_id': self.currency_data['currency'].id,
'amount_currency': -3000.0,
'reconciled': False,
},
# Payable line:
{
'debit': 1500.0,
'credit': 0.0,
'currency_id': self.currency_data['currency'].id,
'amount_currency': 3000.0,
'reconciled': True,
},
])
def test_register_payment_constraints(self):
# Test to register a payment for a draft journal entry.
self.out_invoice_1.button_draft()
with self.assertRaises(UserError), self.cr.savepoint():
self.env['account.payment.register']\
.with_context(active_model='account.move', active_ids=self.out_invoice_1.ids)\
.create({})
# Test to register a payment for an already fully reconciled journal entry.
self.env['account.payment.register']\
.with_context(active_model='account.move', active_ids=self.out_invoice_2.ids)\
.create({})\
._create_payments()
with self.assertRaises(UserError), self.cr.savepoint():
self.env['account.payment.register']\
.with_context(active_model='account.move', active_ids=self.out_invoice_2.ids)\
.create({})
@@ -1,111 +0,0 @@
from odoo.addons.account.tests.common import AccountTestCommon
from odoo.tests import tagged
@tagged('post_install', '-at_install')
class TestBankStatementReconciliation(AccountTestCommon):
@classmethod
def setUpClass(cls):
super(TestBankStatementReconciliation, cls).setUpClass()
cls.bs_model = cls.env['account.bank.statement']
cls.bsl_model = cls.env['account.bank.statement.line']
cls.partner = cls.env['res.partner'].create({'name': 'test'})
cls.currency_usd_id = cls.env.ref("base.USD").id
cls.currency_euro_id = cls.env.ref("base.EUR").id
def test_full_reconcile(self):
self._reconcile_invoice_with_statement('pay_val')
def test_post_at_bank_rec_full_reconcile(self):
""" Test the full reconciliation of a bank statement directly with an invoice.
"""
self._reconcile_invoice_with_statement('bank_rec')
def _reconcile_invoice_with_statement(self, post_at):
""" Tests the reconciliation of an invoice with a bank statement, using
the provided 'post at bank reconciliation' value for the bank journal
where to generate the statement.
"""
self.bs_model.with_context(journal_type='bank')._default_journal().post_at_bank_reconciliation = post_at == 'bank_rec'
rcv_mv_line = self.create_invoice(100)
st_line = self.create_statement_line(100)
# reconcile
st_line.process_reconciliation(counterpart_aml_dicts=[{
'move_line': rcv_mv_line,
'credit': 100,
'debit': 0,
'name': rcv_mv_line.name,
}])
# check everything went as expected
self.assertTrue(st_line.journal_entry_ids)
counterpart_mv_line = None
for l in st_line.journal_entry_ids:
if l.account_id.user_type_id.type == 'receivable':
counterpart_mv_line = l
break
self.assertIsNotNone(counterpart_mv_line)
self.assertTrue(rcv_mv_line.reconciled)
self.assertTrue(counterpart_mv_line.reconciled)
self.assertEqual(counterpart_mv_line.matched_credit_ids, rcv_mv_line.matched_debit_ids)
self.assertEqual(rcv_mv_line.move_id.payment_state, 'paid', "The related invoice's state should now be 'paid'")
def test_reconcile_with_write_off(self):
pass
def create_invoice(self, amount):
""" Return the move line that gets to be reconciled (the one in the receivable account) """
move = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': self.partner.id,
'invoice_line_ids': [(0, 0, {
'quantity': 1,
'price_unit': amount,
'name': 'test invoice',
})],
})
move.post()
return move.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
def create_statement_line(self, st_line_amount):
journal = self.bs_model.with_context(journal_type='bank')._default_journal()
#journal = self.env.ref('l10n_be.bank_journal')
bank_stmt = self.bs_model.create({'journal_id': journal.id})
bank_stmt_line = self.bsl_model.create({
'name': '_',
'statement_id': bank_stmt.id,
'partner_id': self.partner.id,
'amount': st_line_amount,
})
return bank_stmt_line
def test_confirm_statement_usd(self):
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.env['res.currency.rate'].search([]).unlink()
self.env['res.currency.rate'].create({
'currency_id': self.currency_usd_id,
'rate': 2.0,
'name': '2001-01-01',
})
bank_journal_usd = self.env['account.journal'].create({
'name': 'Bank US',
'type': 'bank',
'code': 'BNK68',
'currency_id': self.currency_usd_id,
})
statement = self.bs_model.create({
'journal_id': bank_journal_usd.id,
'balance_end_real': 100,
'line_ids': [(0, 0, {
'name': '_',
'partner_id': self.partner.id,
'amount': 100,
'account_id': bank_journal_usd.default_debit_account_id.id,
})],
})
statement.button_open()
statement.button_confirm_bank()
-546
View File
@@ -1,546 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.account.tests.common import AccountTestCommon
from odoo.tests import tagged
from odoo.tests.common import Form
import time
@tagged('post_install', '-at_install')
class TestPayment(AccountTestCommon):
def setUp(self):
self.env.ref('base.main_company').currency_id = self.env.ref('base.USD')
super(TestPayment, self).setUp()
self.register_payments_model = self.env['account.payment.register'].with_context(active_model='account.move')
self.payment_model = self.env['account.payment']
self.acc_bank_stmt_model = self.env['account.bank.statement']
self.acc_bank_stmt_line_model = self.env['account.bank.statement.line']
self.partner_agrolait = self.env['res.partner'].create({'name': 'Agrolait', 'is_company': True})
self.partner_china_exp = self.env['res.partner'].create({'name': 'China Export', 'is_company': True})
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
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['product.product'].create({
'name': 'Product Product 4',
'standard_price': 500.0,
'list_price': 750.0,
'type': 'consu',
'categ_id': self.env.ref('product.product_category_all').id,
})
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")
self.account_receivable = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_receivable').id)], limit=1)
self.account_payable = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_payable').id)], limit=1)
self.account_revenue = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_revenue').id)], limit=1)
self.bank_journal_euro = self.env['account.journal'].create({'name': 'Bank', 'type': 'bank', 'code': 'BNK67'})
self.account_eur = self.bank_journal_euro.default_debit_account_id
self.cash_journal_euro = self.env['account.journal'].create({'name': 'Cash', 'type': 'cash', 'code': 'CASH'})
self.bank_journal_usd = self.env['account.journal'].create({'name': 'Bank US', 'type': 'bank', 'code': 'BNK68', 'currency_id': self.currency_usd_id})
self.account_usd = self.bank_journal_usd.default_debit_account_id
if not self.env.user.company_id.transfer_account_id:
self.env.user.company_id.transfer_account_id = self.usd_bnk
self.transfer_account = self.env.user.company_id.transfer_account_id
self.diff_income_account = self.env.user.company_id.income_currency_exchange_account_id
self.diff_expense_account = self.env.user.company_id.expense_currency_exchange_account_id
self.form_payment = Form(self.env['account.payment'])
self.env['res.currency.rate'].create([
{
'currency_id': self.env.ref('base.EUR').id,
'name': '2010-01-02',
'rate': 1.0,
}, {
'currency_id': self.env.ref('base.USD').id,
'name': '2010-01-02',
'rate': 1.2834,
}, {
'currency_id': self.env.ref('base.USD').id,
'name': time.strftime('%Y-06-05'),
'rate': 1.5289,
}
])
def create_invoice(self, amount=100, type='out_invoice', currency_id=None, partner=None, account_id=None):
""" Returns an open invoice """
invoice = self.env['account.move'].create({
'move_type': type,
'partner_id': partner or self.partner_agrolait.id,
'currency_id': currency_id or self.currency_eur_id,
'invoice_date': time.strftime('%Y') + '-06-26',
'date': time.strftime('%Y') + '-06-26',
'invoice_line_ids': [
(0, 0, {'product_id': self.product.id, 'quantity': 1, 'price_unit': amount})
],
})
invoice.post()
return invoice
def reconcile(self, liquidity_aml, amount=0.0, amount_currency=0.0, currency_id=None):
""" Reconcile a journal entry corresponding to a payment with its bank statement line """
bank_stmt = self.acc_bank_stmt_model.create({
'journal_id': liquidity_aml.journal_id.id,
'date': time.strftime('%Y') + '-07-15',
})
bank_stmt_line = self.acc_bank_stmt_line_model.create({
'name': 'payment',
'statement_id': bank_stmt.id,
'partner_id': self.partner_agrolait.id,
'amount': amount,
'amount_currency': amount_currency,
'currency_id': currency_id,
'date': time.strftime('%Y') + '-07-15'
})
bank_stmt_line.process_reconciliation(payment_aml_rec=liquidity_aml)
return bank_stmt
def test_full_payment_process(self):
""" Create a payment for one invoice, post it and reconcile it with a bank statement """
inv_1 = self.create_invoice(amount=300, currency_id=self.currency_eur_id, partner=self.partner_agrolait.id)
ids = [inv_1.id]
register_payments = self.register_payments_model.with_context(active_ids=ids).create({
'payment_date': time.strftime('%Y') + '-07-15',
'journal_id': self.bank_journal_euro.id,
'payment_method_id': self.payment_method_manual_in.id,
})
payment = self.payment_model.browse(register_payments.create_payments()['res_id'])
self.assertAlmostEqual(payment.amount, 300)
self.assertEqual(payment.state, 'posted')
self.assertEqual(payment.state, 'posted')
self.assertEqual(inv_1.payment_state, 'paid')
rec_line = payment.move_line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
self.assertRecordValues(payment.move_line_ids.sorted('credit'), [
{'account_id': self.account_eur.id, 'debit': 300.0, 'credit': 0.0, 'amount_currency': 0, 'currency_id': False},
{'account_id': rec_line.account_id.id, 'debit': 0.0, 'credit': 300.0, 'amount_currency': 0, 'currency_id': False},
])
self.assertTrue(rec_line.full_reconcile_id.exists())
liquidity_aml = payment.move_line_ids - rec_line
bank_statement = self.reconcile(liquidity_aml, 300, 0, False)
self.assertEqual(liquidity_aml.statement_id, bank_statement)
self.assertEqual(liquidity_aml.statement_line_id, bank_statement.line_ids[0])
self.assertEqual(payment.state, 'reconciled')
def test_internal_transfer_journal_usd_journal_eur(self):
""" Create a transfer from a EUR journal to a USD journal """
payment = self.payment_model.create({
'partner_id': self.company.partner_id.id,
'payment_date': time.strftime('%Y') + '-07-15',
'payment_type': 'outbound',
'partner_type': 'customer',
'amount': 50,
'currency_id': self.currency_usd_id,
'journal_id': self.bank_journal_usd.id,
'payment_method_id': self.payment_method_manual_out.id,
})
payment.post()
self.assertRecordValues(payment.move_line_ids, [
{'account_id': self.transfer_account.id, 'debit': 32.70, 'credit': 0.0, 'amount_currency': 50, 'currency_id': self.currency_usd_id},
{'account_id': self.account_usd.id, 'debit': 0.0, 'credit': 32.70, 'amount_currency': -50, 'currency_id': self.currency_usd_id},
])
def test_payment_chf_journal_usd(self):
self.env['res.currency.rate'].create({
'rate': 1.3086,
'currency_id': self.currency_chf_id,
'name': '2010-01-02',
})
payment = self.payment_model.create({
'payment_date': time.strftime('%Y') + '-07-15',
'payment_type': 'outbound',
'amount': 50,
'currency_id': self.currency_chf_id,
'journal_id': self.bank_journal_usd.id,
'partner_type': 'supplier',
'partner_id': self.partner_china_exp.id,
'payment_method_id': self.payment_method_manual_out.id,
})
payment.post()
self.assertRecordValues(payment.move_line_ids, [
{'account_id': self.partner_china_exp.property_account_payable_id.id, 'debit': 38.21, 'credit': 0.0, 'amount_currency': 50, 'currency_id': self.currency_chf_id},
{'account_id': self.account_usd.id, 'debit': 0.0, 'credit': 38.21, 'amount_currency': -58.42, 'currency_id': self.currency_usd_id},
])
def test_partial_payment(self):
""" Create test to pay invoices (cust. inv + vendor bill) with partial payment """
# Test Customer Invoice
inv_1 = self.create_invoice(amount=600)
payment_register = Form(self.env['account.payment'].with_context(active_model='account.move', active_ids=inv_1.ids))
payment_register.payment_date = time.strftime('%Y') + '-07-15'
payment_register.journal_id = self.bank_journal_euro
payment_register.payment_method_id = self.payment_method_manual_in
# Perform the partial payment by setting the amount at 550 instead of 600
payment_register.amount = 550
payment = payment_register.save()
self.assertEqual(len(payment), 1)
self.assertEqual(payment.invoice_ids[0].id, inv_1.id)
self.assertAlmostEqual(payment.amount, 550)
self.assertEqual(payment.payment_type, 'inbound')
self.assertEqual(payment.partner_id, self.partner_agrolait)
self.assertEqual(payment.partner_type, 'customer')
# Test Vendor Bill
inv_2 = self.create_invoice(amount=500, type='in_invoice', partner=self.partner_china_exp.id)
payment_register = Form(self.env['account.payment'].with_context(active_model='account.move', active_ids=inv_2.ids))
payment_register.payment_date = time.strftime('%Y') + '-07-15'
payment_register.journal_id = self.bank_journal_euro
payment_register.payment_method_id = self.payment_method_manual_in
# Perform the partial payment by setting the amount at 300 instead of 500
payment_register.amount = 300
payment = payment_register.save()
self.assertEqual(len(payment), 1)
self.assertEqual(payment.invoice_ids[0].id, inv_2.id)
self.assertAlmostEqual(payment.amount, 300)
self.assertEqual(payment.payment_type, 'outbound')
self.assertEqual(payment.partner_id, self.partner_china_exp)
self.assertEqual(payment.partner_type, 'supplier')
def test_payment_and_writeoff_in_other_currency_1(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)
receivable_line = invoice.line_ids.filtered(lambda l: l.account_id.user_type_id.type == 'receivable')
# 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.assertRecordValues(payment.move_line_ids, [
{'account_id': receivable_line.account_id.id, 'debit': 0.0, 'credit': 25.0, 'amount_currency': -38.22, 'currency_id': self.currency_usd_id},
{'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},
])
self.assertTrue(receivable_line.full_reconcile_id)
self.assertEqual(invoice.payment_state, 'paid')
# 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)
payable_line = invoice.line_ids.filtered(lambda l: l.account_id.user_type_id.type == 'payable')
# register payment on invoice
payment = self.payment_model.create({'payment_type': 'outbound',
'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_receivable.id,
'journal_id': self.bank_journal_euro.id,
'invoice_ids': [(4, invoice.id, None)]
})
payment.post()
self.assertRecordValues(payment.move_line_ids, [
{'account_id': payable_line.account_id.id, 'debit': 25.0, 'credit': 0.0, 'amount_currency': 38.22, 'currency_id': self.currency_usd_id},
{'account_id': self.account_eur.id, 'debit': 0.0, 'credit': 16.35, 'amount_currency': -25.0, 'currency_id': self.currency_usd_id},
{'account_id': self.account_receivable.id, 'debit': 0.0, 'credit': 8.65, 'amount_currency': -13.22, 'currency_id': self.currency_usd_id},
])
self.assertTrue(payable_line.full_reconcile_id)
self.assertEqual(invoice.payment_state, 'paid')
def test_payment_and_writeoff_out_refund(self):
# Use case:
# Company is in EUR, create a credit note for 100 EUR and register payment of 90.
# Mark invoice as fully paid with a write_off
# Check that all the aml are correctly created.
invoice = self.create_invoice(amount=100, type='out_refund', currency_id=self.currency_eur_id, partner=self.partner_agrolait.id)
receivable_line = invoice.line_ids.filtered(lambda l: l.account_id.user_type_id.type == 'receivable')
# register payment on invoice
payment = self.payment_model.create({'payment_type': 'outbound',
'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
'partner_type': 'customer',
'partner_id': self.partner_agrolait.id,
'amount': 90,
'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.assertRecordValues(payment.move_line_ids, [
{'account_id': receivable_line.account_id.id, 'debit': 100.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': False},
{'account_id': self.account_eur.id, 'debit': 0.0, 'credit': 90.0, 'amount_currency': 0.0, 'currency_id': False},
{'account_id': self.account_payable.id, 'debit': 0.0, 'credit': 10.0, 'amount_currency': 0.0, 'currency_id': False},
])
self.assertEqual(invoice.payment_state, 'paid')
def test_payment_and_writeoff_in_other_currency_2(self):
# Use case:
# Company is in EUR, create a supplier bill of 5325.6 USD and register payment of 5325 USD, at a different rate
# Mark invoice as fully paid with a write_off
# Check that all the aml are correctly created.
# Set exchange rates 0.895@2017-11-01 and 0.88@2017-12-01
self.env['res.currency.rate'].create({
'currency_id': self.currency_usd_id,
'rate': 0.895,
'name': time.strftime('%Y') + '-06-26'})
self.env['res.currency.rate'].create({
'currency_id': self.currency_usd_id,
'rate': 0.88,
'name': time.strftime('%Y') + '-07-15'})
invoice = self.create_invoice(amount=5325.6, type='in_invoice', currency_id=self.currency_usd_id, partner=self.partner_agrolait.id)
payable_line = invoice.line_ids.filtered(lambda l: l.account_id.user_type_id.type == 'payable')
# register payment on invoice
payment = self.payment_model.create({'payment_type': 'outbound',
'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
'partner_type': 'supplier',
'partner_id': self.partner_agrolait.id,
'amount': 5325,
'currency_id': self.currency_usd_id,
'payment_date': time.strftime('%Y') + '-07-15',
'payment_difference_handling': 'reconcile',
'writeoff_account_id': self.account_revenue.id,
'journal_id': self.bank_journal_euro.id,
'invoice_ids': [(4, invoice.id, None)]
})
payment.post()
self.assertRecordValues(payment.move_line_ids, [
{'debit': 6051.82, 'credit': 0.0, 'amount_currency': 5325.6, 'currency_id': self.currency_usd_id},
{'debit': 0.0, 'credit': 6051.14, 'amount_currency': -5325.0, 'currency_id': self.currency_usd_id},
{'debit': 0.0, 'credit': 0.68, 'amount_currency': -0.6, 'currency_id': self.currency_usd_id},
])
exchange_lines = payable_line.full_reconcile_id.exchange_move_id.line_ids
self.assertRecordValues(exchange_lines, [
{'debit': 0.0, 'credit': 101.43, 'account_id': payable_line.account_id.id},
{'debit': 101.43, 'credit': 0.0, 'account_id': self.diff_expense_account.id},
])
#check the invoice status
self.assertEqual(invoice.payment_state, 'paid')
def test_payment_and_writeoff_in_other_currency_3(self):
# Use case related in revision 20935462a0cabeb45480ce70114ff2f4e91eaf79
# Invoice made in secondary currency for which the rate to the company currency
# is higher than the foreign currency decimal precision.
# E.g: Company currency is EUR, create a customer invoice of 247590.40 EUR and
# register payment of 267 USD (1 USD = 948 EUR)
# Mark invoice as fully paid with a write_off
# Check that all the aml are correctly created and that the invoice is paid
self.env['res.currency.rate'].create({
'currency_id': self.currency_usd_id,
'rate': 1,
'name': time.strftime('%Y') + '-06-26'})
self.env['res.currency.rate'].create({
'currency_id': self.currency_eur_id,
'rate': 948,
'name': time.strftime('%Y') + '-06-26'})
invoice = self.create_invoice(amount=247590.4, type='out_invoice', currency_id=self.currency_eur_id, partner=self.partner_agrolait.id)
receivable_line = invoice.line_ids.filtered(lambda l: l.account_id.user_type_id.type == 'receivable')
# 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': 267,
'currency_id': self.currency_usd_id,
'payment_date': time.strftime('%Y') + '-06-26',
'payment_difference_handling': 'reconcile',
'writeoff_account_id': self.account_revenue.id,
'journal_id': self.bank_journal_euro.id,
'invoice_ids': [(4, invoice.id, None)],
'name': 'test_payment_and_writeoff_in_other_currency_3',
})
payment.post()
self.assertRecordValues(payment.move_line_ids, [
{'account_id': receivable_line.account_id.id, 'debit': 0.0, 'credit': 247589.16, 'amount_currency': -261.17, 'currency_id': self.currency_usd_id},
{'account_id': self.account_eur.id, 'debit': 253116.0, 'credit': 0.0, 'amount_currency': 267.0, 'currency_id': self.currency_usd_id},
{'account_id': self.account_revenue.id, 'debit': 0.0, 'credit': 5526.84, 'amount_currency': -5.83, 'currency_id': self.currency_usd_id},
])
# Check the invoice status and the full reconciliation: the difference on the receivable account
# should have been completed by an exchange rate difference entry
self.assertEqual(invoice.payment_state, 'paid')
self.assertTrue(receivable_line.full_reconcile_id)
def test_post_at_bank_reconciliation_payment(self):
# Create two new payments in a journal requiring the journal entries to be posted at bank reconciliation
post_at_bank_rec_journal = self.env['account.journal'].create({
'name': 'Bank',
'type': 'bank',
'code': 'COUCOU',
'post_at': 'bank_rec',
})
payment_one = 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': 42,
'payment_date': time.strftime('%Y') + '-07-15',
'payment_difference_handling': 'reconcile',
'writeoff_account_id': self.account_receivable.id,
'journal_id': post_at_bank_rec_journal.id,
})
payment_one.post()
payment_two = 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': 11,
'payment_date': time.strftime('%Y') + '-12-15',
'payment_difference_handling': 'reconcile',
'writeoff_account_id': self.account_receivable.id,
'journal_id': post_at_bank_rec_journal.id,
})
payment_two.post()
# Check the payments and their move state
self.assertEqual(payment_one.state, 'posted', "Payment one shoud be in posted state.")
self.assertEqual(payment_one.mapped('move_line_ids.move_id.state'), ['draft'], "A posted payment (payment_one) in a bank journal with the 'post at bank reconciliation' option activated should correspond to a draft account.move")
self.assertEqual(payment_two.state, 'posted', "Payment two shoud be in posted state.")
self.assertEqual(payment_two.mapped('move_line_ids.move_id.state'), ['draft'], "A posted payment (payment_two) in a bank journal with the 'post at bank reconciliation' option activated should correspond to a draft account.move")
# Reconcile the two payments with an invoice, whose full amount is equal to their sum
invoice = self.create_invoice(amount=53, partner=self.partner_agrolait.id)
(payment_one.move_line_ids + payment_two.move_line_ids + invoice.line_ids).filtered(lambda x: x.account_id.user_type_id.type == 'receivable').reconcile()
self.assertEqual(invoice.payment_state, 'in_payment', "Invoice should be in 'in payment' state")
# Match the first payment with a bank statement line
bank_statement_one = self.reconcile(payment_one.move_line_ids.filtered(lambda x: x.account_id.user_type_id.type == 'liquidity'), 42)
stmt_line_date_one = bank_statement_one.mapped('line_ids.date')
self.assertEqual(payment_one.mapped('move_line_ids.move_id.state'), ['posted'], "After bank reconciliation, payment one's account.move should be posted.")
self.assertEqual(payment_one.mapped('move_line_ids.move_id.date'), stmt_line_date_one, "After bank reconciliation, payment one's account.move should share the same date as the bank statement.")
self.assertEqual([payment_one.payment_date], stmt_line_date_one, "After bank reconciliation, payment one should share the same date as the bank statement.")
self.assertEqual(invoice.payment_state, 'in_payment', "The invoice should still be 'in payment', not all its payments are reconciled with a statement")
# Match the second payment with a bank statement line
bank_statement_two = self.reconcile(payment_two.move_line_ids.filtered(lambda x: x.account_id.user_type_id.type == 'liquidity'), 42)
stmt_line_date_two = bank_statement_two.mapped('line_ids.date')
self.assertEqual(payment_two.mapped('move_line_ids.move_id.state'), ['posted'], "After bank reconciliation, payment two's account.move should be posted.")
self.assertEqual(payment_two.mapped('move_line_ids.move_id.date'), stmt_line_date_two, "After bank reconciliation, payment two's account.move should share the same date as the bank statement.")
self.assertEqual([payment_two.payment_date], stmt_line_date_two, "After bank reconciliation, payment two should share the same date as the bank statement.")
# The invoice should now be paid
self.assertEqual(invoice.payment_state, 'paid', "Invoice should be in 'paid' state after having reconciled the two payments with a bank statement")
def test_payment_draft_keep_name(self):
payment = self.payment_model.create({
'payment_type': 'inbound',
'payment_method_id': self.payment_method_manual_in.id,
'partner_type': 'customer',
'partner_id': self.partner_agrolait.id,
'amount': 90,
'payment_date': time.strftime('%Y') + '-07-15',
'payment_difference_handling': 'reconcile',
'journal_id': self.bank_journal_euro.id,
})
payment.post()
self.assertEqual(len(payment.move_line_ids.mapped('move_id')), 1)
name = payment.move_line_ids.mapped('move_id').name
self.assertTrue(name)
payment.action_draft()
self.assertFalse(payment.move_line_ids.mapped('move_id'))
payment.post()
self.assertEqual(len(payment.move_line_ids.mapped('move_id')), 1)
self.assertEqual(name, payment.move_line_ids.mapped('move_id').name)
def test_partial_payment_inv_foreign_payment_domestic(self):
"""
Invoice of 1000$ (foreign $) at 01/01 with foreign exchange rate of 0.50000
Payment of 500 (domestic €) at 15/01 with foreign exchange rate of 1.00000
The residuals should be 500 in foreign and 1500 in domestic.
"""
company = self.env.ref('base.main_company')
self.env['res.currency.rate'].search([]).unlink()
self.env['res.currency.rate'].create({
'name': time.strftime('%Y') + '-01-01',
'rate': 1.0,
'currency_id': self.currency_eur_id,
'company_id': company.id
})
self.env['res.currency.rate'].create({
'name': time.strftime('%Y') + '-01-01',
'rate': 0.5, # Don't change this !
'currency_id': self.currency_usd_id,
'company_id': self.env.ref('base.main_company').id
})
self.env['res.currency.rate'].create({
'name': time.strftime('%Y') + '-01-15',
'rate': 1.0, # Don't change this !
'currency_id': self.currency_usd_id,
'company_id': self.env.ref('base.main_company').id
})
inv1 = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': self.partner_agrolait.id,
'currency_id': self.currency_usd_id,
'invoice_date': time.strftime('%Y') + '-01-01',
'date': time.strftime('%Y') + '-01-01',
'invoice_line_ids': [
(0, 0, {'product_id': self.product.id, 'quantity': 1, 'price_unit': 1000})
],
})
inv1.post()
payment = self.payment_model.create({
'payment_date': time.strftime('%Y') + '-01-15',
'payment_method_id': self.payment_method_manual_in.id,
'payment_type': 'inbound',
'partner_type': 'customer',
'partner_id': inv1.partner_id.id,
'amount': 500,
'journal_id': self.bank_journal_euro.id,
'company_id': company.id,
'currency_id': self.currency_eur_id,
})
payment.post()
inv1_receivable = inv1.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
pay_receivable = payment.move_line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
self.assertEqual(inv1_receivable.balance, 2000)
self.assertEqual(pay_receivable.balance, -500)
(inv1_receivable + pay_receivable).reconcile()
self.assertEqual(inv1.amount_residual, 500)
self.assertEqual(inv1.amount_residual_signed, 1500)
+161 -408
View File
@@ -7,128 +7,12 @@ from datetime import timedelta
from odoo import api, fields
from odoo.addons.account.tests.common import TestAccountReconciliationCommon
from odoo.exceptions import UserError
from odoo.tests import Form, tagged
@tagged('post_install', '-at_install')
class TestReconciliationExec(TestAccountReconciliationCommon):
def test_statement_usd_invoice_eur_transaction_eur(self):
customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_euro_id, 30, self.bank_journal_usd, 42, 30, self.currency_euro_id)
self.assertRecordValues(customer_move_lines, [
{'debit': 0.0, 'credit': 30.0, 'amount_currency': -42, 'currency_id': self.currency_usd_id},
{'debit': 30.0, 'credit': 0.0, 'amount_currency': 42, 'currency_id': self.currency_usd_id},
])
self.assertRecordValues(supplier_move_lines, [
{'debit': 30.0, 'credit': 0.0, 'amount_currency': 42, 'currency_id': self.currency_usd_id},
{'debit': 0.0, 'credit': 30.0, 'amount_currency': -42, 'currency_id': self.currency_usd_id},
])
def test_statement_usd_invoice_usd_transaction_usd(self):
customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_usd_id, 50, self.bank_journal_usd, 50, 0, False)
self.assertRecordValues(customer_move_lines, [
{'debit': 0.0, 'credit': 32.70, 'amount_currency': -50, 'currency_id': self.currency_usd_id},
{'debit': 32.70, 'credit': 0.0, 'amount_currency': 50, 'currency_id': self.currency_usd_id},
])
self.assertRecordValues(supplier_move_lines, [
{'debit': 32.70, 'credit': 0.0, 'amount_currency': 50, 'currency_id': self.currency_usd_id},
{'debit': 0.0, 'credit': 32.70, 'amount_currency': -50, 'currency_id': self.currency_usd_id},
])
def test_statement_usd_invoice_usd_transaction_eur(self):
customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_usd_id, 50, self.bank_journal_usd, 50, 40, self.currency_euro_id)
self.assertRecordValues(customer_move_lines, [
{'debit': 0.0, 'credit': 40.0, 'amount_currency': -50, 'currency_id': self.currency_usd_id},
{'debit': 40.0, 'credit': 0.0, 'amount_currency': 50, 'currency_id': self.currency_usd_id},
])
exchange_lines = customer_move_lines.mapped('full_reconcile_id.exchange_move_id.line_ids')
self.assertRecordValues(exchange_lines, [
{'debit': 7.30, 'credit': 0.0, 'account_id': self.account_rcv.id},
{'debit': 0.0, 'credit': 7.30, 'account_id': self.diff_income_account.id},
])
self.assertRecordValues(supplier_move_lines, [
{'debit': 40.0, 'credit': 0.0, 'amount_currency': 50, 'currency_id': self.currency_usd_id},
{'debit': 0.0, 'credit': 40.0, 'amount_currency': -50, 'currency_id': self.currency_usd_id},
])
exchange_lines = supplier_move_lines.mapped('full_reconcile_id.exchange_move_id.line_ids')
self.assertRecordValues(exchange_lines, [
{'debit': 0.0, 'credit': 7.30, 'account_id': self.account_rsa.id},
{'debit': 7.30, 'credit': 0.0, 'account_id': self.diff_expense_account.id},
])
def test_statement_usd_invoice_chf_transaction_chf(self):
self.env['res.currency.rate'].create({
'rate': 1.3086,
'currency_id': self.env.ref('base.CHF').id,
'name': '2010-01-02',
})
customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_swiss_id, 50, self.bank_journal_usd, 42, 50, self.currency_swiss_id)
self.assertRecordValues(customer_move_lines, [
{'debit': 0.0, 'credit': 27.47, 'amount_currency': -50, 'currency_id': self.currency_swiss_id},
{'debit': 27.47, 'credit': 0.0, 'amount_currency': 42, 'currency_id': self.currency_usd_id},
])
exchange_lines = customer_move_lines.mapped('full_reconcile_id.exchange_move_id.line_ids')
self.assertRecordValues(exchange_lines, [
{'debit': 0.0, 'credit': 10.74, 'account_id': self.account_rcv.id},
{'debit': 10.74, 'credit': 0.0, 'account_id': self.diff_expense_account.id},
])
self.assertRecordValues(supplier_move_lines, [
{'debit': 27.47, 'credit': 0.0, 'amount_currency': 50, 'currency_id': self.currency_swiss_id},
{'debit': 0.0, 'credit': 27.47, 'amount_currency': -42, 'currency_id': self.currency_usd_id},
])
exchange_lines = supplier_move_lines.mapped('full_reconcile_id.exchange_move_id.line_ids')
self.assertRecordValues(exchange_lines, [
{'debit': 10.74, 'credit': 0.0, 'account_id': self.account_rsa.id},
{'debit': 0.0, 'credit': 10.74, 'account_id': self.diff_income_account.id},
])
def test_statement_eur_invoice_usd_transaction_usd(self):
customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_usd_id, 50, self.bank_journal_euro, 40, 50, self.currency_usd_id)
self.assertRecordValues(customer_move_lines, [
{'debit': 0.0, 'credit': 40.0, 'amount_currency': -50, 'currency_id': self.currency_usd_id},
{'debit': 40.0, 'credit': 0.0, 'amount_currency': 50, 'currency_id': self.currency_usd_id},
])
exchange_lines = customer_move_lines.mapped('full_reconcile_id.exchange_move_id.line_ids')
self.assertRecordValues(exchange_lines, [
{'debit': 7.30, 'credit': 0.0, 'account_id': self.account_rcv.id},
{'debit': 0.0, 'credit': 7.30, 'account_id': self.diff_income_account.id},
])
self.assertRecordValues(supplier_move_lines, [
{'debit': 40.0, 'credit': 0.0, 'amount_currency': 50, 'currency_id': self.currency_usd_id},
{'debit': 0.0, 'credit': 40.0, 'amount_currency': -50, 'currency_id': self.currency_usd_id},
])
exchange_lines = supplier_move_lines.mapped('full_reconcile_id.exchange_move_id.line_ids')
self.assertRecordValues(exchange_lines, [
{'debit': 0.0, 'credit': 7.30, 'account_id': self.account_rsa.id},
{'debit': 7.30, 'credit': 0.0, 'account_id': self.diff_expense_account.id},
])
def test_statement_eur_invoice_usd_transaction_eur(self):
customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_usd_id, 50, self.bank_journal_euro, 40, 0.0, False)
self.assertRecordValues(customer_move_lines, [
{'debit': 0.0, 'credit': 40.0, 'amount_currency': 0.0, 'currency_id': False},
{'debit': 40.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': False},
])
self.assertRecordValues(supplier_move_lines, [
{'debit': 40.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': False},
{'debit': 0.0, 'credit': 40.0, 'amount_currency': 0.0, 'currency_id': False},
])
def test_statement_euro_invoice_usd_transaction_chf(self):
customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_usd_id, 50, self.bank_journal_euro, 42, 50, self.currency_swiss_id)
self.assertRecordValues(customer_move_lines, [
{'debit': 0.0, 'credit': 42.0, 'amount_currency': -50, 'currency_id': self.currency_swiss_id},
{'debit': 42.0, 'credit': 0.0, 'amount_currency': 50, 'currency_id': self.currency_swiss_id},
])
self.assertRecordValues(supplier_move_lines, [
{'debit': 42.0, 'credit': 0.0, 'amount_currency': 50, 'currency_id': self.currency_swiss_id},
{'debit': 0.0, 'credit': 42.0, 'amount_currency': -50, 'currency_id': self.currency_swiss_id},
])
def test_statement_euro_invoice_usd_transaction_euro_full(self):
# Create a customer invoice of 50 USD.
partner = self.env['res.partner'].create({'name': 'test'})
@@ -150,7 +34,7 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
'date': '%s-01-01' % time.strftime('%Y'),
'line_ids': [
(0, 0, {
'name': 'test',
'payment_ref': 'test',
'partner_id': partner.id,
'amount': 40.0,
'date': '%s-01-01' % time.strftime('%Y')
@@ -160,22 +44,16 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
# Reconcile the bank statement with the invoice.
receivable_line = move.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
bank_stmt.line_ids[0].process_reconciliation(counterpart_aml_dicts=[{
'move_line': receivable_line,
'debit': 0.0,
'credit': 32.7,
'name': 'test_statement_euro_invoice_usd_transaction_euro_full',
}], new_aml_dicts=[{
'debit': 0.0,
'credit': 7.3,
'name': 'exchange difference',
'account_id': self.diff_income_account.id
}])
bank_stmt.button_post()
bank_stmt.line_ids[0].reconcile([
{'id': receivable_line.id},
{'name': 'exchange difference', 'balance': -7.3, 'account_id': self.diff_income_account.id},
])
self.assertRecordValues(bank_stmt.move_line_ids, [
self.assertRecordValues(bank_stmt.line_ids.line_ids, [
{'debit': 40.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': False},
{'debit': 0.0, 'credit': 7.3, 'amount_currency': 0.0, 'currency_id': False},
{'debit': 0.0, 'credit': 32.7, 'amount_currency': 0.0, 'currency_id': False},
{'debit': 40.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': False},
])
# The invoice should be paid, as the payments totally cover its total
@@ -244,9 +122,7 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
line_id = l
break
for statement_line in statement.line_ids:
statement_line.process_reconciliation([
{'counterpart_move_line_id': line_id.id, 'credit': 1.0, 'debit': 0.0, 'name': line_id.name}
])
statement_line.reconcile([{'id': line_id.id}])
# The invoice should be paid, as the payments totally cover its total
self.assertEqual(invoice.state, 'paid', 'The invoice should be paid by now')
@@ -298,7 +174,9 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
'journal_id': self.bank_journal_euro.id,
'line_ids': [(0,0, debit_line_vals), (0, 0, credit_line_vals)]
}
return self.env['account.move'].create(vals).id
move = self.env['account.move'].create(vals)
move.post()
return move.id
move_list_vals = [
('1', -1.83, 0, self.currency_swiss_id),
('2', 728.35, 795.05, self.currency_swiss_id),
@@ -380,6 +258,7 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
'line_ids': [(0,0, debit_line_vals), (0, 0, credit_line_vals)]
}
move_ids += self.env['account.move'].create(vals)
move_ids.post()
account_move_line = move_ids.mapped('line_ids').filtered(lambda l: l.account_id == self.account_rcv)
writeoff_vals = [{
@@ -395,80 +274,6 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
self.assertTrue(all(l.reconciled for l in writeoff_line), 'The balance lines should be totally reconciled')
self.assertTrue(all(l.reconciled for l in account_move_line), 'The move lines should be totally reconciled')
def test_reconcile_bank_statement_with_payment_and_writeoff(self):
# Use case:
# Company is in EUR, create a bill for 80 USD and register payment of 80 USD.
# create a bank statement in USD bank journal with a bank statement line of 85 USD
# Reconcile bank statement with payment and put the remaining 5 USD in bank fees or another account.
invoice = self.create_invoice(type='out_invoice', invoice_amount=80, currency_id=self.currency_usd_id)
# register payment on invoice
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': 80,
'currency_id': self.currency_usd_id,
'payment_date': time.strftime('%Y') + '-07-15',
'journal_id': self.bank_journal_usd.id,
'invoice_ids': [(6, 0, invoice.ids)],
})
payment.post()
bank_move_line = payment.move_line_ids.filtered(lambda line: line.account_id.internal_type == 'liquidity')
# create bank statement
bank_stmt = self.acc_bank_stmt_model.create({
'journal_id': self.bank_journal_usd.id,
'date': time.strftime('%Y') + '-07-15',
})
bank_stmt_line = self.acc_bank_stmt_line_model.create({'name': 'payment',
'statement_id': bank_stmt.id,
'partner_id': self.partner_agrolait_id,
'amount': 85,
'date': time.strftime('%Y') + '-07-15',})
#reconcile the statement with invoice and put remaining in another account
bank_stmt_line.process_reconciliation(payment_aml_rec= bank_move_line, new_aml_dicts=[{
'account_id': self.diff_income_account.id,
'debit': 0,
'credit': 5,
'name': 'bank fees',
}])
# Check that move lines associated to bank_statement are correct
bank_stmt_aml = self.env['account.move.line'].search([('statement_id', '=', bank_stmt.id)])
bank_stmt_aml |= bank_stmt_aml.mapped('move_id').mapped('line_ids')
self.assertEqual(len(bank_stmt_aml), 4, "The bank statement should have 4 moves lines")
lines = {
self.account_usd.id: [
{'debit': 3.27, 'credit': 0.0, 'amount_currency': 5, 'currency_id': self.currency_usd_id},
{'debit': 52.33, 'credit': 0, 'amount_currency': 80, 'currency_id': self.currency_usd_id}
],
self.diff_income_account.id: {'debit': 0.0, 'credit': 3.27, 'amount_currency': -5, 'currency_id': self.currency_usd_id},
self.account_rcv.id: {'debit': 0.0, 'credit': 52.33, 'amount_currency': -80, 'currency_id': self.currency_usd_id},
}
payments = bank_stmt_aml.mapped('payment_id')
# creation and reconciliation of the over-amount statement
# has created an another payment
self.assertEqual(len(payments), 2)
# Check amount of second, automatically created payment
self.assertEqual((payments - payment).amount, 5)
for aml in bank_stmt_aml:
line = lines[aml.account_id.id]
if type(line) == list:
# find correct line inside the list
if line[0]['debit'] == round(aml.debit, 2):
line = line[0]
else:
line = line[1]
self.assertEqual(round(aml.debit, 2), line['debit'])
self.assertEqual(round(aml.credit, 2), line['credit'])
self.assertEqual(round(aml.amount_currency, 2), line['amount_currency'])
self.assertEqual(aml.currency_id.id, line['currency_id'])
def test_partial_reconcile_currencies_01(self):
# client Account (payable, rsa)
# Debit Credit
@@ -482,9 +287,11 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
# Balance Debit = 18.75
# Counterpart Credit goes in Exchange diff
dest_journal_id = self.env['account.journal'].search([('type', '=', 'purchase'), ('company_id', '=', self.company.id)], limit=1)
dest_journal_id.write({'default_debit_account_id': self.bank_journal_euro.default_credit_account_id,
'default_credit_account_id': self.bank_journal_euro.default_credit_account_id})
dest_journal_id = self.env['account.journal'].create({
'name': 'dest_journal_id',
'type': 'bank',
})
# Setting up rates for USD (main_company is in EUR)
self.env['res.currency.rate'].create({'name': time.strftime('%Y') + '-' + '07' + '-01',
'rate': 0.5,
@@ -531,7 +338,7 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
'currency_id': self.currency_usd_id,
'journal_id': self.bank_journal_euro.id,
'company_id': self.company.id,
'payment_date': time.strftime('%Y') + '-' + '07' + '-01',
'date': time.strftime('%Y') + '-' + '07' + '-01',
'partner_id': self.partner_agrolait_id,
'payment_method_id': self.env.ref('account.account_payment_method_manual_out').id,
'partner_type': 'supplier'})
@@ -542,7 +349,7 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
'currency_id': self.currency_usd_id,
'journal_id': self.bank_journal_euro.id,
'company_id': self.company.id,
'payment_date': time.strftime('%Y') + '-' + '08' + '-01',
'date': time.strftime('%Y') + '-' + '08' + '-01',
'partner_id': self.partner_agrolait_id,
'payment_method_id': self.env.ref('account.account_payment_method_manual_out').id,
'partner_type': 'supplier'})
@@ -553,19 +360,19 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
'currency_id': self.currency_usd_id,
'journal_id': self.bank_journal_euro.id,
'company_id': self.company.id,
'payment_date': time.strftime('%Y') + '-' + '09' + '-01',
'date': time.strftime('%Y') + '-' + '09' + '-01',
'partner_id': self.partner_agrolait_id,
'payment_method_id': self.env.ref('account.account_payment_method_manual_out').id,
'partner_type': 'supplier'})
payment_a.post()
payment_b.post()
payment_c.post()
payment_a.action_post()
payment_b.action_post()
payment_c.action_post()
# Assigning payments to invoices
debit_line_a = payment_a.move_line_ids.filtered(lambda l: l.debit and l.account_id == self.account_rsa)
debit_line_b = payment_b.move_line_ids.filtered(lambda l: l.debit and l.account_id == self.account_rsa)
debit_line_c = payment_c.move_line_ids.filtered(lambda l: l.debit and l.account_id == self.account_rsa)
debit_line_a = payment_a.line_ids.filtered(lambda l: l.debit and l.account_id == self.account_rsa)
debit_line_b = payment_b.line_ids.filtered(lambda l: l.debit and l.account_id == self.account_rsa)
debit_line_c = payment_c.line_ids.filtered(lambda l: l.debit and l.account_id == self.account_rsa)
invoice_a.js_assign_outstanding_line(debit_line_a.id)
invoice_a.js_assign_outstanding_line(debit_line_b.id)
@@ -608,8 +415,8 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
'currency_id': self.currency_usd_id,
'journal_id': self.bank_journal_usd.id,
})
payment.post()
credit_aml = payment.move_line_ids.filtered('credit')
payment.action_post()
credit_aml = payment.line_ids.filtered('credit')
# Check residual before assignation
self.assertAlmostEqual(inv1.amount_residual, 10)
@@ -657,17 +464,17 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
'amount': 111,
'currency_id': self.currency_usd_id,
'journal_id': self.bank_journal_usd.id,
'payment_date': time.strftime('%Y') + '-08-01',
'date': time.strftime('%Y') + '-08-01',
})
payment.post()
credit_aml = payment.move_line_ids.filtered('credit')
payment.action_post()
credit_aml = payment.line_ids.filtered('credit')
# Check residual before assignation
self.assertAlmostEqual(inv.amount_residual, 111)
# Assign credit, check exchange move and residual
inv.js_assign_outstanding_line(credit_aml.id)
self.assertEqual(len(payment.move_line_ids.mapped('full_reconcile_id').exchange_move_id), 1)
self.assertEqual(len(payment.line_ids.mapped('full_reconcile_id').exchange_move_id), 1)
self.assertAlmostEqual(inv.amount_residual, 0)
# Unreconcile invoice and check residual
@@ -681,15 +488,15 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
'partner_type': 'customer',
'partner_id': self.partner_agrolait_id,
'journal_id': self.bank_journal_usd.id,
'payment_date': '2018-06-04',
'date': '2018-06-04',
'amount': 666,
})
payment.post()
payment.action_post()
self.assertEqual(len(payment.move_line_ids), 2)
self.assertEqual(len(payment.line_ids), 2)
bank_line = payment.move_line_ids.filtered(lambda l: l.account_id.id == self.bank_journal_usd.default_debit_account_id.id)
customer_line = payment.move_line_ids - bank_line
bank_line = payment.line_ids.filtered(lambda l: l.account_id.id == self.bank_journal_usd.payment_debit_account_id.id)
customer_line = payment.line_ids - bank_line
self.assertEqual(len(bank_line), 1)
self.assertEqual(len(customer_line), 1)
@@ -705,7 +512,7 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
self.assertEqual(len(reversed_move.line_ids), 2)
# Testing the reconciliation matching between the move lines and their reversed counterparts
reversed_bank_line = reversed_move.line_ids.filtered(lambda l: l.account_id.id == self.bank_journal_usd.default_debit_account_id.id)
reversed_bank_line = reversed_move.line_ids.filtered(lambda l: l.account_id.id == self.bank_journal_usd.payment_debit_account_id.id)
reversed_customer_line = reversed_move.line_ids - reversed_bank_line
self.assertEqual(len(reversed_bank_line), 1)
@@ -763,17 +570,17 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
'amount': 111,
'currency_id': self.currency_usd_id,
'journal_id': self.bank_journal_usd.id,
'payment_date': time.strftime('%Y') + '-08-01',
'date': time.strftime('%Y') + '-08-01',
})
payment.post()
payment.action_post()
credit_aml = payment.move_line_ids.filtered('credit')
credit_aml = payment.line_ids.filtered('credit')
inv.js_assign_outstanding_line(credit_aml.id)
self.assertTrue(inv.payment_state == 'paid', 'The invoice should be paid')
self.assertTrue(inv.payment_state in ('in_payment', 'paid'), "Invoice should be paid")
exchange_reconcile = payment.move_line_ids.mapped('full_reconcile_id')
exchange_reconcile = payment.line_ids.mapped('full_reconcile_id')
exchange_move = exchange_reconcile.exchange_move_id
payment_move = payment.move_line_ids[0].move_id
payment_move = payment.line_ids[0].move_id
reverted_payment_move = payment_move._reverse_moves([{'date': time.strftime('%Y') + '-08-01'}], cancel=True)
@@ -792,10 +599,11 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
# * Dr. 100 USD / 50 EUR - Accounts receivable
# * Cr. 100 USD / 50 EUR - Revenue
####
dest_journal_id = self.env['account.journal'].search(
[('type', '=', 'purchase'),
('company_id', '=', self.company.id)],
limit=1)
dest_journal_id = self.env['account.journal'].create({
'name': 'turlututu',
'type': 'bank',
'company_id': self.env.company.id,
})
# Delete any old rate - to make sure that we use the ones we need.
old_rates = self.env['res.currency.rate'].search(
@@ -836,20 +644,17 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
'name': time.strftime('%Y') + '-01-02',
'rate': 1,
})
# register payment on invoice
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': 50,
'currency_id': self.currency_usd_id,
'payment_date': time.strftime('%Y') + '-01-02',
'journal_id': dest_journal_id.id,
'invoice_ids': [(6, 0, invoice_cust_1.ids)],
})
payment.post()
payment = self.env['account.payment.register']\
.with_context(active_model='account.move', active_ids=invoice_cust_1.ids)\
.create({
'payment_date': time.strftime('%Y') + '-01-02',
'amount': 50,
'journal_id': dest_journal_id.id,
'currency_id': self.currency_usd_id,
})\
._create_payments()
# We expect at this point that the invoice should still be open, in 'partial' state,
# because they owe us still 50 CC.
self.assertEqual(invoice_cust_1.payment_state, 'partial', 'Invoice is in status %s' % invoice_cust_1.state)
@@ -884,7 +689,7 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
)
payment = self.env['account.payment'].create({
'payment_date': time.strftime('%Y') + '-07-15',
'date': time.strftime('%Y') + '-07-15',
'payment_type': 'inbound',
'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
'partner_type': 'customer',
@@ -893,9 +698,9 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
'currency_id': self.currency_usd_id,
'journal_id': self.bank_journal_usd.id,
})
payment.post()
payment.action_post()
receivable_line = payment.move_line_ids.filtered('credit')
receivable_line = payment.line_ids.filtered('credit')
invoice.js_assign_outstanding_line(receivable_line.id)
self.assertTrue(receivable_line.matched_debit_ids)
@@ -946,17 +751,17 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
self.assertTrue(cash_basis_moves.exists())
# check reconciliation in Payable account
purchase_move_line_ids = purchase_move.line_ids.sorted()
payment_move_line_ids = payment_move.line_ids.sorted()
self.assertTrue(purchase_move_line_ids[0].full_reconcile_id.exists())
self.assertEqual(purchase_move_line_ids[0].full_reconcile_id.reconciled_line_ids,
purchase_move_line_ids[0] + purchase_move_line_ids[1] + payment_move_line_ids[0])
purchase_line_ids = purchase_move.line_ids.sorted()
payment_line_ids = payment_move.line_ids.sorted()
self.assertTrue(purchase_line_ids[0].full_reconcile_id.exists())
self.assertEqual(purchase_line_ids[0].full_reconcile_id.reconciled_line_ids,
purchase_line_ids[0] + purchase_line_ids[1] + payment_line_ids[0])
cash_basis_aml_ids = cash_basis_moves.mapped('line_ids')
# check reconciliation in the tax waiting account
self.assertTrue(purchase_move_line_ids[4].full_reconcile_id.exists())
self.assertEqual(purchase_move_line_ids[4].full_reconcile_id.reconciled_line_ids,
cash_basis_aml_ids.filtered(lambda l: l.account_id == self.tax_waiting_account) + purchase_move_line_ids[4])
self.assertTrue(purchase_line_ids[4].full_reconcile_id.exists())
self.assertEqual(purchase_line_ids[4].full_reconcile_id.reconciled_line_ids,
cash_basis_aml_ids.filtered(lambda l: l.account_id == self.tax_waiting_account) + purchase_line_ids[4])
self.assertEqual(len(cash_basis_aml_ids), 8)
@@ -1052,6 +857,7 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
})
payment_move1.post()
(purchase_move + payment_move0 + payment_move1).post()
(purchase_move + payment_move0).mapped('line_ids').sorted().filtered(lambda l: l.account_id.internal_type == 'payable').reconcile()
(purchase_move + payment_move1).mapped('line_ids').sorted().filtered(lambda l: l.account_id.internal_type == 'payable').reconcile()
@@ -1061,21 +867,21 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
self.assertTrue(cash_basis_moves.exists())
# check reconciliation in Payable account
purchase_move_line_ids = purchase_move.line_ids.sorted()
self.assertTrue(purchase_move_line_ids[0].full_reconcile_id.exists())
self.assertEqual(purchase_move_line_ids[0].full_reconcile_id.reconciled_line_ids,
purchase_line_ids = purchase_move.line_ids.sorted()
self.assertTrue(purchase_line_ids[0].full_reconcile_id.exists())
self.assertEqual(purchase_line_ids[0].full_reconcile_id.reconciled_line_ids,
(purchase_move + payment_move0 + payment_move1).mapped('line_ids').filtered(lambda l: l.account_id.internal_type == 'payable'))
cash_basis_aml_ids = cash_basis_moves.mapped('line_ids')
# check reconciliation in the tax waiting account
self.assertTrue(purchase_move_line_ids[3].full_reconcile_id.exists())
self.assertEqual(purchase_move_line_ids[3].full_reconcile_id.reconciled_line_ids,
cash_basis_aml_ids.filtered(lambda l: l.account_id == tax_waiting_account10) + purchase_move_line_ids[3])
self.assertTrue(purchase_line_ids[3].full_reconcile_id.exists())
self.assertEqual(purchase_line_ids[3].full_reconcile_id.reconciled_line_ids,
cash_basis_aml_ids.filtered(lambda l: l.account_id == tax_waiting_account10) + purchase_line_ids[3])
self.assertTrue(purchase_move_line_ids[5].full_reconcile_id.exists())
self.assertEqual(purchase_move_line_ids[5].full_reconcile_id.reconciled_line_ids,
cash_basis_aml_ids.filtered(lambda l: l.account_id == self.tax_waiting_account) + purchase_move_line_ids[5])
self.assertTrue(purchase_line_ids[5].full_reconcile_id.exists())
self.assertEqual(purchase_line_ids[5].full_reconcile_id.reconciled_line_ids,
cash_basis_aml_ids.filtered(lambda l: l.account_id == self.tax_waiting_account) + purchase_line_ids[5])
self.assertEqual(len(cash_basis_aml_ids), 24)
@@ -1124,40 +930,6 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
)
index += 1
def test_reconciliation_to_check(self):
partner = self.env['res.partner'].create({'name': 'UncertainPartner'})
currency = self.env.company.currency_id
invoice = self.create_invoice_partner(currency_id=currency.id, partner_id=partner.id)
journal = self.env['account.journal'].create({'name': 'Bank', 'type': 'bank', 'code': 'THE', 'restrict_mode_hash_table':False})
statement = self.make_payment(invoice, journal, 50)
st_line = statement.line_ids
previous_move_lines = st_line.journal_entry_ids.ids
previous_name = st_line.move_name
with self.assertRaises(UserError): #you need edition mode to be able to change it
st_line.with_context(suspense_moves_mode=False).process_reconciliation(
counterpart_aml_dicts=[],
new_aml_dicts=[{
'debit': 0,
'credit': 50,
'name': 'exchange difference',
'account_id': self.diff_income_account.id
}],
)
st_line.with_context(suspense_moves_mode=True).process_reconciliation(
counterpart_aml_dicts=[],
new_aml_dicts=[{
'debit': 0,
'credit': 50,
'name': 'exchange difference',
'account_id': self.diff_income_account.id
}],
)
self.assertEqual(previous_name, st_line.move_name) # the name of the move hasnt changed
self.assertNotEqual(previous_move_lines, st_line.journal_entry_ids.ids) # the lines are new
def test_reconciliation_cash_basis_fx_01(self):
"""
Company's Currency EUR
@@ -1275,11 +1047,11 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
# check reconciliation in Payable account
purchase_line_ids = purchase_move.line_ids.sorted()
fx_move_01_line_ids = fx_move_01.line_ids.sorted()
payment_move_line_ids = payment_move.line_ids.sorted()
payment_line_ids = payment_move.line_ids.sorted()
self.assertTrue(purchase_line_ids[2].full_reconcile_id.exists())
self.assertEqual(
purchase_line_ids[2].full_reconcile_id.reconciled_line_ids,
purchase_line_ids[2] + fx_move_01_line_ids[0] + payment_move_line_ids[0])
purchase_line_ids[2] + fx_move_01_line_ids[0] + payment_line_ids[0])
# check cash basis
cash_basis_moves = self.env['account.move'].search(
@@ -1455,15 +1227,15 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
to_reconcile.reconcile()
# check reconciliation in Payable account
purchase_move_line_ids = purchase_move.line_ids.sorted()
purchase_line_ids = purchase_move.line_ids.sorted()
fx_move_01_line_ids = fx_move_01.line_ids.sorted()
fx_move_02_line_ids = fx_move_02.line_ids.sorted()
payment_move_line_ids = payment_move.line_ids.sorted()
self.assertTrue(purchase_move_line_ids[2].full_reconcile_id.exists())
payment_line_ids = payment_move.line_ids.sorted()
self.assertTrue(purchase_line_ids[2].full_reconcile_id.exists())
self.assertEqual(
purchase_move_line_ids[2].full_reconcile_id.reconciled_line_ids,
purchase_move_line_ids[2] + fx_move_01_line_ids[0] + fx_move_02_line_ids[0] +
payment_move_line_ids[0])
purchase_line_ids[2].full_reconcile_id.reconciled_line_ids,
purchase_line_ids[2] + fx_move_01_line_ids[0] + fx_move_02_line_ids[0] +
payment_line_ids[0])
# check cash basis
cash_basis_moves = self.env['account.move'].search(
@@ -1665,7 +1437,8 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
self.assertTrue(invoice.currency_id != self.env.user.company_id.currency_id)
# First Payment
payment0 = self.make_payment(invoice, journal, amount=59.99)
line = invoice.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
payment0 = self.make_payment(invoice, journal, amount=59.99, reconcile_param=[{'id': line.id, 'balance': -59.99}])
self.assertEqual(invoice.amount_residual, 0.01)
tax_waiting_line = invoice.line_ids.filtered(lambda l: l.account_id == self.tax_waiting_account)
@@ -1681,7 +1454,7 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
self.assertEqual(pay_receivable_line0.matched_debit_ids, move_caba0.tax_cash_basis_rec_id)
# Second Payment
payment1 = self.make_payment(invoice, journal, 0.01)
payment1 = self.make_payment(invoice, journal, 0.01, reconcile_param=[{'id': line.id, 'balance': -0.01}])
self.assertEqual(invoice.amount_residual, 0)
self.assertEqual(invoice.payment_state, 'paid')
@@ -1778,7 +1551,7 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
inv2 = self.create_invoice(type="out_refund", invoice_amount=400, currency_id=self.currency_usd_id)
payment = self.env['account.payment'].create({
'payment_date': time.strftime('%Y') + '-07-15',
'date': time.strftime('%Y') + '-07-15',
'payment_method_id': self.inbound_payment_method.id,
'payment_type': 'inbound',
'partner_type': 'customer',
@@ -1787,11 +1560,11 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
'journal_id': self.bank_journal_euro.id,
'company_id': company.id,
})
payment.post()
payment.action_post()
inv1_receivable = inv1.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
inv2_receivable = inv2.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
pay_receivable = payment.move_line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
pay_receivable = payment.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
move_balance = self.env['account.move'].create({
'partner_id': inv1.partner_id.id,
@@ -1813,7 +1586,7 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
self.assertEqual(inv1_receivable.full_reconcile_id, pay_receivable.full_reconcile_id)
self.assertEqual(inv1_receivable.full_reconcile_id, move_balance_receiv.full_reconcile_id)
self.assertEqual(inv1.payment_state, 'paid')
self.assertTrue(inv1.payment_state in ('in_payment', 'paid'), "Invoice should be paid")
self.assertEqual(inv2.payment_state, 'paid')
def test_inv_refund_foreign_payment_writeoff_domestic3(self):
@@ -1846,21 +1619,20 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
inv2 = self.create_invoice(type="out_refund", invoice_amount=225, currency_id=self.currency_usd_id)
payment = self.env['account.payment'].create({
'payment_date': time.strftime('%Y') + '-07-15',
'payment_method_id': self.inbound_payment_method.id,
'payment_type': 'inbound',
'partner_type': 'customer',
'partner_id': inv1.partner_id.id,
'amount': 372.10,
'payment_date': time.strftime('%Y') + '-07-01',
'date': time.strftime('%Y') + '-07-01',
'journal_id': self.bank_journal_euro.id,
'company_id': company.id,
})
payment.post()
payment.action_post()
inv1_receivable = inv1.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
inv2_receivable = inv2.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
pay_receivable = payment.move_line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
pay_receivable = payment.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
move_balance = self.env['account.move'].create({
'partner_id': inv1.partner_id.id,
@@ -1884,7 +1656,7 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
self.assertFalse(inv1_receivable.full_reconcile_id.exchange_move_id)
self.assertEqual(inv1.payment_state, 'paid')
self.assertTrue(inv1.payment_state in ('in_payment', 'paid'), "Invoice should be paid")
self.assertEqual(inv2.payment_state, 'paid')
def test_inv_refund_foreign_payment_writeoff_domestic4(self):
@@ -1923,7 +1695,7 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
inv2 = self._create_invoice(type="out_refund", invoice_amount=225, currency_id=self.currency_usd_id, date_invoice=time.strftime('%Y') + '-07-15', auto_validate=True)
payment = self.env['account.payment'].create({
'payment_date': time.strftime('%Y') + '-07-15',
'date': time.strftime('%Y') + '-07-15',
'payment_method_id': self.inbound_payment_method.id,
'payment_type': 'inbound',
'partner_type': 'customer',
@@ -1933,11 +1705,11 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
'company_id': company.id,
'currency_id': self.currency_euro_id,
})
payment.post()
payment.action_post()
inv1_receivable = inv1.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
inv2_receivable = inv2.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
pay_receivable = payment.move_line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
pay_receivable = payment.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
self.assertEqual(inv1_receivable.balance, 658)
self.assertEqual(inv2_receivable.balance, -202.59)
@@ -1963,7 +1735,7 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
self.assertEqual(inv1_receivable.full_reconcile_id, pay_receivable.full_reconcile_id)
self.assertEqual(inv1_receivable.full_reconcile_id, move_balance_receiv.full_reconcile_id)
self.assertEqual(inv1.payment_state, 'paid')
self.assertTrue(inv1.payment_state in ('in_payment', 'paid'), "Invoice should be paid")
self.assertEqual(inv2.payment_state, 'paid')
def test_inv_refund_foreign_payment_writeoff_domestic5(self):
@@ -2006,23 +1778,19 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
inv1.js_assign_outstanding_line(inv2_receivable.id)
self.assertEqual(inv1.amount_residual, 350)
Payment = self.env['account.payment'].with_context(default_invoice_ids=[(4, inv1.id, False)])
payment = Payment.create({
'payment_date': time.strftime('%Y') + '-07-15',
'payment_method_id': self.inbound_payment_method.id,
'payment_type': 'inbound',
'partner_type': 'customer',
'partner_id': inv1.partner_id.id,
'amount': 314.07,
'journal_id': self.bank_journal_euro.id,
'company_id': company.id,
'currency_id': self.currency_euro_id,
'payment_difference_handling': 'reconcile',
'writeoff_account_id': self.diff_income_account.id,
})
payment.post()
payment = self.env['account.payment.register']\
.with_context(active_model='account.move', active_ids=inv1.ids)\
.create({
'payment_date': time.strftime('%Y') + '-07-15',
'amount': 314.07,
'journal_id': self.bank_journal_euro.id,
'currency_id': self.currency_euro_id,
'payment_difference_handling': 'reconcile',
'writeoff_account_id': self.diff_income_account.id,
})\
._create_payments()
payment_receivable = payment.move_line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
payment_receivable = payment.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
self.assertEqual(payment_receivable.balance, -350)
self.assertTrue(inv1_receivable.full_reconcile_id.exists())
@@ -2031,7 +1799,7 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
self.assertFalse(inv1_receivable.full_reconcile_id.exchange_move_id)
self.assertEqual(inv1.payment_state, 'paid')
self.assertTrue(inv1.payment_state in ('in_payment', 'paid'), "Invoice should be paid")
self.assertEqual(inv2.payment_state, 'paid')
def test_inv_refund_foreign_payment_writeoff_domestic6(self):
@@ -2070,24 +1838,19 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
self.assertAlmostEqual(inv1.amount_residual, 350)
self.assertAlmostEqual(inv1_receivable.amount_residual, 315.15)
Payment = self.env['account.payment']
payment = Payment.create({
'payment_date': time.strftime('%Y') + '-07-15',
'payment_method_id': self.inbound_payment_method.id,
'payment_type': 'inbound',
'partner_type': 'customer',
'partner_id': inv1.partner_id.id,
'amount': 314.07,
'journal_id': self.bank_journal_euro.id,
'company_id': company.id,
'currency_id': self.currency_euro_id,
'payment_difference_handling': 'reconcile',
'writeoff_account_id': self.diff_income_account.id,
'invoice_ids': [(4, inv1.id, False)],
})
payment.post()
payment = self.env['account.payment.register']\
.with_context(active_model='account.move', active_ids=inv1.ids)\
.create({
'payment_date': time.strftime('%Y') + '-07-15',
'amount': 314.07,
'journal_id': self.bank_journal_euro.id,
'currency_id': self.currency_euro_id,
'payment_difference_handling': 'reconcile',
'writeoff_account_id': self.diff_income_account.id,
})\
._create_payments()
payment_receivable = payment.move_line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
payment_receivable = payment.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
self.assertTrue(inv1_receivable.full_reconcile_id.exists())
self.assertEqual(inv1_receivable.full_reconcile_id, inv2_receivable.full_reconcile_id)
@@ -2096,7 +1859,7 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
exchange_rcv = inv1_receivable.full_reconcile_id.exchange_move_id.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
self.assertEqual(exchange_rcv.amount_currency, 0.01)
self.assertEqual(inv1.payment_state, 'paid')
self.assertTrue(inv1.payment_state in ('in_payment', 'paid'), "Invoice should be paid")
self.assertEqual(inv2.payment_state, 'paid')
def test_inv_refund_foreign_payment_writeoff_domestic6bis(self):
@@ -2146,24 +1909,19 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
self.assertAlmostEqual(inv1.amount_residual, 350)
self.assertAlmostEqual(inv1_receivable.amount_residual, 315.15)
Payment = self.env['account.payment'].with_context(default_invoice_ids=[(4, inv1.id, False)])
payment = Payment.create({
'payment_date': time.strftime('%Y') + '-07-15',
'payment_method_id': self.inbound_payment_method.id,
'payment_type': 'inbound',
'partner_type': 'customer',
'partner_id': inv1.partner_id.id,
'amount': 314.07,
'journal_id': self.bank_journal_euro.id,
'company_id': company.id,
'currency_id': foreign_0.id,
'payment_difference_handling': 'reconcile',
'writeoff_account_id': self.diff_income_account.id,
'invoice_ids': [(4, inv1.id, False)],
})
payment.post()
payment = self.env['account.payment.register']\
.with_context(active_model='account.move', active_ids=inv1.ids)\
.create({
'payment_date': time.strftime('%Y') + '-07-15',
'amount': 314.07,
'journal_id': self.bank_journal_euro.id,
'currency_id': foreign_0.id,
'payment_difference_handling': 'reconcile',
'writeoff_account_id': self.diff_income_account.id,
})\
._create_payments()
payment_receivable = payment.move_line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
payment_receivable = payment.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
self.assertTrue(inv1_receivable.full_reconcile_id.exists())
self.assertEqual(inv1_receivable.full_reconcile_id, inv2_receivable.full_reconcile_id)
@@ -2171,7 +1929,7 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
self.assertFalse(inv1_receivable.full_reconcile_id.exchange_move_id)
self.assertEqual(inv1.payment_state, 'paid')
self.assertTrue(inv1.payment_state in ('in_payment', 'paid'), "Invoice should be paid")
self.assertEqual(inv2.payment_state, 'paid')
def test_inv_refund_foreign_payment_writeoff_domestic7(self):
@@ -2206,31 +1964,26 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
self.assertAlmostEqual(inv1_receivable.balance, 5384.48)
Payment = self.env['account.payment']
payment = Payment.create({
'payment_date': time.strftime('%Y') + '-07-15',
'payment_method_id': self.inbound_payment_method.id,
'payment_type': 'inbound',
'partner_type': 'customer',
'partner_id': inv1.partner_id.id,
'amount': 5384.43,
'journal_id': self.bank_journal_euro.id,
'company_id': company.id,
'currency_id': self.currency_euro_id,
'payment_difference_handling': 'reconcile',
'writeoff_account_id': self.diff_income_account.id,
'invoice_ids': [(4, inv1.id, False)],
})
payment.post()
payment = self.env['account.payment.register']\
.with_context(active_model='account.move', active_ids=inv1.ids)\
.create({
'payment_date': time.strftime('%Y') + '-07-15',
'amount': 5384.43,
'journal_id': self.bank_journal_euro.id,
'currency_id': self.currency_euro_id,
'payment_difference_handling': 'reconcile',
'writeoff_account_id': self.diff_income_account.id,
})\
._create_payments()
payment_receivable = payment.move_line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
payment_receivable = payment.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
self.assertTrue(inv1_receivable.full_reconcile_id.exists())
self.assertEqual(inv1_receivable.full_reconcile_id, payment_receivable.full_reconcile_id)
self.assertFalse(inv1_receivable.full_reconcile_id.exchange_move_id)
self.assertEqual(inv1.payment_state, 'paid')
self.assertTrue(inv1.payment_state in ('in_payment', 'paid'), "Invoice should be paid")
def test_inv_refund_foreign_payment_writeoff_domestic8(self):
"""
@@ -2260,7 +2013,7 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
Payment = self.env['account.payment']
payment = Payment.create({
'payment_date': time.strftime('%Y') + '-07-15',
'date': time.strftime('%Y') + '-07-15',
'payment_method_id': self.inbound_payment_method.id,
'payment_type': 'inbound',
'partner_type': 'customer',
@@ -2270,8 +2023,8 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
'company_id': company.id,
'currency_id': self.currency_euro_id,
})
payment.post()
payment_receivable = payment.move_line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
payment.action_post()
payment_receivable = payment.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
move_balance = self.env['account.move'].create({
'partner_id': inv1.partner_id.id,
@@ -2294,4 +2047,4 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
exchange_rcv = inv1_receivable.full_reconcile_id.exchange_move_id.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
self.assertEqual(exchange_rcv.amount_currency, 0.01)
self.assertEqual(inv1.payment_state, 'paid')
self.assertTrue(inv1.payment_state in ('in_payment', 'paid'), "Invoice should be paid")
@@ -1,79 +0,0 @@
from odoo import api, fields
from odoo.tests import tagged
from odoo.addons.account.tests.common import TestAccountReconciliationCommon
@tagged('post_install', '-at_install')
class TestReconciliationHeavyLoad(TestAccountReconciliationCommon):
"""Check that reconciliation can be done for a move with many lines
"""
def _create_move(self, journal):
values = {
'ref': "Test reconcile - Auto-generated by script",
'journal_id': journal.id,
'state': 'draft',
'company_id': journal.env.user.company_id.id,
}
return journal.env['account.move'].create(values)
def _get_values_account_move_line(
self, account, journal, name, move,
credit=0, debit=0, date=fields.Date.today()):
return {
'journal_id': journal.id,
'name': name,
'account_id': account.id,
'move_id': move.id,
'quantity': 1,
'credit': credit,
'debit': debit,
'date': date,
}
def setUp(self):
super(TestReconciliationHeavyLoad, self).setUp()
self.account_type = self.env.ref('account.data_account_type_receivable')
self.journal = self.env['account.journal'].search([
('type', '=', 'bank'),
('company_id', '=', self.env.user.company_id.id),
], limit=1)
self.account = self.env['account.account'].search([
('user_type_id', '=', self.account_type.id),
('company_id', '=', self.env.user.company_id.id),
], limit=1)
def test_heavy_load_reconciliation(self):
"""Does reconciliation on a move with nb_lines lines.
To avoid burdening uselessly the runbot, we only set nb_lines to 10,
but it should be of order 10^3 to be meaningful.
The day we manage to use system build settings to execute tests
this could be done automatically for "heavy load builds",
but for now this should be changed manually.
"""
total = 0
line_ids = []
amount_per_line = 1
nb_lines = 10 # change this to 1000 or more
move = self._create_move(self.journal)
for i in range(nb_lines):
name = "Move line credit #%s" % i
total += amount_per_line
values = self._get_values_account_move_line(
self.account, self.journal, name, move, credit=amount_per_line)
line_ids.append((0, False, values))
values = self._get_values_account_move_line(
self.account, self.journal, "Move line Debit", move, debit=total)
line_ids.append((0, False, values))
move.write({'line_ids': line_ids})
move.line_ids.reconcile()
self.assertTrue(all(move.line_ids.mapped('reconciled')))
@@ -13,7 +13,6 @@ class TestReconciliationMatchingRules(AccountTestCommon):
super(TestReconciliationMatchingRules, cls).setUpClass()
cls.company = cls.env.user.company_id
cls.account_pay = cls.a_pay
cls.account_liq = cls.bnk
cls.account_rcv = cls.a_recv
cls.partner_1 = cls.env['res.partner'].create({'name': 'partner_1', 'company_id': cls.company.id})
@@ -65,16 +64,16 @@ class TestReconciliationMatchingRules(AccountTestCommon):
})
cls.bank_line_1 = cls.env['account.bank.statement.line'].create({
'statement_id': cls.bank_st.id,
'name': 'invoice %s-%s-%s' % (invoice_number.split('/')[1], invoice_number.split('/')[2], invoice_number.split('/')[3]),
'payment_ref': 'invoice %s-%s-%s' % (invoice_number.split('/')[1], invoice_number.split('/')[2], invoice_number.split('/')[3]),
'partner_id': cls.partner_1.id,
'amount': '100',
'amount': 100,
'sequence': 1,
})
cls.bank_line_2 = cls.env['account.bank.statement.line'].create({
'statement_id': cls.bank_st.id,
'name': 'xxxxx',
'payment_ref': 'xxxxx',
'partner_id': cls.partner_1.id,
'amount': '600',
'amount': 600,
'sequence': 2,
})
@@ -84,16 +83,16 @@ class TestReconciliationMatchingRules(AccountTestCommon):
})
cls.cash_line_1 = cls.env['account.bank.statement.line'].create({
'statement_id': cls.cash_st.id,
'name': 'yyyyy',
'payment_ref': 'yyyyy',
'partner_id': cls.partner_2.id,
'amount': '-1000',
'amount': -1000,
'sequence': 1,
})
cls.tax21 = cls.env['account.tax'].create({
'name': '21%',
'type_tax_use': 'purchase',
'amount': '21',
'amount': 21,
})
@classmethod
@@ -111,6 +110,11 @@ class TestReconciliationMatchingRules(AccountTestCommon):
lines = invoice.line_ids
return lines.filtered(lambda l: l.account_id.user_type_id.type in ('receivable', 'payable'))
def _post_statements(self):
self.bank_st.balance_end_real = self.bank_st.balance_end
self.cash_st.balance_end_real = self.cash_st.balance_end
(self.bank_st + self.cash_st).button_post()
def _check_statement_matching(self, rules, expected_values, statements=None):
if statements is None:
statements = self.bank_st + self.cash_st
@@ -120,9 +124,9 @@ class TestReconciliationMatchingRules(AccountTestCommon):
values.pop('reconciled_lines', None)
self.assertDictEqual(values, expected_values[st_line_id])
def test_matching_fields(self):
''' Test all fields used to restrict the rules's applicability.'''
self._post_statements()
# Check without restriction.
self._check_statement_matching(self.rule_1, {
@@ -267,6 +271,7 @@ class TestReconciliationMatchingRules(AccountTestCommon):
def test_mixin_rules(self):
''' Test usage of rules together.'''
self._post_statements()
# rule_1 is used before rule_2.
self.rule_1.sequence = 1
@@ -303,6 +308,8 @@ class TestReconciliationMatchingRules(AccountTestCommon):
def test_auto_reconcile(self):
''' Test auto reconciliation.'''
self.bank_line_1.amount += 5
self._post_statements()
self.rule_1.sequence = 2
self.rule_1.auto_reconcile = True
@@ -311,8 +318,6 @@ class TestReconciliationMatchingRules(AccountTestCommon):
self.rule_2.match_partner_ids |= self.partner_2
self.rule_2.auto_reconcile = True
self.bank_line_1.amount += 5
self._check_statement_matching(self.rule_1 + self.rule_2, {
self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'status': 'reconciled'},
self.bank_line_2.id: {'aml_ids': []},
@@ -320,21 +325,20 @@ class TestReconciliationMatchingRules(AccountTestCommon):
})
# Check first line has been well reconciled.
self.assertRecordValues(self.bank_line_1.journal_entry_ids, [
self.assertRecordValues(self.bank_line_1.line_ids, [
{'partner_id': self.partner_1.id, 'debit': 105.0, 'credit': 0.0},
{'partner_id': self.partner_1.id, 'debit': 0.0, 'credit': 5.0},
{'partner_id': self.partner_1.id, 'debit': 0.0, 'credit': 100.0},
{'partner_id': self.partner_1.id, 'debit': 105.0, 'credit': 0.0},
])
# Check second line has been well reconciled.
self.assertRecordValues(self.cash_line_1.journal_entry_ids, [
{'partner_id': self.partner_2.id, 'debit': 1000.0, 'credit': 0.0},
self.assertRecordValues(self.cash_line_1.line_ids, [
{'partner_id': self.partner_2.id, 'debit': 0.0, 'credit': 1000.0},
{'partner_id': self.partner_2.id, 'debit': 1000.0, 'credit': 0.0},
])
def test_auto_reconcile_with_tax(self):
''' Test auto reconciliation with a tax amount included in the bank statement line'''
self.rule_1.write({
'auto_reconcile': True,
'rule_type': 'writeoff_suggestion',
@@ -346,6 +350,7 @@ class TestReconciliationMatchingRules(AccountTestCommon):
self.bank_line_2.unlink()
self.bank_line_1.amount = -121
self._post_statements()
self._check_statement_matching(
self.rule_1,
@@ -356,20 +361,20 @@ class TestReconciliationMatchingRules(AccountTestCommon):
)
# Check first line has been well reconciled.
self.assertRecordValues(self.bank_line_1.journal_entry_ids, [
self.assertRecordValues(self.bank_line_1.line_ids, [
{'partner_id': self.partner_1.id, 'debit': 0.0, 'credit': 121.0, 'tax_ids': [], 'tax_line_id': False},
{'partner_id': self.partner_1.id, 'debit': 100.0, 'credit': 0.0, 'tax_ids': [self.tax21.id], 'tax_line_id': False},
{'partner_id': self.partner_1.id, 'debit': 21.0, 'credit': 0.0, 'tax_ids': [], 'tax_line_id': self.tax21.id},
{'partner_id': self.partner_1.id, 'debit': 0.0, 'credit': 121.0, 'tax_ids': [], 'tax_line_id': False},
])
def test_reverted_move_matching(self):
partner = self.env['res.partner'].create({'name': 'Eugene'})
AccountMove = self.env['account.move']
move = AccountMove.create({
'name': 'To Revert',
'journal_id': self.bank_journal.id,
})
partner = self.env['res.partner'].create({'name': 'Eugene'})
AccountMoveLine = self.env['account.move.line'].with_context(check_move_validity=False)
payment_payable_line = AccountMoveLine.create({
'account_id': self.account_pay.id,
@@ -379,7 +384,7 @@ class TestReconciliationMatchingRules(AccountTestCommon):
'debit': 10,
})
payment_bnk_line = AccountMoveLine.create({
'account_id': self.account_liq.id,
'account_id': self.bank_journal.payment_credit_account_id.id,
'move_id': move.id,
'partner_id': partner.id,
'name': 'I\'m gonna cut you into little pieces',
@@ -398,7 +403,7 @@ class TestReconciliationMatchingRules(AccountTestCommon):
bank_st.journal_id.default_debit_account_id = payment_bnk_line.account_id
bank_line_1 = self.env['account.bank.statement.line'].create({
'statement_id': bank_st.id,
'name': '8',
'payment_ref': '8',
'partner_id': partner.id,
'amount': -10,
'sequence': 1,
@@ -233,8 +233,8 @@
<!-- On the left, display :
- A button corresponding to the bank_statements_source, if it wasn't configured, a button for each of them
- If there are statements to reconcile, a link to reconcile them -->
<div class="col-12 col-sm-5 mb-3 mb-sm-0 o_kanban_primary_left">
<t t-if="journal_type == 'bank'" id="journal_body_bank_cash_left">
<div id="dashboard_bank_cash_left" class="col-12 col-sm-5 mb-3 mb-sm-0 o_kanban_primary_left">
<t t-if="journal_type == 'bank'">
<button t-if="dashboard.bank_statements_source == 'undefined'" name="action_configure_bank_journal" type="object" class="btn btn-primary" groups="account.group_account_invoice">Configure</button>
<div name="bank_journal_cta" class="mt-3 mt-sm-0">
<div name="bank_statement_create_button" groups="account.group_account_invoice">
@@ -250,16 +250,24 @@
</t>
</div>
<!-- On the right, show other common informations/actions -->
<div class="col-12 col-sm-7 o_kanban_primary_right">
<t t-if="dashboard.last_balance != dashboard.account_balance">
<div class="row">
<div class="col overflow-hidden text-left">
<span title="Balance in General Ledger">Balance in GL</span>
</div>
<div class="col-auto text-right">
<span><t t-esc="dashboard.account_balance"/></span>
</div>
<div id="dashboard_bank_cash_right" class="col-12 col-sm-7 o_kanban_primary_right">
<div class="row" t-if="dashboard.nb_lines_bank_account_balance > 0">
<div id="dashboard_bank_cash_balance" class="col overflow-hidden text-left">
<span title="Balance in General Ledger">Balance in GL</span>
</div>
<div class="col-auto text-right">
<span><t t-esc="dashboard.account_balance"/></span>
</div>
</div>
<div class="row" t-if="dashboard.nb_lines_outstanding_pay_account_balance > 0">
<div id="dashboard_bank_cash_outstanding_balance" class="col overflow-hidden text-left">
<span title="Outstanding Payments/Receipts">Outstanding Payments/Receipts</span>
</div>
<div class="col-auto text-right">
<span><t t-esc="dashboard.outstanding_pay_account_balance"/></span>
</div>
</div>
<t t-if="dashboard.has_at_least_one_statement and dashboard.account_balance != dashboard.last_balance">
<div class="row" name="latest_statement">
<div class="col overflow-hidden text-left">
<span title="Latest Statement">Latest Statement</span>
@@ -269,16 +277,6 @@
</div>
</div>
</t>
<t t-else="" id="journal_body_bank_cash_right">
<div class="row">
<div class="col overflow-hidden text-left">
<span title="Balance in General Ledger">Balance in GL</span>
</div>
<div class="col-auto text-right">
<span><t t-esc="dashboard.account_balance"/></span>
</div>
</div>
</t>
</div>
<div t-if="dashboard.is_sample_data">
<span class="badge badge-warning text-uppercase o_sample_data_label">Sample data</span>
+16 -11
View File
@@ -409,7 +409,7 @@
<field name="invoice_date" optional="show" invisible="context.get('default_move_type') not in ('in_invoice', 'in_refund','in_receipt')" string="Bill Date"/>
<field name="invoice_date" optional="show" invisible="context.get('default_move_type') not in ('out_invoice', 'out_refund','out_receipt')" string="Invoice Date"/>
<field name="invoice_origin" optional="hide" string="Source Document"/>
<field name="invoice_payment_ref" optional="hide" invisible="context.get('default_move_type') in ('out_invoice', 'out_refund','out_receipt')"/>
<field name="payment_reference" optional="hide" invisible="context.get('default_move_type') in ('out_invoice', 'out_refund','out_receipt')"/>
<field name="ref" optional="hide"/>
<field name="invoice_user_id" optional="hide" invisible="context.get('default_move_type') not in ('out_invoice', 'out_refund','out_receipt')" string="Sales Person"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}" optional="show"/>
@@ -484,18 +484,19 @@
<button name="action_invoice_sent"
type="object"
string="Send &amp; Print"
attrs="{'invisible':['|', '|', ('state', '!=', 'posted'), ('invoice_sent', '=', True), ('move_type', 'not in', ('out_invoice', 'out_refund'))]}"
attrs="{'invisible':['|', '|', ('state', '!=', 'posted'), ('is_move_sent', '=', True), ('move_type', 'not in', ('out_invoice', 'out_refund'))]}"
class="oe_highlight"
groups="account.group_account_invoice"/>
<button name="action_invoice_sent"
type="object"
string="Send &amp; Print"
attrs="{'invisible':['|', '|', ('state', '!=', 'posted'), ('invoice_sent', '=', False), ('move_type', 'not in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund'))]}"
attrs="{'invisible':['|', '|', ('state', '!=', 'posted'), ('is_move_sent', '=', False), ('move_type', 'not in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund'))]}"
groups="account.group_account_invoice"/>
<!-- Register Payment (only invoices / receipts) -->
<button name="action_invoice_register_payment" id="account_invoice_payment_btn"
<button name="action_register_payment" id="account_invoice_payment_btn"
type="object" class="oe_highlight"
attrs="{'invisible': ['|', '|', ('state', '!=', 'posted'), ('payment_state', 'not in', ('not_paid', 'partial')), ('move_type', 'not in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund', 'out_receipt', 'in_receipt'))]}"
context="{'dont_redirect_to_payments': True}"
string="Register Payment"
groups="account.group_account_invoice"/>
<!-- Preview (only customer invoices) -->
@@ -574,7 +575,7 @@
<field name="display_qr_code" invisible="1"/>
<field name="invoice_has_outstanding" invisible="1"/>
<field name="invoice_sent" invisible="1"/>
<field name="is_move_sent" invisible="1"/>
<field name="invoice_has_matching_suspense_amount" invisible="1"/>
<field name="has_reconciled_entries" invisible="1"/>
<field name="restrict_mode_hash_table" invisible="1"/>
@@ -611,9 +612,9 @@
<label for="ref" string="Bill Reference"
attrs="{'invisible':[('move_type', 'not in', ('in_invoice', 'in_receipt', 'in_refund'))]}" />
<field name="ref" nolabel="1" attrs="{'invisible':[('move_type', 'not in', ('in_invoice', 'in_receipt', 'in_refund'))]}" />
<field name="invoice_payment_ref"
<field name="payment_reference"
attrs="{'invisible': ['|',('move_type', 'not in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund', 'out_receipt', 'in_receipt')), ('state', '=', 'draft')], 'readonly': [('state', '!=', 'draft')]}"/>
<field name="invoice_partner_bank_id"
<field name="partner_bank_id"
context="{'default_partner_id': commercial_partner_id}"
domain="[('partner_id', '=', bank_partner_id)]"
attrs="{'invisible': [('move_type', 'not in', ('in_invoice', 'in_refund', 'in_receipt'))], 'readonly': [('state', '!=', 'draft')]}"/>
@@ -637,7 +638,10 @@
attrs="{'invisible': [('move_type', 'not in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund', 'out_receipt', 'in_receipt'))]}"/>
<field name="date" string="Accounting Date"
attrs="{'invisible': [('move_type', 'in', ('out_invoice', 'out_refund', 'out_receipt'))]}"/>
attrs="{
'invisible': [('move_type', 'in', ('out_invoice', 'out_refund', 'out_receipt'))],
'readonly': [('state', '!=', 'draft')],
}"/>
<!-- Invoice payment terms (only invoices) + due date (only invoices / receipts) -->
<label for="invoice_payment_term_id" string="Due Date"
@@ -993,11 +997,12 @@
<field name="invoice_user_id" domain="[('share', '=', False)]"/>
<field name="invoice_origin" string="Source Document" force_save="1"
attrs="{'invisible': [('invoice_origin', '=', False)]}"/>
<field name="invoice_partner_bank_id"
<field name="partner_bank_id"
context="{'default_partner_id': commercial_partner_id}"
domain="[('partner_id', '=', bank_partner_id)]"
attrs="{'readonly': [('state', '!=', 'draft')]}"/>
<field name="qr_code_method" attrs="{'invisible': [('display_qr_code', '=', False)]}"/>
<field name="qr_code_method"
attrs="{'invisible': [('display_qr_code', '=', False)]}"/>
</group>
<group string="Accounting"
name="accounting_info_group"
@@ -1094,7 +1099,7 @@
filter_domain="[
'|', '|' , '|', '|',
('name', 'ilike', self), ('invoice_origin', 'ilike', self),
('ref', 'ilike', self), ('invoice_payment_ref', 'ilike', self),
('ref', 'ilike', self), ('payment_reference', 'ilike', self),
('partner_id', 'child_of', self)]"/>
<field name="journal_id"/>
<field name="partner_id" operator="child_of"/>
+110 -118
View File
@@ -2,12 +2,14 @@
<odoo>
<data>
<!-- VIEWS -->
<record id="view_account_payment_tree" model="ir.ui.view">
<field name="name">account.payment.tree</field>
<field name="model">account.payment</field>
<field name="arch" type="xml">
<tree decoration-info="state == 'draft'" decoration-muted="state in ['reconciled', 'cancelled']" edit="false">
<field name="payment_date"/>
<tree decoration-info="state == 'draft'" decoration-muted="state == 'cancelled'" edit="false">
<field name="date"/>
<field name="name"/>
<field name="journal_id"/>
<field name="payment_method_id"/>
@@ -15,8 +17,7 @@
<field name="amount" sum="Amount"/>
<field name="state"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="currency_id" invisible="1"/>
<field name="partner_type" invisible="1"/>
<field name="currency_id" groups="base.group_multi_currency"/>
</tree>
</field>
</record>
@@ -40,7 +41,7 @@
<kanban class="o_kanban_mobile">
<field name="name"/>
<field name="partner_id"/>
<field name="payment_date"/>
<field name="date"/>
<field name="state"/>
<templates>
<t t-name="kanban-box">
@@ -50,7 +51,7 @@
<strong><span><field name="name"/></span></strong>
</div>
<div class="col-6 text-right">
<strong><i class="fa fa-clock-o" role="img" aria-label="Date" title="Date"/> <t t-esc="record.payment_date.value"/></strong>
<strong><i class="fa fa-clock-o" role="img" aria-label="Date" title="Date"/> <t t-esc="record.date.value"/></strong>
</div>
</div>
<div class="row">
@@ -82,31 +83,36 @@
<field name="arch" type="xml">
<search string="Payments">
<field name="name" string="Payment"
filter_domain="['|', '|', ('name', 'ilike', self), ('partner_id', 'ilike', self), ('communication', 'ilike', self)]"/>
filter_domain="['|', '|', ('name', 'ilike', self), ('partner_id', 'ilike', self), ('ref', 'ilike', self)]"/>
<field name="partner_id" string="Customer/Vendor"/>
<field name="journal_id"/>
<field name="is_internal_transfer"/>
<separator/>
<filter string="Customer Payments" name="inbound_filter" domain="[('partner_type', '=', 'customer')]"/>
<filter string="Vendor Payments" name="outbound_filter" domain="[('partner_type', '=', 'supplier')]"/>
<filter string="Customer Payments"
name="inbound_filter"
domain="[('partner_type', '=', 'customer'), ('is_internal_transfer', '=', False)]"/>
<filter string="Vendor Payments"
name="outbound_filter"
domain="[('partner_type', '=', 'supplier'), ('is_internal_transfer', '=', False)]"/>
<filter string="Transfers" name="transfers_filter" domain="[('is_internal_transfer', '=', True)]"/>
<separator/>
<filter string="Draft" name="state_draft" domain="[('state', '=', 'draft')]"/>
<filter string="Posted" name="state_posted" domain="[('state', '=', 'posted')]"/>
<filter string="Sent" name="state_sent" domain="[('state', '=', 'sent')]"/>
<filter string="Reconciled" name="reconciled" domain="[('state', '=', 'reconciled')]"/>
<separator/>
<filter string="Payment Date" name="payment_date" date="payment_date"/>
<filter string="Sent" name="state_sent" domain="[('is_move_sent', '=', True)]"/>
<filter string="Bank Matched" name="matched" domain="[('is_matched', '=', True)]"/>
<filter string="Reconciled" name="reconciled" domain="[('is_reconciled', '=', True)]"/>
<separator/>
<filter string="Payment Date" name="date" date="date"/>
<separator/>
<field name="company_id" groups="base.group_multi_company"/>
<filter string="Customer/Vendor" name="partner" domain="[]" context="{'group_by': 'partner_id'}"/>
<filter string="Journal" name="journal" domain="[]" context="{'group_by': 'journal_id'}"/>
<filter string="Payment Method" name="paymentmethod" domain="[]" context="{'group_by': 'payment_method_id'}"/>
<filter string="Status" name="state" domain="[]" context="{'group_by': 'state'}"/>
<filter string="Payment Date" name="groupby_payment_date" domain="[]" context="{'group_by': 'payment_date'}"/>
<filter string="Payment Date" name="groupby_date" domain="[]" context="{'group_by': 'date'}"/>
<filter string="Currency" name="currency" domain="[]" context="{'group_by': 'currency_id'}" groups="base.group_multi_currency"/>
<!-- company_id is not stored (cdca5d4751a979f6b694f02d88160843f81a77f5). But we can't break structure-->
<filter string="Company" name="company" domain="[]" context="{'group_by': 'company_id'}" groups="base.group_multi_company" invisible="1"/>
<filter string="Company" name="company" domain="[]" context="{'group_by': 'company_id'}" groups="base.group_multi_company"/>
<separator/>
<filter invisible="1" string="Late Activities" name="activities_overdue"
domain="[('activity_ids.date_deadline', '&lt;', context_today().strftime('%Y-%m-%d'))]"
@@ -125,28 +131,50 @@
<field name="arch" type="xml">
<form string="Register Payment">
<header>
<button name="post" class="oe_highlight" states="draft" string="Confirm" type="object"/>
<button name="action_draft" class="btn btn-secondary" attrs="{'invisible': [('state', '=', 'draft')]}" string="Reset To Draft" type="object" groups="account.group_account_invoice"/>
<button name="cancel" attrs="{'invisible': [('state', 'in', ('cancelled', 'posted', 'invoicing_legacy'))]}" string="Cancel" type="object"/>
<button name="mark_as_sent" string="Mark as Sent" type="object" attrs="{'invisible': ['|', ('state', '!=', 'posted'), ('payment_method_code', '!=', 'manual')]}"/>
<button name="unmark_as_sent" string="Unmark as Sent" type="object" attrs="{'invisible': ['|', ('state', '!=', 'sent'), ('payment_method_code', '!=', 'manual')]}"/>
<field name="state" widget="statusbar" statusbar_visible="draft,posted,reconciled,cancelled"/>
<button name="action_post" string="Confirm" type="object" class="oe_highlight"
attrs="{'invisible': [('state', '!=', 'draft')]}"/>
<button name="action_draft" string="Reset To Draft" type="object" class="btn btn-secondary"
attrs="{'invisible': [('state', 'not in', ('posted', 'cancel'))]}"
groups="account.group_account_invoice"/>
<button name="action_cancel" string="Cancel" type="object"
attrs="{'invisible': [('state', '!=', 'draft')]}"/>
<button name="mark_as_sent" string="Mark as Sent" type="object"
attrs="{'invisible': ['|', '|', ('state', '!=', 'posted'), ('is_move_sent', '=', True), ('payment_method_code', '!=', 'manual')]}"/>
<button name="unmark_as_sent" string="Unmark as Sent" type="object"
attrs="{'invisible': ['|', '|', ('state', '!=', 'posted'), ('is_move_sent', '=', False), ('payment_method_code', '!=', 'manual')]}"/>
<field name="state" widget="statusbar" statusbar_visible="draft,posted"/>
</header>
<sheet>
<!-- Invisible fields -->
<field name="id" invisible="1"/>
<field name="is_move_sent" invisible="1"/>
<field name="is_reconciled" invisible="1"/>
<field name="payment_method_code" invisible="1"/>
<field name="show_partner_bank_account" invisible="1"/>
<field name="require_partner_bank_account" invisible="1"/>
<field name="hide_payment_method" invisible="1"/>
<field name="available_payment_method_ids" invisible="1"/>
<field name="suitable_journal_ids" invisible="1"/>
<div class="oe_button_box" name="button_box">
<button class="oe_stat_button" name="button_journal_entries"
string="Journal Items" type="object"
groups="account.group_account_readonly"
attrs="{'invisible':['|', ('move_line_ids','=',[]), ('state', '=', 'invoicing_legacy')]}" icon="fa-bars"/>
<field name="move_line_ids" invisible="1"/>
<button class="oe_stat_button" name="button_invoices" type="object" icon="fa-bars" attrs="{'invisible':[('reconciled_invoices_count','=', 0)]}" >
<!-- Invoice stat button -->
<button name="button_open_invoices" type="object"
class="oe_stat_button" icon="fa-bars"
attrs="{'invisible': [('reconciled_invoices_count','=', 0)]}">
<field name="reconciled_invoices_count"/>
<span attrs="{'invisible':[('reconciled_invoices_count','&gt;',1)]}">Invoice</span>
<span attrs="{'invisible':[('reconciled_invoices_count','&lt;=',1)]}">Invoices</span>
<span attrs="{'invisible': [('reconciled_invoices_count','&gt;', 1)]}">Invoice</span>
<span attrs="{'invisible': [('reconciled_invoices_count','&lt;=', 1)]}">Invoices</span>
</button>
<!-- Statement stat button -->
<button name="button_open_statements" type="object"
class="oe_stat_button" icon="fa-bars"
attrs="{'invisible': [('reconciled_statements_count','=', 0)]}">
<field name="reconciled_statements_count"/>
<span attrs="{'invisible': [('reconciled_statements_count','&gt;', 1)]}">Statement</span>
<span attrs="{'invisible': [('reconciled_statements_count','&lt;=', 1)]}">Statements</span>
</button>
<field name="has_invoices" invisible="1"/>
<field name="move_reconciled" invisible="1"/>
</div>
<widget name="web_ribbon" text="Invoicing App Legacy"
@@ -155,54 +183,58 @@
tooltip="This payment has been generated through the Invoicing app, before installing Accounting. It has been disabled by the 'Invoicing Switch Threshold Date' setting so that it does not impact your accounting."
/>
<field name="id" invisible="1"/>
<div class="oe_title" attrs="{'invisible': [('state', '=', 'draft')]}">
<h1><field name="name"/></h1>
<h1><field name="name" attrs="{'readonly': [('state', '!=', 'draft')]}"/></h1>
</div>
<group>
<field name="invoice_ids" invisible="1"/>
<field name="is_internal_transfer" invisible="1"/>
<group name="partner_group" invisible="context.get('active_model') == 'account.move'">
<field name="payment_type" widget="radio"/>
<field name="partner_type" widget="selection" attrs="{'required': [('state', '=', 'draft')], 'invisible': [('is_internal_transfer', '=', True)]}"/>
<field name="partner_id" attrs="{'required': [('state', '=', 'draft')]}" context="{'default_is_company': True}"/>
<field name="company_id" groups="base.group_multi_company" required="1"/>
<group name="group1">
<field name="payment_type" widget="radio"
attrs="{'readonly': [('state', '!=', 'draft')]}"/>
<field name="partner_type" widget="selection"
attrs="{'readonly': [('state', '!=', 'draft')], 'invisible': [('is_internal_transfer', '=', True)]}"/>
<field name="partner_id" context="{'default_is_company': True}"
attrs="{'readonly': ['|', ('state', '!=', 'draft'), ('is_internal_transfer', '=', True)]}"/>
<field name="destination_account_id"
attrs="{'readonly': ['|', ('state', '!=', 'draft'), ('is_internal_transfer', '=', True)]}"/>
<field name="is_internal_transfer"
attrs="{'readonly': [('state', '!=', 'draft')]}"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
<group>
<field name="amount" invisible="1"/>
<field name="_suitable_journal_ids" invisible="1"/>
<field name="journal_id" widget="selection" attrs="{'invisible': [('amount', '=', 0), ('state', '!=', 'draft')]}"/>
<field name="hide_payment_method" invisible="1"/>
<field name="_payment_methods" invisible="1"/>
<field name="payment_method_id" widget="radio" attrs="{'invisible': [('hide_payment_method', '=', True)]}"/>
<field name="possible_bank_partner_ids" invisible="1"/>
<field name="partner_bank_account_id" attrs="{'invisible': [('show_partner_bank_account', '!=', True)], 'required': [('require_partner_bank_account', '=', True)]}" context="{'default_partner_id': partner_id}"/>
<field name="show_partner_bank_account" invisible="1"/>
<field name="require_partner_bank_account" invisible="1"/>
<field name="payment_method_code" invisible="1"/>
<group name="group2">
<field name="journal_id"
widget="selection"
domain="[('type', 'in', ('bank', 'cash'))]"
attrs="{'readonly': [('state', '!=', 'draft')]}"/>
<field name="payment_method_id" widget="radio"
required="1"
attrs="{'readonly': [('state', '!=', 'draft')], 'invisible': [('hide_payment_method', '=', True)]}"/>
<field name="partner_bank_id" context="{'default_partner_id': partner_id}"
attrs="{'invisible': [('show_partner_bank_account', '=', False)], 'required': [('require_partner_bank_account', '=', True)]}"/>
<!-- /!\ Required due to the _inherits but must not be required when saving a new
record. Using 'required': [('id', '!=', False)] is not working due to the groups set on
the field.
-->
<field name="move_id"
required="0"
attrs="{'invisible': [('id', '=', False)]}"
groups="account.group_account_readonly"/>
</group>
<group name="right_column">
<group name="group3">
<label for="amount"/>
<div name="amount_div" class="o_row">
<field name="amount"/>
<field name="currency_id" options="{'no_create': True, 'no_open': True}" groups="base.group_multi_currency"/>
</div>
<field name="payment_date"/>
<field name="communication"/>
</group>
<group name="payment_difference" attrs="{'invisible': [('payment_difference', '=', 0.0)]}" groups="account.group_account_readonly">
<label for="payment_difference"/>
<div>
<field name="payment_difference"/>
<field name="payment_difference_handling" widget="radio" nolabel="1"/>
<div attrs="{'invisible': [('payment_difference_handling','=','open')]}">
<label for="writeoff_account_id" class="oe_edit_only" string="Post Difference In"/>
<field name="writeoff_account_id" options="{'no_create': True}" string="Post Difference In" attrs="{'required': [('payment_difference_handling', '=', 'reconcile'), ('payment_difference', '!=', 0.0)]}"/>
<label for="writeoff_label" class="oe_edit_only" string="Label"/>
<field name="writeoff_label" attrs="{'required': [('payment_difference_handling', '=', 'reconcile'), ('payment_difference', '!=', 0.0)]}"/>
</div>
<field name="amount"
attrs="{'readonly': [('state', '!=', 'draft')]}"/>
<field name="currency_id"
options="{'no_create': True, 'no_open': True}"
required="1"
attrs="{'readonly': [('state', '!=', 'draft')]}"
groups="base.group_multi_currency"/>
</div>
<field name="date"
attrs="{'readonly': [('state', '!=', 'draft')]}"/>
<field name="ref" string="Memo"/>
</group>
<group>
@@ -223,31 +255,6 @@
</field>
</record>
<record id="view_account_payment_form_multi" model="ir.ui.view">
<field name="name">account.payment.form.multi</field>
<field name="model">account.payment.register</field>
<field name="arch" type="xml">
<form string="Register Payment">
<field name="invoice_company_id" invisible="1"/>
<field name="available_payment_methods" invisible="1"/>
<group>
<group>
<field name="payment_method_id" widget="radio"/>
<field name="group_payment"/>
</group>
<group>
<field name="journal_id" widget="selection"/>
<field name="payment_date"/>
</group>
</group>
<footer>
<button string="Create Payment" name="create_payments" type="object" class="oe_highlight"/>
<button string="Cancel" class="btn btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="view_account_payment_graph" model="ir.ui.view">
<field name="name">account.payment.graph</field>
<field name="model">account.payment</field>
@@ -260,6 +267,8 @@
</field>
</record>
<!-- ACTIONS -->
<record id="action_account_payments" model="ir.actions.act_window">
<field name="name">Payments</field>
<field name="res_model">account.payment</field>
@@ -268,7 +277,7 @@
'default_payment_type': 'inbound',
'default_partner_type': 'customer',
'search_default_inbound_filter': 1,
'res_partner_search_mode': 'customer',
'default_move_journal_types': ('bank', 'cash'),
}</field>
<field name="view_id" ref="view_account_payment_tree"/>
<field name="help" type="html">
@@ -288,7 +297,7 @@
'default_payment_type': 'outbound',
'default_partner_type': 'supplier',
'search_default_outbound_filter': 1,
'res_partner_search_mode': 'supplier',
'default_move_journal_types': ('bank', 'cash'),
}</field>
<field name="view_id" ref="view_account_supplier_payment_tree"/>
<field name="help" type="html">
@@ -316,33 +325,16 @@
</field>
</record>
<record id="view_account_payment_invoice_form" model="ir.ui.view">
<field name="name">account.payment.invoice.form</field>
<field name="model">account.payment</field>
<field name="inherit_id" ref="view_account_payment_form"/>
<field name="mode">primary</field>
<field name="arch" type="xml">
<xpath expr="//header" position='replace'></xpath>
<xpath expr="//div[@name='button_box']" position='replace'></xpath>
<xpath expr="//sheet" position='after'>
<footer>
<field name="state" invisible="1"/>
<button string='Validate' name="post" type="object" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</xpath>
</field>
</record>
<record id="action_account_invoice_from_list" model="ir.actions.server">
<field name="name">Register Payment</field>
<field name="groups_id" eval="[(4, ref('account.group_account_invoice'))]"/>
<field name="model_id" ref="account.model_account_payment"/>
<field name="model_id" ref="account.model_account_move"/>
<field name="binding_model_id" ref="account.model_account_move"/>
<field name="binding_view_types">list</field>
<field name="state">code</field>
<field name="code">
action = model.action_register_payment()
if records:
action = records.action_register_payment()
</field>
</record>
@@ -356,7 +348,7 @@
<field name="binding_model_id" ref="account.model_account_payment"/>
<field name="binding_view_types">list</field>
<field name="code">
records.post()
records.action_post()
</field>
</record>
+111 -59
View File
@@ -289,8 +289,22 @@
<field name="refund_sequence" attrs="{'invisible': [('type', 'not in', ['sale', 'purchase'])]}"/>
</group>
<group>
<field name="default_debit_account_id" options="{'no_create': True}" groups="account.group_account_readonly"/>
<field name="default_credit_account_id" options="{'no_create': True}" groups="account.group_account_readonly"/>
<field name="id" invisible="1"/>
<field name="default_debit_account_id" options="{'no_create': True}"
attrs="{'required': [('id', '!=', False), ('type', 'in', ('bank', 'cash'))]}"
groups="account.group_account_readonly"/>
<field name="default_credit_account_id" options="{'no_create': True}"
attrs="{'required': [('id', '!=', False), ('type', 'in', ('bank', 'cash'))]}"
groups="account.group_account_readonly"/>
<field name="payment_debit_account_id" options="{'no_create': True}"
attrs="{'required': [('id', '!=', False), ('type', 'in', ('bank', 'cash'))], 'invisible': [('type', 'not in', ('bank', 'cash'))]}"
groups="account.group_account_readonly"/>
<field name="payment_credit_account_id" options="{'no_create': True}"
attrs="{'required': [('id', '!=', False), ('type', 'in', ('bank', 'cash'))], 'invisible': [('type', 'not in', ('bank', 'cash'))]}"
groups="account.group_account_readonly"/>
<field name="suspense_account_id" options="{'no_create': True}"
attrs="{'required': [('id', '!=', False), ('type', 'in', ('bank', 'cash'))], 'invisible': [('type', 'not in', ('bank', 'cash'))]}"
groups="account.group_account_readonly"/>
<field name="currency_id" options="{'no_create': True}" groups="base.group_multi_currency"/>
</group>
</group>
@@ -313,16 +327,17 @@
<a type='action' name='%(action_open_settings)d' class="btn btn-link" role="button"><i class="fa fa-fw o_button_icon fa-arrow-right"/> Configure Email Servers</a>
</div>
</group>
<group string="Posting" name="column_posting" groups="account.group_account_readonly" attrs="{'invisible': [('type', 'not in', ['bank', 'cash'])]}">
<field name="profit_account_id" options="{'no_create': True}" attrs="{'invisible': [('type', '!=', 'cash')]}"/>
<field name="loss_account_id" options="{'no_create': True}" attrs="{'invisible': [('type', '!=', 'cash')]}"/>
<field name="post_at" widget="radio" attrs="{'invisible': [('type', 'not in', ['bank', 'cash'])]}"/>
<group string="Posting" name="column_posting" groups="account.group_account_readonly" attrs="{'invisible': [('type', '!=', 'cash')]}">
<field name="profit_account_id" options="{'no_create': True}"/>
<field name="loss_account_id" options="{'no_create': True}"/>
</group>
<group string="Control-Access" groups="account.group_account_manager">
<div class="text-muted" colspan="2">Keep empty for no control</div>
<field name="type_control_ids" widget="many2many_tags"/>
<field name="account_control_ids" widget="many2many_tags"/>
<field name="restrict_mode_hash_table" groups="account.group_account_readonly"/>
<field name="restrict_mode_hash_table"
attrs="{'invisible': [('type', 'in', ('bank', 'cash'))]}"
groups="account.group_account_readonly"/>
</group>
<group string="Payment Method Types" attrs="{'invisible': [('type', 'not in', ['bank', 'cash'])]}">
<field name="inbound_payment_method_ids" widget="many2many_checkboxes"/>
@@ -572,23 +587,31 @@ action = model.setting_init_bank_account_action()
<field name="arch" type="xml">
<form string="Bank Statement">
<header>
<field name="all_lines_reconciled" invisible="1" />
<button name="check_confirm_bank" string="Validate" type="object" class="oe_highlight" attrs="{'invisible':['|','|',('all_lines_reconciled','=',False),('line_ids','=',[]),('state', '!=', 'open')]}"/>
<button name="button_reopen" states="confirm" string="Reset to New" type="object"/>
<button string="Post" states="open" class="oe_highlight"
name="button_post" type="object"/>
<button string="Validate" class="oe_highlight"
name="button_validate_or_action" type="object"
attrs="{'invisible':['|', '|', ('all_lines_reconciled', '=', False), ('line_ids','=',[]), ('state', '!=', 'posted')]}"/>
<button string="Reset to New"
name="button_reopen" type="object"
attrs="{'invisible': [('state', '=', 'open')]}"/>
<button name="%(action_cash_box_out)d" string="Take Money In/Out" type="action" class="oe_highlight" attrs="{'invisible': [('journal_type','!=','cash')]}"/>
<field name="state" widget="statusbar" statusbar_visible="open,confirm"/>
<field name="state" widget="statusbar" statusbar_visible="open,posted,confirm"/>
</header>
<div class="alert alert-warning" role="alert" attrs="{'invisible': ['|', ('previous_statement_id', '=', False), ('is_valid_balance_start', '=', True)]}">
The starting balance <b>doesn't match</b> with the ending balance of the previous bank statement.
</div>
<sheet>
<!-- Invisible fields -->
<field name="all_lines_reconciled" invisible="1" />
<field name="move_line_count" invisible="1"/>
<div class="oe_button_box" name="button_box">
<button class="oe_stat_button" name="button_journal_entries"
string="Journal Entries" type="object"
attrs="{'invisible':[('move_line_count','=', 0)]}" icon="fa-bars" />
<field name="move_line_count" invisible="1"/>
attrs="{'invisible':[('move_line_count','=', 0)]}" icon="fa-bars"/>
</div>
<div class="oe_title oe_inline">
<label for="name" class="oe_edit_only"/>
<h1><field name="name"/></h1>
@@ -601,19 +624,20 @@ action = model.setting_init_bank_account_action()
<field name="cashbox_start_id" invisible="1"/>
<field name="cashbox_end_id" invisible="1"/>
<field name="journal_id" domain="[('type', '=', journal_type)]" attrs="{'readonly': [('move_line_count','!=', 0)]}" widget="selection"/>
<field name="date" options="{'datepicker': {'warn_future': true}}" />
<field name="accounting_date" groups="base.group_no_one"/>
<field name="date"
attrs="{'readonly': [('state', '!=', 'open')]}"
options="{'datepicker': {'warn_future': true}}"/>
<field name='company_id' options="{'no_create': True}" groups="base.group_multi_company" />
<field name="currency_id" invisible="1"/>
</group><group>
<label for="balance_start"/>
<div>
<field class="oe_inline" name="balance_start"/>
<field name="balance_start" class="oe_inline" attrs="{'readonly': [('state', '!=', 'open')]}"/>
<button name="open_cashbox_id" attrs="{'invisible': ['|',('state','!=','open'),('journal_type','!=','cash')]}" string="&#8594; Count" type="object" class="oe_edit_only oe_link oe_inline" context="{'balance':'start'}"/>
</div>
<label for="balance_end_real"/>
<div>
<field class="oe_inline" name="balance_end_real"/>
<field name="balance_end_real" class="oe_inline" attrs="{'readonly': [('state', '!=', 'open')]}"/>
<button name="open_cashbox_id" attrs="{'invisible': ['|',('state','!=','open'),('journal_type','!=','cash')]}" string="&#8594; Count" type="object" class="oe_edit_only oe_link oe_inline" context="{'balance':'close'}"/>
</div>
</group>
@@ -621,26 +645,44 @@ action = model.setting_init_bank_account_action()
<notebook>
<page string="Transactions" name="statement_line_ids">
<field name="line_ids" context="{'date':date}">
<tree editable="bottom" string="Statement lines" decoration-muted="journal_entry_ids" limit="500">
<field name="sequence" widget="handle"/>
<field name="journal_entry_ids" invisible="1"/>
<field name="date" attrs="{'readonly' : [('journal_entry_ids', '!=', [])] }"/>
<field name="name" attrs="{'readonly' : [('journal_entry_ids', '!=', [])] }"/>
<field name="partner_id" domain="['|',('parent_id','=',False),('is_company','=',True)]"
attrs="{'readonly' : [('journal_entry_ids', '!=', [])] }"/>
<field name="ref" attrs="{'readonly' : [('journal_entry_ids', '!=', [])] }" groups="base.group_no_one"/>
<field name="transaction_type" attrs="{'readonly' : [('journal_entry_ids', '!=', [])] }" optional="hidden"/>
<field name="note" optional="hidden"/>
<field name="amount" attrs="{'readonly' : [('journal_entry_ids', '!=', [])] }"/>
<field name="journal_currency_id" invisible="1"/>
<field name="bank_account_id" invisible="1" />
<field name="account_number" optional="hidden"/>
<field name="amount_currency" optional="hidden"/>
<field name="currency_id" optional="hidden"/>
<field name="state" invisible="1"/>
<field name="line_ids"
attrs="{'readonly': [('state', '!=', 'open')]}"
context="{'default_date': date, 'default_journal_id': journal_id}">
<tree string="Statement lines"
editable="bottom"
decoration-muted="is_reconciled"
limit="500">
<!-- Invisible fields -->
<field name="company_id" invisible="1"/>
<button name="button_cancel_reconciliation" attrs="{'invisible': ['|',('journal_entry_ids', '=', []), ('state', '=', 'confirm')]}" string="Revert reconciliation" type="object" icon="fa-undo"/>
<field name="state" invisible="1"/>
<field name="is_reconciled" invisible="1"/>
<field name="currency_id" invisible="1"/>
<field name="partner_bank_id" invisible="1"/>
<!-- Visible fields -->
<field name="sequence" widget="handle"/>
<field name="date"
attrs="{'readonly': [('parent.state', '!=', 'open')]}"/>
<field name="payment_ref"/>
<field name="partner_id"
attrs="{'readonly': [('parent.state', '!=', 'open')]}"
domain="['|', ('parent_id','=', False), ('is_company','=',True)]"/>
<field name="ref" optional="hidden"/>
<field name="transaction_type" optional="hidden"/>
<field name="narration" string="Notes" optional="hidden"/>
<field name="amount"
attrs="{'readonly': [('parent.state', '!=', 'open')]}"/>
<field name="amount_currency" optional="hidden" groups="base.group_multi_currency"
attrs="{'readonly': [('parent.state', '!=', 'open')]}"/>
<field name="account_number" optional="hidden"/>
<field name="foreign_currency_id" optional="hidden" groups="base.group_multi_currency"
attrs="{'readonly': [('parent.state', '!=', 'open')]}"/>
<!-- Buttons -->
<button name="button_undo_reconciliation" type="object"
attrs="{'invisible': [('is_reconciled', '=', False)]}"
string="Revert reconciliation" icon="fa-undo"/>
</tree>
</field>
</page>
@@ -700,12 +742,12 @@ action = model.setting_init_bank_account_action()
<field name="model">account.bank.statement.line</field>
<field name="arch" type="xml">
<search string="Search Bank Statements Line">
<field name="name"/>
<field name="payment_ref"/>
<field name="statement_id"/>
<field name="partner_id"/>
<field name="date"/>
<field name="journal_id" domain="[('type', 'in', ('bank', 'cash'))]" />
<field name="note"/>
<field name="narration" string="Notes"/>
<field name="transaction_type"/>
</search>
</field>
@@ -716,21 +758,27 @@ action = model.setting_init_bank_account_action()
<field name="model">account.bank.statement.line</field>
<field name="priority">8</field>
<field name="arch" type="xml">
<form string="Statement Line" create="false">
<group col="4">
<field name="statement_id"/>
<field name="date"/>
<field name="name"/>
<field name="ref"/>
<field name="partner_id"/>
<field name="amount"/>
<field name="journal_currency_id" invisible="1"/>
<field name="sequence"/>
<field name="note"/>
<field name="transaction_type"/>
<field name="company_id" invisible="1"/>
</group>
</form>
<form string="Statement Line" create="false">
<sheet>
<group col="4">
<!-- Invisible fields -->
<field name="state" invisible="1"/>
<field name="company_id" invisible="1"/>
<!-- Displayed fields -->
<field name="statement_id"/>
<field name="date"/>
<field name="payment_ref"/>
<field name="ref"/>
<field name="partner_id"/>
<field name="amount"/>
<field name="sequence"/>
<field name="narration" string="Notes"/>
<field name="transaction_type"/>
<field name="move_id"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_bank_statement_line_tree" model="ir.ui.view">
@@ -739,17 +787,21 @@ action = model.setting_init_bank_account_action()
<field name="priority">8</field>
<field name="arch" type="xml">
<tree string="Statement lines" create="false">
<!-- Invisible fields -->
<field name="state" invisible="1"/>
<field name="sequence" readonly="1" invisible="1"/>
<field name="statement_id" />
<field name="company_id" invisible="1"/>
<field name="journal_id" invisible="1" />
<!-- Displayed fields -->
<field name="statement_id" />
<field name="date"/>
<field name="name"/>
<field name="payment_ref"/>
<field name="ref" groups="base.group_no_one"/>
<field name="partner_id"/>
<field name="amount"/>
<field name="transaction_type" optional="hidden"/>
<field name="note" optional="hidden"/>
<field name="journal_currency_id" invisible="1"/>
<field name="narration" optional="hidden"/>
</tree>
</field>
</record>
@@ -766,7 +818,7 @@ action = model.setting_init_bank_account_action()
A bank statement line is a financial transaction on a bank account
</p><p>
Odoo allows you to reconcile a statement line directly with
the related sale or puchase invoice(s).
the related sale or purchase invoice(s).
</p>
</field>
</record>
+2 -2
View File
@@ -161,8 +161,8 @@
</div>
</div>
</div>
<p t-if="o.move_type in ('out_invoice', 'in_refund') and o.invoice_payment_ref">
Please use the following communication for your payment : <b><span t-field="o.invoice_payment_ref"/></b>
<p t-if="o.move_type in ('out_invoice', 'in_refund') and o.payment_reference">
Please use the following communication for your payment : <b><span t-field="o.payment_reference"/></b>
</p>
<p t-if="o.invoice_payment_term_id" name="payment_term">
<span t-field="o.invoice_payment_term_id.note"/>
@@ -6,8 +6,8 @@
<div class="page">
<h3><strong>Payment Receipt: <span t-field="o.name"/></strong></h3>
<div class="row mt64">
<div class="col-6" t-if="o.payment_date">
<strong>Payment Date: </strong> <span t-field="o.payment_date"/>
<div class="col-6" t-if="o.date">
<strong>Payment Date: </strong> <span t-field="o.date"/>
</div>
</div>
<div class="row">
@@ -27,8 +27,8 @@
<div class="col-6" t-if="o.amount">
<strong>Payment Amount: </strong><span t-field="o.amount" t-options="{'widget': 'monetary', 'display_currency': o.currency_id}"/>
</div>
<div class="col-6" t-if="o.communication">
<strong>Memo: </strong><span t-field="o.communication"/>
<div class="col-6" t-if="o.ref">
<strong>Memo: </strong><span t-field="o.ref"/>
</div>
</div>
<table class="table table-sm">
@@ -43,12 +43,14 @@
</tr>
</thead>
<tbody>
<tr t-foreach="o.reconciled_invoice_ids" t-as="inv">
<tr t-foreach="o.move_id._get_reconciled_invoices_partials()" t-as="rec">
<t t-set="amount" t-value="rec[0]"/>
<t t-set="inv" t-value="rec[2].move_id"/>
<td><span t-field="inv.invoice_date"/></td>
<td><span t-field="inv.name"/></td>
<td><span t-field="inv.ref"/></td>
<td class="text-right"><span t-field="inv.amount_total"/></td>
<td class="text-right"><span t-esc="o._get_invoice_payment_amount(inv)" t-options="{'widget': 'monetary', 'display_currency': inv.currency_id}"/></td>
<td class="text-right"><span t-esc="amount" t-options="{'widget': 'monetary', 'display_currency': inv.currency_id}"/></td>
<td class="text-right"><span t-field="inv.amount_residual"/></td>
</tr>
</tbody>
+4 -4
View File
@@ -25,7 +25,7 @@
<h5>
<strong>
<span t-field="o.journal_id"/>
<t t-if="o.journal_id.bank_account_id"> -
<t t-if="o.journal_id.bank_account_id"> -
<span t-field="o.journal_id.bank_account_id"/>
</t>
<t t-if="o.journal_id.code"> -
@@ -101,9 +101,9 @@
</td>
<td class="p-0">
<span class="d-block font-weight-bold" t-if="line.partner_id" t-field="line.partner_id"/>
<span class="d-block" t-if="line.bank_account_id" t-field="line.bank_account_id"/>
<span class="d-block" t-if="line.name" t-field="line.name"/>
<span class="d-block" t-if="line.note" t-field="line.note"/>
<span class="d-block" t-if="line.partner_bank_id" t-field="line.partner_bank_id"/>
<span class="d-block" t-if="line.payment_ref" t-field="line.payment_ref"/>
<span class="d-block" t-if="line.narration" t-field="line.narration"/>
</td>
<td class="text-right p-0">
<span class="d-block font-weight-bold" t-field="line.amount"/>
@@ -432,16 +432,6 @@
<t groups="account.group_account_user">
<h2>Bank &amp; Cash</h2>
<div class="row mt16 o_settings_container" id="bank_cash">
<div class="col-xs-12 col-md-6 o_setting_box">
<div class="o_setting_left_pane"/>
<div class="o_setting_right_pane">
<label for="account_bank_reconciliation_start"/>
<div class="text-muted">
The payments which have not been matched with a bank statement will not be shown in bank reconciliation data if they were made before this date
</div>
<field name="account_bank_reconciliation_start"/>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box"
id="account_yodlee"
title="Get your bank statements automatically imported every 4 hours, or in one-click, using Yodlee and Plaid services. Once installed, set “Bank Feeds” to “Bank Synchronization” in bank account settings. Then, click “Configure” on the online account to enter your bank credentials.">
+1
View File
@@ -16,3 +16,4 @@ from . import wizard_tax_adjustments
from . import account_invoice_send
from . import base_document_layout
from . import account_transfer_wizard
from . import account_payment_register
@@ -88,7 +88,7 @@ class AccountInvoiceSend(models.TransientModel):
if self.env.context.get('mark_invoice_as_sent'):
#Salesman send posted invoice, without the right to write
#but they should have the right to change this flag
self.mapped('invoice_ids').sudo().write({'invoice_sent': True})
self.mapped('invoice_ids').sudo().write({'is_move_sent': True})
def _print_document(self):
""" to override for each type of models that will use this composer."""
@@ -0,0 +1,504 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
from odoo.exceptions import UserError
class AccountPaymentRegister(models.TransientModel):
_name = 'account.payment.register'
_description = 'Register Payment'
# == Business fields ==
payment_date = fields.Date(String="Payment Date", required=True,
default=fields.Date.context_today)
amount = fields.Monetary(currency_field='currency_id', store=True, readonly=False,
compute='_compute_amount')
communication = fields.Char(string="Memo", store=True, readonly=False,
compute='_compute_communication')
group_payment = fields.Boolean(string="Group Payments", store=True, readonly=False,
compute='_compute_group_payment',
help="Only one payment will be created by partner (bank)/ currency.")
currency_id = fields.Many2one('res.currency', string='Currency', store=True, readonly=False,
compute='_compute_currency_id',
help="The payment's currency.")
journal_id = fields.Many2one('account.journal', store=True, readonly=False,
compute='_compute_journal_id',
domain="[('company_id', '=', company_id), ('type', 'in', ('bank', 'cash'))]")
partner_bank_id = fields.Many2one('res.partner.bank', string="Recipient Bank Account",
readonly=False, store=True,
compute='_compute_partner_bank_id',
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id), ('partner_id', '=', partner_id)]")
company_currency_id = fields.Many2one('res.currency', string="Company Currency",
related='company_id.currency_id')
# == Fields given through the context ==
line_ids = fields.Many2many('account.move.line', 'account_payment_register_move_line_rel', 'wizard_id', 'line_id',
string="Journal items", readonly=True, copy=False,)
payment_type = fields.Selection([
('outbound', 'Send Money'),
('inbound', 'Receive Money'),
], string='Payment Type', store=True, copy=False,
compute='_compute_from_lines')
partner_type = fields.Selection([
('customer', 'Customer'),
('supplier', 'Vendor'),
], store=True, copy=False,
compute='_compute_from_lines')
source_amount = fields.Monetary(
string="Amount to Pay (company currency)", store=True, copy=False,
currency_field='company_currency_id',
compute='_compute_from_lines')
source_amount_currency = fields.Monetary(
string="Amount to Pay (foreign currency)", store=True, copy=False,
currency_field='source_currency_id',
compute='_compute_from_lines')
source_currency_id = fields.Many2one('res.currency',
string='Source Currency', store=True, copy=False,
compute='_compute_from_lines',
help="The payment's currency.")
can_edit_wizard = fields.Boolean(store=True, copy=False,
compute='_compute_from_lines',
help="Technical field used to indicate the user can edit the wizard content such as the amount.")
can_group_payments = fields.Boolean(store=True, copy=False,
compute='_compute_from_lines',
help="Technical field used to indicate the user can see the 'group_payments' box.")
company_id = fields.Many2one('res.company', store=True, copy=False,
compute='_compute_from_lines')
partner_id = fields.Many2one('res.partner',
string="Customer/Vendor", store=True, copy=False, ondelete='restrict',
compute='_compute_from_lines')
# == Payment methods fields ==
payment_method_id = fields.Many2one('account.payment.method', string='Payment Method',
readonly=False, store=True,
compute='_compute_payment_method_id',
domain="[('id', 'in', available_payment_method_ids)]",
help="Manual: Get paid by cash, check or any other method outside of Odoo.\n"\
"Electronic: Get paid automatically through a payment acquirer by requesting a transaction on a card saved by the customer when buying or subscribing online (payment token).\n"\
"Check: Pay bill by check and print it from Odoo.\n"\
"Batch Deposit: Encase several customer checks at once by generating a batch deposit to submit to your bank. When encoding the bank statement in Odoo, you are suggested to reconcile the transaction with the batch deposit.To enable batch deposit, module account_batch_payment must be installed.\n"\
"SEPA Credit Transfer: Pay bill from a SEPA Credit Transfer file you submit to your bank. To enable sepa credit transfer, module account_sepa must be installed ")
available_payment_method_ids = fields.Many2many('account.payment.method',
compute='_compute_payment_method_fields')
hide_payment_method = fields.Boolean(
compute='_compute_payment_method_fields',
help="Technical field used to hide the payment method if the selected journal has only one available which is 'manual'")
# == Payment difference fields ==
payment_difference = fields.Monetary(
compute='_compute_payment_difference')
payment_difference_handling = fields.Selection([
('open', 'Keep open'),
('reconcile', 'Mark invoice as fully paid'),
], default='open', string="Payment Difference Handling")
writeoff_account_id = fields.Many2one('account.account', string="Difference Account", copy=False,
domain="[('deprecated', '=', False), ('company_id', '=', company_id)]")
writeoff_label = fields.Char(string='Journal Item Label', default='Write-Off',
help='Change label of the counterpart that will hold the payment difference')
# == Display purpose fields ==
show_partner_bank_account = fields.Boolean(
compute='_compute_show_require_partner_bank',
help="Technical field used to know whether the field `partner_bank_id` needs to be displayed or not in the payments form views")
require_partner_bank_account = fields.Boolean(
compute='_compute_show_require_partner_bank',
help="Technical field used to know whether the field `partner_bank_id` needs to be required or not in the payments form views")
# -------------------------------------------------------------------------
# HELPERS
# -------------------------------------------------------------------------
@api.model
def _get_batch_communication(self, batch_result):
''' Helper to compute the communication based on the batch.
:param batch_result: A batch returned by '_get_batches'.
:return: A string representing a communication to be set on payment.
'''
return ' '.join(label for label in batch_result['lines'].mapped('name') if label)
@api.model
def _get_line_batch_key(self, line):
''' Turn the line passed as parameter to a dictionary defining on which way the lines
will be grouped together.
:return: A python dictionary.
'''
return {
'partner_id': line.partner_id.id,
'account_id': line.account_id.id,
'currency_id': (line.currency_id or line.company_currency_id).id,
'partner_bank_id': line.move_id.partner_bank_id.id,
'partner_type': 'customer' if line.account_internal_type == 'receivable' else 'supplier',
'payment_type': 'inbound' if line.balance > 0.0 else 'outbound',
}
def _get_batches(self):
''' Group the account.move.line linked to the wizard together.
:return: A list of batches, each one containing:
* key_values: The key as a dictionary used to group the journal items together.
* moves: An account.move recordset.
'''
self.ensure_one()
lines = self.line_ids
if len(lines.company_id) > 1:
raise UserError(_("You can't create payments for entries belonging to different companies."))
if not lines:
raise UserError(_("You can't open the register payment wizard without at least one receivable/payable line."))
batches = {}
for line in lines:
batch_key = self._get_line_batch_key(line)
serialized_key = '-'.join(str(v) for v in batch_key.values())
batches.setdefault(serialized_key, {
'key_values': batch_key,
'lines': self.env['account.move.line'],
})
batches[serialized_key]['lines'] += line
return list(batches.values())
@api.model
def _get_wizard_values_from_batch(self, batch_result):
''' Extract values from the batch passed as parameter (see '_get_batches')
to be mounted in the wizard view.
:param batch_result: A batch returned by '_get_batches'.
:return: A dictionary containing valid fields
'''
key_values = batch_result['key_values']
lines = batch_result['lines']
company = lines[0].company_id
source_amount = abs(sum(lines.mapped('amount_residual')))
if key_values['currency_id'] == company.currency_id.id:
source_amount_currency = source_amount
else:
source_amount_currency = abs(sum(lines.mapped('amount_residual_currency')))
return {
'company_id': company.id,
'partner_id': key_values['partner_id'],
'partner_type': key_values['partner_type'],
'payment_type': key_values['payment_type'],
'source_currency_id': key_values['currency_id'],
'source_amount': source_amount,
'source_amount_currency': source_amount_currency,
}
# -------------------------------------------------------------------------
# COMPUTE METHODS
# -------------------------------------------------------------------------
@api.depends('line_ids')
def _compute_from_lines(self):
''' Load initial values from the account.moves passed through the context. '''
for wizard in self:
batches = wizard._get_batches()
if len(batches) == 1:
# == Single batch to be mounted on the view ==
batch_result = batches[0]
wizard.update(wizard._get_wizard_values_from_batch(batch_result))
wizard.can_edit_wizard = True
wizard.can_group_payments = len(batch_result['lines']) != 1
else:
# == Multiple batches: The wizard is not editable ==
wizard.update({
'company_id': batches[0]['lines'][0].company_id.id,
'partner_id': False,
'partner_type': False,
'payment_type': False,
'source_currency_id': False,
'source_amount': False,
'source_amount_currency': False,
})
wizard.can_edit_wizard = False
wizard.can_group_payments = any(len(batch_result['lines']) != 1 for batch_result in batches)
@api.depends('can_edit_wizard')
def _compute_communication(self):
# The communication can't be computed in '_compute_from_lines' because
# it's a compute editable field and then, should be computed in a separated method.
for wizard in self:
if wizard.can_edit_wizard:
batches = self._get_batches()
wizard.communication = wizard._get_batch_communication(batches[0])
else:
wizard.communication = False
@api.depends('can_edit_wizard')
def _compute_group_payment(self):
for wizard in self:
if wizard.can_edit_wizard:
batches = wizard._get_batches()
wizard.group_payment = len(batches[0]['lines'].move_id) == 1
else:
wizard.group_payment = False
@api.depends('company_id', 'source_currency_id')
def _compute_journal_id(self):
for wizard in self:
domain = [
('type', 'in', ('bank', 'cash')),
('company_id', '=', wizard.company_id.id),
]
journal = None
if wizard.source_currency_id:
journal = self.env['account.journal'].search(domain + [('currency_id', '=', wizard.source_currency_id.id)], limit=1)
if not journal:
journal = self.env['account.journal'].search(domain, limit=1)
wizard.journal_id = journal
@api.depends('journal_id')
def _compute_currency_id(self):
for wizard in self:
wizard.currency_id = wizard.journal_id.currency_id or wizard.source_currency_id or wizard.company_id.currency_id
@api.depends('partner_id')
def _compute_partner_bank_id(self):
''' The default partner_bank_id will be the first available on the partner. '''
for wizard in self:
available_partner_bank_accounts = wizard.partner_id.bank_ids
if available_partner_bank_accounts:
wizard.partner_bank_id = available_partner_bank_accounts[0]._origin
else:
wizard.partner_bank_id = False
@api.depends('journal_id')
def _compute_payment_method_id(self):
for wizard in self:
batches = wizard._get_batches()
payment_type = batches[0]['key_values']['payment_type']
if payment_type == 'inbound':
available_payment_methods = wizard.journal_id.inbound_payment_method_ids
else:
available_payment_methods = wizard.journal_id.outbound_payment_method_ids
# Select the first available one by default.
if available_payment_methods:
wizard.payment_method_id = available_payment_methods[0]._origin
else:
wizard.payment_method_id = False
@api.depends('payment_type',
'journal_id.inbound_payment_method_ids',
'journal_id.outbound_payment_method_ids')
def _compute_payment_method_fields(self):
for wizard in self:
if wizard.payment_type == 'inbound':
wizard.available_payment_method_ids = wizard.journal_id.inbound_payment_method_ids
else:
wizard.available_payment_method_ids = wizard.journal_id.outbound_payment_method_ids
wizard.hide_payment_method = len(wizard.available_payment_method_ids) == 1 and wizard.available_payment_method_ids.code == 'manual'
@api.depends('payment_type',
'journal_id.inbound_payment_method_ids',
'journal_id.outbound_payment_method_ids')
def _compute_payment_method_id(self):
for wizard in self:
if wizard.payment_type == 'inbound':
available_payment_methods = wizard.journal_id.inbound_payment_method_ids
else:
available_payment_methods = wizard.journal_id.outbound_payment_method_ids
# Select the first available one by default.
if available_payment_methods:
wizard.payment_method_id = available_payment_methods[0]._origin
else:
wizard.payment_method_id = False
@api.depends('payment_method_id')
def _compute_show_require_partner_bank(self):
""" Computes if the destination bank account must be displayed in the payment form view. By default, it
won't be displayed but some modules might change that, depending on the payment type."""
for wizard in self:
wizard.show_partner_bank_account = wizard.payment_method_id.code in self.env['account.payment']._get_method_codes_using_bank_account()
wizard.require_partner_bank_account = wizard.payment_method_id.code in self.env['account.payment']._get_method_codes_needing_bank_account()
@api.depends('source_amount', 'source_amount_currency', 'source_currency_id', 'company_id', 'currency_id', 'payment_date')
def _compute_amount(self):
for wizard in self:
if wizard.source_currency_id == wizard.currency_id:
# Same currency.
wizard.amount = wizard.source_amount_currency
elif wizard.currency_id == wizard.company_id.currency_id:
# Payment expressed on the company's currency.
wizard.amount = wizard.source_amount
else:
# Foreign currency on payment different than the one set on the journal entries.
amount_payment_currency = wizard.company_id.currency_id._convert(wizard.source_amount, wizard.currency_id, wizard.company_id, wizard.payment_date)
wizard.amount = amount_payment_currency
@api.depends('amount')
def _compute_payment_difference(self):
for wizard in self:
if wizard.source_currency_id == wizard.currency_id:
# Same currency.
wizard.payment_difference = wizard.source_amount_currency - wizard.amount
elif wizard.currency_id == wizard.company_id.currency_id:
# Payment expressed on the company's currency.
wizard.payment_difference = wizard.source_amount - wizard.amount
else:
# Foreign currency on payment different than the one set on the journal entries.
amount_payment_currency = wizard.company_id.currency_id._convert(wizard.source_amount, wizard.currency_id, wizard.company_id, wizard.payment_date)
wizard.payment_difference = amount_payment_currency - wizard.amount
# -------------------------------------------------------------------------
# LOW-LEVEL METHODS
# -------------------------------------------------------------------------
@api.model
def default_get(self, fields_list):
# OVERRIDE
res = super().default_get(fields_list)
if 'line_ids' in fields_list and 'line_ids' not in res:
# Retrieve moves to pay from the context.
if self._context.get('active_model') == 'account.move':
lines = self.env['account.move'].browse(self._context.get('active_ids', [])).line_ids
elif self._context.get('active_model') == 'account.move.line':
lines = self.env['account.move.line'].browse(self._context.get('active_ids', []))
else:
raise UserError(_(
"The register payment wizard should only be called on account.move or account.move.line records."
))
# Keep lines having a residual amount to pay.
available_lines = self.env['account.move.line']
for line in lines:
if line.move_id.state != 'posted':
raise UserError(_("You can only register payment for posted journal entries."))
if line.account_internal_type not in ('receivable', 'payable'):
continue
if line.currency_id:
if line.currency_id.is_zero(line.amount_residual_currency):
continue
else:
if line.company_currency_id.is_zero(line.amount_residual):
continue
available_lines |= line
# Check.
if not available_lines:
raise UserError(_("You can't register a payment because there is nothing left to pay on the selected journal items."))
if len(lines.company_id) > 1:
raise UserError(_("You can't create payments for entries belonging to different companies."))
if len(set(available_lines.mapped('account_internal_type'))) > 1:
raise UserError(_("You can't register payments for journal items being either all inbound, either all outbound."))
res['line_ids'] = [(6, 0, available_lines.ids)]
return res
# -------------------------------------------------------------------------
# BUSINESS METHODS
# -------------------------------------------------------------------------
def _create_payment_vals_from_wizard(self):
payment_vals = {
'date': self.payment_date,
'amount': self.amount,
'payment_type': self.payment_type,
'partner_type': self.partner_type,
'ref': self.communication,
'journal_id': self.journal_id.id,
'currency_id': self.currency_id.id,
'partner_id': self.partner_id.id,
'partner_bank_id': self.partner_bank_id.id,
'payment_method_id': self.payment_method_id.id,
'destination_account_id': self.line_ids[0].account_id.id
}
if self.payment_difference and self.payment_difference_handling == 'reconcile':
payment_vals['write_off_line_vals'] = {
'name': self.writeoff_label,
'amount': -self.payment_difference,
'account_id': self.writeoff_account_id.id,
}
return payment_vals
def _create_payment_vals_from_batch(self, batch_result):
batch_values = self._get_wizard_values_from_batch(batch_result)
return {
'date': self.payment_date,
'amount': batch_values['source_amount_currency'],
'payment_type': batch_values['payment_type'],
'partner_type': batch_values['partner_type'],
'ref': self._get_batch_communication(batch_result),
'journal_id': self.journal_id.id,
'currency_id': batch_values['source_currency_id'],
'partner_id': batch_values['partner_id'],
'partner_bank_id': batch_result['key_values']['partner_bank_id'],
'payment_method_id': self.payment_method_id.id,
'destination_account_id': batch_result['lines'][0].account_id.id
}
def _create_payments(self):
self.ensure_one()
batches = self._get_batches()
to_reconcile = []
if self.can_edit_wizard and (len(batches[0]['lines']) == 1 or self.group_payment):
payment_vals = self._create_payment_vals_from_wizard()
payment_vals_list = [payment_vals]
to_reconcile.append(batches[0]['lines'])
else:
# Don't group payments: Create one batch per move.
if not self.group_payment:
new_batches = []
for batch_result in batches:
for line in batch_result['lines']:
new_batches.append({
**batch_result,
'lines': line,
})
batches = new_batches
payment_vals_list = []
for batch_result in batches:
payment_vals_list.append(self._create_payment_vals_from_batch(batch_result))
to_reconcile.append(batch_result['lines'])
payments = self.env['account.payment'].create(payment_vals_list)
payments.action_post()
domain = [('account_internal_type', 'in', ('receivable', 'payable')), ('reconciled', '=', False)]
for payment, lines in zip(payments, to_reconcile):
payment_lines = payment.line_ids.filtered_domain(domain)
for account in payment_lines.account_id:
(payment_lines + lines)\
.filtered_domain([('account_id', '=', account.id), ('reconciled', '=', False)])\
.reconcile()
return payments
def action_create_payments(self):
payments = self._create_payments()
if self._context.get('dont_redirect_to_payments'):
return True
action = {
'name': _('Payments'),
'type': 'ir.actions.act_window',
'res_model': 'account.payment',
'context': {'create': False},
}
if len(payments) == 1:
action.update({
'view_mode': 'form',
'res_id': payments.id,
})
else:
action.update({
'view_mode': 'tree,form',
'domain': [('id', 'in', payments.ids)],
})
return action
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="view_account_payment_register_form" model="ir.ui.view">
<field name="name">account.payment.register.form</field>
<field name="model">account.payment.register</field>
<field name="arch" type="xml">
<form string="Register Payment">
<!-- Invisible fields -->
<field name="line_ids" invisible="1"/>
<field name="can_edit_wizard" invisible="1" force_save="1"/>
<field name="can_group_payments" invisible="1" force_save="1"/>
<field name="payment_type" invisible="1" force_save="1"/>
<field name="partner_type" invisible="1" force_save="1"/>
<field name="source_amount" invisible="1" force_save="1"/>
<field name="source_amount_currency" invisible="1" force_save="1"/>
<field name="source_currency_id" invisible="1" force_save="1"/>
<field name="company_id" invisible="1" force_save="1"/>
<field name="partner_id" invisible="1" force_save="1"/>
<field name="show_partner_bank_account" invisible="1"/>
<field name="require_partner_bank_account" invisible="1"/>
<field name="hide_payment_method" invisible="1"/>
<field name="available_payment_method_ids" invisible="1"/>
<field name="company_currency_id" invisible="1"/>
<group>
<group name="group1">
<field name="journal_id" widget="selection"/>
<field name="payment_method_id" widget="radio"
required="1"
attrs="{'invisible': [('hide_payment_method', '=', True)]}"/>
<field name="partner_bank_id"
attrs="{'invisible': ['|', ('show_partner_bank_account', '=', False), '|', ('can_edit_wizard', '=', False), '&amp;', ('can_group_payments', '=', True), ('group_payment', '=', False)],
'required': [('require_partner_bank_account', '=', True), ('can_edit_wizard', '=', True), '|', ('can_group_payments', '=', False), ('group_payment', '=', False)]}"/>
<field name="group_payment"
attrs="{'invisible': [('can_group_payments', '=', False)]}"/>
</group>
<group name="group2">
<label for="amount"
attrs="{'invisible': ['|', ('can_edit_wizard', '=', False), '&amp;', ('can_group_payments', '=', True), ('group_payment', '=', False)]}"/>
<div name="amount_div" class="o_row"
attrs="{'invisible': ['|', ('can_edit_wizard', '=', False), '&amp;', ('can_group_payments', '=', True), ('group_payment', '=', False)]}">
<field name="amount"/>
<field name="currency_id"
options="{'no_create': True, 'no_open': True}"
groups="base.group_multi_currency"/>
</div>
<field name="payment_date"/>
<field name="communication"
attrs="{'invisible': ['|', ('can_edit_wizard', '=', False), '&amp;', ('can_group_payments', '=', True), ('group_payment', '=', False)]}"/>
</group>
<group name="group3"
attrs="{'invisible': ['|', ('payment_difference', '=', 0.0), '|', ('can_edit_wizard', '=', False), '&amp;', ('can_group_payments', '=', True), ('group_payment', '=', False)]}"
groups="account.group_account_readonly">
<label for="payment_difference"/>
<div>
<field name="payment_difference"/>
<field name="payment_difference_handling" widget="radio" nolabel="1"/>
<div attrs="{'invisible': [('payment_difference_handling','=','open')]}">
<label for="writeoff_account_id" string="Post Difference In" class="oe_edit_only"/>
<field name="writeoff_account_id"
string="Post Difference In"
options="{'no_create': True}"
attrs="{'required': [('payment_difference_handling', '=', 'reconcile')]}"/>
<label for="writeoff_label" class="oe_edit_only" string="Label"/>
<field name="writeoff_label" attrs="{'required': [('payment_difference_handling', '=', 'reconcile')]}"/>
</div>
</div>
</group>
</group>
<footer>
<button string="Create Payment" name="action_create_payments" type="object" class="oe_highlight"/>
<button string="Cancel" class="btn btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
</data>
</odoo>
+1 -1
View File
@@ -93,7 +93,7 @@ class SetupBarBankConfigWizard(models.TransientModel):
def _onchange_new_journal_code(self):
for record in self:
if not record.linked_journal_id:
record.new_journal_code = self.env['account.journal'].get_next_bank_cash_default_code('bank', self.env.company.id)
record.new_journal_code = self.env['account.journal'].get_next_bank_cash_default_code('bank', self.env.company)
else:
record.new_journal_code = self.linked_journal_id.code
@@ -7,31 +7,38 @@ from odoo.tools.misc import formatLang, format_date
INV_LINES_PER_STUB = 9
class AccountPaymentRegister(models.TransientModel):
_inherit = "account.payment.register"
def _prepare_payment_vals(self, invoices):
res = super(AccountPaymentRegister, self)._prepare_payment_vals(invoices)
if self.payment_method_id == self.env.ref('account_check_printing.account_payment_method_check'):
currency_id = self.env['res.currency'].browse(res['currency_id'])
res.update({
'check_amount_in_words': currency_id.amount_to_text(res['amount']),
})
return res
class AccountPayment(models.Model):
_inherit = "account.payment"
check_amount_in_words = fields.Char(string="Amount in Words")
check_manual_sequencing = fields.Boolean(related='journal_id.check_manual_sequencing', readonly=1)
check_number = fields.Char(string="Check Number", readonly=True, copy=False,
check_amount_in_words = fields.Char(string="Amount in Words", store=True, readonly=False,
compute='_compute_check_amount_in_words')
check_manual_sequencing = fields.Boolean(related='journal_id.check_manual_sequencing')
check_number = fields.Char(string="Check Number", store=True, readonly=True,
compute='_compute_check_number',
help="The selected journal is configured to print check numbers. If your pre-printed check paper already has numbers "
"or if the current numbering is wrong, you can change it in the journal configuration page.")
check_number_int = fields.Integer(string="Check Number Integer", store=True, compute='_compute_check_number')
check_number_int = fields.Integer(string="Check Number Integer", store=True,
compute='_compute_check_number_int')
@api.depends('payment_method_id', 'currency_id', 'amount')
def _compute_check_amount_in_words(self):
for pay in self:
if pay.currency_id and pay.payment_method_id == self.env.ref('account_check_printing.account_payment_method_check'):
pay.check_amount_in_words = pay.currency_id.amount_to_text(pay.amount)
else:
pay.check_amount_in_words = False
@api.depends('journal_id')
def _compute_check_number(self):
for pay in self:
if pay.journal_id.check_manual_sequencing:
pay.check_number = pay.journal_id.check_sequence_id.number_next_actual
else:
pay.check_number = False
@api.depends('check_number')
def _compute_check_number(self):
def _compute_check_number_int(self):
# store check number as int to avoid doing a lot of checks and transformations
# when calculating next_check_number
for record in self:
@@ -42,29 +49,8 @@ class AccountPayment(models.Model):
number = 0
record.check_number_int = number
@api.onchange('amount', 'currency_id')
def _onchange_amount(self):
res = super(AccountPayment, self)._onchange_amount()
self.check_amount_in_words = self.currency_id.amount_to_text(self.amount) if self.currency_id else ''
return res
@api.onchange('journal_id')
def _onchange_journal_id(self):
if hasattr(super(AccountPayment, self), '_onchange_journal_id'):
super(AccountPayment, self)._onchange_journal_id()
if self.journal_id.check_manual_sequencing:
self.check_number = self.journal_id.check_sequence_id.number_next_actual
def _check_communication(self, payment_method_id, communication):
super(AccountPayment, self)._check_communication(payment_method_id, communication)
if payment_method_id == self.env.ref('account_check_printing.account_payment_method_check').id:
if not communication:
return
if len(communication) > 60:
raise ValidationError(_("A check memo cannot exceed 60 characters."))
def post(self):
res = super(AccountPayment, self).post()
def action_post(self):
res = super(AccountPayment, self).action_post()
payment_method_check = self.env.ref('account_check_printing.account_payment_method_check')
for payment in self.filtered(lambda p: p.payment_method_id == payment_method_check and p.check_manual_sequencing):
sequence = payment.journal_id.check_sequence_id
@@ -105,8 +91,8 @@ class AccountPayment(models.Model):
self.filtered(lambda r: r.state == 'draft').post()
return self.do_print_checks()
def unmark_sent(self):
self.write({'state': 'posted'})
def action_unmark_sent(self):
self.write({'is_move_sent': False})
def do_print_checks(self):
check_layout = self[0].company_id.account_check_printing_layout
@@ -115,13 +101,9 @@ class AccountPayment(models.Model):
report_action = self.env.ref(check_layout, False)
if not report_action:
raise UserError(_("Something went wrong with Check Layout, please select another layout in Invoicing/Accounting Settings and try again."))
self.write({'state': 'sent'})
self.write({'is_move_sent': True})
return report_action.report_action(self)
def set_check_amount_in_words(self):
for payment in self:
payment.check_amount_in_words = payment.currency_id.amount_to_text(payment.amount) if payment.currency_id else ''
#######################
#CHECK PRINTING METHODS
#######################
@@ -132,15 +114,15 @@ class AccountPayment(models.Model):
multi_stub = self.company_id.account_check_printing_multi_stub
return {
'sequence_number': self.check_number if (self.journal_id.check_manual_sequencing and self.check_number != 0) else False,
'payment_date': format_date(self.env, self.payment_date),
'date': format_date(self.env, self.date),
'partner_id': self.partner_id,
'partner_name': self.partner_id.name,
'currency': self.currency_id,
'state': self.state,
'amount': formatLang(self.env, self.amount, currency_obj=self.currency_id) if i == 0 else 'VOID',
'amount_in_word': self._check_fill_line(self.check_amount_in_words) if i == 0 else 'VOID',
'memo': self.communication,
'stub_cropped': not multi_stub and len(self.invoice_ids) > INV_LINES_PER_STUB,
'memo': self.ref,
'stub_cropped': not multi_stub and len(self.move_id._get_reconciled_invoices()) > INV_LINES_PER_STUB,
# If the payment does not reference an invoice, there is no stub line to display
'stub_lines': p,
}
@@ -158,12 +140,12 @@ class AccountPayment(models.Model):
""" The stub is the summary of paid invoices. It may spill on several pages, in which case only the check on
first page is valid. This function returns a list of stub lines per page.
"""
if len(self.reconciled_invoice_ids) == 0:
if len(self.move_id._get_reconciled_invoices()) == 0:
return None
multi_stub = self.company_id.account_check_printing_multi_stub
invoices = self.reconciled_invoice_ids.sorted(key=lambda r: r.invoice_date_due)
invoices = self.move_id._get_reconciled_invoices().sorted(key=lambda r: r.invoice_date_due)
debits = invoices.filtered(lambda r: r.move_type == 'in_invoice')
credits = invoices.filtered(lambda r: r.move_type == 'in_refund')
@@ -201,10 +183,10 @@ class AccountPayment(models.Model):
# Find the account.partial.reconcile which are common to the invoice and the payment
if invoice.move_type in ['in_invoice', 'out_refund']:
invoice_sign = 1
invoice_payment_reconcile = invoice.line_ids.mapped('matched_debit_ids').filtered(lambda r: r.debit_move_id in self.move_line_ids)
invoice_payment_reconcile = invoice.line_ids.mapped('matched_debit_ids').filtered(lambda r: r.debit_move_id in self.line_ids)
else:
invoice_sign = -1
invoice_payment_reconcile = invoice.line_ids.mapped('matched_credit_ids').filtered(lambda r: r.credit_move_id in self.move_line_ids)
invoice_payment_reconcile = invoice.line_ids.mapped('matched_credit_ids').filtered(lambda r: r.credit_move_id in self.line_ids)
if self.currency_id != self.journal_id.company_id.currency_id:
amount_paid = abs(sum(invoice_payment_reconcile.mapped('amount_currency')))
@@ -1,81 +1,105 @@
# Make sure / performs a floating point division even if environment is python 2
from __future__ import division
from odoo.addons.account.tests.common import AccountTestCommon
# -*- coding: utf-8 -*-
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.addons.account_check_printing.models.account_payment import INV_LINES_PER_STUB
from odoo.tests import tagged
from odoo.tests.common import Form
import time
import math
@tagged('post_install', '-at_install')
class TestPrintCheck(AccountTestCommon):
class TestPrintCheck(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls):
super(TestPrintCheck, cls).setUpClass()
super().setUpClass()
cls.invoice_model = cls.env['account.move']
cls.invoice_line_model = cls.env['account.move.line']
cls.payment_model = cls.env['account.payment']
cls.partner_axelor = cls.env['res.partner'].create({'name': 'A Partner'})
cls.product = cls.env['product.product'].create({'name': 'A test Product'})
cls.payment_method_check = cls.env.ref("account_check_printing.account_payment_method_check")
cls.account_payable = cls.env['account.account'].search([('user_type_id', '=', cls.env.ref('account.data_account_type_payable').id)], limit=1)
cls.account_expenses = cls.env['account.account'].search([('user_type_id', '=', cls.env.ref('account.data_account_type_expenses').id)], limit=1)
cls.bank_journal = cls.env['account.journal'].create({'name': 'Bank', 'type': 'bank', 'code': 'BNK67'})
cls.bank_journal.check_manual_sequencing = True
def create_invoice(self, amount=100, is_refund=False):
invoice = self.env['account.move'].with_context(default_move_type=is_refund and 'out_refund' or 'in_invoice').create({
'partner_id': self.partner_axelor.id,
'invoice_date': time.strftime('%Y') + '-06-26',
'date': time.strftime('%Y') + '-06-26',
'invoice_line_ids': [
(0, 0, {
'product_id': self.product.id,
'quantity': 1,
'price_unit': is_refund and amount / 4 or amount,
})
]
cls.company_data['default_journal_bank'].write({
'outbound_payment_method_ids': [(6, 0, (
cls.env.ref('account.account_payment_method_manual_out').id,
cls.payment_method_check.id,
))],
})
invoice.post()
return invoice
def create_payment(self, invoices):
payment_register = Form(self.env['account.payment'].with_context(active_model='account.move', active_ids=invoices.ids))
payment_register.payment_date = time.strftime('%Y') + '-07-15'
payment_register.journal_id = self.bank_journal
payment_register.payment_method_id = self.payment_method_check
payment = payment_register.save()
payment.post()
return payment
def test_inbound_check_manual_sequencing(self):
''' Test the check generation for customer invoices. '''
nb_invoices_to_test = INV_LINES_PER_STUB + 1
def test_print_check(self):
# Make a payment for 10 invoices and 5 credit notes
invoices = self.env['account.move']
for i in range(0, 15):
invoices |= self.create_invoice(is_refund=(i % 3 == 0))
payment = self.create_payment(invoices)
self.assertEqual(all(payment.mapped('check_amount_in_words')), True, 'The amount in words is not set on all the payments')
self.assertEqual(all(payment.mapped('check_number')), True, 'The check number is not set on all the payments')
self.company_data['default_journal_bank'].write({
'check_manual_sequencing': True,
'check_next_number': 42,
})
# Check the data generated for the report
self.env.ref('base.main_company').write({'account_check_printing_multi_stub': True})
# Create 10 customer invoices.
out_invoices = self.env['account.move'].create([{
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'date': '2017-01-01',
'invoice_date': '2017-01-01',
'invoice_line_ids': [(0, 0, {'product_id': self.product_a.id, 'price_unit': 100.0})]
} for i in range(nb_invoices_to_test)])
out_invoices.post()
# Create a single payment.
payment = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=out_invoices.ids).create({
'group_payment': True,
'payment_method_id': self.payment_method_check.id,
})._create_payments()
# Check created payment.
self.assertRecordValues(payment, [{
'payment_method_id': self.payment_method_check.id,
'check_amount_in_words': payment.currency_id.amount_to_text(100.0 * nb_invoices_to_test),
'check_number': '00042',
}])
# Check pages.
self.company_data['company'].account_check_printing_multi_stub = True
report_pages = payment._check_get_pages()
self.assertEqual(len(report_pages), int(math.ceil(len(payment.reconciled_invoice_ids) / INV_LINES_PER_STUB)))
self.env.ref('base.main_company').write({'account_check_printing_multi_stub': False})
self.assertEqual(len(report_pages), int(math.ceil(len(out_invoices) / INV_LINES_PER_STUB)))
self.company_data['company'].account_check_printing_multi_stub = False
report_pages = payment._check_get_pages()
self.assertEqual(len(report_pages), 1)
def test_from_register(self):
invoices = self.env['account.move']
for i in range(0, 3):
invoices |= self.create_invoice(is_refund=(i % 3 == 0))
payment = self.create_payment(invoices)
self.assertEqual(all(payment.mapped('check_amount_in_words')), True, 'The amount in words is not set on all the payments')
def test_outbound_check_manual_sequencing(self):
''' Test the check generation for refunds. '''
nb_invoices_to_test = INV_LINES_PER_STUB + 1
self.company_data['default_journal_bank'].write({
'check_manual_sequencing': True,
'check_next_number': 42,
})
# Create 10 refunds.
out_refunds = self.env['account.move'].create([{
'move_type': 'out_refund',
'partner_id': self.partner_a.id,
'date': '2017-01-01',
'invoice_date': '2017-01-01',
'invoice_line_ids': [(0, 0, {'product_id': self.product_a.id, 'price_unit': 100.0})]
} for i in range(nb_invoices_to_test)])
out_refunds.post()
# Create a single payment.
payment = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=out_refunds.ids).create({
'group_payment': True,
'payment_method_id': self.payment_method_check.id,
})._create_payments()
# Check created payment.
self.assertRecordValues(payment, [{
'payment_method_id': self.payment_method_check.id,
'check_amount_in_words': payment.currency_id.amount_to_text(100.0 * nb_invoices_to_test),
'check_number': '00042',
}])
# Check pages.
self.company_data['company'].account_check_printing_multi_stub = True
report_pages = payment._check_get_pages()
self.assertEqual(len(report_pages), int(math.ceil(len(out_refunds) / INV_LINES_PER_STUB)))
self.company_data['company'].account_check_printing_multi_stub = False
report_pages = payment._check_get_pages()
self.assertEqual(len(report_pages), 1)
@@ -5,17 +5,16 @@
<field name="model">account.payment</field>
<field name="inherit_id" ref="account.view_account_payment_form" />
<field name="arch" type="xml">
<xpath expr="//button[@name='post']" position="before">
<xpath expr="//button[@name='action_post']" position="before">
<button name="print_checks" class="oe_highlight" attrs="{'invisible': ['|', ('payment_method_code', '!=', 'check_printing'), ('state', '!=', 'posted')]}" string="Print Check" type="object"/>
<button name="unmark_sent" attrs="{'invisible': ['|', ('payment_method_code', '!=', 'check_printing'), ('state', '!=', 'sent')]}" string="Unmark Sent" type="object"/>
<button name="set_check_amount_in_words" attrs="{'invisible': ['|', ('payment_method_code', '!=', 'check_printing'), ('check_amount_in_words', '!=', False)]}" string="Reset Amount in Words" type="object" groups="base.group_no_one"/>
<button name="action_unmark_sent" attrs="{'invisible': ['|', ('payment_method_code', '!=', 'check_printing'), ('is_move_sent', '=', False)]}" string="Unmark Sent" type="object"/>
</xpath>
<xpath expr="//div[@name='amount_div']" position="after">
<field name="check_amount_in_words" attrs="{'invisible': [('payment_method_code', '!=', 'check_printing')], 'readonly': [('state', '!=', 'draft')]}" groups="base.group_no_one"/>
</xpath>
<xpath expr="//field[@name='communication']" position="after">
<field name="check_manual_sequencing" invisible="1" readonly="1"/>
<field name="check_number" attrs="{'invisible': ['|', ('payment_method_code', '!=', 'check_printing'), ('check_number', '=', '0')]}"/>
<xpath expr="//field[@name='ref']" position="after">
<field name="check_manual_sequencing" invisible="1"/>
<field name="check_number" attrs="{'invisible': ['|', ('payment_method_code', '!=', 'check_printing'), ('check_number', '=', False)]}"/>
</xpath>
</field>
</record>
@@ -21,7 +21,7 @@ class PrintPreNumberedChecks(models.TransientModel):
def print_checks(self):
check_number = int(self.next_check_number)
payments = self.env['account.payment'].browse(self.env.context['payment_ids'])
payments.filtered(lambda r: r.state == 'draft').post()
payments.filtered(lambda r: r.state == 'draft').action_post()
payments.filtered(lambda r: r.state not in ('sent', 'cancelled')).write({'state': 'sent'})
for payment in payments:
payment.check_number = check_number
@@ -166,7 +166,7 @@
<!-- Reference. -->
<ram:BuyerOrderReferencedDocument>
<ram:IssuerAssignedID t-esc="'%s: %s' % (record.name, record.invoice_payment_ref) if record.invoice_payment_ref else record.name"/>
<ram:IssuerAssignedID t-esc="'%s: %s' % (record.name, record.payment_reference) if record.payment_reference else record.name"/>
</ram:BuyerOrderReferencedDocument>
</ram:ApplicableHeaderTradeAgreement>
@@ -184,9 +184,9 @@
<ram:ApplicableHeaderTradeSettlement>
<!-- Bank account. -->
<ram:SpecifiedTradeSettlementPaymentMeans t-if="record.invoice_partner_bank_id.acc_type == 'iban'">
<ram:SpecifiedTradeSettlementPaymentMeans t-if="record.partner_bank_id.acc_type == 'iban'">
<ram:PayeePartyCreditorFinancialAccount>
<ram:IBANID t-esc="record.invoice_partner_bank_id.sanitized_acc_number"/>
<ram:IBANID t-esc="record.partner_bank_id.sanitized_acc_number"/>
</ram:PayeePartyCreditorFinancialAccount>
</ram:SpecifiedTradeSettlementPaymentMeans>
@@ -119,7 +119,7 @@ class AccountMove(models.Model):
# Name.
elements = tree.xpath('//ram:BuyerOrderReferencedDocument/ram:IssuerAssignedID', namespaces=tree.nsmap)
if elements:
invoice_form.invoice_payment_ref = elements[0].text
invoice_form.payment_reference = elements[0].text
# Comment.
elements = tree.xpath('//ram:IncludedNote/ram:Content', namespaces=tree.nsmap)
@@ -88,7 +88,7 @@
<div t-if="invoice.transaction_ids and invoice.amount_total and not success and not error" class="o_account_payment_tx_status" t-att-data-invoice-id="invoice.id">
<t t-call="payment.payment_confirmation_status">
<t t-set="payment_tx_id" t-value="invoice.get_portal_last_transaction()"/>
<t t-set="reference" t-value="invoice.invoice_payment_ref"/>
<t t-set="reference" t-value="invoice.payment_reference"/>
</t>
</div>
<t t-set="tx_ids" t-value="invoice.transaction_ids.filtered(lambda tx: tx.state in ('authorized', 'done'))"/>
@@ -26,7 +26,7 @@ class TestSEPAQRCode(AccountTestInvoicingCommon):
'move_type': 'out_invoice',
'partner_id': cls.partner_a.id,
'currency_id': cls.env.ref('base.EUR').id,
'invoice_partner_bank_id': cls.acc_sepa_iban.id,
'partner_bank_id': cls.acc_sepa_iban.id,
'company_id': cls.company_data['company'].id,
'invoice_line_ids': [
(0, 0, {'quantity': 1, 'price_unit': 100})
@@ -43,12 +43,12 @@ class TestSEPAQRCode(AccountTestInvoicingCommon):
self.sepa_qr_invoice.generate_qr_code()
# Using a non-SEPA IBAN shouldn't
self.sepa_qr_invoice.invoice_partner_bank_id = self.acc_non_sepa_iban
self.sepa_qr_invoice.partner_bank_id = self.acc_non_sepa_iban
with self.assertRaises(UserError, msg="It shouldn't be possible to generate a SEPA QR-code for IBAN of countries outside SEPA zone."):
self.sepa_qr_invoice.generate_qr_code()
# Changing the currency should break it as well
self.sepa_qr_invoice.invoice_partner_bank_id = self.acc_sepa_iban
self.sepa_qr_invoice.partner_bank_id = self.acc_sepa_iban
self.sepa_qr_invoice.currency_id = self.env.ref('base.USD').id
with self.assertRaises(UserError, msg="It shouldn't be possible to generate a SEPA QR-code for another currency as EUR."):
self.sepa_qr_invoice.generate_qr_code()
@@ -81,10 +81,10 @@ class StockMoveInvoice(AccountTestCommon):
self.invoice.post()
self.journal = self.AccountJournal.search([('type', '=', 'cash'), ('company_id', '=', self.sale_prepaid.company_id.id)], limit=1)
register_payments = self.env['account.payment.register'].with_context(active_ids=self.invoice.ids).create({
register_payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=self.invoice.ids).create({
'journal_id': self.journal.id,
})
register_payments.create_payments()
register_payments._create_payments()
# Check the SO after paying the invoice
self.assertNotEqual(self.sale_prepaid.invoice_count, 0, 'order not invoiced')
+1 -1
View File
@@ -482,7 +482,7 @@ class HrExpense(models.Model):
'partner_id': expense.employee_id.address_home_id.commercial_partner_id.id,
'partner_type': 'supplier',
'journal_id': journal.id,
'payment_date': expense.date,
'date': expense.date,
'state': 'reconciled',
'currency_id': expense.currency_id.id if different_currency else journal_currency.id,
'amount': abs(total_amount_currency) if different_currency else abs(total_amount),
@@ -92,14 +92,14 @@ class HrExpenseSheetRegisterPaymentWizard(models.TransientModel):
'partner_type': 'supplier',
'payment_type': 'outbound',
'partner_id': self.partner_id.id,
'partner_bank_account_id': self.partner_bank_account_id.id,
'partner_bank_id': self.partner_bank_account_id.id,
'journal_id': self.journal_id.id,
'company_id': self.company_id.id,
'payment_method_id': self.payment_method_id.id,
'amount': self.amount,
'currency_id': self.currency_id.id,
'payment_date': self.payment_date,
'communication': self.communication
'date': self.payment_date,
'ref': self.communication
}
def expense_post_payment(self):
@@ -112,7 +112,7 @@ class HrExpenseSheetRegisterPaymentWizard(models.TransientModel):
# Create payment and post it
payment = self.env['account.payment'].create(self._get_payment_vals())
payment.post()
payment.action_post()
# Log the payment in the chatter
body = (_("A payment of %s %s with the reference <a href='/mail/view?%s'>%s</a> related to your expense %s has been made.") % (payment.amount, payment.currency_id.symbol, url_encode({'model': 'account.payment', 'res_id': payment.id}), payment.name, expense_sheet.name))
@@ -120,7 +120,7 @@ class HrExpenseSheetRegisterPaymentWizard(models.TransientModel):
# Reconcile the payment and the expense, i.e. lookup on the payable account move lines
account_move_lines_to_reconcile = self.env['account.move.line']
for line in payment.move_line_ids + expense_sheet.account_move_id.line_ids:
for line in payment.line_ids + expense_sheet.account_move_id.line_ids:
if line.account_id.internal_type == 'payable' and not line.reconciled:
account_move_lines_to_reconcile |= line
account_move_lines_to_reconcile.reconcile()
@@ -12,5 +12,6 @@
<field name="property_tax_payable_account_id" ref="a4512"/>
<field name="property_tax_receivable_account_id" ref="a4112"/>
<field name="default_pos_receivable_account_id" ref="a4001" />
<field name="account_journal_suspense_account_id" ref="a4990"/>
</record>
</odoo>
@@ -7,11 +7,6 @@ from odoo import api, models, fields
class AccountChartTemplate(models.Model):
_inherit = 'account.chart.template'
def get_countries_posting_at_bank_rec(self):
rslt = super(AccountChartTemplate, self).get_countries_posting_at_bank_rec()
rslt.append('BE')
return rslt
@api.model
def _prepare_all_journals(self, acc_template_ref, company, journals_dict=None):
journal_data = super(AccountChartTemplate, self)._prepare_all_journals(
+1 -1
View File
@@ -53,7 +53,7 @@ class AccountMove(models.Model):
invoice_form.ref = elements[0].text
elements = tree.xpath('//cbc:InstructionID', namespaces=namespaces)
if elements:
invoice_form.invoice_payment_ref = elements[0].text
invoice_form.payment_reference = elements[0].text
# Dates
elements = tree.xpath('//cbc:IssueDate', namespaces=namespaces)
+16 -16
View File
@@ -28,7 +28,7 @@ class AccountMove(models.Model):
l10n_ch_isr_sent = fields.Boolean(default=False, help="Boolean value telling whether or not the ISR corresponding to this invoice has already been printed or sent by mail.")
l10n_ch_currency_name = fields.Char(related='currency_id.name', readonly=True, string="Currency Name", help="The name of this invoice's currency") #This field is used in the "invisible" condition field of the 'Print ISR' button.
@api.depends('invoice_partner_bank_id.l10n_ch_isr_subscription_eur', 'invoice_partner_bank_id.l10n_ch_isr_subscription_chf')
@api.depends('partner_bank_id.l10n_ch_isr_subscription_eur', 'partner_bank_id.l10n_ch_isr_subscription_chf')
def _compute_l10n_ch_isr_subscription(self):
""" Computes the ISR subscription identifying your company or the bank that allows to generate ISR. And formats it accordingly"""
def _format_isr_subscription(isr_subscription):
@@ -46,11 +46,11 @@ class AccountMove(models.Model):
for record in self:
record.l10n_ch_isr_subscription = False
record.l10n_ch_isr_subscription_formatted = False
if record.invoice_partner_bank_id:
if record.partner_bank_id:
if record.currency_id.name == 'EUR':
isr_subscription = record.invoice_partner_bank_id.l10n_ch_isr_subscription_eur
isr_subscription = record.partner_bank_id.l10n_ch_isr_subscription_eur
elif record.currency_id.name == 'CHF':
isr_subscription = record.invoice_partner_bank_id.l10n_ch_isr_subscription_chf
isr_subscription = record.partner_bank_id.l10n_ch_isr_subscription_chf
else:
#we don't format if in another currency as EUR or CHF
continue
@@ -60,7 +60,7 @@ class AccountMove(models.Model):
record.l10n_ch_isr_subscription = _format_isr_subscription_scanline(isr_subscription)
record.l10n_ch_isr_subscription_formatted = _format_isr_subscription(isr_subscription)
@api.depends('name', 'invoice_partner_bank_id.l10n_ch_postal')
@api.depends('name', 'partner_bank_id.l10n_ch_postal')
def _compute_l10n_ch_isr_number(self):
""" The ISR reference number is 27 characters long. The first 12 of them
contain the postal account number of this ISR's issuer, removing the zeros
@@ -72,8 +72,8 @@ class AccountMove(models.Model):
of a recursive modulo 10 on its first 26 characters.
"""
for record in self:
if record.name and record.invoice_partner_bank_id and record.invoice_partner_bank_id.l10n_ch_postal:
invoice_issuer_ref = record.invoice_partner_bank_id.l10n_ch_postal.ljust(l10n_ch_ISR_NUMBER_ISSUER_LENGTH, '0')
if record.name and record.partner_bank_id and record.partner_bank_id.l10n_ch_postal:
invoice_issuer_ref = record.partner_bank_id.l10n_ch_postal.ljust(l10n_ch_ISR_NUMBER_ISSUER_LENGTH, '0')
invoice_ref = re.sub('[^\d]', '', record.name)
#We only keep the last digits of the sequence number if it is too long
invoice_ref = invoice_ref[-l10n_ch_ISR_NUMBER_ISSUER_LENGTH:]
@@ -95,7 +95,7 @@ class AccountMove(models.Model):
return res
for record in self:
if record.name and record.invoice_partner_bank_id and record.invoice_partner_bank_id.l10n_ch_postal:
if record.name and record.partner_bank_id and record.partner_bank_id.l10n_ch_postal:
record.l10n_ch_isr_number_spaced = _space_isr_number(record.l10n_ch_isr_number)
else:
record.l10n_ch_isr_number_spaced = False
@@ -103,9 +103,9 @@ class AccountMove(models.Model):
@api.depends(
'currency_id.name', 'amount_residual', 'name',
'invoice_partner_bank_id.l10n_ch_postal',
'invoice_partner_bank_id.l10n_ch_isr_subscription_eur',
'invoice_partner_bank_id.l10n_ch_isr_subscription_chf')
'partner_bank_id.l10n_ch_postal',
'partner_bank_id.l10n_ch_isr_subscription_eur',
'partner_bank_id.l10n_ch_isr_subscription_chf')
def _compute_l10n_ch_isr_optical_line(self):
""" The optical reading line of the ISR looks like this :
left>isr_ref+ bank_ref>
@@ -142,16 +142,16 @@ class AccountMove(models.Model):
@api.depends(
'move_type', 'name', 'currency_id.name',
'invoice_partner_bank_id.l10n_ch_postal',
'invoice_partner_bank_id.l10n_ch_isr_subscription_eur',
'invoice_partner_bank_id.l10n_ch_isr_subscription_chf')
'partner_bank_id.l10n_ch_postal',
'partner_bank_id.l10n_ch_isr_subscription_eur',
'partner_bank_id.l10n_ch_isr_subscription_chf')
def _compute_l10n_ch_isr_valid(self):
"""Returns True if all the data required to generate the ISR are present"""
for record in self:
record.l10n_ch_isr_valid = record.move_type == 'out_invoice' and\
record.name and \
record.l10n_ch_isr_subscription and \
record.invoice_partner_bank_id.l10n_ch_postal and \
record.partner_bank_id.l10n_ch_postal and \
record.l10n_ch_currency_name in ['EUR', 'CHF']
def split_total_amount(self):
@@ -171,7 +171,7 @@ class AccountMove(models.Model):
"""
self.ensure_one()
return self.display_qr_code and \
self.invoice_partner_bank_id._eligible_for_qr_code('ch_qr', self.partner_id, self.currency_id)
self.partner_bank_id._eligible_for_qr_code('ch_qr', self.partner_id, self.currency_id)
def isr_print(self):
""" Triggered by the 'Print ISR' button.
+6 -6
View File
@@ -62,9 +62,9 @@
<t t-if="print_bank">
<div id="voucher-for-bank">
<p t-if="not invoice.company_id.l10n_ch_isr_preprinted_bank">
<t t-esc="invoice.invoice_partner_bank_id.bank_id.name"/><br />
<t t-esc="invoice.invoice_partner_bank_id.bank_id.zip"/>
<t t-esc="invoice.invoice_partner_bank_id.bank_id.city"/>
<t t-esc="invoice.partner_bank_id.bank_id.name"/><br />
<t t-esc="invoice.partner_bank_id.bank_id.zip"/>
<t t-esc="invoice.partner_bank_id.bank_id.city"/>
</p>
</div>
<!--Zugunsten von/En faveur de/A favore di-->
@@ -103,9 +103,9 @@
<t t-if="print_bank">
<div id="slip-for-bank">
<p t-if="not invoice.company_id.l10n_ch_isr_preprinted_bank">
<t t-esc="invoice.invoice_partner_bank_id.bank_id.name"/><br />
<t t-esc="invoice.invoice_partner_bank_id.bank_id.zip"/>
<t t-esc="invoice.invoice_partner_bank_id.bank_id.city"/>
<t t-esc="invoice.partner_bank_id.bank_id.name"/><br />
<t t-esc="invoice.partner_bank_id.bank_id.zip"/>
<t t-esc="invoice.partner_bank_id.bank_id.city"/>
</p>
</div>
<!--Zugunsten von/En faveur de/A favore di-->
+7 -7
View File
@@ -9,7 +9,7 @@
<xpath expr="//div[@id='qrcode']" position="after">
<link type="text/css" rel="stylesheet" href="/l10n_ch/static/src/scss/report_swissqr.css"/>
<div class="swissqr_content" t-if="o.display_swiss_qr_code()">
<div class="swissqr_body" t-if="o.invoice_partner_bank_id._eligible_for_qr_code('ch_qr', o.partner_id, o.currency_id)">
<div class="swissqr_body" t-if="o.partner_bank_id._eligible_for_qr_code('ch_qr', o.partner_id, o.currency_id)">
<div id="title_zone" class="swissqr_column_left title_zone">
<span class="swissqr_text title title_zone">QR-bill payment part</span><br/>
</div>
@@ -19,11 +19,11 @@
<span class="swissqr_text content">Credit transfer</span>
</div>
<img class="swissqr" t-att-src="o.invoice_partner_bank_id.build_qr_code_url(o.amount_residual, o.ref or o.name, o.invoice_payment_ref, o.currency_id, o.partner_id, 'ch_qr')"/>
<img class="swissqr" t-att-src="o.partner_bank_id.build_qr_code_url(o.amount_residual, o.ref or o.name, o.payment_reference, o.currency_id, o.partner_id, 'ch_qr')"/>
<div id="indications_zone" class="swissqr_column_right indication_zone">
<span class="swissqr_text title">Account</span><br/>
<span class="swissqr_text content" t-field="o.invoice_partner_bank_id.acc_number"/><br/>
<span class="swissqr_text content" t-field="o.partner_bank_id.acc_number"/><br/>
<span class="swissqr_text title">Creditor</span><br/>
<span class="swissqr_text content" t-field="o.company_id.name"/><br/>
@@ -33,11 +33,11 @@
<span class="swissqr_text content" t-field="o.company_id.city"/><br/>
<span class="swissqr_text title">Reference number</span><br/>
<t t-if="o.invoice_partner_bank_id.currency_id.name == 'EUR'">
<span class="swissqr_text content" t-field="o.invoice_partner_bank_id.l10n_ch_isr_subscription_eur"/><br/>
<t t-if="o.partner_bank_id.currency_id.name == 'EUR'">
<span class="swissqr_text content" t-field="o.partner_bank_id.l10n_ch_isr_subscription_eur"/><br/>
</t>
<t t-if="o.invoice_partner_bank_id.currency_id.name == 'CHF'">
<span class="swissqr_text content" t-field="o.invoice_partner_bank_id.l10n_ch_isr_subscription_chf"/><br/>
<t t-if="o.partner_bank_id.currency_id.name == 'CHF'">
<span class="swissqr_text content" t-field="o.partner_bank_id.l10n_ch_isr_subscription_chf"/><br/>
</t>
<span class="swissqr_text title">Additional information</span><br/>
+4 -4
View File
@@ -28,9 +28,9 @@ class TestSwissQRCode(AccountTestInvoicingCommon):
'move_type': 'out_invoice',
'partner_id': cls.partner_a.id,
'currency_id': cls.env.ref('base.CHF').id,
'invoice_partner_bank_id': cls.swiss_iban.id,
'partner_bank_id': cls.swiss_iban.id,
'company_id': cls.company_data['company'].id,
'invoice_payment_ref': "Papa a vu le fifi de lolo",
'payment_reference': "Papa a vu le fifi de lolo",
'invoice_line_ids': [
(0, 0, {'quantity': 1, 'price_unit': 100})
],
@@ -61,13 +61,13 @@ class TestSwissQRCode(AccountTestInvoicingCommon):
self.ch_qr_invoice.generate_qr_code()
# Now, check with a QR-IBAN as the payment account
self.ch_qr_invoice.invoice_partner_bank_id = self.swiss_qr_iban
self.ch_qr_invoice.partner_bank_id = self.swiss_qr_iban
with self.assertRaises(UserError, msg="It shouldn't be possible to generate a Swiss QR-cde for a QR-IBAN without giving it a valid QR-reference as payment reference."):
self.ch_qr_invoice.generate_qr_code()
# Assigning a QR reference should fix it
self.ch_qr_invoice.invoice_payment_ref = '210000000003139471430009017'
self.ch_qr_invoice.payment_reference = '210000000003139471430009017'
def test_ch_qr_code_detection(self):
""" Checks Swiss QR-code auto-detection when no specific QR-method
@@ -6,7 +6,7 @@
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_move_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='invoice_sent']" position="after">
<xpath expr="//field[@name='is_move_sent']" position="after">
<field name="l10n_ch_isr_sent" invisible="1"/>
<field name="l10n_ch_currency_name" invisible="1" readonly="1"/>
</xpath>
+1 -1
View File
@@ -26,7 +26,7 @@
<field name="invoice_date" optional="show"/>
<field name="invoice_date_due" optional="show"/>
<field name="date" string="Accounting Date" optional="show"/>
<field name="invoice_payment_ref" optional="hide"/>
<field name="payment_reference" optional="hide"/>
<field name="invoice_user_id" optional="show" invisible="context.get('default_move_type') not in ('out_invoice', 'out_refund','out_receipt')" string="Sales Person"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}" optional="show"/>
<field name="invoice_origin" optional="show" string="Source Document"/>
@@ -6,8 +6,7 @@
('type', '=', 'bank'),
('company_id', '=', obj().env.company.id)]"/>
<field name="date" eval="time.strftime('%Y')+'-01-01'"/>
<field name="name" eval="'BNK/%s/00001' % time.strftime('%Y')"/>
<field name="balance_end_real">8998.2</field>
<field name="balance_end_real">9944.87</field>
<field name="balance_start">5103.0</field>
</record>
@@ -15,7 +14,7 @@
<field name="ref"></field>
<field name="statement_id" ref="l10n_generic_coa.demo_bank_statement_1"/>
<field name="sequence">1</field>
<field name="name" eval="'INV/%s/00002 and INV/%s/00003' % (time.strftime('%Y'), time.strftime('%Y'))"/>
<field name="payment_ref" eval="'INV/%s/00002 and INV/%s/00003' % (time.strftime('%Y'), time.strftime('%Y'))"/>
<field name="journal_id" model="account.journal" search="[
('type', '=', 'bank'),
('company_id', '=', obj().env.company.id)]"/>
@@ -28,7 +27,7 @@
<field name="ref"></field>
<field name="statement_id" ref="l10n_generic_coa.demo_bank_statement_1"/>
<field name="sequence">2</field>
<field name="name">Bank fees</field>
<field name="payment_ref">Bank fees</field>
<field name="journal_id" model="account.journal" search="[
('type', '=', 'bank'),
('company_id', '=', obj().env.company.id)]"/>
@@ -40,7 +39,7 @@
<field name="ref"></field>
<field name="statement_id" ref="l10n_generic_coa.demo_bank_statement_1"/>
<field name="sequence">3</field>
<field name="name">Prepayment</field>
<field name="payment_ref">Prepayment</field>
<field name="journal_id" model="account.journal" search="[
('type', '=', 'bank'),
('company_id', '=', obj().env.company.id)]"/>
@@ -53,7 +52,7 @@
<field name="ref"></field>
<field name="statement_id" ref="l10n_generic_coa.demo_bank_statement_1"/>
<field name="sequence">4</field>
<field name="name" eval="'First 2000 $ of invoice %s/00001' % time.strftime('%Y')"/>
<field name="payment_ref" eval="'First 2000 $ of invoice %s/00001' % time.strftime('%Y')"/>
<field name="journal_id" model="account.journal" search="[
('type', '=', 'bank'),
('company_id', '=', obj().env.company.id)]"/>
@@ -66,23 +65,19 @@
<field name="ref"></field>
<field name="statement_id" ref="l10n_generic_coa.demo_bank_statement_1"/>
<field name="sequence">5</field>
<field name="name">Last Year Interests</field>
<field name="payment_ref">Last Year Interests</field>
<field name="journal_id" model="account.journal" search="[
('type', '=', 'bank'),
('company_id', '=', obj().env.company.id)]"/>
<field name="amount">102.78</field>
<field name="date" eval="time.strftime('%Y')+'-01-01'"/>
<field name="account_id" model="account.account" search="[
('user_type_id', '=', ref('account.data_account_type_revenue')),
('tag_ids', 'in', [ref('account.account_tag_financing')]),
('company_id', '=', obj().env.company.id)]"/>
</record>
<record id="demo_bank_statement_line_6" model="account.bank.statement.line">
<field name="ref"></field>
<field name="statement_id" ref="l10n_generic_coa.demo_bank_statement_1"/>
<field name="sequence">1</field>
<field name="name" eval="'INV/'+time.strftime('%Y')+'/00002'"/>
<field name="payment_ref" eval="'INV/'+time.strftime('%Y')+'/00002'"/>
<field name="journal_id" model="account.journal" search="[
('type', '=', 'bank'),
('company_id', '=', obj().env.company.id)]"/>
@@ -95,7 +90,7 @@
<field name="ref"></field>
<field name="statement_id" ref="l10n_generic_coa.demo_bank_statement_1"/>
<field name="sequence">7</field>
<field name="name">R:9772938 10/07 AX 9415116318 T:5 BRT: 100,00€ C/ croip</field>
<field name="payment_ref">R:9772938 10/07 AX 9415116318 T:5 BRT: 100,00€ C/ croip</field>
<field name="journal_id" model="account.journal" search="[
('type', '=', 'bank'),
('company_id', '=', obj().env.company.id)]"/>
@@ -104,10 +99,6 @@
<field name="partner_id" ref="base.res_partner_2"/>
</record>
<function model="account.bank.statement.line" name="fast_counterpart_creation">
<value eval="[ref('demo_bank_statement_line_5')]"/>
</function>
<!-- Also create a pdf attachment in chatter -->
<record id="ir_attachment_bank_statement_1" model="ir.attachment">
@@ -131,5 +122,8 @@
<field name="attachment_ids" eval="[(6, 0, [ref('ir_attachment_bank_statement_1')])]"/>
</record>
<function model="account.bank.statement" name="button_post">
<value eval="[ref('demo_bank_statement_1')]"/>
</function>
</data>
</odoo>
+2 -3
View File
@@ -3,11 +3,10 @@
<!-- Demo of at(Tax liability on advances). Advance payment need to consideration for which invoices have not been issued in the same month.-->
<record id="demo_payment_at" model="account.payment">
<field name="partner_id" ref="l10n_in.res_partner_registered_customer"/>
<field name="partner_type">customer</field>
<field name="amount">10000</field>
<field name="payment_type">inbound</field>
<field name="payment_date" eval="time.strftime('%Y-%m')+'-01'"/>
<field name="date" eval="time.strftime('%Y-%m')+'-01'"/>
<field name="journal_id" model="account.journal"
eval="obj().search([
('type', '=', 'cash'),
@@ -18,7 +17,7 @@
('company_id', '=', obj().env.company.id)], limit=1).inbound_payment_method_ids[0].id"/>
</record>
<function model="account.payment" name="post">
<function model="account.payment" name="action_post">
<value eval="[ref('demo_payment_at')]"/>
</function>
</odoo>
@@ -65,7 +65,7 @@ class L10nInPaymentReport(models.AbstractModel):
tax.amount AS tax_rate,
am.partner_id,
am.amount_total AS payment_amount,
ap.journal_id,
am.journal_id,
aml.currency_id,
(CASE WHEN ps.l10n_in_tin IS NOT NULL
THEN concat(ps.l10n_in_tin,'-',ps.name)
@@ -137,7 +137,7 @@ class AdvancesPaymentReport(models.Model):
def _select(self):
select_str = super(AdvancesPaymentReport, self)._select()
select_str += """,
ap.payment_date as date,
am.date as date,
(SELECT sum(amount) FROM account_partial_reconcile AS apr
WHERE (apr.credit_move_id = aml.id OR apr.debit_move_id = aml.id)
AND (to_char(apr.max_date, 'MM-YYYY') = to_char(aml.date_maturity, 'MM-YYYY'))
@@ -46,7 +46,7 @@
<template id="account_invoice_it_FatturaPA_export">
<t t-set="currency" t-value="record.currency_id or record.company_currency_id"/>
<t t-set="bank" t-value="record.invoice_partner_bank_id"/>
<t t-set="bank" t-value="record.partner_bank_id"/>
<p:FatturaElettronica t-att-versione="formato_trasmissione" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:p="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2 http://www.fatturapa.gov.it/export/fatturazione/sdi/fatturapa/v1.2/Schema_del_file_xml_FatturaPA_versione_1.2.xsd">
<FatturaElettronicaHeader>
<DatiTrasmissione>
@@ -168,14 +168,14 @@
<t t-set="payments" t-value="record.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))"/>
<CondizioniPagamento><t t-if="len(payments) == 1">TP02</t><t t-else="">TP01</t></CondizioniPagamento>
<DettaglioPagamento>
<t t-set="company_bank_account" t-value="record.invoice_partner_bank_id"/>
<t t-set="company_bank_account" t-value="record.partner_bank_id"/>
<ModalitaPagamento t-if="company_bank_account">MP05</ModalitaPagamento>
<DataScadenzaPagamento t-esc="format_date(record.invoice_date_due)"/>
<ImportoPagamento t-esc="format_numbers_two(record.amount_total)"/>
<IstitutoFinanziario t-if="company_bank_account.bank_id" t-esc="company_bank_account.bank_id.name[:80]"/>
<IBAN t-if="company_bank_account.acc_type == 'iban'" t-esc="company_bank_account.sanitized_acc_number"/>
<BIC t-if="company_bank_account.acc_type == 'bank' and company_bank_account.bank_id.bic" t-esc="company_bank_account.bank_id.bic"/>
<CodicePagamento t-esc="record.invoice_payment_ref[:60]"/>
<CodicePagamento t-esc="record.payment_reference[:60]"/>
</DettaglioPagamento>
</DatiPagamento>
<Allegati t-if="pdf">
+2 -2
View File
@@ -384,7 +384,7 @@ class AccountMove(models.Model):
# Numbering attributed by the transmitter. <1.1.2>
elements = tree.xpath('//ProgressivoInvio')
if elements:
invoice_form.invoice_payment_ref = elements[0].text
invoice_form.payment_reference = elements[0].text
elements = body_tree.xpath('.//DatiGeneraliDocumento//Numero')
if elements:
@@ -490,7 +490,7 @@ class AccountMove(models.Model):
else:
bank = self.env['res.partner.bank'].search([('acc_number', '=', elements[0].text)])
if bank:
invoice_form.invoice_partner_bank_id = bank
invoice_form.partner_bank_id = bank
else:
message_to_log.append("%s<br/>%s" % (
_("Bank account not found, useful informations from XML file:"),
@@ -7,11 +7,6 @@ from odoo import api, models, fields
class AccountChartTemplate(models.Model):
_inherit = 'account.chart.template'
def get_countries_posting_at_bank_rec(self):
rslt = super(AccountChartTemplate, self).get_countries_posting_at_bank_rec()
rslt.append('LU')
return rslt
@api.model
def _prepare_all_journals(self, acc_template_ref, company, journals_dict=None):
journal_data = super(AccountChartTemplate, self)._prepare_all_journals(
-1
View File
@@ -1,7 +1,6 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_tax
from . import account_move
from . import account_journal
from . import account_chart_template
from . import l10n_latam_identification_type
from . import res_partner
-31
View File
@@ -1,31 +0,0 @@
from odoo import models, api
class AccountJournal(models.Model):
_inherit = "account.journal"
@api.model
def _get_sequence_prefix(self, code, refund=False):
"""For peruvian companies we can not use sequences with **/** due to the edi generation which need in the
sequence a plain text ended by a *-* and the length of this prefix"""
if self.env.company.country_id != self.env.ref('base.pe'):
return super()._get_sequence_prefix(code, refund=refund)
prefix = code.upper()
if len(prefix) > 3:
prefix = prefix[:3]
prefix = prefix.ljust(3, 'X')
if refund:
prefix = 'R' + prefix[:-1]
return prefix + '-'
@api.model
def _create_sequence(self, vals, refund=False):
"""For Peruvian companies, a number reset by date do not make sense due to the fact that we can not use a free
prefix the format does not have enough space to put a year on it or any other char, then with this approach we
are avoiding the default behavior there."""
res = super()._create_sequence(vals, refund=refund)
# NOTE: the self element is coming filled just when write and not on create (which is Ok)
journal_type = self.type if not vals.get('type') else vals.get('type')
if self.env.company.country_id == self.env.ref('base.pe') or journal_type in ['sale', 'purchase']:
res.write({'use_date_range': False})
return res
+8 -8
View File
@@ -53,14 +53,14 @@ class TestMembership(TestMembershipCommon):
# the invoice is paid -> customer goes to paid status
bank_journal = self.env['account.journal'].create({'name': 'Bank', 'type': 'bank', 'code': 'BNK67'})
self.env['account.payment'].create({
'payment_method_id': self.env.ref("account.account_payment_method_manual_in").id,
'payment_type': 'inbound',
'invoice_ids': [(6, False, invoice.ids)],
'amount': 86.25,
'journal_id': bank_journal.id,
'partner_type': 'customer',
}).post()
payment = self.env['account.payment.register']\
.with_context(active_model='account.move', active_ids=invoice.ids)\
.create({
'amount': 86.25,
'journal_id': bank_journal.id,
})\
._create_payments()
self.assertEqual(
self.partner_1.membership_state, 'paid',
+2 -2
View File
@@ -88,7 +88,7 @@ class AccountPayment(models.Model):
self.mapped('payment_transaction_id').filtered(lambda x: x.state == 'done' and not x.is_processed)._post_process_after_done()
return res
def post(self):
def action_post(self):
# Post the payments "normally" if no transactions are needed.
# If not, let the acquirer updates the state.
# __________ ______________
@@ -109,7 +109,7 @@ class AccountPayment(models.Model):
payments_need_trans = self.filtered(lambda pay: pay.payment_token_id and not pay.payment_transaction_id)
transactions = payments_need_trans._create_payment_transaction()
res = super(AccountPayment, self - payments_need_trans).post()
res = super(AccountPayment, self - payments_need_trans).action_post()
transactions.s2s_do_transaction()
+25 -15
View File
@@ -639,22 +639,42 @@ class PaymentTransaction(models.Model):
for trans in self:
trans.invoice_ids_nbr = len(trans.invoice_ids)
def _prepare_account_payment_vals(self):
def _create_payment(self, add_payment_vals={}):
''' Create an account.payment record for the current payment.transaction.
If the transaction is linked to some invoices, the reconciliation will be done automatically.
:param add_payment_vals: Optional additional values to be passed to the account.payment.create method.
:return: An account.payment record.
'''
self.ensure_one()
return {
payment_vals = {
'amount': self.amount,
'payment_type': 'inbound' if self.amount > 0 else 'outbound',
'currency_id': self.currency_id.id,
'partner_id': self.partner_id.id,
'partner_type': 'customer',
'invoice_ids': [(6, 0, self.invoice_ids.ids)],
'journal_id': self.acquirer_id.journal_id.id,
'company_id': self.acquirer_id.company_id.id,
'payment_method_id': self.env.ref('payment.account_payment_method_electronic_in').id,
'payment_token_id': self.payment_token_id and self.payment_token_id.id or None,
'payment_transaction_id': self.id,
'communication': self.reference,
'ref': self.reference,
**add_payment_vals,
}
payment = self.env['account.payment'].create(payment_vals)
payment.action_post()
# Track the payment to make a one2one.
self.payment_id = payment
if self.invoice_ids:
self.invoice_ids.filtered(lambda move: move.state == 'draft').post()
(payment.line_ids + self.invoice_ids.line_ids)\
.filtered(lambda line: line.account_id == payment.destination_account_id and not line.reconciled)\
.reconcile()
return payment
def get_last_transaction(self):
transactions = self.filtered(lambda t: t.state != 'draft')
@@ -791,21 +811,11 @@ class PaymentTransaction(models.Model):
invoices.post()
# Create & Post the payments.
payments = defaultdict(lambda: self.env['account.payment'])
for trans in self:
if trans.payment_id:
payments[trans.acquirer_id.company_id.id] += trans.payment_id
continue
payment_vals = trans._prepare_account_payment_vals()
payment = self.env['account.payment'].create(payment_vals)
payments[trans.acquirer_id.company_id.id] += payment
# Track the payment to make a one2one.
trans.payment_id = payment
for company in payments:
payments[company].with_company(company).with_context(company_id=company).post()
trans._create_payment()
def _set_transaction_cancel(self):
'''Move the transaction's payment to the cancel state(e.g. Paypal).'''
@@ -24,7 +24,7 @@ class PaymentLinkWizard(models.TransientModel):
if res_id and res_model == 'account.move':
record = self.env[res_model].browse(res_id)
res.update({
'description': record.invoice_payment_ref,
'description': record.payment_reference,
'amount': record[amount_field],
'currency_id': record.currency_id.id,
'partner_id': record.partner_id.id,
@@ -11,11 +11,12 @@ class AccountBankStatement(models.Model):
pos_session_id = fields.Many2one('pos.session', string="Session", copy=False)
account_id = fields.Many2one('account.account', related='journal_id.default_debit_account_id', readonly=True)
def check_confirm_bank(self):
for bs in self:
if bs.pos_session_id.state in ('opened', 'closing_control') and bs.state == 'open':
def button_validate_or_action(self):
# OVERRIDE to check the consistency of the statement's state regarding the session's state.
for statement in self:
if statement.pos_session_id.state in ('opened', 'closing_control') and statement.state == 'open':
raise UserError(_("You can't validate a bank statement that is used in an opened Session of a Point of Sale."))
return super(AccountBankStatement, self).check_confirm_bank()
return super(AccountBankStatement, self).button_validate_or_action()
def unlink(self):
for bs in self:
+1 -1
View File
@@ -384,7 +384,7 @@ class PosOrder(models.Model):
raise UserError(_('Please provide a partner for the sale.'))
move_vals = {
'invoice_payment_ref': order.name,
'payment_reference': order.name,
'invoice_origin': order.name,
'journal_id': order.session_id.config_id.invoice_journal_id.id,
'move_type': 'out_invoice' if order.amount_total >= 0 else 'out_refund',
+12 -11
View File
@@ -207,7 +207,6 @@ class PosSession(models.Model):
'journal_id': cash_journal.id,
'user_id': self.env.user.id,
'name': pos_name,
'balance_start': self.env["account.bank.statement"]._get_opening_balance(cash_journal.id) if cash_journal.type == 'cash' else 0
}
statement_ids |= statement_ids.with_context(ctx).create(st_values)
@@ -251,7 +250,6 @@ class PosSession(models.Model):
values['start_at'] = fields.Datetime.now()
values['state'] = 'opened'
session.write(values)
session.statement_ids.button_open()
return True
def action_pos_session_closing_control(self):
@@ -300,14 +298,13 @@ class PosSession(models.Model):
self._create_picking_at_end_of_session()
self.with_company(self.company_id)._create_account_move()
if self.move_id.line_ids:
self.move_id.post()
# Set the uninvoiced orders' state to 'done'
self.env['pos.order'].search([('session_id', '=', self.id), ('state', '=', 'paid')]).write({'state': 'done'})
else:
# The cash register needs to be confirmed for cash diffs
# made thru cash in/out when sesion is in cash_control.
if self.config_id.cash_control:
self.cash_register_id.button_confirm_bank()
self.cash_register_id.button_validate()
self.move_id.unlink()
self.write({'state': 'closed'})
return {
@@ -340,7 +337,6 @@ class PosSession(models.Model):
pickings = self.env['stock.picking']._create_picking_from_pos_order_lines(location_dest_id, lines, picking_type)
pickings.write({'pos_session_id': self.id, 'origin': self.name})
def _create_account_move(self):
""" Create account.move and account.move.line records for this session.
@@ -365,6 +361,10 @@ class PosSession(models.Model):
data = self._create_cash_statement_lines_and_cash_move_lines(data)
data = self._create_invoice_receivable_lines(data)
data = self._create_stock_output_lines(data)
if account_move.line_ids:
account_move.post()
data = self._reconcile_account_move_lines(data)
def _accumulate_amounts(self, data):
@@ -633,10 +633,11 @@ class PosSession(models.Model):
for statement in self.statement_ids:
if not self.config_id.cash_control:
statement.write({'balance_end_real': statement.balance_end})
statement.button_confirm_bank()
statement.button_post()
statement.button_validate()
all_lines = (
split_cash_statement_lines[statement].mapped('journal_entry_ids').filtered(lambda aml: aml.account_id.internal_type == 'receivable')
| combine_cash_statement_lines[statement].mapped('journal_entry_ids').filtered(lambda aml: aml.account_id.internal_type == 'receivable')
split_cash_statement_lines[statement].mapped('move_id.line_ids').filtered(lambda aml: aml.account_id.internal_type == 'receivable')
| combine_cash_statement_lines[statement].mapped('move_id.line_ids').filtered(lambda aml: aml.account_id.internal_type == 'receivable')
| split_cash_receivable_lines[statement]
| combine_cash_receivable_lines[statement]
)
@@ -772,11 +773,11 @@ class PosSession(models.Model):
def _get_statement_line_vals(self, statement, receivable_account, amount):
return {
'date': fields.Date.context_today(self),
'amount': amount,
'name': self.name,
'payment_ref': self.name,
'statement_id': statement.id,
'account_id': receivable_account.id,
'journal_id': statement.journal_id.id,
'counterpart_account_id': receivable_account.id,
}
def _update_amounts(self, old_amounts, amounts_to_add, date, round=True, force_company_currency=False):
+3 -3
View File
@@ -31,9 +31,9 @@ class TestPointOfSaleCommon(StockAccountTestCommon):
'company_id': cls.company_id})
cls.company.write({
'anglo_saxon_accounting': coa.use_anglo_saxon,
'bank_account_code_prefix': coa.bank_account_code_prefix,
'cash_account_code_prefix': coa.cash_account_code_prefix,
'transfer_account_code_prefix': coa.transfer_account_code_prefix,
'bank_account_code_prefix': 'test' + coa.bank_account_code_prefix,
'cash_account_code_prefix': 'test' + coa.cash_account_code_prefix,
'transfer_account_code_prefix': 'test' + coa.transfer_account_code_prefix,
'chart_template_id': coa.id,})
cls.product3 = cls.env['product.product'].create({
'name': 'Product 3',
@@ -472,6 +472,7 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
# I test that the total of the attached invoice is correct
invoice = self.env['account.move'].browse(res['res_id'])
invoice.post()
self.assertAlmostEqual(
invoice.amount_total, self.pos_order_pos1.amount_total, places=2, msg="Invoice not correct")
@@ -505,7 +506,7 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
'amount': 1000,
'partner_id': self.partner4.id,
'statement_id': account_statement.id,
'name': 'EXT001'
'payment_ref': 'EXT001'
})
# I modify the bank statement and set the Closing Balance.
account_statement.write({
@@ -522,7 +523,7 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
# I confirm the bank statement using Confirm button
self.AccountBankStatement.button_confirm_bank()
self.AccountBankStatement.button_validate()
def test_create_from_ui(self):
"""
@@ -869,6 +870,7 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
# I test that the total of the attached invoice is correct
invoice = self.env['account.move'].browse(res['res_id'])
invoice.post()
self.assertAlmostEqual(
invoice.amount_total, self.pos_order_pos1.amount_total, places=2, msg="Invoice not correct")
@@ -7,7 +7,7 @@
<field name="priority">1000</field>
<field name="inherit_id" ref="account.view_bank_statement_form"/>
<field name="arch" type="xml">
<xpath expr="//button[@name='check_confirm_bank']" position="replace"/>
<xpath expr="//button[@name='button_validate_or_action']" position="replace"/>
</field>
</record>
+3 -3
View File
@@ -59,13 +59,13 @@ class AccountMove(models.Model):
refs = [ref for ref in refs if ref]
self.ref = ','.join(refs)
# Compute invoice_payment_ref.
# Compute payment_reference.
if len(refs) == 1:
self.invoice_payment_ref = refs[0]
self.payment_reference = refs[0]
self.purchase_id = False
self._onchange_currency()
self.invoice_partner_bank_id = self.bank_partner_id.bank_ids and self.bank_partner_id.bank_ids[0]
self.partner_bank_id = self.bank_partner_id.bank_ids and self.bank_partner_id.bank_ids[0]
@api.onchange('partner_id', 'company_id')
def _onchange_partner_id(self):
+4 -4
View File
@@ -457,12 +457,12 @@ class PurchaseOrder(models.Model):
else:
ref_invoice_vals['invoice_line_ids'] += invoice_vals['invoice_line_ids']
origins.add(invoice_vals['invoice_origin'])
payment_refs.add(invoice_vals['invoice_payment_ref'])
payment_refs.add(invoice_vals['payment_reference'])
refs.add(invoice_vals['ref'])
ref_invoice_vals.update({
'ref': ', '.join(refs)[:2000],
'invoice_origin': ', '.join(origins),
'invoice_payment_ref': len(payment_refs) == 1 and payment_refs.pop() or False,
'payment_reference': len(payment_refs) == 1 and payment_refs.pop() or False,
})
new_invoice_vals_list.append(ref_invoice_vals)
invoice_vals_list = new_invoice_vals_list
@@ -499,8 +499,8 @@ class PurchaseOrder(models.Model):
'invoice_user_id': self.user_id and self.user_id.id,
'partner_id': partner_invoice_id,
'fiscal_position_id': (self.fiscal_position_id or self.fiscal_position_id.get_fiscal_position(partner_invoice_id)).id,
'invoice_payment_ref': self.partner_ref or '',
'invoice_partner_bank_id': self.partner_id.bank_ids[:1].id,
'payment_reference': self.partner_ref or '',
'partner_bank_id': self.partner_id.bank_ids[:1].id,
'invoice_origin': self.name,
'invoice_payment_term_id': self.payment_term_id.id,
'invoice_line_ids': [],
+1 -1
View File
@@ -82,7 +82,7 @@ class AccountMove(models.Model):
for invoice in self.filtered(lambda move: move.is_invoice()):
payments = invoice.mapped('transaction_ids.payment_id')
move_lines = payments.mapped('move_line_ids').filtered(lambda line: not line.reconciled and line.credit > 0.0)
move_lines = payments.line_ids.filtered(lambda line: line.account_internal_type in ('receivable', 'payable') and not line.reconciled)
for line in move_lines:
invoice.js_assign_outstanding_line(line.id)
return res
+4 -4
View File
@@ -555,11 +555,11 @@ class SaleOrder(models.Model):
'partner_id': self.partner_invoice_id.id,
'partner_shipping_id': self.partner_shipping_id.id,
'fiscal_position_id': (self.fiscal_position_id or self.fiscal_position_id.get_fiscal_position(self.partner_invoice_id.id)).id,
'invoice_partner_bank_id': self.company_id.partner_id.bank_ids[:1].id,
'partner_bank_id': self.company_id.partner_id.bank_ids[:1].id,
'journal_id': journal.id, # company comes from the journal
'invoice_origin': self.name,
'invoice_payment_term_id': self.payment_term_id.id,
'invoice_payment_ref': self.reference,
'payment_reference': self.reference,
'transaction_ids': [(6, 0, self.transaction_ids.ids)],
'invoice_line_ids': [],
'company_id': self.company_id.id,
@@ -697,12 +697,12 @@ Reason(s) of this behavior could be:
else:
ref_invoice_vals['invoice_line_ids'] += invoice_vals['invoice_line_ids']
origins.add(invoice_vals['invoice_origin'])
payment_refs.add(invoice_vals['invoice_payment_ref'])
payment_refs.add(invoice_vals['payment_reference'])
refs.add(invoice_vals['ref'])
ref_invoice_vals.update({
'ref': ', '.join(refs)[:2000],
'invoice_origin': ', '.join(origins),
'invoice_payment_ref': len(payment_refs) == 1 and payment_refs.pop() or False,
'payment_reference': len(payment_refs) == 1 and payment_refs.pop() or False,
})
new_invoice_vals_list.append(ref_invoice_vals)
invoice_vals_list = new_invoice_vals_list
+1 -1
View File
@@ -48,7 +48,7 @@ class TestSaleTransaction(AccountTestCommon):
invoice = self.order._create_invoices()
invoice.post()
self.assertEqual(invoice.payment_state, 'paid')
self.assertTrue(invoice.payment_state in ('in_payment', 'paid'), "Invoice should be paid")
def test_sale_transaction_mismatch(self):
"""Test that a transaction for the incorrect amount does not validate the SO."""
@@ -81,9 +81,9 @@ class SaleAdvancePaymentInv(models.TransientModel):
'fiscal_position_id': (order.fiscal_position_id or order.fiscal_position_id.get_fiscal_position(order.partner_id.id)).id,
'partner_shipping_id': order.partner_shipping_id.id,
'currency_id': order.pricelist_id.currency_id.id,
'invoice_payment_ref': order.reference,
'payment_reference': order.reference,
'invoice_payment_term_id': order.payment_term_id.id,
'invoice_partner_bank_id': order.company_id.partner_id.bank_ids[:1].id,
'partner_bank_id': order.company_id.partner_id.bank_ids[:1].id,
'team_id': order.team_id.id,
'campaign_id': order.campaign_id.id,
'medium_id': order.medium_id.id,
@@ -49,7 +49,7 @@ class AccountInvoiceSend(models.TransientModel):
})
letters |= letter
self.invoice_ids.filtered(lambda inv: not inv.invoice_sent).write({'invoice_sent': True})
self.invoice_ids.filtered(lambda inv: not inv.is_move_sent).write({'is_move_sent': True})
if len(self.invoice_ids) == 1:
letters._snailmail_print()
else:
@@ -970,7 +970,7 @@ tour.stepUtils.mobileModifier(tour.stepUtils.autoExpandMoreButtons('.o_control_p
trigger: ".o_field_widget[name=balance_end_real] input",
content: _t("Let's calculate the ending balance."),
position: 'right',
run: 'text 9010.85', // + 12.65
run: 'text 9955.87',
}, {
// FIXME convert list in kanban + form
edition: "enterprise",
@@ -1010,7 +1010,7 @@ tour.stepUtils.mobileModifier(tour.stepUtils.autoExpandMoreButtons('.o_control_p
position: 'right',
}, {
edition: "enterprise",
trigger: ".o_selected_row .o_field_widget[name=name]",
trigger: ".o_selected_row .o_field_widget[name=payment_ref]",
content: _t("Let's enter a name."),
position: "bottom",
run: "text the_flow.statement.line",
@@ -1020,7 +1020,8 @@ tour.stepUtils.mobileModifier(tour.stepUtils.autoExpandMoreButtons('.o_control_p
content: _t('Save.'),
position: 'bottom',
},
...tour.stepUtils.statusbarButtonsSteps('Reconcile', _t('<p><b>Reconcile</p>'), ".breadcrumb-item.active:contains('the_flow.statement')").map(tour.stepUtils.editionEnterpriseModifier),
...tour.stepUtils.statusbarButtonsSteps('Post', _t('Processing'), ".breadcrumb-item.active:contains('the_flow.statement')").map(tour.stepUtils.editionEnterpriseModifier),
...tour.stepUtils.statusbarButtonsSteps('Reconcile', _t('Reconcile'), ".o_statusbar_status .btn.dropdown-toggle:contains(Processing)").map(tour.stepUtils.editionEnterpriseModifier),
{
edition: "enterprise",
trigger: "button.o_reconcile, button.o_validate",
@@ -47,8 +47,6 @@ class BaseTestUi(odoo.tests.HttpCase):
'name': 'Vendor Bills - Test',
'code': 'TEXJ',
'type': 'purchase',
'default_credit_account_id': a_expense.id,
'default_debit_account_id': a_expense.id,
'refund_sequence': True,
})
self.bank_journal = self.env['account.journal'].create({