From 42e23933473bc945a3e43cdac9eff7257834ccef Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Fri, 6 Feb 2026 12:25:31 +0000 Subject: [PATCH] adding imprt export categ, suppliers --- core/__pycache__/forms_import.cpython-311.pyc | Bin 0 -> 569 bytes core/__pycache__/models.cpython-311.pyc | Bin 45442 -> 45582 bytes core/__pycache__/urls.cpython-311.pyc | Bin 12847 -> 13138 bytes core/__pycache__/views_import.cpython-311.pyc | Bin 0 -> 6186 bytes core/forms_import.py | 4 + core/models.py | 2 + core/templates/core/import_categories.html | 54 +++++++ core/templates/core/import_suppliers.html | 55 +++++++ core/templates/core/inventory.html | 5 +- core/templates/core/suppliers.html | 46 +++++- core/urls.py | 5 +- core/views_import.py | 139 ++++++++++++++++++ patch_models_timestamp.py | 43 ++++++ 13 files changed, 347 insertions(+), 6 deletions(-) create mode 100644 core/__pycache__/forms_import.cpython-311.pyc create mode 100644 core/__pycache__/views_import.cpython-311.pyc create mode 100644 core/forms_import.py create mode 100644 core/templates/core/import_categories.html create mode 100644 core/templates/core/import_suppliers.html create mode 100644 core/views_import.py create mode 100644 patch_models_timestamp.py diff --git a/core/__pycache__/forms_import.cpython-311.pyc b/core/__pycache__/forms_import.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..200f5f2eb9dd65f116b881bfc06cf96987f393cf GIT binary patch literal 569 zcmZuty-ve05I!eqRr)8vgqVsD5?vYwS{M*4P{cqN#FE8IV;5SSrZ|l#OdUXCU|>M% z8x$dU3tpf|3{_SpwxTk1!cH0_1ZVqvpYQzL&qF4Y1R={W*L7Fi?-E23n;PQ{Vf26i zK@4nUAOs5R0a31is1!An=tExxcoHWRLnYcFJ)mG9 z0*0ahRU29qNYJy=M}k?LP>dK+1+(U7_wAO$+vPf=WkG&9x7)EO%SoJDDRh|I$;-sd ztWs9V$NdDxP1B|r`$>##hj>idX^hW2lZ75GYw~9T$~tAl*J^_85ymq0WKBPDY^r-z zui5r=+M$-$cDR1-@Kd*CT2!|jPW7NsTn}n1v@WdsuM=qX;epN)LandlG$4*$CCYude(=sTC%nECg*bKYc;1ljZ}ot c5atKJ_eeu%eFT5i-O;`ADc`^TaR%wW0g!WtE&u=k literal 0 HcmV?d00001 diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 747a1b1e5000430f8db5869ddb2729982875fa0e..cab57bd7151e2415f109c11ca0778b0e0887ad97 100644 GIT binary patch delta 688 zcmbV}T}YF06vp4@efOc0^<_nyVG*)W$|3`Wpo^IJF=>XTwsIkDLR-$Y851UDxyYEs z;EW)(ROmy=iaJ8ED`j3tB}%thN|y9twWy0ATD`AIbm3J8&U5iU=Q)Sp`JbB=FTWA> zxAl6RAjhlj^T)qD&{H9XxUVpbTv2dJt_?~_2}kPSnp+TrJ8~|D&EQB62{xl+ZH17% zjtYv-(mP)UO%!!tjE)pKh9WI$8?*RiBmWEWA zrYHM<;VF+dK;5<3^t{4~*Sw|jg$hYZ)_l{<%S-)H8n^CXldVZY3U$?n0N!j5 ztC7Wn$A*BdROcK;8k_1nRoFu#u6E>csXG<_iS)6-yM&Za{hos;rlco#2}?)AK{X{@ zDk^WRQt(!p<;(aK*(;|%4^-~nNOO&gpp)LXh)Rn3$F&xvtXA$|pwH=AZg2V|!YbC)J**;LC+MNb762kHtF-5n1U8sHkx5G$@yXzm` clOUVR7R<|Oq&|8=F_%Fx|Lrhx{(X!12MU7d9RL6T delta 589 zcmZvWT}V>_6vyxX-aD62tw41IpbYyJBJ%YhVC48n_%N1lfcyd|B z8hOtC#22piO-is+_M8rZ1MLYLcJYsnS-?#b7Z;##S721eA#(Lq&_1ml`#Gf*)kZaeY8M-5Ot)5_FC#Lnhp6aeUGVc#$jV)JGdq+S^DQAWgL!7 zi#S0$dzR3^xtz}ain5@LpK5*k16 z^GmovioSq2ztiJ(Jmk8;J3v3bzx&FL7o51S0FQZOBrjo(ZjYw$lAVuENq9q{CttBh zZRs(*=09nh4a+2tuVRV6ji*IqI63J9R(SQ<8To($5AwqLnQ{+Tez$JF3L8$2PnE+U Yn=Ep@Oa+F!OtJ|k`M=@h`RQZgUx7BtM*si- diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 27570f7ef530a7aa002472981d46cf4bc2f9244a..1f239d272102d9fcf121187c0393d6d60bc16a40 100644 GIT binary patch delta 4120 zcmY+`X>=P`6$fxjdP(FpUg{*4yhn;`OSa@JPMkE29q;=lCypIkb}U(u<1Lcyv^0s+ zme2;;Chl{=6ap@k))1Gb7-(2ZQ(BfnNk(M~#f5VEiNl9-2EIbi;m&(kH>a}oKXZTY zubI($<`w^YV<|5sCnvS9s0&2RLx3F9Q|XNI_7oG~t7@NWv3 z#y9C*{pR1iwuM(2GSkeUo27xqqb7iEAx%V00yUE+qo#nylWs*#1r=weulUnY(?Jud zn}K>CXcB2A>Ne11((R}_KvPJwP%WTaNv){apsA!esJWnNqzq&rb< zp!bp5QFno6k~&a}K(~<=qn3bfCv~E_KzERqqLzVXk-AYmpcc|{)ZL&~(mkkqL9Ll$!^@6%cdr^;pmXaPv z?E@_%J%QQ}>LwjPJqhX|9Yh@hEhqJ%4ukF{9YH+>x`%WW)epLtG=Mq=x{ox7dK$EX zG=w@1x}Wq6>ICQk(n-`Q&`Q#2)EUr&q-RmXpod5!sI#E=lg^>egB~V5hx#Ds1Ede3 zeh%~q>BFe!L90kFpk4&6CS5?i1bUS85!6RPYe*kM{XA$b>1EU}fYy;dj`{>>J?RzH zFM>9ZehKwS&_>dyP@fL4@g~x%sMny|OuC5r3}_1}M_mGKC4Cn4%b;zfUqM|4Z72OI z>T{qSq+dh*I%p^9b<}Twc9DJ)bp^DW^m){8f%cGo8}$Wv#$M9zpuPy*UecFPS3!@F zei!w7pvOsXpss=Tk-m)jeb5u6ub|!p?I(Q|^#`B>r0b|Z1U*UmBh(**4wAlxdJA-j z^e3o41@)2s4E5)r!=$gH{sMG_^bOS8pr=URMExb`DCt|MZ-e?te}(#M&;aRgP=51)U&$AN5b5lcaw}{Qz`| z^e?CzpwpxuN-h2?=nU!K*0#K8ii^7ecX?Yh^q=*A)Du_ycb<@HO)WNTB+Z8c=OVt) za|VNjziG{|#7b#c8XcJp1gFEH zKx92X`+a5$ZzTH1N2Y?)Wpm+)NLW9auzsZ$)-UbC`juSR%(DtS#&8C&FDTfqUwef0 zOO1`B(A4~NXf)uP2t{W3V+Btd!~1wr;q3b5!gNDYIII^Vdd%{rot+7f!lnW4kwyNm zovzFe;w;K%|25|2IX0`o%PVcxL9cPEQQI81DgILJ6Wgw3E@iD`D$EgO4wX5yEn?Ly zS96y$)hxGC(WtPdC~Hz#lNK-56XbgKwYjB*)d7W7Mp>oGDz!u}f7O-)$GI!=6I;$^ z(NE{)c8f8?%U$+-SZxzIWzU7}r$nyV^I-d3;hzXe+NC#3MYfBa-lcc0itU>sqeA{I zGR>jy&EwD?)g?0QD1_~2Mcxwmp`!rgQ?Z?2q_5V9920p(YvsiB)5IG?7 zqR8h(zAf?}#rkp0C3@2-vR&j^kxL?96S*PsQ<3@3Liu`ojr?OrmZ{h1Oeq?Tg9y*g|$XmtIArnHrZ(B|AixV$f1+x zlv&MPa_Ht&W!8lrIe23`vag(19d$~pPhrDRHmtH?tyfn1W0l;MO)sR}>{nP-lvSy$ zN*j=cld(e4s(sCUJMrGR49db#tWdIAr@9W`Zd2Iq?kMY4S-0ksmEl+=XL(M|+kGqV z-Zx}KHpcir;W-55aGEE%t>%y%#<|;VT{t6$iP(-DtC?zXz0x+Ouwax0RTk7HWd(jY z^zW(?pTA;P3lAtqx)c^WortW=#wy~BYn0|eg$+g7kjjR%Ia!&HRa~n9wXA`^<4!i8 zlS>cskKpVclEX!A@K~J-a;Rc>Xz?@;E4A>(`J&9HGM`q>#7Cf>-|<+S4RUCV_3yoM zu`_Lwjb_IG2cMr7IkoebJz3@sIdtlKGIzc7gZa=-`?2TboIdycJ5SpW9S+aA&+q-e z@7?Ciy`S}6^(Vicl#~!dpZJRp2Rm}kB&V6bO*xaFkrs$C$Ml-~qSw+djB(@RkDZP? zYf4=ZPZ$d#v;u8}af}JTM8bH+dB7w>D`PS+h0w-0AGm-}9i1`YUkF@8n8?Y+z*NF{ zj7xxb5hgLF0n-VS8SOv^VG3ggFq3dTV-|2J;R42N;4;F6j85Qk!bOZZz+A$`jCnv8 zVJc%juz+w0V|NLOlK?wmJ!++R{-xObTF<2t|H7}Tn$`9n8~;n zSWcM5cn@$L;Znx+zzu}ijQ0XJ5-ww`0B$05GFAes2$wTf18WF#7;Ax>33D0ifLjRj z7`FoJ30;g0z(&G+#wMVbuz;}{*g{yy=mTyeEMnXa+(GDO+zH%8Sj^Z8Y$GgTYzKA_ zdKh;D_Yjsc?ge%dmND)Fb`h>%><0D_-p%L-_7bjS>;nb}S26Af1_@U)_5<%DT*Ej3 zyq|C_;~;Q|u$*xic!2O8#t?9Xa2?|)aEx$0<3Zp9gc}%t1pG1Ky^M!|hY2?_9swRD ztYAC_94FkwcpUf>!b--U0v{x-VtferFkv<0Bfv)qYZy-ePZHKLJ_dZ8a5Lj6-~?eE z;}gIq3AZqw2A(0@%6Jwi3F{fp0nZaQFg^u*ny``a0`MYX6XP?$X9>NG&jFt&Y-W4` z_#$Bo<0SAULLcMHz*h*jF}@1?8R2%u*MP4R?qHk(ULxGd_;cW8!d;Bhz+VuyGF|~* zC2V7y0sfM(o$*(|S;7v+Uju(bxSR2}z-xqi7_S3w5bkCC9q{*ros4e)-z40}_!jVO z!Y;;}z;_6{8UFzMBViBYyTJDd{fvJC{+Y0s@qOS2gnf*+fFBYD7(W7jOt_!%6X2(W zK}HQ2ChTYY4EQvuWOibHPr_`f(wpB&E3r22%a)2iOe&5ToX7s}*3 zyLpjM+VZj~Q>bcvUKVZlt9mleSmBcLTS{)L?F5&RFH*I^Wn>1__JpcelzgJ<-(1FO zN%=;;L{)FTleR;uo>%pH{xXs|wf%#t*#$5$P_m+m50+W|rQPbn7^}3OQWjux+xaT%|=mQ)IVQp{uqS zm#e4--ByeGhG;hDWR0G)`ubi?G=xQiE*c{BNHkdF)v|PJBf2J;>2X-SXqsh($5G#c zW@mJztjX9{lV{pBQ4tmux~PckLZCGo$e+rc_S{VT&z{;4Xt&4-+EWL*-ST^n-MR-& zx13k%u=b$wOHZkTp0MiV`l73@oaxn9w`i?Hnivj?VO=dO@w$*r$m= zSOjzthy)QCj7G92b9LuhZDWfjd|~0!g)cIM$Z#}LI8~#&tKiDmX$;Mw=%n1qG2Kp+?Ur)z-+z zRwUYXU~s39r7pX57rNc@<_de`9yELZXP#$W(KGf1#uFi}`PfkVgst diff --git a/core/__pycache__/views_import.cpython-311.pyc b/core/__pycache__/views_import.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e15ea0cc6b28bcfdc11e7605b295daa04159c3f7 GIT binary patch literal 6186 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 t-K6YWniJGnxO@=$P>VE +
+

{% trans "Import Categories" %}

+ +
+ +
+
+
+
+
{% trans "Upload Excel File" %}
+
+
+
+ {% trans "Excel Format Instructions:" %}
+
    +
  • {% trans "Column A" %}: {% trans "Name (English) - Required" %}
  • +
  • {% trans "Column B" %}: {% trans "Name (Arabic) - Optional (Defaults to English name if blank)" %}
  • +
+ {% trans "Please skip the first row (header)." %} +
+ +
+ {% csrf_token %} + {% for field in form %} +
+ + {{ field }} + {% if field.help_text %} +
{{ field.help_text }}
+ {% endif %} + {% for error in field.errors %} +
{{ error }}
+ {% endfor %} +
+ {% endfor %} + +
+
+
+
+
+ +{% endblock %} diff --git a/core/templates/core/import_suppliers.html b/core/templates/core/import_suppliers.html new file mode 100644 index 0000000..553969c --- /dev/null +++ b/core/templates/core/import_suppliers.html @@ -0,0 +1,55 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} +
+
+

{% trans "Import Suppliers" %}

+ +
+ +
+
+
+
+
{% trans "Upload Excel File" %}
+
+
+
+ {% trans "Excel Format Instructions:" %}
+
    +
  • {% trans "Column A" %}: {% trans "Supplier Name - Required" %}
  • +
  • {% trans "Column B" %}: {% trans "Contact Person - Optional" %}
  • +
  • {% trans "Column C" %}: {% trans "Phone - Optional" %}
  • +
+ {% trans "Please skip the first row (header)." %} +
+ +
+ {% csrf_token %} + {% for field in form %} +
+ + {{ field }} + {% if field.help_text %} +
{{ field.help_text }}
+ {% endif %} + {% for error in field.errors %} +
{{ error }}
+ {% endfor %} +
+ {% endfor %} + +
+
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/inventory.html b/core/templates/core/inventory.html index 39f1e0e..2411165 100644 --- a/core/templates/core/inventory.html +++ b/core/templates/core/inventory.html @@ -25,6 +25,9 @@ + + {% trans "Import Categories" %} + @@ -774,4 +777,4 @@ } }); -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/suppliers.html b/core/templates/core/suppliers.html index 5fa1e18..2e2753c 100644 --- a/core/templates/core/suppliers.html +++ b/core/templates/core/suppliers.html @@ -15,9 +15,14 @@ - +
+ + + {% trans "Import" %} + +
{% if messages %} @@ -60,6 +65,39 @@ + + + {% endfor %} @@ -135,4 +173,4 @@ document.getElementById('saveAndAddAnotherSupp').onclick = () => saveSupplier(true); }); -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/core/urls.py b/core/urls.py index f3ffbce..b341509 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,5 +1,6 @@ from django.urls import path from . import views +from . import views_import urlpatterns = [ path('', views.index, name='index'), @@ -92,6 +93,7 @@ urlpatterns = [ path('suppliers/edit//', views.edit_supplier, name='edit_supplier'), path('suppliers/delete//', views.delete_supplier, name='delete_supplier'), path('api/add-supplier-ajax/', views.add_supplier_ajax, name='add_supplier_ajax'), + path('suppliers/import/', views_import.import_suppliers, name='import_suppliers'), # Inventory path('inventory/suggest-sku/', views.suggest_sku, name='suggest_sku'), @@ -106,6 +108,7 @@ urlpatterns = [ path('inventory/category/edit//', views.edit_category, name='edit_category'), path('inventory/category/delete//', views.delete_category, name='delete_category'), path('api/add-category-ajax/', views.add_category_ajax, name='add_category_ajax'), + path('inventory/category/import/', views_import.import_categories, name='import_categories'), # Units path('inventory/unit/add/', views.add_unit, name='add_unit'), @@ -147,4 +150,4 @@ urlpatterns = [ path('sessions/start/', views.start_session, name='start_session'), path('sessions/close/', views.close_session, name='close_session'), path('sessions//', views.session_detail, name='session_detail'), -] \ No newline at end of file +] diff --git a/core/views_import.py b/core/views_import.py new file mode 100644 index 0000000..608ecf4 --- /dev/null +++ b/core/views_import.py @@ -0,0 +1,139 @@ +from django.shortcuts import render, redirect +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.urls import reverse +from django.utils.text import slugify +import openpyxl +from .models import Category, Supplier +from .forms_import import ImportFileForm + +@login_required +def import_categories(request): + """ + Import categories from an Excel (.xlsx) file. + Expected columns: Name (Eng), Name (Ar) + """ + if request.method == 'POST': + form = ImportFileForm(request.POST, request.FILES) + if form.is_valid(): + excel_file = request.FILES['file'] + + try: + wb = openpyxl.load_workbook(excel_file) + sheet = wb.active + + count = 0 + updated_count = 0 + errors = [] + + # Skip header row (min_row=2) + for i, row in enumerate(sheet.iter_rows(min_row=2, values_only=True), start=2): + if not any(row): continue # Skip empty rows + + # Unpack columns with fallbacks for safety + # Format: name_en, name_ar + name_en = str(row[0]).strip() if row[0] else None + name_ar = str(row[1]).strip() if len(row) > 1 and row[1] else name_en + + if not name_en: + errors.append(f"Row {i}: Missing English Name. Skipped.") + continue + + slug = slugify(name_en) + + category, created = Category.objects.update_or_create( + slug=slug, + defaults={ + 'name_en': name_en, + 'name_ar': name_ar, + } + ) + + if created: + count += 1 + else: + updated_count += 1 + + if count > 0 or updated_count > 0: + msg = f"Import completed: {count} new categories added" + if updated_count > 0: + msg += f", {updated_count} categories updated" + messages.success(request, msg) + + if errors: + for error in errors: + messages.warning(request, error) + + except Exception as e: + messages.error(request, f"Error processing file: {str(e)}") + + return redirect(reverse('inventory') + '#categories-list') + else: + form = ImportFileForm() + + return render(request, 'core/import_categories.html', {'form': form}) + +@login_required +def import_suppliers(request): + """ + Import suppliers from an Excel (.xlsx) file. + Expected columns: Name, Contact Person, Phone + """ + if request.method == 'POST': + form = ImportFileForm(request.POST, request.FILES) + if form.is_valid(): + excel_file = request.FILES['file'] + + try: + wb = openpyxl.load_workbook(excel_file) + sheet = wb.active + + count = 0 + updated_count = 0 + errors = [] + + # Skip header row (min_row=2) + for i, row in enumerate(sheet.iter_rows(min_row=2, values_only=True), start=2): + if not any(row): continue # Skip empty rows + + # Unpack columns with fallbacks for safety + # Format: Name, Contact Person, Phone + name = str(row[0]).strip() if row[0] else None + contact_person = str(row[1]).strip() if len(row) > 1 and row[1] else '' + phone = str(row[2]).strip() if len(row) > 2 and row[2] else '' + + if not name: + errors.append(f"Row {i}: Missing Name. Skipped.") + continue + + supplier, created = Supplier.objects.update_or_create( + name=name, + defaults={ + 'contact_person': contact_person, + 'phone': phone, + } + ) + + if created: + count += 1 + else: + updated_count += 1 + + if count > 0 or updated_count > 0: + msg = f"Import completed: {count} new suppliers added" + if updated_count > 0: + msg += f", {updated_count} suppliers updated" + messages.success(request, msg) + + if errors: + for error in errors: + messages.warning(request, error) + + except Exception as e: + messages.error(request, f"Error processing file: {str(e)}") + + return redirect('suppliers') + else: + form = ImportFileForm() + + return render(request, 'core/import_suppliers.html', {'form': form}) \ No newline at end of file diff --git a/patch_models_timestamp.py b/patch_models_timestamp.py new file mode 100644 index 0000000..73634d9 --- /dev/null +++ b/patch_models_timestamp.py @@ -0,0 +1,43 @@ +import os + +path = 'core/models.py' +with open(path, 'r') as f: + content = f.read() + +# Patch SalePayment +old_sale_payment = """ notes = models.TextField(_("Notes"), blank=True) + created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="sale_payments") + + def __str__(self):""" +new_sale_payment = """ notes = models.TextField(_("Notes"), blank=True) + created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="sale_payments") + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self):""" + +# Patch PurchasePayment +old_purchase_payment = """ notes = models.TextField(_("Notes"), blank=True) + created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="purchase_payments") + + def __str__(self):""" +new_purchase_payment = """ notes = models.TextField(_("Notes"), blank=True) + created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="purchase_payments") + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self):""" + +# Check if SalePayment already has created_at +# A simple check: if we find the new pattern, we skip +if new_sale_payment in content: + print("SalePayment already patched.") +else: + content = content.replace(old_sale_payment, new_sale_payment) + +if new_purchase_payment in content: + print("PurchasePayment already patched.") +else: + content = content.replace(old_purchase_payment, new_purchase_payment) + +with open(path, 'w') as f: + f.write(content) +print("Patched core/models.py")