odex25_standard/odex25_accounting/employee_custody_request/models/models.py

626 lines
22 KiB
Python

# -*- coding: utf-8 -*-
from odoo import api, fields, models, tools, _
import ast
from odoo.exceptions import UserError, ValidationError
from odoo.tools import float_compare, float_is_zero
from datetime import datetime, timedelta
# class HrEmployee(models.Model):
# _inherit='hr.employee'
# @api.model
# def action_open_bank_balance_in_gl(self):
#
# self.ensure_one()
# action = self.env["ir.actions.actions"]._for_xml_id("odex25_account_reports.action_account_report_general_ledger")
# employee = self.browse(self._context.get('active_id'))
# action['context'] = dict(ast.literal_eval(action['context']), default_filter_accounts=employee.journal_id.default_account_id.code)
#
# return action
class HrRequestPledge(models.Model):
_name = 'hr.request.pledge'
_description = 'Request Pledge'
_rec_name = "code"
_inherit = ['mail.thread', 'mail.activity.mixin']
code = fields.Char()
state = fields.Selection(
[('draft', _('Draft')),
('submit', _('Waiting Payroll Officer')),
('direct_manager', _('Wait HR Department')),
('hr_manager', _('Wait GM Approval')),
('executive_manager', _('Wait Transfer')),
('financial_approve', _('Wait Financial Approval')),
('pay', _('Transferred')), ('refused', _('Refused')),
('locked', _('Locked')),
('closed', _('Loan Suspended'))],
default="draft", tracking=True)
date = fields.Date(required=True)
department_id = fields.Many2one(related='employee_id.department_id', readonly=True, store=True)
from_hr_depart = fields.Boolean()
job_id = fields.Many2one(related='employee_id.job_id', readonly=True)
dashboard_id = fields.Many2one(
'base.dashbord',
index=True
)
# is_financial_impact = fields.Boolean(
# compute='_compute_is_financial_impact',
# store=True
# )
custody_type_id = fields.Many2one(
'custody.types',
string='Custody Types',
required=True,
tracking=True
)
journal_id = fields.Many2one(related='custody_type_id.journal_id', readonly=True)
employee_id = fields.Many2one('hr.employee', 'Employee',
default=lambda item: item.get_user_id(), index=True)
emp_expect_amount = fields.Float(string='Request Employee Amount')
description = fields.Char("Statement")
currency_id = fields.Many2one(
'res.currency',
string='Currency',
default=lambda self: self.env.company.currency_id
)
company_id = fields.Many2one(
"res.company",
string="Company",
default=lambda self: self.env.company,
required=True,
readonly=True
)
spent_amount = fields.Float(string="Amount Spent", default=0.0)
remaining_amount = fields.Float(string="Amount Remaining", compute="_compute_remaining_amount", store=True)
custody_status = fields.Selection([
('new', 'New'),
('partial', 'Partial'),
('paid', 'Paid'),
('exceeded', 'Exceeded')
],compute='_compute_custody_status', string='Custody Status', default='new', tracking=True)
payment_ids = fields.One2many(
'account.payment', 'hr_request_pledge_id',
string="Payments"
)
total_paid_amount = fields.Float(
string="Total Paid Amount",
compute='_compute_total_paid_amount',
store=True
)
permanent_pledge = fields.Boolean(
string="Permanent Pledge",
default=False,
)
spent_amount_computed = fields.Float(
string="Computed Spent Amount",
compute="_compute_spent_amount_from_moves",
store=True,
help="Automatically distributed from related expense journal entries by employee."
)
total_refunded_amount = fields.Float(
string="Total Refunded Amount",
compute='_compute_total_refunded_amount',
store=True
)
@api.depends('payment_ids.amount', 'payment_ids.state', 'payment_ids.payment_type')
def _compute_total_refunded_amount(self):
for rec in self:
# Filter posted inbound payments
posted_payments = rec.payment_ids.filtered(lambda p: p.state == 'posted')
inbound_total = sum(posted_payments.filtered(lambda p: p.payment_type == 'inbound').mapped('amount'))
rec.total_refunded_amount = inbound_total
def action_lock_pledge(self):
for record in self:
if record.remaining_amount > 0:
raise UserError(_("You cannot close this pledge because there is a remaining amount of %.2f.") % record.remaining_amount)
record.state = 'locked'
@api.depends('employee_id', 'total_paid_amount')
def _compute_spent_amount_from_moves(self):
Move = self.env['account.move']
all_employees = self.mapped('employee_id')
for emp in all_employees:
moves = Move.search([
('move_type', 'in', ['in_receipt', 'in_invoice']),
('is_petty_paid', '=', True),
('state', '=', 'posted'),
('petty_employee_id', '=', emp.id),
])
total_spent = 0.0
for move in moves:
total_spent += sum(move.line_ids.filtered(lambda l: l.debit > 0).mapped('debit'))
pledges = self.search([('employee_id', '=', emp.id), ('state', 'in', ['pay', 'locked'])], order='id asc')
remaining = total_spent
for pledge in pledges:
if remaining <= 0:
pledge.spent_amount_computed = 0.0
pledge.spent_amount = 0.0
continue
limit = pledge.total_paid_amount or 0.0
if remaining >= limit and limit > 0:
pledge.spent_amount_computed = limit
pledge.spent_amount = limit
remaining -= limit
else:
pledge.spent_amount_computed = remaining
pledge.spent_amount = remaining
remaining = 0
if remaining > 0 and pledges:
pledges[-1].spent_amount_computed += remaining
pledges[-1].spent_amount += remaining
@api.depends('spent_amount', 'total_paid_amount')
def _compute_custody_status(self):
for pledge in self:
remaining = (pledge.total_paid_amount or 0.0) - (pledge.spent_amount or 0.0)
if remaining > 0:
pledge.custody_status = 'partial'
elif remaining == 0:
pledge.custody_status = 'paid'
else:
pledge.custody_status = 'exceeded'
# @api.depends('payment_ids.amount', 'payment_ids.state', 'payment_ids.payment_type')
# def _compute_total_paid_amount(self):
# for rec in self:
# posted_outbound_payments = rec.payment_ids.filtered(
# lambda p: p.state == 'posted' and p.payment_type == 'outbound')
# rec.total_paid_amount = sum(posted_outbound_payments.mapped('amount'))
@api.depends('payment_ids.amount', 'payment_ids.state', 'payment_ids.payment_type')
def _compute_total_paid_amount(self):
for rec in self:
posted_payments = rec.payment_ids.filtered(lambda p: p.state == 'posted')
outbound_total = sum(posted_payments.filtered(lambda p: p.payment_type == 'outbound').mapped('amount'))
rec.total_paid_amount = outbound_total
@api.depends('spent_amount', 'total_paid_amount')
def _compute_remaining_amount(self):
for rec in self:
rec.remaining_amount = (rec.total_paid_amount or 0.0) - (rec.spent_amount or 0.0) - (rec.total_refunded_amount or 0.0)
@api.model
def search(self, args, offset=0, limit=None, order=None, count=False):
if not self.env.su:
user_company_ids = self.env.user.company_ids.ids
company_domain = [('company_id', 'in', user_company_ids)]
args = args + company_domain
return super(HrRequestPledge, self).search(args, offset=offset, limit=limit, order=order, count=count)
@api.model
def search_read(self, domain=None, fields=None, offset=0, limit=None, order=None):
if not self.env.context.get('skip_company_check'):
company_domain = [('company_id', 'in', self.env.user.company_ids.ids)]
domain = (domain or []) + company_domain
return super(HrRequestPledge, self).search_read(
domain, fields, offset, limit, order
)
@api.model
def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
if not self.env.su:
user_company_ids = self.env.user.company_ids.ids
company_domain = [('company_id', 'in', user_company_ids)]
domain = domain + company_domain
return super(HrRequestPledge, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy)
# def unlink(self):
# for i in self:
# if i.state != 'draft':
# raise UserError(_('You can not delete record in state not in draft'))
# return super(HrRequestPledge, self).unlink()
@api.model
def allocate_payment_to_pledges(self, employee_id, amount):
"""
Allocate a payment amount to employee's custody requests ordered by oldest.
Handles partial, full, and exceeded states.
"""
print(f"🚀 allocate_payment_to_pledges called | employee_id={employee_id} | amount={amount}")
remaining_amount = amount
pledges = self.search([
('employee_id', '=', employee_id),
('custody_status', 'in', ['partial','new' ]),
], order="id asc")
print(f"🔍 pledges found: {pledges}")
for pledge in pledges:
if remaining_amount <= 0:
break
spent = pledge.spent_amount or 0.0
total = pledge.emp_expect_amount or 0.0
pledge_remaining = total - spent
if pledge_remaining <= 0:
continue
allocated = min(pledge_remaining, remaining_amount)
pledge.spent_amount = spent + allocated
if pledge.spent_amount < total:
pledge.custody_status = 'partial'
elif pledge.spent_amount == total:
pledge.custody_status = 'paid'
remaining_amount -= allocated
if remaining_amount > 0:
target_pledge = (pledges.filtered(lambda p: p.custody_status == 'partial') or pledges)[-1:]
for pledge in target_pledge:
pledge.spent_amount += remaining_amount
pledge.custody_status = 'exceeded'
break
@api.constrains('custody_type_id', 'emp_expect_amount')
def _check_custody_amount_limit(self):
for record in self:
if record.custody_type_id and record.emp_expect_amount:
if record.emp_expect_amount > record.custody_type_id.max_custody_amount:
raise ValidationError(_(
"The requested amount (%s) exceeds the maximum allowed for the selected custody type (%s)."
) % (record.emp_expect_amount, record.custody_type_id.max_custody_amount))
# @api.depends('dashboard_id.pledge_ids.is_financial_impact')
# def _compute_is_financial_impact(self):
# for record in self:
# if record.dashboard_id:
# record.is_financial_impact = any(
# pledge.is_financial_impact for pledge in record.dashboard_id.pledge_ids
# )
# else:
# record.is_financial_impact = False
# record.is_financial_impact = record.dashboard_id.pledge_ids.is_financial_impact
def get_user_id(self):
employee_id = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1)
if employee_id:
return employee_id.id
else:
return False
# @api.model
# def default_get(self, fields):
# res = super().default_get(fields)
# res['journal_id'] = False
# return res
@api.constrains('emp_expect_amount')
def _check_positive_emp_expect_amount(self):
for rec in self:
if rec.emp_expect_amount <= 0:
raise ValidationError(
_("Employee expect amount should be bigger than zero!")
)
@api.model
def create(self, values):
# Auto-link dashboard if not provided
if not values.get('dashboard_id'):
dashboard = self.env['base.dashbord'].search([ # Fix typo: 'base.dashboard'
('model_name', '=', self._name)
], limit=1)
if dashboard:
values['dashboard_id'] = dashboard.id
# Generate sequence code (your existing logic)
seq = self.env['ir.sequence'].next_by_code('hr.request.pledge') or '/'
values['code'] = seq # Assign the sequence to the 'code' field
# Create the record
return super(HrRequestPledge, self).create(values)
def submit(self):
self.state = "submit"
def direct_manager(self):
self.state = "direct_manager"
def hr_manager(self):
self.state = "hr_manager"
def executive_manager(self):
self.state = "executive_manager"
def refused(self):
self.state = "refused"
def cancel(self):
self.state = "cancel"
def action_open_confirm_wizard(self):
self.ensure_one()
return {
'name': _('Confirmation Wizard for Pledge Payment'),
'type': 'ir.actions.act_window',
'res_model': 'hr.request.pledge.confirm.wizard',
'view_mode': 'form',
'target': 'new',
'context': {
'default_pledge_id': self.id,
'default_action_type': self._context.get('action_type'),
},
}
def pay(self):
if not self.journal_id:
raise ValidationError(_('Please set the journal for this employee.'))
employee_partner = self.employee_id.user_id.partner_id
if not employee_partner:
raise ValidationError(_('Employee must have a related partner.'))
action_type = self.env.context.get('action_type', 'create')
if action_type == 'feed':
amount = self.spent_amount or 0.0
if amount <= 0:
raise ValidationError(_('Spent amount must be greater than zero for feeding the pledge.'))
else:
amount = self.emp_expect_amount or 0.0
payment_vals = {
'payment_type': 'outbound',
'partner_type': 'supplier',
'is_internal_transfer': True,
'skip_paired_payment': True,
'amount': amount,
'journal_id': self.journal_id.id,
'date': fields.datetime.today(),
'ref': self.description,
'hr_request_pledge': self.id,
'hr_request_pledge_id': self.id,
'partner_id': employee_partner.id,
'petty_cash_pledge':True
}
payment = self.env['account.payment'].create(payment_vals)
payment.flush()
payment.refresh()
# employee_partner = self.employee_id.user_id.partner_id
# if employee_partner and payment.move_id:
# debit_lines = payment.move_id.line_ids.filtered(lambda l: l.debit > 0)
# if debit_lines:
# debit_lines.write({'partner_id': employee_partner.id})
if action_type == 'create':
self.state = "pay"
if action_type == 'feed':
message = _("✅ The pledge has been successfully funded with an amount of %.2f.") % amount
else:
message = _("✅ A new pledge has been successfully created with an amount of %.2f.") % amount
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Operation Successful'),
'message': message,
'type': 'success',
'sticky': False,
'next': {'type': 'ir.actions.act_window_close'},
}
}
def refund_remaining_amount(self):
for rec in self:
if rec.remaining_amount <= 0:
raise ValidationError(_('There is no remaining amount to refund.'))
if not rec.journal_id:
raise ValidationError(_('Please set the journal for this employee.'))
employee_partner = rec.employee_id.user_id.partner_id
if not employee_partner:
raise ValidationError(_('Employee must have a related partner.'))
amount = rec.remaining_amount
payment_vals = {
'payment_type': 'inbound',
'partner_type': 'customer',
'is_internal_transfer': True,
'skip_paired_payment': True,
'amount': amount,
'journal_id': rec.journal_id.id,
'date': fields.datetime.today(),
'ref': _('Refund of pledge: %s') % rec.code,
'hr_request_pledge_id': rec.id,
'partner_id': employee_partner.id,
'petty_cash_pledge': True,
}
payment = self.env['account.payment'].create(payment_vals)
payment.flush()
payment.refresh()
message = _("تم إرجاع المبلغ")
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Operation Successful'),
'message': message,
'type': 'success',
'sticky': False,
'next': {'type': 'ir.actions.act_window_close'},
}
}
def action_open_refund_payments(self):
refund_payments = self.env['account.payment'].search([
'|',
('hr_request_pledge_id', '=', self.id),
('hr_request_pledge', '=', self.id),
('payment_type', '=', 'inbound'),
])
return {
'type': 'ir.actions.act_window',
'name': 'Refund Payments',
'res_model': 'account.payment',
'domain': [('id', 'in', refund_payments.ids)] if refund_payments else [('id', '=', False)],
'view_mode': 'tree,form',
'target': 'current',
}
def financialApproval(self):
self.state = "financial_approve"
def action_open_payment_confirmation(self):
"""
"""
return {
'name': _('Confirm Payment'),
'type': 'ir.actions.act_window',
'res_model': 'hr.request.pledge.pay.wizard',
'view_mode': 'form',
'target': 'new',
'context': {'default_pledge_id': self.id, 'action_type': self.env.context.get('action_type')}
}
def action_account_payment_budget_pledge(self):
treeview_ref = self.env.ref('account.view_account_payment_tree', False) # tree view
formview_ref = self.env.ref('odex25_account_saip.view_account_payment_new_approve_form', False)
budget_account_payment = self.env['account.payment'].search([
'|',
('hr_request_pledge_id', '=', self.id),
('hr_request_pledge', '=', self.id),
('payment_type', '=', 'outbound'),
])
if budget_account_payment:
return {
'type': 'ir.actions.act_window',
'name': 'Achieving Budget',
'res_model': 'account.payment',
'domain': [('id', 'in', budget_account_payment.ids)] if budget_account_payment else [],
'view_mode': 'tree,form',
'views': [
(treeview_ref.id, 'tree') if treeview_ref else (False, 'tree'),
(formview_ref.id, 'form') if formview_ref else (False, 'form'),
],
'target': 'current',
}
class AccountMove(models.Model):
_inherit = 'account.move'
def action_post(self):
res = super(AccountMove, self).action_post()
petty_moves = self.filtered(lambda m:
m.move_type in ['in_receipt', 'in_invoice'] and
m.is_petty_paid and
m.state == 'posted'
)
if petty_moves:
employees = petty_moves.mapped('petty_employee_id')
if employees:
pledges = self.env['hr.request.pledge'].search([
('employee_id', 'in', employees.ids),
('state', 'in', ['pay', 'locked'])
])
if pledges:
pledges._compute_spent_amount_from_moves()
return res
class BaseDashboardExtended(models.Model):
_inherit = 'base.dashbord' # Inherit existing dashboard
pledge_ids = fields.One2many(
'hr.request.pledge',
'dashboard_id',
)
class EmployeeJournal(models.Model):
_inherit = "hr.employee"
journal_id = fields.Many2one('account.journal', domain="[('type', '=', 'cash')]")
department = fields.Many2one('hr.department', string="Department")
department_name = fields.Char(
string='Department Name',
related='department.name',
readonly=True,
store=True,
)
class MoveLine(models.Model):
_inherit = 'account.move.line'
@api.model
def _employee_custody_lines_view(self):
employee = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1)
if not employee:
raise UserError("There is no employee associated with this user.")
account_id = employee.journal_id.default_account_id.id
if not account_id:
raise UserError("The employee does not have an associated journal or default account.")
action = {
'type': 'ir.actions.act_window',
'name': 'Employee Account Report',
'res_model': 'account.move.line',
'view_mode': 'tree',
'view_id': self.env.ref('employee_custody_request.view_account_move_line_tree_custom').id,
'domain': [('account_id', '=', account_id)],
'context': {
},
'target': 'current',
}
return action