
    n^jF                     N   d Z ddl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	ed
edefdZd#dededef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gZ G d de      Z G d de      Z G d de      Z G d de      Z G d  d!e      Zy)$u  
budget/models.py
================
Core models for the Budget Management module (Module 1 of EFMS).

Hierarchy:
  BudgetHead  →  BudgetSubHead  →  BudgetItem
                                       ↓
                               BudgetAllocation  (period-based allocation)

Budget code format (auto-generated):
  Head     : OPS          (abbreviation of name)
  SubHead  : OPS-UTL      (head code + sub-abbreviation)
  Item     : OPS-UTL-001  (subhead code + 3-digit sequence)
    N)ValidationError)modelsreverse_lazy)format_html)gettext_lazy)HorillaModelnamemax_lenreturnc                     t        j                  dd|       j                         }|syt        |      dk(  r|d   d| j	                         }n)dj                  d |D              j	                         d| }|xs dS )u   
    Derive a short uppercase abbreviation from a name.
    Examples:
      "Operations"         → "OPS"
      "Human Resources"    → "HR"
      "Information Technology" → "IT"
    z[^a-zA-Z0-9\s] X   r   Nc              3   &   K   | ]	  }|d      yw)r   N ).0ws     1/var/www/python-projects/worksol/budget/models.py	<genexpr>z%_make_abbreviation.<locals>.<genexpr>.   s     +qt+s   )resubsplitlenupperjoin)r
   r   wordsabbrs       r   _make_abbreviationr       sx     FF$b$/557E
5zQQx!'')ww+U++113HW=;3    basefieldc                 .   | }d} |j                   j                  di ||i}|r|j                  |      }|j                         rN|  | }|dz  } |j                   j                  di ||i}|r|j                  |      }|j                         rN|S )u|   
    Ensure a code is unique within `model_class`.  If `base` is taken,
    appends a numeric suffix: BASE2, BASE3, …
       pkr   r   )objectsfilterexcludeexists)r!   model_classr"   instance_pkcodecounterqss          r   _unique_coder0   2   s    
 DG	#			#	#	4udm	4BZZ;Z'
))+y!1'[  ''85$-8{+B ))+ Kr    annual	quarterlymonthlyweeklyAnnual	QuarterlyMonthlyWeeklyc            	           e Zd ZdZ ej
                  ddd ed       ed            Z G d d	      Zd
 Z	d Z
edd       Zy)BudgetSettingszn
    Organisation-wide budget settings.
    There should only ever be one instance (enforced in clean()).
       r$   r   zOver-budget Limit (%)z|Maximum percentage by which a request may exceed the remaining budget balance before it is blocked.  0 = no overrun allowed.
max_digitsdecimal_placesdefaultverbose_name	help_textc                   ,    e Zd Z ed      Z ed      Zy)BudgetSettings.MetazBudget SettingsN)__name__
__module____qualname___r@   verbose_name_pluralr   r    r   MetarC   k   s    *+ 12r    rI   c                     | j                   dk  rt        dt        d      i      t        j                  j                  | j                        }|j                         rt        t        d            y )Nr   overrun_limit_percentzLimit cannot be negative.r%   zBOnly one Budget Settings record is allowed. Edit the existing one.)rK   r   rG   r:   r'   r)   r&   r*   )selfr/   s     r   cleanzBudgetSettings.cleano   sn    %%)!(!,G*HI  ##++tww+799;!VW  r    c                 "    d| j                    dS )Nz Budget Settings (overrun limit: z%))rK   rL   s    r   __str__zBudgetSettings.__str__{   s    1$2L2L1MRPPr    c                 J    | j                   j                  dddi      \  }}|S )Nr   rK   r   )r&   defaults)r'   get_or_create)clsobjrG   s      r   get_settingszBudgetSettings.get_settings~   s+    **a;RTU:V*WQ
r    N)r   r:   )rD   rE   rF   __doc__r   DecimalFieldrG   rK   rI   rM   rP   classmethodrV   r   r    r   r:   r:   Z   se    
 0F//./L
	3 3
Q  r    r:   c                       e Zd ZdZ ej
                  dd ed            Z ej
                  dd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 ed      e_         xZS )
BudgetHeadz
    Top-level budget category (e.g. 'Operations', 'Human Resources').
    Code is auto-generated from the name abbreviation.
       Tz	Head Name)
max_lengthuniquer@      FBudget Coder]   r^   editabler@   Descriptionblankr@   c                   2    e Zd Z ed      Z ed      ZdgZy)BudgetHead.MetaBudget HeadzBudget Headsr
   N)rD   rE   rF   rG   r@   rH   orderingr   r    r   rI   rg      s    '/8r    rI   c                     | j                   s6t        | j                        }t        |t        | j
                        | _         t        |   |i | y )Nr,   )r-   r   r
   r0   r[   r&   supersave)rL   argskwargsr!   	__class__s       r   rm   zBudgetHead.save   s=    yy%dii0D$T:477KDId%f%r    c                 :    d| j                    d| j                   S N[] r-   r
   rO   s    r   rP   zBudgetHead.__str__       499+R		{++r    c                 4    t        dd| j                  i      S )Nzbudget-head-updater&   ro   r   r&   rO   s    r   get_update_urlzBudgetHead.get_update_url       0$IIr    c                     t        d      S Nzgeneric-deleter   rO   s    r   get_delete_urlzBudgetHead.get_delete_url       ,--r    c                     | j                   S Nr%   rO   s    r   get_delete_instancezBudgetHead.get_delete_instance       wwr    c                     t        d      S )Nzbudget-head-viewr   rO   s    r   get_nav_urlzBudgetHead.get_nav_url       .//r    c                 n    | j                   j                  d      j                         }t        d|      S NT)	is_active8<span class="badge oh-badge oh-badge--primary">{}</span>)	sub_headsr(   countr   rL   r   s     r   sub_head_countzBudgetHead.sub_head_count   s4    %%%5;;=F
 	
r    z	Sub-Heads)rD   rE   rF   rW   r   	CharFieldrG   r
   r-   	TextFielddescriptionrI   rm   rP   rz   r~   r   r   r   short_description__classcell__rp   s   @r   r[   r[      s    
 6s4anUD6}%	D #&""Am<LMK 
&,J.0
 ()~N$$r    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                  dd	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 ed      e_         xZS )BudgetSubHeadz}
    Second-level budget category (e.g. 'Utilities', 'Maintenance').
    Code is auto-generated as {HEAD_CODE}-{ABBREV}.
    r   rh   	on_deleterelated_namer@   r\   zSub-Head Namer]   r@      TFr`   ra   rc   rd   c                   >    e Zd Z ed      Z ed      ZddgZddggZy)BudgetSubHead.MetaBudget Sub-HeadzBudget Sub-Heads
head__namer
   headNrD   rE   rF   rG   r@   rH   ri   unique_togetherr   r    r   rI   r      s0    *+ 23 &)"F+,r    rI   c                     | j                   sQt        | j                        }| j                  j                    d| }t	        |t
        | j                        | _         t        |    |i | y )N-rk   )	r-   r   r
   r   r0   r   r&   rl   rm   )rL   rn   ro   r   r!   rp   s        r   rm   zBudgetSubHead.save   sV    yy%dii0Diinn%Qtf-D$T=dggNDId%f%r    c                 :    d| j                    d| j                   S rr   ru   rO   s    r   rP   zBudgetSubHead.__str__   rv   r    c                 4    t        dd| j                  i      S )Nzbudget-sub-head-updater&   rx   ry   rO   s    r   rz   zBudgetSubHead.get_update_url   s    4dDGG_MMr    c                     t        d      S r}   r   rO   s    r   r~   zBudgetSubHead.get_delete_url   r   r    c                     | j                   S r   r%   rO   s    r   r   z!BudgetSubHead.get_delete_instance   r   r    c                     t        d      S )Nzbudget-sub-head-viewr   rO   s    r   r   zBudgetSubHead.get_nav_url   s    233r    c                 n    | j                   j                  d      j                         }t        d|      S r   )itemsr(   r   r   r   s     r   
item_countzBudgetSubHead.item_count   s4    

!!D!1779F
 	
r    Items)rD   rE   rF   rW   r   
ForeignKeyr[   CASCADErG   r   r   r
   r-   r   r   rI   rm   rP   rz   r~   r   r   r   r   r   r   s   @r   r   r      s    
 6.. }%	D 6s?9KLD6}%	D #&""Am<LMK- -&,N.4
 $%W:J  r    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                  dd	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 ed      e_         xZS )
BudgetItemz
    Leaf-level budget line item (e.g. 'Generator Fuel', 'Electrical Repairs').
    Code is auto-generated as {SUBHEAD_CODE}-{3-digit-sequence}.
    r   r   r   r\   z	Item Namer   (   TFr`   ra   rc   rd   c                   >    e Zd Z ed      Z ed      Zg dZddggZy)BudgetItem.MetaBudget ItemzBudget Items)sub_head__head__namesub_head__namer
   sub_headr
   Nr   r   r    r   rI   r   !  s)    '/E&/0r    rI   c                    | j                   st        j                  j                  | j                  | j                  j                         j                  d      j                         }|r*	 t        |j                   j                  dd      d         }nd}| j                  j                    d|dz   d| _         t        | ,  |i | y # t        $ r d}Y Aw xY w)N)r   code__startswithr-   r   r   r   03d)r-   r   r'   r(   r   order_bylastintrsplit
ValueErrorrl   rm   )rL   rn   ro   r   last_seqrp   s        r   rm   zBudgetItem.save'  s    yy ""))!]]T]]=O=O *  &!  !"499#3#3C#;B#?@H ==--.a1S/ABDId%f% " ! H!s   -(C CCc                 :    d| j                    d| j                   S rr   ru   rO   s    r   rP   zBudgetItem.__str__;  rv   r    c                 4    t        dd| j                  i      S )Nzbudget-item-updater&   rx   ry   rO   s    r   rz   zBudgetItem.get_update_urlB  r{   r    c                     t        d      S r}   r   rO   s    r   r~   zBudgetItem.get_delete_urlE  r   r    c                     | j                   S r   r%   rO   s    r   r   zBudgetItem.get_delete_instanceH  r   r    c                     t        d      S )Nzbudget-item-viewr   rO   s    r   r   zBudgetItem.get_nav_urlK  r   r    c                 N    | j                   j                         }t        d|      S )Nr   )allocationsr   r   r   s     r   allocation_countzBudgetItem.allocation_countR  s(      &&(F
 	
r    Allocations)rD   rE   rF   rW   r   r   r   r   rG   r   r   r
   r-   r   r   rI   rm   rP   rz   r~   r   r   r   r   r   r   s   @r   r   r     s    
 !v  ..()	H 6s;HD6}%	D #&""Am<LMK1 1&(,J.0
 *+=)9&&r    r   c            	          e Zd ZdZ ej
                  eej                  d ed            Z	 ej                   ed       ed            Z ej                  dee ed	      
      Z ej                  dd ed       ed            Z ej"                  ddd ed            Z ej"                  ddd ed       ed            Z ej"                  ddd ed       ed            Z ej"                  ddd ed       ed            Z G d d      Zd Zd Zd Zd  Zed!        Zed"        Zed#        Zed$        Zd% Z d& Z! ed'      e!_"        d( Z# ed)      e#_"        d* Z$ ed+      e$_"        d, Z%y-).BudgetAllocationu?  
    Period-based allocation record for a BudgetItem.

    One record per (item, fiscal_year, period_type, period_value).
    The `period_value` semantics depend on `period_type`:
      - annual    → NULL (one per fiscal year)
      - quarterly → 1–4
      - monthly   → 1–12
      - weekly    → 1–53
    r   r   r   zFiscal Yearu!   e.g. 2025 for FY 2025 (Jan–Dec))r@   rA   r_   zPeriod Type)r]   choicesr?   r@   TzPeriod ValueuJ   Quarter (1–4), Month (1–12), or Week (1–53). Leave blank for Annual.)nullre   r@   rA      r$   r   u   Allocated Amount (₦))r=   r>   r?   r@   u   Committed Amount (₦)z,Total of pending/submitted expense requests.r<   u   Approved Amount (₦)z1Total of approved but not yet disbursed requests.u   Disbursed Amount (₦)zTotal actually paid out.c                   >    e Zd Z ed      Z ed      Zg dZg dgZy)BudgetAllocation.MetazBudget AllocationzBudget Allocations)z-fiscal_yearperiod_typeperiod_value)itemfiscal_yearr   r   Nr   r   r    r   rI   r     s&    ,- 45BQRr    rI   c                 4    t        dd| j                  i      S )Nzbudget-allocation-updater&   rx   ry   rO   s    r   rz   zBudgetAllocation.get_update_url  s    6dggOOr    c                     t        d      S r}   r   rO   s    r   r~   zBudgetAllocation.get_delete_url  r   r    c                     | j                   S r   r%   rO   s    r   r   z$BudgetAllocation.get_delete_instance  r   r    c                     t        d      S )Nzbudget-allocation-viewr   rO   s    r   r   zBudgetAllocation.get_nav_url  s    455r    c                 4    | j                   | j                  z
  S )u+   Remaining budget = allocated – disbursed.allocated_amountdisbursed_amountrO   s    r   available_balancez"BudgetAllocation.available_balance       $$t'<'<<<r    c                 n    | j                   dk(  ryt        | j                  | j                   z  dz  d      S )Nr   d   r   )r   roundr   rO   s    r   utilization_percentz$BudgetAllocation.utilization_percent  s6      A%d++d.C.CCsJANNr    c                 4    | j                   | j                  kD  S r   )r   r   rO   s    r   
is_overrunzBudgetAllocation.is_overrun  s    $$t'<'<<<r    c                 4    | j                   | j                  z
  S )z'Positive = surplus, Negative = overrun.r   rO   s    r   variancezBudgetAllocation.variance  r   r    c                 &   t         j                         }t        |j                        }t        | j                        }t        |      }||k  ry|dk(  rdt        d      j                  ||      fS t        | j                        d|dz  z   z  }t        | j                        t        | j                        z   t        | j                        z   }||z   |k  rdt        d      j                  |      fS dt        d	      j                  |||      fS )
z
        Returns (allowed: bool, reason: str).
        Validates whether a new request of `requested_amount` can proceed
        given the current balance and the configured overrun limit.
        )Tr   r   FuH   Insufficient budget balance. Available: ₦{:.2f}, Requested: ₦{:.2f}.r   r   TzYWarning: This request exceeds the budget balance but is within the {:.0f}% overrun limit.ux   Request exceeds the budget balance and the configured {:.0f}% overrun limit. Available: ₦{:.2f}, Requested: ₦{:.2f}.)r:   rV   floatrK   r   rG   formatr   r   approved_amountcommitted_amount)rL   requested_amountsettings	limit_pctbalancereqmax_allowedalready_outs           r   check_overrunzBudgetAllocation.check_overrun  s     "..0(889	../$%'>>!ZfWc"# # D112a)c/6IJD112U4;O;O5PPSXY]YnYnSoo+) fY   
 a:
 &GS
)* 	*r    c                    t         t        d      t        t        d      j                  | j                  xs d      t
        t        d      j                  | j                  rg d| j                  xs d   nd| j                        t        t        d	      j                  | j                  | j                        i}|j                  | j                  | j                        S )
Nr5   zQ{}r   z{month} {year})r   JanFebMarAprMayJunJulAugSepOctNovDecr   r   )monthyearzWeek {} / {})
PERIOD_ANNUALrG   PERIOD_QUARTERLYr   r   PERIOD_MONTHLYr   PERIOD_WEEKLYgetr   )rL   labelss     r   period_labelzBudgetAllocation.period_label  s    1X;ahood.?.?.E2FA f .2->-> ##(q* EH%%   1^,33D4E4EtGWGWX
 zz$**D,<,<==r    Periodc                 N    | j                   }|dk  rdnd}t        d|d|d      S )Nr   zoh-badge--dangerzoh-badge--successz)<span class="badge oh-badge {}">{}</span>u   ₦,.2f)r   r   )rL   r   colours      r   balance_displayz BudgetAllocation.balance_display  s;    (('.{#8K7'$ 
 	
r    zAvailable Balancec                 F    | j                   rt        d      S t        d      S )Nzm<span class="badge oh-badge oh-badge--danger"><ion-icon name="warning-outline"></ion-icon> Over-budget</span>z><span class="badge oh-badge oh-badge--success">On Track</span>)r   r   rO   s    r   overrun_indicatorz"BudgetAllocation.overrun_indicator  s-    ?? 
 L
 	
r    Statusc           	          d| j                   j                   d| j                  j                          d| j                   d| j
                  dS )Nrs   rt   z FYu    — ₦r  )r   r-   r   
capitalizer   r   rO   s    r   rP   zBudgetAllocation.__str__  sT    		r$"2"2"="="?!@ A!!"(4+@+@*FH	
r    N)&rD   rE   rF   rW   r   r   r   r   rG   r   PositiveSmallIntegerFieldr   r   PERIOD_TYPE_CHOICESr  r   r   rX   r   r   r   r   rI   rz   r~   r   r   propertyr   r   r   r   r   r  r   r  r  rP   r   r    r   r   r   _  s   	 6.."}%	D 3&22}%78K #&""#}%	K 4633~&`a	L +v**/0	 +v**/0BC *f))./GHO +v**/0./S SP.6 = = O O
 = = = =*H>" &'x[L"
 )**=(>O%	
 +,H+'
r    r   )r;   )r-   N)rW   r   django.core.exceptionsr   	django.dbr   django.urlsr   django.utils.htmlr   django.utils.translationr   rG   horilla.modelsr	   strr   r   r0   r  r  r  r  r  r:   r[   r   r   r   r   r    r   <module>r      s     
 2  $ ) 6 'S 3 s $s  SV .   AhK q~&Qy\"AhK 	 '\ '\76 76|?.L ?.LL: L:f@
| @
r    