[IMP] hr_expense: add "payment_status" field + change error msg
- Add "payment_status" field to be more explicit. - Turn error message "Expenses must have an expense journal specified to generate accounting entries." into "Specify expense journal in tab Other Info to generate accounting entries." closes odoo/odoo#65528 Task: 2424870 Signed-off-by: Laurent Smet <smetl@users.noreply.github.com>
This commit is contained in:
@@ -38,6 +38,14 @@ def calc_check_digits(number):
|
||||
checksum = int(number_base10) % 97
|
||||
return '%02d' % ((98 - 100 * checksum) % 97)
|
||||
|
||||
PAYMENT_STATE_SELECTION = [
|
||||
('not_paid', 'Not Paid'),
|
||||
('in_payment', 'In Payment'),
|
||||
('paid', 'Paid'),
|
||||
('partial', 'Partially Paid'),
|
||||
('reversed', 'Reversed'),
|
||||
('invoicing_legacy', 'Invoicing App Legacy'),
|
||||
]
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_name = "account.move"
|
||||
@@ -233,15 +241,8 @@ class AccountMove(models.Model):
|
||||
amount_by_group = fields.Binary(string="Tax amount by group",
|
||||
compute='_compute_invoice_taxes_by_group',
|
||||
help='Edit Tax amounts if you encounter rounding issues.')
|
||||
payment_state = fields.Selection(selection=[
|
||||
('not_paid', 'Not Paid'),
|
||||
('in_payment', 'In Payment'),
|
||||
('paid', 'Paid'),
|
||||
('partial', 'Partially Paid'),
|
||||
('reversed', 'Reversed'),
|
||||
('invoicing_legacy', 'Invoicing App Legacy')],
|
||||
string="Payment Status", store=True, readonly=True, copy=False, tracking=True,
|
||||
compute='_compute_amount')
|
||||
payment_state = fields.Selection(PAYMENT_STATE_SELECTION, string="Payment Status", store=True,
|
||||
readonly=True, copy=False, tracking=True, compute='_compute_amount')
|
||||
|
||||
# ==== Cash basis feature fields ====
|
||||
tax_cash_basis_rec_id = fields.Many2one(
|
||||
@@ -1271,7 +1272,7 @@ class AccountMove(models.Model):
|
||||
if line.currency_id and line in move._get_lines_onchange_currency():
|
||||
currencies.add(line.currency_id)
|
||||
|
||||
if move.is_invoice(include_receipts=True):
|
||||
if move._payment_state_matters():
|
||||
# === Invoices ===
|
||||
|
||||
if not line.exclude_from_invoice_tab:
|
||||
@@ -1315,8 +1316,7 @@ class AccountMove(models.Model):
|
||||
# Compute 'payment_state'.
|
||||
new_pmt_state = 'not_paid' if move.move_type != 'entry' else False
|
||||
|
||||
if move.is_invoice(include_receipts=True) and move.state == 'posted':
|
||||
|
||||
if move._payment_state_matters() and move.state == 'posted':
|
||||
if currency.is_zero(move.amount_residual):
|
||||
reconciled_payments = move._get_reconciled_payments()
|
||||
if not reconciled_payments or all(payment.is_matched for payment in reconciled_payments):
|
||||
@@ -2024,6 +2024,13 @@ class AccountMove(models.Model):
|
||||
def get_invoice_types(self, include_receipts=False):
|
||||
return ['out_invoice', 'out_refund', 'in_refund', 'in_invoice'] + (include_receipts and ['out_receipt', 'in_receipt'] or [])
|
||||
|
||||
def _payment_state_matters(self):
|
||||
''' Determines when new_pmt_state must be upated.
|
||||
Method created to allow overrides.
|
||||
:return: Boolean '''
|
||||
self.ensure_one()
|
||||
return self.is_invoice(include_receipts=True)
|
||||
|
||||
def is_invoice(self, include_receipts=False):
|
||||
return self.move_type in self.get_invoice_types(include_receipts)
|
||||
|
||||
|
||||
@@ -3,6 +3,15 @@
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = "account.move"
|
||||
|
||||
def _payment_state_matters(self):
|
||||
self.ensure_one()
|
||||
if self.line_ids.expense_id:
|
||||
return True
|
||||
return super()._payment_state_matters()
|
||||
|
||||
|
||||
class AccountMoveLine(models.Model):
|
||||
_inherit = "account.move.line"
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import re
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tools import email_split, float_is_zero, float_repr
|
||||
from odoo.addons.account.models.account_move import PAYMENT_STATE_SELECTION
|
||||
|
||||
|
||||
class HrExpense(models.Model):
|
||||
@@ -538,6 +538,7 @@ Or send your receipts at <a href="mailto:%(email)s?subject=Lunch%%20with%%20cust
|
||||
'currency_id': expense.currency_id.id,
|
||||
'expense_id': expense.id,
|
||||
'partner_id': partner_id,
|
||||
'exclude_from_invoice_tab': True,
|
||||
}
|
||||
move_line_values.append(move_line_dst)
|
||||
|
||||
@@ -828,9 +829,11 @@ class HrExpenseSheet(models.Model):
|
||||
('submit', 'Submitted'),
|
||||
('approve', 'Approved'),
|
||||
('post', 'Posted'),
|
||||
('done', 'Paid'),
|
||||
('done', 'Done'),
|
||||
('cancel', 'Refused')
|
||||
], string='Status', index=True, readonly=True, tracking=True, copy=False, default='draft', required=True, help='Expense Report State')
|
||||
payment_state = fields.Selection(selection=PAYMENT_STATE_SELECTION, string="Payment Status",
|
||||
store=True, readonly=True, copy=False, tracking=True, compute='_compute_payment_state')
|
||||
employee_id = fields.Many2one('hr.employee', string="Employee", required=True, readonly=True, tracking=True, states={'draft': [('readonly', False)]}, default=_default_employee_id, check_company=True, domain= lambda self: self.env['hr.expense']._get_employee_id_domain())
|
||||
address_id = fields.Many2one('res.partner', compute='_compute_from_employee_id', store=True, readonly=False, copy=True, string="Employee Home Address", check_company=True)
|
||||
payment_mode = fields.Selection(related='expense_line_ids.payment_mode', default='own_account', readonly=True, string="Paid By", tracking=True)
|
||||
@@ -839,7 +842,7 @@ class HrExpenseSheet(models.Model):
|
||||
amount_residual = fields.Monetary(
|
||||
string="Amount Due", store=True,
|
||||
currency_field='currency_id',
|
||||
compute='_compute_amount_residual')
|
||||
related='account_move_id.amount_residual')
|
||||
company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True, states={'draft': [('readonly', False)]}, default=lambda self: self.env.company)
|
||||
currency_id = fields.Many2one('res.currency', string='Currency', readonly=True, states={'draft': [('readonly', False)]}, default=lambda self: self.env.company.currency_id)
|
||||
attachment_number = fields.Integer(compute='_compute_attachment_number', string='Number of Attachments')
|
||||
@@ -863,21 +866,10 @@ class HrExpenseSheet(models.Model):
|
||||
for sheet in self:
|
||||
sheet.total_amount = sum(sheet.expense_line_ids.mapped('total_amount_company'))
|
||||
|
||||
@api.depends(
|
||||
'currency_id',
|
||||
'account_move_id.line_ids.amount_residual',
|
||||
'account_move_id.line_ids.amount_residual_currency',
|
||||
'account_move_id.line_ids.account_internal_type',)
|
||||
def _compute_amount_residual(self):
|
||||
@api.depends('account_move_id.payment_state')
|
||||
def _compute_payment_state(self):
|
||||
for sheet in self:
|
||||
if sheet.currency_id == sheet.company_id.currency_id:
|
||||
residual_field = 'amount_residual'
|
||||
else:
|
||||
residual_field = 'amount_residual_currency'
|
||||
|
||||
payment_term_lines = sheet.account_move_id.line_ids\
|
||||
.filtered(lambda line: line.account_internal_type in ('receivable', 'payable'))
|
||||
sheet.amount_residual = -sum(payment_term_lines.mapped(residual_field))
|
||||
sheet.payment_state = sheet.account_move_id.payment_state or 'not_paid'
|
||||
|
||||
def _compute_attachment_number(self):
|
||||
for sheet in self:
|
||||
@@ -977,7 +969,7 @@ class HrExpenseSheet(models.Model):
|
||||
raise UserError(_("You can only generate accounting entry for approved expense(s)."))
|
||||
|
||||
if any(not sheet.journal_id for sheet in self):
|
||||
raise UserError(_("Expenses must have an expense journal specified to generate accounting entries."))
|
||||
raise UserError(_("Specify expense journal in tab Other Info to generate accounting entries."))
|
||||
|
||||
expense_line_ids = self.mapped('expense_line_ids')\
|
||||
.filtered(lambda r: not float_is_zero(r.total_amount, precision_rounding=(r.currency_id or self.env.company.currency_id).rounding))
|
||||
|
||||
@@ -8,6 +8,61 @@ from odoo import fields
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestExpenses(TestExpenseCommon):
|
||||
|
||||
def test_expense_sheet_payment_state(self):
|
||||
''' Test expense sheet payment states when partially paid, in payment and paid. '''
|
||||
|
||||
def get_payment(expense_sheet, amount):
|
||||
ctx = {'active_model': 'account.move', 'active_ids': expense_sheet.account_move_id.ids}
|
||||
payment_register = self.env['account.payment.register'].with_context(**ctx).create({
|
||||
'amount': amount,
|
||||
'journal_id': self.company_data['default_journal_bank'].id,
|
||||
'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
|
||||
})
|
||||
return payment_register._create_payments()
|
||||
|
||||
expense_sheet = self.env['hr.expense.sheet'].create({
|
||||
'name': 'Expense for John Smith',
|
||||
'employee_id': self.expense_employee.id,
|
||||
'accounting_date': '2021-01-01',
|
||||
'expense_line_ids': [(0, 0, {
|
||||
'name': 'Car Travel Expenses',
|
||||
'employee_id': self.expense_employee.id,
|
||||
'product_id': self.product_a.id,
|
||||
'unit_amount': 350.00,
|
||||
})]
|
||||
})
|
||||
|
||||
expense_sheet.action_submit_sheet()
|
||||
expense_sheet.approve_expense_sheets()
|
||||
expense_sheet.action_sheet_move_create()
|
||||
|
||||
payment = get_payment(expense_sheet, 100.0)
|
||||
liquidity_lines1 = payment._seek_for_lines()[0]
|
||||
|
||||
self.assertEqual(expense_sheet.payment_state, 'partial', 'payment_state should be partial')
|
||||
|
||||
payment = get_payment(expense_sheet, 250.0)
|
||||
liquidity_lines2 = payment._seek_for_lines()[0]
|
||||
|
||||
in_payment_state = expense_sheet.account_move_id._get_invoice_in_payment_state()
|
||||
self.assertEqual(expense_sheet.payment_state, in_payment_state, 'payment_state should be ' + in_payment_state)
|
||||
|
||||
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': 'pay_ref',
|
||||
'amount': -350.0,
|
||||
'partner_id': self.expense_employee.address_home_id.id,
|
||||
}),
|
||||
],
|
||||
})
|
||||
statement.button_post()
|
||||
statement.line_ids.reconcile([{'id': liquidity_lines1.id}, {'id': liquidity_lines2.id}])
|
||||
|
||||
self.assertEqual(expense_sheet.payment_state, 'paid', 'payment_state should be paid')
|
||||
|
||||
def test_expense_values(self):
|
||||
""" Checking accounting move entries and analytic entries when submitting expense """
|
||||
|
||||
|
||||
@@ -625,6 +625,7 @@
|
||||
<field name="currency_id" optional="hide"/>
|
||||
<field name="journal_id" optional="hide"/>
|
||||
<field name="state" optional="show" decoration-info="state == 'draft'" decoration-success="state in ['submit', 'approve', 'post', 'done']" decoration-danger="state == 'cancel'" widget="badge"/>
|
||||
<field name="payment_state" optional="show" decoration-info="payment_state in ('partial','in_payment')" decoration-success="payment_state == 'paid'" decoration-danger="payment_state in ('reversed','not_paid')" widget="badge"/>
|
||||
<field name="message_unread" invisible="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
@@ -698,7 +699,11 @@
|
||||
<field name="attachment_number" widget="statinfo" string="Documents"/>
|
||||
</button>
|
||||
</div>
|
||||
<widget name="web_ribbon" title="Paid" bg_color="bg-success" attrs="{'invisible': [('state', '!=', 'done')]}"/>
|
||||
<field name="payment_state" invisible="True"/>
|
||||
<widget name="web_ribbon" title="Paid" bg_color="bg-success" attrs="{'invisible': [('payment_state', '!=', 'paid')]}"/>
|
||||
<widget name="web_ribbon" title="Partial" bg_color="bg-info" attrs="{'invisible': [('payment_state', '!=', 'partial')]}"/>
|
||||
<widget name="web_ribbon" title="Reversed" bg_color="bg-danger" attrs="{'invisible': [('payment_state', '!=', 'reversed')]}"/>
|
||||
<widget name="web_ribbon" title="In Payment" attrs="{'invisible': [('payment_state', '!=', 'in_payment')]}"/>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1>
|
||||
|
||||
Reference in New Issue
Block a user