From ee5b4ff280dc4a3dd17825c10e4341f8b18f60a5 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sun, 8 Feb 2026 06:12:20 +0000 Subject: [PATCH] update final --- .../__pycache__/signals.cpython-311.pyc | Bin 6652 -> 7022 bytes accounting/__pycache__/views.cpython-311.pyc | Bin 14777 -> 15167 bytes accounting/signals.py | 46 ++- accounting/views.py | 7 +- core/__pycache__/urls.cpython-311.pyc | Bin 13378 -> 13508 bytes core/__pycache__/views.cpython-311.pyc | Bin 48263 -> 62449 bytes core/patch_views_sales_list.py | 31 ++ core/templates/core/expenses.html | 72 +++- core/urls.py | 1 + core/views.py | 332 +++++++++++++++++- debug_accounting.py | 31 ++ move_project.py | 33 ++ patch_expense_categories.py | 43 +++ patch_invoice_list.py | 52 +++ patch_views_expense_edit.py | 30 ++ patch_views_sales.py | 206 +++++++++++ 16 files changed, 857 insertions(+), 27 deletions(-) create mode 100644 core/patch_views_sales_list.py create mode 100644 debug_accounting.py create mode 100644 move_project.py create mode 100644 patch_expense_categories.py create mode 100644 patch_invoice_list.py create mode 100644 patch_views_expense_edit.py create mode 100644 patch_views_sales.py diff --git a/accounting/__pycache__/signals.cpython-311.pyc b/accounting/__pycache__/signals.cpython-311.pyc index 7f8a8b79ab48d08e81e8fc17a610d5b30b88365f..9452ace47f6c6f5c6cacc5d43210ed31b979387b 100644 GIT binary patch delta 1961 zcmaJ?TWl0n7(O#Q*S&T2GQCr#yX|(j5Ys{hg{I1_fr5gmQHYUc*d1kScU#X4w=$dd z#bhBw+nj0|39%2RhQNcy#5WE4=7WjbOxVdNk;K4*iDpqr8smfK%+j(&bdve@|Nrxy z^ZoyS&YAst>u=kFp9BJ4fS<5Z%AJ%y3x3GZM?8p<`_T`~#pyRekumAL;PpdARs==^ zVb&fav}yrhvRO~dn!7{+a2>z3XT4YCOfJj3#gH-BYI0=eAhu0rQLORT;U>KF`UNO( zc{mOza12}?Ip;YB&Qd0$(^+~Hpqp$L8sxgBi@eeHXcZG=*(Dlhgx}f&1rv<t`Z^X~ zja~s+lJgqt3b^XIAueJ6`dX|gm<-;h?M2o|5XIM?tSI1)dmrr;_I&HSx_!LiU&8xz z-Q6pa32yx}rZ5Up0W`!Lu(`iRYrweLcOQ#`eaY}SN6DP-D z7h$tGL(A*1gV??^>Ub`jb|VgxGZZcFX5)C>9NpSr5;JvjViiR}-wHABv)CrLL%)R{ z2(L_6IS`R&4%(7j1p-@x5>uslQNAm;UmabHE}r_hqr9Qd+R#^)`z^V@G;BA>3)H2l z(v;oVGt0~#oeSDhTSba3OR>d)vXr!>w?%0- zGFvE%-Imy01s-?ElDi*`jhDr^CB})4JMDNyPA<#IvfOLQy(RBmf3xLRD*m2je~%q# zvZEVqv?LB>+N*$Ly)KlP+d{+=+A2b9S%}$TOln?enLliYgo+wDlxf-pZ+6@eJP z4_MLrCjp%1D)@mvG;>G~;0Avk-$n0E(BCBJ-MuW@=~d8I!7nnSPK!og}&titkWJ&@D@t ze5n3ErNL1`e}O9w2rrhQEZt{6zNjxHbfH*d>32`U&)Y s(|6KV3ZJSq7^BeJ&C%8nH9}PZ!TTOS$1JL|O1rVd=N~m6qZqR8KRqVHRR910 delta 1618 zcmaJ=TWlLu5WTzJhZFCvorm*IYsaaBp@8$|ks9S8(m(`(l!i*7(b`_635^q7ZxUK< zXpr~_&{h$*LKZFJFDN8PAozlxexV8pDUJ-WmPYUc{8Y#l2q9DfGd4-g^GbU>Gk5Ns zJ9CeJ=(^Lcy`!q71lGxgm!?K5KhWM3_*I_0D}2nG5_@0paxzPc6|KZ9!>q!xNZhf; z6S4tcVbKn=V$UW7_n9U&OKT2Dad%Avm6$g7Wb0DR`={_ZO^(&;zDuO zRq~c&Q(S{n8~Z1h1dAUa&-E{fmKc|6_l}ad{Jek;?KBS^G%!dk0rH#&dEk}&#Ve|@ zS-J)_Hu80_7Dp})Si&d?x!7;=h3e86HFVR6Pw3`(-WLdowh#xOedG*uO8U&~e8QwS z8e7(5G4yP~JU3^A1Y0y<>l{6T_X{F41B5*6cV}D3mI`HU`HY^_O_SR4q+y~#VT{Cf zBxV<>A1pRgQr{xkj@jrmZNeRK$~0z@^bpvz1p`7pAYtxSNmFSj7;gBI5q78K*`!D{Xl}Hzzn1pCxDNgP=lp|VBL$MOYCLUAK2AkN@>lX z(frGU-?++hgcIGV+Wb$}sjWykjeVjH^R(;r@-Nxfn(TQH4$~flV+axUvv&AllpDoc z*g{f2YZUGx({2p+BJ?5jV}diLC(q<>Hh{5qHsbCQhrxW5z2<(E1D;xTzvuc$5e00X zFCUqwF%Yof8%iw9PQ?sgfIb2np?_zQrxFWDVoUm5VGO|IWhcrS#i!xavuvU473A9S zJ_#-zLMUQnx5@(#9W>>f`;+{C8lz8PjmHtNCoel)aYTFu+-bH_ak0?XeU&#o|DdY# zcJv%j$q9YNm@pFm(U;43Va`Y-jeJi2l3VC^fLf`O};~!})W3755mIBZ#*hgl{^C Ng+DB-JH-je@&|F~dSd_p diff --git a/accounting/__pycache__/views.cpython-311.pyc b/accounting/__pycache__/views.cpython-311.pyc index c4192dc6b4592adbcae0102dc6a5acc4024b0be4..60fe96357c91ca2609b50ee19b7b7061c0491937 100644 GIT binary patch delta 2707 zcma)8ZERCj7`~_LwsyDezS@0sZE3f$t-x9s+t|j39}E;w#>T=&0ZZw3*_dVUla7MW4p1pm) z?t7l|p7-=~?VDE}7wz^k0iLYysB*aZxTB67I^LR^NVp=dL`|fI2$CRK)!IZ|q>jsF zYJI{TadTNz8xjqX1}@uFPr@7Va@nr>B0kw7IgSVyAoXR;&!5OV(3a29`axSORU9ER z-s=kP0Weg~ac=}|)f{aTXshRFo7rySS~?$Wt~s_A(ALb+wo3NRLa>gVAm>XEgQCeIj$sP>RLrsr64gZt5Y#m*5YzzrRhd7sbCLF-}cl)@zjmr0nkhU zem|Xu3P0OnUBSM$_(*{LVR`vUu8T+&J8EqQ=Ns0Oq?>IhyG@*|PFyRlz-%iKRxw5N zyPg5ju*(O=l5Qj~WEbMsD-;V)BF;%6=H8leS zO>{lVLjbd@HY(%$VpNZ&MxwEJJe5xB8XX3o#cW%7KM7*+0`1iNiS!E&2@{elw5$A%NWNWy>wagE;6jaZ0Nn9Fs2*x(2iq`wC39R9YWP>(LQxNxDQ3 znBokV^rA-}JM9cjh!nY_e8VlVWHO~A)zzpgN2oxkM5qEVD#oZhBJ1&eQ7Wt8Da~yV z1^gaskn+(~nkHjvRECljS)(d=yntRgVL}jI7alm=c}L?t$HF^~h4-DF$<1fy-K_j} zJli{v>mA7V4rINXa^6jO@20y$TeI7?=Y}Hrp~zh&om0m1%6Qg!Am==gcOJO!@=nIj zhHnOLS+aeDxxT@C-(c1^l=BVceM5JLw`U_ea>G0G!#nTpAJ6SSnBRXe>nc5za~;aN z4zV{}8=+Zqu6yQ^K`4!GWmnzH#ib~AA}nJM-PeiAE;cOKfGx|CX)$=9U zp?87pJux+wjLY*BW-LjypG=xg?6!FsgKCz17kJnkYVaXSPKxe6}bKg6_W^U?8k6N#c34JAe==w z$NY<3Q;bh>9o6>9vQ968`HRfP#eX5=YdfMuM0P1>yyuB){<^c3b#*Ry3U(ZwP2x|O z`D26t`?XV^cn-z$0Kw8({l?qszm{5DSn3sw`UK%B!lwXQF#z6lQ&*ihMpGk-9#vDC zMz4YGK009_jKx~t$%%Wl?RRQfw(Fc2042p=55YEexZ6*nY_j|O#H`uumPTXAy{VA2 zCzMD@vZ{qf(#g24q>>sR=!R{|A+0N_R@fQ5c?|1JU|~9$N3~Rv4&eKS5q2Zs(B{*H zA7QCnNW4 IB4*6~51G1Zxc~qF delta 2433 zcma)7Uu;uV81L!YuHEgQ{cF4R-#W_J8enS&W58fRgpCE5EL%Vd+H}2lqj1~KbMFio zOi@8hCdPS4L{tozOy_vA!N3#p<^#qJXO`T^gAY8IX)rNSA{@BGep zzH`1m=exIGzNOedv)L+iFpj)=F7-ob(q2bKCY!T4q7!wZMRulLF&CFBWOv#fb8}gc z>(ceHdM;aKPs}5kMcY~36quPoTYSX4z}i=^K44v<<1Beh+YfwYN&C5?kpR$DCA!Eq z09(C+4FX%Uf^7u0mIaBk-nn8eBw7b`VKN7B-*vx!^?V zMyLl+^Z>yi^&#PB?^@Qe7IP&tTLL7&nl1aEto|3M-{tCymWyNqJ5zC&RI&X+kFW)8 zZbj%~SA<~uHV`$NbaG6}C{jG7N@;~|N3J}X&8X5zRl&3cON08@Z-U3T4@g_yVI9)L zl|=i4mS=EJSwfa+4@lGi5Z2RPlrhL9m)J`Vw{ZZNHg>_Wiv;rb9CryBV4qa&BSY+$ zs%U`k8N`i;5DwyQG>2D!)4Qs@#us3^ksYc2#v1@BNdSt&0zw)5e_zMCYqlFiqFUHQ z&3bQnQ9-9eZrRMp-meLoubZYI>N9%wb7KR0vChG|*SK<`S+o@BqW-HybrfZt?zB#< z5QWnuMW*1)89bK{NVc_Kv+-!g3JfqT$uoNbm@Q77UjY-pl?gW;$;*wqy)U2A2 zR8r-XqSC`i)gWa`sM&NXNn`A=zsqz4`J?Qz-zSuck{)Dt{Efm1TpC75vS0n4 zF1(XQBn7Qw(Jc-BBDLCdA~OyVTOPSnGzZ%K`JI6U13APzp^(1;Ep{Ma=8HKtVESnt zi-p#J-fN-u&6v(*jy2-66dUkj44$S~cu?lTgOX9@5L*oSOy^KM%WSLbJ=iTYdm@>{ zYKzN~I4V&(0pz*-j@4EI@6_(D&yO^1)_3qMV3R3atqmofiFTAWAhZI6%L-4haDVoF zb2L>56RuS(OUnV=foaif#h?^A39s(mxXP>aJW9MZSe9A?y?|`VF!kc}EtYNx5g(gu zsWD<9Udd0l>;@n555o`3;ykoL)C(arA*=-m6S@v%?hATAxoZiO`18|i`Xtr#BBcyD z9)gP~VVw;Uh_wD2!7Y6?{V diff --git a/accounting/signals.py b/accounting/signals.py index 6d0060c..34804c6 100644 --- a/accounting/signals.py +++ b/accounting/signals.py @@ -24,22 +24,45 @@ def create_journal_entry(obj, description, items): if not items: return None + # Filter out items with 0 amount + valid_items = [] + for item in items: + try: + # Ensure amount is Decimal + amount = Decimal(str(item['amount'])) + if amount > 0: + item['amount'] = amount + valid_items.append(item) + except: + continue + + if not valid_items: + return None + + # Determine Entry Date + entry_date = timezone.now().date() + if hasattr(obj, 'date'): + entry_date = obj.date + elif hasattr(obj, 'payment_date'): + entry_date = obj.payment_date + elif hasattr(obj, 'created_at'): + entry_date = obj.created_at.date() + entry = JournalEntry.objects.create( - date=getattr(obj, 'created_at', timezone.now()).date() if hasattr(obj, 'created_at') else timezone.now().date(), + date=entry_date, description=description, content_type=content_type, object_id=obj.id, reference=f"{obj.__class__.__name__} #{obj.id}" ) - for item in items: - if item['amount'] > 0: - JournalItem.objects.create( - entry=entry, - account=item['account'], - type=item['type'], - amount=item['amount'] - ) + for item in valid_items: + JournalItem.objects.create( + entry=entry, + account=item['account'], + type=item['type'], + amount=item['amount'] + ) return entry @receiver(post_save, sender=Sale) @@ -51,15 +74,10 @@ def sale_accounting_handler(sender, instance, created, **kwargs): ar_acc = get_account('1200') sales_acc = get_account('4000') - vat_acc = get_account('2100') if not ar_acc or not sales_acc: return - # Subtotal and VAT logic (assuming total_amount includes VAT for now as per Sale model simplicity) - # Actually Sale model has total_amount and discount. - # Let's assume total_amount is the final amount. - items = [ {'account': ar_acc, 'type': 'debit', 'amount': instance.total_amount}, {'account': sales_acc, 'type': 'credit', 'amount': instance.total_amount}, diff --git a/accounting/views.py b/accounting/views.py index 43f269a..6a563f6 100644 --- a/accounting/views.py +++ b/accounting/views.py @@ -4,7 +4,8 @@ from django.contrib.auth.decorators import login_required from django.contrib import messages from .models import Account, JournalEntry, JournalItem from .forms import AccountForm, JournalEntryForm -from django.db.models import Sum, Q +from django.db.models import Sum, Q, Value, DecimalField +from django.db.models.functions import Coalesce from django.utils import timezone from datetime import datetime from django.db import transaction @@ -70,8 +71,8 @@ def account_create_update(request, pk=None): @login_required def journal_entries(request): entries = JournalEntry.objects.annotate( - total_debit=Sum('items__amount', filter=Q(items__type='debit')), - total_credit=Sum('items__amount', filter=Q(items__type='credit')) + total_debit=Coalesce(Sum('items__amount', filter=Q(items__type='debit')), Value(0), output_field=DecimalField()), + total_credit=Coalesce(Sum('items__amount', filter=Q(items__type='credit')), Value(0), output_field=DecimalField()) ).prefetch_related('items__account').order_by('-date', '-id') return render(request, 'accounting/journal_entries.html', {'entries': entries}) diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index aa96f4c2859b24a2da052bda6224b5a920fd6721..366bd668829734226a7d96a06ff0e61d4cc98a76 100644 GIT binary patch delta 2165 zcmZ{lTTE0(7=YQ`pK?G!6j65%cibC`TwE`Nt#!F9vMhI578jI7Sr9~^B9&|L!5G_E zm5gcjp=tM_shiX^G&E`Z(r%hQ^`V`U%!4s~skMD;I(=!PX_L;* QEXS09ioB7T^ z|IVB_`_08~CvqNSXJ@5}>wf92iE7(=&H+Q>Q~SGF1{3)z%W8nlr&-Ge0g;_4xVf_g zNUkB>`6OzZoJ-x7x<-F~nDLP*E$w6R#cSOVzB>B!bei**%};X885#Sd3$xMbIg-rV zn z4B|WYj9jpK3q!_T#z&xBZ@#tXR`B*<>>35T26hH^ZVHj4RW*djkJdsFpIeJXwAcz? zN!`PDl49HbjBMG?Nwv@KlI`5I^h}Y>axZxQY{E>zt$~|?o9~vRd*mq1v?bD+!%I&M zQW(-;h`|uglfvHr7xF`d*-F-=b^vEl??u@@=SCI$QW}JiT3BR|B;N(9A%=Xz?q6Con_^6(l*1^tmfEJrdDkXQ}AivW8mWf;oXq- z0~x=Q@i!TBD^gk9GWN^3DC2uFel6n@8Pi=U(;;I(#%Wio;=8W0fUICaub?dMV+TFS zDhfRs^f2h*iw0>d>Bhl$l-b16_EQLH5MmJG%c!i7$1YV_MR=3^?NT$>5WGfGT50KQ zFv?Vo#;u=Q9uG3Bc$Usl7}j8z!7y(^rBzpomZS12E)F8+p$X8KmB+o@L zO(wZT&7`2(qWBP6$ZG;lA+(Z6wVLTiXfu(wD=n1)q;}KRz81uytjJC40~FddXlKyQ zJ5cF0kyVE|^E6VQiF|O_tOSwzNp_8@gb)TuO^v#C1|h5`w#HYO-ACKS0f}f3VG!Yi zs6=$7D4zad&PJGmR|78tFCRhSye>Er)`!)}9WP3|fWoLQR3vj@-Vj71K`Q2UG yC)Z)}rJJia#6GrY(88dFU&5QFbVII`c--441T+XR2=L2NxxORyxpG7NY5xPiM4mkW delta 1993 zcmZ{lZERCj7{}@QAa`YN0+aP#wv|EO_R7|RV*_mm-PU&PUdp+{MWwM~O;1Osyk1lOn8hm|p`7#AJ12+e^uqKJaWmuBrkgHN^%2h3O-c|Wj{v1(6 z7F|2H6szNs{PBrWbzHV-bD>Pr?+m_mJZGoiW8mZ96Hlw^HdUpCuH0ts3DB-l3TXyu z4r#Gn8Fu`?p*%_aZpWgsRp2Pbohkl|chyFrok2T?c41d#{hvwja(8ZqLMww-4z0pa z0DP7nIsH;sgV%F~9IQ9As}?n#d6JfJ4Pk*S z)*Fl05pI|h-L*W$t6J#6Fog(%2#1JRL`$Gx{aW^{405O3C}R{d3^E)tB3PjEJ55XV zN;%(sfF6!h=w;B$p;vUFrQ5Vrt!(BFKe0CyYhf(KNn?Yd^&<3<{stq~k1$}Kq;lJ6 zFiByE!4QWbkwD9^Y5DI;kDwz(9?1Angf#g}!f}K#1sQ}%@=c?zO(C2lzc%WH(+H;u z$Pcx;HiMKUuQeLlEW&AWPr@05b1LCH!UdIZ5#bUhTt>R060Rc5laVIb8iW8@XwtQy zRV``Ovy$&U_QHOo0V~<YW8{r}M&Ter(n@YO>Wf22!)CH`Wr4dx zv`6mraR%cY#>EI)#!QRs{0AkgDGC7w0S*C?LBqId@Z_BLnjUWbcWx7CIAI!Ub0J=T z_+g9!Df4OC3s{~c>1NqEgeI((MG9D_LybK)#o z=1hw_SI)hiGL%WP{g)~UGiZD<5T8#KLgatFno7>Op!n7|- zVV1!xhgoqQEgi72iybsPPGN$<1cwQ61k@HdN*>7k5JH4JZqc<4xM5elBCzayG5-_*7Q{_U*y$tKSwA;8ALfm#jJ|Dxi$YicT2uq{|3v; BV_yIO diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 081938d0ef7b6dbeedef5c29be15cb90a475ac9a..8244ec091df496e7bcae5a22fbea3fd0fe78b622 100644 GIT binary patch delta 14139 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 bF~J}ft)!PtQvewfCV*0945JVKP)h$l&ZEvB delta 4184 zcmaJ^3vg8B72f|Q_a?cU$A&ytNLcb@lSx8|qDUhm3W=3eB^Z^EWy#(om+Z@bH^^g@ zM*`uQ7!L%fh@#eaHM-zqtb?@EL9pnCrW#gJ@KLk{B~rCm9ed8thRG(=?#%u6{O3FW z`Op8j=RbGoOjPGZDeCUd-0cuHjz5NoVsSHWUqk>ZG%Z?JX- zo-`I$GbnRnI!eHpi)GI>qVMCi!_xZUQ?VR}dnhmhLKEL{;$-FE7(Q*Bz zya7|*n(C9rqwb=zQ1BBpjbetH^VAv0JWSTpagez&Wh72vu(l%w*0f~GmDqj+9&Tx7 z6F}Ar- z7%|W4j7h$Ta2&S}8y(&B-^fCeUJ_+~4V&S*rUnr@~>%iD-KD$%1*}XciMT?j4 zxd2J0s^FL2w21YnTbHENAsb-eT-PFnSLv&Dsmk4`J-{HU-DVsy9JtyGlzOCIWw5(_ zaRe3JQJ~t8dL19W2&b0@-nU9BX|~Zt;^nc@p|~?n z^`h?2q;DEyf(IbQ*XH#)b&Q9j&0(?|J80&{6xEN^5u#f$ra7=`qiJ{nAKjRs+>6XR z3<4WF)DLuu`YX{i zDlKs-c(AJ)>(T54 z1M^lphPG;JmePre4;k#;x^UzPGA9f>w%M23m-9_%brK(bXE=mQ2Y_!YRkot~69!Li zt21sPi}0qzcI7Kw*gnmmqAQZx(N(DQAoUr8KXye73s-lW6-p#uFu0?;o^28N>R5NO zvICW87<}Fx>1xgcx#}*YzQTv!F1CGS5QiSfSAUDtKZ))}I*nemh~s@jPm!_*>2nNt z&*G750B3tLl)XrO!yxX#NUHt8E7W~R4UyyFm!ed}YaXZ4+BvjkcyaK@(cl7ubcl?5 zIbt3cUfsTkJumV`>5h2yMO6L=AN~YQx#R^B zFReRD5s5=}dOaA6->y43<%^#g?B7vm9MJ>Pd#%cTq<&#=Q*T}Q2KItMtAV{q zlS@tT`R&|q^JzYx?s4Y`4fB;WHQ9ZILMfB*1sO zi(#=AhvO8%{o9ooZMj!-`!txZ65PV4> zvd%w95((xUNfv@{2t;o9mZX0Xd`Iv-!4CxgCb)X2 zpBNM$%~1>(msJYPKYCY`BB8ucf|rl=D$yt|lAz|@hKOYL>btqBf|Ly({s>JlaPi%b zW7$f$XL*L%D)A7W1^y92$k-UZo^84V_AbAB0sn%6NgOU@-EFQV8eeIu@mO@f=JHu| zw}rNpAEw?)o7-#G9ae?A$fH~6@ZI&d`M64h(<{YI65Kpk9D4yBTV*~T%2pI6MpM5~ zj2B;X-R^{KgJx|!Ek&{z-D_Xr(%gQVOY^sQ9HIE(HWfc&6ydlwJd;q2IAj<{KL3E? z$R+>^Ms|Aa4qqsGiN~>mccBKdSLUZrCVm}3KEVlsLD}=&u5e6I0^1Qk+V7!jnUY7EX^>EKulPqq-M?OD82u0oFU%^Fcpbr^dN4i+2?$&G=0?(u#AEVBfiC}^3P^poQs~sQTP&DjW zRTNkox|!lm^v@OlQ0$PPNl_&5-nT~Za9^$6eE2^g2zrpUT+glCo3gL zy(2-z=@Aqr_ZLnlrIsOej2w%akUmVTM*{t)^O%HRP(OTHF0Ia>Ej1Hl5lkY;C7|yP zPM;XOn4pB9oPfR(IDHoIYY1i#R1(Z2m_4az9B_+HorC_$q??2&gRMYYC{p;#59yDw#w@#Hse- zT?9P@RJm}fR95p{BvR?Yskq=&M(}+ER1k2w`Z-@&IP9vz-h1fZ*Ysn2dMZO`7!oaE{~(lsa?l^jVyXI_?E^S4CkX5V}k~3 z&=9bm(PVQJ)7g0g;ZT%8GIeC1mxM|UJaP7EW*Rh92MyIkC3K9(2HgL3W}yA++oK8w zS<;}PDrl$*_rr!zRR#@}msOQuIiHzVA2ifoR@Dou+MuEKvZ@wUaNoDn3kD6>1`XF< Q)?X`3$EX75zKv%81^onxn*aa+ diff --git a/core/patch_views_sales_list.py b/core/patch_views_sales_list.py new file mode 100644 index 0000000..7c06679 --- /dev/null +++ b/core/patch_views_sales_list.py @@ -0,0 +1,31 @@ +@login_required +def invoice_list(request): + sales = Sale.objects.all().order_by('-created_at') + + # Filter by date range + start_date = request.GET.get('start_date') + end_date = request.GET.get('end_date') + if start_date: + sales = sales.filter(created_at__date__gte=start_date) + if end_date: + sales = sales.filter(created_at__date__lte=end_date) + + # Filter by customer + customer_id = request.GET.get('customer') + if customer_id: + sales = sales.filter(customer_id=customer_id) + + # Filter by status + status = request.GET.get('status') + if status: + sales = sales.filter(status=status) + + paginator = Paginator(sales, 25) + + context = { + 'sales': paginator.get_page(request.GET.get('page')), + 'customers': Customer.objects.all(), + 'payment_methods': PaymentMethod.objects.filter(is_active=True), + 'site_settings': SystemSetting.objects.first(), + } + return render(request, 'core/invoices.html', context) \ No newline at end of file diff --git a/core/templates/core/expenses.html b/core/templates/core/expenses.html index 21558ca..37a448d 100644 --- a/core/templates/core/expenses.html +++ b/core/templates/core/expenses.html @@ -117,12 +117,82 @@ {% endif %} + + + + -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/core/urls.py b/core/urls.py index a2bade7..045ad69 100644 --- a/core/urls.py +++ b/core/urls.py @@ -62,6 +62,7 @@ urlpatterns = [ # Expenses path('expenses/', views.expenses_view, name='expenses'), path('expenses/create/', views.expense_create_view, name='expense_create'), + path('expenses/edit//', views.expense_edit_view, name='expense_edit'), path('expenses/delete//', views.expense_delete_view, name='expense_delete'), path('expenses/categories/', views.expense_categories_view, name='expense_categories'), path('expenses/categories/delete//', views.expense_category_delete_view, name='expense_category_delete'), diff --git a/core/views.py b/core/views.py index c121f76..253ed2a 100644 --- a/core/views.py +++ b/core/views.py @@ -364,8 +364,34 @@ def cashflow_report(request): @login_required def invoice_list(request): sales = Sale.objects.all().order_by('-created_at') + + # Filter by date range + start_date = request.GET.get('start_date') + end_date = request.GET.get('end_date') + if start_date: + sales = sales.filter(created_at__date__gte=start_date) + if end_date: + sales = sales.filter(created_at__date__lte=end_date) + + # Filter by customer + customer_id = request.GET.get('customer') + if customer_id: + sales = sales.filter(customer_id=customer_id) + + # Filter by status + status = request.GET.get('status') + if status: + sales = sales.filter(status=status) + paginator = Paginator(sales, 25) - return render(request, 'core/invoices.html', {'sales': paginator.get_page(request.GET.get('page'))}) + + context = { + 'sales': paginator.get_page(request.GET.get('page')), + 'customers': Customer.objects.all(), + 'payment_methods': PaymentMethod.objects.filter(is_active=True), + 'site_settings': SystemSetting.objects.first(), + } + return render(request, 'core/invoices.html', context) @login_required def invoice_detail(request, pk): @@ -431,7 +457,129 @@ def create_purchase_return_api(request): return JsonResponse({'success': False}) @login_required def export_expenses_excel(request): return redirect('expenses') @csrf_exempt -def update_sale_api(request, pk): return JsonResponse({'success': False}) +def update_sale_api(request, pk): + if request.method != 'POST': + return JsonResponse({'success': False, 'error': 'Invalid request method'}) + + try: + sale = Sale.objects.get(pk=pk) + data = json.loads(request.body) + + customer_id = data.get('customer_id') + items = data.get('items', []) + discount = decimal.Decimal(str(data.get('discount', 0))) + paid_amount = decimal.Decimal(str(data.get('paid_amount', 0))) + payment_type = data.get('payment_type', 'cash') + payment_method_id = data.get('payment_method_id') + due_date = data.get('due_date') + notes = data.get('notes', '') + invoice_number = data.get('invoice_number') + + if not items: + return JsonResponse({'success': False, 'error': 'No items in sale'}) + + with transaction.atomic(): + # 1. Revert Stock + for item in sale.items.all(): + product = item.product + product.stock_quantity += item.quantity + product.save() + + # 2. Delete existing items + sale.items.all().delete() + + # 3. Update Sale Details + if customer_id: + sale.customer_id = customer_id + else: + sale.customer = None + + sale.discount = discount + sale.notes = notes + if invoice_number: + sale.invoice_number = invoice_number + + if due_date: + sale.due_date = due_date + else: + sale.due_date = None + + # 4. Create New Items and Deduct Stock + subtotal = decimal.Decimal(0) + + for item_data in items: + product = Product.objects.get(pk=item_data['id']) + quantity = decimal.Decimal(str(item_data['quantity'])) + price = decimal.Decimal(str(item_data['price'])) + + # Deduct stock + product.stock_quantity -= quantity + product.save() + + line_total = price * quantity + subtotal += line_total + + SaleItem.objects.create( + sale=sale, + product=product, + quantity=qty, + unit_price=price, + line_total=line_total + ) + + sale.subtotal = subtotal + sale.total_amount = subtotal - discount + + # 5. Handle Payments + if payment_type == 'credit': + sale.status = 'unpaid' + sale.paid_amount = 0 + sale.balance_due = sale.total_amount + sale.payments.all().delete() + + elif payment_type == 'cash': + sale.status = 'paid' + sale.paid_amount = sale.total_amount + sale.balance_due = 0 + + sale.payments.all().delete() + SalePayment.objects.create( + sale=sale, + amount=sale.total_amount, + payment_method_id=payment_method_id if payment_method_id else None, + payment_date=timezone.now().date(), + notes='Full Payment (Edit)' + ) + + elif payment_type == 'partial': + sale.paid_amount = paid_amount + sale.balance_due = sale.total_amount - paid_amount + if sale.balance_due <= 0: + sale.status = 'paid' + sale.balance_due = 0 + else: + sale.status = 'partial' + + sale.payments.all().delete() + SalePayment.objects.create( + sale=sale, + amount=paid_amount, + payment_method_id=payment_method_id if payment_method_id else None, + payment_date=timezone.now().date(), + notes='Partial Payment (Edit)' + ) + + sale.save() + + return JsonResponse({'success': True, 'sale_id': sale.id}) + + except Sale.DoesNotExist: + return JsonResponse({'success': False, 'error': 'Sale not found'}) + except Product.DoesNotExist: + return JsonResponse({'success': False, 'error': 'Product not found'}) + except Exception as e: + return JsonResponse({'success': False, 'error': str(e)}) + @csrf_exempt def hold_sale_api(request): return JsonResponse({'success': False}) @csrf_exempt @@ -524,16 +672,132 @@ def start_session(request): return redirect('cashier_session_list') def close_session(request): return redirect('cashier_session_list') @login_required def session_detail(request, pk): return redirect('cashier_session_list') + @login_required -def expenses_view(request): return render(request, 'core/expenses.html') +def expenses_view(request): + expenses = Expense.objects.all().select_related('category', 'payment_method', 'created_by').order_by('-date') + categories = ExpenseCategory.objects.all() + payment_methods = PaymentMethod.objects.filter(is_active=True) + + paginator = Paginator(expenses, 25) + page_number = request.GET.get('page') + page_obj = paginator.get_page(page_number) + + context = { + 'expenses': page_obj, + 'categories': categories, + 'payment_methods': payment_methods, + } + return render(request, 'core/expenses.html', context) + @login_required -def expense_create_view(request): return redirect('expenses') +def expense_create_view(request): + if request.method == 'POST': + try: + category_id = request.POST.get('category') + amount = request.POST.get('amount') + date = request.POST.get('date') + description = request.POST.get('description') + payment_method_id = request.POST.get('payment_method') + + category = get_object_or_404(ExpenseCategory, pk=category_id) + payment_method = get_object_or_404(PaymentMethod, pk=payment_method_id) if payment_method_id else None + + expense = Expense.objects.create( + category=category, + amount=amount, + date=date or timezone.now().date(), + description=description, + payment_method=payment_method, + created_by=request.user + ) + + if 'attachment' in request.FILES: + expense.attachment = request.FILES['attachment'] + expense.save() + + messages.success(request, _('Expense added successfully.')) + except Exception as e: + messages.error(request, _('Error adding expense: ') + str(e)) + + return redirect('expenses') + @login_required -def expense_delete_view(request, pk): return redirect('expenses') +def expense_edit_view(request, pk): + expense = get_object_or_404(Expense, pk=pk) + if request.method == 'POST': + try: + category_id = request.POST.get('category') + amount = request.POST.get('amount') + date = request.POST.get('date') + description = request.POST.get('description') + payment_method_id = request.POST.get('payment_method') + + category = get_object_or_404(ExpenseCategory, pk=category_id) + payment_method = get_object_or_404(PaymentMethod, pk=payment_method_id) if payment_method_id else None + + expense.category = category + expense.amount = amount + expense.date = date or expense.date + expense.description = description + expense.payment_method = payment_method + + if 'attachment' in request.FILES: + expense.attachment = request.FILES['attachment'] + + expense.save() + messages.success(request, _('Expense updated successfully.')) + except Exception as e: + messages.error(request, _('Error updating expense: ') + str(e)) + + return redirect('expenses') + @login_required -def expense_categories_view(request): return render(request, 'core/expense_categories.html') +def expense_delete_view(request, pk): + expense = get_object_or_404(Expense, pk=pk) + expense.delete() + messages.success(request, _('Expense deleted successfully.')) + return redirect('expenses') + @login_required -def expense_category_delete_view(request, pk): return redirect('expense_categories') +def expense_categories_view(request): + if request.method == 'POST': + category_id = request.POST.get('category_id') + name_en = request.POST.get('name_en') + name_ar = request.POST.get('name_ar') + description = request.POST.get('description') + + if category_id: + # Update existing category + category = get_object_or_404(ExpenseCategory, pk=category_id) + category.name_en = name_en + category.name_ar = name_ar + category.description = description + category.save() + messages.success(request, _('Expense category updated successfully.')) + else: + # Create new category + ExpenseCategory.objects.create( + name_en=name_en, + name_ar=name_ar, + description=description + ) + messages.success(request, _('Expense category added successfully.')) + return redirect('expense_categories') + + categories = ExpenseCategory.objects.all().order_by('-id') + return render(request, 'core/expense_categories.html', {'categories': categories}) + +@login_required +def expense_category_delete_view(request, pk): + category = get_object_or_404(ExpenseCategory, pk=pk) + if category.expenses.exists(): + messages.error(request, _('Cannot delete category because it has related expenses.')) + else: + category.delete() + messages.success(request, _('Expense category deleted successfully.')) + return redirect('expense_categories') + @login_required def expense_report(request): return render(request, 'core/expense_report.html') @login_required @@ -541,9 +805,59 @@ def customer_payments(request): return redirect('invoices') @login_required def customer_payment_receipt(request, pk): return redirect('invoices') @login_required -def sale_receipt(request, pk): return redirect('invoices') +def sale_receipt(request, pk): + sale = get_object_or_404(Sale, pk=pk) + settings = SystemSetting.objects.first() + return render(request, 'core/sale_receipt.html', { + 'sale': sale, + 'settings': settings + }) + @login_required -def edit_invoice(request, pk): return redirect('invoices') +def edit_invoice(request, pk): + sale = get_object_or_404(Sale, pk=pk) + customers = Customer.objects.all() + products = Product.objects.filter(is_active=True).select_related('category') + payment_methods = PaymentMethod.objects.filter(is_active=True) + site_settings = SystemSetting.objects.first() + + decimal_places = 2 + if site_settings: + decimal_places = site_settings.decimal_places + + # Serialize items for Vue + cart_items = [] + for item in sale.items.all().select_related('product'): + cart_items.append({ + 'id': item.product.id, + 'name_en': item.product.name_en, + 'name_ar': item.product.name_ar, + 'sku': item.product.sku, + 'price': float(item.unit_price), + 'quantity': float(item.quantity), + 'stock': float(item.product.stock_quantity) + }) + + cart_json = json.dumps(cart_items) + + # Get first payment method if exists + payment_method_id = "" + first_payment = sale.payments.first() + if first_payment and first_payment.payment_method: + payment_method_id = first_payment.payment_method.id + + context = { + 'sale': sale, + 'customers': customers, + 'products': products, + 'payment_methods': payment_methods, + 'site_settings': site_settings, + 'decimal_places': decimal_places, + 'cart_json': cart_json, + 'payment_method_id': payment_method_id + } + return render(request, 'core/invoice_edit.html', context) + @login_required def add_sale_payment(request, pk): return redirect('invoices') @login_required diff --git a/debug_accounting.py b/debug_accounting.py new file mode 100644 index 0000000..1fa0972 --- /dev/null +++ b/debug_accounting.py @@ -0,0 +1,31 @@ +import os +import django +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') +django.setup() + +from accounting.models import Account, JournalEntry, JournalItem +from core.models import Expense + +print("Checking Accounts...") +acc_1000 = Account.objects.filter(code='1000').first() +acc_5400 = Account.objects.filter(code='5400').first() + +print(f"Account 1000 (Cash): {acc_1000}") +print(f"Account 5400 (General Expense): {acc_5400}") + +print("\nChecking Journal Entries for Expenses...") +expenses = Expense.objects.all() +for exp in expenses: + print(f"Expense {exp.id}: {exp.description} - Amount: {exp.amount}") + # Find linked entry + from django.contrib.contenttypes.models import ContentType + ct = ContentType.objects.get_for_model(Expense) + entries = JournalEntry.objects.filter(content_type=ct, object_id=exp.id) + for entry in entries: + print(f" -> JournalEntry {entry.id}: {entry.description}") + items = entry.items.all() + if items.exists(): + for item in items: + print(f" -> Item: {item.account.code} {item.type} {item.amount}") + else: + print(f" -> NO ITEMS FOUND!") \ No newline at end of file diff --git a/move_project.py b/move_project.py new file mode 100644 index 0000000..50a99f4 --- /dev/null +++ b/move_project.py @@ -0,0 +1,33 @@ +import os +import shutil + +DEST = 'meezan' +EXCLUDE = {'.git', '.gemini', DEST, 'move_project.py'} + +def move_project(): + # Ensure destination exists + if not os.path.exists(DEST): + os.makedirs(DEST) + print(f"Created directory: {DEST}") + + # Iterate and move + for item in os.listdir('.'): + if item in EXCLUDE: + continue + + # specific check for .env to avoid errors if it doesn't exist yet but user mentioned it + if item == '.env': + pass # allow moving .env if it exists + + src = item + dst = os.path.join(DEST, item) + + try: + print(f"Moving {src} -> {dst}...") + shutil.move(src, dst) + except Exception as e: + print(f"Error moving {src}: {e}") + +if __name__ == "__main__": + move_project() + print("Move complete.") diff --git a/patch_expense_categories.py b/patch_expense_categories.py new file mode 100644 index 0000000..dc4c7a3 --- /dev/null +++ b/patch_expense_categories.py @@ -0,0 +1,43 @@ +import os + +file_path = 'core/views.py' +search_text = "@login_required\ndef expense_categories_view(request): return render(request, 'core/expense_categories.html')" +replace_text = """@login_required +def expense_categories_view(request): + if request.method == 'POST': + category_id = request.POST.get('category_id') + name_en = request.POST.get('name_en') + name_ar = request.POST.get('name_ar') + description = request.POST.get('description') + + if category_id: + # Update existing category + category = get_object_or_404(ExpenseCategory, pk=category_id) + category.name_en = name_en + category.name_ar = name_ar + category.description = description + category.save() + messages.success(request, _('Expense category updated successfully.')) + else: + # Create new category + ExpenseCategory.objects.create( + name_en=name_en, + name_ar=name_ar, + description=description + ) + messages.success(request, _('Expense category added successfully.')) + return redirect('expense_categories') + + categories = ExpenseCategory.objects.all().order_by('-id') + return render(request, 'core/expense_categories.html', {'categories': categories})""" + +with open(file_path, 'r') as f: + content = f.read() + +if search_text in content: + new_content = content.replace(search_text, replace_text) + with open(file_path, 'w') as f: + f.write(new_content) + print("Successfully patched expense_categories_view") +else: + print("Could not find the target function to replace") diff --git a/patch_invoice_list.py b/patch_invoice_list.py new file mode 100644 index 0000000..d4acaaa --- /dev/null +++ b/patch_invoice_list.py @@ -0,0 +1,52 @@ +import os + +file_path = 'core/views.py' + +old_content = """@login_required +def invoice_list(request): + sales = Sale.objects.all().order_by('-created_at') + paginator = Paginator(sales, 25) + return render(request, 'core/invoices.html', {'sales': paginator.get_page(request.GET.get('page'))})""" + +new_content = """@login_required +def invoice_list(request): + sales = Sale.objects.all().order_by('-created_at') + + # Filter by date range + start_date = request.GET.get('start_date') + end_date = request.GET.get('end_date') + if start_date: + sales = sales.filter(created_at__date__gte=start_date) + if end_date: + sales = sales.filter(created_at__date__lte=end_date) + + # Filter by customer + customer_id = request.GET.get('customer') + if customer_id: + sales = sales.filter(customer_id=customer_id) + + # Filter by status + status = request.GET.get('status') + if status: + sales = sales.filter(status=status) + + paginator = Paginator(sales, 25) + + context = { + 'sales': paginator.get_page(request.GET.get('page')), + 'customers': Customer.objects.all(), + 'payment_methods': PaymentMethod.objects.filter(is_active=True), + 'site_settings': SystemSetting.objects.first(), + } + return render(request, 'core/invoices.html', context)""" + +with open(file_path, 'r') as f: + content = f.read() + +if old_content in content: + content = content.replace(old_content, new_content) + with open(file_path, 'w') as f: + f.write(content) + print("Successfully patched invoice_list") +else: + print("Could not find exact match for invoice_list function") diff --git a/patch_views_expense_edit.py b/patch_views_expense_edit.py new file mode 100644 index 0000000..fd824fd --- /dev/null +++ b/patch_views_expense_edit.py @@ -0,0 +1,30 @@ + +@login_required +def expense_edit_view(request, pk): + expense = get_object_or_404(Expense, pk=pk) + if request.method == 'POST': + try: + category_id = request.POST.get('category') + amount = request.POST.get('amount') + date = request.POST.get('date') + description = request.POST.get('description') + payment_method_id = request.POST.get('payment_method') + + category = get_object_or_404(ExpenseCategory, pk=category_id) + payment_method = get_object_or_404(PaymentMethod, pk=payment_method_id) if payment_method_id else None + + expense.category = category + expense.amount = amount + expense.date = date or expense.date + expense.description = description + expense.payment_method = payment_method + + if 'attachment' in request.FILES: + expense.attachment = request.FILES['attachment'] + + expense.save() + messages.success(request, _('Expense updated successfully.')) + except Exception as e: + messages.error(request, _('Error updating expense: ') + str(e)) + + return redirect('expenses') diff --git a/patch_views_sales.py b/patch_views_sales.py new file mode 100644 index 0000000..62ea21c --- /dev/null +++ b/patch_views_sales.py @@ -0,0 +1,206 @@ +import os + +file_path = 'core/views.py' + +# New Implementations +edit_invoice_code = """ +@login_required +def edit_invoice(request, pk): + sale = get_object_or_404(Sale, pk=pk) + customers = Customer.objects.all() + products = Product.objects.filter(is_active=True).select_related('category') + payment_methods = PaymentMethod.objects.filter(is_active=True) + site_settings = SystemSetting.objects.first() + + decimal_places = 2 + if site_settings: + decimal_places = site_settings.decimal_places + + # Serialize items for Vue + cart_items = [] + for item in sale.items.all().select_related('product'): + cart_items.append({ + 'id': item.product.id, + 'name_en': item.product.name_en, + 'name_ar': item.product.name_ar, + 'sku': item.product.sku, + 'price': float(item.unit_price), + 'quantity': float(item.quantity), + 'stock': float(item.product.stock_quantity) + }) + + cart_json = json.dumps(cart_items) + + # Get first payment method if exists + payment_method_id = "" + first_payment = sale.payments.first() + if first_payment and first_payment.payment_method: + payment_method_id = first_payment.payment_method.id + + context = { + 'sale': sale, + 'customers': customers, + 'products': products, + 'payment_methods': payment_methods, + 'site_settings': site_settings, + 'decimal_places': decimal_places, + 'cart_json': cart_json, + 'payment_method_id': payment_method_id + } + return render(request, 'core/invoice_edit.html', context) +""" + +sale_receipt_code = """ +@login_required +def sale_receipt(request, pk): + sale = get_object_or_404(Sale, pk=pk) + settings = SystemSetting.objects.first() + return render(request, 'core/sale_receipt.html', { + 'sale': sale, + 'settings': settings + }) +""" + +update_sale_api_code = """ +@csrf_exempt +def update_sale_api(request, pk): + if request.method != 'POST': + return JsonResponse({'success': False, 'error': 'Invalid request method'}) + + try: + sale = Sale.objects.get(pk=pk) + data = json.loads(request.body) + + customer_id = data.get('customer_id') + items = data.get('items', []) + discount = decimal.Decimal(str(data.get('discount', 0))) + paid_amount = decimal.Decimal(str(data.get('paid_amount', 0))) + payment_type = data.get('payment_type', 'cash') + payment_method_id = data.get('payment_method_id') + due_date = data.get('due_date') + notes = data.get('notes', '') + invoice_number = data.get('invoice_number') + + if not items: + return JsonResponse({'success': False, 'error': 'No items in sale'}) + + with transaction.atomic(): + # 1. Revert Stock + for item in sale.items.all(): + product = item.product + product.stock_quantity += item.quantity + product.save() + + # 2. Delete existing items + sale.items.all().delete() + + # 3. Update Sale Details + if customer_id: + sale.customer_id = customer_id + else: + sale.customer = None + + sale.discount = discount + sale.notes = notes + if invoice_number: + sale.invoice_number = invoice_number + + if due_date: + sale.due_date = due_date + else: + sale.due_date = None + + # 4. Create New Items and Deduct Stock + subtotal = decimal.Decimal(0) + + for item_data in items: + product = Product.objects.get(pk=item_data['id']) + quantity = decimal.Decimal(str(item_data['quantity'])) + price = decimal.Decimal(str(item_data['price'])) + + # Deduct stock + product.stock_quantity -= quantity + product.save() + + line_total = price * quantity + subtotal += line_total + + SaleItem.objects.create( + sale=sale, + product=product, + quantity=quantity, + unit_price=price, + line_total=line_total + ) + + sale.subtotal = subtotal + sale.total_amount = subtotal - discount + + # 5. Handle Payments + if payment_type == 'credit': + sale.status = 'unpaid' + sale.paid_amount = 0 + sale.balance_due = sale.total_amount + sale.payments.all().delete() + + elif payment_type == 'cash': + sale.status = 'paid' + sale.paid_amount = sale.total_amount + sale.balance_due = 0 + + sale.payments.all().delete() + SalePayment.objects.create( + sale=sale, + amount=sale.total_amount, + payment_method_id=payment_method_id if payment_method_id else None, + payment_date=timezone.now().date(), + notes='Full Payment (Edit)' + ) + + elif payment_type == 'partial': + sale.paid_amount = paid_amount + sale.balance_due = sale.total_amount - paid_amount + if sale.balance_due <= 0: + sale.status = 'paid' + sale.balance_due = 0 + else: + sale.status = 'partial' + + sale.payments.all().delete() + SalePayment.objects.create( + sale=sale, + amount=paid_amount, + payment_method_id=payment_method_id if payment_method_id else None, + payment_date=timezone.now().date(), + notes='Partial Payment (Edit)' + ) + + sale.save() + + return JsonResponse({'success': True, 'sale_id': sale.id}) + + except Sale.DoesNotExist: + return JsonResponse({'success': False, 'error': 'Sale not found'}) + except Product.DoesNotExist: + return JsonResponse({'success': False, 'error': 'Product not found'}) + except Exception as e: + return JsonResponse({'success': False, 'error': str(e)}) +""" + +with open(file_path, 'r') as f: + content = f.read() + +# Replace stubs +content = content.replace("def sale_receipt(request, pk): return redirect('invoices')", sale_receipt_code) +content = content.replace("def edit_invoice(request, pk): return redirect('invoices')", edit_invoice_code) +content = content.replace("@csrf_exempt\ndef update_sale_api(request, pk): return JsonResponse({'success': False})", update_sale_api_code) + +# Handle potential whitespace variations if single-line replace fails +if "def edit_invoice(request, pk): return redirect('invoices')" in content: # Check if it persisted + pass # worked +else: + # Fallback for manual check if needed (it should work given exact match from read_file) + pass + +with open(file_path, 'w') as f: + f.write(content)