
    n^jF                        d Z ddlZddlmZ ddlmZmZ ddlmZ ddl	m
Z
 ddlmZ ddlmZ dd	lmZ dd
lmZ ddlmZ ddlmZmZ ddlmZ ddlmZ dZdZdZdZ dZ!dZ"dZ#e ed      fe ed      fe ed      fe  ed      fe! ed      fe" ed      fe# ed      fgZ$ee!e gZ%ededede d e!d!e"d"e#d#iZ& G d$ d%e      Z' G d& d'e      Z(d( Z)d) Z*d* Z+ G d+ d,e      Z, G d- d.e      Z-d/Z.dZ/dZ0dZ1d0Z2dZ3dZ4dZ5e. ed1      fe/ ed      fe0 ed      fe1 ed2      fe2 ed3      fe3 ed      fe4 ed      fe5 ed      fgZ6 G d4 d5ejn                        Z8 G d6 d7ejn                        Z9y)8u  
expense/models.py
=================
Models for Module 2 (Expense Management) + Module 3 (Approval Workflow Engine).

Status lifecycle:
  submit → PENDING → [HOD → Director → MD → Accountant] → APPROVED → [Disbursement] → DISBURSED
                  ↘ KIV ↗ (recall)
                  ↘ REJECTED → (resubmit once) → RESUBMITTED → re-enters chain
                  ↘ CLOSED (after 2nd rejection)
    N)date)GroupUser)ValidationError)FileExtensionValidator)modelsreverse_lazy)format_html)gettext_lazy)
Department)BudgetAllocation
BudgetItem)Employee)HorillaModelpendingapprovedrejectedkivresubmitted	disbursedclosedzPending ApprovalApprovedRejectedzKept In View (KIV)Resubmitted	DisbursedClosedzoh-badge--warningzoh-badge--successzoh-badge--dangeroh-badge--secondaryzoh-badge--infozoh-badge--primaryzoh-badge--darkc                       e Zd ZdZ ej
                  dd ed            Z ej                  d ed            Z	 G d d	      Z
d
 Zd Zd Zed        Zy)ExpenseApprovalWorkflowzt
    Named approval workflow (e.g. 'Expense Approval').
    An organisation typically has one default workflow.
       TzWorkflow Name)
max_lengthuniqueverbose_nameDescriptionblankr$   c                   2    e Zd Z ed      Z ed      ZdgZy)ExpenseApprovalWorkflow.MetaApproval WorkflowzApproval WorkflowsnameN__name__
__module____qualname___r$   verbose_name_pluralordering     2/var/www/python-projects/worksol/expense/models.pyMetar)   N   s     ,- 458r4   r6   c                     | j                   S N)r+   selfs    r5   __str__zExpenseApprovalWorkflow.__str__S   s    yyr4   c                     t        d      S )Nzexpense-workflow-configr	   r9   s    r5   get_absolute_urlz(ExpenseApprovalWorkflow.get_absolute_urlV   s    566r4   c                 4    t        dd| j                  i      S )Nzexpense-workflow-editpkkwargsr
   r?   r9   s    r5   get_update_urlz&ExpenseApprovalWorkflow.get_update_urlY   s    3T477OLLr4   c                 t    | j                   j                  d      j                  d      j                         S )zDReturn the first active workflow (used as default for new requests).T)	is_activeid)objectsfilterorder_byfirst)clss    r5   get_defaultz#ExpenseApprovalWorkflow.get_default\   s0     {{!!D!1::4@FFHHr4   N)r-   r.   r/   __doc__r   	CharFieldr0   r+   	TextFielddescriptionr6   r;   r=   rC   classmethodrL   r3   r4   r5   r    r    E   sm    
 6s4aFXYD"&""Am<LMK 
7M I Ir4   r    c                      e Zd ZdZ ej
                  eej                  d ed            Z	 ej                  d ed            Z ej
                  eej                   ed       ed	      
      Z ej                   ed       ed            Z ej"                  d ed       ed            Z G d d      Zd Zd Zd Zd Zy)ExpenseApprovalStagezq
    One stage in an approval chain.
    Members of the linked Django Group are the approvers at this stage.
    stagesWorkflow	on_deleterelated_namer$   r!   z
Stage Namer"   r$   zApprover Groupz7Members of this Django Group can approve at this stage.)rW   r$   	help_textOrderz<Execution order (1 = first). Must be unique within workflow.)r$   rZ   FzIs Disbursement StagezNWhen this stage approves, the request is marked DISBURSED instead of APPROVED.)defaultr$   rZ   c                   >    e Zd Z ed      Z ed      ZddgZddggZy)ExpenseApprovalStage.MetazApproval StagezApproval StagesworkfloworderN)r-   r.   r/   r0   r$   r1   r2   unique_togetherr3   r4   r5   r6   r^      s0    )* 12(&01r4   r6   c                 f    | j                   j                   d| j                   d| j                   S )Nu    — Stage z: )r_   r+   r`   r9   s    r5   r;   zExpenseApprovalStage.__str__   s,    --$$%[BtyykJJr4   c                 J    t        d| j                  | j                  d      S )Nzexpense-stage-edit)workflow_pkr?   r@   )r
   workflow_idr?   r9   s    r5   rC   z#ExpenseApprovalStage.get_update_url   s$     #'#3#3477C
 	
r4   c                     t         j                  j                  | j                  | j                  d      j                  d      j                         S )zDReturn the next stage in this workflow, or None if this is the last.T)r_   	order__gtrE   r`   rS   rG   rH   r_   r`   rI   rJ   r9   s    r5   get_next_stagez#ExpenseApprovalStage.get_next_stage   sD     !((//$** 0  XgUW	
r4   c                     t         j                  j                  | j                  | j                  d      j                  d      j                         S )NT)r_   	order__ltrE   z-orderrh   r9   s    r5   get_previous_stagez'ExpenseApprovalStage.get_previous_stage   sB     ((//$** 0  XhUW	
r4   N)r-   r.   r/   rM   r   
ForeignKeyr    CASCADEr0   r_   rN   r+   r   PROTECTgroupPositiveSmallIntegerFieldr`   BooleanFieldis_disbursement_stager6   r;   rC   ri   rl   r3   r4   r5   rS   rS   b   s    
 !v  ..z]	H 6s<IDF..'(MN	E -F,,wZRSE 0F//./\
2 2K


r4   rS   c                  `   t        j                         j                  } d|  d}t        j                  j                  |      j                  d      j                         }|r,	 t        |j                  j                  d      d         dz   }nd}| |dS # t        t        f$ r d}Y w xY w)z/Auto-generate reference number: EXP-YYYY-NNNNNNzEXP--)reference_number__startswithreference_number   06d)r   todayyearExpenseRequestrG   rH   rI   lastintrw   split
ValueError
IndexError)r|   prefixr~   seqs       r5   _generate_referencer      s    ::<DD6^F%%6%J	$	%	 	
 	d++11#6r:;a?C Xc#Y	 J' 	C	s   %*B B-,B-c                     t         j                  j                  |      d   j                         }d| j                  j
                   d| j                  xs d | S )Nry   zexpense/documents//new)ospathsplitextlowerexpenserw   r?   )instancefilenameexts      r5   _expense_doc_upload_pathr      sU    
''

8
$Q
'
-
-
/C 0 0 A AB!HKKDXSXCYZ]Y^__r4   c                 >   g d}t         j                  j                  | j                        d   j	                  d      j                         }||vr)t        t        d      |dj                  |      dz        | j                  dkD  rt        t        d            y )	N)pdfjpgjpegpngdocdocxry   .z7Unsupported file type '.%(ext)s'. Allowed: %(allowed)s.z, )r   allowedi  P zFile size must not exceed 5 MB.)
r   r   r   r+   lstripr   r   r0   joinsize)fileallowed_extr   s      r5   _validate_documentr      s    >K
''

499
%a
(
/
/
4
:
:
<C
+GHdii&<=>
 	
 yy?"a ABCC #r4   c            
           e Zd ZdZ ej
                  ddd ed            Z ej                  e	ej                  d ed      	      Z ej                  eej                  dd ed
       ed            Z ej                  eej                   d ed      	      Z ej
                  d ed            Z ej&                  dd ed            Z ej*                   ed            Z ej
                  dee ed            Z ej                  eej                    ed            Z ej                  eej                  ddd ed            Z ej<                  d ed             Z ej@                  d! ed"      #      Z! G d$ d%      Z" fd&Z#d' Z$d( Z%d) Z&d* Z'd+ Z(d, Z)d- Z*d. Z+d/ Z,d0 Z-d1 Z.d2 Z/d3 Z0d4 Z1d5 Z2d6 Z3d7 Z4 ed      e4_5        d8 Z6 ed9      e6_5         xZ7S ):r}   z<
    A single expense request submitted by an employee.
       TFzReference No.)r"   r#   editabler$   expense_requestsr   rV   r   z"Auto-filled from employee profile.)rW   nullr'   r$   rZ   zBudget Itemi,  zRequest TitlerY         u   Amount (₦))
max_digitsdecimal_placesr$   zPurpose / Justificationr$      Statusr"   choicesr\   r$   r*   )rW   r$   pending_requestszCurrent StagerW   r   r'   rX   r$   zSubmitted Atauto_now_addr$   r   zResubmit Count)r\   r$   c                   2    e Zd Z ed      Z ed      ZdgZy)ExpenseRequest.MetaExpense RequestzExpense Requestsz-submitted_atNr,   r3   r4   r5   r6   r     s!    *+ 23#$r4   r6   c                 Z    | j                   st               | _         t        |   |i | y r8   )rw   r   supersaver:   argsrA   	__class__s      r5   r   zExpenseRequest.save  s(    $$$7$9D!d%f%r4   c                 V    | j                    d| j                   d| j                  ddS )N    — u    (₦z,.2f))rw   titleamountr9   s    r5   r;   zExpenseRequest.__str__  s.    ''(djj\t{{4>PPQRRr4   c                 4    t        dd| j                  i      S Nzexpense-request-detailr?   r@   rB   r9   s    r5   r=   zExpenseRequest.get_absolute_url      4dDGG_MMr4   c                 4    t        dd| j                  i      S r   rB   r9   s    r5   get_detail_urlzExpenseRequest.get_detail_url"  r   r4   c                 "    | j                         S )uG   Expense requests cannot be edited directly — link to the detail view.)r   r9   s    r5   rC   zExpenseRequest.get_update_url%  s    ""$$r4   c                     t        d      S )Nzgeneric-deleter	   r9   s    r5   get_delete_urlzExpenseRequest.get_delete_url)  s    ,--r4   c                     | j                   S r8   )r?   r9   s    r5   get_delete_instancez"ExpenseRequest.get_delete_instance,  s    wwr4   c                 6    t        d| j                  |d      S )zGReturn the URL of an action modal (approve/reject/kiv/resubmit/recall).zexpense-action-modal)r?   actionr@   rB   )r:   r   s     r5   get_action_urlzExpenseRequest.get_action_url/  s    "''V4
 	
r4   c                 4    t        dd| j                  i      S )Nzexpense-approver?   r@   rB   r9   s    r5   get_approve_urlzExpenseRequest.get_approve_url6  s    -tTWWoFFr4   c                 4    t        dd| j                  i      S )Nzexpense-rejectr?   r@   rB   r9   s    r5   get_reject_urlzExpenseRequest.get_reject_url9  s    ,dDGG_EEr4   c                 4    t        dd| j                  i      S )Nzexpense-kivr?   r@   rB   r9   s    r5   get_kiv_urlzExpenseRequest.get_kiv_url<  s    M4/BBr4   c                 4    t        dd| j                  i      S )Nzexpense-recall-kivr?   r@   rB   r9   s    r5   get_recall_kiv_urlz!ExpenseRequest.get_recall_kiv_url?  s    0$IIr4   c                 4    t        dd| j                  i      S )Nzexpense-resubmitr?   r@   rB   r9   s    r5   get_resubmit_urlzExpenseRequest.get_resubmit_urlB  s    .dggGGr4   c                 J    | j                   t        k(  xr | j                  dk  S )Nry   )statusSTATUS_REJECTEDresubmit_countr9   s    r5   can_resubmitzExpenseRequest.can_resubmitI  s!    {{o-I$2E2E2IIr4   c                     | j                   t        k7  ry| j                  j                  |k(  ry| j                  r?|j
                  j                  | j                  j                        j                         ryy)zGOriginal requester or any member of the current stage group can recall.FTrF   )	r   
STATUS_KIVemployeeemployee_user_idcurrent_stagegroupsrH   group_idexistsr:   users     r5   can_recall_kivzExpenseRequest.can_recall_kivL  s^    ;;*$==))T1$++"4"48J8J8S8S"4"T"["["]r4   c                     | j                   t        t        fvry| j                  sy|j                  j                  | j                  j                        j                         S )zFTrue if the user is in the current stage's group and status is active.Fr   )r   STATUS_PENDINGSTATUS_RESUBMITTEDr   r   rH   r   r   r   s     r5   is_approvable_byzExpenseRequest.is_approvable_byV  sP    ;;~/ABB!!{{!!T%7%7%@%@!AHHJJr4   c                    t        j                         }|j                  }t        j                  j                  | j                  |d      j                         }|r|S t        j                  j                  | j                  |d|j                        j                         }|r|S |j                  dz
  dz  dz   }t        j                  j                  | j                  |d|      j                         S )z=Find the best matching BudgetAllocation for tracking amounts.annual)itemfiscal_yearperiod_typemonthly)r   r   r   period_valuery      	quarterly)	r   r{   r|   r   rG   rH   budget_itemrJ   month)r:   r{   r|   allocquarters        r5   get_current_allocationz%ExpenseRequest.get_current_allocation^  s    

zz ((//!!t 0 

%' 	 L ((//!!t 0 
 %' 	 L;;?q(1,''..!!t  / 
 %'	r4   c                 |    t         j                  | j                  d      }| j                         }t	        d||      S )Nr   z)<span class="badge oh-badge {}">{}</span>)STATUS_BADGE_CLASSESgetr   get_status_displayr   )r:   csslabels      r5   status_badgezExpenseRequest.status_badgez  s;    "&&t{{4IJ'')7e
 	
r4   c                 .    t        d| j                        S )Nu
   ₦{:,.2f})r   r   r9   s    r5   amount_displayzExpenseRequest.amount_display  s    <55r4   Amount)8r-   r.   r/   rM   r   rN   r0   rw   rm   r   rn   r   r   SET_NULL
departmentr   ro   r   r   DecimalFieldr   rO   justificationSTATUS_CHOICESr   r   r    r_   rS   r   DateTimeFieldsubmitted_atrq   r   r6   r   r;   r=   r   rC   r   r   r   r   r   r   r   r   r   r   r   r   r  short_descriptionr  __classcell__r   s   @r5   r}   r}      sK    (v'''	 !v  ..'z]	H #""//|_89J $&##..'}%	K F!O:LME V  ~&F
 %F$$!4M2NOMVx[	F !v  ..*+H
 &F%%//''M (6''T.HYZL5V55'(N
% %
&
SNN%.
GFCJHJK8
 &'x[L"6 (){N$$r4   r}   c            
       8    e Zd ZdZ ej
                  eej                  d ed            Z	 ej                  eeg ed            Z ej                  dd	      Z ej
                  eej"                  d
dd ed            Z G d d      Z fdZd Z xZS )ExpenseDocumentz4Supporting documents attached to an expense request.	documentsr   rV   File)	upload_to
validatorsr$      F)r"   r   Texpense_documents_uploadedzUploaded By)rW   r   r   rX   r$   c                   ,    e Zd Z ed      Z ed      Zy)ExpenseDocument.MetazExpense DocumentzExpense DocumentsN)r-   r.   r/   r0   r$   r1   r3   r4   r5   r6   r    s    +, 34r4   r6   c                     | j                   rD| j                  s8t        j                  j	                  | j                   j
                        | _        t        |   |i | y r8   )r   	file_namer   r   basenamer+   r   r   r   s      r5   r   zExpenseDocument.save  s?    99T^^WW--diinn=DNd%f%r4   c                 H    | j                   xs t        | j                        S r8   )r  strr   r9   s    r5   r;   zExpenseDocument.__str__  s    ~~/TYY/r4   )r-   r.   r/   rM   r   rm   r}   rn   r0   r   	FileFieldr   r   r   rN   r  r   r  uploaded_byr6   r   r;   r  r  s   @r5   r  r    s    >f.. ()	G 6*&'vYD
 !  C%@I#&##//1}%K5 5&
0r4   r  	submittedrecalled	SubmittedzKept In ViewzRecalled from KIVc            	          e Zd ZdZ ej
                  eej                  d ed            Z	 ej
                  e
ej                  dd ed            Z ej                  de ed	      
      Z ej
                  eej                  d ed            Z ej$                  d ed            Z ej(                  d ed            Z G d d      Zd Zy)ExpenseApprovalLogu   
    Immutable audit log. Never updated or deleted — only appended to.
    Does NOT extend HorillaModel to prevent soft-delete and modification tracking.
    approval_logsr   rV   TzWorkflow Stage)rW   r   r'   r$   r   Action)r"   r   r$   ActorrW   r   r$   zComment / Reasonr&   	Timestampr   c                   4    e Zd Z ed      Z ed      ZddgZy)ExpenseApprovalLog.MetazApproval Log EntryzApproval Log Entriesr   
created_atNr,   r3   r4   r5   r6   r+    s#    -. 67|,r4   r6   c                     | j                   j                   d| j                   d| j                   d| j                  dS )Nr   z by z at z%Y-%m-%d %H:%M)r   rw   r   actorr,  r9   s    r5   r;   zExpenseApprovalLog.__str__  s>    ,,//0dkk]$tzzlRVW[WfWfguVvwwr4   N)r-   r.   r/   rM   r   rm   r}   rn   r0   r   rS   r  stagerN   ACTION_CHOICESr   r   r.  rO   commentr
  r,  r6   r;   r3   r4   r5   r$  r$    s    
  f..$()	G F//'(E Vx[F
 F//wZ	E fT:L8MNG%%%4anUJ- -
xr4   r$  c            
       B   e Zd ZdZ ej
                  eej                  d ed            Z	 ej
                  e
ej                  d ed            Z ej
                  eej                  dd ed	      
      Z ej                   ed            Z ej"                   ed            Z ej&                  dee ed            Z ej.                  dd ed            Z ej
                  eej                  ddd ed            Z G d d      Zed        Zd Zy)	KIVRecordz
    Tracks a 'Kept In View' hold on an expense request.
    A request can be KIV'd multiple times (one active at a time).
    kiv_recordsr   rV   TzStage at KIVr(  
kiv_set_byzSet By)rW   r   rX   r$   z
KIV Reasonr   zRevisit Dater   zStatus Before KIVr   zRecalled At)r   r'   r$   kiv_recalled_byzRecalled Byr   c                   2    e Zd Z ed      Z ed      ZdgZy)KIVRecord.Metaz
KIV RecordzKIV Recordsz-revisit_dateNr,   r3   r4   r5   r6   r8  &  s    .#$r4   r6   c                     | j                   d u S r8   )recalled_atr9   s    r5   rE   zKIVRecord.is_active+  s    4''r4   c                 t    | j                   rdnd}d| d| j                  j                   d| j                   S )NActiveRecalledzKIV [u   ] — z	 revisit )rE   r   rw   revisit_date)r:   r   s     r5   r;   zKIVRecord.__str__/  s:    !^^vhfT\\%B%B$C9TM^M^L_``r4   N)r-   r.   r/   rM   r   rm   r}   rn   r0   r   rS   r  r/  r   set_byrO   reason	DateFieldr>  rN   r	  r   status_before_kivr
  r:  recalled_byr6   propertyrE   r;   r3   r4   r5   r3  r3    sG   
  f.."()	G F//~&	E V//!x[F V1\?;F#6##>1BCL(((*+	 '&&&D1]K[\K#&##//&}%K% %
 ( (ar4   r3  ):rM   r   datetimer   django.contrib.auth.modelsr   r   django.core.exceptionsr   django.core.validatorsr   	django.dbr   django.urlsr
   django.utils.htmlr   django.utils.translationr   r0   base.modelsr   budget.modelsr   r   employee.modelsr   horilla.modelsr   r   STATUS_APPROVEDr   r   r   STATUS_DISBURSEDSTATUS_CLOSEDr	  ACTIVE_STATUSESr   r    rS   r   r   r   r}   r  ACTION_SUBMITTEDACTION_APPROVEDACTION_REJECTED
ACTION_KIVACTION_RECALLEDACTION_RESUBMITTEDACTION_DISBURSEDACTION_CLOSEDr0  Modelr$  r3  r3   r4   r5   <module>r^     s  
 
  2 2 9  $ ) 6 " 6 $ ' 
"   Q)*+a
m$a
m$'()=)*q~&AhK  "#5zB '('%()# Il I:?
< ?
N &`
	Dy3\ y3B"0l "0R  
"   q~&a
m$a
m$>"#a+,-=)*q~&AhK 	'x 'x^6a 6ar4   