From 3f9709efef4401d50f93e5ddfb8b14386ba04c40 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 3 Feb 2026 10:34:50 +0000 Subject: [PATCH] enhancing items form --- core/__pycache__/models.cpython-311.pyc | Bin 36288 -> 36983 bytes core/__pycache__/views.cpython-311.pyc | Bin 127266 -> 130082 bytes core/edit_product_fixed.py | 35 +++ ...expiry_date_product_has_expiry_and_more.py | 33 ++ .../0021_product_min_stock_level.py | 18 ++ ..._alter_product_min_stock_level_and_more.py | 53 ++++ ..._alter_product_min_stock_level_and_more.py | 53 ++++ ...roduct_has_expiry_and_more.cpython-311.pyc | Bin 0 -> 1348 bytes ...21_product_min_stock_level.cpython-311.pyc | Bin 0 -> 898 bytes ...t_min_stock_level_and_more.cpython-311.pyc | Bin 0 -> 1980 bytes ...t_min_stock_level_and_more.cpython-311.pyc | Bin 0 -> 1996 bytes core/models.py | 21 +- core/templates/core/index.html | 16 + core/templates/core/inventory.html | 287 +++++++++++++++++- core/templates/core/invoice_create.html | 2 +- core/templates/core/invoice_detail.html | 2 +- core/templates/core/invoice_edit.html | 2 +- core/templates/core/pos.html | 6 +- core/templates/core/purchase_create.html | 10 +- core/templates/core/purchase_detail.html | 2 +- .../core/purchase_return_create.html | 2 +- .../core/purchase_return_detail.html | 2 +- core/templates/core/quotation_create.html | 2 +- core/templates/core/quotation_detail.html | 2 +- core/templates/core/sale_return_create.html | 2 +- core/templates/core/sale_return_detail.html | 2 +- core/views.py | 66 +++- core/views_patch.py | 35 +++ 28 files changed, 620 insertions(+), 33 deletions(-) create mode 100644 core/edit_product_fixed.py create mode 100644 core/migrations/0020_product_expiry_date_product_has_expiry_and_more.py create mode 100644 core/migrations/0021_product_min_stock_level.py create mode 100644 core/migrations/0022_alter_product_min_stock_level_and_more.py create mode 100644 core/migrations/0023_alter_product_min_stock_level_and_more.py create mode 100644 core/migrations/__pycache__/0020_product_expiry_date_product_has_expiry_and_more.cpython-311.pyc create mode 100644 core/migrations/__pycache__/0021_product_min_stock_level.cpython-311.pyc create mode 100644 core/migrations/__pycache__/0022_alter_product_min_stock_level_and_more.cpython-311.pyc create mode 100644 core/migrations/__pycache__/0023_alter_product_min_stock_level_and_more.cpython-311.pyc create mode 100644 core/views_patch.py diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 27ce5b1854dc80a131cdbed19b1db881942f3a38..125d3366e9871805d9f878dfba41731aad77c906 100644 GIT binary patch delta 6048 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 delta 5319 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 Ph_Riw{Zsr{cPak|t*Qf+ diff --git a/core/edit_product_fixed.py b/core/edit_product_fixed.py new file mode 100644 index 0000000..f64efac --- /dev/null +++ b/core/edit_product_fixed.py @@ -0,0 +1,35 @@ +@login_required +def edit_product(request, pk): + product = get_object_or_404(Product, pk=pk) + if request.method == 'POST': + product.name_en = request.POST.get('name_en') + product.name_ar = request.POST.get('name_ar') + product.sku = request.POST.get('sku') + product.category = get_object_or_404(Category, id=request.POST.get('category')) + + unit_id = request.POST.get('unit') + product.unit = get_object_or_404(Unit, id=unit_id) if unit_id else None + + supplier_id = request.POST.get('supplier') + product.supplier = get_object_or_404(Supplier, id=supplier_id) if supplier_id else None + + product.cost_price = request.POST.get('cost_price', 0) + product.price = request.POST.get('price', 0) + product.vat = request.POST.get('vat', 0) + product.description = request.POST.get('description', '') + product.opening_stock = request.POST.get('opening_stock', 0) + product.stock_quantity = request.POST.get('stock_quantity', 0) + product.min_stock_level = request.POST.get('min_stock_level', 0) + product.is_active = request.POST.get('is_active') == 'on' + product.has_expiry = request.POST.get('has_expiry') == 'on' + product.expiry_date = request.POST.get('expiry_date') + if not product.has_expiry: + product.expiry_date = None + + if 'image' in request.FILES: + product.image = request.FILES['image'] + + product.save() + messages.success(request, _("Product updated successfully!")) + return redirect(reverse('inventory') + '#items') + return redirect(reverse('inventory') + '#items') diff --git a/core/migrations/0020_product_expiry_date_product_has_expiry_and_more.py b/core/migrations/0020_product_expiry_date_product_has_expiry_and_more.py new file mode 100644 index 0000000..c803b72 --- /dev/null +++ b/core/migrations/0020_product_expiry_date_product_has_expiry_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.7 on 2026-02-03 10:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0019_systemsetting_wablas_secret_key'), + ] + + operations = [ + migrations.AddField( + model_name='product', + name='expiry_date', + field=models.DateField(blank=True, null=True, verbose_name='Expiry Date'), + ), + migrations.AddField( + model_name='product', + name='has_expiry', + field=models.BooleanField(default=False, verbose_name='Has Expiry Date'), + ), + migrations.AddField( + model_name='purchaseitem', + name='expiry_date', + field=models.DateField(blank=True, null=True, verbose_name='Expiry Date'), + ), + migrations.AddField( + model_name='purchasereturnitem', + name='expiry_date', + field=models.DateField(blank=True, null=True, verbose_name='Expiry Date'), + ), + ] diff --git a/core/migrations/0021_product_min_stock_level.py b/core/migrations/0021_product_min_stock_level.py new file mode 100644 index 0000000..327af1e --- /dev/null +++ b/core/migrations/0021_product_min_stock_level.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2026-02-03 10:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0020_product_expiry_date_product_has_expiry_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='product', + name='min_stock_level', + field=models.PositiveIntegerField(default=0, verbose_name='Stock Level (Alert)'), + ), + ] diff --git a/core/migrations/0022_alter_product_min_stock_level_and_more.py b/core/migrations/0022_alter_product_min_stock_level_and_more.py new file mode 100644 index 0000000..f1d7e9c --- /dev/null +++ b/core/migrations/0022_alter_product_min_stock_level_and_more.py @@ -0,0 +1,53 @@ +# Generated by Django 5.2.7 on 2026-02-03 10:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0021_product_min_stock_level'), + ] + + operations = [ + migrations.AlterField( + model_name='product', + name='min_stock_level', + field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='Stock Level (Alert)'), + ), + migrations.AlterField( + model_name='product', + name='opening_stock', + field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='Opening Stock'), + ), + migrations.AlterField( + model_name='product', + name='stock_quantity', + field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='In Stock'), + ), + migrations.AlterField( + model_name='purchaseitem', + name='quantity', + field=models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Quantity'), + ), + migrations.AlterField( + model_name='purchasereturnitem', + name='quantity', + field=models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Quantity'), + ), + migrations.AlterField( + model_name='quotationitem', + name='quantity', + field=models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Quantity'), + ), + migrations.AlterField( + model_name='saleitem', + name='quantity', + field=models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Quantity'), + ), + migrations.AlterField( + model_name='salereturnitem', + name='quantity', + field=models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Quantity'), + ), + ] diff --git a/core/migrations/0023_alter_product_min_stock_level_and_more.py b/core/migrations/0023_alter_product_min_stock_level_and_more.py new file mode 100644 index 0000000..c53fd41 --- /dev/null +++ b/core/migrations/0023_alter_product_min_stock_level_and_more.py @@ -0,0 +1,53 @@ +# Generated by Django 5.2.7 on 2026-02-03 10:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0022_alter_product_min_stock_level_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='product', + name='min_stock_level', + field=models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='Stock Level (Alert)'), + ), + migrations.AlterField( + model_name='product', + name='opening_stock', + field=models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='Opening Stock'), + ), + migrations.AlterField( + model_name='product', + name='stock_quantity', + field=models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='In Stock'), + ), + migrations.AlterField( + model_name='purchaseitem', + name='quantity', + field=models.DecimalField(decimal_places=2, max_digits=15, verbose_name='Quantity'), + ), + migrations.AlterField( + model_name='purchasereturnitem', + name='quantity', + field=models.DecimalField(decimal_places=2, max_digits=15, verbose_name='Quantity'), + ), + migrations.AlterField( + model_name='quotationitem', + name='quantity', + field=models.DecimalField(decimal_places=2, max_digits=15, verbose_name='Quantity'), + ), + migrations.AlterField( + model_name='saleitem', + name='quantity', + field=models.DecimalField(decimal_places=2, max_digits=15, verbose_name='Quantity'), + ), + migrations.AlterField( + model_name='salereturnitem', + name='quantity', + field=models.DecimalField(decimal_places=2, max_digits=15, verbose_name='Quantity'), + ), + ] diff --git a/core/migrations/__pycache__/0020_product_expiry_date_product_has_expiry_and_more.cpython-311.pyc b/core/migrations/__pycache__/0020_product_expiry_date_product_has_expiry_and_more.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..30269246d4fe83c8baa1cf907e22dba3c1d4142b GIT binary patch literal 1348 zcmb_czi-n(6h1qS6T1lotB9erv{j3Ukea|&sY0SE1tI9tl)=l%@x7)_9ow93+EhX+ z5(8s5{)19P2mS-Fx4=@1F85pVyIiKEHC( z3PN8J&@<`Y_jsN=ZxKO6a#39}rMhg&5|YtFMC8|qq(G{r`hk1g`!GOeiYP0nkp3!` z3o_H|PL*1b<9i`f>plTDwEL0bo9lt&SxHa=G9`jcnMhU1Ovwn8TE8exO~i~^%k&cv zg^%=sdcK5%%z1nvOE$@p9c0mnelWBr^JDP0`}`qC?!Q!$|F`Xsg^HUwLNLmQY_5tO%w z9wcaDk)uM2rFodQ!4xlcU6y+RwBm<=J*zGj_2f1a#CPyHmR@tfB}`xET-~%w+ry8n zaN@_TGRqKHvtl=*d9F3f1u?aG1US54X7mv+G^U=ITQ2A7|&}-87=vZ{XX|=fS z*P$3!VlRq|umyG;`Lwv{(~U5&Y$yt`i)YPUES2t-a8K3V=@U^0b7!hx%Om_06lQ}4 zRXJN?gcE+_W@j)uD$?dUDS1TQdD795cKwas%8s_LO|-9!w~eXYn=NB%f8^uhrds>^ z=y+d><$}UAQnS3OKT9gfT_!i!6>CgvOZML2-f4Ee+YtBYB!~Q81clGI>7*n{;sQ#A dCc1U;*HK3$wSz92UA=XA`baFC{9-lX{SKTcUBmzY literal 0 HcmV?d00001 diff --git a/core/migrations/__pycache__/0021_product_min_stock_level.cpython-311.pyc b/core/migrations/__pycache__/0021_product_min_stock_level.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..64c5b4605d7b9e5918cf4add9f697a415e7432ce GIT binary patch literal 898 zcmZuwzi-n(6uz?^$F@>IO{Id$5Fvz`p>;rv6;hX?qEe75fn*|`T;FTz)IY-64lPrV z7#O?tKPW%`;HO% zDL}a@Q)j%!ofAY6#XbtKjRVP+Fp|(?M5XtL%78m^S#-+1&ogApRM|m|$}f>_NSP6M zZRR9i6vmkvL=^nkEi1)Wn}TtRJ0}Q`jVZDvird(hB?L;RB*j}(5m7tUX$_(Zt(>W+ z5+}gl$vUh4O^Dy4QH@j5GyQ4Fg%eZUh(Y$S*?iC>T^7;QO$Z!zJ$6K>lfW#t@5E)? z2`LFgRdrgBT?@RB#7X2H5FdKrGeP6+jU5rGZ;42KdEEz=GyLQn5fjD zBU_iwR8=$dPw3dCQjju4H&yDy76cPh@9YFhpj05v@#VdH_xru~?%wm~Y*qt&RDZs+ zKBWNoEtK}?Y6*u^T=)nuz>*CdsVF&eQI>!Vo&zku2RK2ba-t2|<VW)1ClJagW|5SSW(J6 z?0DcL?!pRARn^@j4}!n(sHV>bb&f~7kmuHwc`_IBWUt86jeE}VD7d%UcQ%XvWCd@8 zrT<1)25yAq_VrjwYEi{^co7b=^vibj@ls5h9wqGF`1s@ms!$JyW`G>aMSkF!tH>tn z#Ad0Mf%SA)_Cq*v)iD@~8jViWb5}&0oSvEpD z;C*0hAZ(Saz-O6tLYF+BAlGn+p5Qjo7m>!2VvThzTg1jJ>s5$rxurP7t?a8<%|*Uh zcO3XyXt;qDY;LJDZp)IXgw$Lyd}0N}VQPB^Q|DWI^#sj|d_*c#v>=0R2qi&C-3XXn z3%wxf-I&KW?3l4zNT(a;^fXgZ6hA^tLx|@S+Jws)gt&8Cn1B{lK#t)$N_4on~xg8uY7FIlm6vCy@4Z;H1AZ8eNwD8uWtA2%_ zc|qKA;q0@4%Bggl^Z&o(uX ze(*u7tM?m&xklf^2F&k_)M5VH{CDZcQVj()81BKa4#Q?Dnbr;fpN_=T0nBv*-5T`v zpjU_9<-nm%pjCtAJy@>8@+H80qwnrtz{%R{#TrC=5Y-{N1orrPu#xM*hOP&DbOqRq zq(f!Qr3jPv|IDd7S|N4WF6` iNs@S>NO}#7p8qwKf}}LT`DsV1_YNNj!_fuS$nPH{j`*1X literal 0 HcmV?d00001 diff --git a/core/migrations/__pycache__/0023_alter_product_min_stock_level_and_more.cpython-311.pyc b/core/migrations/__pycache__/0023_alter_product_min_stock_level_and_more.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..26d8ee40bc3e23ba571bee6e3865275f72d8032e GIT binary patch literal 1996 zcmb_c%}*0S6rb6a?RIVX7*GyIQ;Z-R5=!t&j3h>bK@7oo=z+uLH4u|9Ee2 zWdQgkh1P0oG6!RV`2+|c&;c$iL02g$5GddUAj$_oQWREFEzqXmwvMGzilk?N(Y7bm zhQf8%E;BQl5k(RtqWrJDbA6lp;e?P=W-M6avezlu`gqS6WGl&_wR26_Iwb zNSd?}m1MTH&9q2Dyvf?mo=)nVAN@iJ-4 z_l4cQwD)UKXh1j({9E85L zh#k629c+38yV5w*wBQ|QO!h>#0w&2`hahX+GZJ2yl<>mfxInSJtVv3QiUJ7r zESm;QenGfCF7M^Jtd_}X=DBCd0mfTg7YES%n0x^TLJy8+iwnL>i_u){% trans "All stock levels are healthy!" %}

{% endif %} +
{% trans "Expired Items Alert" %}
+ {% if expired_count > 0 %} +
+ {% else %} +
+ +

{% trans "No expired items in stock." %}

+
+ {% endif %} + diff --git a/core/templates/core/inventory.html b/core/templates/core/inventory.html index 19f84ef..39f1e0e 100644 --- a/core/templates/core/inventory.html +++ b/core/templates/core/inventory.html @@ -59,6 +59,11 @@ {% trans "Units" %} +
@@ -127,8 +132,8 @@ {{ product.sku }} {{ product.category.name_ar }} / {{ product.category.name_en }} - - {{ product.stock_quantity }} {{ product.unit.short_name|default:"" }} + + {{ product.stock_quantity|floatformat:2 }} {{ product.unit.short_name|default:"" }} {{ site_settings.currency_symbol }}{{ product.cost_price|floatformat:3 }} @@ -154,6 +159,157 @@
+ + + + + + {% empty %} @@ -250,6 +406,71 @@ + + +
+
+
+
+
+
{% trans "Expired Items" %}
+

{{ expired_products.count }}

+
+
+
+
+
+
+
{% trans "Expiring within 30 days" %}
+

{{ expiring_soon_products.count }}

+
+
+
+
+ +
+
+ + + + + + + + + + + + {% for product in expired_products %} + + + + + + + + {% empty %} + {% endfor %} + {% for product in expiring_soon_products %} + + + + + + + + {% empty %} + {% if not expired_products %} + + + + {% endif %} + {% endfor %} + +
{% trans "Item" %}{% trans "SKU" %}{% trans "Stock" %}{% trans "Expiry Date" %}{% trans "Status" %}
{{ product.name_ar }} / {{ product.name_en }}{{ product.sku }}{{ product.stock_quantity|floatformat:2 }} {{ product.unit.short_name }}{{ product.expiry_date|date:"Y-m-d" }}{% trans "Expired" %}
{{ product.name_ar }} / {{ product.name_en }}{{ product.sku }}{{ product.stock_quantity|floatformat:2 }} {{ product.unit.short_name }}{{ product.expiry_date|date:"Y-m-d" }}{% trans "Expiring Soon" %}
{% trans "No expired or expiring items found." %}
+
+
+
@@ -363,6 +584,15 @@ {% endfor %} +
+ + +
@@ -373,7 +603,43 @@
- + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ @@ -440,7 +706,7 @@ const response = await fetch('{% url "add_category_ajax" %}', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', 'X-CSRFToken': '{{ csrf_token }}' }, body: JSON.stringify({ name_en: nameEn, name_ar: nameAr }) }); const data = await response.json(); @@ -465,7 +731,7 @@ const response = await fetch('{% url "add_unit_ajax" %}', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', 'X-CSRFToken': '{{ csrf_token }}' }, body: JSON.stringify({ name_en: nameEn, name_ar: nameAr, short_name: shortName }) }); const data = await response.json(); @@ -488,6 +754,15 @@ document.getElementById('saveUnit').onclick = () => saveUnit(false); document.getElementById('saveAndAddAnotherUnit').onclick = () => saveUnit(true); + // Expiry Date Toggle + const hasExpiryCheck = document.getElementById('hasExpiryCheck'); + const expiryDateDiv = document.getElementById('expiryDateDiv'); + if (hasExpiryCheck && expiryDateDiv) { + hasExpiryCheck.onchange = function() { + expiryDateDiv.style.display = this.checked ? 'block' : 'none'; + }; + } + // SKU Suggestion const suggestBtn = document.getElementById('suggestSkuBtn'); if (suggestBtn) { @@ -499,4 +774,4 @@ } }); -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/core/templates/core/invoice_create.html b/core/templates/core/invoice_create.html index 134a336..ed6b6d7 100644 --- a/core/templates/core/invoice_create.html +++ b/core/templates/core/invoice_create.html @@ -75,7 +75,7 @@ - + [[ currencySymbol ]][[ (item.price * item.quantity).toFixed(decimalPlaces) ]] diff --git a/core/templates/core/invoice_detail.html b/core/templates/core/invoice_detail.html index 4f96ac3..058b6dd 100644 --- a/core/templates/core/invoice_detail.html +++ b/core/templates/core/invoice_detail.html @@ -126,7 +126,7 @@
{{ item.product.name_en }}
{{ settings.currency_symbol }}{{ item.unit_price|floatformat:3 }} - {{ item.quantity }} + {{ item.quantity|floatformat:2 }} {{ settings.currency_symbol }}{{ item.line_total|floatformat:3 }} {% endfor %} diff --git a/core/templates/core/invoice_edit.html b/core/templates/core/invoice_edit.html index 64b76f3..9634dff 100644 --- a/core/templates/core/invoice_edit.html +++ b/core/templates/core/invoice_edit.html @@ -80,7 +80,7 @@ - + [[ currencySymbol ]][[ (parseFloat(item.price) * parseFloat(item.quantity)).toFixed(decimalPlaces) ]] diff --git a/core/templates/core/pos.html b/core/templates/core/pos.html index 32da2e9..2be11e6 100644 --- a/core/templates/core/pos.html +++ b/core/templates/core/pos.html @@ -598,11 +598,11 @@
${item.name_ar}
${item.name_en}
-
${currency} ${formatAmount(item.price)} x ${item.quantity}
+
${currency} ${formatAmount(item.price)} x ${parseFloat(item.quantity).toFixed(2)}
- ${item.quantity} + ${parseFloat(item.quantity).toFixed(2)}
@@ -850,7 +850,7 @@
${item.name_ar}
${item.name_en}
- ${item.qty} + ${parseFloat(item.qty).toFixed(2)} ${data.business.currency} ${formatAmount(item.total)} `; diff --git a/core/templates/core/purchase_create.html b/core/templates/core/purchase_create.html index 6769ba5..d01fae9 100644 --- a/core/templates/core/purchase_create.html +++ b/core/templates/core/purchase_create.html @@ -60,6 +60,7 @@ {% trans "Product" %} {% trans "Cost Price" %} + {% trans "Expiry Date" %} {% trans "Quantity" %} {% trans "Total" %} @@ -75,7 +76,10 @@ - + + + + [[ currencySymbol ]][[ (item.price * item.quantity).toFixed(decimalPlaces) ]] @@ -224,7 +228,8 @@ name_en: product.name_en, sku: product.sku, price: product.cost_price, - quantity: 1 + quantity: 1, + expiry_date: "" }); } this.searchQuery = ''; @@ -251,6 +256,7 @@ id: item.id, quantity: item.quantity, price: item.price, + expiry_date: item.expiry_date, line_total: item.price * item.quantity })), total_amount: this.subtotal, diff --git a/core/templates/core/purchase_detail.html b/core/templates/core/purchase_detail.html index 149e2b6..f91ae88 100644 --- a/core/templates/core/purchase_detail.html +++ b/core/templates/core/purchase_detail.html @@ -120,7 +120,7 @@
{{ item.product.name_en }}
{{ settings.currency_symbol }}{{ item.cost_price|floatformat:3 }} - {{ item.quantity }} + {{ item.quantity|floatformat:2 }} {{ settings.currency_symbol }}{{ item.line_total|floatformat:3 }} {% endfor %} diff --git a/core/templates/core/purchase_return_create.html b/core/templates/core/purchase_return_create.html index 1cbc84f..99efd1c 100644 --- a/core/templates/core/purchase_return_create.html +++ b/core/templates/core/purchase_return_create.html @@ -84,7 +84,7 @@ - + [[ currencySymbol ]][[ (item.price * item.quantity).toFixed(decimalPlaces) ]] diff --git a/core/templates/core/purchase_return_detail.html b/core/templates/core/purchase_return_detail.html index 7b44615..2e2961a 100644 --- a/core/templates/core/purchase_return_detail.html +++ b/core/templates/core/purchase_return_detail.html @@ -98,7 +98,7 @@
{{ item.product.name_en }}
{{ settings.currency_symbol }}{{ item.cost_price|floatformat:3 }} - {{ item.quantity }} + {{ item.quantity|floatformat:2 }} {{ settings.currency_symbol }}{{ item.line_total|floatformat:3 }} {% endfor %} diff --git a/core/templates/core/quotation_create.html b/core/templates/core/quotation_create.html index 3acd4f0..0450d79 100644 --- a/core/templates/core/quotation_create.html +++ b/core/templates/core/quotation_create.html @@ -75,7 +75,7 @@ - + [[ currencySymbol ]][[ (item.price * item.quantity).toFixed(decimalPlaces) ]] diff --git a/core/templates/core/quotation_detail.html b/core/templates/core/quotation_detail.html index b92dfcc..748ee79 100644 --- a/core/templates/core/quotation_detail.html +++ b/core/templates/core/quotation_detail.html @@ -142,7 +142,7 @@
{{ item.product.name_en }}
{{ settings.currency_symbol }}{{ item.unit_price|floatformat:3 }} - {{ item.quantity }} + {{ item.quantity|floatformat:2 }} {{ settings.currency_symbol }}{{ item.line_total|floatformat:3 }} {% endfor %} diff --git a/core/templates/core/sale_return_create.html b/core/templates/core/sale_return_create.html index 81e9e96..d605dc1 100644 --- a/core/templates/core/sale_return_create.html +++ b/core/templates/core/sale_return_create.html @@ -84,7 +84,7 @@ - + [[ currencySymbol ]][[ (item.price * item.quantity).toFixed(decimalPlaces) ]] diff --git a/core/templates/core/sale_return_detail.html b/core/templates/core/sale_return_detail.html index 88a9fe9..04197e7 100644 --- a/core/templates/core/sale_return_detail.html +++ b/core/templates/core/sale_return_detail.html @@ -98,7 +98,7 @@
{{ item.product.name_en }}
{{ settings.currency_symbol }}{{ item.unit_price|floatformat:3 }} - {{ item.quantity }} + {{ item.quantity|floatformat:2 }} {{ settings.currency_symbol }}{{ item.line_total|floatformat:3 }} {% endfor %} diff --git a/core/views.py b/core/views.py index 22c79d0..fb5606a 100644 --- a/core/views.py +++ b/core/views.py @@ -11,7 +11,7 @@ from django.urls import reverse import random import string from django.shortcuts import render, get_object_or_404, redirect -from django.db.models import Sum, Count, F +from django.db.models import Sum, Count, F, Q from django.db.models.functions import TruncDate, TruncMonth from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt @@ -42,6 +42,10 @@ def index(request): total_sales_amount = Sale.objects.aggregate(total=Sum('total_amount'))['total'] or 0 total_customers = Customer.objects.count() + # Expired Items Alert + today = timezone.now().date() + expired_count = Product.objects.filter(has_expiry=True, expiry_date__lt=today, stock_quantity__gt=0).count() + # Stock Alert (Low stock < 5) low_stock_products = Product.objects.filter(stock_quantity__lt=5) @@ -71,7 +75,7 @@ def index(request): 'total_sales_count': total_sales_count, 'total_sales_amount': total_sales_amount, 'total_customers': total_customers, - 'low_stock_products': low_stock_products, + 'low_stock_products': low_stock_products, 'expired_count': expired_count, 'recent_sales': recent_sales, 'chart_labels': json.dumps(chart_labels), 'chart_data': json.dumps(chart_data), @@ -81,17 +85,42 @@ def index(request): @login_required def inventory(request): products_list = Product.objects.all().select_related('category', 'unit', 'supplier').order_by('-created_at') + + # Filter by category + category_id = request.GET.get('category') + if category_id: + products_list = products_list.filter(category_id=category_id) + + # Search + search = request.GET.get('search') + if search: + products_list = products_list.filter( + Q(name_en__icontains=search) | + Q(name_ar__icontains=search) | + Q(sku__icontains=search) + ) + + # Expired items + today = timezone.now().date() + expired_products = Product.objects.filter(has_expiry=True, expiry_date__lt=today, stock_quantity__gt=0) + expiring_soon_products = Product.objects.filter(has_expiry=True, expiry_date__gte=today, expiry_date__lte=today + timedelta(days=30), stock_quantity__gt=0) + paginator = Paginator(products_list, 25) page_number = request.GET.get('page') products = paginator.get_page(page_number) + categories = Category.objects.all() suppliers = Supplier.objects.all() units = Unit.objects.all() + context = { 'products': products, 'categories': categories, 'suppliers': suppliers, - 'units': units + 'units': units, + 'expired_products': expired_products, + 'expiring_soon_products': expiring_soon_products, + 'today': today } return render(request, 'core/inventory.html', context) @@ -241,16 +270,24 @@ def create_purchase_api(request): for item in items: product = Product.objects.get(id=item['id']) + item_expiry = item.get('expiry_date') PurchaseItem.objects.create( purchase=purchase, product=product, quantity=item['quantity'], cost_price=item['price'], + expiry_date=item_expiry if item_expiry else None, line_total=item['line_total'] ) # Update Stock product.stock_quantity += int(item['quantity']) product.cost_price = item['price'] + + if item_expiry: + product.has_expiry = True + if not product.expiry_date or str(item_expiry) > str(product.expiry_date): + product.expiry_date = item_expiry + product.save() return JsonResponse({'success': True, 'purchase_id': purchase.id}) @@ -996,6 +1033,10 @@ def add_payment_method(request): name_en = request.POST.get('name_en') name_ar = request.POST.get('name_ar') is_active = request.POST.get('is_active') == 'on' + has_expiry = request.POST.get('has_expiry') == 'on' + expiry_date = request.POST.get('expiry_date') + if not has_expiry: + expiry_date = None PaymentMethod.objects.create(name_en=name_en, name_ar=name_ar, is_active=is_active) messages.success(request, _("Payment method added successfully!")) return redirect(reverse('settings') + '#payments') @@ -1105,9 +1146,14 @@ def add_product(request): cost_price = request.POST.get('cost_price', 0) price = request.POST.get('price', 0) vat = request.POST.get('vat', 0) + description = request.POST.get('description', '') opening_stock = request.POST.get('opening_stock', 0) stock_quantity = request.POST.get('stock_quantity', 0) is_active = request.POST.get('is_active') == 'on' + has_expiry = request.POST.get('has_expiry') == 'on' + expiry_date = request.POST.get('expiry_date') + if not has_expiry: + expiry_date = None category = get_object_or_404(Category, id=category_id) unit = get_object_or_404(Unit, id=unit_id) if unit_id else None @@ -1123,9 +1169,13 @@ def add_product(request): cost_price=cost_price, price=price, vat=vat, + description=description, opening_stock=opening_stock, stock_quantity=stock_quantity, - is_active=is_active + is_active=is_active, + has_expiry=has_expiry, + min_stock_level=request.POST.get('min_stock_level', 0), + expiry_date=expiry_date ) if 'image' in request.FILES: @@ -1153,9 +1203,15 @@ def edit_product(request, pk): product.cost_price = request.POST.get('cost_price', 0) product.price = request.POST.get('price', 0) product.vat = request.POST.get('vat', 0) + product.description = request.POST.get('description', '') product.opening_stock = request.POST.get('opening_stock', 0) product.stock_quantity = request.POST.get('stock_quantity', 0) + product.min_stock_level = request.POST.get('min_stock_level', 0) product.is_active = request.POST.get('is_active') == 'on' + product.has_expiry = request.POST.get('has_expiry') == 'on' + product.expiry_date = request.POST.get('expiry_date') + if not product.has_expiry: + product.expiry_date = None if 'image' in request.FILES: product.image = request.FILES['image'] @@ -1165,6 +1221,8 @@ def edit_product(request, pk): return redirect(reverse('inventory') + '#items') return redirect(reverse('inventory') + '#items') + return redirect(reverse('inventory') + '#items') + @login_required def delete_product(request, pk): product = get_object_or_404(Product, pk=pk) diff --git a/core/views_patch.py b/core/views_patch.py new file mode 100644 index 0000000..f64efac --- /dev/null +++ b/core/views_patch.py @@ -0,0 +1,35 @@ +@login_required +def edit_product(request, pk): + product = get_object_or_404(Product, pk=pk) + if request.method == 'POST': + product.name_en = request.POST.get('name_en') + product.name_ar = request.POST.get('name_ar') + product.sku = request.POST.get('sku') + product.category = get_object_or_404(Category, id=request.POST.get('category')) + + unit_id = request.POST.get('unit') + product.unit = get_object_or_404(Unit, id=unit_id) if unit_id else None + + supplier_id = request.POST.get('supplier') + product.supplier = get_object_or_404(Supplier, id=supplier_id) if supplier_id else None + + product.cost_price = request.POST.get('cost_price', 0) + product.price = request.POST.get('price', 0) + product.vat = request.POST.get('vat', 0) + product.description = request.POST.get('description', '') + product.opening_stock = request.POST.get('opening_stock', 0) + product.stock_quantity = request.POST.get('stock_quantity', 0) + product.min_stock_level = request.POST.get('min_stock_level', 0) + product.is_active = request.POST.get('is_active') == 'on' + product.has_expiry = request.POST.get('has_expiry') == 'on' + product.expiry_date = request.POST.get('expiry_date') + if not product.has_expiry: + product.expiry_date = None + + if 'image' in request.FILES: + product.image = request.FILES['image'] + + product.save() + messages.success(request, _("Product updated successfully!")) + return redirect(reverse('inventory') + '#items') + return redirect(reverse('inventory') + '#items')