Skip to content

Commit

Permalink
[12.0][IMP] hr_expense_invoice
Browse files Browse the repository at this point in the history
From expense sheet, add action "Create Invoice" from multiple expenses
Change the way reference invoice_id is checked.
- No more onchange invoice_id that set values to expense
- Instead, check amount on expense and invoice during post entry
- Change the way to allow reconcile with > 2 account move lines
kittiu authored and Saran440 committed Oct 7, 2021

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 09bebb9 commit d3bbfad
Showing 15 changed files with 393 additions and 90 deletions.
1 change: 1 addition & 0 deletions hr_expense_invoice/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from . import models
from . import wizard
3 changes: 2 additions & 1 deletion hr_expense_invoice/__manifest__.py
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@

{
'name': 'Supplier invoices on HR expenses',
'version': '12.0.1.1.0',
'version': '12.0.1.2.0',
'category': 'Human Resources',
'author': 'Tecnativa, '
'Odoo Community Association (OCA)',
@@ -15,6 +15,7 @@
],
'data': [
'views/hr_expense_views.xml',
'wizard/expense_create_invoice.xml',
],
'installable': True,
}
2 changes: 2 additions & 0 deletions hr_expense_invoice/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
from . import hr_expense
from . import hr_expense_sheet
from . import account_payment
from . import account_move_line
15 changes: 15 additions & 0 deletions hr_expense_invoice/models/account_move_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright 2019 Kitti U. <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, models


class AccountMoveLine(models.Model):
_inherit = 'account.move.line'

@api.multi
def reconcile(self, writeoff_acc_id=False, writeoff_journal_id=False):
if self._context.get('use_hr_expense_invoice'):
self = self.filtered(lambda l: not l.reconciled)
res = super().reconcile(writeoff_acc_id=writeoff_acc_id,
writeoff_journal_id=writeoff_journal_id)
return res
17 changes: 17 additions & 0 deletions hr_expense_invoice/models/account_payment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright 2019 Kitti U. <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, _
from odoo.exceptions import ValidationError


class AccountPayment(models.Model):
_inherit = 'account.payment'

def action_validate_invoice_payment(self):
# Do not allow register payment for invoices from expenses
expenses = self.env['hr.expense'].sudo().\
search([('invoice_id', 'in', self.invoice_ids.ids)])
if expenses:
raise ValidationError(_("Register payment on expense's "
"invoice is not allowed"))
return super(AccountPayment, self).action_validate_invoice_payment()
39 changes: 9 additions & 30 deletions hr_expense_invoice/models/hr_expense.py
Original file line number Diff line number Diff line change
@@ -2,7 +2,8 @@
# Copyright 2017 Vicent Cubells <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from odoo import api, fields, models
from odoo import api, fields, models, _
from odoo.exceptions import UserError


class HrExpense(models.Model):
@@ -13,38 +14,16 @@ class HrExpense(models.Model):
string='Vendor Bill',
domain="[('type', '=', 'in_invoice'), ('state', '=', 'open')]",
oldname='invoice',
copy=False,
)

@api.onchange('invoice_id')
def onchange_invoice_id(self):
self.date = self.invoice_id.date_invoice
self.name = self.invoice_id.number
self.reference = self.invoice_id.number or self.invoice_id.reference
self.analytic_account_id = False
self.unit_amount = self.invoice_id.residual
self.quantity = 1.0
self.total_amount = self.unit_amount
self.description = self.invoice_id.reference

def _check_vals(self, vals):
if vals.get('invoice_id'):
# Rewrite values because readonly fields are not stored
invoice = self.env['account.invoice'].browse(vals['invoice_id'])
vals['date'] = invoice.date_invoice
vals['analytic_account_id'] = False
vals['unit_amount'] = invoice.residual
vals['total_amount'] = invoice.residual
vals['quantity'] = 1.0

@api.model
def create(self, vals):
self._check_vals(vals)
return super(HrExpense, self).create(vals)

@api.multi
def write(self, vals):
self._check_vals(vals)
return super(HrExpense, self).write(vals)
@api.constrains('invoice_id')
def _check_invoice_id(self):
for expense in self: # Only non binding expense
if not expense.sheet_id and expense.invoice_id and \
expense.invoice_id.state != 'open':
raise UserError(_("Vendor bill state must be Open"))

@api.multi
def _get_account_move_line_values(self):
99 changes: 72 additions & 27 deletions hr_expense_invoice/models/hr_expense_sheet.py
Original file line number Diff line number Diff line change
@@ -2,44 +2,89 @@
# Copyright 2017 Vicent Cubells <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from odoo import _, api, models
from odoo import models, fields, api, _
from odoo.exceptions import UserError
from odoo.tools import float_compare


class HrExpenseSheet(models.Model):
_inherit = "hr.expense.sheet"

invoice_count = fields.Integer(
compute='_compute_invoice_count',
)
invoice_fully_created = fields.Boolean(
compute='_compute_invoice_count',
)

@api.multi
def action_sheet_move_create(self):
DecimalPrecision = self.env['decimal.precision']
precision = DecimalPrecision.precision_get('Product Price')
expense_line_ids = \
self.mapped('expense_line_ids').filtered('invoice_id')
self._validate_expense_invoice(expense_line_ids)
res = super(HrExpenseSheet, self).action_sheet_move_create()
move_lines = self.env['account.move'].search(
[('ref', 'in', self.mapped('name'))],
).mapped('line_ids')
for line in expense_line_ids:
partner = line.invoice_id.partner_id.commercial_partner_id
c_move_lines = move_lines.filtered(
lambda x:
x.partner_id == partner and
x.debit == line.invoice_id.residual and
not x.reconciled
)
if len(c_move_lines) > 1:
c_move_lines = c_move_lines[0]
residual = line.invoice_id.residual
c_move_lines |= line.invoice_id.move_id.line_ids.filtered(
lambda x:
x.account_id == line.invoice_id.account_id and
float_compare(x.credit, residual, precision) == 0)
if len(c_move_lines) != 2:
raise UserError(
_('Cannot reconcile supplier invoice payable with '
'generated line. Please check amounts and see '
'if the invoice is already added or paid. '
'Invoice: %s') % line.invoice_id.number)
for sheet in self:
move_lines = res[sheet.id].line_ids
expense_line_ids = self.expense_line_ids.filtered('invoice_id')
c_move_lines = self.env['account.move.line']
for line in expense_line_ids:
partner = line.invoice_id.partner_id.commercial_partner_id
c_move_lines |= move_lines.filtered(
lambda x:
x.partner_id == partner and x.debit and not x.reconciled)
c_move_lines |= line.invoice_id.move_id.line_ids.filtered(
lambda x:
x.account_id == line.invoice_id.account_id
and x.credit and not x.reconciled)
c_move_lines.reconcile()
return res

@api.multi
def _compute_invoice_count(self):
Invoice = self.env['account.invoice']
can_read = Invoice.check_access_rights('read', raise_exception=False)
for sheet in self:
sheet.invoice_count = can_read and \
len(sheet.expense_line_ids.mapped('invoice_id')) or 0
sheet.invoice_fully_created = \
not any(self.mapped('expense_line_ids').
filtered(lambda l: not l.invoice_id))

@api.model
def _validate_expense_invoice(self, expense_lines):
DecimalPrecision = self.env['decimal.precision']
precision = DecimalPrecision.precision_get('Product Price')
invoices = expense_lines.mapped('invoice_id')
if not invoices:
return
# All invoices must confirmed
if any(invoices.filtered(lambda i: i.state != 'open')):
raise UserError(_('Vendor bill state must be Open'))
expense_amount = sum(expense_lines.mapped('total_amount'))
invoice_amount = sum(invoices.mapped('residual'))
# Expense amount must equal invoice amount
if float_compare(expense_amount, invoice_amount, precision) != 0:
raise UserError(
_('Vendor bill amount mismatch!\nPlease make sure amount in '
'vendor bills equal to amount of its expense lines'))

@api.multi
def action_view_invoices(self):
self.ensure_one()
action = {
'name': _('Invoices'),
'type': 'ir.actions.act_window',
'res_model': 'account.invoice',
'target': 'current',
}
invoice_ids = self.expense_line_ids.mapped('invoice_id').ids
view = self.env.ref('account.invoice_supplier_form')
if len(invoice_ids) == 1:
invoice = invoice_ids[0]
action['res_id'] = invoice
action['view_mode'] = 'form'
action['views'] = [(view.id, 'form')]
else:
action['view_mode'] = 'tree,form'
action['domain'] = [('id', 'in', invoice_ids)]
return action
6 changes: 6 additions & 0 deletions hr_expense_invoice/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
This module should be used when a supplier invoice is paid by an employee. It
allows to set a supplier invoice for each expense line, adding the
corresponding journal items to transfer the debt to the employee.

There are 2 ways to reference expense to invoice.

1. On expense, directly select one invoice.
2. On expense report, use button "Create Vendor Bill" to create one invoice
for multiple expenses.
11 changes: 11 additions & 0 deletions hr_expense_invoice/readme/USAGE.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
**Reference one invoice to an expense**

* Create an expense sheet.
* Add an expense line to sheet with an invoice_id selected or create one new.
* Process expense sheet.
* On paying expense sheet, you are reconciling supplier invoice too.

**Create one invoice to multiple expenses**

* Create an expense sheet with one or multiple expense lines
* After approved, click button "Create Vendor Bill"
* Select multiple expense to create an invoice, and process it.
* New invoice will be create and link to the selected expense lines.
* Validate newly create invoice.
* On paying expense sheet, you are reconciling supplier invoice(s) too.
Loading

0 comments on commit d3bbfad

Please sign in to comment.