# -*- 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