Upload folder using huggingface_hub
Browse files- Proforma_Invoice_Customer_20250713.pdf +85 -0
- app.py +53 -52
Proforma_Invoice_Customer_20250713.pdf
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
%PDF-1.3
|
| 2 |
+
%���� ReportLab Generated PDF document http://www.reportlab.com
|
| 3 |
+
1 0 obj
|
| 4 |
+
<<
|
| 5 |
+
/F1 2 0 R /F2 4 0 R
|
| 6 |
+
>>
|
| 7 |
+
endobj
|
| 8 |
+
2 0 obj
|
| 9 |
+
<<
|
| 10 |
+
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
| 11 |
+
>>
|
| 12 |
+
endobj
|
| 13 |
+
3 0 obj
|
| 14 |
+
<<
|
| 15 |
+
/BitsPerComponent 8 /ColorSpace /DeviceRGB /Filter [ /ASCII85Decode /FlateDecode ] /Height 241 /Length 12453 /Subtype /Image
|
| 16 |
+
/Type /XObject /Width 442
|
| 17 |
+
>>
|
| 18 |
+
stream
|
| 19 |
+
Gb"0WLKbJE_?TKo8mr*8NM=FajP")3]d0K30C@oY^p9UU9u2E*$?jB0$eC+C@kT7B4'Q<Dn$@CSAcX=gbZRtKiqn)Jj'rMAS14@'R@,ko1cGL:zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz!!!#sUU],teNbhP%0O<.cp8r<&";A[btT@>8o1d'Il<(7]e;J\#s27H-+B,9YgXHJ\:3(X.f@#d]Sg[AfOEH$\F;ab*;\aeji\;ma7XKk5;mT3L(1t,n"2B!2Z`Ue;`'?a2I9TI6#>4Drg`'$2"\BK82tEHneG(gZgbg<ba'uL$VE&0l%dN]'%$X&jGg%hOFm"=jU0<%*9`8IT:A&c<go3*s(10eq+/])'"0OKm@W^012W'(_C/s[CoQa:O^8jLq*!Hk.9_F)r5)%1DG>3-Y%Wh@?/*f*\o6Dn/q-003ANtT9[;f<k__a"S3)'m1,Pj-bMksKOHrIlo@co/^XMB8!D$(n,h<@6]e5g[i%nS,X0X+:$^Z0H?1!DaVr^uE0]f-fh[iMj<*qfsBfXfh/YN=a[+CQ[Q#p_3Eklp-Fg'!^]W>&pA8#YB9H'YL[L!!%j*oX&>"Hht'@INI\Gnb7.&9h1VTFuV,uZ$nW95J]DspP9bGfMaYETpu/Ibi?rNkljOHpt9Ua<D<^:C)^)asgl.L2@=YsF;Zk9X>-s/)O%"t*-r>e0+;O(#5oJ$tmTddlF?](WK^<RO6k3tJ/:WBDeUm[Q:.b@D8s?g5aGk\2Zh2;&UbhFMSFXe(m'<ir$0Z3kGAWU4G?F7OGqC.;BUXR=lSZYII:2(MaN/SeFi?G1C0\1qJ>6\a#YAXZR^B@cElCY$F/o'-r?'j94`:bS/Rd*KQ]UT%i-RnIbCXAMMg3Q#+rWd@i!kI$MTTPLir`JT#r4s5rQ+/_@H@()G4Z<F@k]lr5#[A9'YQ^0'LT8X\2iku/4gDtXgB!]B`O$k/o^%K`<,TO%Yb;.GP/Z!2NRf.8U%uUmc)RUkiWR=\FO>-q!I(_C?#7All](5as?Z"LbHTb-PFnEb;QS)P^kir7-a0"/9RTL5\`,t@X*gZ&;F.G^+Zu`,.bA`kekNj>gWo#_)$ITQuo(dZ?D;6C0))Q'LE?&[EPA5G,*qC+".IVR/8iRe;gZ\D-8K6OJFF(W,nt1?C)si#o7[#coWZ=W>OKDb"$$s3fVX)',g1@S#11SQ5EXCWiD>A,`R$N,DJoDhFoL/hQ8B=<TpE-Wrd<,W*SDVTe>o[!Q3'^66&%KW^,+Pl858Omr5Y`7r,qeg#\C0`9[!_H*8bnCPP+psaiY!uB\?P2\i?KT`,$`'#G<C0KpumhbH-4na^].a@R^L>*/6srRhV?e*Q7ZFKIdO_=s6lj+V;&3(eWJ&Tmd?=&BWX9s/\V:^O<<%g[I"bsP.C;9WM46`ig2p=]&DVc?M=t5125g=N5e?s%Auskgg3nT^YaP@G4RY'lc@'u>-MtkloB-!c6@`]S%NdpnkC&]rUfQ.P9!P$\\IQIh:jlJDPrD/]9j7pjFF[LR[0h3\h6LM/"[]bCV*fJm:r=u+fBY/6djD'k.f;N&(qfsoeLl"WRu7!f_s`!fPTkq@fqH%[`dDn0%Ril\L?1eN?^"ajg-1\=+gs:*3Ao%D+FPRbLo*NS")XOel]>3h7(>,B:`Ph'(V[W;a9$?n*n#j$T[kl&hJ%@#T"i@9D4jYnWrKiNU9>hq:%'pURR5J<jS(7om7J<GCF8Y4kI2n)-aYqE,_rDfXKEf^YA%2>n&1ZWk2];JY[L2?f,UZ6pgmWbO!h;\0'<`Y4!LfCHh_\HYA&*Eg_C6')euDqXW>;-<Dg0X&b^MIR>T!]<]]Sn>Pg3h2oOi"h[G%)HRIG-S5iN3]&B>Sc@Lhil]U^&r(W!e/*!QS3CMa/6.ioA8&>@>5saH,UN-^ht`H2adCt7roN"mHVR)oYB[Cq1jd59M@/iak^s*Ng+O]c%^00O>+.d_O:6CQ$+0,D$OKC_`U`NW_`bVUX]W%0P7]%]NbuO"P4r0Qp.>-uDtc%T;reSUe4GkG[mk6%WQ)qIYG\;MY/shHhn+R,eg3m\D:'Une*YoLk1X0"1iMmiO*KaZ>C0PRj>8X2PPP6D`P4I`CXFT,g7jG<VYXUp4PS,$lW1Q`A[W;,4D9,u**6<Y[t!@cHHpX1#A39=Q*;Fm:]<!fIo?&K/Xsju)/VM4XnUr-YgY!I1F&SYd&-KKWj?!Tea8m'<XocY.!`S1/usou<E8>hnpecIBlZ]\p+3FT.J1<a'sd.p_\>mVak$RVYm-RcNn\p6"04<T@oPc8KN3,$L[<TA(Of7.e:FDVq4@8mr@9lu<p?XAdW0SY#1Y1o\rIgd;Neme,1T&ZFK+VrYJ7eupMTkdFHVrcD+=7FCM6><Zj_[Uldi/&d8Yj:`I[umdo**mOTtFB'e@^Do(Tmch`67PBWgoG3csSq0k5_3jd-q<Y</<2NppF;nKVc%M2q^Y--kS4EBQH$F(nn+B<5F;c_5#Sno(2aO7U#/J,YPBmIjHo1S(hIY5=!$53p:41!)%Ko_/E^OQuSa-PqigoP#bMOB`OfE"slK]DF0,H50elaSk1A,hVM9>U19tkapJ1(2J-<:hNFS>Jq/:C[%_)r)"S:eWnIh?f1;'4cf6(R?7bhPe%nV2]Pr%c:QXXY;o!LRdtUQQ9p$nU<SfU3GB`sSTh)#AlUK!(hcL1QLUeA[&kWK:&T23K_+BP[oFQMdP;dP?b^c$l/K[X9c3gQ8<#,F@r?Yck`!S"p7L5kHhZr6bemki.r"iI,6#HID[IuB*Og44>[#4O1[L]'eV@(i,Lu5W=4#UP[LhonlY;k!SLI?HY5O@OpXG'1RT93TZaJf\VNO`g5"B4s>S%L%VkZsZdU6ip%V9VLKs]^eMDX)LD<0EZl\9@BVrOt/g>])XV4?E_d:k.DeRX5A_mQ>i*VK<)h\=o:]t%RM>,>5ro>`okOR-3Wrd7P`a>p-$=Qk$R0puru-@8q9?W,"bl,*`XpC*65/P1lpk`MVR-[\PBCQ#nX=g[tGP:'WHL[4D4c.5[h*at7G[@C=:4Lfk&DdDULWjLjEH)t9%rim'u[:si4/i54M/Vh)$YiCY7q/$XTSR:tbZZVN-%KPSMms]_jj`d]QZEJs=1::=U\/UHPg'Z,81S=Z-2D9\t&(;<nLhmrH]o^-W@mANl9hK9m,cVeCe9)o$bcR;9qaoKF\+uYdQ@'tn=]p2<s(.``Bh;Knj2R#irpL(f91_aub2k.p-%Di161QTg\siCMR=%]Pa1dOYZ0$=XOkDdMlELrC?Lu:23\5smPKQX.HmI+_;94-gYOukc\K9@[Da0H/E*^THJDk(_0'`2<<"$U&YZ7aXO7jIM6FNp`j@b0jQaLULjGr!,S%FUci-B=1.2XG&0HVu#UNI\0o!EjY-(BC-=tX!_^gQ)UE)2Y([+T9ek0/ftF,%'YPSca]MlB(!<pn]Ymb!:?lnn!Oc)"--G5o^hgYDJ[3q)E:B?S7QiV9%h8[i']dJ6a@h^;L#ipU3:.QMD7m.S853,$'1.)%9$JsrfJ+*P&.@%:r!`IAIf$h`G#d2Nd21P4b5D&_GNW[I\$l3Wn?qgBJuhaJhu)<?^Zm44rC8%MBKI7n[h"9$3N+7r/SHXKS&JOh7^S!L$@L6>d[4_!K*b]2[_N/]&u8L8#qS>g)pIP49UQ=nO89p`u$mS]2i*]8<JWVhoJMl'1:/=/nADfRIt43qC(Gj]PpA'G4XjpLpgkf[TB%55:6>jLL!W^f2L+@o^,]:WBm[%+@YeaH]d^5Yaj`E9H$m"sP.POlDFea(1-c'U#f?eaEq<C@g@.6Qm\1I[Ku[MUJ00EpM4Q5Ilk]kC3DT]o1o2FJadA)C.[%3JE(1o=q1bBMm4V^]]'C3^:AFpV?+`-SDUr<%B?hmJn)UM,9]PFnhT=J4P7:Qm_0r!etUks:c)aZ8X#d\`il<V"&:h`ZJgS:A)tA3+r.m.;`*W.KV#EmCc>d6<acP*Z.Dj]C0jPr&FtUV=I6j1%W*6COcUo=!"`7@I3LqXs/L8&TF66.J>cIs/A>]_>Fh/)NnED`[lmOnK*.d6taPo*s5@QlgcUr7sC4A$p0$qj(?2@>CF'S1(\^/,YdYqsmA6YL>]LkEfSeW/Pu'r&>k\'&(dPeuWfdK-I;9[d2+L#$TK=[,c?U;ea_/I.HI)?-iO7`s)+]%@,,8hARMjX]X62mU;=@]Rhmg-2NLQIP!Ro&[EM6cd"8!j2Gp?s%"k;iqR:rDkSodOu=mVTDhtl;cERIS32'ppg7LZ-4/.Ajg+!TQdXu,AaSeK2WqD\e*.@nd$.`n426gC]N;49%]?mc-B4=oK4'A'#55h^g)&*.]B7$OW6Or1\lq*fr26#hU3^-NNd8eCe.9MWX8&(92<TC_0!>2>\tY&BU:%o$4MOG[1p'\[#2*(R@4"1;pa9FFT*:TPmH`\Y?)MH53Y5N&,uZgb[H7/)A]UQ>1uF$#;J4>I%`r+n<_@MPPFPCV/)+MSeq'*nBfU\"geL!:+$Fr"g/!("VWGHN?V457=!2Wf>/l]B*<OAo1+c_C7!o*%T-J&`b4TQ%4!43o]>=f@i^:XkX4EPX>#%?_9NO-)lD>=g0QN-mo/ttn,OJhj5's[EiCSI!>F3XI#K1O$fec@hYO.t0a7JHRc<6[@_\qc=h<!gY'>8HJV'QFq:$uujBkbYR)>5ZM1_Qc`Fe?dR44$[KjL-#W>enT*>V!O40&K=:BQC'3rTta);OomIr(RJU=)PX?+3uH4+'?sgjb!="raBM4Y,4c2BWf9PLSkYEG4]JF3ge1S'haljV55G<>)$9/B%s4_='3II\`NJPO\h^gq=F<g-9m]o1K%/;e8dk.h5Oku^4TnWTId(VCaT/`>t5Ke1EHKmr!-K:o\8,L#mklQS9uVsqiI-#GP)k#AQi+e%G<h%7F.lC_3k#HdcY-OXJ&;1da.a@iS,FaT7aLP`V0s5=s>P^.P1ro`<*'kB1Q432&eS^Cq:^\<XW2k`@j``897!Vg$K+AmLE/P;<4XE^X-#W"a&'O/L1tuM#V_j0,>:Ol@!`Kr=Y8;/P5i31+cXlQF8D_bPk8[l!0lk0DCQ8^\^["V9$%e/XqqW/M1W7Vl'8$7;qStfFAFN*4IG?8f:>B<C3i@6`.l+ona$&Ci&cjnJC420M->mdm=,U7OE>4aR92E8[`h?aOXeD&>Wm\8P>\AjK#1W:GXWa-[SsC"d#$@b@F7=f7ThJKq,;]8)XTcqL703,!hjo]Y[2q&j-,RF7H'gNgK@.33MYq-q&:;b`]8sb<*BikPk@CpaOb#$CRaa(]FGQ4%RP\P;\H.//T2M5OWPZ6NP)r"l_%_Zp\(E>?V6<O&_&onX]3'6.F@HcecEL&U'S%LGi!=d(tKAonHeua`mDOiC&eO'5bsHC`REE=YJil]^?HqdE(P*?Nk/sEmiC5V*[eW*QY]iq`8[^T>#\2Y/Ec3"_lY[HtaIA>F99fW<.XmQ9+KLT:dQB;<rGPUNn!-O2bSB>1E/s&d`g8WYH5=kOXFTT"Z:4-,Ki<Wg+d#_"+$\$U8Lc@#dQW[[a]gVi`;XUW%W?hsKaFH0s>FBh1mZCI),^-XU%HJpr-e@]E3)X-D;?^)n^MTR0!DJQ/70nr[oVM*:E6HR)HgU+`?CfeR?T&O6&>kp=eHL($9Jc`VP!T;j+f>>C?CcWINFo'HOY[\1Nr$mCCW.EE2fJMPXdBpidO8^9fsfds/t+%[EhI,b1[H51b`geoXYW4F-,\\j$qcB#3A$L"*r[Ytg/AFf7Ils7L(>7s`q8h0OYa[b!(/urakOI#?2\`;RGV'TKS,hhXX],H_$,IEZA[&TNGU\XfDiei#>gj3ZBd]tN?g!J)2S!Nu!i^h8\dSb>:6hNG`(Z"GlV51*;o';?3l=f#ck?iH_SJH>2`LJ`#eLH[l[7U6l=$.e%HQ)iOBDm8g5H))Z.rgCmGGOt5?.ZIpijA(%hbsEGO(i=3/#Jk,6nR3$@Icl+%rLkqh@/*Te+)n[RZ5<S)qgYh]bWuc.X/cb0dfOW)anE9iq`0pP9s"n4D!4\;.\1/:Y2dURlk0l`Kul\%eCouLBZ`l/B]YQZ8>Em8K&Ytfa:+_IR($08O4"SWapbjgMcMU\tc+HOg55DG.m7%aa((EHq;63m,Efb:hD]U[-VGsae(u8Q-A-B/XjVsRbqb^VWIfR+fe3p3a?dp#6'%F^87[qd-XccQd+(.-U-TejY4,XV>JrbYg^NZ-Ni?pO\h_6nmU^-Mq9(D99M2N8fj$@3o@m69ZrYWWI4;=?gk,L]<cl4.D'fk0&0Ju^+ZFF<"E[\[]6aSP:6<obCos1RNk,%\b7tn6%F#e[S-GD_9%$=1c9"T`equ'd_n!`s$<TK:WG\E&lg#RQGdli;ff#X@0U4FOqh\hVcYThFkk\\^U]7U\+c?;r0Mn]Z4CY@Y))n)2&%h"J`G63P7[7_DiiGfY\1\NIXf`>]);W#K?GB0bJ"ghC"]r(gDAfj'@XDN4>t+6V)6F$M*WT^4St??DV/_`<%apa,a5SeH[(IHZZ)#.hB4jlUa.5'G'uXUP^M+PcRpA\[*GJua^@fcki`o^fQuqB<d6&nr"GA5Bdl0o2P9hKX[o4un^;OrOh,66``[&+ofTu'Z8+/LVihqG1ATD)0;[j=N.cT@e%_*mPk\#f8PAr??2T@V/sE_O*:5hKIas(<22THJn>KCCa-.d3-9ou2Gg$&q<h2<=9Z$9>9li'\.`P`d?qea3Q*+gMS17k.m61;O?YY9[NdK_'i=s^N8IYJ3gZsa0YHEStc$,QaOHpu0=uJl1B76%"=);'?KNtgQr4/4'#2!"!6SBpb9_%33L80\VCt!(ga,5/n,E.R)Fkkh@Y4T\6.8+o\fb\uh@AIJYZR':4j1&*_/"]PM@@<XO;^.6%BVHHHP`[V([VD)pEdkgn4S^cHC&(W(-/T*o-JdmM$@]'XcK?Gh;cM@[jG+1EX(-Vf]><AC4o$o075<,7pj'BL2]27l&31kr-<n@5%FiWj'hHMc%gj*_=uEJ[LCKNle2_63lon/C(nGYEPKf04BtmLdp;[+&8eO\<O$#!c'2c)Q*7Y]uD;g/PK6?%RcmgG--DUf\]r6m5^_>&cr1[48.P:J:YB_?a&\Ac<;YQA2h5qK-g%03?OO3W%a]GU@p/`S*ZR%<iT:<S[*5ZMdm(mffX/>&[%j7ZZZDZfGc;/`F.+daqSJs<MA.;Nh%5])V1s1c-rjfT5=/le:.8T)P8DT+TBR-N3re4B@F0YD$;"TM'N=8uih5tlTVPPcr-V>QB+-+BGpmKV=-]5YAM!u(2BH9$TONf.[<@S1P2&'-/bP:M/fht3!d]g5&\c!%QRET@o9p&,A13NE@AP3gmBe.Amd$4e+ju5D=k\e_.`h)9sQM>HQ82F&:OF*R)?k9=Qs&n:7Y)5()gY9(?NXL*5kHY^aNAWp:m=EfX5PEmcc%@f(q'U'*U\&oqZ^&]pPl8eE(D9cG,FcTB)"W:M/F7S-?IbIir3"p;9;<Ir[O$qdRf$F+$N;pSjQpH2ZqTJdjCap[5$/gg9Mp8R584HYWV"b;_thnJg50rgFX=WIl/K`Ti2;OercoX;P``X'f?;#^Z&).I2HOM&<Yfb&oVfTnQGQ</97lNr#iGU8f2BHdnt1;BDp]lZmTse`@h0D_P"UYg$i)O;RfN?6oDG?C<Wu^6Oi\\<e\Ne?KK1-pHCk-Sa/SX'+YAm</\Aamkk018:S$3G>$>65-F2tdh;4sm0HXOISu:aC@t::,:mT$r\X\.Q/cLC(Pd/d)aAm^0a3JUJU"_T.0>1Dt-C[&#*qP*+pF@8[C!"+;hVl1UfWK;Ofk\?"k.%s#9X3J'GMYIoBmnm]qspb9pK[ZW0D&g3H7_2::LhX=rjHFSlZmeO=#Mjpa$?_(B)OMEdbRUe!s`O#\C/f91+ce=QlIdDcr?-]r:%+0h;5Udr2TWfd3Oi;)dC11:KNE-+_bXNq`g,3&Pl@O*&O;eeW`Q]k&=*bQQt%GrJNLcHJ0a!1G**qW^5pSAJEhtr)S_(H6kf<F/X4F&bk1\[)rg*'T[G4l*ZTfBo=<Xrg&s!r0dh,nPm(p\_0kjSt)E%Su2BQs$0`tY-^$TbuF.g7nXB=`;/dU^XH78UqIVCqI*+,`-uc?gkX5+E'g,*Vo*%<9uk$3HFu-0cUgd[I80\gg?:'AiX/UDW@b+)O&%RB,A?3jQKh)?7fr+e$98%=X3/PW4$^Z;KPaE<-d2gIZlF1%RD:P5=t8ZURZDke3FN%ZiCCT&*T;9k3psm%[&S8\kEBLR6L$q(j1)Y#k;E7&d]tO$C@jY@K+7u^4GE'icB'R>2^J41&g8ihQA"YOXU2WpfVsrYlUn&NhUVA^#b"NeP*!*us2!#%iI,jVkLC_-UVG[[Zm:Tc^SZ>EM^<)JD[Fi)BpkK*l+D5%IkRQ-]o.76'J!aLMM34S]&u-Hbk:J(-?!EWSPm$eMS<OcaO8OIY@aSA6IXA3Xd&*:&(2))RETC0I<h*Oq-6i5rhfXfYM/-:LelCQUb2OM9K2WN,"3q:TD?/\dS$bnUh02M'9\[BRnb:/XT>4DdPAA^12)';/"bL]n[NoAoI6T%o@\OVLrQ4IMO^0;q]\ldd[bRmp:;lPAb=RfP*YM`V^^K-6<YQbP<1A,J;6:l9)eq?1AZgIFg&C>$AdYeE(I+,Hb.q1BGFE^n^QtrBkn5n)?'6HUug&]8B[&J/_@..9@\BeF'?kX9iV5"e+tFDq'FYl7Q$s/pp=Rh-MHsA:K5tTckL$aV)#nI81VUeF;%GRmS%S'qGd2%eUseRBh@U:f.6@rLXc`h*Z*35#pLr+;d'[EJ%rlXe)>7jh<Hk;V^4'ImQ?pFUo)8pe^Y[DAO_tk3.Crt=pc;"`MB?jYJ'i:Ner53,BO*])/c#?XH-%F^JWD)GL6%L-5Z7(=>N'_'h;HmG'q[dLV7X+@LC,3^Xq0g/cE7.l<K4Rj?&,u/-pA<6'RuBEspgQYmtLDj=E[il_LuCOssg8L](U@pBT62@eg=b<eP;C\Bg?L"DQOVWS"m3(jnQ)bhL.HZY%FW>!f-ogotrBD&opgZD-rW76Q+tG^O,#pg6f;16eTrrHn9bh*mo:f9h06KKnZDm8ZO(WoiH^%.m/\3l9))nPk[d-]g:8eSnd;MJ1J%%uT/)Ia+Y]$ZXa!BlZ_"rrS(TA["$D6^B&GIk8RO4JWjeTBUXSTlZIYILY\fg<B9:3.L6>aS]0q:Ae82GAc=/M8/<RV*rD-dr,2;WqVi7:X1$X.NE5I\UfIAln2g>)*MHip6*j>Fk:Ng\on#-q$lZ@P78<m7;peb/1N38V+Sgl`@kZTR'A_F\(B6`hn+70d[ujaXBQLCT(jUj*W:gae,)R.aK![S9T<O!KUrPX2p7WH'6P9^<Xht]SuVBgqm=ig:C.pY`N4kZ/bqNVn"/u_38o:,_qTFbH"Xn#F;e.`SY&fXN21$#%7=J#XuTJ9W)lS#HPa47fD8MArU9/8lOT)CSq^ho;Y'4p<j-H(>/hKf(`RTZkNJ0Ff?rZ8lRN`rSmM&in!!]X3r1H.5b+t^Oj&,+\(?':rU1t&98hL$-07;-QBK*4hT0-Cj\hM"IN[NCBh;K_UP=i*au\dK'QS:d"icK>9pS9Gc6EYY+2@ilm<86.F.sjE03bu_mi<>$(QoN5S.%o9<MGL`,j8_KIheMUK"<_5Q+Lt6aNn66D+dlpKNrV7'J_"R'PUJ1P]GKKagWjGcOYBZ\#J`G\+AKa'H8PIOio0%2-\*T>=S4jHVj,\SO2UfFJ@S4k?<VDVqDSkV4*o2EiDD!6gN!9Z)prZe:6d$ca/Od3bK)h,=tO`_m++(>,mr^8ElA)*62XhpO@&WOD^]2;1Tl>,DV>iI_GKFQ;OO>5:/'6e^9<4/Yg!ts%:d\ClUng%hA1?2VP<%=V=`Cgig(rjEffVb@o9k+)JnRZm2QGja;j<WoPp9?TsCV_,'cc<i?,^gJ@ag)f4V6cIH>_$h'QhbdIhb02b75"8-O#/.%/Ke$8@$?c##($1s@U-&JV$b5H<j<+==>d-L<SQ`oV.<CcL[Q4t,?!WeF1aa&:>BbL%m+Zk0a8u@:.M7d+9A7'-C7aP4+U/kn$RX2_u)\mR#p0h<"FCd8>hC'Mup"I9>9YBtNM(UOr;nCBo0;q>aSGbpf-7<*/!V2pjO,^T?q5sn5k662P*bk\',\]geNQ-tD2[jq7&fQaae>22=B^UJ0/OC,>>eKOX^>87UG_:`UrfBdN`-r%%HP](:nI3puSZp.LW*E*r<]dic/tF><6uK$3lsiNeN=ZfXdcl3Tllkd[YNiJ@dknp0$jhuo[\8J`V-f<pN`gp(39[lc8!>lhm)BPl:QRsrN60C1$E':,[%Db/Bh>7&Wc,W.S0jL-O[:(XEf!b].d9S:U,Fp!D>2>HT1tk/P=UD"9L+>*KnZ<48K!XFI+a[Zf<3\EPdB,"N,P*]bkRd'FB?\aIS.u7CW2"pf?79bM1gPhPaYAF(_Jn9X8'%IX`=?VGE,p$H/[G&]e!*#q8@C7h]^I\*pOCm2O\E2@1(#RaL,amIrhg^+j!Nu_cKs;.2Z\Xo6\R6j1Fn&kC<4BHXO68FIjb\/,bi^5fO>9W^pbp5-nTLkE:2;8$COVDTs(`#>ui,M`b!#C;bo9kKf&k9D-,R_pd$_<fnl"1M_0NdunZs4@]l'?G'\)kRjR86\ig;8\QHUmKuQj^D)?a63;?,Pc8pgb$"b560]eG6S`3CI=(^3<uJ,@]<f.tahgK=NZ4TCQV3$f1+b$Dr^LJb^UME`->WGQ<7703Cs(8P?Z5r8_F62?Fh6T1*aS"e&k%IZGp3Uq`5-n<GPjt3)ns<r1_2cRMX/(jMH-VC3=@Tgh+"]$`^:\.:X<6[J)R#Mk76ZnrK.O*Au3mC96;e/ToafQ.qRcHprU%2_mJ/_Z_%0Hp[1e#'D6OaK`[\IbLo,P]B1gCA(:oHhn_(7s&1:=5fW<>R&$9GbM)bg,!X'X@P:\bUJ08RA/b0c[9grECS+!=U'<+1!^?(29;WrkOI@t07VRPOD92kP_XqJO/PNdA[P+bP0@W;?Fi#Db),/FG_nMt-Rla"2U`7RaX'd'[X/p]d\ojLWqFZ/2HK1lpIe`Zp%?GgBJ`,[M)aA`Eh,bOSMHp.X%3)cofh.G*")YHpiAab?S+XfD.p&hII/7Jc7ZG]TVoe`O9U2HOP4+!2Bc3=K`_;jWFjYdE5CWXMSmAtc2SLKP\?i8aj8/FXpX5p5hgbN/h3D:N#P34hGO+_cXf2$n\)58XR.o^tku4IAr#@k?</*M@/s/Sd1KriP$-?2q:`oSVR)p!hcMZ[/U`'!a2rEb9fm<SkM8hJm\8\([kI4_K1*-1ZHlNbr2Ob1pp+qd,;P0>.lmlrC'_/lNq1lh4,6N7L=b21%2f/0`?8h<BB(P9e":[#Z^Ci65FPK#e54shBh\+c=chTiLaUm<;Ocr!eM@=Xa-rOnfq/"JGZ_#K`hQ)\*#$o;aniZs$<`_suUk\guaT@lnWS22&pabHrFC@ji=g[[(G=Vn%)%9Yji<3HNBDPnTfn4'31Y$rLh"0M\kI/B-J`[5.C@5t!DS*:+FSpJQjYC,p$siB;VBAg.4'^%fiKUh%J_tYA08^1D6V'-#[XY!S=NK'00VA0Eo<5iVebr$::0?'f^YeMQG\E#]lOAdO@"puklK.mUH*]"JEa<98S"DflI-'paj2[21<*(0?WZn::]&r(1PBBs15()j]d:h6X;"sj7Zl@Hmf6aW\_r59DC3nCVE/X2DH1&W@j+jXMWqKL_.n7[t!sai-4^*34p?Innp[6mdrp2X\@/Yr:=/>d-$$aPuS;>$8k<nm1$X0!@46MF7Ficj0fX%faa0iDt_+":8D^3kV@&4,FG3B%P,L;94>n+iEpM-4B&%_0N,2(t$HB'g,8\/pKl?=6T#K(G#OF?Lth=J@k7[Dr?`jXm<=$p$UIWF#kGgijF(c8o8HYoLGZ6JTGnqS3>JF8io?+p!oAbVruY(oO$=+Dsu&%`pSn3"JHH[FMnr#N)C,?2\b!9+a?4k8V+MXRB//jHJiFsLL7B)mGmLU>i1-iT106Y"u/JH"4e<u_4u!sf.f:X6Hk*d-'&GMFUQ?/K)(QtnnY'74]#[V2t8k"Rmc]#dl879X=I"9A3EiI#h-r=Ol?i_XDE$?*OQoig=DTZU^"Q^7r'K)bm8#gR3u$:!qT9C$o#e9phb:;]g.!;p=P,VAgeJE8Qhg*[!92#mUu9&+-[C44p*<Q]DFB(uuuTEbH^ntAiPM=@gW,W\,VY'@8F@SW#52:1(Y!:XFeBj!9TJ*gD_la`th2cf#;ptfR<fp_Tb.tsQ2(IJs\,l#45D?'Z-!g/;r(%F<&'TNmBM[Od!\WFo,SH/]jzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz#-\%33+J<Q~>endstream
|
| 20 |
+
endobj
|
| 21 |
+
4 0 obj
|
| 22 |
+
<<
|
| 23 |
+
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
|
| 24 |
+
>>
|
| 25 |
+
endobj
|
| 26 |
+
5 0 obj
|
| 27 |
+
<<
|
| 28 |
+
/Contents 9 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
|
| 29 |
+
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /XObject <<
|
| 30 |
+
/FormXob.a883044e48600fc86784b4f6109cdee8 3 0 R
|
| 31 |
+
>>
|
| 32 |
+
>> /Rotate 0 /Trans <<
|
| 33 |
+
|
| 34 |
+
>>
|
| 35 |
+
/Type /Page
|
| 36 |
+
>>
|
| 37 |
+
endobj
|
| 38 |
+
6 0 obj
|
| 39 |
+
<<
|
| 40 |
+
/PageMode /UseNone /Pages 8 0 R /Type /Catalog
|
| 41 |
+
>>
|
| 42 |
+
endobj
|
| 43 |
+
7 0 obj
|
| 44 |
+
<<
|
| 45 |
+
/Author (anonymous) /CreationDate (D:20250713105430+05'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20250713105430+05'00') /Producer (ReportLab PDF Library - www.reportlab.com)
|
| 46 |
+
/Subject (unspecified) /Title (untitled) /Trapped /False
|
| 47 |
+
>>
|
| 48 |
+
endobj
|
| 49 |
+
8 0 obj
|
| 50 |
+
<<
|
| 51 |
+
/Count 1 /Kids [ 5 0 R ] /Type /Pages
|
| 52 |
+
>>
|
| 53 |
+
endobj
|
| 54 |
+
9 0 obj
|
| 55 |
+
<<
|
| 56 |
+
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1132
|
| 57 |
+
>>
|
| 58 |
+
stream
|
| 59 |
+
GasaogN):5&:N^lqIQ[=\%"u7I"=4/+e)%))=XN3p.\p,]OP0I/7t3_hmP*<e0$NcV$*;Ph0mch-kLdQqL+n:N+2!F.8U6="t8q\LnETXDZ/;s]ri/.5`X1+ms;A06V&%4[X(lKo=reT/#(*0S4an,KP]4:&4V&d2ZY6'FWtB"'@ls5WnfA%61(Q,A_%<>:MiK`JWSjb3IH9Ybbt'Oo+2sTYOdP>h/<l)"7bj2TT5Ag7+Z.)IR>nl7@Ck)8e3N\N_mknfN@Sg]H1+Z]T0+-_m"kj3jdKdX:;Kmq^)ZVKmPC_;8TZlAJ3"4/DG'&TQ,0?JqXh/)f:J'8AkrFhKY$hU6*a<]-%+\h/L\$=N1lX!O-P88m[fl(kc8Khc%!%\WBM->EU#3qpMEJC,aS%Rb3BaZB[J)kC(&Wkm`#b&Nl%Lk(qCHFkmJfGRnI3N'o2"o4=P:(h*E38Ps'`ZOh0HnIDQt#0UL^p?QH;h<8T:q:+uYEimf]8QF`j+Zt=r.Zr=[N1WpRc+sF<:7qdQebc[Dc#os$S=lf#Hu^W(04D%.gH-V?B?k!u6j>l%J2TXV8`'+cB6qu:o]3IDetP\j=2S$A>2XN0O";=q?/cXi#[<P/&ElA[)BP#iQe[\lN3DsA+QSllKD$m9V26dPljtbT9\^1*]HrH3nNRR24;6fY<@jA([q.QSd\ol(;W\DD^4n;VX$kpjelQuTlW<iUYN>juc.KR6$tg60kOJ'+7OT$WLVii:Hhq^AGkS")UA+p+BgeDTJ/],M@BFctXUKPdR#)e1fWQ.F4]35umK!),�+Tc<Xm#5<4T34*3=9CFoq7=K*k<13o6>a&!5P!B`Q+*q?[0i!4-U4-/P/V`krso+J:X@BGD4XeoaqfIgj\m)m+X1P!+;N#!NELpboobUj(+p%0F<3@<8N`)T(D-P4CD;62("2j"lP!MpI]cDm90`rD5:$,gP#*>FG6#)P=(YgFp*9FF'&$#3E#Zq5BmPD6Ql;p2Se2,X=bCBAOgj%\GkN)S6gJ<u>XhsI>!_/.R5FumSNn:4M`(5Kc5S(\YO<6OoNCQ</0!b0\GTjT1h[(PT_N`F'^Z0/-NmCDtG6(M`IQI4Q*PtjtGI>NFkKMhS.(hijYT%A+%~>endstream
|
| 60 |
+
endobj
|
| 61 |
+
xref
|
| 62 |
+
0 10
|
| 63 |
+
0000000000 65535 f
|
| 64 |
+
0000000073 00000 n
|
| 65 |
+
0000000114 00000 n
|
| 66 |
+
0000000221 00000 n
|
| 67 |
+
0000012866 00000 n
|
| 68 |
+
0000012978 00000 n
|
| 69 |
+
0000013234 00000 n
|
| 70 |
+
0000013302 00000 n
|
| 71 |
+
0000013598 00000 n
|
| 72 |
+
0000013657 00000 n
|
| 73 |
+
trailer
|
| 74 |
+
<<
|
| 75 |
+
/ID
|
| 76 |
+
[<0720c27523b68e3845d9276aee7c891f><0720c27523b68e3845d9276aee7c891f>]
|
| 77 |
+
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
|
| 78 |
+
|
| 79 |
+
/Info 7 0 R
|
| 80 |
+
/Root 6 0 R
|
| 81 |
+
/Size 10
|
| 82 |
+
>>
|
| 83 |
+
startxref
|
| 84 |
+
14880
|
| 85 |
+
%%EOF
|
app.py
CHANGED
|
@@ -8,25 +8,24 @@ import json
|
|
| 8 |
import base64
|
| 9 |
from sendgrid import SendGridAPIClient
|
| 10 |
from sendgrid.helpers.mail import Mail, Attachment, FileContent, FileName, FileType, Disposition
|
| 11 |
-
import
|
| 12 |
-
locale.setlocale(locale.LC_ALL, 'en_IN')
|
| 13 |
|
| 14 |
|
| 15 |
-
# Initialize OpenAI
|
| 16 |
client = OpenAI()
|
| 17 |
|
|
|
|
| 18 |
def parse_natural_language(prompt_text):
|
| 19 |
-
"""Use OpenAI to parse plain text into structured product data"""
|
| 20 |
system_prompt = (
|
| 21 |
"You are a helpful assistant. Extract the customer name, address (if any), gstin (if any), "
|
| 22 |
-
"and a list of products with description, code (optional), quantity,
|
| 23 |
-
"
|
|
|
|
| 24 |
"{"
|
| 25 |
"\"customer_name\": \"...\", "
|
| 26 |
"\"gstin\": \"...\", "
|
| 27 |
"\"address\": \"...\", "
|
| 28 |
-
"\"products\": [{\"description\": \"...\", \"code\": \"...\", \"qty\": ..., \"rate\": ...}]
|
| 29 |
-
"\"gst_percentage\": 18"
|
| 30 |
"}"
|
| 31 |
)
|
| 32 |
|
|
@@ -43,7 +42,6 @@ def parse_natural_language(prompt_text):
|
|
| 43 |
text = response.choices[0].message.content
|
| 44 |
print("LLM returned:", text)
|
| 45 |
|
| 46 |
-
# Try to parse
|
| 47 |
try:
|
| 48 |
parsed = json.loads(text)
|
| 49 |
except json.JSONDecodeError:
|
|
@@ -51,47 +49,57 @@ def parse_natural_language(prompt_text):
|
|
| 51 |
|
| 52 |
return parsed
|
| 53 |
|
|
|
|
| 54 |
def format_inr(value):
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
def generate_proforma_invoice_from_prompt(prompt_text):
|
| 58 |
-
# Call OpenAI to parse
|
| 59 |
parsed = parse_natural_language(prompt_text)
|
| 60 |
|
| 61 |
customer_name = parsed.get("customer_name", "Unknown")
|
| 62 |
address = parsed.get("address", "Not provided")
|
| 63 |
gstin = parsed.get("gstin", "")
|
| 64 |
products = parsed.get("products", [])
|
| 65 |
-
gst_percentage = parsed.get("gst_percentage", 18)
|
| 66 |
|
| 67 |
today = datetime.today().strftime("%Y%m%d")
|
| 68 |
safe_customer_name = customer_name.replace(" ", "_")
|
| 69 |
filename = f"Proforma_Invoice_{safe_customer_name}_{today}.pdf"
|
| 70 |
|
| 71 |
-
|
| 72 |
product_rows = []
|
|
|
|
| 73 |
for row in products:
|
| 74 |
try:
|
| 75 |
description = str(row["description"])
|
| 76 |
code = str(row.get("code", ""))
|
| 77 |
qty = float(row["qty"])
|
| 78 |
rate = float(row["rate"])
|
| 79 |
-
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
product_rows.append({
|
| 82 |
'description': description,
|
| 83 |
'code': code,
|
| 84 |
'qty': qty,
|
| 85 |
'rate': rate,
|
| 86 |
-
'
|
|
|
|
|
|
|
|
|
|
| 87 |
})
|
|
|
|
| 88 |
except (KeyError, ValueError) as e:
|
| 89 |
print(f"Error processing product row {row}: {e}")
|
| 90 |
continue
|
| 91 |
|
| 92 |
-
gst_amount = (subtotal * gst_percentage) / 100
|
| 93 |
-
grand_total = subtotal + gst_amount
|
| 94 |
-
|
| 95 |
c = canvas.Canvas(filename, pagesize=letter)
|
| 96 |
width, height = letter
|
| 97 |
|
|
@@ -110,11 +118,10 @@ def generate_proforma_invoice_from_prompt(prompt_text):
|
|
| 110 |
height=LOGO_HEIGHT,
|
| 111 |
preserveAspectRatio=True
|
| 112 |
)
|
| 113 |
-
|
| 114 |
c.setFont("Helvetica-Bold", 16)
|
| 115 |
c.drawString(50, height - 70, "PROFORMA INVOICE")
|
| 116 |
|
| 117 |
-
# Consignor Details
|
| 118 |
c.setFont("Helvetica", 10)
|
| 119 |
c.drawString(50, height - 100, "Consignor:")
|
| 120 |
c.drawString(120, height - 100, "True Vybes")
|
|
@@ -123,11 +130,9 @@ def generate_proforma_invoice_from_prompt(prompt_text):
|
|
| 123 |
c.drawString(50, height - 145, "Thane - 400607")
|
| 124 |
c.drawString(50, height - 160, "GSTIN: 27BV0PS7767J2ZF")
|
| 125 |
|
| 126 |
-
# Consignee Details
|
| 127 |
c.drawString(50, height - 190, "Consignee:")
|
| 128 |
c.drawString(120, height - 190, customer_name)
|
| 129 |
-
|
| 130 |
-
# Multi-line address
|
| 131 |
address_lines = address.split('\n')
|
| 132 |
for i, line in enumerate(address_lines):
|
| 133 |
c.drawString(120, height - 205 - (i * 15), line)
|
|
@@ -135,42 +140,41 @@ def generate_proforma_invoice_from_prompt(prompt_text):
|
|
| 135 |
c.drawString(120, height - 205 - (len(address_lines) * 15), f"GSTIN: {gstin}")
|
| 136 |
c.drawString(400, height - 190, f"Date: {datetime.today().strftime('%d-%b-%Y')}")
|
| 137 |
|
| 138 |
-
#
|
| 139 |
table_top = height - 250 - (len(address_lines) * 15)
|
| 140 |
c.line(50, table_top, width - 50, table_top)
|
| 141 |
-
c.setFont("Helvetica-Bold",
|
| 142 |
c.drawString(50, table_top - 15, "Description")
|
| 143 |
-
c.drawString(
|
| 144 |
-
c.drawString(
|
| 145 |
-
c.drawString(
|
| 146 |
-
c.drawString(
|
|
|
|
|
|
|
| 147 |
c.line(50, table_top - 20, width - 50, table_top - 20)
|
| 148 |
|
| 149 |
-
|
| 150 |
# Product rows
|
| 151 |
-
c.setFont("Helvetica",
|
| 152 |
row_y = table_top - 35
|
|
|
|
| 153 |
for product in product_rows:
|
| 154 |
c.drawString(50, row_y, product['description'])
|
| 155 |
-
c.drawString(
|
| 156 |
-
c.drawString(
|
| 157 |
-
c.drawString(
|
| 158 |
-
c.drawString(
|
|
|
|
|
|
|
| 159 |
row_y -= 20
|
| 160 |
|
| 161 |
-
#
|
| 162 |
c.line(350, row_y - 10, width - 50, row_y - 10)
|
| 163 |
-
c.drawString(400, row_y - 25, "Total")
|
| 164 |
-
c.drawString(480, row_y - 25, format_inr(subtotal))
|
| 165 |
-
c.drawString(400, row_y - 40, f"GST @ {gst_percentage}%")
|
| 166 |
-
c.drawString(480, row_y - 40, format_inr(gst_amount))
|
| 167 |
-
c.line(350, row_y - 45, width - 50, row_y - 45)
|
| 168 |
c.setFont("Helvetica-Bold", 10)
|
| 169 |
-
c.drawString(400, row_y -
|
| 170 |
-
c.drawString(480, row_y -
|
| 171 |
|
| 172 |
-
# Bank
|
| 173 |
-
bank_y = row_y -
|
| 174 |
c.setFont("Helvetica", 10)
|
| 175 |
c.drawString(50, bank_y, "Bank Details For True Vybes")
|
| 176 |
c.drawString(50, bank_y - 15, "A/C Name: True Vybes")
|
|
@@ -178,7 +182,6 @@ def generate_proforma_invoice_from_prompt(prompt_text):
|
|
| 178 |
c.drawString(50, bank_y - 45, "A/C Type: Current")
|
| 179 |
c.drawString(50, bank_y - 60, "IFSC Code: KKBK0000656")
|
| 180 |
|
| 181 |
-
# Terms
|
| 182 |
terms_y = bank_y - 90
|
| 183 |
c.drawString(50, terms_y, "Other terms and conditions:")
|
| 184 |
c.drawString(50, terms_y - 15, "GST: Extra at actuals")
|
|
@@ -187,18 +190,15 @@ def generate_proforma_invoice_from_prompt(prompt_text):
|
|
| 187 |
c.drawString(50, terms_y - 60, "Validity: The validity of this price is 10 days from today")
|
| 188 |
c.drawString(50, terms_y - 75, "Freight: Extra on To-Pay basis")
|
| 189 |
|
| 190 |
-
# Signature
|
| 191 |
c.line(width - 150, terms_y - 90, width - 50, terms_y - 90)
|
| 192 |
c.drawString(width - 140, terms_y - 105, "Authorised Signatory")
|
| 193 |
|
| 194 |
c.save()
|
| 195 |
|
| 196 |
-
# ✅ Send the email
|
| 197 |
send_invoice_email(filename)
|
| 198 |
|
| 199 |
return filename
|
| 200 |
|
| 201 |
-
|
| 202 |
|
| 203 |
def send_invoice_email(pdf_filename):
|
| 204 |
SENDGRID_API_KEY = os.getenv("SENDGRID_API_KEY")
|
|
@@ -206,7 +206,7 @@ def send_invoice_email(pdf_filename):
|
|
| 206 |
raise RuntimeError("SENDGRID_API_KEY is not set in your environment")
|
| 207 |
|
| 208 |
message = Mail(
|
| 209 |
-
from_email="truevybescg@gmail.com",
|
| 210 |
to_emails="truevybescg@gmail.com",
|
| 211 |
subject="New Proforma Invoice",
|
| 212 |
html_content="<p>Dear True Vybes Team,<br><br>Please find attached the new Proforma Invoice.<br><br>Regards,<br>Automated System</p>"
|
|
@@ -230,13 +230,14 @@ def send_invoice_email(pdf_filename):
|
|
| 230 |
except Exception as e:
|
| 231 |
print(f"Error sending email: {e}")
|
| 232 |
|
|
|
|
| 233 |
# Gradio Interface
|
| 234 |
with gr.Blocks() as demo:
|
| 235 |
gr.Markdown("# 📄 True Vybes NLP Proforma Invoice Generator")
|
| 236 |
|
| 237 |
user_prompt = gr.Textbox(
|
| 238 |
label="Proforma Invoice Request",
|
| 239 |
-
placeholder="E.g. Create PI for Customer, GST-18902801280128, Addresss -Andheri Mumbai, 300 candles at 20 each, 500 umbrella at 200 each
|
| 240 |
lines=4
|
| 241 |
)
|
| 242 |
|
|
|
|
| 8 |
import base64
|
| 9 |
from sendgrid import SendGridAPIClient
|
| 10 |
from sendgrid.helpers.mail import Mail, Attachment, FileContent, FileName, FileType, Disposition
|
| 11 |
+
from babel.numbers import format_currency
|
|
|
|
| 12 |
|
| 13 |
|
|
|
|
| 14 |
client = OpenAI()
|
| 15 |
|
| 16 |
+
|
| 17 |
def parse_natural_language(prompt_text):
|
| 18 |
+
"""Use OpenAI to parse plain text into structured product data with per-item GST"""
|
| 19 |
system_prompt = (
|
| 20 |
"You are a helpful assistant. Extract the customer name, address (if any), gstin (if any), "
|
| 21 |
+
"and a list of products with description, code (optional), quantity, price per unit, "
|
| 22 |
+
"and GST percentage for each product from the following text. "
|
| 23 |
+
"Respond ONLY with a JSON object like this: "
|
| 24 |
"{"
|
| 25 |
"\"customer_name\": \"...\", "
|
| 26 |
"\"gstin\": \"...\", "
|
| 27 |
"\"address\": \"...\", "
|
| 28 |
+
"\"products\": [{\"description\": \"...\", \"code\": \"...\", \"qty\": ..., \"rate\": ..., \"gst_percentage\": ...}] "
|
|
|
|
| 29 |
"}"
|
| 30 |
)
|
| 31 |
|
|
|
|
| 42 |
text = response.choices[0].message.content
|
| 43 |
print("LLM returned:", text)
|
| 44 |
|
|
|
|
| 45 |
try:
|
| 46 |
parsed = json.loads(text)
|
| 47 |
except json.JSONDecodeError:
|
|
|
|
| 49 |
|
| 50 |
return parsed
|
| 51 |
|
| 52 |
+
|
| 53 |
def format_inr(value):
|
| 54 |
+
# Format with Rs and commas — no ₹ symbol
|
| 55 |
+
formatted = format_currency(value, '', locale='en_IN').strip()
|
| 56 |
+
return f"Rs {formatted}"
|
| 57 |
+
|
| 58 |
|
| 59 |
def generate_proforma_invoice_from_prompt(prompt_text):
|
|
|
|
| 60 |
parsed = parse_natural_language(prompt_text)
|
| 61 |
|
| 62 |
customer_name = parsed.get("customer_name", "Unknown")
|
| 63 |
address = parsed.get("address", "Not provided")
|
| 64 |
gstin = parsed.get("gstin", "")
|
| 65 |
products = parsed.get("products", [])
|
|
|
|
| 66 |
|
| 67 |
today = datetime.today().strftime("%Y%m%d")
|
| 68 |
safe_customer_name = customer_name.replace(" ", "_")
|
| 69 |
filename = f"Proforma_Invoice_{safe_customer_name}_{today}.pdf"
|
| 70 |
|
| 71 |
+
grand_total = 0
|
| 72 |
product_rows = []
|
| 73 |
+
|
| 74 |
for row in products:
|
| 75 |
try:
|
| 76 |
description = str(row["description"])
|
| 77 |
code = str(row.get("code", ""))
|
| 78 |
qty = float(row["qty"])
|
| 79 |
rate = float(row["rate"])
|
| 80 |
+
gst_pct = float(row.get("gst_percentage", 0))
|
| 81 |
+
|
| 82 |
+
row_subtotal = qty * rate
|
| 83 |
+
row_gst = (row_subtotal * gst_pct) / 100
|
| 84 |
+
row_total_with_gst = row_subtotal + row_gst
|
| 85 |
+
|
| 86 |
+
grand_total += row_total_with_gst
|
| 87 |
+
|
| 88 |
product_rows.append({
|
| 89 |
'description': description,
|
| 90 |
'code': code,
|
| 91 |
'qty': qty,
|
| 92 |
'rate': rate,
|
| 93 |
+
'subtotal': row_subtotal,
|
| 94 |
+
'gst_pct': gst_pct,
|
| 95 |
+
'gst_value': row_gst,
|
| 96 |
+
'total_with_gst': row_total_with_gst
|
| 97 |
})
|
| 98 |
+
|
| 99 |
except (KeyError, ValueError) as e:
|
| 100 |
print(f"Error processing product row {row}: {e}")
|
| 101 |
continue
|
| 102 |
|
|
|
|
|
|
|
|
|
|
| 103 |
c = canvas.Canvas(filename, pagesize=letter)
|
| 104 |
width, height = letter
|
| 105 |
|
|
|
|
| 118 |
height=LOGO_HEIGHT,
|
| 119 |
preserveAspectRatio=True
|
| 120 |
)
|
| 121 |
+
|
| 122 |
c.setFont("Helvetica-Bold", 16)
|
| 123 |
c.drawString(50, height - 70, "PROFORMA INVOICE")
|
| 124 |
|
|
|
|
| 125 |
c.setFont("Helvetica", 10)
|
| 126 |
c.drawString(50, height - 100, "Consignor:")
|
| 127 |
c.drawString(120, height - 100, "True Vybes")
|
|
|
|
| 130 |
c.drawString(50, height - 145, "Thane - 400607")
|
| 131 |
c.drawString(50, height - 160, "GSTIN: 27BV0PS7767J2ZF")
|
| 132 |
|
|
|
|
| 133 |
c.drawString(50, height - 190, "Consignee:")
|
| 134 |
c.drawString(120, height - 190, customer_name)
|
| 135 |
+
|
|
|
|
| 136 |
address_lines = address.split('\n')
|
| 137 |
for i, line in enumerate(address_lines):
|
| 138 |
c.drawString(120, height - 205 - (i * 15), line)
|
|
|
|
| 140 |
c.drawString(120, height - 205 - (len(address_lines) * 15), f"GSTIN: {gstin}")
|
| 141 |
c.drawString(400, height - 190, f"Date: {datetime.today().strftime('%d-%b-%Y')}")
|
| 142 |
|
| 143 |
+
# Table header
|
| 144 |
table_top = height - 250 - (len(address_lines) * 15)
|
| 145 |
c.line(50, table_top, width - 50, table_top)
|
| 146 |
+
c.setFont("Helvetica-Bold", 9)
|
| 147 |
c.drawString(50, table_top - 15, "Description")
|
| 148 |
+
c.drawString(170, table_top - 15, "Code")
|
| 149 |
+
c.drawString(240, table_top - 15, "Qty")
|
| 150 |
+
c.drawString(280, table_top - 15, "Rate/Unit")
|
| 151 |
+
c.drawString(360, table_top - 15, "GST %")
|
| 152 |
+
c.drawString(410, table_top - 15, "GST Value")
|
| 153 |
+
c.drawString(480, table_top - 15, "Total w/ GST")
|
| 154 |
c.line(50, table_top - 20, width - 50, table_top - 20)
|
| 155 |
|
|
|
|
| 156 |
# Product rows
|
| 157 |
+
c.setFont("Helvetica", 9)
|
| 158 |
row_y = table_top - 35
|
| 159 |
+
|
| 160 |
for product in product_rows:
|
| 161 |
c.drawString(50, row_y, product['description'])
|
| 162 |
+
c.drawString(170, row_y, product['code'])
|
| 163 |
+
c.drawString(240, row_y, str(product['qty']))
|
| 164 |
+
c.drawString(280, row_y, format_inr(product['rate']))
|
| 165 |
+
c.drawString(360, row_y, f"{product['gst_pct']}%")
|
| 166 |
+
c.drawString(410, row_y, format_inr(product['gst_value']))
|
| 167 |
+
c.drawString(480, row_y, format_inr(product['total_with_gst']))
|
| 168 |
row_y -= 20
|
| 169 |
|
| 170 |
+
# Final grand total
|
| 171 |
c.line(350, row_y - 10, width - 50, row_y - 10)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
c.setFont("Helvetica-Bold", 10)
|
| 173 |
+
c.drawString(400, row_y - 25, "GRAND TOTAL")
|
| 174 |
+
c.drawString(480, row_y - 25, format_inr(grand_total))
|
| 175 |
|
| 176 |
+
# Bank & Terms
|
| 177 |
+
bank_y = row_y - 60
|
| 178 |
c.setFont("Helvetica", 10)
|
| 179 |
c.drawString(50, bank_y, "Bank Details For True Vybes")
|
| 180 |
c.drawString(50, bank_y - 15, "A/C Name: True Vybes")
|
|
|
|
| 182 |
c.drawString(50, bank_y - 45, "A/C Type: Current")
|
| 183 |
c.drawString(50, bank_y - 60, "IFSC Code: KKBK0000656")
|
| 184 |
|
|
|
|
| 185 |
terms_y = bank_y - 90
|
| 186 |
c.drawString(50, terms_y, "Other terms and conditions:")
|
| 187 |
c.drawString(50, terms_y - 15, "GST: Extra at actuals")
|
|
|
|
| 190 |
c.drawString(50, terms_y - 60, "Validity: The validity of this price is 10 days from today")
|
| 191 |
c.drawString(50, terms_y - 75, "Freight: Extra on To-Pay basis")
|
| 192 |
|
|
|
|
| 193 |
c.line(width - 150, terms_y - 90, width - 50, terms_y - 90)
|
| 194 |
c.drawString(width - 140, terms_y - 105, "Authorised Signatory")
|
| 195 |
|
| 196 |
c.save()
|
| 197 |
|
|
|
|
| 198 |
send_invoice_email(filename)
|
| 199 |
|
| 200 |
return filename
|
| 201 |
|
|
|
|
| 202 |
|
| 203 |
def send_invoice_email(pdf_filename):
|
| 204 |
SENDGRID_API_KEY = os.getenv("SENDGRID_API_KEY")
|
|
|
|
| 206 |
raise RuntimeError("SENDGRID_API_KEY is not set in your environment")
|
| 207 |
|
| 208 |
message = Mail(
|
| 209 |
+
from_email="truevybescg@gmail.com",
|
| 210 |
to_emails="truevybescg@gmail.com",
|
| 211 |
subject="New Proforma Invoice",
|
| 212 |
html_content="<p>Dear True Vybes Team,<br><br>Please find attached the new Proforma Invoice.<br><br>Regards,<br>Automated System</p>"
|
|
|
|
| 230 |
except Exception as e:
|
| 231 |
print(f"Error sending email: {e}")
|
| 232 |
|
| 233 |
+
|
| 234 |
# Gradio Interface
|
| 235 |
with gr.Blocks() as demo:
|
| 236 |
gr.Markdown("# 📄 True Vybes NLP Proforma Invoice Generator")
|
| 237 |
|
| 238 |
user_prompt = gr.Textbox(
|
| 239 |
label="Proforma Invoice Request",
|
| 240 |
+
placeholder="E.g. Create PI for Customer, GST-18902801280128, Addresss -Andheri Mumbai, 300 candles at 20 each with 5% GST, 500 umbrella at 200 each with GST 18%",
|
| 241 |
lines=4
|
| 242 |
)
|
| 243 |
|