Initial commit: Hr packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:50 +02:00
commit 62531cd146
2820 changed files with 1432848 additions and 0 deletions

View file

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
from . import hr_expense_refuse_reason
from . import account_payment_register
from . import hr_expense_approve_duplicate
from . import hr_expense_split_wizard
from . import hr_expense_split

View file

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
class AccountPaymentRegister(models.TransientModel):
_inherit = 'account.payment.register'
# -------------------------------------------------------------------------
# BUSINESS METHODS
# -------------------------------------------------------------------------
@api.model
def _get_line_batch_key(self, line):
# OVERRIDE to set the bank account defined on the employee
res = super()._get_line_batch_key(line)
expense_sheet = self.env['hr.expense.sheet'].search([('payment_mode', '=', 'own_account'), ('account_move_id', 'in', line.move_id.ids)])
if expense_sheet and not line.move_id.partner_bank_id:
res['partner_bank_id'] = expense_sheet.employee_id.sudo().bank_account_id.id or line.partner_id.bank_ids and line.partner_id.bank_ids.ids[0]
return res
def _init_payments(self, to_process, edit_mode=False):
# OVERRIDE
payments = super()._init_payments(to_process, edit_mode=edit_mode)
for payment, vals in zip(payments, to_process):
expenses = vals['batch']['lines'].expense_id
if expenses:
payment.line_ids.write({'expense_id': expenses[0].id})
return payments
def _reconcile_payments(self, to_process, edit_mode=False):
# OVERRIDE
res = super()._reconcile_payments(to_process, edit_mode=edit_mode)
for vals in to_process:
expense_sheets = vals['batch']['lines'].expense_id.sheet_id
for expense_sheet in expense_sheets:
if expense_sheet.currency_id.is_zero(expense_sheet.amount_residual):
expense_sheet.state = 'done'
return res

View file

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
class HrExpenseApproveDuplicate(models.TransientModel):
"""
This wizard is shown whenever an approved expense is similar to one being
approved. The user has the opportunity to still validate it or decline.
"""
_name = "hr.expense.approve.duplicate"
_description = "Expense Approve Duplicate"
sheet_ids = fields.Many2many('hr.expense.sheet')
expense_ids = fields.Many2many('hr.expense', readonly=True)
@api.model
def default_get(self, fields):
res = super().default_get(fields)
if 'sheet_ids' in fields:
res['sheet_ids'] = [(6, 0, self.env.context.get('default_sheet_ids', []))]
if 'duplicate_expense_ids' in fields:
res['expense_ids'] = [(6, 0, self.env.context.get('default_expense_ids', []))]
return res
def action_approve(self):
self.sheet_ids._do_approve()
def action_refuse(self):
self.sheet_ids.refuse_sheet(_('Duplicate Expense'))

View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="hr_expense_approve_duplicate_view_form" model="ir.ui.view">
<field name="model">hr.expense.approve.duplicate</field>
<field name="arch" type="xml">
<form string="Expense Validate Duplicate">
<field name="sheet_ids" invisible="1" />
<p>The following approved expenses have similar employee, amount and category than some expenses of this report. Please verify this report does not contain duplicates.</p>
<field name="expense_ids" nolabel="1">
<tree>
<field name="date" readonly="1" />
<field name="employee_id" readonly="1" />
<field name="product_id" readonly="1" />
<field name="total_amount_company" readonly="1" />
<field name="name" readonly="1" />
<field name="approved_by" readonly="1" />
<field name="approved_on" readonly="1" />
</tree>
</field>
<footer>
<button string="Refuse" class="btn-primary" name="action_refuse" type="object" attrs="{'invisible': [('sheet_ids', '=', [])]}" data-hotkey="q" />
<button string="Approve" class="btn-secondary" name="action_approve" type="object" attrs="{'invisible': [('sheet_ids', '=', [])]}" data-hotkey="w" />
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z"/>
</footer>
</form>
</field>
</record>
<record id="hr_expense_approve_duplicate_action" model="ir.actions.act_window">
<field name="name">Validate Duplicate Expenses</field>
<field name="res_model">hr.expense.approve.duplicate</field>
<field name="view_mode">form</field>
<field name="view_id" ref="hr_expense_approve_duplicate_view_form"/>
<field name="target">new</field>
</record>
</odoo>

View file

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class HrExpenseRefuseWizard(models.TransientModel):
"""This wizard can be launched from an he.expense (an expense line)
or from an hr.expense.sheet (En expense report)
'hr_expense_refuse_model' must be passed in the context to differentiate
the right model to use.
"""
_name = "hr.expense.refuse.wizard"
_description = "Expense Refuse Reason Wizard"
reason = fields.Char(string='Reason', required=True)
hr_expense_ids = fields.Many2many('hr.expense')
hr_expense_sheet_id = fields.Many2one('hr.expense.sheet')
@api.model
def default_get(self, fields):
res = super(HrExpenseRefuseWizard, self).default_get(fields)
active_ids = self.env.context.get('active_ids', [])
refuse_model = self.env.context.get('hr_expense_refuse_model')
if refuse_model == 'hr.expense':
res.update({
'hr_expense_ids': active_ids,
'hr_expense_sheet_id': False,
})
elif refuse_model == 'hr.expense.sheet':
res.update({
'hr_expense_sheet_id': active_ids[0] if active_ids else False,
'hr_expense_ids': [],
})
return res
def expense_refuse_reason(self):
self.ensure_one()
if self.hr_expense_ids:
self.hr_expense_ids.refuse_expense(self.reason)
if self.hr_expense_sheet_id:
self.hr_expense_sheet_id.refuse_sheet(self.reason)
return {'type': 'ir.actions.act_window_close'}

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="hr_expense_refuse_wizard_view_form" model="ir.ui.view">
<field name="name">hr.expense.refuse.wizard.form</field>
<field name="model">hr.expense.refuse.wizard</field>
<field name="arch" type="xml">
<form string="Expense refuse reason">
<separator string="Reason to refuse Expense"/>
<field name="hr_expense_ids" invisible="1"/>
<field name="hr_expense_sheet_id" invisible="1"/>
<field name="reason" class="w-100"/>
<footer>
<button string='Refuse' name="expense_refuse_reason" type="object" class="oe_highlight" data-hotkey="q"/>
<button string="Cancel" class="oe_link" special="cancel" data-hotkey="z"/>
</footer>
</form>
</field>
</record>
<record id="hr_expense_refuse_wizard_action" model="ir.actions.act_window">
<field name="name">Refuse Expense</field>
<field name="res_model">hr.expense.refuse.wizard</field>
<field name="view_mode">form</field>
<field name="view_id" ref="hr_expense_refuse_wizard_view_form"/>
<field name="target">new</field>
</record>
</odoo>

View file

@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, api
from odoo.tools import float_compare
class HrExpenseSplit(models.TransientModel):
_name = 'hr.expense.split'
_inherit = ['analytic.mixin']
_description = 'Expense Split'
def default_get(self, fields):
result = super(HrExpenseSplit, self).default_get(fields)
if 'expense_id' in result:
expense = self.env['hr.expense'].browse(result['expense_id'])
result['total_amount'] = 0.0
result['name'] = expense.name
result['tax_ids'] = expense.tax_ids
result['product_id'] = expense.product_id
result['company_id'] = expense.company_id
result['analytic_distribution'] = expense.analytic_distribution
result['employee_id'] = expense.employee_id
result['currency_id'] = expense.currency_id
return result
name = fields.Char('Description', required=True)
wizard_id = fields.Many2one('hr.expense.split.wizard')
expense_id = fields.Many2one('hr.expense', string='Expense')
product_id = fields.Many2one('product.product', string='Product', required=True, domain="[('can_be_expensed', '=', True), '|', ('company_id', '=', False), ('company_id', '=', company_id)]")
tax_ids = fields.Many2many('account.tax', domain="[('company_id', '=', company_id), ('type_tax_use', '=', 'purchase')]")
total_amount = fields.Monetary("Total In Currency", required=True, compute='_compute_from_product_id', store=True, readonly=False)
amount_tax = fields.Monetary(string='Tax amount in Currency', compute='_compute_amount_tax')
employee_id = fields.Many2one('hr.employee', string="Employee", required=True)
company_id = fields.Many2one('res.company')
currency_id = fields.Many2one('res.currency')
product_has_tax = fields.Boolean("Whether tax is defined on a selected product", compute='_compute_product_has_tax')
product_has_cost = fields.Boolean("Is product with non zero cost selected", compute='_compute_from_product_id', store=True)
@api.depends('total_amount', 'tax_ids')
def _compute_amount_tax(self):
for split in self:
taxes = split.tax_ids.with_context(force_price_include=True).compute_all(price_unit=split.total_amount, currency=split.currency_id, quantity=1, product=split.product_id)
split.amount_tax = taxes['total_included'] - taxes['total_excluded']
@api.depends('product_id')
def _compute_from_product_id(self):
for split in self:
split.product_has_cost = split.product_id and (float_compare(split.product_id.standard_price, 0.0, precision_digits=2) != 0)
if split.product_has_cost:
split.total_amount = split.product_id.price_compute('standard_price', currency=split.currency_id)[split.product_id.id]
@api.onchange('product_id')
def _onchange_product_id(self):
"""
In case we switch to the product without taxes defined on it, taxes should be removed.
Computed method won't be good for this purpose, as we don't want to recompute and reset taxes in case they are removed on purpose during splitting.
"""
self.tax_ids = self.tax_ids if self.product_has_tax and self.tax_ids else self.product_id.supplier_taxes_id.filtered(lambda tax: tax.company_id == self.company_id)
@api.depends('product_id')
def _compute_product_has_tax(self):
for split in self:
split.product_has_tax = split.product_id and split.product_id.supplier_taxes_id.filtered(lambda tax: tax.company_id == split.company_id)
def _get_values(self):
self.ensure_one()
vals = {
'name': self.name,
'product_id': self.product_id.id,
'total_amount': self.total_amount,
'tax_ids': [(6, 0, self.tax_ids.ids)],
'analytic_distribution': self.analytic_distribution,
'employee_id': self.employee_id.id,
'product_uom_id': self.product_id.uom_id.id,
'unit_amount': self.product_id.price_compute('standard_price', currency=self.currency_id)[self.product_id.id]
}
account = self.product_id.product_tmpl_id._get_product_accounts()['expense']
if account:
vals['account_id'] = account.id
return vals

View file

@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, api, _
from odoo.tools import float_compare
class HrExpenseSplitWizard(models.TransientModel):
_name = 'hr.expense.split.wizard'
_description = 'Expense Split Wizard'
expense_id = fields.Many2one('hr.expense', string='Expense', required=True)
expense_split_line_ids = fields.One2many('hr.expense.split', 'wizard_id')
total_amount = fields.Monetary('Total Amount', compute='_compute_total_amount', currency_field='currency_id')
total_amount_original = fields.Monetary('Total amount original', related='expense_id.total_amount', currency_field='currency_id', help='Total amount of the original Expense that we are splitting')
total_amount_taxes = fields.Monetary('Taxes', currency_field='currency_id', compute='_compute_total_amount_taxes')
split_possible = fields.Boolean(help='The sum of after split shut remain the same', compute='_compute_split_possible')
currency_id = fields.Many2one('res.currency', related='expense_id.currency_id')
@api.depends('expense_split_line_ids.total_amount')
def _compute_total_amount(self):
for wizard in self:
wizard.total_amount = sum(wizard.expense_split_line_ids.mapped('total_amount'))
@api.depends('expense_split_line_ids.amount_tax')
def _compute_total_amount_taxes(self):
for wizard in self:
wizard.total_amount_taxes = sum(wizard.expense_split_line_ids.mapped('amount_tax'))
@api.depends('total_amount_original', 'total_amount')
def _compute_split_possible(self):
for wizard in self:
wizard.split_possible = wizard.total_amount_original and (float_compare(wizard.total_amount_original, wizard.total_amount, precision_digits=2) == 0)
def action_split_expense(self):
self.ensure_one()
expense_split = self.expense_split_line_ids[0]
copied_expenses = self.env["hr.expense"]
if expense_split:
self.expense_id.write(expense_split._get_values())
self.expense_split_line_ids -= expense_split
if self.expense_split_line_ids:
for split in self.expense_split_line_ids:
copied_expenses |= self.expense_id.copy(split._get_values())
attachment_ids = self.env['ir.attachment'].search([
('res_model', '=', 'hr.expense'),
('res_id', '=', self.expense_id.id)
])
for coplied_expense in copied_expenses:
for attachment in attachment_ids:
attachment.copy({'res_model': 'hr.expense', 'res_id': coplied_expense.id})
return {
'type': 'ir.actions.act_window',
'res_model': 'hr.expense',
'name': _('Split Expenses'),
'view_mode': 'tree,form',
'target': 'current',
'domain': [('id', 'in', (copied_expenses | self.expense_split_line_ids.expense_id).ids)],
}

View file

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="hr_expense_split" model="ir.ui.view">
<field name="name">Expense split</field>
<field name="model">hr.expense.split.wizard</field>
<field name="arch" type="xml">
<form>
<field name="total_amount_original" invisible="1"/>
<field name="expense_id" invisible="1"/>
<field name="expense_split_line_ids" widget="one2many" context="{'default_expense_id': expense_id}">
<tree editable="bottom">
<field name="currency_id" invisible="1"/>
<field name="expense_id" invisible="1"/>
<field name="company_id" invisible="1"/>
<field name="product_has_tax" invisible="1"/>
<field name="product_has_cost" invisible="1"/>
<field name="name"/>
<field name="product_id"/>
<field name="total_amount" force_save="1" attrs="{'readonly': [('product_has_cost', '=', True)]}"/>
<field name="tax_ids" widget="many2many_tags" attrs="{'readonly': [('product_has_tax', '=', False)]}"/>
<field name="amount_tax"/>
<field name="analytic_distribution" widget="analytic_distribution"
optional="show"
groups="analytic.group_analytic_accounting"/>
<field name="employee_id" widget="many2one_avatar_employee"/>
</tree>
</field>
<field name="currency_id" invisible="1"/>
<group class="oe_subtotal_footer oe_right" colspan="2" name="expense_total">
<label for="total_amount" attrs="{'invisible': [('split_possible', '=', True)]}"/>
<field name="total_amount" nolabel="1" class="text-danger" attrs="{'invisible': [('split_possible', '=', True)]}"/>
<field name="total_amount" attrs="{'invisible': [('split_possible', '=', False)]}"/>
<field name="total_amount_original" widget='monetary' string="Original Amount"/>
<field name="total_amount_taxes" widget='monetary' string="Taxes"/>
</group>
<field name="split_possible" invisible="1"/>
<footer>
<button name="action_split_expense" attrs="{'invisible': [ ('split_possible', '=', True)]}" string="Split Expense" type="object" class="oe_highlight" disabled="disabled" data-hotkey="q"/>
<button name="action_split_expense" string="Split Expense" attrs="{'invisible': [('split_possible', '=', False)]}" type="object" class="oe_highlight" data-hotkey="q"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z"/>
</footer>
</form>
</field>
</record>
</data>
</odoo>