From b9b8c69129133da64d730ee8fe1bff9aa4663be4 Mon Sep 17 00:00:00 2001 From: Marwan Alwali Date: Sun, 31 Aug 2025 10:47:23 +0300 Subject: [PATCH] update --- .DS_Store | Bin 10244 -> 14340 bytes inventory/__pycache__/forms.cpython-312.pyc | Bin 21762 -> 21737 bytes inventory/__pycache__/models.cpython-312.pyc | Bin 39006 -> 39292 bytes inventory/__pycache__/views.cpython-312.pyc | Bin 50694 -> 50694 bytes inventory/forms.py | 6 +- inventory/models.py | 216 +- inventory/views.py | 14 +- logs/hospital_management.log | 3666 +++++++++++++++++ operating_theatre/.DS_Store | Bin 0 -> 6148 bytes .../__pycache__/urls.cpython-312.pyc | Bin 5728 -> 5704 bytes .../__pycache__/views.cpython-312.pyc | Bin 62157 -> 61771 bytes operating_theatre/management/.DS_Store | Bin 0 -> 6148 bytes operating_theatre/management/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 201 bytes .../management/commands/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 210 bytes .../__pycache__/or_data.cpython-312.pyc | Bin 0 -> 29965 bytes .../management/commands/or_data.py | 585 +++ operating_theatre/urls.py | 8 +- operating_theatre/views.py | 56 +- or_data.py | 866 ++++ templates/.DS_Store | Bin 18436 -> 18436 bytes .../orders/purchase_order_detail.html | 1313 ++++++ .../inventory/orders/purchase_order_form.html | 1505 +++++++ .../inventory/orders/purchase_order_list.html | 981 +++++ templates/inventory/stock/stock_form.html | 60 +- templates/inventory/stock/stock_list.html | 49 +- .../inventory/suppliers/supplier_detail.html | 6 +- .../blocks/block_confirm_delete.html | 4 +- .../blocks/block_detail.html | 4 +- .../operating_theatre/blocks/block_form.html | 10 +- ...ock_schedule_list.html => block_list.html} | 8 +- templates/operating_theatre/dashboard.html | 2 +- .../equipment/equipment_confirm_delete.html | 4 +- .../equipment/equipment_detail.html | 4 +- .../equipment/equipment_form.html | 4 +- .../equipment/equipment_list.html | 32 +- .../notes/operative_note_detail.html | 4 +- .../notes/operative_note_list.html | 10 +- .../notes/surgical_note_confirm_delete.html | 4 +- .../notes/surgical_note_detail.html | 4 +- .../notes/surgical_note_form.html | 4 +- .../notes/surgical_note_list.html | 4 +- .../rooms/operating_room_list.html | 2 +- ...surgical_note_template_confirm_delete.html | 4 +- .../surgical_note_template_detail.html | 4 +- .../surgical_note_template_form.html | 64 +- .../surgical_note_template_list.html | 4 +- .../templates/template_confirm_delete.html | 4 +- .../templates/template_detail.html | 4 +- .../templates/template_form.html | 4 +- .../templates/template_list.html | 4 +- 52 files changed, 9225 insertions(+), 302 deletions(-) create mode 100644 operating_theatre/.DS_Store create mode 100644 operating_theatre/management/.DS_Store create mode 100644 operating_theatre/management/__init__.py create mode 100644 operating_theatre/management/__pycache__/__init__.cpython-312.pyc create mode 100644 operating_theatre/management/commands/__init__.py create mode 100644 operating_theatre/management/commands/__pycache__/__init__.cpython-312.pyc create mode 100644 operating_theatre/management/commands/__pycache__/or_data.cpython-312.pyc create mode 100644 operating_theatre/management/commands/or_data.py create mode 100644 or_data.py create mode 100644 templates/inventory/orders/purchase_order_detail.html create mode 100644 templates/inventory/orders/purchase_order_form.html create mode 100644 templates/inventory/orders/purchase_order_list.html rename templates/operating_theatre/blocks/{block_schedule_list.html => block_list.html} (98%) diff --git a/.DS_Store b/.DS_Store index d2afdf183504dc47f249f592b0bcbec115a4b232..c2658e59603f0cc5f7fd4294bd740717a62a786d 100644 GIT binary patch delta 254 zcmZn(Xem%&U|?W$DortDU@!nOIe-{M3-ADmHUF#y>D3=D}3 zISiQ$B@C6GITH(&CmX1+u!0n@Otu!+WBUJpW1&0yW_Au14o2?Hnd0*}HZ~-(GpcUp z5eQ?OtRSX5IYZ(&h~_B^F3QWv&r1iHKRI6P&g3of!5bT5*(F(lB0ylk4J2HVowKp< tJ@aIK6=7cGe|4YL~bDA3Nm%$ t!SBqI`Au{g877VdvBUsQ?;nFgeF$;$}wGL`J9)lX(PY0|0`TA-wl=-WWngs=ETx){ZgHKWP?=cY{qu=-LCB*yix#xGz{r&Fwo$(_5E|6~O zdi56doE?$FSD)%f1De>WmOK!+MicxkVA?M%IlXc^=(5`%q3|cxD1RKOCPzW7#A9A> zPf(t_LkF9SNIU`4U{YZ?6h1sxL?@Dx^P%^x69^xGhhWO8C|2}zI-AQFM)piDmrfT* zO!D=P!_*|_JJz<=mGIpxcn#k0LO4S&W!teEHZDeew2f;6uFF+XaTDcRij=sG5H}L9 z05;U%_hNk%MS~%sRJ3c0|!#sZck~K4?NVpP2 zBJ(Zi-LHur^%A`RCx~YNGxFO+0&z(OJ5Ldnn7dL`lE1os(gCZ*cc{*R58yd?p)g1h zt_*hu$r=9Jy?1xhs!!A0tIjITv%br&c|@dA`%=G0_MaHEIm%cs@@(%(`Y7Ff->Gin zOt?1Iuy4ZvHod#-iTR1jJQi=KvYd)H5-svVe^6;l6!u%~FX4B~VAIY`R7_q@93qpq tC%yEETas_R)`d`8sX0B8IX^VoHaIHI8R9ci4IGcT*pu?mR}Q7-{s9=i!VCZa delta 734 zcmZwDUr5tY6bJCm+Hds7-;8Rt{%ov2&?ZrVQYAhJ5413*aS~;_1dXy^wpG22K2<*4HF%AbnF26StHlJ$0$S zzqhALl-m)LDBlLS@2L`Z5NCh^hS(Xjd+s9igLbeU+*719#eY2*+!-7w&x5}O8!N&7 z<~MHqrAHGdaIX#Eo_Go{29HTZ5$EO3meWLePPfJ=FK1i7(m?}-SKuv}0nfm5MT!)S zmHAB|Il&9Z_H1=8xioG2l68&d__NupIYe`%_Icwr={w$Svdm#W&-dFpXjW2afg;P> z#(}jU1Wp1BQ7oIpibGK6(oUWVZ;_!>R-!rX2sbD%*YD!-@L!r|8L3k)MzS>q-uwVQ zf)yJpqEh6^Xe|}_XVgjWxVnAXS?(9LQq|cD@x;00f$pT}ONmcN+12N5Y>qkT3-6A- F{s*Da%`gA} diff --git a/inventory/__pycache__/models.cpython-312.pyc b/inventory/__pycache__/models.cpython-312.pyc index 5cf8b48a9c95123b14d94fe040e8eab6dc566736..0c449c0ae0c22c57bc7b4c4805872304f8dfe580 100644 GIT binary patch delta 8328 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 delta 8254 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 sEb+?n+En4@$4^r=H94t*$Lna8*P3y4lx=>Px# diff --git a/inventory/__pycache__/views.cpython-312.pyc b/inventory/__pycache__/views.cpython-312.pyc index 259cb5ae5eb015f36d56e6eb68af0a0f2436a9f8..e3e690fbe611bb4d22b756763526cc38113c9c8e 100644 GIT binary patch delta 829 zcmaiyZAep57{~YAyE{!8x$1*6r}HIp2{U^`Fr9-{=AtR6L=#-YmkM9TomxVoG&ASR z8a%0NDK-p;7)CpkBTyszP!yyLA?R&U(3d_eLW>CMoQvOtx*z`caQL6!bDrma`iA(v zAzt}HsZ5a3?_(o5-+W)GcEK-9cWcq;dIbie>RsbG1K{Ma28I3{IE?!K7%vtAItG$4 z8WYH-iE(Qj-7*3O%*`Y)_yfRz zzTqr|iJdj!li}lF!is)1zN{=kyQfU0yU@~6-_q79cILH;b@if?JoJnS;Kb86H{?em z2`N}QQlD*R$C-Sgc&DD0{n;z~#&JS?Od(UI^c;J=%YdsRnP8USQR`7WIeJEwy9GWw z+Qr4~VYCB)dA@R}#vY#;8t~8AWc=*gpSq&fMH2SiE0`%*Ib@0Cmd6Cywj{4|H9jpC zj}B7?mirGXsu-XO-F^)iG3eKDRe)N3GQRhFw>E^AZ_WH?fW_naU?h(59QkGz3M;E% z#f96MbbwK+R@zt{HU?_B8YvqR4V;6tf93*Maz{%;Nuq4;)LkyIfkBs9X0>=?x`?v@ zKJMF%f$2}1DcPC9j&ng)bv2j;cCsG4!9x?dHoL)rgIt+=%kOAmt_KX0PDLjd+wfvY z3+*@A*`Q6S|0>{N_Q#vH5(c#pm-DI(T(B8@jPLTn09D zhtp{wGhrRLNF~0oLnjd o4|%hMyk2^C(&LdLlcp*)*~F|4CS6QiOguQgtbqygX!#WP7sYE1%>V!Z delta 851 zcmaJql%vWujPiV+1FO}gkRFhc7hsPCQqU4-Z2Ifw5&@AJIR`+n^`VtbFM zex_EdWc>MN^PaRmP-|Ob8?~ou&pB)BT~Y(?YkjY@fK1kyhD$9PwEN=OfwsFMSn$*J zeAtb9JC=pUB&=*sKuhaJyzfoGh`)<1bjrby&FiEbf?N)6k(=oC&C+rHmVRk2_h|8A zATH*Wz|X9J{=yp8-Tf5=3!tgUpv`j=n0sSY zo*P!B;Qm3U(MClCAGxaAOxfCGp9Mj&VOc3tZ)-mIW@`pM9yAEHHOWX>Hf9bTQyKrt z28J#PF%fEXNrtH-M}$(q{u4SJ8rcO-{9)JN;>e!3n+ai+KB)0)UhK>YCud`#r_xwL z<^)@KUbpFg=9xI^(FrAhdb0*!dj|f=gBQHxx^?G<7GFA8S)DIUPD3&m>6CV|wByXE z0WvT$Y7p#ex#~I!vC3Z|FXR7dGc4kF!1i_30j;<7cqI_eEd~M;LadXt=V=h-STL3& zuOO|uLt6BYefYg32XP!Y86^Lg!4zw8ATtlpfq?E+LB$wQH zdP)!V*gbU=3h>=jDoChcRk3a&))Sn?^UMHE%)`Ema0Z8`)1e#}ri=Lsv>_v0MoY*B z71$X{f>In0nV^M*L&*wGU72a%RM%_?r#_iALpy$%9Z00+rKi+_f?7~Y#q@oZPD}CD rTo&}Qw{s|(qqi=4i=x|%&Nmveny4;_=??}1B!q)8WG0z@)9Fl0ziwKF5JDRxuR=(k5MqD|OqtLu5iu@wK~mbY3rOW0 z9nvN}(2zt6(dhV(42ZMagEMpB%p5Xver`PMy8(A${KM@B{xB++zi29*+1brz&1^3F zV0U?JM@}>vHk`qo=v{LjxKrD4Z~S)L${$?tFmn8`J&*~$*M^Y$TR-%8+~A|o8_HZ; z4Vb23w(^DXxK=wUv1+9@DY0?AS}w8EdUY}}jl-kkv#Zu~&-rBMK?t1IL*n=?OVF%Wn?s4WPqlpY4 z1IWO}4Cp(>$Zb3WCL#mKz_v0V?gs`c(9)PFlvf8dvIGFkfLjXKqLvUjOrxbSQ3xX- zRHXu{RH{o1s?x#FbeyFzQK-@h)x`(ZD^p#eP`NtH&ty2EmO^Zi0c2pFfwb!8#ruD` zy8fR}!X7e!3~UtxG}E-34cL-?TNgGb-dYCw2r5LxB?^C2z>rTd#EPf504fFiOc;Qc U#zY}_K=6-%qyZab;7=L&0i{`R&j0`b literal 0 HcmV?d00001 diff --git a/operating_theatre/__pycache__/urls.cpython-312.pyc b/operating_theatre/__pycache__/urls.cpython-312.pyc index 3feda0632fcff331ce008346fa6dfdf6ed8377b1..7959966305c74c93bdec24108c119c5b2aae9cd5 100644 GIT binary patch delta 143 zcmaE$b3%vrG%qg~0}$Mf+nlj#BX1rL7hh^&X=XugYF^3YZXQWP0XR24xhOTUB(+Ky zE~0OfnO9;}kZq@b3!!ZCLmm|_1b4G2?_YM7kSi>qlf^`pS>ms-BuusiG7_(_Bu!2M OGBU2PWKQlDy$S$AgEFiD delta 163 zcmX@1^FW99G%qg~0}w3Z>&Q5?kvEUWQY5voG_xQ#HLoPTv^X(6H9jY^xTH!DE~=kg zl$uzQs((uiE(B3Hc?FNG0EXas9u*A1&3}3Rva>{8VTqpnPehp|^$JVcWK~gRmh>wu S8Iyg0jJzu>`IC!9uL1xvqCB+# diff --git a/operating_theatre/__pycache__/views.cpython-312.pyc b/operating_theatre/__pycache__/views.cpython-312.pyc index ec00a43e16e2552e90a034eb5da4732a5b49790b..012c773da0bd8b0626e81ba750c5ca09e222480b 100644 GIT binary patch delta 11155 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 Yr?3+IrDu~(o*@42PPq7rhWGpb0TAk9PXGV_ delta 11631 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 SAy`20M?lhm4Knbf47>t>n{Dd= literal 0 HcmV?d00001 diff --git a/operating_theatre/management/__init__.py b/operating_theatre/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/operating_theatre/management/__pycache__/__init__.cpython-312.pyc b/operating_theatre/management/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..84bcfb857a5a1a94693ec6c432ff268b2d1343ff GIT binary patch literal 201 zcmZ8aI|>3Z5Z$;6BJ4o~F^#2o1Uu^?=wKrGk<7a6Nj!tKXRz`Hww^$?vvLAfKA4&J z9`7-arkPcY`{Jg{eUZ`ffUaK@>HI&Rp%U#BlrBkU>UfE7r!@{>2VhShR zlhh$cL&^f%>WB_KLGCl5gY!y;h)I$0M<9)g5|;KaCIO5$1h9<#bGmln&|2K22|pG* MxsZ1)gh(~BFYZA(NdN!< literal 0 HcmV?d00001 diff --git a/operating_theatre/management/commands/__init__.py b/operating_theatre/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/operating_theatre/management/commands/__pycache__/__init__.cpython-312.pyc b/operating_theatre/management/commands/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4b0fc01fb21a91a07468d4bc61f4fdcca2c889be GIT binary patch literal 210 zcmZ8bI|>3Z5KUYK5%wU0n8s2(f}Qmcc8A?aekNI$J&9+q_869Sww^$?vvLAfKA4&J z9`C{2%W}a8kJYWs_m$T_#wj@$Hn1h%Us;%t8gu4z5{HN(3L`@=t<=5LiiGVV_~4FM zCDFR*RgxOWmJJ%T3E~hF8aQu+^B81Owt>{5OaXKI7Za5;r0OUDlvN2}?T6=lYh2xF S+=Ur^z-eM8?}Bq43SwVVnLObD literal 0 HcmV?d00001 diff --git a/operating_theatre/management/commands/__pycache__/or_data.cpython-312.pyc b/operating_theatre/management/commands/__pycache__/or_data.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5373f3f2d8183c42f42829fa90bd533aa5610e33 GIT binary patch literal 29965 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 tQ~h&p*U!0@zcv;|jm59&erB{TU;N!yU( 0.6 + + if is_female: + first_name = random.choice(self.SAUDI_FEMALE_FIRST_NAMES) + gender = 'F' + else: + first_name = random.choice(self.SAUDI_MALE_FIRST_NAMES) + gender = 'M' + + last_name = random.choice(self.SAUDI_FAMILY_NAMES) + + # Create user for patient + username = f"patient_{first_name.lower()}_{i}" + user, _ = User.objects.get_or_create( + username=username, + defaults={ + 'email': f"{username}@example.com", + 'first_name': first_name, + 'last_name': last_name + } + ) + + # Create patient profile + patient, created = PatientProfile.objects.get_or_create( + user=user, + defaults={ + 'tenant': tenant, + 'patient_id': f"P{datetime.now().year}{i:06d}", + 'date_of_birth': fake.date_of_birth(minimum_age=18, maximum_age=80), + 'gender': gender, + 'blood_group': random.choice(['A+', 'A-', 'B+', 'B-', 'O+', 'O-', 'AB+', 'AB-']), + 'phone': f"+966{random.randint(500000000, 599999999)}", + 'email': user.email, + 'address': f"{random.randint(1, 999)} {random.choice(['King Fahd Road', 'Olaya Street', 'Tahlia Street'])}, {random.choice(self.SAUDI_CITIES)}", + 'emergency_contact_name': random.choice(self.SAUDI_MALE_FIRST_NAMES) + ' ' + last_name, + 'emergency_contact_phone': f"+966{random.randint(500000000, 599999999)}", + 'national_id': f"{random.randint(1000000000, 2999999999)}", + 'insurance_provider': random.choice(['Bupa', 'Tawuniya', 'MedGulf', 'Allianz', 'AXA']), + 'insurance_number': f"INS{random.randint(100000, 999999)}" + } + ) + + patients.append(patient) + + self.stdout.write(self.style.SUCCESS(f'Created {len(patients)} patients')) + return patients + + def create_surgical_note_templates(self, tenant): + """Create surgical note templates.""" + self.stdout.write('Creating surgical note templates...') + + specialties = ['GENERAL', 'CARDIAC', 'NEURO', 'ORTHOPEDIC', 'OBSTETRIC'] + + for specialty in specialties: + for template_type in ['Standard', 'Complex', 'Emergency']: + SurgicalNoteTemplate.objects.get_or_create( + tenant=tenant, + name=f"{specialty} - {template_type} Template", + defaults={ + 'description': f"Standard {template_type.lower()} template for {specialty.lower()} procedures", + 'specialty': specialty, + 'procedure_type': random.choice(self.SURGICAL_PROCEDURES.get(specialty, ['General'])), + 'preoperative_diagnosis_template': "Preoperative Diagnosis: [Enter diagnosis]", + 'planned_procedure_template': "Planned Procedure: [Enter procedure]", + 'indication_template': "Indication: Patient presents with [symptoms] requiring intervention", + 'procedure_performed_template': "Procedure: [Describe procedure performed]", + 'surgical_approach_template': "Approach: [Describe approach]", + 'findings_template': "Findings: [Describe findings]", + 'technique_template': "Technique: Standard surgical technique employed", + 'postoperative_diagnosis_template': "Postoperative Diagnosis: [Enter diagnosis]", + 'is_active': True, + 'is_default': template_type == 'Standard' + } + ) + + self.stdout.write(self.style.SUCCESS('Created surgical note templates')) + + def create_operating_rooms(self, tenant, count=5): + """Create operating rooms.""" + self.stdout.write('Creating operating rooms...') + rooms = [] + + room_types = ['GENERAL', 'CARDIAC', 'NEURO', 'ORTHOPEDIC', 'OBSTETRIC'] + + for i in range(1, count + 1): + room_type = room_types[(i-1) % len(room_types)] + + room = OperatingRoom.objects.create( + tenant=tenant, + room_number=f"OR-{i:03d}", + room_name=f"{room_type.title()} Operating Room {i}", + room_type=room_type, + status='AVAILABLE', + floor_number=random.randint(1, 4), + room_size=random.uniform(40, 80), + ceiling_height=random.uniform(3.0, 4.5), + temperature_min=18.0, + temperature_max=24.0, + humidity_min=30.0, + humidity_max=60.0, + air_changes_per_hour=random.randint(20, 25), + positive_pressure=True, + equipment_list=self._get_equipment_list(room_type), + special_features=self._get_special_features(room_type), + has_c_arm=room_type in ['ORTHOPEDIC', 'NEURO'], + has_ct=room_type in ['NEURO', 'CARDIAC'] and random.random() > 0.5, + has_mri=room_type == 'NEURO' and random.random() > 0.7, + has_ultrasound=True, + has_neuromonitoring=room_type in ['NEURO', 'ORTHOPEDIC'], + supports_robotic=room_type in ['GENERAL', 'CARDIAC'] and random.random() > 0.4, + supports_laparoscopic=room_type in ['GENERAL', 'OBSTETRIC'], + supports_microscopy=room_type in ['NEURO', 'OPHTHALMOLOGY'], + max_case_duration=random.randint(240, 720), + turnover_time=random.randint(20, 45), + cleaning_time=random.randint(30, 60), + required_nurses=random.randint(2, 4), + required_techs=random.randint(1, 3), + is_active=True, + accepts_emergency=True, + building=random.choice(['Main', 'East Wing', 'West Wing']), + wing=random.choice(['North', 'South', 'Central']) + ) + + rooms.append(room) + + self.stdout.write(self.style.SUCCESS(f'Created {len(rooms)} operating rooms')) + return rooms + + def _get_equipment_list(self, room_type): + """Get equipment list for room type.""" + basic = [ + "Anesthesia Machine", + "Patient Monitor", + "Surgical Lights", + "Operating Table", + "Electrocautery Unit" + ] + + specialized = { + 'CARDIAC': ["Heart-Lung Machine", "IABP"], + 'NEURO': ["Surgical Microscope", "Neuronavigation"], + 'ORTHOPEDIC': ["C-Arm", "Power Tools"], + 'GENERAL': ["Laparoscopic Tower", "Harmonic Scalpel"] + } + + equipment = basic.copy() + if room_type in specialized: + equipment.extend(specialized[room_type]) + + return equipment + + def _get_special_features(self, room_type): + """Get special features for room type.""" + features = ["Laminar Flow", "HEPA Filtration"] + + special = { + 'CARDIAC': ["Hybrid OR Capability"], + 'NEURO': ["Intraoperative MRI Compatible"], + 'ORTHOPEDIC': ["Class 100 Clean Room"], + 'OBSTETRIC': ["Neonatal Resuscitation Area"] + } + + if room_type in special: + features.extend(special[room_type]) + + return features + + def create_or_blocks_with_cases(self, room, users, patients, num_blocks, num_cases): + """Create OR blocks with surgical cases.""" + self.stdout.write(f'Creating blocks for {room.room_number}...') + + surgeons = [u for u in users if 'Dr.' in u.first_name or random.random() > 0.5] + nurses = [u for u in users if u not in surgeons] + + for block_num in range(num_blocks): + # Create OR block + block_date = timezone.now().date() + timedelta(days=random.randint(1, 30)) + start_hour = random.choice([7, 8, 9, 13]) + duration = random.randint(2, 6) + + block = ORBlock.objects.create( + operating_room=room, + date=block_date, + start_time=time(start_hour, 0), + end_time=time((start_hour + duration) % 24, 0), + block_type='SCHEDULED', + primary_surgeon=random.choice(surgeons), + service=room.room_type, + status=random.choice(['SCHEDULED', 'ACTIVE', 'COMPLETED']), + allocated_minutes=duration * 60, + used_minutes=random.randint(duration * 45, duration * 60), + special_equipment=random.sample(self.MEDICAL_EQUIPMENT, k=random.randint(0, 2)), + notes=f"Block for {room.room_type} procedures" + ) + + # Add assistant surgeons + if surgeons: + assistants = random.sample(surgeons, k=min(random.randint(0, 2), len(surgeons))) + block.assistant_surgeons.set(assistants) + + # Create surgical cases for this block + for case_num in range(random.randint(1, num_cases)): + self.create_surgical_case(block, patients, surgeons, nurses) + + def create_surgical_case(self, block, patients, surgeons, nurses): + """Create a surgical case.""" + procedure_type = block.service + procedures = self.SURGICAL_PROCEDURES.get(procedure_type, ['General Surgery']) + + case_time = datetime.combine(block.date, block.start_time) + timedelta(minutes=random.randint(0, 120)) + duration = random.randint(30, 240) + + case = SurgicalCase.objects.create( + or_block=block, + patient=random.choice(patients), + primary_surgeon=block.primary_surgeon, + anesthesiologist=random.choice(surgeons) if surgeons else block.primary_surgeon, + circulating_nurse=random.choice(nurses) if nurses else None, + scrub_nurse=random.choice(nurses) if nurses else None, + primary_procedure=random.choice(procedures), + secondary_procedures=[], + procedure_codes=[f"CPT{random.randint(10000, 99999)}" for _ in range(random.randint(1, 3))], + case_type=random.choice(['ELECTIVE', 'URGENT', 'EMERGENCY']), + approach=random.choice(['OPEN', 'LAPAROSCOPIC', 'ROBOTIC']), + anesthesia_type=random.choice(['GENERAL', 'REGIONAL', 'SPINAL']), + scheduled_start=timezone.make_aware(case_time), + estimated_duration=duration, + status=random.choice(['SCHEDULED', 'COMPLETED', 'IN_PROGRESS']), + diagnosis=self._get_diagnosis(), + diagnosis_codes=[f"ICD10-{random.choice(['K', 'I', 'M'])}{random.randint(10, 99)}.{random.randint(0, 9)}"], + clinical_notes="Patient prepared for surgery as per protocol", + special_equipment=random.sample(self.MEDICAL_EQUIPMENT, k=random.randint(0, 2)), + patient_position=random.choice(['SUPINE', 'PRONE', 'LATERAL']), + estimated_blood_loss=random.randint(10, 500) if random.random() > 0.5 else None + ) + + # Add assistant surgeons + if surgeons: + assistants = random.sample(surgeons, k=min(random.randint(0, 2), len(surgeons))) + case.assistant_surgeons.set(assistants) + + # Create surgical note for completed cases + if case.status == 'COMPLETED': + self.create_surgical_note(case) + + # Create equipment usage + self.create_equipment_usage(case) + + return case + + def _get_diagnosis(self): + """Get a random diagnosis.""" + diagnoses = [ + "Acute Appendicitis", + "Cholelithiasis", + "Inguinal Hernia", + "Coronary Artery Disease", + "Spinal Stenosis", + "Osteoarthritis", + "Brain Tumor", + "Breast Cancer" + ] + return random.choice(diagnoses) + + def create_surgical_note(self, case): + """Create surgical note for completed case.""" + SurgicalNote.objects.create( + surgical_case=case, + surgeon=case.primary_surgeon, + preoperative_diagnosis=case.diagnosis, + planned_procedure=case.primary_procedure, + indication="Symptomatic disease requiring surgical intervention", + procedure_performed=case.primary_procedure, + surgical_approach=f"{case.approach} approach utilized", + findings="As per preoperative diagnosis", + technique="Standard surgical technique employed", + postoperative_diagnosis=case.diagnosis, + condition=random.choice(['STABLE', 'GOOD', 'FAIR']), + disposition=random.choice(['RECOVERY', 'ICU', 'WARD']), + complications="None", + estimated_blood_loss=case.estimated_blood_loss or random.randint(10, 200), + specimens="Sent to pathology" if random.random() > 0.5 else None, + closure="Layered closure with absorbable sutures", + postop_instructions="Standard postoperative care protocol", + follow_up="2 weeks in surgical clinic", + status='SIGNED', + signed_datetime=timezone.now() + ) + + def create_equipment_usage(self, case): + """Create equipment usage records.""" + equipment_types = [ + ('Surgical Drape Set', 'DISPOSABLE', 1, 'SET', 150.00), + ('Harmonic Scalpel', 'SURGICAL_INSTRUMENT', 1, 'EACH', 2500.00), + ('Suture Pack', 'DISPOSABLE', 3, 'PACK', 75.00), + ('Surgical Gloves', 'DISPOSABLE', 10, 'PAIR', 5.00) + ] + + for name, eq_type, qty, unit, cost in random.sample(equipment_types, k=random.randint(2, 4)): + EquipmentUsage.objects.create( + surgical_case=case, + equipment_name=name, + equipment_type=eq_type, + manufacturer=random.choice(['Medtronic', 'Johnson & Johnson', 'Stryker']), + quantity_used=qty, + unit_of_measure=unit, + unit_cost=Decimal(str(cost)), + total_cost=Decimal(str(cost * qty)), + lot_number=f"LOT{random.randint(10000, 99999)}", + expiration_date=timezone.now().date() + timedelta(days=random.randint(180, 730)), + sterilization_date=timezone.now().date() - timedelta(days=random.randint(1, 7)), + recorded_by=case.primary_surgeon + ) + + def print_summary(self, tenant): + """Print summary of generated data.""" + self.stdout.write("\n" + "="*60) + self.stdout.write(self.style.SUCCESS("Data Generation Summary")) + self.stdout.write("="*60) + + rooms = OperatingRoom.objects.filter(tenant=tenant) + blocks = ORBlock.objects.filter(operating_room__tenant=tenant) + cases = SurgicalCase.objects.filter(or_block__operating_room__tenant=tenant) + notes = SurgicalNote.objects.filter(surgical_case__or_block__operating_room__tenant=tenant) + equipment = EquipmentUsage.objects.filter(surgical_case__or_block__operating_room__tenant=tenant) + templates = SurgicalNoteTemplate.objects.filter(tenant=tenant) + + self.stdout.write(f"Tenant: {tenant.name}") + self.stdout.write(f"Operating Rooms: {rooms.count()}") + self.stdout.write(f"OR Blocks: {blocks.count()}") + self.stdout.write(f"Surgical Cases: {cases.count()}") + self.stdout.write(f"Surgical Notes: {notes.count()}") + self.stdout.write(f"Equipment Usage Records: {equipment.count()}") + self.stdout.write(f"Surgical Templates: {templates.count()}") + self.stdout.write("="*60) \ No newline at end of file diff --git a/operating_theatre/urls.py b/operating_theatre/urls.py index f55225d6..84195064 100644 --- a/operating_theatre/urls.py +++ b/operating_theatre/urls.py @@ -60,10 +60,10 @@ urlpatterns = [ # ============================================================================ # EQUIPMENT USAGE URLS (LIMITED CRUD - Operational Data) # ============================================================================ - path('equipment/', views.EquipmentUsageListView.as_view(), name='equipment_usage_list'), - path('equipment/create/', views.EquipmentUsageCreateView.as_view(), name='equipment_usage_create'), - path('equipment//', views.EquipmentUsageDetailView.as_view(), name='equipment_usage_detail'), - path('equipment//update/', views.EquipmentUsageUpdateView.as_view(), name='equipment_usage_update'), + path('equipment/', views.EquipmentUsageListView.as_view(), name='equipment_list'), + path('equipment/create/', views.EquipmentUsageCreateView.as_view(), name='equipment_create'), + path('equipment//', views.EquipmentUsageDetailView.as_view(), name='equipment_detail'), + path('equipment//update/', views.EquipmentUsageUpdateView.as_view(), name='equipment_update'), # Note: No delete view for equipment usage - operational tracking data # ============================================================================ diff --git a/operating_theatre/views.py b/operating_theatre/views.py index ee689bef..180617b8 100644 --- a/operating_theatre/views.py +++ b/operating_theatre/views.py @@ -354,7 +354,7 @@ class SurgicalNoteTemplateListView(LoginRequiredMixin, ListView): List all surgical note templates with filtering and search. """ model = SurgicalNoteTemplate - template_name = 'operating_theatre/surgical_note_template_list.html' + template_name = 'operating_theatre/templates/surgical_note_template_list.html' context_object_name = 'surgical_note_templates' paginate_by = 25 @@ -380,7 +380,7 @@ class SurgicalNoteTemplateListView(LoginRequiredMixin, ListView): if active_only: queryset = queryset.filter(is_active=True) - return queryset.order_by('template_name') + return queryset.order_by('name') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -396,7 +396,7 @@ class SurgicalNoteTemplateDetailView(LoginRequiredMixin, DetailView): Display detailed information about a surgical note template. """ model = SurgicalNoteTemplate - template_name = 'operating_theatre/surgical_note_template_detail.html' + template_name = 'operating_theatre/templates/surgical_note_template_detail.html' context_object_name = 'surgical_note_template' def get_queryset(self): @@ -421,7 +421,7 @@ class SurgicalNoteTemplateCreateView(LoginRequiredMixin, PermissionRequiredMixin """ model = SurgicalNoteTemplate form_class = SurgicalNoteTemplateForm - template_name = 'operating_theatre/surgical_note_template_form.html' + template_name = 'operating_theatre/templates/surgical_note_template_form.html' permission_required = 'operating_theatre.add_surgicalnotetemplate' success_url = reverse_lazy('operating_theatre:surgical_note_template_list') @@ -452,7 +452,7 @@ class SurgicalNoteTemplateUpdateView(LoginRequiredMixin, PermissionRequiredMixin """ model = SurgicalNoteTemplate form_class = SurgicalNoteTemplateForm - template_name = 'operating_theatre/surgical_note_template_form.html' + template_name = 'operating_theatre/templates/surgical_note_template_form.html' permission_required = 'operating_theatre.change_surgicalnotetemplate' def get_queryset(self): @@ -485,7 +485,7 @@ class SurgicalNoteTemplateDeleteView(LoginRequiredMixin, PermissionRequiredMixin Delete a surgical note template (soft delete by deactivating). """ model = SurgicalNoteTemplate - template_name = 'operating_theatre/surgical_note_template_confirm_delete.html' + template_name = 'operating_theatre/templates/surgical_note_template_confirm_delete.html' permission_required = 'operating_theatre.delete_surgicalnotetemplate' success_url = reverse_lazy('operating_theatre:surgical_note_template_list') @@ -500,7 +500,7 @@ class SurgicalNoteTemplateDeleteView(LoginRequiredMixin, PermissionRequiredMixin self.object.save() # Log the action - AuditLogger.log_action( + AuditLogger.log_event( user=request.user, action='SURGICAL_NOTE_TEMPLATE_DEACTIVATED', model='SurgicalNoteTemplate', @@ -521,12 +521,12 @@ class ORBlockListView(LoginRequiredMixin, ListView): List all OR blocks with filtering and search. """ model = ORBlock - template_name = 'operating_theatre/or_block_list.html' + template_name = 'operating_theatre/blocks/block_list.html' context_object_name = 'or_blocks' paginate_by = 25 def get_queryset(self): - queryset = ORBlock.objects.filter(tenant=self.request.user.tenant) + queryset = ORBlock.objects.filter(operating_room__tenant=self.request.user.tenant) # Filter by date range date_from = self.request.GET.get('date_from') @@ -566,7 +566,7 @@ class ORBlockDetailView(LoginRequiredMixin, DetailView): Display detailed information about an OR block. """ model = ORBlock - template_name = 'operating_theatre/or_block_detail.html' + template_name = 'operating_theatre/blocks/block_detail.html' context_object_name = 'or_block' def get_queryset(self): @@ -609,7 +609,7 @@ class ORBlockCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView) """ model = ORBlock form_class = ORBlockForm - template_name = 'operating_theatre/or_block_form.html' + template_name = 'operating_theatre/blocks/block_form.html' permission_required = 'operating_theatre.add_orblock' success_url = reverse_lazy('operating_theatre:or_block_list') @@ -855,12 +855,12 @@ class SurgicalNoteListView(LoginRequiredMixin, ListView): List all surgical notes with filtering and search. """ model = SurgicalNote - template_name = 'operating_theatre/surgical_note_list.html' + template_name = 'operating_theatre/notes/operative_note_list.html' context_object_name = 'surgical_notes' paginate_by = 25 def get_queryset(self): - queryset = SurgicalNote.objects.filter(tenant=self.request.user.tenant) + queryset = SurgicalNote.objects.filter(surgeon__tenant=self.request.user.tenant) # Search functionality search = self.request.GET.get('search') @@ -895,8 +895,8 @@ class SurgicalNoteListView(LoginRequiredMixin, ListView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update({ - 'note_types': SurgicalNote._meta.get_field('note_type').choices, - 'statuses': SurgicalNote._meta.get_field('status').choices, + # 'note_types': SurgicalNote._meta.get_field('note_type').choices, + 'statuses': SurgicalNote.STATUS_CHOICES, }) return context @@ -906,7 +906,7 @@ class SurgicalNoteDetailView(LoginRequiredMixin, DetailView): Display detailed information about a surgical note. """ model = SurgicalNote - template_name = 'operating_theatre/surgical_note_detail.html' + template_name = 'operating_theatre/notes/operative_note_detail.html' context_object_name = 'surgical_note' def get_queryset(self): @@ -919,7 +919,7 @@ class SurgicalNoteCreateView(LoginRequiredMixin, PermissionRequiredMixin, Create """ model = SurgicalNote form_class = SurgicalNoteForm - template_name = 'operating_theatre/surgical_note_form.html' + template_name = 'operating_theatre/notes/surgical_note_form.html' permission_required = 'operating_theatre.add_surgicalnote' success_url = reverse_lazy('operating_theatre:surgical_note_list') @@ -956,12 +956,12 @@ class EquipmentUsageListView(LoginRequiredMixin, ListView): List all equipment usage records with filtering and search. """ model = EquipmentUsage - template_name = 'operating_theatre/equipment_usage_list.html' + template_name = 'operating_theatre/equipment/equipment_list.html' context_object_name = 'equipment_usage_records' paginate_by = 25 def get_queryset(self): - queryset = EquipmentUsage.objects.filter(tenant=self.request.user.tenant) + queryset = EquipmentUsage.objects.filter(surgical_case__admission__tenant=self.request.user.tenant) # Filter by equipment type equipment_type = self.request.GET.get('equipment_type') @@ -993,8 +993,8 @@ class EquipmentUsageListView(LoginRequiredMixin, ListView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update({ - 'equipment_types': EquipmentUsage._meta.get_field('equipment_type').choices, - 'statuses': EquipmentUsage._meta.get_field('status').choices, + 'equipment_types': EquipmentUsage.EQUIPMENT_TYPE_CHOICES, + # 'statuses': EquipmentUsage._meta.get_field('status').choices, 'operating_rooms': OperatingRoom.objects.filter( tenant=self.request.user.tenant, is_active=True @@ -1031,7 +1031,7 @@ class EquipmentUsageCreateView(LoginRequiredMixin, PermissionRequiredMixin, Crea """ model = EquipmentUsage form_class = EquipmentUsageForm - template_name = 'operating_theatre/equipment_usage_form.html' + template_name = 'operating_theatre/equipment/equipment_form.html' permission_required = 'operating_theatre.add_equipmentusage' success_url = reverse_lazy('operating_theatre:equipment_usage_list') @@ -1114,18 +1114,18 @@ def operating_theatre_stats(request): status='IN_USE' ).count(), 'cases_in_progress': SurgicalCase.objects.filter( - tenant=tenant, + admission__tenant=tenant, status='IN_PROGRESS' ).count(), 'cases_completed_today': SurgicalCase.objects.filter( - tenant=tenant, - actual_end_time__date=today, + admission__tenant=tenant, + actual_end__date=today, status='COMPLETED' ).count(), 'emergency_cases_today': SurgicalCase.objects.filter( - tenant=tenant, - scheduled_start_time__date=today, - priority='EMERGENCY' + admission__tenant=tenant, + scheduled_start__date=today, + case_type='EMERGENCY' ).count(), } diff --git a/or_data.py b/or_data.py new file mode 100644 index 00000000..7bda8dd9 --- /dev/null +++ b/or_data.py @@ -0,0 +1,866 @@ +""" +Saudi-influenced Operating Theatre Data Generator +Generates realistic test data for hospital operating room management system +with Saudi Arabian cultural context and medical practices. +""" + +import random +import uuid +from datetime import datetime, timedelta, date, time +from decimal import Decimal +from faker import Faker +from typing import List, Dict, Any, Optional + +# Initialize Faker +fake = Faker('en_US') + + +class SaudiOperatingTheatreDataGenerator: + """ + Generates Saudi-influenced data for Operating Theatre models. + """ + + # Saudi hospital names (in English) + SAUDI_HOSPITALS = [ + "King Faisal Specialist Hospital", + "King Fahad Medical City", + "King Abdulaziz University Hospital", + "King Khaled Eye Specialist Hospital", + "Prince Sultan Military Medical City", + "King Abdullah Medical Complex", + "King Saud Medical City", + "National Guard Hospital", + "Prince Mohammed Bin Abdulaziz Hospital", + "King Salman Hospital", + "Security Forces Hospital", + "Aramco Medical Center", + "Dr. Sulaiman Al Habib Medical Group", + "Saudi German Hospital", + "International Medical Center" + ] + + # Saudi cities + SAUDI_CITIES = [ + "Riyadh", "Jeddah", "Mecca", "Medina", "Dammam", + "Khobar", "Dhahran", "Taif", "Tabuk", "Buraidah", + "Khamis Mushait", "Hofuf", "Jubail", "Yanbu", "Abha", + "Najran", "Jizan", "Hail", "Al-Qassim", "Qatif" + ] + + # Saudi regions + SAUDI_REGIONS = [ + "Riyadh Region", "Makkah Region", "Eastern Province", + "Asir Region", "Madinah Region", "Qassim Region", + "Tabuk Region", "Hail Region", "Northern Borders", + "Jazan Region", "Najran Region", "Al-Baha Region" + ] + + # Common Saudi male first names + SAUDI_MALE_FIRST_NAMES = [ + "Mohammed", "Abdullah", "Abdulrahman", "Khalid", "Fahad", + "Sultan", "Salman", "Saud", "Faisal", "Turki", "Ahmed", + "Omar", "Youssef", "Ibrahim", "Hamad", "Nasser", "Bandar", + "Mansour", "Majed", "Waleed", "Talal", "Rakan", "Yazeed", + "Meshal", "Naif", "Abdulaziz", "Saad", "Ali", "Hassan" + ] + + # Common Saudi female first names + SAUDI_FEMALE_FIRST_NAMES = [ + "Nora", "Fatima", "Aisha", "Mariam", "Sarah", "Reem", + "Lama", "Hind", "Mona", "Amal", "Dalal", "Jawaher", + "Latifa", "Hessa", "Nouf", "Asma", "Khadija", "Layla", + "Rana", "Dina", "Hala", "Salma", "Yasmin", "Zainab", + "Lubna", "Hanaa", "Samira", "Najla", "Afaf", "Ghada" + ] + + # Common Saudi family names + SAUDI_FAMILY_NAMES = [ + "Al-Saud", "Al-Rasheed", "Al-Qahtani", "Al-Otaibi", "Al-Dossari", + "Al-Harbi", "Al-Zahrani", "Al-Ghamdi", "Al-Shehri", "Al-Asmari", + "Al-Mutairi", "Al-Enezi", "Al-Shamari", "Al-Maliki", "Al-Johani", + "Al-Subaie", "Al-Hajri", "Al-Khaldi", "Al-Turki", "Al-Obaid", + "Al-Hassan", "Al-Sheikh", "Al-Najjar", "Al-Omari", "Al-Bakri" + ] + + # Medical specialties common in Saudi Arabia + MEDICAL_SPECIALTIES = [ + "General Surgery", "Cardiac Surgery", "Neurosurgery", + "Orthopedic Surgery", "Pediatric Surgery", "Vascular Surgery", + "Thoracic Surgery", "Plastic Surgery", "Bariatric Surgery", + "Transplant Surgery", "Ophthalmology", "ENT Surgery", + "Urology", "Obstetrics", "Maxillofacial Surgery" + ] + + # Common surgical procedures in Saudi context + SURGICAL_PROCEDURES = { + "GENERAL": [ + "Laparoscopic Cholecystectomy", + "Appendectomy", + "Hernia Repair", + "Bowel Resection", + "Gastric Bypass", + "Sleeve Gastrectomy", + "Thyroidectomy", + "Hemorrhoidectomy" + ], + "CARDIAC": [ + "Coronary Artery Bypass Grafting", + "Valve Replacement", + "Atrial Septal Defect Repair", + "Pacemaker Insertion", + "Angioplasty", + "Heart Transplant" + ], + "ORTHOPEDIC": [ + "Total Knee Replacement", + "Total Hip Replacement", + "Spinal Fusion", + "ACL Reconstruction", + "Rotator Cuff Repair", + "Fracture Fixation", + "Arthroscopy" + ], + "NEURO": [ + "Craniotomy", + "Brain Tumor Resection", + "Spinal Decompression", + "VP Shunt Placement", + "Aneurysm Clipping", + "Deep Brain Stimulation" + ], + "OBSTETRIC": [ + "Cesarean Section", + "Hysterectomy", + "Myomectomy", + "Ovarian Cystectomy", + "Tubal Ligation", + "D&C Procedure" + ] + } + + # Equipment commonly used in Saudi hospitals + MEDICAL_EQUIPMENT = [ + "Da Vinci Surgical Robot", + "C-Arm Fluoroscopy", + "Zeiss Surgical Microscope", + "Harmonic Scalpel", + "LigaSure Device", + "Medtronic Neuromonitoring System", + "Stryker Navigation System", + "Karl Storz Laparoscopic Tower", + "GE Ultrasound Machine", + "Phillips CT Scanner" + ] + + def __init__(self, tenant_id: Optional[str] = None): + """Initialize the data generator.""" + self.tenant_id = tenant_id or str(uuid.uuid4()) + self.generated_surgeons = [] + self.generated_patients = [] + self.generated_rooms = [] + + def generate_saudi_name(self, gender: str = 'male') -> Dict[str, str]: + """Generate a Saudi-style name.""" + if gender.lower() == 'male': + first_name = random.choice(self.SAUDI_MALE_FIRST_NAMES) + else: + first_name = random.choice(self.SAUDI_FEMALE_FIRST_NAMES) + + family_name = random.choice(self.SAUDI_FAMILY_NAMES) + + # Sometimes add a middle name (father's name for males) + if gender.lower() == 'male' and random.random() > 0.5: + middle_name = random.choice(self.SAUDI_MALE_FIRST_NAMES) + full_name = f"Dr. {first_name} {middle_name} {family_name}" + else: + full_name = f"Dr. {first_name} {family_name}" + + return { + 'first_name': first_name, + 'last_name': family_name, + 'full_name': full_name + } + + def generate_operating_room(self, room_number: int) -> Dict[str, Any]: + """Generate operating room data with Saudi context.""" + room_types = ['GENERAL', 'CARDIAC', 'NEURO', 'ORTHOPEDIC', 'OBSTETRIC', + 'PEDIATRIC', 'OPHTHALMOLOGY', 'ENT', 'UROLOGY'] + + room_type = random.choice(room_types) + hospital = random.choice(self.SAUDI_HOSPITALS) + + # Advanced equipment more common in Saudi hospitals + has_advanced_equipment = random.random() > 0.3 + + room_data = { + 'tenant': self.tenant_id, + 'room_id': str(uuid.uuid4()), + 'room_number': f"OR-{room_number:03d}", + 'room_name': f"{hospital} - {room_type.title()} OR {room_number}", + 'room_type': room_type, + 'status': random.choice(['AVAILABLE', 'OCCUPIED', 'CLEANING', 'SETUP']), + 'floor_number': random.randint(1, 5), + 'room_size': round(random.uniform(40, 80), 2), + 'ceiling_height': round(random.uniform(3.0, 4.5), 2), + 'temperature_min': 18.0, + 'temperature_max': 24.0, + 'humidity_min': 30.0, + 'humidity_max': 60.0, + 'air_changes_per_hour': random.randint(20, 25), + 'positive_pressure': True, + 'equipment_list': self._generate_equipment_list(room_type, has_advanced_equipment), + 'special_features': self._generate_special_features(room_type), + 'has_c_arm': room_type in ['ORTHOPEDIC', 'NEURO', 'VASCULAR'] or random.random() > 0.5, + 'has_ct': room_type in ['NEURO', 'CARDIAC'] and has_advanced_equipment, + 'has_mri': room_type == 'NEURO' and has_advanced_equipment and random.random() > 0.7, + 'has_ultrasound': True, + 'has_neuromonitoring': room_type in ['NEURO', 'ORTHOPEDIC'], + 'supports_robotic': has_advanced_equipment and room_type in ['GENERAL', 'UROLOGY', 'CARDIAC'], + 'supports_laparoscopic': room_type in ['GENERAL', 'OBSTETRIC', 'UROLOGY'], + 'supports_microscopy': room_type in ['NEURO', 'OPHTHALMOLOGY', 'ENT'], + 'supports_laser': room_type in ['OPHTHALMOLOGY', 'ENT', 'UROLOGY'], + 'max_case_duration': random.randint(240, 720), + 'turnover_time': random.randint(20, 45), + 'cleaning_time': random.randint(30, 60), + 'required_nurses': random.randint(2, 4), + 'required_techs': random.randint(1, 3), + 'is_active': True, + 'accepts_emergency': room_type != 'OPHTHALMOLOGY', + 'building': f"Building {random.choice(['A', 'B', 'C', 'Main', 'East', 'West'])}", + 'wing': random.choice(['North', 'South', 'East', 'West', 'Central']), + 'created_at': fake.date_time_between(start_date='-2y', end_date='now'), + 'updated_at': fake.date_time_between(start_date='-30d', end_date='now') + } + + return room_data + + def _generate_equipment_list(self, room_type: str, has_advanced: bool) -> List[str]: + """Generate equipment list based on room type.""" + basic_equipment = [ + "Anesthesia Machine", + "Patient Monitor", + "Surgical Lights", + "Operating Table", + "Electrocautery Unit", + "Suction System", + "Instrument Tables" + ] + + specialized_equipment = { + 'CARDIAC': ["Heart-Lung Machine", "IABP", "TEE Machine"], + 'NEURO': ["Surgical Microscope", "Neuronavigation", "CUSA"], + 'ORTHOPEDIC': ["C-Arm", "Power Tools", "Traction Table"], + 'OPHTHALMOLOGY': ["Phaco Machine", "OCT", "YAG Laser"], + 'ENT': ["ENT Microscope", "Shaver System", "Navigation"], + 'UROLOGY': ["Cystoscopy Tower", "Laser System", "Lithotripter"] + } + + equipment = basic_equipment.copy() + + if room_type in specialized_equipment: + equipment.extend(specialized_equipment[room_type]) + + if has_advanced: + equipment.extend(random.sample(self.MEDICAL_EQUIPMENT, k=random.randint(2, 4))) + + return equipment + + def _generate_special_features(self, room_type: str) -> List[str]: + """Generate special features for OR.""" + features = ["Laminar Flow", "HEPA Filtration", "Integrated Video System"] + + special_features = { + 'CARDIAC': ["Hybrid OR Capability", "Cardiac Catheterization"], + 'NEURO': ["Intraoperative MRI Compatible", "Neuromonitoring"], + 'ORTHOPEDIC': ["Laminar Flow Class 100", "Biplane Imaging"], + 'OPHTHALMOLOGY': ["Laser Safety", "Microscope Integration"], + 'OBSTETRIC': ["Neonatal Resuscitation Area", "Fetal Monitoring"] + } + + if room_type in special_features: + features.extend(special_features[room_type]) + + return features + + def generate_or_block(self, operating_room_id: str, block_date: date) -> Dict[str, Any]: + """Generate OR block schedule.""" + surgeon = self.generate_saudi_name('male') + services = ['GENERAL', 'CARDIAC', 'NEURO', 'ORTHOPEDIC', 'OBSTETRIC', + 'OPHTHALMOLOGY', 'ENT', 'UROLOGY'] + + # Saudi working hours typically 7 AM to 3 PM or 8 AM to 4 PM + start_hours = [7, 8, 9, 13, 14] + start_hour = random.choice(start_hours) + duration_hours = random.randint(2, 8) + + block_data = { + 'operating_room': operating_room_id, + 'block_id': str(uuid.uuid4()), + 'date': block_date, + 'start_time': time(start_hour, 0), + 'end_time': time((start_hour + duration_hours) % 24, 0), + 'block_type': random.choice(['SCHEDULED', 'EMERGENCY', 'RESERVED']), + 'primary_surgeon': surgeon['full_name'], + 'service': random.choice(services), + 'status': random.choice(['SCHEDULED', 'ACTIVE', 'COMPLETED']), + 'allocated_minutes': duration_hours * 60, + 'used_minutes': random.randint(duration_hours * 45, duration_hours * 60), + 'special_equipment': random.sample(self.MEDICAL_EQUIPMENT, k=random.randint(0, 3)), + 'special_setup': self._generate_special_setup(), + 'notes': self._generate_block_notes(), + 'created_at': fake.date_time_between(start_date='-30d', end_date='now'), + 'updated_at': fake.date_time_between(start_date='-7d', end_date='now') + } + + return block_data + + def _generate_special_setup(self) -> str: + """Generate special setup requirements.""" + setups = [ + "Prone positioning required", + "Lateral positioning with bean bag", + "Beach chair position", + "Microscope setup required", + "Robot docking from patient left", + "Fluoroscopy setup", + "Neuromonitoring setup required", + "Cell saver required", + "Warming blanket needed" + ] + return random.choice(setups) if random.random() > 0.5 else "" + + def _generate_block_notes(self) -> str: + """Generate block notes.""" + notes = [ + "Complex case - allow extra time", + "Teaching case for residents", + "VIP patient - special protocols", + "Latex allergy - latex-free environment", + "Multi-specialty case", + "Consultant from King Faisal Hospital attending", + "Research protocol case", + "International patient - translator needed" + ] + return random.choice(notes) if random.random() > 0.3 else "" + + def generate_surgical_case(self, or_block_id: str, patient_id: str) -> Dict[str, Any]: + """Generate surgical case data.""" + case_type = random.choice(['ELECTIVE', 'URGENT', 'EMERGENCY']) + service_type = random.choice(list(self.SURGICAL_PROCEDURES.keys())) + procedure = random.choice(self.SURGICAL_PROCEDURES[service_type]) + + # Generate Saudi medical team + primary_surgeon = self.generate_saudi_name('male') + anesthesiologist = self.generate_saudi_name(random.choice(['male', 'female'])) + nurse = self.generate_saudi_name('female') # Nursing often female in Saudi + + scheduled_start = fake.date_time_between(start_date='now', end_date='+30d') + duration = random.randint(30, 360) + + case_data = { + 'or_block': or_block_id, + 'case_id': str(uuid.uuid4()), + 'case_number': f"SURG-{datetime.now().strftime('%Y%m%d')}-{random.randint(1, 9999):04d}", + 'patient': patient_id, + 'primary_surgeon': primary_surgeon['full_name'], + 'anesthesiologist': anesthesiologist['full_name'], + 'circulating_nurse': nurse['full_name'], + 'scrub_nurse': self.generate_saudi_name('female')['full_name'], + 'primary_procedure': procedure, + 'secondary_procedures': self._generate_secondary_procedures(service_type), + 'procedure_codes': self._generate_procedure_codes(), + 'case_type': case_type, + 'approach': random.choice(['OPEN', 'LAPAROSCOPIC', 'ROBOTIC', 'ENDOSCOPIC']), + 'anesthesia_type': random.choice(['GENERAL', 'REGIONAL', 'SPINAL', 'LOCAL']), + 'scheduled_start': scheduled_start, + 'estimated_duration': duration, + 'actual_start': scheduled_start + timedelta(minutes=random.randint(-15, 30)) if random.random() > 0.5 else None, + 'actual_end': scheduled_start + timedelta(minutes=duration + random.randint(-30, 60)) if random.random() > 0.5 else None, + 'status': random.choice(['SCHEDULED', 'IN_PROGRESS', 'COMPLETED', 'DELAYED']), + 'diagnosis': self._generate_diagnosis(), + 'diagnosis_codes': self._generate_diagnosis_codes(), + 'clinical_notes': self._generate_clinical_notes(), + 'special_equipment': random.sample(self.MEDICAL_EQUIPMENT, k=random.randint(1, 3)), + 'blood_products': self._generate_blood_products() if random.random() > 0.7 else [], + 'implants': self._generate_implants(service_type) if random.random() > 0.6 else [], + 'patient_position': random.choice(['SUPINE', 'PRONE', 'LATERAL', 'LITHOTOMY']), + 'complications': [] if random.random() > 0.1 else ["Minor bleeding controlled"], + 'estimated_blood_loss': random.randint(10, 500) if random.random() > 0.3 else None, + 'created_at': fake.date_time_between(start_date='-7d', end_date='now'), + 'updated_at': fake.date_time_between(start_date='-1d', end_date='now') + } + + return case_data + + def _generate_secondary_procedures(self, service_type: str) -> List[str]: + """Generate secondary procedures.""" + if random.random() > 0.6: + procedures = self.SURGICAL_PROCEDURES.get(service_type, []) + return random.sample(procedures, k=min(random.randint(1, 2), len(procedures))) + return [] + + def _generate_procedure_codes(self) -> List[str]: + """Generate CPT procedure codes.""" + # Sample CPT codes + codes = [] + for _ in range(random.randint(1, 3)): + codes.append(f"{random.randint(10000, 99999)}") + return codes + + def _generate_diagnosis(self) -> str: + """Generate diagnosis.""" + diagnoses = [ + "Acute Appendicitis", + "Cholelithiasis", + "Inguinal Hernia", + "Coronary Artery Disease", + "Brain Tumor", + "Degenerative Disc Disease", + "Osteoarthritis", + "Breast Cancer", + "Colorectal Cancer", + "Thyroid Nodule", + "Uterine Fibroids", + "Kidney Stones", + "Prostate Hyperplasia" + ] + return random.choice(diagnoses) + + def _generate_diagnosis_codes(self) -> List[str]: + """Generate ICD-10 diagnosis codes.""" + # Sample ICD-10 codes + codes = [] + for _ in range(random.randint(1, 3)): + letter = random.choice(['K', 'I', 'M', 'C', 'N', 'E']) + codes.append(f"{letter}{random.randint(10, 99)}.{random.randint(0, 9)}") + return codes + + def _generate_clinical_notes(self) -> str: + """Generate clinical notes.""" + notes = [ + "Patient with history of diabetes mellitus type 2, controlled on metformin", + "No known drug allergies. Previous surgery without complications", + "Hypertensive patient on ACE inhibitors, blood pressure stable", + "Patient fasting since midnight as per protocol", + "Preoperative antibiotics administered", + "Patient counseled about procedure risks and benefits", + "Informed consent obtained in Arabic and English" + ] + return random.choice(notes) + + def _generate_blood_products(self) -> List[str]: + """Generate blood product requirements.""" + products = [] + if random.random() > 0.5: + products.append(f"PRBC {random.randint(1, 4)} units") + if random.random() > 0.7: + products.append(f"FFP {random.randint(1, 2)} units") + if random.random() > 0.8: + products.append(f"Platelets {random.randint(1, 2)} units") + return products + + def _generate_implants(self, service_type: str) -> List[str]: + """Generate implant requirements based on service type.""" + implants = { + 'ORTHOPEDIC': [ + "Total Knee Prosthesis - Zimmer", + "Total Hip Prosthesis - Stryker", + "Spinal Fusion Cage - Medtronic", + "ACL Graft", + "Fracture Plate and Screws" + ], + 'CARDIAC': [ + "Mechanical Valve - St. Jude", + "Bioprosthetic Valve", + "Pacemaker - Medtronic", + "Coronary Stent", + "Vascular Graft" + ], + 'NEURO': [ + "VP Shunt", + "Deep Brain Stimulator", + "Cranial Plate", + "Aneurysm Clip" + ], + 'GENERAL': [ + "Mesh for Hernia Repair", + "Gastric Band", + "Biliary Stent" + ] + } + + if service_type in implants: + return random.sample(implants[service_type], k=1) + return [] + + def generate_surgical_note(self, surgical_case_id: str, surgeon_id: str) -> Dict[str, Any]: + """Generate surgical note.""" + note_data = { + 'surgical_case': surgical_case_id, + 'note_id': str(uuid.uuid4()), + 'surgeon': surgeon_id, + 'preoperative_diagnosis': self._generate_diagnosis(), + 'planned_procedure': random.choice([proc for procs in self.SURGICAL_PROCEDURES.values() for proc in procs]), + 'indication': self._generate_indication(), + 'procedure_performed': self._generate_procedure_details(), + 'surgical_approach': self._generate_surgical_approach(), + 'findings': self._generate_findings(), + 'technique': self._generate_technique(), + 'postoperative_diagnosis': self._generate_diagnosis(), + 'condition': random.choice(['STABLE', 'GOOD', 'FAIR']), + 'disposition': random.choice(['RECOVERY', 'ICU', 'WARD']), + 'complications': "None" if random.random() > 0.1 else "Minor bleeding, controlled", + 'estimated_blood_loss': random.randint(10, 500), + 'blood_transfusion': "None" if random.random() > 0.8 else "1 unit PRBC", + 'specimens': self._generate_specimens() if random.random() > 0.5 else None, + 'implants': self._generate_implant_details() if random.random() > 0.6 else None, + 'drains': "JP drain placed" if random.random() > 0.5 else None, + 'closure': self._generate_closure_details(), + 'postop_instructions': self._generate_postop_instructions(), + 'follow_up': "Follow up in 2 weeks in surgical clinic", + 'status': random.choice(['DRAFT', 'COMPLETED', 'SIGNED']), + 'signed_datetime': fake.date_time_between(start_date='-7d', end_date='now') if random.random() > 0.3 else None, + 'created_at': fake.date_time_between(start_date='-7d', end_date='now'), + 'updated_at': fake.date_time_between(start_date='-1d', end_date='now') + } + + return note_data + + def _generate_indication(self) -> str: + """Generate surgical indication.""" + indications = [ + "Symptomatic for 6 months, failed conservative management", + "Progressive symptoms despite medical therapy", + "Acute presentation with signs of peritonitis", + "Elective procedure for quality of life improvement", + "Urgent intervention to prevent complications", + "Diagnostic and therapeutic intervention", + "Staged procedure as per treatment protocol" + ] + return random.choice(indications) + + def _generate_procedure_details(self) -> str: + """Generate detailed procedure description.""" + return "Procedure performed as planned using standard technique with no intraoperative complications" + + def _generate_surgical_approach(self) -> str: + """Generate surgical approach description.""" + approaches = [ + "Midline laparotomy incision", + "Laparoscopic approach with 4 ports", + "Lateral thoracotomy through 5th intercostal space", + "Posterior approach to spine", + "Deltopectoral approach", + "Bikini incision for cesarean section" + ] + return random.choice(approaches) + + def _generate_findings(self) -> str: + """Generate intraoperative findings.""" + findings = [ + "Findings consistent with preoperative diagnosis", + "Adhesions from previous surgery noted and lysed", + "No evidence of metastatic disease", + "Inflammation noted in surrounding tissues", + "Anatomy normal, procedure proceeded as planned" + ] + return random.choice(findings) + + def _generate_technique(self) -> str: + """Generate surgical technique description.""" + return "Standard surgical technique employed with meticulous hemostasis throughout the procedure" + + def _generate_specimens(self) -> str: + """Generate specimen details.""" + specimens = [ + "Appendix sent to pathology", + "Gallbladder sent to pathology", + "Lymph nodes sent for frozen section", + "Tissue biopsy sent to pathology", + "Tumor specimen sent with margins marked" + ] + return random.choice(specimens) + + def _generate_implant_details(self) -> str: + """Generate implant details.""" + return "Implant placed per manufacturer protocol, position confirmed with imaging" + + def _generate_closure_details(self) -> str: + """Generate closure details.""" + closures = [ + "Layered closure with absorbable sutures", + "Skin closed with staples", + "Subcuticular closure with absorbable sutures", + "Wound closed in layers, dressing applied", + "Closure with 3-0 Vicryl and skin adhesive" + ] + return random.choice(closures) + + def _generate_postop_instructions(self) -> str: + """Generate postoperative instructions.""" + instructions = [ + "NPO until bowel sounds return, advance diet as tolerated", + "Ambulate within 6 hours, incentive spirometry every hour", + "Pain control with PCA, transition to oral when tolerating diet", + "DVT prophylaxis with heparin, sequential compression devices", + "Monitor vitals every 4 hours, daily labs", + "Antibiotics for 24 hours postoperatively", + "Foley catheter to be removed POD 1" + ] + return "; ".join(random.sample(instructions, k=random.randint(2, 4))) + + def generate_equipment_usage(self, surgical_case_id: str) -> List[Dict[str, Any]]: + """Generate equipment usage records.""" + equipment_records = [] + num_equipment = random.randint(3, 8) + + for _ in range(num_equipment): + equipment_type = random.choice([ + 'SURGICAL_INSTRUMENT', 'MONITORING_DEVICE', 'ELECTROCAUTERY', + 'LASER', 'MICROSCOPE', 'ULTRASOUND', 'DISPOSABLE' + ]) + + equipment_name = self._get_equipment_name(equipment_type) + + usage_data = { + 'surgical_case': surgical_case_id, + 'usage_id': str(uuid.uuid4()), + 'equipment_name': equipment_name, + 'equipment_type': equipment_type, + 'manufacturer': self._get_manufacturer(), + 'model': f"Model-{random.randint(100, 999)}", + 'serial_number': f"SN{random.randint(100000, 999999)}", + 'quantity_used': random.randint(1, 5), + 'unit_of_measure': random.choice(['EACH', 'SET', 'PACK', 'BOX']), + 'start_time': fake.date_time_between(start_date='-1d', end_date='now'), + 'end_time': fake.date_time_between(start_date='now', end_date='+4h'), + 'unit_cost': Decimal(str(round(random.uniform(10, 5000), 2))), + 'lot_number': f"LOT{random.randint(10000, 99999)}", + 'expiration_date': fake.date_between(start_date='+30d', end_date='+2y'), + 'sterilization_date': fake.date_between(start_date='-7d', end_date='today'), + 'notes': self._generate_equipment_notes(), + 'created_at': fake.date_time_between(start_date='-1d', end_date='now'), + 'updated_at': fake.date_time_between(start_date='-1h', end_date='now') + } + + equipment_records.append(usage_data) + + return equipment_records + + def _get_equipment_name(self, equipment_type: str) -> str: + """Get equipment name based on type.""" + equipment_names = { + 'SURGICAL_INSTRUMENT': [ + "Harmonic Scalpel", "LigaSure Device", "Surgical Stapler", + "Laparoscopic Grasper Set", "Retractor Set", "Scalpel Set" + ], + 'MONITORING_DEVICE': [ + "Cardiac Monitor", "Pulse Oximeter", "BIS Monitor", + "Arterial Line Monitor", "Central Line Kit" + ], + 'ELECTROCAUTERY': [ + "Bovie Electrocautery", "Bipolar Forceps", "Monopolar Cautery" + ], + 'LASER': [ + "CO2 Laser", "YAG Laser", "Holmium Laser", "Argon Laser" + ], + 'MICROSCOPE': [ + "Zeiss OPMI", "Leica Surgical Microscope", "Pentero Microscope" + ], + 'ULTRASOUND': [ + "GE Ultrasound", "Phillips EPIQ", "Sonosite Edge" + ], + 'DISPOSABLE': [ + "Surgical Drape Set", "Gown Pack", "Suture Set", + "Surgical Gloves", "Sponge Pack" + ] + } + + return random.choice(equipment_names.get(equipment_type, ["Generic Equipment"])) + + def _get_manufacturer(self) -> str: + """Get equipment manufacturer.""" + manufacturers = [ + "Medtronic", "Johnson & Johnson", "Stryker", "Boston Scientific", + "Abbott", "GE Healthcare", "Siemens", "Phillips", "Karl Storz", + "Olympus", "Zimmer Biomet", "Smith & Nephew", "B. Braun" + ] + return random.choice(manufacturers) + + def _generate_equipment_notes(self) -> str: + """Generate equipment usage notes.""" + notes = [ + "Equipment functioning properly", + "Calibrated before use", + "Backup equipment available", + "Special settings documented", + "Used per protocol", + "" + ] + return random.choice(notes) + + def generate_surgical_note_template(self) -> Dict[str, Any]: + """Generate surgical note template.""" + specialties = ['ALL', 'GENERAL', 'CARDIAC', 'NEURO', 'ORTHOPEDIC', + 'OBSTETRIC', 'OPHTHALMOLOGY', 'ENT', 'UROLOGY'] + + specialty = random.choice(specialties) + + template_data = { + 'tenant': self.tenant_id, + 'template_id': str(uuid.uuid4()), + 'name': f"{specialty} Surgery Template - {random.choice(['Standard', 'Complex', 'Emergency'])}", + 'description': f"Standard template for {specialty.lower()} surgical procedures", + 'procedure_type': random.choice(self.SURGICAL_PROCEDURES.get(specialty, ["General Procedure"])) if specialty != 'ALL' else None, + 'specialty': specialty, + 'preoperative_diagnosis_template': "Preoperative Diagnosis: [Diagnosis]", + 'planned_procedure_template': "Planned Procedure: [Procedure Name]", + 'indication_template': "Indication: Patient presents with [symptoms] requiring surgical intervention", + 'procedure_performed_template': "Procedure Performed: [Actual procedure]", + 'surgical_approach_template': "Approach: [Describe surgical approach]", + 'findings_template': "Findings: [Describe intraoperative findings]", + 'technique_template': "Technique: [Describe surgical technique in detail]", + 'postoperative_diagnosis_template': "Postoperative Diagnosis: [Final diagnosis]", + 'complications_template': "Complications: [None/Describe if any]", + 'specimens_template': "Specimens: [List specimens sent to pathology]", + 'implants_template': "Implants: [List any implants used]", + 'closure_template': "Closure: [Describe closure technique]", + 'postop_instructions_template': "Postoperative Instructions: [List instructions]", + 'is_active': True, + 'is_default': random.random() > 0.7, + 'usage_count': random.randint(0, 100), + 'created_at': fake.date_time_between(start_date='-1y', end_date='now'), + 'updated_at': fake.date_time_between(start_date='-30d', end_date='now') + } + + return template_data + + def generate_complete_dataset(self, + num_rooms: int = 10, + num_blocks_per_room: int = 5, + num_cases_per_block: int = 3) -> Dict[str, List]: + """Generate complete dataset for all models.""" + + dataset = { + 'operating_rooms': [], + 'or_blocks': [], + 'surgical_cases': [], + 'surgical_notes': [], + 'equipment_usage': [], + 'surgical_note_templates': [] + } + + # Generate operating rooms + for i in range(1, num_rooms + 1): + room = self.generate_operating_room(i) + dataset['operating_rooms'].append(room) + self.generated_rooms.append(room) + + # Generate OR blocks for each room + for j in range(num_blocks_per_room): + block_date = fake.date_between(start_date='today', end_date='+30d') + block = self.generate_or_block(room['room_id'], block_date) + dataset['or_blocks'].append(block) + + # Generate surgical cases for each block + for k in range(random.randint(1, num_cases_per_block)): + # Generate a patient ID (placeholder) + patient_id = str(uuid.uuid4()) + + case = self.generate_surgical_case(block['block_id'], patient_id) + dataset['surgical_cases'].append(case) + + # Generate surgical note for completed cases + if case['status'] == 'COMPLETED': + note = self.generate_surgical_note(case['case_id'], block['primary_surgeon']) + dataset['surgical_notes'].append(note) + + # Generate equipment usage + equipment_usage = self.generate_equipment_usage(case['case_id']) + dataset['equipment_usage'].extend(equipment_usage) + + # Generate surgical note templates + for _ in range(15): + template = self.generate_surgical_note_template() + dataset['surgical_note_templates'].append(template) + + return dataset + + def print_statistics(self, dataset: Dict[str, List]) -> None: + """Print statistics about generated data.""" + print("\n" + "="*60) + print("Saudi Operating Theatre Data Generation Complete") + print("="*60) + print(f"Operating Rooms Generated: {len(dataset['operating_rooms'])}") + print(f"OR Blocks Generated: {len(dataset['or_blocks'])}") + print(f"Surgical Cases Generated: {len(dataset['surgical_cases'])}") + print(f"Surgical Notes Generated: {len(dataset['surgical_notes'])}") + print(f"Equipment Usage Records: {len(dataset['equipment_usage'])}") + print(f"Surgical Note Templates: {len(dataset['surgical_note_templates'])}") + print("="*60) + + # Room type distribution + room_types = {} + for room in dataset['operating_rooms']: + room_type = room['room_type'] + room_types[room_type] = room_types.get(room_type, 0) + 1 + + print("\nOperating Room Types:") + for room_type, count in sorted(room_types.items()): + print(f" {room_type}: {count}") + + # Case type distribution + case_types = {} + for case in dataset['surgical_cases']: + case_type = case['case_type'] + case_types[case_type] = case_types.get(case_type, 0) + 1 + + print("\nSurgical Case Types:") + for case_type, count in sorted(case_types.items()): + print(f" {case_type}: {count}") + + # Status distribution + case_statuses = {} + for case in dataset['surgical_cases']: + status = case['status'] + case_statuses[status] = case_statuses.get(status, 0) + 1 + + print("\nCase Status Distribution:") + for status, count in sorted(case_statuses.items()): + print(f" {status}: {count}") + + +# Example usage +if __name__ == "__main__": + # Initialize generator + generator = SaudiOperatingTheatreDataGenerator() + + # Generate complete dataset + dataset = generator.generate_complete_dataset( + num_rooms=8, + num_blocks_per_room=4, + num_cases_per_block=3 + ) + + # Print statistics + generator.print_statistics(dataset) + + # Example: Print first operating room + if dataset['operating_rooms']: + print("\n" + "="*60) + print("Sample Operating Room:") + print("="*60) + room = dataset['operating_rooms'][0] + for key, value in room.items(): + if key not in ['equipment_list', 'special_features']: + print(f"{key}: {value}") + + # Example: Print first surgical case + if dataset['surgical_cases']: + print("\n" + "="*60) + print("Sample Surgical Case:") + print("="*60) + case = dataset['surgical_cases'][0] + for key, value in case.items(): + if key not in ['special_equipment', 'blood_products', 'implants']: + print(f"{key}: {value}") \ No newline at end of file diff --git a/templates/.DS_Store b/templates/.DS_Store index 97c6de1b36f026fb05349841f6543864c3fbf8a8..2a9416b44f5e191e8382e395f2c690545f7e4d0d 100644 GIT binary patch delta 46 vcmZpfz}PZ@af7?@WErVFn^{akm>Ib@N0^JUZO$`&#Ry{CL)nwV)z<<5VrmaI delta 90 zcmZpfz}PZ@af7=t +.order-header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border-radius: 0.5rem; + padding: 2rem; + margin-bottom: 2rem; +} + +.order-info-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.info-card { + background: white; + border: 1px solid #dee2e6; + border-radius: 0.5rem; + padding: 1.5rem; + text-align: center; + transition: transform 0.2s, box-shadow 0.2s; +} + +.info-card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0,0,0,0.1); +} + +.info-icon { + width: 50px; + height: 50px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 1rem; + color: white; + font-size: 1.25rem; +} + +.info-value { + font-size: 1.5rem; + font-weight: bold; + color: #495057; + margin-bottom: 0.5rem; +} + +.info-label { + color: #6c757d; + font-size: 0.875rem; + font-weight: 600; + text-transform: uppercase; +} + +.section-card { + background: white; + border: 1px solid #dee2e6; + border-radius: 0.5rem; + margin-bottom: 2rem; + overflow: hidden; +} + +.section-header { + background: #f8f9fa; + border-bottom: 1px solid #dee2e6; + padding: 1rem 1.5rem; + font-weight: 600; + color: #495057; + display: flex; + justify-content: between; + align-items: center; +} + +.section-content { + padding: 1.5rem; +} + +.status-badge { + padding: 0.5rem 1rem; + border-radius: 0.25rem; + font-size: 0.875rem; + font-weight: 600; + text-transform: uppercase; +} + +.status-draft { background: #f8f9fa; color: #6c757d; } +.status-pending { background: #fff3cd; color: #856404; } +.status-approved { background: #d1ecf1; color: #0c5460; } +.status-ordered { background: #d4edda; color: #155724; } +.status-received { background: #d4edda; color: #155724; } +.status-cancelled { background: #f8d7da; color: #721c24; } + +.priority-badge { + padding: 0.375rem 0.75rem; + border-radius: 0.25rem; + font-size: 0.75rem; + font-weight: 600; +} + +.priority-low { background: #d4edda; color: #155724; } +.priority-medium { background: #fff3cd; color: #856404; } +.priority-high { background: #f8d7da; color: #721c24; } +.priority-urgent { background: #f5c6cb; color: #721c24; } + +.supplier-card { + background: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 0.375rem; + padding: 1.5rem; + margin-bottom: 1.5rem; +} + +.supplier-avatar { + width: 60px; + height: 60px; + border-radius: 50%; + background: #007bff; + color: white; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 1rem; +} + +.items-table { + margin-bottom: 0; +} + +.item-row { + border-bottom: 1px solid #f1f3f4; + transition: background-color 0.2s; +} + +.item-row:hover { + background: #f8f9fa; +} + +.item-image { + width: 40px; + height: 40px; + border-radius: 0.25rem; + object-fit: cover; + background: #f8f9fa; + border: 1px solid #dee2e6; +} + +.item-details { + flex: 1; +} + +.item-name { + font-weight: 600; + color: #495057; + margin-bottom: 0.25rem; +} + +.item-description { + font-size: 0.875rem; + color: #6c757d; + margin-bottom: 0.25rem; +} + +.item-specs { + font-size: 0.75rem; + color: #adb5bd; +} + +.quantity-info { + text-align: center; +} + +.quantity-number { + font-size: 1.25rem; + font-weight: bold; + color: #495057; +} + +.quantity-unit { + font-size: 0.75rem; + color: #6c757d; + text-transform: uppercase; +} + +.price-info { + text-align: right; +} + +.unit-price { + font-size: 0.875rem; + color: #6c757d; + margin-bottom: 0.25rem; +} + +.total-price { + font-size: 1.1rem; + font-weight: bold; + color: #28a745; +} + +.order-totals { + background: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 0.375rem; + padding: 1.5rem; +} + +.total-row { + display: flex; + justify-content: between; + align-items: center; + padding: 0.5rem 0; + border-bottom: 1px solid #dee2e6; +} + +.total-row:last-child { + border-bottom: none; + font-weight: bold; + font-size: 1.1rem; + color: #495057; +} + +.timeline { + position: relative; + padding-left: 2rem; +} + +.timeline::before { + content: ''; + position: absolute; + left: 0.75rem; + top: 0; + bottom: 0; + width: 2px; + background: #dee2e6; +} + +.timeline-item { + position: relative; + margin-bottom: 2rem; +} + +.timeline-item::before { + content: ''; + position: absolute; + left: -0.5rem; + top: 0.25rem; + width: 1rem; + height: 1rem; + border-radius: 50%; + background: white; + border: 3px solid #007bff; +} + +.timeline-item.completed::before { + background: #28a745; + border-color: #28a745; +} + +.timeline-item.current::before { + background: #ffc107; + border-color: #ffc107; +} + +.timeline-content { + background: white; + border: 1px solid #dee2e6; + border-radius: 0.375rem; + padding: 1rem; + margin-left: 1rem; +} + +.timeline-title { + font-weight: 600; + color: #495057; + margin-bottom: 0.5rem; +} + +.timeline-description { + font-size: 0.875rem; + color: #6c757d; + margin-bottom: 0.5rem; +} + +.timeline-meta { + font-size: 0.75rem; + color: #adb5bd; +} + +.documents-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 1rem; +} + +.document-card { + background: white; + border: 1px solid #dee2e6; + border-radius: 0.375rem; + padding: 1rem; + text-align: center; + transition: all 0.2s; + cursor: pointer; +} + +.document-card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0,0,0,0.1); +} + +.document-icon { + width: 40px; + height: 40px; + border-radius: 0.25rem; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 0.75rem; + color: white; + font-size: 1rem; +} + +.doc-pdf { background: #dc3545; } +.doc-excel { background: #28a745; } +.doc-word { background: #007bff; } +.doc-image { background: #6f42c1; } + +.document-name { + font-weight: 600; + color: #495057; + margin-bottom: 0.25rem; + font-size: 0.875rem; +} + +.document-size { + font-size: 0.75rem; + color: #6c757d; +} + +.action-buttons { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.notes-section { + background: #fff3cd; + border: 1px solid #ffeaa7; + border-radius: 0.375rem; + padding: 1rem; + margin-bottom: 1.5rem; +} + +.note-item { + background: white; + border: 1px solid #ffeaa7; + border-radius: 0.25rem; + padding: 1rem; + margin-bottom: 1rem; +} + +.note-item:last-child { + margin-bottom: 0; +} + +.note-header { + display: flex; + justify-content: between; + align-items: center; + margin-bottom: 0.5rem; +} + +.note-author { + font-weight: 600; + color: #495057; +} + +.note-date { + font-size: 0.75rem; + color: #6c757d; +} + +.note-content { + color: #495057; + line-height: 1.5; +} + +.approval-workflow { + background: #e3f2fd; + border: 1px solid #bbdefb; + border-radius: 0.375rem; + padding: 1.5rem; + margin-bottom: 1.5rem; +} + +.approval-step { + display: flex; + align-items: center; + padding: 1rem; + background: white; + border: 1px solid #bbdefb; + border-radius: 0.25rem; + margin-bottom: 1rem; +} + +.approval-step:last-child { + margin-bottom: 0; +} + +.approval-icon { + width: 40px; + height: 40px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-right: 1rem; + color: white; + font-size: 1rem; +} + +.step-pending { background: #6c757d; } +.step-approved { background: #28a745; } +.step-rejected { background: #dc3545; } +.step-current { background: #ffc107; } + +.approval-details { + flex: 1; +} + +.approval-title { + font-weight: 600; + color: #495057; + margin-bottom: 0.25rem; +} + +.approval-user { + font-size: 0.875rem; + color: #6c757d; + margin-bottom: 0.25rem; +} + +.approval-date { + font-size: 0.75rem; + color: #adb5bd; +} + +@media (max-width: 768px) { + .order-header { + padding: 1.5rem; + } + + .order-info-grid { + grid-template-columns: repeat(2, 1fr); + gap: 1rem; + } + + .section-content { + padding: 1rem; + } + + .action-buttons { + justify-content: center; + } + + .timeline { + padding-left: 1.5rem; + } + + .documents-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media print { + .action-buttons, .btn { + display: none !important; + } + + .section-card { + break-inside: avoid; + margin-bottom: 1rem; + } + + .section-header { + background: none; + border-bottom: 2px solid #000; + color: #000; + } + + .order-header { + background: none; + color: #000; + border: 2px solid #000; + } +} + +{% endblock %} + +{% block content %} +
+ +
+
+ +

+ Purchase Order Details +

+
+
+
+ + + + {% if order.can_edit %} + + Edit + + {% endif %} + {% if order.can_approve %} + + {% endif %} + {% if order.can_cancel %} + + {% endif %} +
+
+
+ + +
+
+
+

{{ order.order_number }}

+

{{ order.title|default:"Purchase Order" }}

+
+ + {{ order.get_status_display }} + + + {{ order.get_priority_display }} Priority + +
+
+
+
Total Amount
+
ê{{ order.total_amount|floatformat:'2g' }}
+
{{ order.total_items }} items
+
+
+
+ + +
+
+
+ +
+
{{ order.created_at|date:"M d, Y" }}
+
Date Created
+
+ +
+
+ +
+
+ {% if order.expected_delivery_date %} + {{ order.expected_delivery_date|date:"M d, Y" }} + {% else %} + Not Set + {% endif %} +
+
Expected Delivery
+
+ +
+
+ +
+
{{ order.created_by.get_full_name }}
+
Created By
+
+ +
+
+ +
+
{{ order.department.name|default:"General" }}
+
Department
+
+ +
+
+ +
+
{{ order.reference_number|default:"N/A" }}
+
Reference Number
+
+
+ +
+
+ +
+
+
+ Supplier Information +
+ + View Supplier + +
+
+
+
+
+
+ {{ order.supplier.name.0|upper }} +
+
+
+
{{ order.supplier.name }}
+

+ {{ order.supplier.contact_person|default:"N/A" }} +

+

+ {{ order.supplier.email|default:"N/A" }} +

+

+ {{ order.supplier.phone|default:"N/A" }} +

+
+
+
Billing Address
+
+ {{ order.supplier.address|default:"Address not available" }}
+ {% if order.supplier.city %}{{ order.supplier.city }}, {% endif %} + {% if order.supplier.state %}{{ order.supplier.state }} {% endif %} + {% if order.supplier.zip_code %}{{ order.supplier.zip_code }}{% endif %} +
+
+
+
+
+
+ + +
+
+
+ Order Items ({{ order.items.count }}) +
+
+ + +
+
+
+
+ + + + + + + + + + + + + {% for item in order.items.all %} + + + + + + + + + {% endfor %} + +
ImageItem DetailsQuantityUnit PriceTotal PriceStatus
+ {% if item.product.image %} + {{ item.product.name }} + {% else %} +
+ +
+ {% endif %} +
+
+
{{ item.product.name }}
+
{{ item.product.description|truncatechars:100 }}
+
+ SKU: {{ item.product.sku|default:"N/A" }} | + Category: {{ item.product.category|default:"N/A" }} +
+
+
+
+
{{ item.quantity }}
+
{{ item.unit|default:"pcs" }}
+
+
+
+
ê{{ item.unit_price|floatformat:'2g' }}
+
+
+
+
ê{{ item.total_price|floatformat:'2g' }}
+
+
+ + {{ item.get_status_display|default:"Pending" }} + +
+
+
+
+ + +
+
+ Order Summary +
+
+
+
+
+
+ Subtotal: + ê{{ order.subtotal|floatformat:'2g' }} +
+ {% if order.discount_amount %} +
+ Discount ({{ order.discount_percentage }}%): + -ê{{ order.discount_amount|floatformat:'2g' }} +
+ {% endif %} + {% if order.tax_amount %} +
+ Tax ({{ order.tax_percentage }}%): + ê{{ order.tax_amount|floatformat:'2g' }} +
+ {% endif %} + {% if order.shipping_cost %} +
+ Shipping: + ê{{ order.shipping_cost|floatformat:'2g' }} +
+ {% endif %} +
+ Total Amount: + ê{{ order.total_amount|floatformat:'2g' }} +
+
+
+
+
Payment Terms
+

{{ order.payment_terms|default:"Net 30 days" }}

+ +
Delivery Terms
+

{{ order.delivery_terms|default:"FOB Destination" }}

+ + {% if order.special_instructions %} +
Special Instructions
+

{{ order.special_instructions }}

+ {% endif %} +
+
+
+
+ + + {% if order.documents.exists %} +
+
+
+ Attachments ({{ order.documents.count }}) +
+ +
+
+
+ {% for document in order.documents.all %} +
+
+ +
+
{{ document.name }}
+
{{ document.file.size|filesizeformat }}
+
+ {% endfor %} +
+
+
+ {% endif %} +
+ +
+ + {% if order.approval_workflow %} +
+
+ Approval Workflow +
+
+
+ {% for step in order.approval_steps %} +
+
+ {% if step.status == 'approved' %} + + {% elif step.status == 'rejected' %} + + {% elif step.status == 'current' %} + + {% else %} + + {% endif %} +
+
+
{{ step.title }}
+
{{ step.approver.get_full_name }}
+ {% if step.approved_at %} +
{{ step.approved_at|date:"M d, Y g:i A" }}
+ {% endif %} +
+
+ {% endfor %} +
+
+
+ {% endif %} + + +
+
+ Order Timeline +
+
+
+ {% for event in order.timeline %} +
+
+
{{ event.title }}
+
{{ event.description }}
+
+ {% if event.user %}{{ event.user.get_full_name }} • {% endif %} + {{ event.created_at|date:"M d, Y g:i A" }} +
+
+
+ {% endfor %} +
+
+
+ + + {% if order.notes.exists %} +
+
+
+ Notes ({{ order.notes.count }}) +
+ +
+
+
+ {% for note in order.notes.all %} +
+
+
{{ note.created_by.get_full_name }}
+
{{ note.created_at|date:"M d, Y g:i A" }}
+
+
{{ note.content }}
+
+ {% endfor %} +
+
+
+ {% endif %} + + +
+
+ Quick Actions +
+
+
+ {% if order.can_send_to_supplier %} + + {% endif %} + + {% if order.can_mark_received %} + + {% endif %} + + + + + + +
+
+
+
+
+
+ + + + + + +{% endblock %} + +{% block js %} + +{% endblock %} + diff --git a/templates/inventory/orders/purchase_order_form.html b/templates/inventory/orders/purchase_order_form.html new file mode 100644 index 00000000..4d2d3da1 --- /dev/null +++ b/templates/inventory/orders/purchase_order_form.html @@ -0,0 +1,1505 @@ +{% extends 'base.html' %} +{% load static %} + +{% block title %}{% if order.pk %}Edit{% else %}Create{% endif %} Purchase Order{% endblock %} + +{% block css %} + + + +{% endblock %} + +{% block content %} +
+ +
+ Auto-saved +
+ + +
+
+ +

+ + {% if order.pk %}Edit Purchase Order{% else %}Create Purchase Order{% endif %} +

+
+
+ + + + Back to List + +
+
+ + +
+
+ Basic Info +
+
+ Supplier +
+
+ Items +
+
+ Review +
+
+ +
+ {% csrf_token %} + + + {% if not order.pk %} +
+
+ Quick Start Templates +
+

Choose a template to pre-populate common order types

+ +
+
+
+ Medical Supplies +
+
+
+ Office Supplies +
+
+
+ Equipment +
+
+
+ Pharmaceuticals +
+
+
+ Maintenance +
+
+
+ Blank Order +
+
+
+ {% endif %} + + +
+
+
+ Basic Information +
+
Step 1 of 4
+
+
+
+
+
+ {{ form.title }} + + {% if form.title.errors %} +
{{ form.title.errors.0 }}
+ {% endif %} +
Enter a descriptive title for this purchase order
+
+
+ +
+
+ + {{ form.priority }} + {% if form.priority.errors %} +
{{ form.priority.errors.0 }}
+ {% endif %} +
Set the urgency level
+
+
+ +
+
+ + {{ form.department }} + {% if form.department.errors %} +
{{ form.department.errors.0 }}
+ {% endif %} +
Requesting department
+
+
+
+ +
+
+
+ + {{ form.expected_delivery_date }} + {% if form.expected_delivery_date.errors %} +
{{ form.expected_delivery_date.errors.0 }}
+ {% endif %} +
When do you need these items delivered?
+
+
+ +
+
+ + {{ form.reference_number }} + {% if form.reference_number.errors %} +
{{ form.reference_number.errors.0 }}
+ {% endif %} +
Internal reference or requisition number
+
+
+
+ +
+ + {{ form.description }} + {% if form.description.errors %} +
{{ form.description.errors.0 }}
+ {% endif %} +
Brief description of the purpose of this order
+
+
+
+ + +
+
+
+ Supplier Information +
+
Step 2 of 4
+
+
+
+
+
+ + {{ form.supplier }} + {% if form.supplier.errors %} +
{{ form.supplier.errors.0 }}
+ {% endif %} +
Select the supplier for this order
+
+
+ +
+
+ +
+ +
+
+
+
+ + +
+
+
S
+
+
Supplier Name
+

Contact Person

+

Email Address

+
+
+
Rating
+
+ + + + + +
+
+
+
+ +
+
+
+ + {{ form.payment_terms }} + {% if form.payment_terms.errors %} +
{{ form.payment_terms.errors.0 }}
+ {% endif %} +
Payment terms for this order
+
+
+ +
+
+ + {{ form.delivery_terms }} + {% if form.delivery_terms.errors %} +
{{ form.delivery_terms.errors.0 }}
+ {% endif %} +
Delivery terms and conditions
+
+
+ +
+
+ + {{ form.shipping_method }} + {% if form.shipping_method.errors %} +
{{ form.shipping_method.errors.0 }}
+ {% endif %} +
Preferred shipping method
+
+
+
+
+
+ + +
+
+
+ Order Items +
+
Step 3 of 4
+
+
+ +
+
+ Quick Add Item +
+ +
+ + +
+ +
+ +
+ +
+ +
+ +
+
+
+ + +
+
+
+ + + + + + + + + + + + + +
ItemQuantityUnit PriceTotalActions
+
+
+ +
+ Add Another Item +
+
+ + +
+
+ Subtotal: +
+ ê0.00 +
+
+ +
+ Discount: +
+ + % + ê0.00 +
+
+ +
+ Tax: +
+ + % + ê0.00 +
+
+ +
+ Shipping: +
+ + ê0.00 +
+
+ +
+ Total Amount: +
+ ê0.00 +
+
+
+
+
+ + +
+
+
+ Additional Information +
+
Step 4 of 4
+
+
+
+ + {{ form.special_instructions }} + {% if form.special_instructions.errors %} +
{{ form.special_instructions.errors.0 }}
+ {% endif %} +
Any special instructions or requirements for this order
+
+ +
+
+
+ {{ form.requires_approval }} + +
Check if this order needs approval before sending to supplier
+
+
+ +
+
+ {{ form.urgent_delivery }} + +
Check if this is an urgent delivery request
+
+
+
+ +
+ + {{ form.internal_notes }} + {% if form.internal_notes.errors %} +
{{ form.internal_notes.errors.0 }}
+ {% endif %} +
Internal notes (not visible to supplier)
+
+
+
+ + +
+
+ + +
+ +
+ + Cancel + + + +
+
+
+
+ + + + + + +{% endblock %} + +{% block js %} + + + + +{% endblock %} + diff --git a/templates/inventory/orders/purchase_order_list.html b/templates/inventory/orders/purchase_order_list.html new file mode 100644 index 00000000..166248fb --- /dev/null +++ b/templates/inventory/orders/purchase_order_list.html @@ -0,0 +1,981 @@ +{% extends 'base.html' %} +{% load static %} + +{% block title %}Purchase Orders{% endblock %} + +{% block css %} + + + + +{% endblock %} + +{% block content %} +
+ +
+
+ +

+ Purchase Orders +

+
+
+ + + + New Purchase Order + +
+
+ + +
+
+
+ +
+
{{ stats.total_orders|default:0 }}
+
Total Orders
+
+ +
+
+ +
+
{{ stats.pending_orders|default:0 }}
+
Pending Orders
+
+ +
+
+ +
+
{{ stats.approved_orders|default:0 }}
+
Approved Orders
+
+ +
+
+ +
+
{{ stats.received_orders|default:0 }}
+
Received Orders
+
+ +
+
+ +
+
ê{{ stats.total_value|default:0|floatformat:0 }}
+
Total Value
+
+
+ + + + + +
+
+ Advanced Filters +
+ +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + Clear + +
+
+
+
+ + +
+
+
+ 0 orders selected +
+
+ + + +
+
+
+ + +
+
+
+ Purchase Orders List +
+
+
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + {% for order in orders %} + + + + + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
+
+ +
+
Order NumberSupplierDate CreatedExpected DeliveryItemsTotal AmountPriorityStatusCreated ByActions
+
+ +
+
+
{{ order.order_number }}
+ {% if order.reference_number %} + Ref: {{ order.reference_number }} + {% endif %} +
+
+
+ {{ order.supplier.name.0|upper }} +
+
+
{{ order.supplier.name }}
+ {{ order.supplier.contact_person|default:"" }} +
+
+
+
{{ order.created_at|date:"M d, Y" }}
+ {{ order.created_at|time:"g:i A" }} +
+ {% if order.expected_delivery_date %} +
{{ order.expected_delivery_date|date:"M d, Y" }}
+ + {% if order.is_overdue %} + Overdue + {% else %} + {{ order.days_until_delivery }} days + {% endif %} + + {% else %} + Not set + {% endif %} +
+
+
+
{{ order.total_items }} items
+ {{ order.unique_items }} unique +
+
+
+
ê{{ order.total_amount|floatformat:'2g' }}
+ {% if order.tax_amount %} + +ê{{ order.tax_amount|floatformat:'2g' }} VAT + {% endif %} +
+ + {{ order.get_priority_display }} + + + + {{ order.get_status_display }} + + +
{{ order.created_by.get_full_name }}
+ {{ order.created_by.department|default:"" }} +
+
+ + + + {% if order.can_edit %} + + + + {% endif %} + {% if order.can_approve %} + + {% endif %} + {% if order.can_delete %} + + {% endif %} +
+
+
+ +
No Purchase Orders Found
+

No purchase orders match your current filters.

+ + Create First Order + +
+
+
+ + + +
+ + {% if is_paginated %} + {% include 'partial/pagination.html'%} + {% endif %} +
+
+
+ + + + + + +{% endblock %} + +{% block js %} + + + + + + +{% endblock %} + diff --git a/templates/inventory/stock/stock_form.html b/templates/inventory/stock/stock_form.html index 45ef2049..2ffaee96 100644 --- a/templates/inventory/stock/stock_form.html +++ b/templates/inventory/stock/stock_form.html @@ -24,14 +24,18 @@
-
-
-

- - Stock Information -

-
-
+
+
+

{{ _("Stock Information")}}

+
+{# #} +{# #} +{# #} +{# #} + +
+
+
{% if messages %} {% for message in messages %}