From 9d3b9739fa6e04e8b16948223f8956359fdd68d6 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Fri, 6 Feb 2026 15:27:01 +0000 Subject: [PATCH] Autosave: 20260206-152701 --- core/__pycache__/models.cpython-311.pyc | Bin 45582 -> 45732 bytes core/__pycache__/views.cpython-311.pyc | Bin 157098 -> 152678 bytes core/models.py | 3 + core/templates/core/settings.html | 8 ++ core/views.py | 178 +++--------------------- 5 files changed, 29 insertions(+), 160 deletions(-) diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index cab57bd7151e2415f109c11ca0778b0e0887ad97..3b99c6564408fb96ec807f16e92cdfd526ffacad 100644 GIT binary patch delta 1040 zcmZWnT}TvB6yCe;j<)5R%DSSvIqMp_X{r5MieR7=S}&%Q6gD}du8uC;oe+NlAF{ME zg4{oaU zBY}ZB6^)P&4R)zyTEEz!KCU5;b|uN$TbXKPAlZ#(a=0l*Lzp_B!LeP_zXNQqW zHBOf|_&`3Gw~$u7(fU6xdI!(ITgkHC8n5MJd}{kA!iLpUl`d}-L@GBMNPLsx1ewTu zT&N+|r!S6YCUoRfkR?{j+k9#ae_$eh*pI5x^-nblRFV^Xn2L%IljRTIR25kGL%Awj zB6;7mb&|~xMMo9u6hzTpS;E>Sp~A^l*`-o8iFHbjqVlX7LuMs+O%&K1M>Qu()ogCH zld|NEI~+3g(1|&BQy=xJcR`_^Ad5S|f+Okqye*n=_a=>D9OxI;2-s1m=oLCSQE(D{ z^Ag&W-!?mtbzgEvzQ>$26+9K}K9aYZobO4_cV89U=HgXzvBzBO)4q!=UHJgv0PtvV z9_$ojb!4$OQP&4afF<&$_c!X3fA!JV*bmtO3KuZ^EWB-q@ftb-GARIFYd{*2X9gI= zxM1H!76voWN1_=ji%=5El^3A_z6+Pb?lEZ`x}uweGr(*4>(D$xlVp0N)&xy9KV>wy zk&@S#A}nx{KqoRw&WxVczMzD4(lYuZwF3m^fX}S?mvT-{lJIsTegb8M(?WHq<}4(h z0z4=4V=>wVy8R@78LLMric966D05M)!9^H2I08(=o53auKLgS#9Q<`rNXN@^|F}V$ aU_#N~R6C(H>p{+U$Zu^I)8+N4c=Q*oQ5n7f delta 901 zcmZuuUr1A76u;-rU1_vwjH_-rx7j3Ds|*Sj_EIo|GK1wqhLNJ&sW^i+lrK@WZBE)iBAoFC`U?|06*=T5DN{a-}O zeY4pl@V7h~Y58`3&axO0g*=?tRK<+f0wS2PHtNVXD_azoX2n>QjJ;8d>`sCpJm8jn zidm7BavZP}2W;y(Q3x58Z8v?^ zbaU|Lsr&e+tNRHi@z>Z_X_9I-Ts>|z(N)wL9=}3kB9$M{s<5!hf$tJdc!NI@0naF9 zC1Rc=C?QBwu7Ny#&2BtBzQ>fHB*9A@9RCdoc6%Zs7EMt09)}a^G`ZQ;&^KkzZjxkm zQxZrcREuNd^db0&Bh#&RJ)w7Rq&f64>P7MxmL<;=%u-D7mRkqI4<08=CC30}@yJYf zDXF~%{WxlrtCpm5YA6^E@q5hU^2}l5Gfwz~6{(-q!`xFJ6JJ`&zYy#QhQsQHP+g$1 z&WZ6pyyrY6o)A36Q)!Pe!`&6uo9+d>zOMdP^jqjV(jYa1cFh5=nO~EQJ{y#O7ygBT Vn!&nN%CBt{{n#^C#A0(Y`~iiK?fL)! diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index ff8faf4709ab40485d297aa8363f460e6fbede05..33b7d83f9df0d05599422451c1d541bfc6b53c7a 100644 GIT binary patch delta 11177 zcma)C34D`Pw$HgqzBEbqHrbpEo-6dYk`(c!3vbp1Z*j7xk*`SYSBSP5t-r# zTvU)E4g&}eG!H%yRGa~skpTii)gA3 zT8gO62el*G!v}T94O)v~o})WT)8s5e;;i||rTgeTfoO&gT83z*4_c0BmJhlB(O&Xk zT_j{vJ+TY1oa3WaiD+-RL1)SA<0Gp^SzjM?ks5nH@(EmS(uHRfIZ<(dkD?3FTpzSX zw(2eV#V8vjPtsfJ^L#XxqHM4ax(v}FK4>kXLw(RE5gq1(x)B}zAj*pBQtm!Xv6xgKG6C z@sF)V*>oR^mk^!dgUX0L=7X+7w8#ftFK78#^czq%Q=a2zsh{Peu@PmnebAQ?E%rfQ zL3EA}`YNI&KIkSy=RSzCqRsMU4Cpl!+T~zlBsi$~@r_uX_n>moR!1u0^GPG;b^8-5 zG)3F6T;`+q2BPIY=emdpYo9%Mp?ZNdc+P>HMZrR73M?} z1bB{FZs)V)K)H2SwAR42^4brUiius>v+{+3X&&v|=`6+_T<-7P@SUsNx{;b$zpV=~ zYFsg5STgIW4Y5bb4fA8;L*1d}fnG&dqL>UkuUMj4s(nne1nh>oFpMzNtHJxZ zgLofzpxv-QM;Om^2bG6;rS8B5dKFD}CCb+Fn7~eP28)KTAWFXcgkBrw>Orlu$W`T0 zX02QAmfQxn9~xbm8pH4_%h5G6%x!Y}bLRH11iQu^;MSB!c#WMuo~agFj?;*{(vTK9uRE-Onm-&e@@_C!>4xINh&VejS+FYoTPdR4QT z?d~prR@UYJ^eHxbAV|(Fw>;EwX1G1Y9cb3rQ+-|gbZ1awio7;ET(<5HX6~T&i4zj! z4N0T1+sKccwqVk1Tt*pQ0rHt%p8Iz!m1lWzn)db{oBTQkf_yKp9QkZ7-N?;`(!_m}=b@^11i{$%$wYE?Rs3?wZ4iYe+m}NNi>EnD}@!SUjz+ zT~OaDX~N^0!?Ig7hJg6DHk&nt^f`dJh88b523A<;P*yJ*d5&Erv;4}*po@#{qNu(WYT z_7^?F!e9J=s4-|~EQ8R^wF$E_vv-rP2~a zwldFq+y2hX%g76nJpsqUJ6)4tz4;HcI9VD0cLQt62tt~gB5A|_6-K^aIrSS=*J{Ys3 z$Le%$-Tl{CdHW`SXt3AnUMe))g>%;Xl!wT34n+$884wiN$wPZ~Q!9pLGi$u$hTX6o zJL*mRv^}Lv&76u{-P?nWC}rj0Ub&lLl$QIY`?*R-+-|V@VYxjo!IU7%YqQ(9-H0{f zr|qfJcWAdCwtHcn&Xs+?_IiECHgs$|^6l9j^08izyG=fCunzr*slh{gvKFH)WyKw3 zYTl)$f0WyFm$UYViL1mk-1dif>SX!OYgUo98qAX2->olCkl))M3juP={s~=NG()-) zp-IQjVs!ih5}VOGW%3RbWA3&2K(cJv6=Zmm6!JixxGm2774eNk;^n^{h#%37*l2Qd zl{j4A8g-B5Cy=vAypjOoc|9pQd2?8^xksx;ryqDeJbqo_ zC*dh)!&AcXPphoZ3@c06dbiKaO09a(9~nX znbe8RsS}$fO>3Ssvw4#J3|`}$33fCGJDP$Wmm(9Jtl9f`Q)KR$$lT`0+@|1rf312= z@WAg)ot~rvVZ$Kc;v+2lGW_k(P-q;Jy zW$Fgd^L+uZ4mW^E`NDev{&t6}q^yFkN2MX2e;qxfgMsp%>Q4M{?NCC#Rx=btijDn^9qz z)^q%18uNP*d7Fvjz0~$Ji6#|aBlkSrP1=I^O0DP7(?!g*75O#f(5;4iqv>^kZEDCH zn?mHwvyy?ofhsR+J;%--(=h>}*Aa*mTb6=e{_MCx-t&p6tpN$!P>p{<>pf6+O4cEHXN7|!0?_sFjzF*m+v7+|cL=F(??E1c2 zK8&P?k+kSXkDD-RD$gh2JtFTCd7sEpQN0tQL_-o7 zKr0d=S&V1WcikcJd`6$$6`y1bKASPPX~^_588ezQW;D&5b2eknb7PzRdwLfCeZ3A2 z%Kra4X8JQa$i(-U^O^`=33;B#TY{lz?+=-9LjL@Rdi_aM$n%u`coh&B{Zj^fB-i|u zsy~JBSkLP}#j`H=TTOktQ^)sp?%g!`^1tWz>a69Y1#YE>Qy$skUt+fxm)2mWztF*X z6Z$FiEWNXbnLC)tS$D^EEVuWHPHw!Lp+AR4MV@nauLGPFhXIoG|Aojr1q1&MLdnlg zZChyPk>}~+iWcTe_xJ)GSWPX6FQmTMDA(OdRT_1W&b$ieBpB!Ay`w?(28i(n=wiBr zrc20li+uj>AmwcXGyq(rFrvjwBN$Deqrx+&!2esRGQxa-Vu8DIsq{7Ct8~gxGhCF+ zSFpgZ68W1*35CIMP|OR3g2ZnLx3 za>AgObd30$?fqpEtHU72+`+5(C=8}c|3tO*Iwd6>3XM7UrRrk2U-93`nOZyE7~Tuv zbBg1p*clJ00j-GItelc19w(RF4U>QU!6g1259t~AeLl>c+8EW7PrOSLAO!v)RwY1p z=@$AwrBeXii#wdDgA)hzjVs!Bv`}XCxN3G8CF{2a?G#d%lN-h_%n^oCYs`d zls|QY#~J$>gf$h?QQJG5Mp2at8Kz%RU5C@txdVr?oATFGcoRr_Sr63yg@!aH!0TFr zub?31bPq^nCWd@%)FYK;E^#FpVnj?jOiv-uiFIr4Pjx?<>NTjw^(c&>nqH^C#wl0Q zVITu0;-L{gvm3K$&E`ja@noH->9sSQUhc zz|pXfYOWg%y-ew-w~9JpgjnE$H05ujVXYQBVR1er8?-2##?!>se5gPya6FDrCSqI3 zV4`Xu&>%@!Hy-R-v$~ku_I9Byge3S>bT0&#cilD?LbjBH<_Go4`9kOy41GlK9N6YZ zw*ofc1H|DuU^Ndw4fUdck5ayz16nYTBP5^51aYeb`dj-GpNqt1;~TNjE&gTBVrNaY zgV#85|LQM#<+3DEITsod)EftXr^8iTUE*{usp597R-oqexgxF<`ezi9JM!5Q;8Zrc&r-E+7e!(|X0<4zC$P)#!|E z^M?+SJXai90R5y{q?s#zS^(YjcI2~_t`%^SK~HhB5~4uu>kQGi3YM7Wp?DCU5S(K= z^t8SjR%^3pJLPImQ~T7n-)P>8T+^G9e@)~YA|KPx^K9{f6Xtf>yfa*oqkjS|rYiGX z@S_gO#q4D;O1}`Ha%IOdctRW6n|$XW!E?+fZEZ-Z11=>}Mdjj^I!J_S@lG9NN*tjT zl5(>S`ZCBB-IqfSdMsKFHmDFQmP5K!gBr4=D9d3GfJ@wZ8hW9q`!kSYT7sgj)X+R} zcsax>{26$X1%E)bqeR0;5_d@avI273P!(8)GF~gjJPY;kfjIFj43VBf=nsLL}M7{_qte2Z&vQoHp$W7z#5PJj(Uf5lurt~1-Y0?@-++k7mJ4nnKL3|_0 zw-PBJ(lIzJ;t^QGhmzhs!w-H31=4TP(`iYuG(ei(qD}>x87Cshj9^t)o%yRt_eHUD10+ce$X}C` zGaF!w9yW+Yo1joH5DQk0Z-Uu6xFM3a;<`{@nujR#m&Kf|V3RhWjxAKKo{)%}+TN`& zxlQFMElP?CBFU?k}bOs;^U|)x?SRaRP!E@)B-!;usGR*mzgj6 ziO>s>Vt$w8?-3awMqYr=K!~mvVXXNt1gY0+ABn{mp+8QRy%%ADz8UcXCGip*3aD2| zN4=XHL{+JG(~KV^{ve4iA&0wuZQGZMKNu6L)do+h;iXTMI30L@zPPHa)WHvt^*baB zYL`uMxN3MMf1gnQNhQk`I>hvEVPxzFr29uAe?sEKmTKBkUVdC0_!exrpZlTgLsFPZ zk)0r}L#xri=Et8S5e3TChJQp#Cq?uP*pw`&^L`s>%iXu9Q3DzO0^`DA&`Bt@;DqXdbTI$Nx-5$jnn}JUttHWe) zM2!76JQw@$E+l7N#Oz-n#;>Ae;aq#k-~l4w4#dKrh3yWEH(W%80{%zwR|f#SnE zu-JO34I|%IkT~(9xpxfVSFs?r-hpIH6QBMC`T+b@D8It^MA|m^--u8Tv}hemtNAr5 zeo5rI=yDfdt=}R4Pd{b$U6>+4x_C>^*7{G!PAuS$(J-3DzI;d&3nk{yZZa{s3Zg^Q zJ@6*^PY4YAio4H18cM!-=-APeCFtelDn1>qE7#tL@2CIG@VY!lo2ivtqWu}EGAo*colTnF|WaSKIh!xRnCisgh(JW62LeVy(k{rWY z08HX;EQ`a$I4+L$mBLWuF)B0SSOmk}xhjG6mLd^5YE*V6uw!6~#=@VeJ079CH97AO z24$a>%?1{$r7bMfR8d`pU8mj(#-Z*RqtZW#y`!@vV4-byQP*7}6|KUM!mcE%qK~Qf z)ZM3poRriR7%Ti!*+3~7feS`uVk*0(hv&rb4E7i#iA@=-65@n8lkI_6aVV4Z)u*7M zLAjO5ELzkv<*=l1il)|o!lXj0-BICiIqVrcQVh*uL(pb<4vRP2P$38Z)6V&dIVNHm^=j_ zX}is~(xcug@K-5fA~nO(=KkbH?T8_wI-gZ| zg>xohwbFPNEYYQ|Y*OOJGlwp26c+H8MDD<|AZF{X;^GRIqiw)Pi_L|sH%`H`g)9xm zh`WU>NjDbt#qNBTAVy4LkrKs=FKcw(%{-ELBKu8B!z4Bqq{nINoh3e-%pzcp_0Cwar#Y+)6Zb)PhY%_xaCAv z5}})Z{sNH(B6K0HUU+l5xaOOQY$39h$QwlHN{;U!Lf5tYI1#!y;wOomBGLr)Na+rS z)3pnyyA)2hBAjkMI9+pax?kXQeCKptSI^~~PTQQ0(ducK(?OP-h|pn`+lbH!k<+P< z=MYhkI&^a3bUxv9gy3{&sOOwmO7r;=B9x5tXNXX)%qdmnls0lfgt9zNi5#c=jK5BV zk{nJs4S$;mWiFgD3Qn&&PTM=DOl(r&Hn+~U4hSToCX`ymj z>6}(Er)8;5{1!dFbr(8$KLXU*N0Yv7($Z8@rv$#dDj#K0=2-|}S81Gg0+qFovR8F+ zPcWDats3>~A6g9ySP|RG+Q2Dd_T%j1h;+bTp^=a@ctt`h&Z6wr*~*BStO6qXL2j!C PNxP5$t{k7mt{MIZ?P;_< delta 11581 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 U2wmzmC>luuDwXOX?3(d^0K*c^7ytkO diff --git a/core/models.py b/core/models.py index be90b4d..eac88e7 100644 --- a/core/models.py +++ b/core/models.py @@ -396,6 +396,9 @@ class SystemSetting(models.Model): vat_number = models.CharField(_("VAT Number"), max_length=50, blank=True) registration_number = models.CharField(_("Registration Number"), max_length=50, blank=True) + # Financial Settings + allow_zero_stock_sales = models.BooleanField(_("Allow selling items with 0 stock"), default=False) + # Loyalty Settings loyalty_enabled = models.BooleanField(_("Enable Loyalty System"), default=False) points_per_currency = models.DecimalField(_("Points Earned per Currency Unit"), max_digits=10, decimal_places=2, default=1.0) diff --git a/core/templates/core/settings.html b/core/templates/core/settings.html index 5c16a86..19810d1 100644 --- a/core/templates/core/settings.html +++ b/core/templates/core/settings.html @@ -120,6 +120,14 @@
{% trans "For price display" %}
+
+
+ + +
{% trans "If enabled, sales can be processed even if product stock is 0 or less." %}
+
+
+
{% trans "Loyalty Configuration" %}
diff --git a/core/views.py b/core/views.py index fdaf73a..85890aa 100644 --- a/core/views.py +++ b/core/views.py @@ -467,166 +467,15 @@ def create_sale_api(request): if not settings: settings = SystemSetting.objects.create() - loyalty_discount = 0 - if settings.loyalty_enabled and customer and points_to_redeem > 0: - if customer.loyalty_points >= points_to_redeem: - loyalty_discount = float(points_to_redeem) * float(settings.currency_per_point) - - sale = Sale.objects.create( - customer=customer, - invoice_number=invoice_number, - subtotal=subtotal, - vat_amount=vat_amount, - total_amount=total_amount, - paid_amount=paid_amount, - balance_due=float(total_amount) - float(paid_amount), - discount=discount, - loyalty_points_redeemed=points_to_redeem, - loyalty_discount_amount=loyalty_discount, - payment_type=payment_type, - due_date=due_date if due_date else None, - notes=notes, - created_by=request.user - ) - - # Set status based on payments - if float(paid_amount) >= float(total_amount): - sale.status = 'paid' - elif float(paid_amount) > 0: - sale.status = 'partial' - else: - sale.status = 'unpaid' - sale.save() - - # Record initial payment if any - if float(paid_amount) > 0: - pm = None - if payment_method_id: - pm = PaymentMethod.objects.filter(id=payment_method_id).first() - - SalePayment.objects.create( - sale=sale, - amount=paid_amount, - payment_method=pm, - payment_method_name=pm.name_en if pm else payment_type.capitalize(), - notes="Initial payment", - created_by=request.user - ) - - for item in items: - product = Product.objects.get(id=item['id']) - SaleItem.objects.create( - sale=sale, - product=product, - quantity=item['quantity'], - unit_price=item['price'], - line_total=item['line_total'] - ) - product.stock_quantity -= int(item['quantity']) - product.save() - - # Handle Loyalty Points - if settings.loyalty_enabled and customer: - # Earn Points - points_earned = float(total_amount) * float(settings.points_per_currency) - if customer.loyalty_tier: - points_earned *= float(customer.loyalty_tier.point_multiplier) - - if points_earned > 0: - customer.loyalty_points += decimal.Decimal(str(points_earned)) - LoyaltyTransaction.objects.create( - customer=customer, - sale=sale, - transaction_type='earned', - points=points_earned, - notes=f"Points earned from Sale #{sale.id}" - ) - - # Redeem Points - if points_to_redeem > 0: - customer.loyalty_points -= decimal.Decimal(str(points_to_redeem)) - LoyaltyTransaction.objects.create( - customer=customer, - sale=sale, - transaction_type='redeemed', - points=-points_to_redeem, - notes=f"Points redeemed for Sale #{sale.id}" - ) - - customer.update_tier() - customer.save() - - return JsonResponse({ - 'success': True, - 'sale_id': sale.id, - 'business': { - 'name': settings.business_name, - 'address': settings.address, - 'phone': settings.phone, - 'email': settings.email, - 'currency': settings.currency_symbol, - 'vat_number': settings.vat_number, - 'registration_number': settings.registration_number, - 'logo_url': settings.logo.url if settings.logo else None - }, - 'sale': { - 'id': sale.id, - 'invoice_number': sale.invoice_number, - 'created_at': sale.created_at.strftime("%Y-%m-%d %H:%M"), - 'subtotal': float(sale.subtotal), - 'vat_amount': float(sale.vat_amount), - 'total': float(sale.total_amount), - 'discount': float(sale.discount), - 'paid': float(sale.paid_amount), - 'balance': float(sale.balance_due), - 'customer_name': sale.customer.name if sale.customer else 'Guest', - 'items': [ - { - 'name_en': si.product.name_en, - 'name_ar': si.product.name_ar, - 'qty': si.quantity, - 'price': float(si.unit_price), - 'total': float(si.line_total) - } for si in sale.items.all() - ] - } - }) - except Exception as e: - return JsonResponse({'success': False, 'error': str(e)}, status=400) -def create_sale_api(request): - if request.method == 'POST': - try: - data = json.loads(request.body) - customer_id = data.get('customer_id') - invoice_number = data.get('invoice_number', '') - items = data.get('items', []) - - # Retrieve amounts - subtotal = data.get('subtotal', 0) - vat_amount = data.get('vat_amount', 0) - total_amount = data.get('total_amount', 0) - - paid_amount = data.get('paid_amount', 0) - discount = data.get('discount', 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', '') - - # Loyalty data - points_to_redeem = data.get('loyalty_points_redeemed', 0) - - customer = None - if customer_id: - customer = Customer.objects.get(id=customer_id) - - if not customer and payment_type != 'cash': - return JsonResponse({'success': False, 'error': _('Credit or Partial payments are not allowed for Guest customers.')}, status=400) - - settings = SystemSetting.objects.first() - if not settings: - settings = SystemSetting.objects.create() - + # Check for stock availability if overselling is not allowed + if not settings.allow_zero_stock_sales: + for item in items: + try: + product = Product.objects.get(id=item["id"]) + if product.stock_quantity < float(item["quantity"]): + return JsonResponse({"success": False, "error": _("Insufficient stock for product: ") + product.name_en}, status=400) + except Product.DoesNotExist: + pass loyalty_discount = 0 if settings.loyalty_enabled and customer and points_to_redeem > 0: if customer.loyalty_points >= points_to_redeem: @@ -909,6 +758,14 @@ def convert_quotation_to_invoice(request, pk): if quotation.status == 'converted': messages.warning(request, _("This quotation has already been converted to an invoice.")) return redirect('invoices') + + # Check stock before converting + settings = SystemSetting.objects.first() or SystemSetting.objects.create() + if not settings.allow_zero_stock_sales: + for item in quotation.items.all(): + if item.product.stock_quantity < item.quantity: + messages.error(request, _("Insufficient stock for product: ") + item.product.name_en) + return redirect('quotation_detail', pk=pk) # Create Sale from Quotation sale = Sale.objects.create( @@ -1173,6 +1030,7 @@ def settings_view(request): settings.decimal_places = request.POST.get("decimal_places", 3) settings.vat_number = request.POST.get("vat_number", "") settings.registration_number = request.POST.get("registration_number", "") + settings.allow_zero_stock_sales = request.POST.get("allow_zero_stock_sales") == "on" settings.loyalty_enabled = request.POST.get("loyalty_enabled") == "on" settings.points_per_currency = request.POST.get("points_per_currency", 1.0)