From f0a01d026be228f0fed126e5c625ce648b2ffb6f Mon Sep 17 00:00:00 2001 From: pjpatil12 Date: Mon, 23 Mar 2026 11:37:15 +0530 Subject: [PATCH] report code changes by pankaj --- .env | 9 + .gitignore | 8 - AppCode/Auth.py | 54 - AppCode/Block.py | 108 - AppCode/ItemCRUD.py | 248 - AppCode/ReportGenerator.py | 0 AppCode/State.py | 246 - AppRoutes/StateRoute.py | 0 README.md | 8 - Reconciliation-Product.code-workspace | 8 - Version-1.code-workspace | 8 - __pycache__/config.cpython-313.pyc | Bin 0 -> 916 bytes __pycache__/config.cpython-314.pyc | Bin 0 -> 940 bytes activity.log | 2898 +++++++++- config.py | 13 +- .../auth_controller.cpython-313.pyc | Bin 0 -> 2305 bytes .../auth_controller.cpython-314.pyc | Bin 0 -> 2340 bytes .../block_controller.cpython-313.pyc | Bin 0 -> 4515 bytes .../block_controller.cpython-314.pyc | Bin 0 -> 4574 bytes .../district_controller.cpython-313.pyc | Bin 0 -> 3302 bytes .../district_controller.cpython-314.pyc | Bin 0 -> 3411 bytes .../excel_upload_controller.cpython-313.pyc | Bin 0 -> 18565 bytes .../excel_upload_controller.cpython-314.pyc | Bin 0 -> 20763 bytes .../gst_release_controller.cpython-313.pyc | Bin 0 -> 4485 bytes .../gst_release_controller.cpython-314.pyc | Bin 0 -> 4717 bytes .../hold_types_controller.cpython-313.pyc | Bin 0 -> 2905 bytes .../hold_types_controller.cpython-314.pyc | Bin 0 -> 2960 bytes .../invoice_controller.cpython-313.pyc | Bin 0 -> 5262 bytes .../invoice_controller.cpython-314.pyc | Bin 0 -> 5385 bytes .../log_controller.cpython-313.pyc | Bin 0 -> 1259 bytes .../log_controller.cpython-314.pyc | Bin 0 -> 1289 bytes .../payment_controller.cpython-313.pyc | Bin 0 -> 5715 bytes .../payment_controller.cpython-314.pyc | Bin 0 -> 5939 bytes .../pmc_report_controller.cpython-313.pyc | Bin 0 -> 1655 bytes .../pmc_report_controller.cpython-314.pyc | Bin 0 -> 1662 bytes .../report_controller.cpython-313.pyc | Bin 0 -> 2486 bytes .../report_controller.cpython-314.pyc | Bin 0 -> 9795 bytes .../state_controller.cpython-313.pyc | Bin 0 -> 2732 bytes .../state_controller.cpython-314.pyc | Bin 0 -> 2831 bytes .../subcontractor_controller.cpython-313.pyc | Bin 0 -> 4817 bytes .../subcontractor_controller.cpython-314.pyc | Bin 0 -> 4983 bytes .../village_controller.cpython-313.pyc | Bin 0 -> 5406 bytes .../village_controller.cpython-314.pyc | Bin 0 -> 5565 bytes controllers/auth_controller.py | 46 + controllers/block_controller.py | 119 + controllers/district_controller.py | 84 + controllers/excel_upload_controller.py | 388 ++ controllers/gst_release_controller.py | 70 + controllers/hold_types_controller.py | 77 + controllers/invoice_controller.py | 98 + controllers/log_controller.py | 31 + controllers/payment_controller.py | 103 + controllers/pmc_report_controller.py | 37 + controllers/report_controller.py | 193 + controllers/state_controller.py | 69 + controllers/subcontractor_controller.py | 117 + controllers/village_controller.py | 167 + main.py | 5053 +---------------- model/Auth.py | 63 + model/Block.py | 165 + {AppCode => model}/ContractorInfo.py | 40 +- {AppCode => model}/District.py | 43 +- model/FolderAndFile.py | 39 + model/GST.py | 55 + model/HoldTypes.py | 90 + model/Invoice.py | 379 ++ model/ItemCRUD.py | 359 ++ {AppCode => model}/Log.py | 0 model/PmcReport.py | 456 ++ model/Report.py | 275 + model/State.py | 168 + model/Subcontractor.py | 140 + {AppCode => model}/Utilities.py | 4 +- {AppCode => model}/Village.py | 31 +- model/__pycache__/Auth.cpython-313.pyc | Bin 0 -> 2724 bytes model/__pycache__/Auth.cpython-314.pyc | Bin 0 -> 2846 bytes model/__pycache__/Block.cpython-313.pyc | Bin 0 -> 5033 bytes model/__pycache__/Block.cpython-314.pyc | Bin 0 -> 5189 bytes .../ContractorInfo.cpython-313.pyc | Bin 0 -> 3365 bytes .../ContractorInfo.cpython-314.pyc | Bin 0 -> 3546 bytes model/__pycache__/District.cpython-313.pyc | Bin 0 -> 4832 bytes model/__pycache__/District.cpython-314.pyc | Bin 0 -> 5003 bytes .../__pycache__/FolderAndFile.cpython-313.pyc | Bin 0 -> 2255 bytes .../__pycache__/FolderorFile.cpython-313.pyc | Bin 0 -> 2254 bytes model/__pycache__/GST.cpython-313.pyc | Bin 0 -> 2223 bytes model/__pycache__/GST.cpython-314.pyc | Bin 0 -> 2343 bytes model/__pycache__/HoldTypes.cpython-313.pyc | Bin 0 -> 3679 bytes model/__pycache__/HoldTypes.cpython-314.pyc | Bin 0 -> 3843 bytes model/__pycache__/Invoice.cpython-313.pyc | Bin 0 -> 16386 bytes model/__pycache__/Invoice.cpython-314.pyc | Bin 0 -> 17044 bytes model/__pycache__/ItemCRUD.cpython-313.pyc | Bin 0 -> 15291 bytes model/__pycache__/ItemCRUD.cpython-314.pyc | Bin 0 -> 15647 bytes model/__pycache__/Log.cpython-313.pyc | Bin 0 -> 5486 bytes model/__pycache__/Log.cpython-314.pyc | Bin 0 -> 5730 bytes model/__pycache__/PmcReport.cpython-313.pyc | Bin 0 -> 14273 bytes model/__pycache__/PmcReport.cpython-314.pyc | Bin 0 -> 16453 bytes model/__pycache__/Report.cpython-313.pyc | Bin 0 -> 16223 bytes model/__pycache__/Report.cpython-314.pyc | Bin 0 -> 8811 bytes model/__pycache__/State.cpython-313.pyc | Bin 0 -> 5654 bytes model/__pycache__/State.cpython-314.pyc | Bin 0 -> 5778 bytes .../__pycache__/Subcontractor.cpython-313.pyc | Bin 0 -> 6056 bytes .../__pycache__/Subcontractor.cpython-314.pyc | Bin 0 -> 6217 bytes model/__pycache__/Utilities.cpython-313.pyc | Bin 0 -> 3969 bytes model/__pycache__/Utilities.cpython-314.pyc | Bin 0 -> 4100 bytes model/__pycache__/Village.cpython-313.pyc | Bin 0 -> 5940 bytes model/__pycache__/Village.cpython-314.pyc | Bin 0 -> 6089 bytes model/__pycache__/gst_release.cpython-313.pyc | Bin 0 -> 7074 bytes model/__pycache__/gst_release.cpython-314.pyc | Bin 0 -> 7236 bytes model/__pycache__/payment.cpython-313.pyc | Bin 0 -> 8014 bytes model/__pycache__/payment.cpython-314.pyc | Bin 0 -> 8203 bytes model/gst_release.py | 150 + model/payment.py | 158 + static/js/block.js | 181 +- static/js/village.js | 278 +- templates/add_block.html | 152 +- templates/add_district.html | 4 +- templates/add_hold_type.html | 8 +- templates/add_invoice.html | 9 +- templates/add_payment.html | 11 + templates/add_state.html | 112 +- templates/add_subcontractor.html | 4 +- templates/add_village.html | 4 +- templates/edit_gst_release.html | 2 +- templates/edit_hold_type.html | 4 +- templates/edit_invoice.html | 2 +- templates/edit_payment.html | 2 +- templates/index.html | 217 +- templates/subcontractor_report.html | 2 +- unusedCode/unused.py | 482 ++ unusedCode/workOrder.py | 128 + v-2/.gitignore | 7 - v-2/README.md | 1 - v-2/__pycache__/config.cpython-313.pyc | Bin 0 -> 916 bytes v-2/__pycache__/config.cpython-314.pyc | Bin 0 -> 940 bytes .../auth_controller.cpython-313.pyc | Bin 0 -> 2305 bytes .../auth_controller.cpython-314.pyc | Bin 0 -> 2340 bytes .../block_controller.cpython-313.pyc | Bin 0 -> 4515 bytes .../block_controller.cpython-314.pyc | Bin 0 -> 4574 bytes .../district_controller.cpython-313.pyc | Bin 0 -> 3302 bytes .../district_controller.cpython-314.pyc | Bin 0 -> 3411 bytes .../excel_upload_controller.cpython-313.pyc | Bin 0 -> 18565 bytes .../excel_upload_controller.cpython-314.pyc | Bin 0 -> 20763 bytes .../gst_release_controller.cpython-313.pyc | Bin 0 -> 4485 bytes .../gst_release_controller.cpython-314.pyc | Bin 0 -> 4717 bytes .../hold_types_controller.cpython-313.pyc | Bin 0 -> 2905 bytes .../hold_types_controller.cpython-314.pyc | Bin 0 -> 2960 bytes .../invoice_controller.cpython-313.pyc | Bin 0 -> 5262 bytes .../invoice_controller.cpython-314.pyc | Bin 0 -> 5385 bytes .../log_controller.cpython-313.pyc | Bin 0 -> 1259 bytes .../log_controller.cpython-314.pyc | Bin 0 -> 1289 bytes .../payment_controller.cpython-313.pyc | Bin 0 -> 5715 bytes .../payment_controller.cpython-314.pyc | Bin 0 -> 5939 bytes .../pmc_report_controller.cpython-313.pyc | Bin 0 -> 1655 bytes .../pmc_report_controller.cpython-314.pyc | Bin 0 -> 1662 bytes .../report_controller.cpython-313.pyc | Bin 0 -> 2486 bytes .../report_controller.cpython-314.pyc | Bin 0 -> 9795 bytes .../state_controller.cpython-313.pyc | Bin 0 -> 2732 bytes .../state_controller.cpython-314.pyc | Bin 0 -> 2831 bytes .../subcontractor_controller.cpython-313.pyc | Bin 0 -> 4817 bytes .../subcontractor_controller.cpython-314.pyc | Bin 0 -> 4983 bytes .../village_controller.cpython-313.pyc | Bin 0 -> 5406 bytes .../village_controller.cpython-314.pyc | Bin 0 -> 5565 bytes v-2/model/__pycache__/Auth.cpython-313.pyc | Bin 0 -> 2724 bytes v-2/model/__pycache__/Auth.cpython-314.pyc | Bin 0 -> 2846 bytes v-2/model/__pycache__/Block.cpython-313.pyc | Bin 0 -> 5033 bytes v-2/model/__pycache__/Block.cpython-314.pyc | Bin 0 -> 5189 bytes .../ContractorInfo.cpython-313.pyc | Bin 0 -> 3365 bytes .../ContractorInfo.cpython-314.pyc | Bin 0 -> 3546 bytes .../__pycache__/District.cpython-313.pyc | Bin 0 -> 4832 bytes .../__pycache__/District.cpython-314.pyc | Bin 0 -> 5003 bytes .../__pycache__/FolderAndFile.cpython-313.pyc | Bin 0 -> 2255 bytes .../__pycache__/FolderorFile.cpython-313.pyc | Bin 0 -> 2254 bytes v-2/model/__pycache__/GST.cpython-313.pyc | Bin 0 -> 2223 bytes v-2/model/__pycache__/GST.cpython-314.pyc | Bin 0 -> 2343 bytes .../__pycache__/HoldTypes.cpython-313.pyc | Bin 0 -> 3679 bytes .../__pycache__/HoldTypes.cpython-314.pyc | Bin 0 -> 3843 bytes v-2/model/__pycache__/Invoice.cpython-313.pyc | Bin 0 -> 16386 bytes v-2/model/__pycache__/Invoice.cpython-314.pyc | Bin 0 -> 17044 bytes .../__pycache__/ItemCRUD.cpython-313.pyc | Bin 0 -> 15291 bytes .../__pycache__/ItemCRUD.cpython-314.pyc | Bin 0 -> 15647 bytes v-2/model/__pycache__/Log.cpython-313.pyc | Bin 0 -> 5486 bytes v-2/model/__pycache__/Log.cpython-314.pyc | Bin 0 -> 5730 bytes .../__pycache__/PmcReport.cpython-313.pyc | Bin 0 -> 14273 bytes .../__pycache__/PmcReport.cpython-314.pyc | Bin 0 -> 16453 bytes v-2/model/__pycache__/Report.cpython-313.pyc | Bin 0 -> 16223 bytes v-2/model/__pycache__/Report.cpython-314.pyc | Bin 0 -> 8811 bytes v-2/model/__pycache__/State.cpython-313.pyc | Bin 0 -> 5654 bytes v-2/model/__pycache__/State.cpython-314.pyc | Bin 0 -> 5778 bytes .../__pycache__/Subcontractor.cpython-313.pyc | Bin 0 -> 6056 bytes .../__pycache__/Subcontractor.cpython-314.pyc | Bin 0 -> 6217 bytes .../__pycache__/Utilities.cpython-313.pyc | Bin 0 -> 3969 bytes .../__pycache__/Utilities.cpython-314.pyc | Bin 0 -> 4100 bytes v-2/model/__pycache__/Village.cpython-313.pyc | Bin 0 -> 5940 bytes v-2/model/__pycache__/Village.cpython-314.pyc | Bin 0 -> 6089 bytes .../__pycache__/gst_release.cpython-313.pyc | Bin 0 -> 7074 bytes .../__pycache__/gst_release.cpython-314.pyc | Bin 0 -> 7236 bytes v-2/model/__pycache__/payment.cpython-313.pyc | Bin 0 -> 8014 bytes v-2/model/__pycache__/payment.cpython-314.pyc | Bin 0 -> 8203 bytes 198 files changed, 8312 insertions(+), 6173 deletions(-) create mode 100644 .env delete mode 100644 .gitignore delete mode 100644 AppCode/Auth.py delete mode 100644 AppCode/Block.py delete mode 100644 AppCode/ItemCRUD.py delete mode 100644 AppCode/ReportGenerator.py delete mode 100644 AppCode/State.py delete mode 100644 AppRoutes/StateRoute.py delete mode 100644 Reconciliation-Product.code-workspace delete mode 100644 Version-1.code-workspace create mode 100644 __pycache__/config.cpython-313.pyc create mode 100644 __pycache__/config.cpython-314.pyc create mode 100644 controllers/__pycache__/auth_controller.cpython-313.pyc create mode 100644 controllers/__pycache__/auth_controller.cpython-314.pyc create mode 100644 controllers/__pycache__/block_controller.cpython-313.pyc create mode 100644 controllers/__pycache__/block_controller.cpython-314.pyc create mode 100644 controllers/__pycache__/district_controller.cpython-313.pyc create mode 100644 controllers/__pycache__/district_controller.cpython-314.pyc create mode 100644 controllers/__pycache__/excel_upload_controller.cpython-313.pyc create mode 100644 controllers/__pycache__/excel_upload_controller.cpython-314.pyc create mode 100644 controllers/__pycache__/gst_release_controller.cpython-313.pyc create mode 100644 controllers/__pycache__/gst_release_controller.cpython-314.pyc create mode 100644 controllers/__pycache__/hold_types_controller.cpython-313.pyc create mode 100644 controllers/__pycache__/hold_types_controller.cpython-314.pyc create mode 100644 controllers/__pycache__/invoice_controller.cpython-313.pyc create mode 100644 controllers/__pycache__/invoice_controller.cpython-314.pyc create mode 100644 controllers/__pycache__/log_controller.cpython-313.pyc create mode 100644 controllers/__pycache__/log_controller.cpython-314.pyc create mode 100644 controllers/__pycache__/payment_controller.cpython-313.pyc create mode 100644 controllers/__pycache__/payment_controller.cpython-314.pyc create mode 100644 controllers/__pycache__/pmc_report_controller.cpython-313.pyc create mode 100644 controllers/__pycache__/pmc_report_controller.cpython-314.pyc create mode 100644 controllers/__pycache__/report_controller.cpython-313.pyc create mode 100644 controllers/__pycache__/report_controller.cpython-314.pyc create mode 100644 controllers/__pycache__/state_controller.cpython-313.pyc create mode 100644 controllers/__pycache__/state_controller.cpython-314.pyc create mode 100644 controllers/__pycache__/subcontractor_controller.cpython-313.pyc create mode 100644 controllers/__pycache__/subcontractor_controller.cpython-314.pyc create mode 100644 controllers/__pycache__/village_controller.cpython-313.pyc create mode 100644 controllers/__pycache__/village_controller.cpython-314.pyc create mode 100644 controllers/auth_controller.py create mode 100644 controllers/block_controller.py create mode 100644 controllers/district_controller.py create mode 100644 controllers/excel_upload_controller.py create mode 100644 controllers/gst_release_controller.py create mode 100644 controllers/hold_types_controller.py create mode 100644 controllers/invoice_controller.py create mode 100644 controllers/log_controller.py create mode 100644 controllers/payment_controller.py create mode 100644 controllers/pmc_report_controller.py create mode 100644 controllers/report_controller.py create mode 100644 controllers/state_controller.py create mode 100644 controllers/subcontractor_controller.py create mode 100644 controllers/village_controller.py create mode 100644 model/Auth.py create mode 100644 model/Block.py rename {AppCode => model}/ContractorInfo.py (57%) rename {AppCode => model}/District.py (63%) create mode 100644 model/FolderAndFile.py create mode 100644 model/GST.py create mode 100644 model/HoldTypes.py create mode 100644 model/Invoice.py create mode 100644 model/ItemCRUD.py rename {AppCode => model}/Log.py (100%) create mode 100644 model/PmcReport.py create mode 100644 model/Report.py create mode 100644 model/State.py create mode 100644 model/Subcontractor.py rename {AppCode => model}/Utilities.py (97%) rename {AppCode => model}/Village.py (77%) create mode 100644 model/__pycache__/Auth.cpython-313.pyc create mode 100644 model/__pycache__/Auth.cpython-314.pyc create mode 100644 model/__pycache__/Block.cpython-313.pyc create mode 100644 model/__pycache__/Block.cpython-314.pyc create mode 100644 model/__pycache__/ContractorInfo.cpython-313.pyc create mode 100644 model/__pycache__/ContractorInfo.cpython-314.pyc create mode 100644 model/__pycache__/District.cpython-313.pyc create mode 100644 model/__pycache__/District.cpython-314.pyc create mode 100644 model/__pycache__/FolderAndFile.cpython-313.pyc create mode 100644 model/__pycache__/FolderorFile.cpython-313.pyc create mode 100644 model/__pycache__/GST.cpython-313.pyc create mode 100644 model/__pycache__/GST.cpython-314.pyc create mode 100644 model/__pycache__/HoldTypes.cpython-313.pyc create mode 100644 model/__pycache__/HoldTypes.cpython-314.pyc create mode 100644 model/__pycache__/Invoice.cpython-313.pyc create mode 100644 model/__pycache__/Invoice.cpython-314.pyc create mode 100644 model/__pycache__/ItemCRUD.cpython-313.pyc create mode 100644 model/__pycache__/ItemCRUD.cpython-314.pyc create mode 100644 model/__pycache__/Log.cpython-313.pyc create mode 100644 model/__pycache__/Log.cpython-314.pyc create mode 100644 model/__pycache__/PmcReport.cpython-313.pyc create mode 100644 model/__pycache__/PmcReport.cpython-314.pyc create mode 100644 model/__pycache__/Report.cpython-313.pyc create mode 100644 model/__pycache__/Report.cpython-314.pyc create mode 100644 model/__pycache__/State.cpython-313.pyc create mode 100644 model/__pycache__/State.cpython-314.pyc create mode 100644 model/__pycache__/Subcontractor.cpython-313.pyc create mode 100644 model/__pycache__/Subcontractor.cpython-314.pyc create mode 100644 model/__pycache__/Utilities.cpython-313.pyc create mode 100644 model/__pycache__/Utilities.cpython-314.pyc create mode 100644 model/__pycache__/Village.cpython-313.pyc create mode 100644 model/__pycache__/Village.cpython-314.pyc create mode 100644 model/__pycache__/gst_release.cpython-313.pyc create mode 100644 model/__pycache__/gst_release.cpython-314.pyc create mode 100644 model/__pycache__/payment.cpython-313.pyc create mode 100644 model/__pycache__/payment.cpython-314.pyc create mode 100644 model/gst_release.py create mode 100644 model/payment.py create mode 100644 unusedCode/unused.py create mode 100644 unusedCode/workOrder.py delete mode 100644 v-2/.gitignore delete mode 100644 v-2/README.md create mode 100644 v-2/__pycache__/config.cpython-313.pyc create mode 100644 v-2/__pycache__/config.cpython-314.pyc create mode 100644 v-2/controllers/__pycache__/auth_controller.cpython-313.pyc create mode 100644 v-2/controllers/__pycache__/auth_controller.cpython-314.pyc create mode 100644 v-2/controllers/__pycache__/block_controller.cpython-313.pyc create mode 100644 v-2/controllers/__pycache__/block_controller.cpython-314.pyc create mode 100644 v-2/controllers/__pycache__/district_controller.cpython-313.pyc create mode 100644 v-2/controllers/__pycache__/district_controller.cpython-314.pyc create mode 100644 v-2/controllers/__pycache__/excel_upload_controller.cpython-313.pyc create mode 100644 v-2/controllers/__pycache__/excel_upload_controller.cpython-314.pyc create mode 100644 v-2/controllers/__pycache__/gst_release_controller.cpython-313.pyc create mode 100644 v-2/controllers/__pycache__/gst_release_controller.cpython-314.pyc create mode 100644 v-2/controllers/__pycache__/hold_types_controller.cpython-313.pyc create mode 100644 v-2/controllers/__pycache__/hold_types_controller.cpython-314.pyc create mode 100644 v-2/controllers/__pycache__/invoice_controller.cpython-313.pyc create mode 100644 v-2/controllers/__pycache__/invoice_controller.cpython-314.pyc create mode 100644 v-2/controllers/__pycache__/log_controller.cpython-313.pyc create mode 100644 v-2/controllers/__pycache__/log_controller.cpython-314.pyc create mode 100644 v-2/controllers/__pycache__/payment_controller.cpython-313.pyc create mode 100644 v-2/controllers/__pycache__/payment_controller.cpython-314.pyc create mode 100644 v-2/controllers/__pycache__/pmc_report_controller.cpython-313.pyc create mode 100644 v-2/controllers/__pycache__/pmc_report_controller.cpython-314.pyc create mode 100644 v-2/controllers/__pycache__/report_controller.cpython-313.pyc create mode 100644 v-2/controllers/__pycache__/report_controller.cpython-314.pyc create mode 100644 v-2/controllers/__pycache__/state_controller.cpython-313.pyc create mode 100644 v-2/controllers/__pycache__/state_controller.cpython-314.pyc create mode 100644 v-2/controllers/__pycache__/subcontractor_controller.cpython-313.pyc create mode 100644 v-2/controllers/__pycache__/subcontractor_controller.cpython-314.pyc create mode 100644 v-2/controllers/__pycache__/village_controller.cpython-313.pyc create mode 100644 v-2/controllers/__pycache__/village_controller.cpython-314.pyc create mode 100644 v-2/model/__pycache__/Auth.cpython-313.pyc create mode 100644 v-2/model/__pycache__/Auth.cpython-314.pyc create mode 100644 v-2/model/__pycache__/Block.cpython-313.pyc create mode 100644 v-2/model/__pycache__/Block.cpython-314.pyc create mode 100644 v-2/model/__pycache__/ContractorInfo.cpython-313.pyc create mode 100644 v-2/model/__pycache__/ContractorInfo.cpython-314.pyc create mode 100644 v-2/model/__pycache__/District.cpython-313.pyc create mode 100644 v-2/model/__pycache__/District.cpython-314.pyc create mode 100644 v-2/model/__pycache__/FolderAndFile.cpython-313.pyc create mode 100644 v-2/model/__pycache__/FolderorFile.cpython-313.pyc create mode 100644 v-2/model/__pycache__/GST.cpython-313.pyc create mode 100644 v-2/model/__pycache__/GST.cpython-314.pyc create mode 100644 v-2/model/__pycache__/HoldTypes.cpython-313.pyc create mode 100644 v-2/model/__pycache__/HoldTypes.cpython-314.pyc create mode 100644 v-2/model/__pycache__/Invoice.cpython-313.pyc create mode 100644 v-2/model/__pycache__/Invoice.cpython-314.pyc create mode 100644 v-2/model/__pycache__/ItemCRUD.cpython-313.pyc create mode 100644 v-2/model/__pycache__/ItemCRUD.cpython-314.pyc create mode 100644 v-2/model/__pycache__/Log.cpython-313.pyc create mode 100644 v-2/model/__pycache__/Log.cpython-314.pyc create mode 100644 v-2/model/__pycache__/PmcReport.cpython-313.pyc create mode 100644 v-2/model/__pycache__/PmcReport.cpython-314.pyc create mode 100644 v-2/model/__pycache__/Report.cpython-313.pyc create mode 100644 v-2/model/__pycache__/Report.cpython-314.pyc create mode 100644 v-2/model/__pycache__/State.cpython-313.pyc create mode 100644 v-2/model/__pycache__/State.cpython-314.pyc create mode 100644 v-2/model/__pycache__/Subcontractor.cpython-313.pyc create mode 100644 v-2/model/__pycache__/Subcontractor.cpython-314.pyc create mode 100644 v-2/model/__pycache__/Utilities.cpython-313.pyc create mode 100644 v-2/model/__pycache__/Utilities.cpython-314.pyc create mode 100644 v-2/model/__pycache__/Village.cpython-313.pyc create mode 100644 v-2/model/__pycache__/Village.cpython-314.pyc create mode 100644 v-2/model/__pycache__/gst_release.cpython-313.pyc create mode 100644 v-2/model/__pycache__/gst_release.cpython-314.pyc create mode 100644 v-2/model/__pycache__/payment.cpython-313.pyc create mode 100644 v-2/model/__pycache__/payment.cpython-314.pyc diff --git a/.env b/.env new file mode 100644 index 0000000..5bfeba1 --- /dev/null +++ b/.env @@ -0,0 +1,9 @@ +Secret_Key = 9f2a1b8c4d6e7f0123456789abcdef01 + +MYSQL_HOST=127.0.0.1 +MYSQL_USER=root +MYSQL_PASSWORD=root +MYSQL_DB=test + +DEFAULT_USERNAME=admin +DEFAULT_PASSWORD=admin123 diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 14f61be..0000000 --- a/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -venv/ -*.pyc -__pycache__/ -.env -.uploads -static/download/ - - diff --git a/AppCode/Auth.py b/AppCode/Auth.py deleted file mode 100644 index cf92480..0000000 --- a/AppCode/Auth.py +++ /dev/null @@ -1,54 +0,0 @@ -from flask import Flask, render_template, request, redirect, url_for, send_from_directory, flash, jsonify, json -from flask_login import LoginManager, UserMixin - -from logging.handlers import RotatingFileHandler -from ldap3 import Server, Connection, ALL, SUBTREE - -from ldap3 import Server, Connection, ALL -from ldap3.core.exceptions import LDAPBindError - - - -class DefaultCredentials: - username = 'admin' - password = 'admin123' - -class LoginLDAP: - def __init__(self, request): - self.username = request.form['username'].strip() - self.password = request.form['password'] - self.isDefaultCredentials = False - self.isValidLogin = False - self.errorMessage = "" - ldap_user_dn = f"uid={self.username},ou=users,dc=lcepl,dc=org" - ldap_server = 'ldap://localhost:389' - #Need to re-factor further - - - # Static fallback user - if self.username == DefaultCredentials.username and self.password == DefaultCredentials.password: - self.isDefaultCredentials = True - self.isValidLogin = True - return - - try: - # LDAP authentication - conn = Connection( - Server(self.ldap_server, get_info=ALL), - user=self.ldap_user_dn, - password=self.password, - auto_bind=True - ) - - self.isValidLogin = True - return - - except LDAPBindError: - self.errorMessage = "Invalid credentials." - except Exception as e: - self.errorMessage = str(e) - - -class User(UserMixin): - def __init__(self, id): - self.id = id diff --git a/AppCode/Block.py b/AppCode/Block.py deleted file mode 100644 index 84e7a58..0000000 --- a/AppCode/Block.py +++ /dev/null @@ -1,108 +0,0 @@ -from flask import Flask, render_template, request, redirect, url_for, send_from_directory, flash, jsonify, json -from flask import current_app - -from datetime import datetime -from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user - -from AppCode.Utilities import RegEx, ResponseHandler, HtmlHelper, ItemCRUDType -from AppCode.Log import LogData, LogHelper - -import os -import config -import re - -import mysql.connector -from mysql.connector import Error - -from AppCode.ItemCRUD import ItemCRUD, itemCRUDMapping - - -class Block: - - isSuccess = False - resultMessage = "" - - def __init__(self): - self.isSuccess = False - self.resultMessage = "" - - # ---------------------------------------------------------- - # Add Block - # ---------------------------------------------------------- - def AddBlock(self, request): - - block = ItemCRUD(itemType=ItemCRUDType.Block) - - district_id = request.form.get('district_Id') - block_name = request.form.get('block_Name', '').strip() - - block.AddItem(request=request, parentid=district_id, childname=block_name, storedprocfetch="GetVillageByNameAndBlock", storedprocadd="SaveVillage" ) - self.isSuccess = block.isSuccess - self.resultMessage = block.resultMessage - return - - # ---------------------------------------------------------- - # Get All Blocks - # ---------------------------------------------------------- - def GetAllBlocks(self): - - block = ItemCRUD(itemType=ItemCRUDType.Block) - blocksdata = block.GetAllData(request=request, storedproc="GetAllBlock") - self.isSuccess = block.isSuccess - self.resultMessage = block.resultMessage - return blocksdata - - - - # ---------------------------------------------------------- - # Check Block Exists - # ---------------------------------------------------------- - - def CheckBlock(self, request): - block = ItemCRUD(itemType=ItemCRUDType.Block) - block_name = request.json.get('block_Name', '').strip() - district_id = request.json.get('district_Id') - result = block.CheckItem(request=request, parentid=district_id, childname=block_name, storedprocfetch="GetBlockByNameAndDistrict") - self.isSuccess = block.isSuccess - self.resultMessage = block.resultMessage - return result - - - # ---------------------------------------------------------- - # Get Block By ID - # ---------------------------------------------------------- - def GetBlockByID(self, id): - - block = ItemCRUD(itemType=ItemCRUDType.Village) - blockdata = block.GetAllData("GetBlockDataByID") - self.isSuccess = block.isSuccess - self.resultMessage = block.resultMessage - return blockdata - - - # ---------------------------------------------------------- - # Update Block - # ---------------------------------------------------------- - def EditBlock(self, request, block_id): - - block = ItemCRUD(itemType=ItemCRUDType.Block) - - district_id = request.form.get('district_Id') - block_name = request.form.get('block_Name', '').strip() - - block.EditItem(request=request, childid=block_id, parentid=district_id, childname=block_name, storedprocadd="UpdateBlockById" ) - self.isSuccess = block.isSuccess - self.resultMessage = block.resultMessage - return - - - # ---------------------------------------------------------- - # Delete Block - # ---------------------------------------------------------- - def DeleteBlock(self, id): - block = ItemCRUD(itemType=ItemCRUDType.Block) - - block.DeleteItem(request=request, itemID=id, storedprocDelete="DeleteBlock" ) - self.isSuccess = block.isSuccess - self.resultMessage = block.resultMessage - return \ No newline at end of file diff --git a/AppCode/ItemCRUD.py b/AppCode/ItemCRUD.py deleted file mode 100644 index 91b3db9..0000000 --- a/AppCode/ItemCRUD.py +++ /dev/null @@ -1,248 +0,0 @@ - -from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user - -from AppCode.Utilities import RegEx, ResponseHandler, HtmlHelper, ItemCRUDType -from AppCode.Log import LogData, LogHelper - -import os -import config -import re - -import mysql.connector -from mysql.connector import Error - - -class itemCRUDMapping: - name = "" - - def __init__(self, itemType): - if itemType is ItemCRUDType.Village: - self.name = "Village" - elif itemType is ItemCRUDType.Block: - self.name = "Block" - elif itemType is ItemCRUDType.State: - self.name = "State" - - - -class ItemCRUD: - isSuccess = False - resultMessage = "" - itemCRUDType = ItemCRUDType.Village - itemCRUDMapping = ItemCRUDType(itemCRUDType) - #itemCRUDMapping itemCRUDMapping - - - def __init__(self, itemType): - self.isSuccess = False - self.resultMessage = "" - self.itemCRUDType = itemType - self.itemCRUDMapping = ItemCRUDType(self.itemCRUDType) - - - def DeleteItem(self, request, itemID, storedprocDelete): - self.isSuccess = False - self.resultMessage = "" - connection = config.get_db_connection() - cursor = connection.cursor() - LogHelper.log_action("Delete Village", f"User {current_user.id} deleted village '{itemID}'") - - try: - cursor.callproc(storedprocDelete, (itemID,)) - connection.commit() - self.resultMessage = ResponseHandler.delete_success(self.itemCRUDMapping.name) # Simple message, route will handle redirect - self.isSuccess = True - except mysql.connector.Error as e: - print(f"Error deleting village: {e}") - self.isSuccess = False - self.resultMessage = HtmlHelper.json_response(ResponseHandler.delete_failure(self.itemCRUDMapping.name), 500) - finally: - cursor.close() - connection.close() - - #return self.resultMessage - - - - def AddItem(self, request, parentid, childname, storedprocfetch, storedprocadd): - - connection = config.get_db_connection() - if not connection: - self.isSuccess = False - self.resultMessage = HtmlHelper.json_response(ResponseHandler.db_connection_failure(), 500) - return - - cursor = connection.cursor() - - - LogHelper.log_action(f"Add '{self.itemCRUDMapping.name}'", - f"User {current_user.id} adding '{self.itemCRUDMapping.name}' '{childname}' to block '{parentid}'") - - if not parentid: - self.isSuccess = False - self.resultMessage = HtmlHelper.json_response(ResponseHandler.add_failure("block"), - 400) # Assuming this is a valid response - return - - if not re.match(RegEx.patternAlphabetOnly, childname): - self.isSuccess = False - self.resultMessage = HtmlHelper.json_response(ResponseHandler.invalid_name(self.itemCRUDMapping.name), 400) - return - - try: - # Check if the village already exists in the block - cursor.callproc(storedprocfetch, (childname, parentid,)) - for rs in cursor.stored_results(): - existing_item = rs.fetchone() - - if existing_item: - print("Existing ", self.itemCRUDMapping.name) - self.isSuccess = False - self.resultMessage = HtmlHelper.json_response(ResponseHandler.already_exists(self.itemCRUDMapping.name), 409) - return - - # Insert new village - cursor.callproc(storedprocadd, (childname, parentid)) - connection.commit() - self.isSuccess = True - self.resultMessage = HtmlHelper.json_response(ResponseHandler.add_success(self.itemCRUDMapping.name), 200) - return - - except mysql.connector.Error as e: - print(f"Database Error: {e}") - print("DatabaseError") - self.isSuccess = False - self.resultMessage = HtmlHelper.json_response(ResponseHandler.add_failure("village"), 500) - return - finally: - cursor.close() - connection.close() - - - - def EditItem(self, request, childid, parentid, childname, storedprocupdate): - """Handles the POST logic for updating a district.""" - self.isSuccess = False - self.resultMessage = "" - connection = config.get_db_connection() - cursor = connection.cursor() - - #district_name = request.form['district_Name'].strip() - #state_id = request.form['state_Id'] - LogHelper.log_action("Edit District", f"User {current_user.id} Edited District '{childid}'") - - #Need to add validation to see if item exits - - # Added validation consistent with your other Edit methods - if not re.match(RegEx.patternAlphabetOnly, childname): - self.isSuccess = False - self.resultMessage = ResponseHandler.invalid_name(self.itemCRUDMapping.name), 400 - return self.resultMessage - - try: - cursor.callproc(storedprocupdate, (childid, parentid, childname,)) - connection.commit() - self.isSuccess = True - self.resultMessage = "Successfully Edited" - except mysql.connector.Error as e: - print(f"Error updating district: {e}") - self.isSuccess = False - self.resultMessage = ResponseHandler.update_failure(self.itemCRUDMapping.name), 500 - finally: - cursor.close() - connection.close() - - return self.resultMessage - - - - def GetAllData(self, request, storedproc): - - data = [] - connection = config.get_db_connection() - - self.isSuccess = False - self.resultMessage = "" - - if not connection: - return [] - - cursor = connection.cursor() - - try: - cursor.callproc(storedproc) - for result in cursor.stored_results(): - data = result.fetchall() - self.isSuccess = True - - except mysql.connector.Error as e: - print(f"Error fetching villages: {e}") - self.isSuccess = False - self.resultMessage = HtmlHelper.json_response(ResponseHandler.fetch_failure(self.itemCRUDMapping.name), 500) - return [] - - finally: - cursor.close() - connection.close() - - return data - - - - def GetDataByID(self, id, storedproc): - data = None - connection = config.get_db_connection() - cursor = connection.cursor() - - try: - cursor.callproc(storedproc, (id,)) - for rs in cursor.stored_results(): - data = rs.fetchone() - - except mysql.connector.Error as e: - print(f"Error fetching block data: {e}") - return None - finally: - cursor.close() - connection.close() - - return data - - - - def CheckItem(self, request, parentid, childname, storedprocfetch): - self.isSuccess = False - self.resultMessage = "" - connection = config.get_db_connection() - cursor = connection.cursor() - - LogHelper.log_action("Check Block", f"User {current_user.id} Checked block '{childname}'") - - if not re.match(RegEx.patternAlphabetOnly, childname): - self.isSuccess = False - self.resultMessage = HtmlHelper.json_response(ResponseHandler.invalid_name(self.itemCRUDMapping.name), 400) - return HtmlHelper.json_response(ResponseHandler.invalid_name(self.itemCRUDMapping.name), 400) - - try: - cursor.callproc(storedprocfetch, (childname, parentid)) - for rs in cursor.stored_results(): - existing_item = rs.fetchone() - - if existing_item: - self.isSuccess = False - self.resultMessage = HtmlHelper.json_response(ResponseHandler.already_exists(self.itemCRUDMapping.name), 409) - return HtmlHelper.json_response(ResponseHandler.already_exists(self.itemCRUDMapping.name), 409) - - self.isSuccess = True - self.resultMessage = HtmlHelper.json_response(ResponseHandler.is_available(self.itemCRUDMapping.name), 200) - return HtmlHelper.json_response(ResponseHandler.is_available(self.itemCRUDMapping.name), 200) - - except mysql.connector.Error as e: - print(f"Error checking block: {e}") - self.isSuccess = False - self.resultMessage = HtmlHelper.json_response(ResponseHandler.fetch_failure(self.itemCRUDMapping.name), 500) - return HtmlHelper.json_response(ResponseHandler.fetch_failure(self.itemCRUDMapping.name), 500) - - finally: - cursor.close() - connection.close() diff --git a/AppCode/ReportGenerator.py b/AppCode/ReportGenerator.py deleted file mode 100644 index e69de29..0000000 diff --git a/AppCode/State.py b/AppCode/State.py deleted file mode 100644 index 8646119..0000000 --- a/AppCode/State.py +++ /dev/null @@ -1,246 +0,0 @@ -from flask import Flask, render_template, request, redirect, url_for, send_from_directory, flash, jsonify, json -from flask import current_app - -from datetime import datetime -from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user - -from AppCode.Utilities import RegEx, ResponseHandler, HtmlHelper, ItemCRUDType -from AppCode.Log import LogData, LogHelper - -import os -import config -import re - -import mysql.connector -from mysql.connector import Error - -class State: - - isSuccess = False - resultMessage = "" - - def __init__(self): - self.isSuccess = False - self.resultMessage = "" - - def AddState(self, request): - """Log user actions with timestamp, user, action, and details.""" - statedata = [] - - connection = config.get_db_connection() - - if connection: - cursor = connection.cursor() - state_name = request.form['state_Name'].strip() - LogHelper.log_action("Add State", f"User {current_user.id} added state '{state_name}'") - - if not re.match(RegEx.patternAlphabetOnly, state_name): - self.isSuccess = False - self.resultMessage = HtmlHelper.json_response(ResponseHandler.invalid_name("state"), 400) - return - - try: - - cursor.callproc("CheckStateExists", (state_name,)) - for data in cursor.stored_results(): - existing_state = data.fetchone() - - if existing_state: - self.isSuccess = False - self.resultMessage = HtmlHelper.json_response(ResponseHandler.already_exists("state"), 409) - return - - # cursor.execute("call SaveState (%s)", (state_name,)) - cursor.callproc("SaveState", (state_name,)) - connection.commit() - self.isSuccess = True - self.resultMessage = HtmlHelper.json_response(ResponseHandler.add_success("state"), 200) - return - - except mysql.connector.Error as e: - print(f"Error inserting state: {e}") - self.isSuccess = False - self.resultMessage = HtmlHelper.json_response(ResponseHandler.add_failure("state"), 500) - return - - #Need to make this seperate - - - - def GetAllStates(self, request): - """Log user actions with timestamp, user, action, and details.""" - statedata = [] - connection = config.get_db_connection() - - self.isSuccess = False - self.resultMessage = "" - - if not connection: - return [] - - cursor = connection.cursor() - - try: - cursor.callproc("GetAllStates") - for res in cursor.stored_results(): - statedata = res.fetchall() - self.isSuccess = True - - - except mysql.connector.Error as e: - print(f"Error fetching states: {e}") - self.isSuccess = False - self.resultMessage = HtmlHelper.json_response(ResponseHandler.fetch_failure("state"), 500) - return [] - - finally: - cursor.close() - connection.close() - - return statedata - - - - def CheckState(self, request): - self.isSuccess = False - self.resultMessage = "" - - connection = config.get_db_connection() - #connection closing needs to be verified - if connection: - cursor = connection.cursor() - state_name = request.json.get('state_Name', '').strip() - LogHelper.log_action("Check State", f"User {current_user.id} Checked state '{state_name}'") - if not re.match(RegEx.patternAlphabetOnly, state_name): - self.isSuccess = False - self.resultMessage = HtmlHelper.json_response(ResponseHandler.invalid_name("state"), 400) - return HtmlHelper.json_response(ResponseHandler.invalid_name("state"), 400) - - try: - # cursor.execute("SELECT * FROM states WHERE State_Name = %s", (state_name,)) - # existing_state = cursor.fetchone() - - cursor.callproc("CheckStateExists", (state_name,)) - for data in cursor.stored_results(): - existing_state = data.fetchone() - - if existing_state: - self.isSuccess = False - self.resultMessage = HtmlHelper.json_response(ResponseHandler.already_exists("state"), 409) - return HtmlHelper.json_response(ResponseHandler.already_exists("state"), 409) - else: - self.isSuccess = True - self.resultMessage = HtmlHelper.json_response(ResponseHandler.is_available("state"), 200) - return HtmlHelper.json_response(ResponseHandler.is_available("state"), 200) - - except mysql.connector.Error as e: - self.isSuccess = False - self.resultMessage = HtmlHelper.json_response(ResponseHandler.add_failure("state"), 500) - - print(f"Error checking state: {e}") - return HtmlHelper.json_response(ResponseHandler.add_failure("state"), 500) - finally: - cursor.close() - connection.close() - - - - def DeleteState(self, request, id): - self.isSuccess = False - self.resultMessage = "" - - connection = config.get_db_connection() - cursor = connection.cursor() - LogHelper.log_action("Delete State", f"User {current_user.id} Deleted state '{id}'") - try: - cursor.callproc('DeleteState', (id,)) - connection.commit() - - self.resultMessage = "Successfully Deleted" - self.isSuccess = True - except mysql.connector.Error as e: - print(f"Error deleting data: {e}") - self.isSuccess = False - self.resultMessage = HtmlHelper.json_response(ResponseHandler.delete_failure("state"), 500) - return HtmlHelper.json_response(ResponseHandler.delete_failure("state"), 500) - - finally: - cursor.close() - connection.close() - return self.resultMessage - - - def EditState(self, request, id): - self.isSuccess = False - self.resultMessage = "" - - connection = config.get_db_connection() - cursor = connection.cursor() - # str_pattern_reg = r"^[A-Za-z\s]+$" - - state_name = request.form['state_Name'].strip() - LogHelper.log_action("Edit State", f"User {current_user.id} Edited state '{state_name}'") - if not re.match(RegEx.patternAlphabetOnly, state_name): - self.isSuccess = False - self.resultMessage = ResponseHandler.invalid_name("state"), 400 - return ResponseHandler.invalid_name("state"), 400 - - try: - # cursor.execute("UPDATE states SET State_Name = %s WHERE State_ID = %s", (state_name, id)) - cursor.callproc("UpdateStateById", (id, state_name)) - connection.commit() - self.isSuccess = True - self.resultMessage = "Successfully Edited" - return redirect(url_for('add_state')) - except mysql.connector.Error as e: - print(f"Error updating data: {e}") - self.isSuccess = True - self.resultMessage = ResponseHandler.add_failure("state"), 500 - - return ResponseHandler.add_failure("state"), 500 - finally: - cursor.close() - connection.close() - - - - - - def GetStateByID(self, request, id): - """Log user actions with timestamp, user, action, and details.""" - statedata = [] - - self.isSuccess = False - self.resultMessage = "" - - connection = config.get_db_connection() - if not connection: - return [] - cursor = connection.cursor() - - try: - cursor.callproc("GetStateByID", (id,)) - for res in cursor.stored_results(): - statedata = res.fetchone() - - if statedata: - self.isSuccess = True - self.resultMessage = "Success in Fetching" - else: - self.isSuccess = False - self.resultMessage = "State Not Found" - - - except mysql.connector.Error as e: - print(f"Error fetching states: {e}") - self.isSuccess = False - self.resultMessage = HtmlHelper.json_response(ResponseHandler.fetch_failure("state"), 500) - return [] - - finally: - cursor.close() - connection.close() - - return statedata - - diff --git a/AppRoutes/StateRoute.py b/AppRoutes/StateRoute.py deleted file mode 100644 index e69de29..0000000 diff --git a/README.md b/README.md index 3f469cc..e69de29 100644 --- a/README.md +++ b/README.md @@ -1,8 +0,0 @@ -# Payment reconciliation - - - - -http://103.186.132.129:3000/pjpatil12/Version-1-old - -file path:C:\Work\lcepl_Projects\Payment reconciliation \ No newline at end of file diff --git a/Reconciliation-Product.code-workspace b/Reconciliation-Product.code-workspace deleted file mode 100644 index 876a149..0000000 --- a/Reconciliation-Product.code-workspace +++ /dev/null @@ -1,8 +0,0 @@ -{ - "folders": [ - { - "path": "." - } - ], - "settings": {} -} \ No newline at end of file diff --git a/Version-1.code-workspace b/Version-1.code-workspace deleted file mode 100644 index 876a149..0000000 --- a/Version-1.code-workspace +++ /dev/null @@ -1,8 +0,0 @@ -{ - "folders": [ - { - "path": "." - } - ], - "settings": {} -} \ No newline at end of file diff --git a/__pycache__/config.cpython-313.pyc b/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..af5230c88c8ffeff29f29db60c103a676da792cc GIT binary patch literal 916 zcmZ`%UvJV-6u&K{6iNqmiHasla4)DV8)haLr9gC%oA!79++Tn9oO3%;ln}x7@%`tT!V~h- z4W`RCaYkRzIV2j(s0m73x^cS1k0Xpgn$Q+~3lkO#N-` zUY;#@`P^+y+u6!*Fv*KI?v%YQJPe?5M>Q%T9Fn4!Ma<(sPp-WGWQLHUIL#HMnKv6? zF+9y_Tt-+TNj{qtnDC6+4ioll3z*b0ZM#jO!la7nm?hJ;k^$y#bnKUP7A(_d(<(a@ zSYYCUfQsN20`AG+Ht>mz5DBZ^y`k?=c%j$JR;zC0fIi2MZ9Qjp8dlSZgM}jHTD@jE zHQLm(R(n-%d3(mcdxlMSowf;9D!rOoxt2<=q_6Av@oufU-0Cp3YB@%wWK3#8F<8U` zBKX>u5xQg`M|?D!7qY}^rXxphVzeU1@-zY|N~Z`73?(YIosCR(hHJ%)!&X N9PdgaQRKW<{s8Dz!?OSY literal 0 HcmV?d00001 diff --git a/__pycache__/config.cpython-314.pyc b/__pycache__/config.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f7ac19fd6340f33fca43abf40abb3efc98865ffb GIT binary patch literal 940 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 R5DqeXjCFhd5@E_S`UgZ-_6`65 literal 0 HcmV?d00001 diff --git a/controllers/__pycache__/block_controller.cpython-313.pyc b/controllers/__pycache__/block_controller.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ebd73363ea5c2484cdf22e0ea22591e3133676c3 GIT binary patch literal 4515 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 F`oBW~t55&{ literal 0 HcmV?d00001 diff --git a/controllers/__pycache__/district_controller.cpython-314.pyc b/controllers/__pycache__/district_controller.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a85b06983ba03620ba402d923353a008b2558a7b GIT binary patch literal 3411 zcma)8O>7fK6rNqL?M>D{iA|amCv`#~#7dLUKuLf=1qAF!K%xcG9-O+ay-BP%wmUlp zB?s!Bdf-CU3kMDyI0T6UhaNapoT?tzawMm#pr$HS)tlRJ>ZxzWp0yoJVWgd(o$tMQ z^L}T<<8DXL9)15~DkdZJJ6-T*uTAz;0igo=9I3)AiU>rAh(wIoh%F)!DPkw~h=VwS zbYGlxMr0zhxox&B(oWjhT$)uPF5(KJMPUG`_C@yzvC5lx7CrMpeqeo69ja56$3(Sl zvW8rg)%I?5q6Xn3>39RZ0@OkiwW|en=NqWqC*a1k!Kt1WbiOyxd0R-`(SqKub`GMD z?=N~p$YHi!NawX2PGyMc#abq*;TX}@a_KnHOefa9%4<3?WvnGrSW6JonaAnaQWl%` zrF2|h2??eron204ViXFHB>a(QQaZt@1cABzI-#88WbN06dpw>@#`w~-g)iMOrTMw* zHvsHh)5uCTsVAsxyg6WEgKy8pJRw@s4zlc#L(!U@5So`bS$$N|)Mp4K2NCK;3uxY1 z-{G@5y(&!B_y;K#ymEgVL{WwR|63nh@y&_A*IUQ%Sxc-f+R67-aly7A zs7sWJ3W8>WHB$V^97=caN# zndEje-B{D}X>wK5_4u-e>4i;KSR?1tX=YE|^j24#LdR6`Ad!ng?d%+UfF9z9PN$pw z(9G{Ho{rwsHLOR^&s?3o7M;=bRg%qFV=lhFrez3tQzDy5q|&K4No6xJJ-bA1$FVlL z7EfiOZv#zyGOaSF>!zzwZ@3pErOMH7LsvkrWYp1f zccTz_>GXVi=bJnC-`jEqx8;tq+*_1;AAM@by<76=wzs?NJyP@@G2|mJU7dHsg-hFR zU)dcjx`R)ghC5htA1hqm^#{uSk)nU(hp^!vDfy=hS6(Uyx8=Zg*TA;7>y=Y-IVwo9 zJNDpcwBa33tZ0cflNOrj8b}F~>07 z7wO5|w#-ieK~1aE=;+4*=uJoO1dDZhwto;h+- z^nDIfWhhV*JbP+(1J7?~1qvSm$i@fUm;3WMC`MYcU$6S|Qa+ttA26M@B|Z#j)2?Bh z#T!10D3c}qOWAxT$r9CuugTe1wZl}WA$PU3(Tc@_4pTjvu1nyXoEILVo3ymoBZvhQ zD{35dVSRQ6_fU`oUC3UKFno~yrFD%}IGtvtAzIz4Y0=#z8Xkq+N%-`iplgV@oobzE ztSh^;uJje2*R&uu8ihh zprJp|;0x6Eo2UDJ`@=}dGg?q84kWtEg0Cp}?tQX(^XcWyIYaQ3gz0U;`?uIBIQDEv z?4aG*iUiBbu!T4Lhc|C+t}yr{gG<733qDnmU|I1rz$Z(>6vMR%r!Cy6iUi9_PXq4b a=l18fo_)<2&oP2PgB&J=17Z7=-Fy{3PHS!?6E}UictOC!RS) z81^pa!91d~7@2yCND=ogCdKd+pOuVANhzKxlaVsWlbp>Pk(2Tf1*sTOlFAVksT#>A z^GDRAdPGBNI9MsvDQL`Uk#;hXhcuyaqN=30`tf(8IFikeIko5 z6}ubwj%J=7g-7X8HHkg>9pXz;kJ^eINf+`@(v+j7<_J)Uf~O!;i!|qe)

H)QU%! z1#OuZxe7l+NXA_vwjdau4}`p=|N2UR^v}BFtgIp=DN%vi*@|R!jOrvwqUM z>|b08`j-8y0$>4t24Ggmpm#1zvhvp>;ZR_1mCc`7Apw5b>swla7S#OfO2kjXyK*!< zKjaTCaW9&Ia1apphGqu>K|d?=-x(oPJ=2?*gKaVLSg7ynD@O+98 z@envRUBu_Q4SXu|1O=9QWYb1I$8F|QnP&m0{NZ#$iM&n5t3R|=!q=L%^C^$w@VXvl zhrm?_HeDvbtN6Y|+zc!)kQ(%=g#vF{iHayWuC1d|4HYIj?VKIS>w!NHth- zV3tIBMk3`Q^B{7GNj)mzdWGsokPhQG`_2B&OH*O;+NI!(e<|oaM}}XAQ5d;&&bPV< zV-ANg84k?^f&m{we`(x*v*prKstg8sc9}O4o?E`@BmE7nEe*|W4Xw?sM=t@uG8qmA z{bb})YVf6L$P_d#t+IKkT(TTGg@hb=AELLg=Xsby`>n5j?W-HcxV-F%*7(jFZ@-b$ z;+V#`#joleb8fs}nZR}Xm&_)~Do+~huBWY}VWNh0y{e!XZkHw9B zYeOHab?*$nJ-oh5cPM{1nWRWe1U@2Kx-?`kQNlX7-%PyXcc4=-;|?6y$4 zvF9?0rU1|xY~LBA97lHy|Hb}OJ8heY>(5c@b5HWER7p!bzm<}*;p?o6WZQ}DX zPmhX_dh*eTZW3h-7Qk9EFno%imG#&VjLDFiP`d@g#=~Hl9?Hj{H~AQj5puq#vtULE znOiV|nOYt--w!aJnl@ol$gb_1gW88vQ2@2IhzdgP(Q(v>|Anbh2-GM9YD#lbQ^@xR zs42>#Ob^pSdPD(b0;ZQxW`Nqo9AyfR@n3~9r9heTOHgLw`va7jv#8up+2K;M0KAo> z;a^(U@&p?41R7vXltWue4(*4^Eljo5>_~P3f5GyP!Hhs?Q~B=714!YO#|to}sIFH$ zR+F*`AzerdJqEl7178PES+tizG-IJ6pk42&2pOj9cuXENPt@~onWsmBHX*!S0II$N zR;)P33{goG=bBSOM#8v{3)R4dVxovJ0&$qfG2O(sNf>zZf_B9Hx9N`_e+uLdr* z^H?*_7dZZ>K$!)kW<8wigazjE95eX~9JdfwPYvQYQRF_BX_HU!w1b>MGgpAWp^}g- zSG$BQSG$CjC_$}^fhMBaY`TMQ!Q-@Igw>72cOYci=2JYMJ@U>Ur=`6H+Z>_Zp}e>9 zwQy|gUV1Lo13f;V#ZK5$V~XdC53JX;i$~~jL3u42b6}_IAYGqMH}Lfeu(rG`{4ic} z%*>5hnDp5*b5?6jSukIwHL%SbFHZ>lZ6>5dQHCEx9w8?bgp!aEdKeQe9r+HhL^%A1 zv~yTFHVtDg737!?MoOqKt2bptAy6l5qj1cFr*b^y?c#{I3{E~mg}sHaUV5o@airnq zM03~)Nf|aLDo1Oqm)7ASU9ID2f3BSME&IITP;m7#H0^(eR?99iDaAh&{ znFJM}#j{JwYCJ1fxg7@Vir^?g=2_+r8BPZx%VYo!8DxFI@XR$pteS;a*x2~URkmQz zzsv#lt&YRKjYOs|D(7(LXF;Fa0rfACRdkWx2jx#q<0z8 ztXfEV1G7=xX^_`d#2!CMhRIGmS|a3x;iQJ!OyYAs*z?YI;-5VSp^`|ZpP|vkN)axX z1P*>U$$HT)n$_?xD~neFDY71)p^Z>f})Ax@1yEt8^nJ)^4|43*CF|N zL;*!#S6M{sN{c9}#shPBKSzEiPNLx*Ev!#xq=|+69+a=E$B{&EBoci1H1N7I^N#ct zG}J$fm-Dp|H$guNaDNcw!?zJNaGg)>(b2XwbxNUPmFUGA2+f6A-c~}(aGY004grDO`3G47DOLfx z6Ixom6=XHsao$ZhxVRb)Ut{yo>BkK}D}^JQAlZ*#!7PO40`qL)JRF?NUiHFj2u?O& ztV7WX83D7P00{+`$nXqX5CJpcpM?{R$VzZI0>>o&<(Y+W$Pbv8pv_r?ZgDZNoGyWS ztSlJ5=?B{~6AVZEtP&mXEOF>%sl!eg=?l&KS=FNN7Br4b6e|Jq!^)5iS|ZWL5Y8Q; z5E(I90FFZ8_$RWm$mXS#EOl;E%AE$0Xi3eAZ(fDk3w}5v8i03l3`N5zf)x)&o<-3p zM64=9EO{PfwZbTkq{JXzSSHb|%ob*q1c9Y1ORUIG4kDD}D4IY(qSsj^kmBV! z$ZCWt-fR9IwsZv&~T2t;7U`2+tJjPy5DXdJfWlhDHZEG4X^+|C_ zvbf<K=ISd*f9{zpYBQSH-s{ z-%@VZ(9L~RQ$N*zm6{1si#O?+Th!G|djivrZ+Gl?=`MomJfF}_9blTP>HV^YWpP*M zBSSUEwO{68=31en9oxe@VY>S~)isqcTwn~PNkbKFs7e@W1d#6S)!j1slybv94vTc+%QLTbmwRTh{xSYUhJ%_pg0- z@sB5O1Op3-(l4VM$Vl(>gq%tN7SpTvIY8%bJ=onb*{x z6dKpBZ&W3%wY0T1_6lund|cSH)_b_#uMK~sHLtBQHO_VAhKtrY7`^2?o=wZ2TuItn zXnRZC-u8jMjWOVxy}+{d(I<|YnDV>hNyl;8aXjwmOgc`{j#F_*-})F+*`BOCPFEh^ zw#O?6*GHjJ(ojPKmus3CS1sdge=3){jK9RB#ipl)SW)wH8&+gOUA@T#fHr zA6mCXJ2f#YI%kK27gZqi{W2u6_rzW*dwTG#ds?UEh zB|^&eYh-in&oHs32l_&pow350gfiAAbPbH9ee0F2e#&%wyOJ`UNa#*{th1)n?*m=M zZ}*f?=eK(bK_?;eHxV>{{b%J+?PL%3cWs)<4(WfFmY}$-2yzk~G8Ffyh+~ou6b7PI z@_|i+(sn7)iG9#2L+N7@lyh zMC-W&!Hrg;2ZBeq_X_<&~YSCnS8p~nX0Lm$C5TG@8kACx%P_}(ga4ZE} zE6V{}whtE10flEn<$x*Az~q3a_(F)vFNAPpGzJbqrDsGTm8TjVCNBNhQyS7t3%&zx zv_n1+z>A>9J0PqUOb1otu;@WJF0XSwl%pwURFjSBOLd}na#l{>E!bpMx z_`_Q%;&Fs@IUw``1Uf`Jtf$2SL{Sb1;b=Uh&jFD+oHpcuFbk~|=YX&X5XKx3!XazO zlmntfz+uh-Aza0TEIA;`1RT~J5V!zQk^`b#fUxC&s1P9RIUp(ph|(Mo4gsQUvXQsU zgHqpR44Z5`?3kLLQC@a^7Q*2W8t(#q_f>(7#}B)N^3(|}mwW0DyD%Ye4@Q(rGcE=B zo`y75baiq#)&>D<1(DCw<12xqvm9+qm3tb|!6#9XdSOSaRj)UF(IvKG3`W=QI#6p&?5iZG^BI!n5VnPDuDw(!Ts77ydV)o@^wv zgpMeL3q`p4fa^CSVIs@~Tz3&AN77CqIQB)51m-Di*M}Tgy*WxaGOgy|?Xe6@sEVla z97o1IRGo#TgQ(76nMMPJ3xsSM9qMy|FXv_HgSm0KnJ;y>@~O-doPRQFb=f|w!nNWH zSRVsc;o{HJ372s)y>R8sk9wgGKLLG$@}X^@rzVS@E}~}tWf4(x=#19QV+=WSK%5XD zT!d3-cL~}(bWKY*1^8N`_Uc8TCNq+I2q&xwP7<}yH(?Ki!$km5O}L0!&na*qs>>n6 zUZT#^hvWv&{`B)i`gs~Xo1{E10i)b4Txe$G!H?Q~+Ifbk+s~=77+i~fZ(qL$4svts zN2!a6iOX5k4f2%Z*u*Q-$mnr_XlxgGhOF4@3eWIMBndLl!Zkt^q(@5$jF(~q4ryn$6B%7izV<+9p8&*8HXrKtTQ>GOr0txsh`x94T?Gc%{bzTqXa z?ghZ0$!-5VrDz5K-RAm3VC|+%8-wz(e@HtBe3A4rrw*@rE@quNOt*_+lz=8347?5l zI*272|^E3O~jj7+lw%=JVmY9;`G7~==ZgepyuFKIxWJs}#lfmi`>zI;)7flM?;KoiLQ`3Jd8sO_ zYS1&u!>J}ud-$}{bDB?TCPLnRbQc+kpu0@X(CRE1_D;fA6@kz^n~$!3)1cgqI;%k_ zGu71tfsilABP&R|dGqh;$?$Tfv(8N)1t;xc+x`r?4KY+ ztvn1Mm?OiB`0@fcGH{;*?DH@{Yk)TRf7S<4mWKgQ2}iu3V5vZeGTz<1sY!bqn9|+` z81goFC>jSh163dr2*DkCXa+P7d8dFA0>YoVIt9yzr9BTwWD8iEaNd20%Yg~{z}>cT zE(g8jBmZ^kvhQzecPA5`sbt>;s_z11eI7ykoUhxc$k zqs9Y;ICA^|cLQzO0U@QsojCHp=|_J5ob!zrL?w)o;n~zRaXxo-?Bm7>Ta@xOL7F=f z4up`tkl-4Jk@rxv2~pIN0)g?1D&nsL7&oq=bpwbFIpm~D;FAS%6>^UvY~=`>#jj08An!H7V0_i^T5PjAWZLQlxQfG7uV z2*jHTDCc;yhlyHV7qR0DJI6NPOxASMHQn)=p2xPHN8<~hlm7w0z{yK858ngAhk+a; z&|C|%t7;I&CS*nMlup7Fc91DiCI5v|XUc-7Us+R%#S%aR@G?}RJUfceijqXK=8OY)`YZ*( z;Yh}{#7dFPBVR+MQz*KCqE}J$Cn%bMh?V$&m1?L24t7DWAIu^vMcse`@QRvFfrM6B zF$tePg6kGJi>h8hkrx$$=T0zq)i-l3<$Bc2>h}A01!hsdfvMo)$L0&ZT_~HMa1VG4 zk=GD%j$#>SnpveVU$MfR&H48!g{huZWhQg-D+s|`DEb?GUqJN8`LzXM744K>ro6k446b8R9t`m?VgHVl?`pEeRQqGiD7zjPJ zOoY$t&{me!A@i4Z?&6j$-0a3pS!@AUhNPBuhDsax2ni<{7-#Cq{|KoFY@NW~|1ROw zsnq1~UlAe{ad`GX&pzkAnKPHYcl*xmq^X8B)xh^`anlJ(YCvDl-G2LaQd>rA%Qn?< zZNpnbPl_z>)!(U47FEzi6`LpHMQv}LJrqu7LJlQKodY~SHZR9@ZEp=f(Oci^zSEu5 zSJL{*&GERt?XA(n@;=Jf{$V>H-H=n|U6iRit_80ZO4~;%`aY89e`EQ{KF25q{8b7o z=)#J4p<^v?Pdp@1mVIQgZy45Pdt#Hlh%wkU{PzNP12ILsv}I2MSsxp!Hdh|3-e09! z`gZ!_HG_LH$OYepzH#`$k*JSjXV1Hv?yEm zLUtj_1|Jbutm=K&Ll@OPwCjl1PVDJXz5y$?^WwJnX8pV%uw**2`$u}M65_5Q1|H+Q7*sxy0blwJBlb{WdXvEqu& zmc4S6u6O~Y5@kDHpsfmJS7SBx4+8fCRP(7FN4$EFakx?p<=Dd1Hf58|l;HO|?sjYv zvA%do(=QqT!Mbv<8LK-&>uT23jJ?FaraM7{_rH#d3Lv(x;RHo z%~SIWR9hgSd!4aV+_T@cZ{CVqn%DD~in<34_Zz6@-greHcv+P>@4a#Njo6iVSvNvk zeed$!%dzgb{rI}-K-qrCs>TPG?_Z|c2IEyjl&<1HBm42}f3sEH8@oFetBc!?t}7Yb z^+0o96AQ=jo^{Pj-}bj(Ui-j)-%d51jF+EUFF4TEgPk*$y|;L0aq~>vSi3IyU-}Bh zR+qH3(6*Mitu1Nmpluy-Ti5y-#)Jcm$;DLHA!oBKSG>A=BcG|MeQ@jktu5JBaO*2n zbAP<%^aePF**A;-{hLgs^Fi1BuGo!u<n33`TmL#eki`13H zj`rQN(Ey>3G)$@x)wVMj)6_o^gI=aFr~GzQL1xvcaA#$8g(_ul-9)T z+nSv>sPk8!%fyyh5xC{qE5WJp>bkZ^<8{Luqp8Bv$&wDbq~pgkbT3K!ug81I zWDiOAkUw*_Z%UuwRg|-1+p%q=s!t{GUZ(BX4~~ET`1V-5Z3Ns?nWnb)Z#}%VT@Y^? z+*E&9*~YXU`+@8GF2;HMnMBmqxq0^EnpV1|=ZO=BXedn9kEo_Yl)H06w&xwXp&)xFA(nM77T`iK`AwysItg?4X?;31=7A}=f{*dEzc zgZ!x3C8o4_t9E;CCrC}ASBhNOLk@j?;^z{P1$-~TNfy2&XKd{syp1wq)CzJmIEKxZ@`#<1QlU znx5n+CaFj_!sN<}1i)mTUhAW3Sx2 zxK*}OPo29&UGX8`K-)5W&TbI{v%v4z?4r%Cgt?BZ+_RIntNp-y;ZuwAFFnt>b^HGq zUjRA60{&N+sPT0X%$5*;!s-3M*2s=}H;D9o;TpC0M#B76q22aWySDAyyO*eGpmvF} zT}RDYf2qO@);|mWRpg1mvQf8rBNpB6q0U~Wyntc}c_61elsCt=j&67Ee3hCCP}go! ztGB7vuO7kH#6bF{!rEYTF0uQurx=jqZ59lae}r{al0TEZ`!kd_}~8l+o(4gt{Cd z8>DK^?Dzmt)i6~u64#&I1)S#+>T@5fi(W+f+r4~*1UXF}*5+>_EwK9gTjy&EzYY6G zn`*9A@{iT!a}APD(SRrH@q1n_f16N@~AXip*%iGGGDe}<_b{yA3tGfevtb|r~jp|LAJ#~eS$%6^7vevUQ# zeZFCRYGWauU-Om%)+cOC!)}iR+o)lj9 woC35nE^<8;uZTn{R9w5qCHH`NDRk@V%^RBw2v^fn@svd5LQu`ABuC2s1@cgs)&Kwi literal 0 HcmV?d00001 diff --git a/controllers/__pycache__/excel_upload_controller.cpython-314.pyc b/controllers/__pycache__/excel_upload_controller.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a30548e0360dddd136dd5d3b7b839ee1a3c293c6 GIT binary patch literal 20763 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

mI6`F(eK@%v~@WY%XuNnXi5u z8hO!c^hZDoUCYF$75qa$~&ynQ89)%uwi9U^^2RwP)H7YY0d+<6i2XNL`p%Bhd% z<0FM4bv=%RfFmj3BPXDwJn@X?4a^jW9WUh?BHn@8WaADONPx$zVM$J$@5au|;UW@=+};H-_nxP7q5YKjKt->hTJ&wF1Odg$k5TFhwVi|*_Ia^jC{#<9Xe+% zrU(orBSF^E!yZ4v={Zy)n<{aFTgl6ayemsgMohzEr->!(5haDz4n}zG0<>OqIDFI? z8s8*JV7G@+7L>e(5&{xNo)|234i#j?M#QN2>FA9~L%8Up4k3t~q#76SbH&359Mm{M zIfatb2uN^=k-<=`EP;%YZ(_adSf!4$5KeH5!O<+M5mHc_*o@6Yt(I4os6GJlmOxSk zN1*Ai*x{%=F`1~>p$$4D^?oG-9U1OrVCvjk;t%rj@3h`-z0-BOD~uls;VqkILV4Y{ zq)(Lj;PZF{K9AuoaaeimmN2T$y(7CVyCs2^O8xrjdoy=u0==8I^_h^NEn?^n8oHy3 zytUJJW^T`H^agBeGok#ZNdD1a{?VwSU|n)gepkM6Wiw}89@4f%wCzD{dph_%nIvC+ zOSYGdD`tA zx_L0H9*UM!L`qtMB`uFjj;swvORFCk?;As<&2Nu>EW(UOKP)VbYKqXQ@y1YKWOHG+ zzMHP=p?l8LWpiPT^}{^F6OE464{SSW%gpZJIeK80o}HtuPWqB>*SbQ_U8Rezg*Dd^ zl8M_Bbj9)Q_MMsC{xkH9op!i)XFPP@m9U12meoYcT7zY+kIUNEGom%-hpKl}p_=w@ zDS?dgjt`4#qS}&s%Dc*d9LS&9Z5gJUN9Yj;ebG&OuI*mDPCHiV)i2UjH^SQ2Kg`1u zR88*=o}mZM(r3@nvq06g-P!B(xmCKzAJ%*^g{tY@{?qg{P(|%dd+9!3ShKQ^s>-?t z*8A2_<P3VJW|vcENXmQbZD(FT2=Gl z^8L$ywDk4_G%araySb%j7*^RY49+dl(=&S*c2&?T{1nWDpilU5>=v#hHYNkbyOl>G zm7T%L&QRsCE$?>sZV&OOl0fa$rO*y=ybr3ZxUKkR_MRNeDUPV|pc)UW%ih-%+>%8L zb+;7JLhUWZ2RQ|6SJo>dMYX}A+Q4hOMdrsjjkkKETEhV|^VY~ymG;fmXm!n+Y~2)8 zS48u4-Y(awopY!q~dt6 z;&`Z{Z*4qUemGLz7A$YuGK9*9*2bVwL{l9Eu4^kdR-@YLKu%O!6;SMDicJOofQj=9 zKhD8&o1W{j+(Ja~y@tCDfzsWgy2m;7KrW1DII7aT<$u$^ZVL!*`9rGOh^h(x9;uq5 zrrI^>x<06`ikg~{iaa)TfQZ+I*B7ED^TVQdinavX)q$drX*6Q81WgvGITK=asJ8Qi zyn8g>rjJ!vZo{)&EWh;DiKr@X z?Nmfn9#)knf^9mDR4X&UUpnHjp|Tv$DeI>_Ge1;%%)JLsxT zx^rx2iY9Ek;}__$d3xSU`>q2nKcIvTRs$KF>L#q@AjUG}Rz8MOJ;o|IQkDEy`O3amwc zW$tY}+k*XdNjaqdMaVw+Z0?BMLfe^vL1;OTEQ{W7@y#GpQF7XPHV98x+z7fOC= zmY}pHYosxSpgn+8go0;Nj>rAvV-N&{7t0#%#_syGEo zp9V^w0%e%PKmsLcU`tY9jcK5aDNuMCC_Dj0l%|0xO@b*)15=g+Q=SH=3HO604TP5OB|i;>j)y2n z1Hm7adkWJ)6!SQ=X(04Ggf0yP@4y3x#=%@S@;HjqK;S%tJ`F@E4`E0HQN}}*q=6{s zA&iq|&OQ%F?BH6{W0U5CLN>USJ$_J#22sQJUP_oO2e~$I zB8My$B=$C0;F5^pJ@XR0wLIQ3GK(9Py#lpJ*Uy>KROb{~minATgJQfm9TuSWCGC

TIh&hqYXqK?cW>Pf|6am)sRGhD=TWE;UIaR_z> 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 PNMJ%xO`ot?c2NHh-XJH# literal 0 HcmV?d00001 diff --git a/controllers/__pycache__/gst_release_controller.cpython-313.pyc b/controllers/__pycache__/gst_release_controller.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1386a8f5e521db1e398a84605578053f5a775975 GIT binary patch literal 4485 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 pCR11X2$X2U@_vibiAOzTc=SR>q`It literal 0 HcmV?d00001 diff --git a/controllers/__pycache__/gst_release_controller.cpython-314.pyc b/controllers/__pycache__/gst_release_controller.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ff435e400889e685d2ee15ce286023dd2f3a36d9 GIT binary patch literal 4717 zcmcInZEO?C8J_j$+FpMqj?EFU(})ir#DS0mgpL%#m!mtCwm)vo$Rspn%Q-) zaZ`>{rEB`DMLh{BRcchV68%vrl`55!PK7^}@^hLG1+8+TRijGxFOZX}(m$P9+wt0G zaQmYx?d-fW?>qDE%)HO@&JmB>3GnmlpZ!VvAp^kQiBNs4mC8<(0^lB)0xUHG5|m0M zOsXkiR?P{EYDrjCYr>}5BBb9mVo%U2op7iQT{e$66E4-I*DWJV!mYX^V3O(rtaZ}U zZz_+kdMCYOUDZizU)ILjSvqcF9VaWAlQioLf&Pk0^{e_{WAAEW&or?QyvE+$#NN}y zzK!*EgP89t;#bV3IfkX8P=I1y)x1#1X9UQpLare3s-W4SaHl9JszyU0BSImqYW51 zk7cSsHZ{oR2HDaeTN`AXQI6Y3P49ji00-EQeJD^@s5mH?QkJR*W43Co_GRs-CIHC% zoBV-6ldfr0YxTYt%q_HOqh|$63vEZ8wzY+}vrgOALfciRZEvB?)M?W#wB2>uj+Blu zWW~lX#xy<{4Lqzj<*Bxh`KqG$@@hmla zJQg#p+w&FstZrYGRNcNJnTsmad`fXTE$3Cpr&SqpVy5Ihm&ru2qtS|^udLTGjzmlC zQ49=1xvPQ~(L)+_=+%A$SYQN+H{eI9y`dpKtL7w(Ta3X@(dfz=C^0uAc%pS=1vNd( z@seajBLxzo(QIWH(kvJYbC8@9&52=`?7Z3BA_Z-j?d!b7X!v&a(sCj*>& zUzkDrfgOjn-S&dUwFQ3L3fvy#*}u+27X6E-ezW%xzr_4be#W>L^6SjLhj$kDuSbq8 zbuEpq_nm)w^=a|X6FXM3!}-F7&0ms%ww<7>d)o!9{;xfN_WtPZCwCuqtl1-*{_um& z`<)MG*ZqeUY@5N(2dVq1#iQ%N-Ua7oX#a!x`}2#V>!A}1%o`<7@F&ubrH2FSo~{M+ zW;pUo=I0DLaB@9-YQeol`#0#$6}l7ci!UAcf_dWo+>1^oP$GpU(L#eFyXYY2!wTnW1zAU$9|$S9E3_#Z20oS(Ce{ zM3}S6tuuIpPC)Vw0}?mmF-qsLt^b0@>NvNKADgjW9V547y*j>b$$E8cUTDR7bzEO) z#d`KSX0o?rCYp7$WF}|Qgqg|pKQWVua8uIWf|-~qBUQh3%;ZkuOosK;m?`Fa6*Rzn zG%u^sEGC_d&MB}BpFT*4tr_f8RiHnIeaVAWDEN{)CVNx&!>h9vE!U}FeW$-rlf zf()g8k0~ln(hc-e(E1{vs zcUMEB$kP7YwfC9#2%?W{***VPEHr>$@6x~*olhd4N6@JeG%|%a0ZnC5b_N}vU8BVs z_zgWhjV47jH@`;zpgH*Up^Mki2h(e=G_s_3@IQjm;a+^*5bD>vHRh4U)G_ZL`a-q3 zP1G>&BaD$SV8A_`=i!XJdNTzqzLT;h*4%5VMhI&;VZu?ei-luN(2LwZS=*aqA4jx3 z32;f*#kFx}hu-h(Gh+{2H4mnii3c%;T&xs@pAky2ra>8%VKmE&5{@lZj+U2IvneXC z78T7ZKqy0af~fPFkv|D#dgf`;{$9(p+4#SHzgToT3H3 zxxJ9AV6)hZnc*Pm!ne9|>ZFi8#zCXU3mhWNq#&>IDK7L7Y46@>8(3)@Sjw-qUHs&| zdzYWP0-N5BhYr*|ipD0;O&*1&SG{Q@%p&hBqGz`(?hQ+L#S%t)dX|EJnE&I)zyBBw zUPhNE(UgQHb10Wbz4Dr+u;~i?bY!vfQ6BZ2L1(U^$T&()p$|mV_Tj4QHp&%{tAH#8 zcnZ5R8f(|AS&3I}L*lfaHG{@!noWOC=O-8dqxu^aZsM%V%tnn z)N^ok16+L$`o9D{UxLWr{5U~5@@Qh!KX}ixZ38CX1{M4Z6+|6}mj)gmTyYA`yxJb>7V X87bt4;fr)G4EdTAjI`VamG%=`S9 literal 0 HcmV?d00001 diff --git a/controllers/__pycache__/hold_types_controller.cpython-313.pyc b/controllers/__pycache__/hold_types_controller.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8b471450467d3f1998eb9d289595b5381c13dad0 GIT binary patch literal 2905 zcma)8%Wo4$7@yf)+w1o$kc8r5ha_M@Vn_*1C`CL%DNdV0Hfbxx(#o|biD6^Av+JVd zfI=@Eda2YR^^ijjJ%k&F9ys-Hh-)NUE1|Y3m)?xh-uun2cm2RY9m%sh-)m;R-}n30 z2Y$Z?!T9-yUke|)5c-QPtl?tag zS~NNEHw&C}>h9}Gg_LQbsOdpUig`k1jck;atVVPfC0|#Fs_CwKYN=RQ-PA=&@&!tA zAgWMBUMP< zNf+*XCM|)rYlCR(rM#MB=d%W@7#@BHzrgeat+eujZ1jp~rG-2~i-P6WeoFY#mk4DO z2rZ*UsK?&X_O@~tXO?2G9YUi_?tN2k)lU(x_${r(En|tF{J|(J}T_bw%7SXOLiltU5 z%R!RU1(s#%gN4fS6OZfP-nf+cqD1d!lpHB5@*P^b2XLyHJK4<*Qq1jcwwnC}gI?3H{mt9wZ zvEnic>?*wUFED_eS@et*yV$J40kH8xk~aX<{WsXA7AyvyNlcGI5S7(%oDRaQ2YZf2 zkApPEjMz&@MjGB=-5aZVV>@d#@A$qjPKUu@YbXV*)Y?}|T?RT|D&()^Y@&dfO`_+p z=zB-promfCGy){TZzs_*vKlea2p>Wt+>b`!**bHP>mm}@L!@Z;l`ST@?V{`C5RJf< zb>B_Ml8wax6EWUclIxqd=S<%4I#n3v_Q!Pl9qBe(L8ibs+Xe z4G&kv;hj^vQ!m8vt||@<)I+1y(CF@BEp&SOwo%8iM&zA#9h+DmOXZ0|H1faEz6S^D zk2WCl){5>#am8+$D{csA3<+CUSx=~aZeeT4#z39yVtUeHvLY~NT)|4bK?}+@pse90 zISPFmnJh88mPwOP@69R8_$>U?voLjJy&;HoVX!I;?zna%FND!o-sq3F_K#nz4ZL6T z&g=^_W-f!NN|C}*vMR~TYpRxmDE6@Faur07KwGhBe`#zH8tMEJF;8Picp3h?82z7L z*ot;oA`tP4iN=PCQ9=#e#|$=!BFNz&7x?Q9u=9-}j$pJO&-FqxTvR+0G4o2YqD zLMvr$G1l;hd&+4cc|>=tDp~bDWu;*5#m-NiY_c)Yy@ru&eCa8x06n--0z4)y5LFXe zw<(2BzIh>0mW$a9BFnnVsEV)3x`UP~8Zo-Q(dZ2B7*JAni>Jr{$AnzG4Dl)oxk4|1 ziggC{UznOa#`t%1w~p?o-^C7rieKAS=D@Rd(21r1%Vxw58n5AmVdd&L=E!RSmPT6s16S={1ONa4 literal 0 HcmV?d00001 diff --git a/controllers/__pycache__/hold_types_controller.cpython-314.pyc b/controllers/__pycache__/hold_types_controller.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f9eb4d102d066a7294ae23695b7c26aae6aef54b GIT binary patch literal 2960 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 V5@$6Dj?y--RU_1W7W&$~{SRmIn(P1o literal 0 HcmV?d00001 diff --git a/controllers/__pycache__/invoice_controller.cpython-314.pyc b/controllers/__pycache__/invoice_controller.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6bfc202429b26283fdc7e03de7f37e418918b8de GIT binary patch literal 5385 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 l(y5W7?F{Ld{u52b0$;>X} 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 XJuMqe$VOJmuD#|m^~3Ud(*EAZyqS6L&2Q&a z*V72bzem41%Q8ZL38Oc_j>hRPAfBNLs^BG5#uS$mG*OnQR1&_#l3Y&GWLcp~WJ^l} zWtFNWWa4pDkFX^Z#bSs$u}+V z)_A7cqOg=%t&mUy^VC|ix=sS0GM)~aRR|RNPGP2R%-j~b=ijNuB}TPn044ZN_W+)u zhmljjdfcir2Porc1N92^@CN=ko~*%+4g4_Ki>)L!a77X(NI90tSSqm`h@~3KR4mg% zR~X+jPQ`*J?f@^yo)8d?(khb9$3CT`oIb z-RG*~ed{|_67uw>@764~9e_U22pM$9Ze5oh2bWZ?k#Y+W-~h!O)i!A_xCWt04QMtU>E(PpiJ>ln|P^B{B-|7ySDrJ zA6l-HyZJ^N?WiLieQ58>Q?oNv+iEzkAs+PS7(%vlBEBhG#e%Kzi+25!L> z1QUJkemPM|RHQ`-*e472O#u6%M%m(Dj7eArE#jcy<*H9LNaO+hhPoYEHe+^mQ_!l% zg!~1SQNeOUrbQ53p(Jd%jO%`j1ubUPfvbe_RNZlj<=IUV3i6a-Q1NKdcX}`iN;m@m zSEZut@S%S7rGEAK!ht@wd;f23sFPcK{r+TIes_1}jrQLDcfYZ=w){d~rk7#Os*&Z% zx@(6|sOV-y&?qggMP4gGE8)q7*HQ>*u)jDBwv7<&Z9~ zM8-d(=-8+PuHJ^tS}wUmufUXek-{dxv4k;xh3>Y|-B;+=?`ZPR!IAyz&((v$*?obL Dd)}F? literal 0 HcmV?d00001 diff --git a/controllers/__pycache__/pmc_report_controller.cpython-314.pyc b/controllers/__pycache__/pmc_report_controller.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..76b3ae2655a85970e87a161f932edd64b81f48ac GIT binary patch literal 1662 zcmbtUTWcFf6rNp4tCdz4E0XFujcXZU2gSEUc5PBZz>Ntu#6~P_N{ifOBaN(0_Qvd} z#)g9aK){d`^w@_!q<^CHC)iPq#XxCF10iototNgRXI7GBL`okz58s|MbLPx9XTGyD ziFg#j`0>fl#@_*i{^U-71bfEWQ!tw78>9*wXkDPfx=6+K01Zs=IdLPn9-^TMv@J{` zHLxu$iQRoPyd5s3`pE`gHMk^nJvB6nmU;^`@-NZSd35+fbX1k|s1SM0cNYRI@|oGd zE-`G%k_6jTOf-ru*VHLyX%8l}8gVSGY7ktZ4%sUREK;&6Ds~-0S=6;E8u9J@%)-D~ zxW#STxn1cI_PNFYp;YLM0W?wBpX5;LUHp6kj7fAK^)KKG_l2vy#U03S1*u9Nl|%h_ z71mX8_Hy4x2>ig19~|;SL%uZRhlhN5$d6p~)#ys3dpZ<1_HKO3$(1(mmFty>QLP*$b_qtC>(g%Nt{E1Lz2S?2QIOKNI3D=PvM~o z0V09T5<8e`1B!&pxgavB`=%^d)v50BC!5Z(CwD$Af8k-`l~>m`H@3=a*n31B_x#)A z3>T<4cEvCaof?jIBbOx?z|9E2wy5?)u_f@p^H&3|S~~Op~8Dpr{R|=iNOL z25vs^>v#_Vn&_>BV%ZZV|6IxcaJQ|@G}m4!$xd?dP+okgT<*wYop|bC`f<6F${svC zPEB^wqpjF+da5&$Ir!@P6Ge~Iu{;jWehTqO8|YXq~CK-!W**?BpLWHiD0i` zQWkd_)NN3$2Fk{SMQVnLHCwl^$K^Gh%jL)iV33dcz`NNi0{Cy27(Iz!dmg{`Y`GmT zHt)VtQk~?@L;2>5iK(ODmFDJ4<>S_OztCTmdxyb$WE8e-6%-b#nY#Cg@T>bO_L;Gy z;gdIFMA#H~!177V@%(W;4m literal 0 HcmV?d00001 diff --git a/controllers/__pycache__/report_controller.cpython-313.pyc b/controllers/__pycache__/report_controller.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..15b9a92a064431e0b1c5aefdf5d4596aa9fd274c GIT binary patch literal 2486 zcma)7&2Jk;6rWk|`fF`(oTO>KXq3ueswD&Rj!~(Z z1;=0kFg~dpw!;E9ZL3;d-C!XbP4cT{)6gXrGp)69HP4O8V3b&-SO?s~$=7WIbjl)W zqh^8gfnnB!9J*$$xxzAPu8r)~a*60LH}3 zxryoX6Eo8@vpFC*+v11>+UWBIVNBKF!K>&7pNG{x>d3dfD}Y80n#7HAV^5vk#3i~C-XV& zwP)AjKA2Uq;`bD3zVD%dtt4f9E28}(W6@Uc9&9UR#XYhOja z*1pslTB4DdZCqMv%qD*wZv?NsP<)F2MVLeepQ!p{RsZo!Q=Qqm`dsb3cj4~a4SCS5 zqtjFShs8SPy*hOTFM`Wm%jMF1vC~r+8V5s^QWBZ}Dy8EjOS+QPDXe3>NNAQ!(=5G8 zXDN25Z<$38{8p!%LkDx{M(Fmh&&B!(=A)|%+J_Lmjd+h^h%knv8*Ucw`i;9KoIsn` zx!o)h0X|1=;;}mMpwd*YaO8%!gC}-W1pz=@WEbIVUuZ8yLvq+& z3Z?&xC%=XSp3}&5-(TMAlIGe&Ba64&97j!Ben`bJ@wB4&v_t=gQ|M~*1h$LqG_t_S z1+KuC**ovud90O3s{V7dq3R8Fu^}&tGwH7}Wz{U$w}1yCpcNembO(ZQJrSutD`i(3-BNukasU0%*ltiDB&{LxAki-s|+94CWei9wH zxBh*w8PT^wtss%qFUCF}yEoLNBimHll_-5<&qt&lPDWcYimhsAD_Vl2L4pc5LUG^s=xDQh&$lt(J^ptG{D)soCC3qVLm=+1#$oWpU_#0Ptxt A7XSbN literal 0 HcmV?d00001 diff --git a/controllers/__pycache__/report_controller.cpython-314.pyc b/controllers/__pycache__/report_controller.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bc846ee77b41b56e6e6c082dbc09d4b320d4c444 GIT binary patch literal 9795 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 VrHcJAL3pYRXMU-}b?I&H{{dT}&CLJ+ literal 0 HcmV?d00001 diff --git a/controllers/__pycache__/state_controller.cpython-313.pyc b/controllers/__pycache__/state_controller.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7fa1d642dcb8359ad58be6bbc4ddd28f98eeb35d GIT binary patch literal 2732 zcma)7&1)M+6rWkGv@5NoRqBtpR^zB@aH2N0V!Mv92~O?A*p{gqyfGt653z z?22GRgK|nj3kj4!4msqIg9APIRC?-P(8CH*Y?g+)G?(5K+*40|Gpk+6MhP9*`Fiio z%zMB0etQ%O1rW3sKm1bg_z?PwE}Z6VPL75#LQjx_6ug8|nBWvgxRi%@Qas^PUgC{W z8Mow12}EFX&ytw(6Mqb)aU3apIxx=JXAvnat-SAC&>9u5;!}hqr-+kWT2TD`XuK(O zMiP39WdQ7O(@yGO7k-Ofa2&j%4ekqdu#?|n7e4kbA}~Uh?ZF|lLWTHT6v>T%1|AUP!o!(v02XRIr;>rQ7}F59sLB|6SUTJ9=U-o zp|vJR2(9prS!-1A)Rzcl&my#nR)p5RGizBZ+$4@sEzyy+#&dLEYOS5waqALZ3poyf!w_>$7^!j6yt}Hp8ZI?7K#5jVMgL+WURXQTIx5WpX_bdq#2W+ zIf7HsPB59X)&|6a5qev8XPE!TQ%Jw)MN|AjjT9cwL=1nPTN*7K3BB6>qy~(_) zsfKEMmAnFVIaSC{=iG7tCAUeEhinFoj%Bju z7D-zva1LiVBZSbi{F1ZNoGax2m09KtyQ$Kw153Kj+-`-|@)vZalFLB_kS=%+q31dEV)Sz z2(2Sh4wl(&ggRp8ajdriRBjK zS0}pBzr8${PbG;yz_A0Fns#=(6yylJJ%fo*U1m%7sBrB1Gy$9o0;}| zSM=Gud2YP^!R30_gc+PT?CRU^8mx5gYMo3e#Vj) z_ZDl?w8>AiR9#M-vb^hBR=-WC1I#y8jja$^5|d}x@0?KJEU8$ADjK#Or2i>P%w$U0 zqMFHAqE7$rnXk4iFDX|Hm61JTIzp?B(0IH=>~fnC#Vc@HMN_9q5=4~9^k1QCcreC) zpoQPi!YefR3Z3~=>VACd>1JIzUlkiZ#0BmT+#7ftd|ugIe_pN*nE3JmmfvszJd2M! zh>Or}qQTR3qu)^v@AmGBRC$Cc^LWVB9c}QiY=oPtX5>O0CzzHWf9h&|+~8r^=ySCO b_j-Qz?aB1a&zN=(zUpev;5R(1nYaG|0ALp$ literal 0 HcmV?d00001 diff --git a/controllers/__pycache__/state_controller.cpython-314.pyc b/controllers/__pycache__/state_controller.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ba9b5cec04b6ad97b26c1f74f1d7f55259e4e41e GIT binary patch literal 2831 zcma)8OK%%h6ux&n&Wt^d$0d)3I!)39rI9I784wlXhcO2-LNx+6)VoY;~Be=Te#BP=edt_ zzVCeYxo~F?L3#Gg_jz9jLci02Tl|gA(HusoioQS!PN5_=agsB+q|fvvd6Q52P5&TW z<5C?-!4z2EmlBf!Gcbr&@EN4=E5S+5Im?t*q@}mr3!1IsS2`3S&MD$l!*WGX0w>XA z!^CaP&e!Y@!fyP^ZmG@g@N0I5Cc#5m;MUGI`{dW`3%7Z-+-847iHxAwi9hHSF~1#{ z(<@q;)6RoIfBx7oYvYs_HTOit(70obhk!Wh3XgSjsDn!q$mPicSVZF4LFJ`C> ztf?^LzhTmCeuFlf!9-S7Gpuj>7O&p4`K3>8+ypbBpqc9>)yUB&xP=1jgYW1EsH!O4 z2mH0aCMOHYSh&*f2x8!-t)lQ-r72Uk18Af(ZvqOuTIjiez%^)-$F^w|uEizF}y_8N_(Q`W+ zd(y1M&mO_4Xfu?^t!ug44hvu58;uy_+Z?;T9p8Wf)Z$o4d)!ly%Ob!T6B!I+K8B$k zoTt|^a2TF|KgGtNtQ#UdP}@?=%SaSvPEvzGs9J(?C`IhAmFgWS;1&6 zI8mJka30P)oY|UbCFV={)NQugMdZIN`w6od5+j{}))d0K&Ech@0Z`1_AuI z0m2&v5MaeFtRR7HA#-6>fv)|iEqt5{o7;kH;-xUe?pR^-Cyq_Ru<23jix|<`h_d@4 zk7(_0;#L9ReiTP#E={SxisD=56yEh2Xy#|aOisfpp;@;nHxXjuOisaUEZ7ufLLIOJ zR{=T|7mpgYtq@OSnohw@czKPfk&aImF?B1 zrRqQpUpm0@pIi{n;Ugd7BD9*S^K@K4<(h}Ld$&bue32RRc+9gMuk$dhha09=PZg$n?w)nRO4I@vLX@Up$PNxBmiO!yf|x literal 0 HcmV?d00001 diff --git a/controllers/__pycache__/subcontractor_controller.cpython-313.pyc b/controllers/__pycache__/subcontractor_controller.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b220d1eb2292bdbc14011117dd7c52c63b636f10 GIT binary patch literal 4817 zcmb_fOKcm*8J<}#$rWEqiLy-VVX>+FpdC^2D~|k1Y&n%o*^14znie9kpvjd)ha#2P zWdjRv>K^(4a&UmgfDbz4C<8tC(1UK}(WjOK4aitHt&183e3L=90eb5H4_~`9owPv) z;LmyeGynYaegFJh^ZPvniun6KW&h58WBMMRI9Enp#;{p@n4(5o9 zOpH63Gwx!p2#yPL?zqGxUU$s(#68RtAxZicQN*P8giv3P`N9NxoP$Jhwfg+L&kcQ2 zt1poBB}SXZwEvYJ#iMv91jRQcBqhZ^NKP~coBu5M2F-v?Q|_YK`v%RRO|!R)X5VsO zH1q;3iMq_53%R1U#5^v&L#}R?Dqv>;>W?-MBKo94?B#+AxTFWD!77 zFAL;EqrO0o3u7co=cANKFc=j~F_SVR<*nl0jCu5>1-~WGO~qya$fuNNOG+{1`e@~sd{0__%y||@uNq^Cy$LE zojm$Zk~^u8%V|tc+MJ}eM`CNYOh0-X7Sz`Yd0jKZ?bF9>BVMpt?*(y(JeLQ)Fh0M1 zf2k~wm8G#7bdm=I&impK)S_-vQq_EFRZ~^dqpGWgOfiRbpQ?UROywFQvZ^j+nQr8= zc`aYSVRTP6y{Z{2g$xV9O6D$At#_&^!(iDPMMKk7l?}ic+l8VJ1yHsw@V4}R5WgW$ zoV_)HI``Ea#MQ@Zbp@PjLrL>;Yu91qHIx8G+6V#EI4o@!v_(tXji|D;CB?;hmfcaw zloJi_&ZhDixR4hzTG4_IG+MC213{Fbim*TeaN11-Ep)SD+MW&e}9cIOB!$Hu~ z(aCqnv%w#gC*Lm(l%#U(QbmrJr8xKI|9J%V7Ob`&XdfVz$wb(Xp|gIY`*25#xn3A= z89UTztYr|doZmY)^25b3cg987-%Mht2gVZ?@S!jvcBt8;)D82PSMm(R9pwaNQKIeyCiBM+UfrK}fen3d{tMr?r0rMy;X086omy zu^CNBLWG$eMWLtQaH*dYvOp5fEqI9>^kZx*V-b0tU3A@m1!h~N*vg2B=9=*iOU zio8&k7TW6$RNbS`B=5E~T#<%1K6)(e>$>XUO7QUJht=Sjt#o_U9S_+vY_sUOyw|eJ z&K@E~IJeh_Cm@|P>=!a|22#*d*~~fHd)1=z#+&F&7iacrna-4yRm3|j)UAownb_f+V1;Xt80MKPqVyAJCKX5LrVO(d3 zgQ&w4vKe+3+dOwhC`BTTRIKlH3)w}OFbhBZ40o*g) z`at^*?)?=`+Vv@Y1NRouv%GX58hzhUY>56=?zMT6z!apTSP*SxX6re%JQa>6lAHfL zb+1Q%#4LLs1f)#(e3+q&JVCRgi9?M;3LkU+6;zM^JbP*(EGn>R97lB7qJ5~#9k3ag zwSqG-x3U21ndsgYhbrRG#>`iV$Kt_f-oV|nchB0Pb*SPywE14ucX~^)KuaRN{x4_B z!zZi#?^eB2WpRpwU_Kf$olCit{xL(ib0Y9}kF`wa-^8YuPg42kJ?nv~W(d9$wOq{Z z4s;YxFwLNDrK);ktF~r|C)^dJv4k@-CU`LPL@gV8;^tTdr3fi^iFBlBUef0N>{m3m?~|hYoLT3 gJ7+99CrkSt4U|T?&NZ&%rKei^BK+WZ#&3ua)&;{X5v literal 0 HcmV?d00001 diff --git a/controllers/__pycache__/subcontractor_controller.cpython-314.pyc b/controllers/__pycache__/subcontractor_controller.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8f6332d27171b68a0b6421945c7acbc358270d10 GIT binary patch literal 4983 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 literal 0 HcmV?d00001 diff --git a/controllers/__pycache__/village_controller.cpython-314.pyc b/controllers/__pycache__/village_controller.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..888646e04030a038cc237fa62e6b8321187ecc8f GIT binary patch literal 5565 zcmeHL&rciK9e-nw?XkyW<6k&nzz_ngLx4bjZIj)!5E38>33YbeRCQuG_5cnByKjbF zk;BT7Dg|YwidDMW9(veADiJA%J?tTes>)wb*=t%`qpXS)dT38h6ZOzu`h9O^Jcc#w z=GH@>;LW@@-|zeJ{eHf`Jm?Y}2-@REf6V;HjnGRn(3?ejdHp8_p#r*%qRb4MW@KjC zB%7wqvU!@7*=dVxnYPN-X`5^d6B^SDH*J^g^xZt;kR33`&hXPt*-7UtGs3h>c7@R` zrUyl>x7;U9>OOMIt(Li7eRJa(wMDt8eb^LroY5$6*`s_AozzJ5r|jK_-+2Ec4l< zw5ZtbO1YKH{91%jT-n@0W+g#*fHr*%rfjiHHk(`!;ni|Orb~XL&fIg3B-81HHd4$} z7e7$gx$8GRAY99$yqHT%DRM-;S>eSD|F4}CjB!n+s1AcXifiydXpYs{jAsW_#F=kHCXN_mGb@L`z7YuH1Yf5X5 zdvv;}DQ1e9qvkV27SKSIEGFwbVKA}Rdo2xO9bS}|vk{YGm5A&mh10|>MeLZIo^mKw zRoseWJe}6`r3hG*R;z)PIVH;D*(}wgq`1_P#voxLaX1ROzG9*KVISGT z51m9lc0*G(E}V^jD2Z5#k5A6dT#HYN(vqCZ*Pr?1+OoJJ1Bg<&l~g91Ny?erNMXl!)o_=%yh<721eu!@Xx*(}*nyL`f!8_usOe1p7j2b|MI4*W56 z1@wkP?$%E;g{d8z>%k}YKlyFzmMvW6+!e09#I-*@Q{>vWxWTHswc{hGCoo*gYeTfSGNa! z2B99qUI`HQ3d}3Fu0KECkg5sf? zy+d%g0xlKs4WCxA1WuJpC9~N)&ZQKWBROd`Rak$C^>4AQ+y2(V z>`S3_ovCtx3fEEMI;zf=jh>D1&yQ4{-i`5%Js?BDg7^+QUFY8f~sp`O*g5TN!$&}#66@t3>{#eZ_pz? zMm|uyTUAx#VVF@Ibf7`~&Uc~iAW+vB-UWsU_MS4yI{*J+3n2eZd8lgBZkd3uefF>0{KwP*) z;*{=GcAXBNgD(j}5{9lJ)K+U&L@ zL^gWkf6(=t+?d3_18*EdaAz%s(x^jn&^>KH8!sFpd1bsF1z~MkSnVc{HpI=@*6*b@ z8BsP~kBISl))+PG5lDZMZ19OmP3s(nWE6{tF@zVJw3P$PVxO%~Y{twCo=% z^Cybz2^BA0BT%%+KMXDP>f+aM{-*Z97@7G2)y3~=b-^^tn+$%y6|)OhM5cS1j;r(_ z#Z^6T0$h<~d{2uuBBhQGBUfpL6r+CGXHC{XA5f5FeoxzVBa40`sBD-y$mI4mgF0%} zJxSgiCs3j-YNM46XH+(Cz3TyTT=-#fY`C*wX9vsiZEhPg&4b73AUQKsarO5Dr2w@W z(I|eObUz{8IO%>29T*;Qb82KH4*8y^5%PNZ=hQtxIw>ddDZ=P9>OV|$rSKQQR&<-c zKo)Ic=(On5h$hw`z12!WHCXE9>*|z08@xmAS-Bj_&kIEfq%f53(=eajl zBzWs2!7b=G+GLc{$b$x-C}689Qx_{)oP%l5{Q%nk*^1efk-)UcKySTI6+C+}%ZY_lt%zkqy6d zrfm;o(S0QksR%uNWzXq#>oyE?9d-E`qybcZ)%o6~grQ*>D~Gg>D^fceDqZ*(@Ohx)%A5ta9 literal 0 HcmV?d00001 diff --git a/controllers/auth_controller.py b/controllers/auth_controller.py new file mode 100644 index 0000000..0e63627 --- /dev/null +++ b/controllers/auth_controller.py @@ -0,0 +1,46 @@ +from flask import Blueprint, render_template, request, redirect, url_for, flash, session +from flask_login import login_user, logout_user, login_required, current_user + +from model.Auth import LoginLDAP, User +from model.Log import LogHelper + +auth_bp = Blueprint('auth', __name__) + + +@auth_bp.route('/login', methods=['GET', 'POST']) +def login(): + + if request.method == 'POST': + + loginData = LoginLDAP(request) + + if loginData.isValidLogin: + + if loginData.isDefaultCredentials: + LogHelper.log_action('Login', f"User {loginData.username} logged in (static user)") + else: + LogHelper.log_action('Login', f"User {loginData.username} logged in (LDAP)") + + session['username'] = loginData.username + + login_user(User(loginData.username)) + + return redirect(url_for('index')) + + else: + flash(loginData.errorMessage, 'danger') + + return render_template("login.html") + + +@auth_bp.route('/logout') +@login_required +def logout(): + + LogHelper.log_action('Logout', f"User {current_user.id} logged out") + + logout_user() + + flash('You have been logged out.', 'info') + + return redirect(url_for('auth.login')) \ No newline at end of file diff --git a/controllers/block_controller.py b/controllers/block_controller.py new file mode 100644 index 0000000..7e04a13 --- /dev/null +++ b/controllers/block_controller.py @@ -0,0 +1,119 @@ +from flask import Blueprint, render_template, request, redirect, url_for, jsonify +from flask_login import login_required + +import config +import mysql.connector + +from model.Block import Block +from model.Utilities import HtmlHelper + +block_bp = Blueprint('block', __name__) + + +@block_bp.route('/add_block', methods=['GET', 'POST']) +@login_required +def add_block(): + + block = Block() + + if request.method == 'POST': + block.AddBlock(request) + return block.resultMessage + + connection = config.get_db_connection() + cursor = connection.cursor() + + cursor.callproc("GetAllStates") + for rs in cursor.stored_results(): + states = rs.fetchall() + + block_data = block.GetAllBlocks(request) + + cursor.close() + connection.close() + + return render_template( + 'add_block.html', + states=states, + block_data=block_data + ) + + +# ✅ NEW ROUTE (FIX FOR DISTRICT FETCH) +@block_bp.route('/get_districts/') +@login_required +def get_districts(state_id): + + connection = config.get_db_connection() + cursor = connection.cursor() + + cursor.callproc("GetDistrictsByStateId", (state_id,)) + + districts = [] + for rs in cursor.stored_results(): + districts = rs.fetchall() + + cursor.close() + connection.close() + + district_list = [] + + for district in districts: + district_list.append({ + "id": district[0], + "name": district[1] + }) + + return jsonify(district_list) + + +@block_bp.route('/check_block', methods=['POST']) +@login_required +def check_block(): + + block = Block() + return block.CheckBlock(request) + + +@block_bp.route('/edit_block/', methods=['GET', 'POST']) +@login_required +def edit_block(block_id): + + block = Block() + + if request.method == 'POST': + block.EditBlock(request, block_id) + return block.resultMessage + + connection = config.get_db_connection() + cursor = connection.cursor() + + cursor.callproc("GetAllStates") + for rs in cursor.stored_results(): + states = rs.fetchall() + + cursor.callproc("GetAllDistrictsData") + for rs in cursor.stored_results(): + districts = rs.fetchall() + + block_data = block.GetBlockByID(block_id) + + cursor.close() + connection.close() + + return render_template( + 'edit_block.html', + block_data=block_data, + states=states, + districts=districts + ) + + +@block_bp.route('/delete_block/') +@login_required +def delete_block(block_id): + + block = Block() + block.DeleteBlock(request, block_id) + + return redirect(url_for('block.add_block')) \ No newline at end of file diff --git a/controllers/district_controller.py b/controllers/district_controller.py new file mode 100644 index 0000000..d326d68 --- /dev/null +++ b/controllers/district_controller.py @@ -0,0 +1,84 @@ +from flask import Blueprint, render_template, request, redirect, url_for, flash +from flask_login import login_required + +from model.District import District +from model.State import State + +district_bp = Blueprint('district', __name__) + + +@district_bp.route('/add_district', methods=['GET', 'POST']) +@login_required +def add_district(): + + district = District() + + if request.method == 'POST': + district.AddDistrict(request=request) + return district.resultMessage + + state = State() + states = state.GetAllStates(request=request) + + districtdata = district.GetAllDistricts(request=request) + + return render_template( + 'add_district.html', + districtdata=districtdata, + states=states + ) + + +@district_bp.route('/check_district', methods=['POST']) +@login_required +def check_district(): + + district = District() + + return district.CheckDistrict(request=request) + + +@district_bp.route('/delete_district/') +@login_required +def delete_district(district_id): + + district = District() + + district.DeleteDistrict(request=request, district_id=district_id) + + if not district.isSuccess: + return district.resultMessage + else: + return redirect(url_for('district.add_district')) + + +@district_bp.route('/edit_district/', methods=['GET', 'POST']) +@login_required +def edit_district(district_id): + + district = District() + state = State() + + if request.method == 'POST': + + district.EditDistrict(request=request, district_id=district_id) + + if district.isSuccess: + flash("District updated successfully!", "success") + return redirect(url_for('district.add_district')) + else: + flash(district.resultMessage, "error") + + districtdata = district.GetDistrictByID(request=request, district_id=district_id) + + if not districtdata: + flash("District not found", "error") + return redirect(url_for('district.add_district')) + + states = state.GetAllStates(request=request) + + return render_template( + 'edit_district.html', + districtdata=districtdata, + states=states + ) \ No newline at end of file diff --git a/controllers/excel_upload_controller.py b/controllers/excel_upload_controller.py new file mode 100644 index 0000000..0f543d5 --- /dev/null +++ b/controllers/excel_upload_controller.py @@ -0,0 +1,388 @@ +import os +import ast +import re +from flask_login import login_required +import openpyxl +from flask import Blueprint, request, render_template, redirect, url_for, jsonify, current_app +from flask_login import current_user +from model.Log import LogHelper +import config +from model.FolderAndFile import FolderAndFile + +excel_bp = Blueprint('excel', __name__) + +# Default folder in case config not set +# DEFAULT_UPLOAD_FOLDER = 'uploads' + + +# def get_upload_folder(): +# """Returns the upload folder from Flask config or default, ensures it exists.""" +# folder = current_app.config.get('UPLOAD_FOLDER', DEFAULT_UPLOAD_FOLDER) +# if not os.path.exists(folder): +# os.makedirs(folder) +# return folder + + +# ---------------- Upload Excel File ---------------- +@excel_bp.route('/upload_excel_file', methods=['GET', 'POST']) +@login_required +def upload(): + if request.method == 'POST': + file = request.files.get('file') + if file and file.filename.endswith('.xlsx'): + # upload_folder = get_upload_folder() + # filepath = os.path.join(upload_folder, file.filename) + + filepath =FolderAndFile.get_upload_path(file.filename) + + file.save(filepath) + + LogHelper.log_action( + "Upload Excel File", + f"User {current_user.id} Upload Excel File '{file.filename}'" + ) + return redirect(url_for('excel.show_table', filename=file.filename)) + return render_template('uploadExcelFile.html') + + +# ---------------- Show Excel Table ---------------- +@excel_bp.route('/show_table/') +def show_table(filename): + global data + data = [] + + # filepath = os.path.join(get_upload_folder(), filename) + filepath = FolderAndFile.get_upload_path(filename) + wb = openpyxl.load_workbook(filepath, data_only=True) + sheet = wb.active + + file_info = { + "Subcontractor": sheet.cell(row=1, column=2).value, + "State": sheet.cell(row=2, column=2).value, + "District": sheet.cell(row=3, column=2).value, + "Block": sheet.cell(row=4, column=2).value, + } + + errors = [] + subcontractor_data = None + state_data = None + district_data = None + block_data = None + + connection = config.get_db_connection() + if connection: + try: + cursor = connection.cursor(dictionary=True) + + cursor.callproc('GetStateByName', [file_info['State']]) + for result in cursor.stored_results(): + state_data = result.fetchone() + if not state_data: + errors.append(f"State '{file_info['State']}' is not valid. Please add it.") + + if state_data: + cursor.callproc('GetDistrictByNameAndStates', [file_info['District'], state_data['State_ID']]) + for result in cursor.stored_results(): + district_data = result.fetchone() + if not district_data: + errors.append(f"District '{file_info['District']}' is not valid under state '{file_info['State']}'.") + + if district_data: + cursor.callproc('GetBlockByNameAndDistricts', [file_info['Block'], district_data['District_ID']]) + for result in cursor.stored_results(): + block_data = result.fetchone() + if not block_data: + errors.append(f"Block '{file_info['Block']}' is not valid under district '{file_info['District']}'.") + + cursor.callproc('GetSubcontractorByName', [file_info['Subcontractor']]) + for result in cursor.stored_results(): + subcontractor_data = result.fetchone() + + if not subcontractor_data: + cursor.callproc('InsertSubcontractor', [file_info['Subcontractor']]) + connection.commit() + cursor.callproc('GetSubcontractorByName', [file_info['Subcontractor']]) + for result in cursor.stored_results(): + subcontractor_data = result.fetchone() + + cursor.callproc("GetAllHoldTypes") + hold_types_data = [] + for ht in cursor.stored_results(): + hold_types_data = ht.fetchall() + hold_types_lookup = {row['hold_type'].lower(): row['hold_type_id'] for row in hold_types_data if row['hold_type']} + + cursor.close() + except Exception as e: + print(f"Database error: {e}") + return f"Database operation failed: {e}", 500 + finally: + connection.close() + + variables = {} + hold_columns = [] + hold_counter = 0 + + for j in range(1, sheet.max_column + 1): + col_value = sheet.cell(row=5, column=j).value + if col_value: + variables[col_value] = j + if 'hold' in str(col_value).lower(): + hold_counter += 1 + hold_type_key = str(col_value).lower().strip() + hold_type_id = hold_types_lookup.get(hold_type_key) + hold_columns.append({ + 'column_name': col_value, + 'column_number': j, + 'hold_type_id': hold_type_id + }) + + for i in range(6, sheet.max_row + 1): + row_data = {} + if sheet.cell(row=i, column=1).value: + row_data["Row Number"] = i + for var_name, col_num in variables.items(): + row_data[var_name] = sheet.cell(row=i, column=col_num).value + if sum(1 for value in row_data.values() if value) >= 4: + data.append(row_data) + + for hold in hold_columns: + if hold['hold_type_id']: + print(f" if Column: {hold['column_name']}, Column Number: {hold['column_number']}, Hold Type ID: {hold['hold_type_id']}") + else: + errors.append(f"Hold Type not added ! Column name '{hold['column_name']}'.") + print(f" else Column: {hold['column_name']}, Column Number: {hold['column_number']}, Hold Type ID: {hold['hold_type_id']}") + + return render_template( + 'show_excel_file.html', + file_info=file_info, + variables=variables, + data=data, + subcontractor_data=subcontractor_data, + state_data=state_data, + district_data=district_data, + block_data=block_data, + errors=errors, + hold_columns=hold_columns, + hold_counter=hold_counter + ) + + + # save Excel data +@excel_bp.route('/save_data', methods=['POST']) +def save_data(): + # Extract form data + subcontractor_id = request.form.get("subcontractor_data") + state_id = request.form.get("state_data") + district_id = request.form.get("district_data") + block_id = request.form.get("block_data") + variables = request.form.getlist('variables[]') + hold_columns = request.form.get("hold_columns") + hold_counter = request.form.get("hold_counter") + if not data: + return jsonify({"error": "No data provided to save"}), 400 + if data: + connection = config.get_db_connection() + cursor = connection.cursor() + try: + for entry in data: + save_data = { + "PMC_No": entry.get("PMC_No"), + "Invoice_Details": entry.get("Invoice_Details", ''), + "Work_Type": 'none', + "Invoice_Date": entry.get("Invoice_Date").strftime('%Y-%m-%d') if entry.get( + "Invoice_Date") else None, + "Invoice_No": entry.get("Invoice_No", ''), + "Basic_Amount": entry.get("Basic_Amount", 0.00), + "Debit_Amount": entry.get("Debit_Amount", 0.00), + "After_Debit_Amount": entry.get("After_Debit_Amount", 0.00), + "Amount": entry.get("Amount", 0.00), + "GST_Amount": entry.get("GST_Amount", 0.00), + "TDS_Amount": entry.get("TDS_Amount", 0.00), + "SD_Amount": entry.get("SD_Amount", 0.00), + "On_Commission": entry.get("On_Commission", 0.00), + "Hydro_Testing": entry.get("Hydro_Testing", 0.00), + "Hold_Amount": 0, + "GST_SD_Amount": entry.get("GST_SD_Amount", 0.00), + "Final_Amount": entry.get("Final_Amount", 0.00), + "Payment_Amount": entry.get("Payment_Amount", 0.00), + "Total_Amount": entry.get("Total_Amount", 0.00), + "TDS_Payment_Amount": entry.get("TDS_Payment_Amount", 0.00), + "UTR": entry.get("UTR", ''), + } + village_name, work_type = None, None + village_id = 0 + LogHelper.log_action("Data saved", f"User {current_user.id} Data saved'{ village_name}'") + PMC_No = save_data.get('PMC_No') + Invoice_Details = save_data.get('Invoice_Details') + Invoice_Date = save_data.get('Invoice_Date') + Invoice_No = save_data.get('Invoice_No') + Basic_Amount = save_data.get('Basic_Amount') + Debit_Amount = save_data.get('Debit_Amount') + After_Debit_Amount = save_data.get('After_Debit_Amount') + Amount = save_data.get('Amount') + GST_Amount = save_data.get('GST_Amount') + TDS_Amount = save_data.get('TDS_Amount') + SD_Amount = save_data.get('SD_Amount') + On_Commission = save_data.get('On_Commission') + Hydro_Testing = save_data.get('Hydro_Testing') + GST_SD_Amount = save_data.get('GST_SD_Amount') + Final_Amount = save_data.get('Final_Amount') + Payment_Amount = save_data.get('Payment_Amount') + Total_Amount = save_data.get('Total_Amount') + TDS_Payment_Amount = save_data.get('TDS_Payment_Amount') + UTR = save_data.get('UTR') + + if Invoice_Details: + words = Invoice_Details.lower().split() + if 'village' in words: + village_pos = words.index('village') + village_name = " ".join(words[:village_pos]) + if 'work' in words: + work_pos = words.index('work') + if village_name: + work_type = " ".join(words[village_pos + 1:work_pos + 1]) + else: + work_type = " ".join(words[:work_pos + 1]) + if Invoice_Details and 'village' in Invoice_Details.lower() and 'work' in Invoice_Details.lower(): + print("village_name ::", village_name, "|| work_type ::", work_type) + if block_id and village_name: + village_id = None + cursor.callproc("GetVillageId", (block_id, village_name)) + for result in cursor.stored_results(): + result = result.fetchone() + village_id = result[0] if result else None + if not village_id: + cursor.callproc("SaveVillage", (village_name, block_id)) + cursor.callproc("GetVillageId", (block_id, village_name)) + for result in cursor.stored_results(): + result = result.fetchone() + village_id = result[0] if result else None + print("village_id :", village_id) + print("block_id :", block_id) + print("invoice :", PMC_No, village_id, work_type, Invoice_Details, Invoice_Date, Invoice_No, + Basic_Amount, Debit_Amount, After_Debit_Amount, Amount, GST_Amount, TDS_Amount, + SD_Amount, On_Commission, Hydro_Testing, GST_SD_Amount, Final_Amount) + + args = ( + PMC_No, village_id, work_type, Invoice_Details, Invoice_Date, Invoice_No, + Basic_Amount, Debit_Amount, After_Debit_Amount, Amount, GST_Amount, TDS_Amount, + SD_Amount, On_Commission, Hydro_Testing, GST_SD_Amount, Final_Amount, + subcontractor_id, 0 + ) + + print("All invoice Details ",args) + results = cursor.callproc('SaveInvoice', args) + invoice_id = results[-1] + print("invoice id from the excel ", invoice_id) + if isinstance(hold_columns, str): + hold_columns = ast.literal_eval(hold_columns) + if isinstance(hold_columns, list) and all(isinstance(hold, dict) for hold in hold_columns): + for hold in hold_columns: + print(f"Processing hold: {hold}") + hold_column_name = hold.get('column_name') # Get column name + hold_type_id = hold.get('hold_type_id') # Get hold_type_id + if hold_column_name: + hold_amount = entry.get( + hold_column_name) # Get the value for that specific hold column + if hold_amount is not None: + print(f"Processing hold type: {hold_column_name}, Hold Amount: {hold_amount}") + hold_join_data = { + "Contractor_Id": subcontractor_id, + "Invoice_Id": invoice_id, + "hold_type_id": hold_type_id, + "hold_amount": hold_amount + } + cursor.callproc('InsertHoldJoinData', [ + hold_join_data['Contractor_Id'], hold_join_data['Invoice_Id'], + hold_join_data['hold_type_id'], hold_join_data['hold_amount'] + ]) + connection.commit() + print(f"Inserted hold join data: {hold_join_data}") + else: + print(f"Invalid hold entry: {hold}") + else: + print("Hold columns data is not a valid list of dictionaries.") +#---------------------------------------------Credit Note--------------------------------------------------------------------------- + elif any(keyword in Invoice_Details.lower() for keyword in ['credit note','logging report']): + print("Credit note found:", PMC_No, Invoice_No, Basic_Amount, Debit_Amount, Final_Amount, + After_Debit_Amount, GST_Amount, Amount, Final_Amount, Payment_Amount, Total_Amount, UTR, Invoice_No) + cursor.callproc( + 'AddCreditNoteFromExcel', + [ + PMC_No, Invoice_Details, Basic_Amount, Debit_Amount, After_Debit_Amount, + GST_Amount, Amount, Final_Amount, Payment_Amount, Total_Amount, UTR, + subcontractor_id, Invoice_No + ] + ) +#-----------------------------------------------Hold Amount---------------------------------------------------------------------- + # Step 1: Normalize Invoice_Details: lowercase, trim, remove extra spaces + normalized_details = re.sub(r'\s+', ' ', Invoice_Details.strip()).lower() + # Step 2: Define lowercase keywords + keywords = [ + 'excess hold', + 'ht', + 'hold release amount', + 'dpr excess hold amount', + 'excess hold amount', + 'Multi to Single layer bill', + 'hold amount', + 'logging report' + ] + # Step 3: Matching condition + if any(kw in normalized_details for kw in keywords): + print("✅ Match found. Inserting hold release for:", Invoice_Details) + cursor.callproc( + 'AddHoldReleaseFromExcel', + [PMC_No, Invoice_No, Invoice_Details, Basic_Amount, Final_Amount, UTR, subcontractor_id] +) + connection.commit() + print("✅ Hold release inserted for:", PMC_No, Invoice_Details) + #------------------------------------------------------------------------------------------------------------------ + elif Invoice_Details and any( + keyword in Invoice_Details.lower() for keyword in ['gst', 'release', 'note']): + print("Gst rels :", PMC_No, Invoice_No, Basic_Amount, Final_Amount,Total_Amount,UTR, subcontractor_id) + cursor.callproc( + 'AddGSTReleaseFromExcel', + [PMC_No, Invoice_No, Basic_Amount, Final_Amount, Total_Amount, UTR, subcontractor_id] + ) + + if PMC_No and Total_Amount and UTR: + print("Payment :", PMC_No, Invoice_No, Payment_Amount, TDS_Payment_Amount, Total_Amount, UTR ) + cursor.callproc("SavePayment",(PMC_No, Invoice_No, Payment_Amount, TDS_Payment_Amount, Total_Amount, UTR )) + if not village_id: + village_id = None + cursor.callproc('InsertOrUpdateInPayment', ( + PMC_No, + village_id, + work_type, + Invoice_Details, + Invoice_Date, + Invoice_No, + Basic_Amount, + Debit_Amount, + After_Debit_Amount, + Amount, + GST_Amount, + TDS_Amount, + SD_Amount, + On_Commission, + Hydro_Testing, + 0, + GST_SD_Amount, + Final_Amount, + Payment_Amount, + TDS_Payment_Amount, + Total_Amount, + UTR, + subcontractor_id + )) + connection.commit() + return jsonify({"success": "Data saved successfully!"}), 200 + except Exception as e: + connection.rollback() + return jsonify({"error": f"An unexpected error occurred: {e}"}), 500 + finally: + cursor.close() + connection.close() + return render_template('index.html') +# ---------------------- Report -------------------------------- \ No newline at end of file diff --git a/controllers/gst_release_controller.py b/controllers/gst_release_controller.py new file mode 100644 index 0000000..a5fea92 --- /dev/null +++ b/controllers/gst_release_controller.py @@ -0,0 +1,70 @@ +from flask import Blueprint, render_template, request, redirect, url_for, jsonify +from flask_login import login_required, current_user +from model.gst_release import GSTReleasemodel +from model.Log import LogHelper + +gst_release_bp = Blueprint('gst_release_bp', __name__) + +# ------------------- Add GST Release ------------------- +@gst_release_bp.route('/add_gst_release', methods=['GET', 'POST']) +@login_required +def add_gst_release(): + gst_releases_dict = GSTReleasemodel.fetch_all_gst_releases() + gst_releases = [ + [ + g['GST_Release_Id'], g['PMC_No'], g['invoice_no'], + g['Basic_Amount'], g['Final_Amount'], g['Total_Amount'], g['UTR'] + ] for g in gst_releases_dict + ] if gst_releases_dict else [] + + if request.method == 'POST': + pmc_no = request.form['PMC_No'] + invoice_no = request.form['invoice_No'] + basic_amount = request.form['basic_amount'] + final_amount = request.form['final_amount'] + total_amount = request.form['total_amount'] + utr = request.form['utr'] + contractor_id = request.form['subcontractor_id'] + + LogHelper.log_action("Add GST Release", f"User {current_user.id} added GST release '{pmc_no}'") + GSTReleasemodel.insert_gst_release(pmc_no, invoice_no, basic_amount, final_amount, total_amount, utr, contractor_id) + + return redirect(url_for('gst_release_bp.add_gst_release')) + + return render_template('add_gst_release.html', gst_releases=gst_releases, invoices=[]) + + +# ------------------- Edit GST Release ------------------- +@gst_release_bp.route('/edit_gst_release/', methods=['GET', 'POST']) +@login_required +def edit_gst_release(gst_release_id): + gst_release_data = GSTReleasemodel.fetch_gst_release_by_id(gst_release_id) + if not gst_release_data: + return "GST Release not found", 404 + + if request.method == 'POST': + pmc_no = request.form['PMC_No'] + invoice_no = request.form['invoice_No'] + basic_amount = request.form['basic_amount'] + final_amount = request.form['final_amount'] + total_amount = request.form['total_amount'] + utr = request.form['utr'] + + LogHelper.log_action("Edit GST Release", f"User {current_user.id} edited GST release '{pmc_no}'") + GSTReleasemodel.update_gst_release(gst_release_id, pmc_no, invoice_no, basic_amount, final_amount, total_amount, utr) + + return redirect(url_for('gst_release_bp.add_gst_release')) + + return render_template('edit_gst_release.html', gst_release_data=gst_release_data, invoices=[]) + + +# ------------------- Delete GST Release ------------------- +@gst_release_bp.route('/delete_gst_release/', methods=['GET', 'POST']) +@login_required +def delete_gst_release(gst_release_id): + success, utr = GSTReleasemodel.delete_gst_release(gst_release_id) + if not success: + return jsonify({"message": "GST Release not found or failed to delete", "status": "error"}), 404 + + LogHelper.log_action("Delete GST Release", f"User {current_user.id} deleted GST release '{gst_release_id}'") + return jsonify({"message": f"GST Release {gst_release_id} deleted successfully.", "status": "success"}), 200 \ No newline at end of file diff --git a/controllers/hold_types_controller.py b/controllers/hold_types_controller.py new file mode 100644 index 0000000..0b163ab --- /dev/null +++ b/controllers/hold_types_controller.py @@ -0,0 +1,77 @@ +from flask import Blueprint, render_template, request, jsonify, redirect, url_for +from flask_login import login_required +from model.HoldTypes import HoldTypes +from model.GST import GST + +hold_bp = Blueprint("hold_types", __name__) + + +# ---------------- ADD HOLD TYPE ---------------- +@hold_bp.route('/add_hold_type', methods=['GET','POST']) +@login_required +def add_hold_type(): + + hold = HoldTypes() + + if request.method == 'POST': + hold.AddHoldType(request) # ✅ + return hold.resultMessage + + hold_types = hold.GetAllHoldTypes() # ✅ + + return render_template( + "add_hold_type.html", + Hold_Types_data=hold_types + ) + + +# ---------------- CHECK HOLD TYPE (OPTIONAL LIKE BLOCK) ---------------- +@hold_bp.route('/check_hold_type', methods=['POST']) +@login_required +def check_hold_type(): + + hold = HoldTypes() + return hold.CheckHoldType(request) # if exists + + +# ---------------- EDIT HOLD TYPE ---------------- +@hold_bp.route('/edit_hold_type/', methods=['GET','POST']) +@login_required +def edit_hold_type(id): + + hold = HoldTypes() + + if request.method == 'POST': + hold.EditHoldType(request, id) # ✅ + return hold.resultMessage + + hold_data = hold.GetHoldTypeByID(id) # ✅ + + return render_template( + "edit_hold_type.html", + hold_type=hold_data + ) + + +# ---------------- DELETE HOLD TYPE ---------------- +@hold_bp.route('/delete_hold_type/') +@login_required +def delete_hold_type(id): + + hold = HoldTypes() + hold.DeleteHoldType(request, id) # ✅ + + return redirect(url_for("hold_types.add_hold_type")) + + +# ---------------- GST ---------------- +@hold_bp.route('/unreleased_gst') +@login_required +def unreleased_gst(): + + data = GST.get_unreleased_gst() + + return render_template( + "unreleased_gst.html", + data=data + ) \ No newline at end of file diff --git a/controllers/invoice_controller.py b/controllers/invoice_controller.py new file mode 100644 index 0000000..d8af37a --- /dev/null +++ b/controllers/invoice_controller.py @@ -0,0 +1,98 @@ +# controllers/invoice_controller.py + +from flask import Blueprint, request, jsonify, render_template +from flask_login import login_required, current_user +from model.Invoice import * +from model.Log import LogHelper + +invoice_bp = Blueprint('invoice', __name__) + +# -------------------------------- Add Invoice --------------------------------- +@invoice_bp.route('/add_invoice', methods=['GET', 'POST']) +@login_required +def add_invoice(): + if request.method == 'POST': + try: + village_name = request.form.get('village') + village_result = get_village_id(village_name) + + if not village_result: + return jsonify({"status": "error", "message": f"Village '{village_name}' not found"}), 400 + + village_id = village_result['Village_Id'] + data = request.form + + invoice_id = insert_invoice(data, village_id) + assign_subcontractor(data, village_id) + insert_hold_types(data, invoice_id) + + LogHelper.log_action("Add invoice", f"User {current_user.id} Added invoice '{data.get('pmc_no')}'") + + return jsonify({"status": "success", "message": "Invoice added successfully"}), 201 + + except Exception as e: + return jsonify({"status": "error", "message": str(e)}), 500 + + invoices = get_all_invoice_details() + villages = get_all_villages() + return render_template('add_invoice.html', invoices=invoices, villages=villages) + + +# ------------------- Search Subcontractor ------------------- +@invoice_bp.route('/search_subcontractor', methods=['POST']) +@login_required +def search_subcontractor(): + sub_query = request.form.get("query") + results = search_contractors(sub_query) + + if not results: + return "

  • No subcontractor found
  • " + + output = "".join( + f"
  • {row['Contractor_Name']}
  • " + for row in results + ) + return output + + +# ------------------- Get Hold Types ------------------- +@invoice_bp.route('/get_hold_types', methods=['GET']) +@login_required +def get_hold_types(): + hold_types = get_all_hold_types() + LogHelper.log_action("Get hold type", f"User {current_user.id} Get hold type '{hold_types}'") + return jsonify(hold_types) + + +# ------------------- Edit Invoice ------------------- +@invoice_bp.route('/edit_invoice/', methods=['GET', 'POST']) +@login_required +def edit_invoice(invoice_id): + if request.method == 'POST': + data = request.form + update_invoice(data, invoice_id) + update_inpayment(data) + + LogHelper.log_action("Edit invoice", f"User {current_user.id} Edit invoice '{invoice_id}'") + return jsonify({"status": "success", "message": "Invoice updated successfully"}), 200 + + invoice = get_invoice_by_id(invoice_id) + return render_template('edit_invoice.html', invoice=invoice) + + +# ------------------- Delete Invoice ------------------- +@invoice_bp.route('/delete_invoice/', methods=['GET']) +@login_required +def delete_invoice_route(invoice_id): + try: + delete_invoice_data(invoice_id, current_user.id) + LogHelper.log_action("Delete Invoice", f"User {current_user.id} deleted Invoice '{invoice_id}'") + return jsonify({ + "message": f"Invoice {invoice_id} deleted successfully.", + "status": "success" + }) + except Exception as e: + return jsonify({ + "message": str(e), + "status": "error" + }), 500 \ No newline at end of file diff --git a/controllers/log_controller.py b/controllers/log_controller.py new file mode 100644 index 0000000..145c91b --- /dev/null +++ b/controllers/log_controller.py @@ -0,0 +1,31 @@ +from flask import Blueprint, render_template, request +from flask_login import login_required + +from model.Log import LogData + +log_bp = Blueprint('log', __name__) + + +@log_bp.route('/activity_log', methods=['GET', 'POST']) +@login_required +def activity_log(): + + start_date = request.values.get("start_date") + end_date = request.values.get("end_date") + user_name = request.values.get("username") + + logData = LogData() + + filtered_logs = logData.GetFilteredActivitiesLog( + start_date, + end_date, + user_name + ) + + return render_template( + "activity_log.html", + logs=filtered_logs, + start_date=start_date, + end_date=end_date, + username=user_name + ) \ No newline at end of file diff --git a/controllers/payment_controller.py b/controllers/payment_controller.py new file mode 100644 index 0000000..a0dfc40 --- /dev/null +++ b/controllers/payment_controller.py @@ -0,0 +1,103 @@ +from flask import Blueprint, render_template, request, redirect, url_for, jsonify +from flask_login import login_required, current_user +from model.payment import Paymentmodel +from model.Log import LogHelper + +payment_bp = Blueprint('payment_bp', __name__) + +# ------------------- Add Payment ------------------- +@payment_bp.route('/add_payment', methods=['GET', 'POST']) +@login_required +def add_payment(): + payments_dicts = Paymentmodel.fetch_all_payments() + # Convert to array for template + payments = [ + [ + p['Payment_Id'], p['PMC_No'], p['Invoice_No'], + p['Payment_Amount'], p['TDS_Payment_Amount'], p['Total_Amount'], p['UTR'] + ] for p in payments_dicts + ] if payments_dicts else [] + + if request.method == 'POST': + subcontractor_id = request.form.get('subcontractor_id') + pmc_no = request.form['PMC_No'] + invoice_no = request.form['invoice_No'] + amount = request.form['Payment_Amount'] + tds_amount = request.form['TDS_Payment_Amount'] + total_amount = request.form['total_amount'] + utr = request.form['utr'] + + LogHelper.log_action("Add Payment", f"User {current_user.id} Add Payment '{pmc_no}'") + Paymentmodel.insert_payment(pmc_no, invoice_no, amount, tds_amount, total_amount, utr) + Paymentmodel.update_inpayment(subcontractor_id, pmc_no, invoice_no, amount, tds_amount, total_amount, utr) + + return redirect(url_for('payment_bp.add_payment')) + + return render_template('add_payment.html', payments=payments) + + +# ------------------- Get PMC Nos ------------------- +@payment_bp.route('/get_pmc_nos_by_subcontractor/') +@login_required +def get_pmc_nos_by_subcontractor(subcontractorId): + connection = Paymentmodel.get_connection() + cur = connection.cursor() + cur.callproc('GetDistinctPMCNoByContractorId', [subcontractorId]) + results = [] + for result in cur.stored_results(): + results = result.fetchall() + cur.close() + pmc_nos = [row[0] for row in results] + return jsonify({'pmc_nos': pmc_nos}) + + +# ------------------- Edit Payment ------------------- +@payment_bp.route('/edit_payment/', methods=['GET', 'POST']) +@login_required +def edit_payment(payment_id): + payment_data = Paymentmodel.fetch_payment_by_id(payment_id) + + if not payment_data: + return "Payment not found", 404 + + if request.method == 'POST': + pmc_no = request.form['PMC_No'] + invoice_no = request.form['invoice_No'] + amount = request.form['Payment_Amount'] + tds_amount = request.form['TDS_Payment_Amount'] + total_amount = request.form['total_amount'] + utr = request.form['utr'] + + LogHelper.log_action("Edit Payment", f"User {current_user.id} Edit Payment '{pmc_no}'") + Paymentmodel.call_update_payment_proc(payment_id, pmc_no, invoice_no, amount, tds_amount, total_amount, utr) + + # Update inpayment + connection = Paymentmodel.get_connection() + cursor = connection.cursor() + cursor.callproc( + 'UpdateInpaymentByPMCInvoiceUTR', + [amount, tds_amount, total_amount, pmc_no, invoice_no, utr] +) + connection.commit() + cursor.close() + connection.close() + + return redirect(url_for('payment_bp.add_payment')) + + return render_template('edit_payment.html', payment_data=payment_data) + + +# ------------------- Delete Payment ------------------- +@payment_bp.route('/delete_payment/', methods=['GET', 'POST']) +@login_required +def delete_payment(payment_id): + success, pmc_no, invoice_no = Paymentmodel.delete_payment(payment_id) + if not success: + return jsonify({"message": "Payment not found or failed to delete", "status": "error"}), 404 + + LogHelper.log_action("Delete Payment", f"User {current_user.id} deleted Payment '{payment_id}'") + + return jsonify({ + "message": f"Payment ID {payment_id} deleted successfully.", + "status": "success" + }), 200 \ No newline at end of file diff --git a/controllers/pmc_report_controller.py b/controllers/pmc_report_controller.py new file mode 100644 index 0000000..7c92cb7 --- /dev/null +++ b/controllers/pmc_report_controller.py @@ -0,0 +1,37 @@ +from flask import Blueprint, render_template, send_from_directory +from flask_login import login_required, current_user +from model.PmcReport import PmcReport + +pmc_report_bp = Blueprint("pmc_report", __name__) + +@pmc_report_bp.route("/pmc_report/") +@login_required +def pmc_report(pmc_no): + data = PmcReport.get_pmc_report(pmc_no) + if not data: + return "No PMC found with this number", 404 + + return render_template( + "pmc_report.html", + info=data["info"], + invoices=data["invoices"], + hold_types=data["hold_types"], + gst_rel=data["gst_rel"], + payments=data["payments"], + credit_note=data["credit_note"], + hold_release=data["hold_release"], + total=data["total"] + ) + +@pmc_report_bp.route("/download_pmc_report/") +@login_required +def download_pmc_report(pmc_no): + + result = PmcReport.download_pmc_report(pmc_no) + + if not result: + return "No contractor found for this PMC No", 404 + + output_folder, file_name = result + + return send_from_directory(output_folder, file_name, as_attachment=True) \ No newline at end of file diff --git a/controllers/report_controller.py b/controllers/report_controller.py new file mode 100644 index 0000000..5810a8a --- /dev/null +++ b/controllers/report_controller.py @@ -0,0 +1,193 @@ +from flask import Blueprint, render_template, request, jsonify, send_file +from flask_login import login_required, current_user +from model.Report import ReportHelper +from model.Log import LogHelper +import config +import os +import openpyxl +from openpyxl.styles import Font +from model.ContractorInfo import ContractorInfo +from model.FolderAndFile import FolderAndFile + + +report_bp = Blueprint("report", __name__) + + +# ---------------- Report Page ---------------- +@report_bp.route("/report") +@login_required +def report_page(): + return render_template("/report.html") + + +# ---------------- Search Contractor ---------------- +@report_bp.route("/search_contractor", methods=["POST"]) +@login_required +def search_contractor(): + + subcontractor_name = request.form.get("subcontractor_name") + + LogHelper.log_action( + "Search Contractor", + f"User {current_user.id} searched contractor '{subcontractor_name}'" + ) + + data = ReportHelper.search_contractor(request) + + return jsonify(data) + +# ---------------- Contractor Report ---------------- +@report_bp.route('/contractor_report/') +@login_required +def contractor_report(contractor_id): + + data = ReportHelper.get_contractor_report(contractor_id) + + return render_template( + 'subcontractor_report.html', + contractor_id=contractor_id, + **data + ) + +@report_bp.route('/download_report/') +@login_required +def download_report(contractor_id): + + return ReportHelper().download_report(contractor_id=contractor_id) + + + + +# @report_bp.route('/download_report/') +# @login_required +# def download_report(contractor_id): +# try: +# connection = config.get_db_connection() +# cursor = connection.cursor(dictionary=True) + +# # -------- Contractor Info -------- +# contractor = ContractorInfo(contractor_id) +# contInfo = contractor.contInfo + +# if not contInfo: +# return "No contractor found", 404 + +# # -------- Invoice Data -------- +# cursor.callproc('FetchInvoicesByContractor', [contractor_id]) + +# invoices = [] +# for result in cursor.stored_results(): +# invoices.extend(result.fetchall()) + +# if not invoices: +# return "No invoice data found" + +# # -------- Create Workbook -------- +# workbook = openpyxl.Workbook() +# sheet = workbook.active +# sheet.title = "Contractor Report" + +# # ================= CONTRACTOR DETAILS ================= +# sheet.append(["SUB CONTRACTOR DETAILS"]) +# sheet.cell(row=sheet.max_row, column=1).font = Font(bold=True) +# sheet.append([]) + +# sheet.append(["Name", contInfo.get("Contractor_Name") or ""]) +# sheet.append(["Mobile No", contInfo.get("Mobile_No") or ""]) +# sheet.append(["Email", contInfo.get("Email") or ""]) +# sheet.append(["Village", contInfo.get("Village_Name") or ""]) +# sheet.append(["Block", contInfo.get("Block_Name") or ""]) +# sheet.append(["District", contInfo.get("District_Name") or ""]) +# sheet.append(["State", contInfo.get("State_Name") or ""]) +# sheet.append(["Address", contInfo.get("Address") or ""]) +# sheet.append(["GST No", contInfo.get("GST_No") or ""]) +# sheet.append(["PAN No", contInfo.get("PAN_No") or ""]) +# sheet.append([]) +# sheet.append([]) + +# # ================= TABLE HEADERS ================= +# headers = [ +# "PMC No", "Village", "Invoice No", "Invoice Date", "Work Type","Invoice_Details", +# "Basic Amount", "Debit Amount", "After Debit Amount", +# "Amount", "GST Amount", "TDS Amount", "SD Amount", +# "On Commission", "Hydro Testing", "Hold Amount", +# "GST SD Amount", "Final Amount", +# "Payment Amount", "TDS Payment", +# "Total Amount", "UTR" +# ] +# sheet.append(headers) +# for col in range(1, len(headers) + 1): +# sheet.cell(row=sheet.max_row, column=col).font = Font(bold=True) + +# # ================= DATA ================= +# total_final = 0 +# total_payment = 0 +# total_amount = 0 + +# for inv in invoices: +# row = [ +# inv.get("PMC_No"), +# inv.get("Village_Name"), +# inv.get("invoice_no"), +# inv.get("Invoice_Date"), +# inv.get("Work_Type"), +# inv.get("Invoice_Details"), +# inv.get("Basic_Amount"), +# inv.get("Debit_Amount"), +# inv.get("After_Debit_Amount"), +# inv.get("Amount"), +# inv.get("GST_Amount"), +# inv.get("TDS_Amount"), +# inv.get("SD_Amount"), +# inv.get("On_Commission"), +# inv.get("Hydro_Testing"), +# inv.get("Hold_Amount"), +# inv.get("GST_SD_Amount"), +# inv.get("Final_Amount"), +# inv.get("Payment_Amount"), +# inv.get("TDS_Payment_Amount"), +# inv.get("Total_Amount"), +# inv.get("UTR") +# ] + +# total_final += float(inv.get("Final_Amount") or 0) +# total_payment += float(inv.get("Payment_Amount") or 0) +# total_amount += float(inv.get("Total_Amount") or 0) + +# sheet.append(row) + +# # ================= TOTAL ROW ================= +# sheet.append([]) +# sheet.append([ +# "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", +# "TOTAL", +# total_final, +# total_payment, +# "", +# total_amount, +# "" +# ]) + +# # ================= AUTO WIDTH ================= +# for column in sheet.columns: +# max_length = 0 +# column_letter = column[0].column_letter +# for cell in column: +# if cell.value: +# max_length = max(max_length, len(str(cell.value))) +# sheet.column_dimensions[column_letter].width = max_length + 2 + +# # ================= SAVE FILE ================= +# # output_folder = "downloads" +# # os.makedirs(output_folder, exist_ok=True) +# # filename = f"Contractor_Report_{contInfo.get('Contractor_Name')}.xlsx" +# # output_file = os.path.join(output_folder, filename) + +# filename = f"Contractor_Report_{contInfo.get('Contractor_Name')}.xlsx" +# output_file = FolderAndFile.get_download_path(filename) +# workbook.save(output_file) + +# return send_file(output_file, as_attachment=True) + +# except Exception as e: +# return str(e) \ No newline at end of file diff --git a/controllers/state_controller.py b/controllers/state_controller.py new file mode 100644 index 0000000..2ccada9 --- /dev/null +++ b/controllers/state_controller.py @@ -0,0 +1,69 @@ +from flask import Blueprint, render_template, request, redirect, url_for +from flask_login import login_required +from model.State import State + +state_bp = Blueprint('state', __name__) + + +@state_bp.route('/add_state', methods=['GET', 'POST']) +@login_required +def add_state(): + + state = State() + + if request.method == 'POST': + state.AddState(request=request) + return state.resultMessage + + statedata = state.GetAllStates(request=request) + + return render_template('add_state.html', statedata=statedata) + + +@state_bp.route('/check_state', methods=['POST']) +@login_required +def check_state(): + + state = State() + + return state.CheckState(request=request) + + +@state_bp.route('/delete_state/') +@login_required +def deleteState(id): + + state = State() + + msg = state.DeleteState(request=request, id=id) + + if not state.isSuccess: + return state.resultMessage + else: + return redirect(url_for('state.add_state')) + + +@state_bp.route('/edit_state/', methods=['GET', 'POST']) +@login_required +def editState(id): + + state = State() + + if request.method == 'POST': + + state.EditState(request=request, id=id) + + if state.isSuccess: + return redirect(url_for('state.add_state')) + else: + return state.resultMessage + + statedata = state.GetStateByID(request=request, id=id) + + if not state.isSuccess: + return state.resultMessage + + if statedata is None: + statedata = [] + + return render_template('edit_state.html', state=statedata) \ No newline at end of file diff --git a/controllers/subcontractor_controller.py b/controllers/subcontractor_controller.py new file mode 100644 index 0000000..884ed6a --- /dev/null +++ b/controllers/subcontractor_controller.py @@ -0,0 +1,117 @@ +from flask import Blueprint, render_template, request, redirect, url_for, jsonify +from flask_login import login_required +from model.Subcontractor import Subcontractor + +subcontractor_bp = Blueprint('subcontractor', __name__) + +# ---------------------------------------------------------- +# Helpers (unchanged) +# ---------------------------------------------------------- +class HtmlHelper: + @staticmethod + def json_response(data, status=200): + return jsonify(data), status + +class ResponseHandler: + @staticmethod + def fetch_failure(entity): + return {"status": "error", "message": f"Failed to fetch {entity}"} + + @staticmethod + def add_failure(entity): + return {"status": "error", "message": f"Failed to add {entity}"} + + @staticmethod + def update_failure(entity): + return {"status": "error", "message": f"Failed to update {entity}"} + + @staticmethod + def delete_failure(entity): + return {"status": "error", "message": f"Failed to delete {entity}"} + + +# ---------------------------------------------------------- +# LIST + ADD +# ---------------------------------------------------------- +@subcontractor_bp.route('/subcontractor', methods=['GET', 'POST']) +@login_required +def subcontract(): + + sub = Subcontractor() + + # ---------------- GET ---------------- + if request.method == 'GET': + subcontractor = sub.GetAllSubcontractors(request) + + if not sub.isSuccess: + return HtmlHelper.json_response( + ResponseHandler.fetch_failure("Subcontractor"), 500 + ) + + return render_template('add_subcontractor.html', subcontractor=subcontractor) + + # ---------------- POST (ADD) ---------------- + if request.method == 'POST': + + sub.AddSubcontractor(request) + + if not sub.isSuccess: + return HtmlHelper.json_response( + ResponseHandler.add_failure("Subcontractor"), 500 + ) + + # Reload list after insert + subcontractor = sub.GetAllSubcontractors(request) + + return render_template('add_subcontractor.html', subcontractor=subcontractor) + + +# ---------------------------------------------------------- +# EDIT +# ---------------------------------------------------------- +@subcontractor_bp.route('/edit_subcontractor/', methods=['GET', 'POST']) +@login_required +def edit_subcontractor(id): + + sub = Subcontractor() + + # Fetch data + subcontractor = sub.GetSubcontractorByID(id) + + if not subcontractor: + return HtmlHelper.json_response( + ResponseHandler.fetch_failure("Subcontractor"), 404 + ) + + # ---------------- POST (UPDATE) ---------------- + if request.method == 'POST': + + sub.EditSubcontractor(request, id) + + if not sub.isSuccess: + return HtmlHelper.json_response( + ResponseHandler.update_failure("Subcontractor"), 500 + ) + + return redirect(url_for('subcontractor.subcontract')) + + return render_template('edit_subcontractor.html', subcontractor=subcontractor) + + +# ---------------------------------------------------------- +# DELETE +# ---------------------------------------------------------- +@subcontractor_bp.route('/deleteSubContractor/', methods=['GET', 'POST']) +@login_required +def deleteSubContractor(id): + + sub = Subcontractor() + + sub.DeleteSubcontractor(request, id) + + if not sub.isSuccess: + return HtmlHelper.json_response( + ResponseHandler.delete_failure("Subcontractor"), 500 + ) + + return redirect(url_for('subcontractor.subcontract')) \ No newline at end of file diff --git a/controllers/village_controller.py b/controllers/village_controller.py new file mode 100644 index 0000000..02df862 --- /dev/null +++ b/controllers/village_controller.py @@ -0,0 +1,167 @@ +from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify +from flask_login import login_required + +import config + +from model.Village import Village +from model.State import State + +# Create Blueprint +village_bp = Blueprint('village', __name__) + + +# ------------------------- Add Village ------------------------- +@village_bp.route('/add_village', methods=['GET', 'POST']) +@login_required +def add_village(): + + village = Village() + + if request.method == 'POST': + village.AddVillage(request=request) + return village.resultMessage + + state = State() + states = state.GetAllStates(request=request) + + villages = village.GetAllVillages(request=request) + + return render_template( + 'add_village.html', + states=states, + villages=villages + ) + + +# ------------------------- Fetch Districts ------------------------- +@village_bp.route('/get_districts/') +@login_required +def get_districts(state_id): + + connection = config.get_db_connection() + cursor = connection.cursor() + + cursor.callproc("GetDistrictByStateID", [state_id]) + + districts = [] + + for rs in cursor.stored_results(): + districts = rs.fetchall() + + cursor.close() + connection.close() + + district_list = [] + + for d in districts: + district_list.append({ + "id": d[0], + "name": d[1] + }) + + return jsonify(district_list) + + +# ------------------------- Fetch Blocks ------------------------- +@village_bp.route('/get_blocks/') +@login_required +def get_blocks(district_id): + + connection = config.get_db_connection() + cursor = connection.cursor() + + cursor.callproc("GetBlocksByDistrictID", [district_id]) + + blocks = [] + + for rs in cursor.stored_results(): + blocks = rs.fetchall() + + cursor.close() + connection.close() + + block_list = [] + + for b in blocks: + block_list.append({ + "id": b[0], + "name": b[1] + }) + + return jsonify(block_list) + + +# ------------------------- Check Village ------------------------- +@village_bp.route('/check_village', methods=['POST']) +@login_required +def check_village(): + + village = Village() + return village.CheckVillage(request=request) + + +# ------------------------- Delete Village ------------------------- +@village_bp.route('/delete_village/') +@login_required +def delete_village(village_id): + + village = Village() + + village.DeleteVillage(request=request, village_id=village_id) + + if not village.isSuccess: + flash(village.resultMessage, "error") + else: + flash(village.resultMessage, "success") + + return redirect(url_for('village.add_village')) + + +# ------------------------- Edit Village ------------------------- +@village_bp.route('/edit_village/', methods=['GET', 'POST']) +@login_required +def edit_village(village_id): + + village = Village() + + if request.method == 'POST': + + village.EditVillage(request=request, village_id=village_id) + + if village.isSuccess: + flash(village.resultMessage, "success") + return redirect(url_for('village.add_village')) + + else: + flash(village.resultMessage, "error") + + village_data = village.GetVillageByID(request=request, id=village_id) + blocks = village.GetAllBlocks(request=request) + + return render_template( + 'edit_village.html', + village_data=village_data, + blocks=blocks + ) + + else: + + village_data = village.GetVillageByID(request=request, id=village_id) + + if not village.isSuccess: + flash(village.resultMessage, "error") + return redirect(url_for('village.add_village')) + + blocks = village.GetAllBlocks(request=request) + + if village_data is None: + village_data = [] + + if blocks is None: + blocks = [] + + return render_template( + 'edit_village.html', + village_data=village_data, + blocks=blocks + ) \ No newline at end of file diff --git a/main.py b/main.py index 870a7b9..96a2977 100644 --- a/main.py +++ b/main.py @@ -1,5019 +1,68 @@ -from decimal import Decimal +# main.py +from flask import Flask, render_template +from flask_login import LoginManager +from model.Auth import User -from AppCode import ContractorInfo, Auth, Utilities, Log -from AppCode.Utilities import RegEx, ResponseHandler, HtmlHelper -from AppCode.Auth import LoginLDAP, User -from AppCode.Log import LogData, LogHelper -from AppCode.State import State -from AppCode.District import District -from AppCode.Block import Block -from AppCode.Village import Village -# need to optimize above import lines +# Import Blueprints / Controllers +from controllers.auth_controller import auth_bp +from controllers.log_controller import log_bp +from controllers.state_controller import state_bp +from controllers.district_controller import district_bp +from controllers.block_controller import block_bp +from controllers.village_controller import village_bp +from controllers.invoice_controller import invoice_bp +from controllers.subcontractor_controller import subcontractor_bp +from controllers.payment_controller import payment_bp +from controllers.gst_release_controller import gst_release_bp +from controllers.excel_upload_controller import excel_bp +from controllers.report_controller import report_bp +from controllers.pmc_report_controller import pmc_report_bp -from flask import Flask, render_template, request, redirect, url_for, send_from_directory, flash, jsonify, json -from flask import current_app, session, send_file - -from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user - -import mysql.connector -from mysql.connector import Error - -import config +from controllers.hold_types_controller import hold_bp +from dotenv import load_dotenv import os -import re -import ast -from datetime import datetime - - -import openpyxl -from openpyxl import load_workbook -from openpyxl.styles import Font, PatternFill, Alignment -from openpyxl.utils import get_column_letter - - -import logging - -import pandas as pd - - -#import AppRoutes.StateRoute - - -# this is server +# ---------------- Initialize App ---------------- app = Flask(__name__) + +load_dotenv() +Secret_Key = os.getenv("SECRET_KEY") +app.secret_key = Secret_Key + +# ---------------- Login Manager ---------------- login_manager = LoginManager() login_manager.init_app(app) -login_manager.login_view = 'login' - +login_manager.login_view = 'auth.login' @login_manager.user_loader def load_user(user_id): return User(user_id) -#need to check and understand above function -app.secret_key = '9f2a1b8c4d6e7f0123456789abcdef01' -#Shouldnt be hardcoded - - - - -# this is Index page OR Home page.. +# ---------------- Home Route ---------------- @app.route('/') -@login_required def index(): - return render_template('index.html') - - - - - -# ---------------- LOGIN ROUTE ---------------- - -@app.route('/login', methods=['GET', 'POST']) -def login(): - if request.method == 'POST': - loginData = LoginLDAP(request) - # If bind successful → set session and log - - if loginData.isValidLogin: - if loginData.isDefaultCredentials: - LogHelper.log_action('Login', f"User {loginData.username} logged in (static user)") - else: - LogHelper.log_action('Login', f"User {loginData.username} logged in (LDAP)") - - session['username'] = loginData.username - login_user(User(loginData.username)) - return redirect(url_for('index', login='success')) - else: - flash(loginData.errorMessage, 'danger') - - return render_template('login.html') - - - - -@app.route('/logout') -@login_required -def logout(): - LogHelper.log_action('Logout', f"User {current_user.id} logged out") # log the event - logout_user() - flash('You have been logged out.', 'info') - return redirect(url_for('login')) - - - - -@app.route('/activity_log', methods=['GET', 'POST']) -@login_required -def activity_log(): - - # Filters (GET or POST) - start_date = request.values.get("start_date") - end_date = request.values.get("end_date") - user_name = request.values.get("username") - - logData = LogData() - filtered_logs = logData.GetFilteredActivitiesLog(start_date,end_date,user_name) - - return render_template( - "activity_log.html", - logs=filtered_logs, - start_date=start_date, - end_date=end_date, - username=user_name - ) - - - -# ------------------------- State controller ------------------------------------------ -@app.route('/add_state', methods=['GET', 'POST']) -@login_required -def add_state(): - state = State() - if request.method == 'POST': - state.AddState(request=request) - return state.resultMessage - - statedata = state.GetAllStates(request=request) - return render_template('add_state.html', statedata=statedata) - - - -# AJAX route to check state existence -@app.route('/check_state', methods=['POST']) -@login_required -def check_state(): - state = State() - return state.CheckState(request=request) - - -# Delete State -@app.route('/delete_state/', methods=['GET']) -@login_required -def deleteState(id): - state = State() - msg = state.DeleteState(request=request, id=id) - - if not state.isSuccess: - return state.resultMessage - else: - return redirect(url_for('add_state')) - - -# Edit State -@app.route('/edit_state/', methods=['GET', 'POST']) -@login_required -def editState(id): - connection = config.get_db_connection() - cursor = connection.cursor() - - state = State() - statedata = [] - - if request.method == 'POST': - state.EditState(request=request, id=id) - if state.isSuccess: - return redirect(url_for('add_state')) - else: - return state.resultMessage - else: - statedata = state.GetStateByID(request=request, id=id) - - if not state.isSuccess: - return state.resultMessage - - - if statedata is None: - statedata = [] - - return render_template('edit_state.html', state=statedata) - - -# -------- end State controller ----------- - -# ------------------------- District controller ------------------------------------------ -@app.route('/add_district', methods=['GET', 'POST']) -@login_required -def add_district(): - district = District() - - if request.method == 'POST': - district.AddDistrict(request=request) - return district.resultMessage - state = State() - states = state.GetAllStates(request=request) - - districtdata = district.GetAllDistricts(request=request) - - return render_template('add_district.html', districtdata=districtdata, states=states) - - -# AJAX route to check district existence -@app.route('/check_district', methods=['POST']) -@login_required -def check_district(): - district = District() - return district.CheckDistrict(request=request) - - -# Delete District -@app.route('/delete_district/', methods=['GET']) -@login_required -def delete_district(district_id): - district = District() - district.DeleteDistrict(request=request, id=district_id) - - if not district.isSuccess: - return district.resultMessage - else: - return redirect(url_for('add_district')) - - -# Edit District -@app.route('/edit_district/', methods=['GET', 'POST']) -@login_required -def edit_district(district_id): - district = District() - - if request.method == 'POST': - district.EditDistrict(request=request, id=district_id) - if district.isSuccess: - return redirect(url_for('add_district')) - else: - - flash(district.resultMessage, "error") - - districtdata = district.GetDistrictByID(request=request, id=district_id) - state = State() - states = state.GetAllStates(request=request) - return render_template('edit_district.html', districtdata=districtdata, states=states) - - # GET Request - else: - districtdata = district.GetDistrictByID(request=request, id=district_id) - - if not district.isSuccess: - flash(district.resultMessage, "error") - return redirect(url_for('add_district')) - - - state = State() - states = state.GetAllStates(request=request) - - if districtdata is None: - districtdata = [] - if states is None: - states = [] - - return render_template('edit_district.html', districtdata=districtdata, states=states) - - -# --------- end District controller ------------- - -# ------------------------- Block controller ------------------------------------------ -@app.route('/add_block', methods=['GET', 'POST']) -@login_required -def add_block(): - block = Block() - district = District() - - # form submission - if request.method == 'POST': - block.AddBlock(request) - return block.resultMessage - - # Fetch all states - connection = config.get_db_connection() - cursor = connection.cursor() - cursor.callproc("GetAllStates") - for rs in cursor.stored_results(): - states = rs.fetchall() - - # Fetch all blocks - block_data = block.GetAllBlocks() - - return render_template('add_block.html', states=states, block_data=block_data) - - -@app.route('/check_block', methods=['POST']) -@login_required -def check_block(): - block = Block() - return block.CheckBlock(request) - - -@app.route('/edit_block/', methods=['GET', 'POST']) -@login_required -def edit_block(block_id): - block = Block() - - if request.method == 'POST': - return block.EditBlock(request, block_id) - - # Load all states - connection = config.get_db_connection() - cursor = connection.cursor() - cursor.callproc("GetAllStates") - for rs in cursor.stored_results(): - states = rs.fetchall() - - # Load all districts - cursor.callproc("GetAllDistrictsData") - for rs in cursor.stored_results(): - districts = rs.fetchall() - - block_data = block.GetBlockByID(block_id) - - return render_template('edit_block.html', block_data=block_data, states=states, districts=districts) - - -@app.route('/delete_block/') -@login_required -def delete_block(block_id): - block = Block() - block.DeleteBlock(block_id) - return redirect(url_for('add_block')) - - -# get block by district id -@app.route('/get_blocks/', methods=['GET']) -@login_required -def get_blocks(district_id): - connection = config.get_db_connection() - cursor = connection.cursor() - blocks = [] - - try: - # cursor.execute("SELECT Block_Id, Block_Name FROM blocks WHERE District_id = %s", (district_id,)) - # blocks = cursor.fetchall() - cursor.callproc("GetBlocksByDistrict", (district_id,)) - for rs in cursor.stored_results(): - blocks = rs.fetchall() - # log_action("Get blocks", f"User {current_user.id} Get Blocks '{district_id}'") - except mysql.connector.Error as e: - print(f"Error fetching blocks: {e}") - return HtmlHelper.json_response({"error": "Failed to fetch blocks"}, 500) - finally: - cursor.close() - connection.close() - - return jsonify({"blocks": [{"Block_Id": block[0], "Block_Name": block[1]} for block in blocks]}) - - - - -# this is get district all data by using state id .. -@app.route('/get_districts/', methods=['GET']) -@login_required -def get_districts(state_id): - connection = config.get_db_connection() - districts = [] - - if connection: - cursor = connection.cursor() - try: - # cursor.execute("SELECT District_id, District_Name FROM districts WHERE State_Id = %s", (state_id,)) - # districts = cursor.fetchall() - - cursor.callproc("GetDistrictsByStateId", (state_id,)) - for dis in cursor.stored_results(): - districts = dis.fetchall() - LogHelper.log_action("Get District", f"User {current_user.id} Get District '{state_id}'") - except mysql.connector.Error as e: - print(f"Error fetching districts: {e}") - return HtmlHelper.json_response(ResponseHandler.fetch_failure("districts"), 500) - finally: - cursor.close() - connection.close() - - return jsonify({ - "districts": [{"District_id": d[0], "District_Name": d[1]} for d in districts] - }) - - -# ----------- end Block controller ----------------- - -# ------------------------- Village controller ------------------------------------------ -# Route to add a village -@app.route('/add_village', methods=['GET', 'POST']) -@login_required -def add_village(): - village = Village() - - if request.method == 'POST': - village.AddVillage(request=request) - return village.resultMessage - - - state = State() # Use the State class to get states - states = state.GetAllStates(request=request) - - villages = village.GetAllVillages(request=request) - - return render_template('add_village.html', states=states, villages=villages) - -@app.route('/check_village', methods=['POST']) -@login_required -def check_village(): - village = Village() - return village.CheckVillage(request=request) - - -# Delete Village -@app.route('/delete_village/', methods=['GET']) -@login_required -def delete_village(village_id): - village = Village() - village.DeleteVillage(request=request, village_id=village_id) - - if not village.isSuccess: - flash(village.resultMessage, "error") - return redirect(url_for('add_village')) - else: - - return redirect(url_for('add_village')) - - -# Edit Village -@app.route('/edit_village/', methods=['GET', 'POST']) -@login_required -def edit_village(village_id): - village = Village() - - if request.method == 'POST': - village.EditVillage(request=request, village_id=village_id) - if village.isSuccess: - flash(village.resultMessage, "success") - return redirect(url_for('add_village')) - else: - - flash(village.resultMessage, "error") - - village_data = village.GetVillageByID(request=request, id=village_id) - blocks = village.GetAllBlocks(request=request) - return render_template('edit_village.html', village_data=village_data, blocks=blocks) - - - else: - village_data = village.GetVillageByID(request=request, id=village_id) - - if not village.isSuccess: - flash(village.resultMessage, "error") - return redirect(url_for('add_village')) - - blocks = village.GetAllBlocks(request=request) - - if village_data is None: - village_data = [] # Ensure it's iterable in template - if blocks is None: - blocks = [] - - return render_template('edit_village.html', village_data=village_data, blocks=blocks) - - - -# ---- end Village controller --------------------- - - -# -------------------------------- Invoice controller ------------------------------------------ -@app.route('/add_invoice', methods=['GET', 'POST']) -@login_required -def add_invoice(): - connection = config.get_db_connection() - if not connection: - return jsonify({"status": "error", "message": "Database connection failed"}), 500 - - if request.method == 'POST': - try: - cursor = connection.cursor(dictionary=True) - - # Get the village name from the form - village_name = request.form.get('village') - - print("village name", village_name) - - # Query the database to get the corresponding Village_Id based on the village name - # cursor.execute("SELECT Village_Id FROM villages WHERE Village_Name = %s", (village_name,)) - # village_result = cursor.fetchone() - cursor.callproc("GetVillageIdByName", (village_name,)) - for rs in cursor.stored_results(): - village_result = rs.fetchone() - - if not village_result: - return jsonify({"status": "error", "message": f"Village '{village_name}' not found"}), 400 - - village_id = village_result['Village_Id'] - - # Fetch form data - pmc_no = request.form.get('pmc_no') - work_type = request.form.get('work_type') - invoice_details = request.form.get('invoice_details') - invoice_date = request.form.get('invoice_date') - invoice_no = request.form.get('invoice_no') - basic_amount = request.form.get('basic_amount') - basic_amount=float(basic_amount) if basic_amount else 0.0 - debit_amount = request.form.get('debit_amount') - debit_amount=float(debit_amount) if debit_amount else 0.0 - after_debit_amount = request.form.get('after_debit_amount') - after_debit_amount=float(after_debit_amount) if after_debit_amount else 0.0 - amount = request.form.get('amount') - amount=float(amount) if amount else 0.0 - gst_amount = request.form.get('gst_amount') - gst_amount=float(gst_amount) if gst_amount else 0.0 - tds_amount = request.form.get('tds_amount') - tds_amount=float(tds_amount) if tds_amount else 0.0 - sd_amount = request.form.get('sd_amount') - sd_amount=float(sd_amount) if sd_amount else 0.0 - on_commission = request.form.get('on_commission') - on_commission=float(on_commission) if on_commission else 0.0 - hydro_testing = request.form.get('hydro_testing') - hydro_testing=float(hydro_testing) if hydro_testing else 0.0 - gst_sd_amount = request.form.get('gst_sd_amount') - gst_sd_amount=float(gst_sd_amount) if gst_sd_amount else 0.0 - final_amount = request.form.get('final_amount') - final_amount=float(final_amount) if final_amount else 0.0 - - # insert_invoice_query = ''' - # INSERT INTO invoice ( - # PMC_No, Village_Id, Work_Type, Invoice_Details, Invoice_Date, Invoice_No, - # Basic_Amount, Debit_Amount, After_Debit_Amount, Amount, GST_Amount, TDS_Amount, - # SD_Amount, On_Commission, Hydro_Testing, GST_SD_Amount, Final_Amount - # ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) - # ''' - # invoice_values = ( - # pmc_no, village_id, work_type, invoice_details, invoice_date, invoice_no, - # basic_amount, debit_amount, after_debit_amount, amount, gst_amount, tds_amount, - # sd_amount, on_commission, hydro_testing, gst_sd_amount, final_amount - # ) - # cursor.execute(insert_invoice_query, invoice_values) - # connection.commit() - # invoice_id = cursor.lastrowid - cursor.callproc('InsertInvoice', [ - pmc_no, village_id, work_type, invoice_details, invoice_date, invoice_no, - basic_amount, debit_amount, after_debit_amount, amount, gst_amount, tds_amount, - sd_amount, on_commission, hydro_testing, gst_sd_amount, final_amount]) - LogHelper.log_action("Add invoice", f"User {current_user.id} Added invoice '{ pmc_no}'") - for result in cursor.stored_results(): - invoice_id = result.fetchone()['invoice_id'] - connection.commit() - - print("This is the invocie id from the invoice table ", invoice_id) - - # Insert into assign_subcontractors table - # subcontractor_id = request.form.get('subcontractor_id') - # insert_assign_query = ''' - # INSERT INTO assign_subcontractors (PMC_no, Contractor_Id, Village_Id) - # VALUES (%s, %s, %s) - # ''' - # cursor.execute(insert_assign_query, (pmc_no, subcontractor_id, village_id)) - # connection.commit() - subcontractor_id = request.form.get('subcontractor_id') - cursor.callproc('AssignSubcontractor', [pmc_no, subcontractor_id, village_id]) - connection.commit() - - # Insert Hold Amounts into invoice_subcontractor_hold_join table - hold_types = request.form.getlist('hold_type[]') - hold_amounts = request.form.getlist('hold_amount[]') - hold_count = 0 - - for hold_type, hold_amount in zip(hold_types, hold_amounts): - # cursor.execute("SELECT hold_type_id FROM hold_types WHERE hold_type = %s", (hold_type,)) - # hold_type_result = cursor.fetchone() - cursor.callproc('GetHoldTypeIdByName', [hold_type]) - for result in cursor.stored_results(): - hold_type_result = result.fetchone() - print("hold type from invoice ", hold_type_result) - if not hold_type_result: - return jsonify({"status": "error", "message": f"Invalid Hold Type: {hold_type}"}), 400 - hold_type_id = hold_type_result['hold_type_id'] - # insert_hold_query = ''' - # INSERT INTO invoice_subcontractor_hold_join (Contractor_Id, Invoice_Id, hold_type_id, hold_amount) - # VALUES (%s, %s, %s, %s) - # ''' - # cursor.execute(insert_hold_query, (subcontractor_id, invoice_id, hold_type_id, hold_amount)) - # hold_count += 1 - - # connection.commit() - cursor.callproc('InsertInvoiceSubcontractorHold', [ - subcontractor_id, invoice_id, hold_type_id, hold_amount - ]) - connection.commit() - hold_count += 1 - print("Hold count from the invoice", hold_count) - connection.commit() - - return jsonify({"status": "success", "message": "Invoice added successfully"}), 201 - - except mysql.connector.Error as e: - connection.rollback() - return jsonify({"status": "error", "message": f"Failed to add invoice: {str(e)}"}), 500 - finally: - cursor.close() - connection.close() - - # GET request: fetch and display all invoices (all fields) along with the form - try: - cursor = connection.cursor(dictionary=True) - # cursor.execute("SELECT * FROM view_invoice_details") - # invoices = cursor.fetchall() - cursor.callproc('GetAllInvoiceDetails') - for result in cursor.stored_results(): - invoices = result.fetchall() - - villages = [] - cursor.callproc("GetAllVillages") - for result in cursor.stored_results(): - villages = result.fetchall() - - except mysql.connector.Error as e: - print(f"Error: {e}") - invoices = [] - finally: - cursor.close() - connection.close() - - return render_template('add_invoice.html', invoices=invoices, villages=villages) - - -# search subcontraactor to assing invoice -@app.route('/search_subcontractor', methods=['POST']) -@login_required -def search_subcontractor(): - connection = config.get_db_connection() - if not connection: - return HtmlHelper.json_response(ResponseHandler.fetch_failure("database connection"), 500) - - sub_query = request.form.get("query") - try: - cursor = connection.cursor(dictionary=True) - # cursor.execute( - # "SELECT Contractor_Id, Contractor_Name FROM subcontractors WHERE Contractor_Name LIKE %s", - # (f"%{sub_query}%",) - # ) - # results = cursor.fetchall() - cursor.callproc('SearchContractorsByName', [sub_query]) - for result in cursor.stored_results(): - results = result.fetchall() - print(results) - if not results: - return "
  • No subcontractor found
  • " - - output = "".join( - f"
  • {row['Contractor_Name']}
  • " - for row in results - ) - print("Ajax Call for subcontractor", output) - - return output - - except mysql.connector.Error as e: - return HtmlHelper.json_response(ResponseHandler.fetch_failure(f"Search failed: {str(e)}"), 500) - - finally: - cursor.close() - connection.close() - - -# get hold types -@app.route('/get_hold_types', methods=['GET']) -@login_required -def get_hold_types(): - connection = config.get_db_connection() - try: - cursor = connection.cursor(dictionary=True) - # cursor.execute("SELECT hold_type_id, hold_type FROM hold_types") - # hold_types = cursor.fetchall() - - cursor.callproc("GetAllHoldTypes") - for hold in cursor.stored_results(): - hold_types = hold.fetchall() - LogHelper.log_action("Get hold type", f"User {current_user.id} Get hold type'{ hold_types}'") - return jsonify(hold_types) - except mysql.connector.Error as e: - return ResponseHandler.fetch_failure({str(e)}), 500 - # return jsonify({"status": "error", "message": f"Failed to fetch hold types: {str(e)}"}), 500 - finally: - cursor.close() - connection.close() - - -# update invoice by id - -@app.route('/edit_invoice/', methods=['GET', 'POST']) -@login_required -def edit_invoice(invoice_id): - connection = config.get_db_connection() - if not connection: - return jsonify({"status": "error", "message": "Database connection failed"}), 500 - - cursor = connection.cursor(dictionary=True) - - if request.method == 'POST': - try: - # Fetch updated form data - subcontractor_id = request.form.get('subcontractor_id', '').strip() - subcontractor_id = int(subcontractor_id) if subcontractor_id else None - - village_name = request.form.get('village') - # cursor.execute("SELECT Village_Id FROM villages WHERE Village_Name = %s", (village_name,)) - # village_result = cursor.fetchone() - cursor.callproc("GetVillageIdByName", (village_name,)) - for rs in cursor.stored_results(): - village_result = rs.fetchone() - if not village_result: - return jsonify({"status": "error", "message": "Invalid Village Name"}), 400 - village_id = village_result['Village_Id'] - - pmc_no = request.form.get('pmc_no') - work_type = request.form.get('work_type') - invoice_details = request.form.get('invoice_details') - invoice_date = request.form.get('invoice_date') - invoice_no = request.form.get('invoice_no') - LogHelper.log_action("Edit invoice", f"User {current_user.id} Edit invoice'{ invoice_id}'") - # Convert numeric fields properly - numeric_fields = { - "basic_amount": request.form.get('basic_amount'), - "debit_amount": request.form.get('debit_amount'), - "after_debit_amount": request.form.get('after_debit_amount'), - "amount": request.form.get('amount'), - "gst_amount": request.form.get('gst_amount'), - "tds_amount": request.form.get('tds_amount'), - "sd_amount": request.form.get('sd_amount'), - "on_commission": request.form.get('on_commission'), - "hydro_testing": request.form.get('hydro_testing'), - "gst_sd_amount": request.form.get('gst_sd_amount'), - "final_amount": request.form.get('final_amount'), - } - numeric_fields = {k: float(v) if v else 0 for k, v in numeric_fields.items()} - - # # Update invoice - # update_invoice_query = ''' - # UPDATE invoice - # SET PMC_No=%s, Village_Id=%s, Work_Type=%s, Invoice_Details=%s, Invoice_Date=%s, - # Invoice_No=%s, Basic_Amount=%s, Debit_Amount=%s, After_Debit_Amount=%s, - # Amount=%s, GST_Amount=%s, TDS_Amount=%s, SD_Amount=%s, On_Commission=%s, - # Hydro_Testing=%s, GST_SD_Amount=%s, Final_Amount=%s - # WHERE Invoice_Id=%s - # ''' - # invoice_values = ( - # pmc_no, village_id, work_type, invoice_details, invoice_date, invoice_no, - # *numeric_fields.values(), invoice_id - # ) - # cursor.execute(update_invoice_query, invoice_values) - # connection.commit() - cursor.callproc('UpdateInvoice', [ - pmc_no, village_id, work_type, invoice_details, invoice_date, invoice_no, - *numeric_fields.values(), invoice_id - ]) - connection.commit() - - # Handle holds - hold_types = request.form.getlist('hold_type[]') - hold_amounts = request.form.getlist('hold_amount[]') - - for hold_type, hold_amount in zip(hold_types, hold_amounts): - if not hold_type: - continue # skip empty hold types - - # Get or insert hold type - # cursor.execute("SELECT hold_type_id FROM hold_types WHERE hold_type = %s", (hold_type,)) - # hold_type_result = cursor.fetchone() - cursor.callproc('GetHoldTypeIdByName', [hold_type]) - for result in cursor.stored_results(): - hold_type_result = result.fetchone() - - # if not hold_type_result: - # cursor.execute("INSERT INTO hold_types (hold_type) VALUES (%s)", (hold_type,)) - # connection.commit() - # hold_type_id = cursor.lastrowid - # else: - # hold_type_id = hold_type_result['hold_type_id'] - - if not hold_type_result: - # Call stored procedure to insert and return new ID - cursor.callproc('InsertHoldType', [hold_type, 0]) - for result in cursor.stored_results(): - pass # advance past any results - cursor.execute("SELECT @_InsertHoldType_1") - hold_type_id = cursor.fetchone()[0] - print("if not hold type result anish:", hold_type_id) - else: - hold_type_id = hold_type_result['hold_type_id'] - print("if hold type result anish:", hold_type_id) - - hold_amount = float(hold_amount) if hold_amount else 0 - - # Check if join exists - # cursor.execute(""" - # SELECT join_id FROM invoice_subcontractor_hold_join - # WHERE Invoice_Id = %s AND Contractor_Id = %s AND hold_type_id = %s - # """, (invoice_id, subcontractor_id, hold_type_id)) - # join_result = cursor.fetchone() - cursor.callproc('GetHoldJoinId', [invoice_id, subcontractor_id, hold_type_id]) - for result in cursor.stored_results(): - join_result = result.fetchone() - - if join_result: - # cursor.execute(""" - # UPDATE invoice_subcontractor_hold_join - # SET hold_amount = %s - # WHERE join_id = %s - # """, (hold_amount, join_result['join_id'])) - cursor.callproc('UpdateHoldAmountByJoinId', [hold_amount, join_result['join_id']]) - connection.commit() - - else: - # cursor.execute(""" - # INSERT INTO invoice_subcontractor_hold_join (Contractor_Id, Invoice_Id, hold_type_id, hold_amount) - # VALUES (%s, %s, %s, %s) - # """, (subcontractor_id, invoice_id, hold_type_id, hold_amount)) - cursor.callproc('InsertInvoiceSubcontractorHold', [ - subcontractor_id, invoice_id, hold_type_id, hold_amount - ]) - connection.commit() - - connection.commit() - return jsonify({"status": "success", "message": "Invoice updated successfully"}), 200 - - except mysql.connector.Error as e: - connection.rollback() - return jsonify({"status": "error", "message": f"Failed to update invoice: {str(e)}"}), 500 - finally: - cursor.close() - connection.close() - - # ------------------ GET Request ------------------ - - try: - # Fetch invoice data - # cursor.execute( - # """SELECT i.*, s.Contractor_Name, v.Village_Name - # FROM invoice i - # LEFT JOIN assign_subcontractors a ON i.PMC_No = a.PMC_no AND i.Village_Id = a.Village_Id - # LEFT JOIN subcontractors s ON a.Contractor_Id = s.Contractor_Id - # LEFT JOIN villages v ON i.Village_Id = v.Village_Id - # WHERE i.Invoice_Id = %s""", (invoice_id,) - # ) - # invoice = cursor.fetchone() - cursor.callproc('GetInvoiceDetailsById', [invoice_id]) - for result in cursor.stored_results(): - invoice = result.fetchone() - - if not invoice: - return jsonify({"status": "error", "message": "Invoice not found"}), 404 - - # Important! Clear unread result issue - while cursor.nextset(): - pass - - # Fetch hold amounts - # cursor.execute( - # """SELECT h.hold_type, ihj.hold_amount - # FROM invoice_subcontractor_hold_join ihj - # JOIN hold_types h ON ihj.hold_type_id = h.hold_type_id - # WHERE ihj.Invoice_Id = %s""", (invoice_id,) - # ) - # hold_amounts = cursor.fetchall() - # invoice["hold_amounts"] = hold_amounts - cursor.callproc('GetHoldAmountsByInvoiceId', [invoice_id]) - for result in cursor.stored_results(): - hold_amounts = result.fetchall() - - invoice["hold_amounts"] = hold_amounts - - - except mysql.connector.Error as e: - return jsonify({"status": "error", "message": f"Database error: {str(e)}"}), 500 - finally: - cursor.close() - connection.close() - - return render_template('edit_invoice.html', invoice=invoice) - - -# delete invoice by id -@app.route('/delete_invoice/', methods=['GET']) -@login_required -def delete_invoice(invoice_id): - connection = config.get_db_connection() - if not connection: - return HtmlHelper.json_response(ResponseHandler.fetch_failure("invoice"), 500) - - try: - cursor = connection.cursor() - # cursor.execute("DELETE FROM invoice WHERE Invoice_Id = %s", (invoice_id,)) - - cursor.callproc("DeleteInvoice", (invoice_id,)) - LogHelper.log_action("Delete invoice", f"User {current_user.id} Delete invoice'{ invoice_id}'") - connection.commit() - - # Check if the invoice was actually deleted - if cursor.rowcount == 0: - return HtmlHelper.json_response(ResponseHandler.fetch_failure("invoice"), 404) - - return redirect(url_for('add_invoice')) - - except mysql.connector.Error as e: - print("Error deleting invoice:", e) - return HtmlHelper.json_response(ResponseHandler.delete_failure("invoice"), 500) - - finally: - cursor.close() - connection.close() - - -# ---------- end Invoice controller ------------------ - - -# ----------------------------- Payment controller ------------------------------------------ -# this is Payment Page to add data -# @app.route('/add_payment', methods=['GET', 'POST']) -# def add_payment(): -# connection = config.get_db_connection() -# payments = [] # List to hold payment history -# -# if not connection: -# return HtmlHelper.json_response(ResponseHandler.fetch_failure("payment"), 500) -# -# try: -# cursor = connection.cursor() -# -# # Retrieve payment history -# # cursor.execute( -# # "SELECT Payment_Id, PMC_No, Invoice_No, Payment_Amount, TDS_Payment_Amount, Total_Amount, UTR FROM payment" -# # ) -# # payments = cursor.fetchall() -# cursor.callproc("GetAllPayments") -# for result in cursor.stored_results(): -# payments = result.fetchall() -# -# except mysql.connector.Error as e: -# print(f"Error fetching payment history: {e}") -# return HtmlHelper.json_response(ResponseHandler.fetch_failure("payment"), 500) -# finally: -# cursor.close() -# -# if request.method == 'POST': -# pmc_no = request.form['PMC_No'] -# invoice_no = request.form['invoice_No'] -# amount = request.form['Payment_Amount'] -# tds_amount = request.form['TDS_Payment_Amount'] -# total_amount = request.form['total_amount'] -# utr = request.form['utr'] -# -# try: -# cursor = connection.cursor() -# cursor.callproc('SavePayment', ( -# pmc_no, invoice_no, amount, tds_amount, total_amount, utr -# )) -# connection.commit() -# return redirect(url_for('add_payment')) # Redirect to add_payment page to reload the form -# except mysql.connector.Error as e: -# print(f"Error inserting payment: {e}") -# return HtmlHelper.json_response(ResponseHandler.add_failure("payment"), 500) -# finally: -# cursor.close() -# connection.close() -# -# return render_template('add_payment.html', payments=payments) - -@app.route('/add_payment', methods=['GET', 'POST']) -@login_required -def add_payment(): - connection = config.get_db_connection() - payments = [] - - if connection: - cursor = connection.cursor() - - try: - # cursor.execute( - # "SELECT Payment_Id, PMC_No, Invoice_No, Payment_Amount, TDS_Payment_Amount, Total_Amount, UTR FROM payment" - # ) - # payments = cursor.fetchall() - cursor.callproc('GetAllPayments') - for result in cursor.stored_results(): - payments = result.fetchall() - - except mysql.connector.Error as e: - print(f"Error fetching payment history: {e}") - return "Failed to fetch payment history", 500 - finally: - cursor.close() - - if request.method == 'POST': - pmc_no = request.form['PMC_No'] - invoice_no = request.form['invoice_No'] - amount = request.form['Payment_Amount'] - tds_amount = request.form['TDS_Payment_Amount'] - total_amount = request.form['total_amount'] - utr = request.form['utr'] - LogHelper.log_action("Add Payment", f"User {current_user.id} Add Payment'{ pmc_no}'") - try: - cursor = connection.cursor() - # cursor.execute('''INSERT INTO payment (PMC_No, invoice_no, Payment_Amount, TDS_Payment_Amount, Total_Amount, UTR) - # VALUES (%s, %s, %s, %s, %s, %s)''', - # (pmc_no, invoice_no, amount, tds_amount, total_amount, utr)) - # connection.commit() - cursor.callproc('InsertPayments', [ - pmc_no, invoice_no, amount, tds_amount, total_amount, utr - ]) - connection.commit() - - return redirect(url_for('add_payment')) - - except mysql.connector.Error as e: - print(f"Error inserting payment: {e}") - return "Failed to add payment", 500 - finally: - cursor.close() - connection.close() - - return render_template('add_payment.html', payments=payments) - - -@app.route('/get_pmc_nos_by_subcontractor/') -@login_required -def get_pmc_nos_by_subcontractor(subcontractorId): - connection = config.get_db_connection() - cur = connection.cursor() - print(subcontractorId) - # query = """ - # SELECT DISTINCT i.PMC_No - # FROM invoice i - # JOIN assign_subcontractors a ON i.PMC_No = a.PMC_no - # JOIN subcontractors s ON a.Contractor_Id = s.Contractor_Id - # WHERE s.Contractor_Id=%s; - # """ - # cur.execute(query, (subcontractorId,)) - # results = cur.fetchall() - cur.callproc('GetDistinctPMCNoByContractorId', [subcontractorId]) - for result in cur.stored_results(): - results = result.fetchall() - - print(results) - pmc_nos = [row[0] for row in results] - cur.close() - return jsonify({'pmc_nos': pmc_nos}) - - -# Edit Payment Route -@app.route('/edit_payment/', methods=['GET', 'POST']) -@login_required -def edit_payment(payment_id): - connection = config.get_db_connection() - payment_data = {} # To hold the payment data for the given ID - - if not connection: - return HtmlHelper.json_response(ResponseHandler.fetch_failure("payment"), 500) - - try: - cursor = connection.cursor() - - # Fetch the existing payment data for the given payment_id - # cursor.execute( - # "SELECT Payment_Id, PMC_No, Invoice_No, Payment_Amount, TDS_Payment_Amount, Total_Amount, UTR FROM payment WHERE Payment_Id = %s", - # (payment_id,) - # ) - # payment_data = cursor.fetchone() - - cursor.callproc("GetPaymentById", (payment_id,)) - for result in cursor.stored_results(): - payment_data = result.fetchone() - - # Handle POST request to update the payment - if request.method == 'POST': - pmc_no = request.form['PMC_No'] - invoice_no = request.form['invoice_No'] - amount = request.form['Payment_Amount'] - tds_amount = request.form['TDS_Payment_Amount'] - total_amount = request.form['total_amount'] - utr = request.form['utr'] - LogHelper.log_action("Edit Payment", f"User {current_user.id} Edit Payment'{ pmc_no}'") - try: - # cursor.execute('''UPDATE payment SET PMC_No=%s, Invoice_No=%s, Payment_Amount=%s, TDS_Payment_Amount=%s, - # Total_Amount=%s, UTR=%s WHERE Payment_Id=%s''', - # (pmc_no, invoice_no, amount, tds_amount, total_amount, utr, payment_id)) - - cursor.callproc("UpdatePayment", - (payment_id, pmc_no, invoice_no, amount, tds_amount, total_amount, utr,)) - connection.commit() - - return redirect(url_for('add_payment')) # Redirect to add_payment page to view the updated list - except mysql.connector.Error as e: - print(f"Error updating payment: {e}") - return HtmlHelper.json_response(ResponseHandler.update_failure("payment"), 500) - - except mysql.connector.Error as e: - print(f"Error fetching payment data: {e}") - return HtmlHelper.json_response(ResponseHandler.fetch_failure("payment"), 500) - finally: - cursor.close() - connection.close() - - return render_template('edit_payment.html', payment_data=payment_data) - - -# Delete Payment Route -@app.route('/delete_payment/', methods=['GET', 'POST']) -@login_required -def delete_payment(payment_id): - connection = config.get_db_connection() - if not connection: - return HtmlHelper.json_response(ResponseHandler.fetch_failure("payment"), 500) - try: - cursor = connection.cursor() - # cursor.execute("DELETE FROM payment WHERE Payment_Id = %s", (payment_id,)) - - cursor.callproc("DeletePayment", (payment_id,)) - LogHelper.log_action("Delete Payment", f"User {current_user.id} Delete Payment'{ payment_id}'") - connection.commit() - # Check if any rows were deleted - if cursor.rowcount == 0: - return HtmlHelper.json_response(ResponseHandler.fetch_failure("payment"), 404) - return redirect(url_for('add_payment')) # Redirect back to the add_payment page - - except mysql.connector.Error as e: - print(f"Error deleting payment: {e}") - return HtmlHelper.json_response(ResponseHandler.delete_failure("payment"), 500) - finally: - cursor.close() - connection.close() - - -# --- end Payment controller ----------- - -# ------------------------- GST Release controller ------------------------------------------ -@app.route('/add_gst_release', methods=['GET', 'POST']) -@login_required -def add_gst_release(): - connection = config.get_db_connection() - gst_releases = [] # List to hold GST Release history - invoices = [] # List to hold invoices for the dropdown - - if not connection: - return HtmlHelper.json_response(ResponseHandler.fetch_failure("GST Release"), 500) - - try: - cursor = connection.cursor() - - # Retrieve GST Release history - cursor.execute("SELECT GST_Release_Id, PMC_No, Invoice_No, Basic_Amount, Final_Amount,Total_Amount,UTR FROM gst_release") - gst_releases = cursor.fetchall() - - # cursor.callproc("GetAllGSTReleases") - # for result in cursor.stored_results(): - # gst_releases = result.fetchall() - - if request.method == 'POST': - pmc_no = request.form['PMC_No'] - invoice_no = request.form['invoice_No'] - basic_amount = request.form['basic_amount'] - final_amount = request.form['final_amount'] - total_amount = request.form['total_amount'] - utr = request.form['utr'] - contractor_id = request.form['subcontractor_id'] - LogHelper.log_action("Add gst_release", f"User {current_user.id} Add gst_release'{ pmc_no}'") - # cursor.callproc('SaveGSTRelease', ( - # pmc_no, invoice_no, basic_amount, final_amount,total_amount, utr - # )) - # connection.commit() - cursor.execute(""" - INSERT INTO gst_release (PMC_No, - invoice_no, - Basic_Amount, - Final_Amount, - Total_Amount, - UTR, - Contractor_Id) - VALUES (%s, %s, %s, %s, %s, %s, %s) - """, ( - pmc_no, - invoice_no, - basic_amount, - final_amount, - total_amount, - utr, - contractor_id - )) - connection.commit() - - return redirect(url_for('add_gst_release')) # Redirect to add_gst_release page - - except mysql.connector.Error as e: - print(f"Error: {e}") - return HtmlHelper.json_response(ResponseHandler.add_failure("GST Release"), 500) - - finally: - cursor.close() - connection.close() - - return render_template('add_gst_release.html', invoices=invoices, gst_releases=gst_releases) - - -# update gst Release by id -@app.route('/edit_gst_release/', methods=['GET', 'POST']) -@login_required -def edit_gst_release(gst_release_id): - connection = config.get_db_connection() - gst_release_data = {} # To hold the GST release data for the given ID - invoices = [] # List to hold invoices for the dropdown - - if not connection: - return HtmlHelper.json_response(ResponseHandler.fetch_failure("GST Release"), 500) - - try: - cursor = connection.cursor() - - # Fetch the existing GST release data for the given gst_release_id - cursor.execute( - "SELECT GST_Release_Id, PMC_No, Invoice_No, Basic_Amount, Final_Amount,Total_Amount,UTR FROM gst_release WHERE GST_Release_Id = %s", - (gst_release_id,) - ) - gst_release_data = cursor.fetchone() - - # cursor.callproc("GetGSTReleaseById", (gst_release_id,)) - # for result in cursor.stored_results(): - # gst_release_data = result.fetchone() - - if request.method == 'POST': - pmc_no = request.form['PMC_No'] - invoice_no = request.form['invoice_No'] - basic_amount = request.form['basic_amount'] - final_amount = request.form['final_amount'] - total_amount = request.form['total_amount'] - utr = request.form['utr'] - LogHelper.log_action("Edit gst_release", f"User {current_user.id} Edit gst_release'{ pmc_no}'") - try: - cursor.execute(""" - UPDATE gst_release - SET PMC_No = %s, - invoice_no = %s, - Basic_Amount = %s, - Final_Amount = %s, - Total_Amount = %s, - UTR = %s - WHERE GST_Release_Id = %s - """, ( - pmc_no, - invoice_no, - basic_amount, - final_amount, - total_amount, - utr, - gst_release_id - )) - - # cursor.callproc("UpdateGSTRelease", (gst_release_id, pmc_id, invoice_no, basic_amount, final_amount)) - # - # connection.commit() - - return redirect(url_for('add_gst_release')) # Redirect to the page to view the updated list - - except mysql.connector.Error as e: - print(f"Error updating GST Release: {e}") - return HtmlHelper.json_response(ResponseHandler.update_failure("GST Release"), 500) - - except mysql.connector.Error as e: - print(f"Error fetching GST Release data: {e}") - return HtmlHelper.json_response(ResponseHandler.fetch_failure("GST Release"), 500) - - finally: - cursor.close() - connection.close() - - return render_template('edit_gst_release.html', gst_release_data=gst_release_data, invoices=invoices) - - -# delete gst release by id -@app.route('/delete_gst_release/', methods=['GET', 'POST']) -@login_required -def delete_gst_release(gst_release_id): - connection = config.get_db_connection() - if not connection: - return HtmlHelper.json_response(ResponseHandler.fetch_failure("GST Release"), 500) - try: - cursor = connection.cursor() - # cursor.execute("DELETE FROM gst_release WHERE GST_Release_Id = %s", (gst_release_id,)) - cursor.callproc("DeleteGSTRelease", (gst_release_id,)) - LogHelper.log_action("delete gst_release", f"User {current_user.id} delete gst_release'{ gst_release_id}'") - connection.commit() - # Check if any rows were deleted - if cursor.rowcount == 0: - return HtmlHelper.json_response(ResponseHandler.fetch_failure("GST Release"), 404) - return redirect(url_for('add_gst_release')) # Redirect to the add_gst_release page - except mysql.connector.Error as e: - print(f"Error deleting GST Release: {e}") - return HtmlHelper.json_response(ResponseHandler.delete_failure("GST Release"), 500) - finally: - cursor.close() - connection.close() - - -# --- end GST Release controller ----- - -# ------------------------- Subcontractor controller ------------------------------------------ -@app.route('/subcontractor', methods=['GET', 'POST']) -@login_required -def subcontract(): - connection = config.get_db_connection() - subcontractor = [] - - if not connection: - return HtmlHelper.json_response(ResponseHandler.fetch_failure("Subcontractor"), 500) - - try: - cursor = connection.cursor() - - if request.method == 'GET': - try: - # cursor.execute('SELECT * FROM subcontractors;') - # subcontractor = cursor.fetchall() # Fetch the current subcontractor list - # connection.commit() - cursor.callproc('GetAllSubcontractors') - for result in cursor.stored_results(): - subcontractor = result.fetchall() - - except Error as e: - print(f"Error fetching data: {e}") - return HtmlHelper.json_response(ResponseHandler.fetch_failure("Subcontractor"), 500) - - if request.method == 'POST': - contractor_data = { - 'Contractor_Name': request.form['Contractor_Name'], - 'Address': request.form['Address'], - 'Mobile_No': request.form['Mobile_No'], - 'PAN_No': request.form['PAN_No'], - 'Email': request.form['Email'], - 'Gender': request.form['Gender'], - 'GST_Registration_Type': request.form['GST_Registration_Type'], - 'GST_No': request.form['GST_No'], - 'Contractor_password': request.form['Contractor_password'], - } - - try: - cursor.callproc('SaveContractor', ( - contractor_data['Contractor_Name'], - contractor_data['Address'], - contractor_data['Mobile_No'], - contractor_data['PAN_No'], - contractor_data['Email'], - contractor_data['Gender'], - contractor_data['GST_Registration_Type'], - contractor_data['GST_No'], - contractor_data['Contractor_password'] - )) - connection.commit() - - # Re-fetch subcontractors after inserting the new one - # cursor.execute('SELECT * FROM subcontractors') - # subcontractor = cursor.fetchall() - cursor.callproc('GetAllSubcontractors') - for result in cursor.stored_results(): - subcontractor = result.fetchall() - - - except Error as e: - print(f"Error inserting data: {e}") - return HtmlHelper.json_response(ResponseHandler.add_failure("Subcontractor"), 500) - - except Error as e: - print(f"Error handling subcontractor data: {e}") - return HtmlHelper.json_response(ResponseHandler.fetch_failure("Subcontractor"), 500) - - finally: - cursor.close() - connection.close() - - return render_template('add_subcontractor.html', subcontractor=subcontractor) - - -# update subcontractor by id -@app.route('/edit_subcontractor/', methods=['GET', 'POST']) -@login_required -def edit_subcontractor(id): - connection = config.get_db_connection() - - if not connection: - return HtmlHelper.json_response(ResponseHandler.fetch_failure("Subcontractor"), 500) - - try: - cursor = connection.cursor() - subcontractor = None - - # Fetch existing subcontractor data by ID - # cursor.execute('SELECT * FROM subcontractors WHERE Contractor_Id = %s', (id,)) - # subcontractor = cursor.fetchone() - - cursor.callproc("GetSubcontractorById", (id,)) - for contractors in cursor.stored_results(): - subcontractor = contractors.fetchone() - - if not subcontractor: - return HtmlHelper.json_response(ResponseHandler.fetch_failure("Subcontractor"), 404) - - if request.method == 'POST': - updated_data = { - 'Contractor_Name': request.form['Contractor_Name'], - 'Address': request.form['Address'], - 'Mobile_No': request.form['Mobile_No'], - 'PAN_No': request.form['PAN_No'], - 'Email': request.form['Email'], - 'Gender': request.form['Gender'], - 'GST_Registration_Type': request.form['GST_Registration_Type'], - 'GST_No': request.form['GST_No'], - 'Contractor_password': request.form['Contractor_password'], - 'id': id - } - LogHelper.log_action("Edit Subcontractor", f"User {current_user.id}Edit Subcontractor'{ id}'") - try: - # cursor.execute("""UPDATE subcontractors SET - # Contractor_Name=%(Contractor_Name)s, - # Address=%(Address)s, - # Mobile_No=%(Mobile_No)s, - # PAN_No=%(PAN_No)s, - # Email=%(Email)s, - # Gender=%(Gender)s, - # GST_Registration_Type=%(GST_Registration_Type)s, - # GST_No=%(GST_No)s, - # Contractor_password=%(Contractor_password)s - # WHERE Contractor_Id=%(id)s""", updated_data) - - cursor.callproc("UpdateSubcontractor", ( - id, - updated_data['Contractor_Name'], - updated_data['Address'], - updated_data['Mobile_No'], - updated_data['PAN_No'], - updated_data['Email'], - updated_data['Gender'], - updated_data['GST_Registration_Type'], - updated_data['GST_No'], - updated_data['Contractor_password'] - )) - connection.commit() - return redirect(url_for('subcontract')) - - except Error as e: - print(f"Error updating subcontractor: {e}") - return HtmlHelper.json_response(ResponseHandler.update_failure("Subcontractor"), 500) - - except Error as e: - print(f"Error fetching subcontractor data: {e}") - return HtmlHelper.json_response(ResponseHandler.fetch_failure("Subcontractor"), 500) - - finally: - cursor.close() - connection.close() - - return render_template('edit_subcontractor.html', subcontractor=subcontractor) - - -# delete Sub-Contractor methods by id .. -# @app.route('/deleteSubContractor/', methods=['GET', 'POST']) -# def deleteSubContractor(id): -# connection = config.get_db_connection() - -# if not connection: -# return HtmlHelper.json_response(ResponseHandler.fetch_failure("Subcontractor"), 500) - -# try: -# cursor = connection.cursor() - -# # cursor.execute("DELETE FROM subcontractors WHERE Contractor_Id = %s", (id,)) -# cursor.callproc("DeleteSubcontractor", (id,)) -# connection.commit() - -# # Check if any row was deleted (subcontractor found) -# if cursor.rowcount == 0: -# return HtmlHelper.json_response(ResponseHandler.fetch_failure("Subcontractor"), 404) - -# except Error as e: -# print(f"Error deleting subcontractor: {e}") -# return HtmlHelper.json_response(ResponseHandler.delete_failure("Subcontractor"), 500) - -# finally: -# cursor.close() -# connection.close() - -# return redirect(url_for('subcontract')) -@app.route('/deleteSubContractor/', methods=['GET', 'POST']) -@login_required -def deleteSubContractor(id): - connection = config.get_db_connection() - - if not connection: - return HtmlHelper.json_response(ResponseHandler.fetch_failure("Subcontractor"), 500) - - try: - cursor = connection.cursor() - - # Optional: check if subcontractor exists before attempting delete - cursor.execute("SELECT 1 FROM subcontractors WHERE Contractor_Id = %s", (id,)) - if cursor.fetchone() is None: - return HtmlHelper.json_response(ResponseHandler.fetch_failure("Subcontractor not found"), 404) - - # Call stored procedure to delete subcontractor and related records - cursor.callproc("DeleteSubcontractor", (id,)) - connection.commit() - - # Retrieve result from procedure (SELECT ROW_COUNT()) - affected_rows = 0 - for result in cursor.stored_results(): - row = result.fetchone() - affected_rows = row[0] if row else 0 - LogHelper.log_action("Delete Subcontractor", f"User {current_user.id}Delete Subcontractor'{ id}'") - if affected_rows == 0: - return HtmlHelper.json_response(ResponseHandler.fetch_failure("Subcontractor not deleted"), 404) - - except Error as e: - print(f"Error deleting subcontractor: {e}") - return HtmlHelper.json_response(ResponseHandler.delete_failure("Subcontractor"), 500) - - finally: - cursor.close() - connection.close() - - return redirect(url_for('subcontract')) # redirect to subcontractor list page - - -# ------------------------------- Show Report Subcontractor --------------------- - -UPLOAD_FOLDER = 'uploads' -app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER - -if not os.path.exists(UPLOAD_FOLDER): - os.makedirs(UPLOAD_FOLDER) - - -# Upload Excel file html page -@app.route('/upload_excel_file', methods=['GET', 'POST']) -def upload(): - if request.method == 'POST': - file = request.files['file'] - if file and file.filename.endswith('.xlsx'): - filepath = os.path.join(app.config['UPLOAD_FOLDER'], file.filename) - file.save(filepath) - LogHelper.log_action("Upload Excel File", f"User {current_user.id}Upload Excel File'{file}'") - return redirect(url_for('show_table', filename=file.filename)) - return render_template('uploadExcelFile.html') - - -# Show excel data in tables6 -# @app.route('/show_table/') -# def show_table(filename): -# global data -# data = [] -# -# filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) -# wb = openpyxl.load_workbook(filepath, data_only=True) -# sheet = wb.active -# -# # Extract key file information from the first 4 rows -# file_info = { -# "Subcontractor": sheet.cell(row=1, column=2).value, -# "State": sheet.cell(row=2, column=2).value, -# "District": sheet.cell(row=3, column=2).value, -# "Block": sheet.cell(row=4, column=2).value, -# } -# -# errors = [] -# subcontractor_data = None -# state_data = None -# district_data = None -# block_data = None -# -# # Database connection -# connection = config.get_db_connection() -# if connection: -# try: -# cursor = connection.cursor(dictionary=True) -# -# # Validate State -# # cursor.execute("SELECT State_ID, State_Name FROM states WHERE State_Name = %s", (file_info['State'],)) -# # state_data = cursor.fetchone() -# cursor.callproc('GetStateByName', [file_info['State']]) -# for result in cursor.stored_results(): -# state_data = result.fetchone() -# -# if not state_data: -# errors.append(f"State '{file_info['State']}' is not valid. Please add it.") -# -# # Validate District -# if state_data: -# # cursor.execute( -# # "SELECT District_ID, District_Name FROM districts WHERE District_Name = %s AND State_ID = %s", -# # (file_info['District'], state_data['State_ID']) -# # ) -# # district_data = cursor.fetchone() -# cursor.callproc('GetDistrictByNameAndStates', [file_info['District'], state_data['State_ID']]) -# for result in cursor.stored_results(): -# district_data = result.fetchone() -# -# if not district_data: -# errors.append( -# f"District '{file_info['District']}' is not valid under state '{file_info['State']}'.") -# -# # Validate Block -# if district_data: -# # cursor.execute( -# # "SELECT Block_Id, Block_Name FROM blocks WHERE Block_Name = %s AND District_ID = %s", -# # (file_info['Block'], district_data['District_ID']) -# # ) -# # block_data = cursor.fetchone() -# cursor.callproc('GetBlockByNameAndDistricts', [file_info['Block'], district_data['District_ID']]) -# for result in cursor.stored_results(): -# block_data = result.fetchone() -# -# if not block_data: -# errors.append( -# f"Block '{file_info['Block']}' is not valid under district '{file_info['District']}'.") -# -# # old code -# # # Validate Subcontractor -# # cursor.execute("SELECT Contractor_Id, Contractor_Name FROM SubContractors WHERE Contractor_Name = %s", -# # (file_info['Subcontractor'],)) -# # subcontractor_data = cursor.fetchone() -# cursor.callproc('GetSubcontractorByName', [file_info['Subcontractor']]) -# for result in cursor.stored_results(): -# subcontractor_data = result.fetchone() -# -# if not subcontractor_data: -# # cursor.execute("INSERT INTO subcontractors (Contractor_Name) VALUES (%s)", -# # (file_info['Subcontractor'],)) -# # connection.commit() -# cursor.callproc('InsertSubcontractor', [file_info['Subcontractor']]) -# connection.commit() -# -# # cursor.execute("SELECT Contractor_Id, Contractor_Name FROM SubContractors WHERE Contractor_Name = %s", -# # (file_info['Subcontractor'],)) -# # subcontractor_data = cursor.fetchone() -# cursor.callproc('GetSubcontractorByName', [file_info['Subcontractor']]) -# for result in cursor.stored_results(): -# subcontractor_data = result.fetchone() -# -# # new code -# # cursor.callproc('ValidateAndInsertSubcontractor', (file_info['Subcontractor'], 0, '')) -# # -# # for con in cursor.stored_results(): -# # subcontractor_data = con.fetchone() -# # print("subcon:",subcontractor_data) -# # -# # print("subcontractor_data",subcontractor_data) -# -# # Get hold types data from database (for faster lookup) -# # cursor.execute("SELECT hold_type_id, hold_type FROM hold_types") -# # hold_types_data = cursor.fetchall() -# -# cursor.callproc("GetAllHoldTypes") -# for ht in cursor.stored_results(): -# hold_types_data = ht.fetchall() -# -# hold_types_lookup = {row['hold_type'].lower(): row['hold_type_id'] for row in hold_types_data if -# row['hold_type']} -# -# cursor.close() -# except mysql.connector.Error as e: -# print(f"Database error: {e}") -# return "Database operation failed", 500 -# finally: -# connection.close() -# -# # Extract dynamic variable names from row 5 and detect "hold" columns -# variables = {} -# hold_columns = [] -# hold_counter = 0 -# -# for j in range(1, sheet.max_column + 1): -# col_value = sheet.cell(row=5, column=j).value -# if col_value: -# variables[col_value] = j # Store column name with its position -# -# # Check if the column header contains the word 'hold' -# if 'hold' in str(col_value).lower(): -# hold_counter += 1 -# # Lookup hold type id from database -# hold_type_key = str(col_value).lower().strip() -# hold_type_id = hold_types_lookup.get(hold_type_key, None) -# hold_columns.append({ -# 'column_name': col_value, -# 'column_number': j, -# 'hold_type_id': hold_type_id -# }) -# -# # Extract data dynamically based on row numbers -# for i in range(6, sheet.max_row + 1): -# row_data = {} -# if sheet.cell(row=i, column=1).value: -# row_data["Row Number"] = i # Store row number -# for var_name, col_num in variables.items(): -# row_data[var_name] = sheet.cell(row=i, column=col_num).value -# # Check if at least 4 non-empty cells exist in the row -# if sum(1 for value in row_data.values() if value) >= 4: -# data.append(row_data) -# -# # For debugging or console output, you can print the hold columns info -# for hold in hold_columns: -# if hold['hold_type_id']: -# print( -# f" if Column: {hold['column_name']}, Column Number: {hold['column_number']}, Hold Type ID: {hold['hold_type_id']}") -# else: -# errors.append( -# f"Hold Type not added ! Column name '{hold['column_name']}'.") -# print( -# f" else Column: {hold['column_name']}, Column Number: {hold['column_number']}, Hold Type ID: {hold['hold_type_id']}") -# -# return render_template( -# 'show_excel_file.html', -# file_info=file_info, -# variables=variables, -# data=data, -# subcontractor_data=subcontractor_data, -# state_data=state_data, -# district_data=district_data, -# block_data=block_data, -# errors=errors, -# hold_columns=hold_columns, -# hold_counter=hold_counter -# ) - -@app.route('/show_table/') -def show_table(filename): - global data - data = [] - - filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) - wb = openpyxl.load_workbook(filepath, data_only=True) - sheet = wb.active - - file_info = { - "Subcontractor": sheet.cell(row=1, column=2).value, - "State": sheet.cell(row=2, column=2).value, - "District": sheet.cell(row=3, column=2).value, - "Block": sheet.cell(row=4, column=2).value, - } - - errors = [] - subcontractor_data = None - state_data = None - district_data = None - block_data = None - - connection = config.get_db_connection() - if connection: - try: - cursor = connection.cursor(dictionary=True) - - print(f"Calling GetStateByName with: {file_info['State']}") - cursor.callproc('GetStateByName', [file_info['State']]) - for result in cursor.stored_results(): - state_data = result.fetchone() - - if not state_data: - errors.append(f"State '{file_info['State']}' is not valid. Please add it.") - - if state_data: - print(f"Calling GetDistrictByNameAndStates with: {file_info['District']}, {state_data['State_ID']}") - cursor.callproc('GetDistrictByNameAndStates', [file_info['District'], state_data['State_ID']]) - for result in cursor.stored_results(): - district_data = result.fetchone() - - if not district_data: - errors.append(f"District '{file_info['District']}' is not valid under state '{file_info['State']}'.") - - if district_data: - print(f"Calling GetBlockByNameAndDistricts with: {file_info['Block']}, {district_data['District_ID']}") - cursor.callproc('GetBlockByNameAndDistricts', [file_info['Block'], district_data['District_ID']]) - for result in cursor.stored_results(): - block_data = result.fetchone() - - if not block_data: - errors.append(f"Block '{file_info['Block']}' is not valid under district '{file_info['District']}'.") - - print(f"Calling GetSubcontractorByName with: {file_info['Subcontractor']}") - cursor.callproc('GetSubcontractorByName', [file_info['Subcontractor']]) - for result in cursor.stored_results(): - subcontractor_data = result.fetchone() - - if not subcontractor_data: - print(f"Inserting subcontractor: {file_info['Subcontractor']}") - cursor.callproc('InsertSubcontractor', [file_info['Subcontractor']]) - connection.commit() - print(f"Calling GetSubcontractorByName again with: {file_info['Subcontractor']}") - cursor.callproc('GetSubcontractorByName', [file_info['Subcontractor']]) - for result in cursor.stored_results(): - subcontractor_data = result.fetchone() - - print("Calling GetAllHoldTypes") - cursor.callproc("GetAllHoldTypes") - hold_types_data = [] - for ht in cursor.stored_results(): - hold_types_data = ht.fetchall() - - hold_types_lookup = {row['hold_type'].lower(): row['hold_type_id'] for row in hold_types_data if row['hold_type']} - - cursor.close() - except mysql.connector.Error as e: - print(f"Database error: {e}") - return f"Database operation failed: {e}", 500 - finally: - connection.close() - - variables = {} - hold_columns = [] - hold_counter = 0 - - for j in range(1, sheet.max_column + 1): - col_value = sheet.cell(row=5, column=j).value - if col_value: - variables[col_value] = j - if 'hold' in str(col_value).lower(): - hold_counter += 1 - hold_type_key = str(col_value).lower().strip() - hold_type_id = hold_types_lookup.get(hold_type_key, None) - hold_columns.append({ - 'column_name': col_value, - 'column_number': j, - 'hold_type_id': hold_type_id - }) - - for i in range(6, sheet.max_row + 1): - row_data = {} - if sheet.cell(row=i, column=1).value: - row_data["Row Number"] = i - for var_name, col_num in variables.items(): - row_data[var_name] = sheet.cell(row=i, column=col_num).value - if sum(1 for value in row_data.values() if value) >= 4: - data.append(row_data) - - for hold in hold_columns: - if hold['hold_type_id']: - print(f" if Column: {hold['column_name']}, Column Number: {hold['column_number']}, Hold Type ID: {hold['hold_type_id']}") - else: - errors.append(f"Hold Type not added ! Column name '{hold['column_name']}'.") - print(f" else Column: {hold['column_name']}, Column Number: {hold['column_number']}, Hold Type ID: {hold['hold_type_id']}") - - return render_template( - 'show_excel_file.html', - file_info=file_info, - variables=variables, - data=data, - subcontractor_data=subcontractor_data, - state_data=state_data, - district_data=district_data, - block_data=block_data, - errors=errors, - hold_columns=hold_columns, - hold_counter=hold_counter - ) - -# Show excel data in tables6 -# @app.route('/show_table/') -# def show_table(filename): -# global data -# data = [] -# -# filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) -# wb = openpyxl.load_workbook(filepath, data_only=True) -# sheet = wb.active -# -# # Extract key file information from the first 4 rows -# file_info = { -# "Subcontractor": sheet.cell(row=1, column=2).value, -# "State": sheet.cell(row=2, column=2).value, -# "District": sheet.cell(row=3, column=2).value, -# "Block": sheet.cell(row=4, column=2).value, -# } -# -# errors = [] -# subcontractor_data = None -# state_data = None -# district_data = None -# block_data = None -# -# # Database connection -# connection = config.get_db_connection() -# if connection: -# try: -# cursor = connection.cursor(dictionary=True) -# -# # Validate State -# cursor.execute("SELECT State_ID, State_Name FROM states WHERE State_Name = %s", (file_info['State'],)) -# state_data = cursor.fetchone() -# if not state_data: -# errors.append(f"State '{file_info['State']}' is not valid. Please add it.") -# -# # Validate District -# if state_data: -# cursor.execute( -# "SELECT District_ID, District_Name FROM districts WHERE District_Name = %s AND State_ID = %s", -# (file_info['District'], state_data['State_ID']) -# ) -# district_data = cursor.fetchone() -# if not district_data: -# errors.append( -# f"District '{file_info['District']}' is not valid under state '{file_info['State']}'.") -# -# # Validate Block -# if district_data: -# cursor.execute( -# "SELECT Block_Id, Block_Name FROM blocks WHERE Block_Name = %s AND District_ID = %s", -# (file_info['Block'], district_data['District_ID']) -# ) -# block_data = cursor.fetchone() -# if not block_data: -# errors.append( -# f"Block '{file_info['Block']}' is not valid under district '{file_info['District']}'.") -# -# -# # old code -# # # Validate Subcontractor -# cursor.execute("SELECT Contractor_Id, Contractor_Name FROM subcontractors WHERE Contractor_Name = %s", -# (file_info['Subcontractor'],)) -# subcontractor_data = cursor.fetchone() -# -# if not subcontractor_data: -# cursor.execute("INSERT INTO subcontractors (Contractor_Name) VALUES (%s)", -# (file_info['Subcontractor'],)) -# connection.commit() -# cursor.execute("SELECT Contractor_Id, Contractor_Name FROM subcontractors WHERE Contractor_Name = %s", -# (file_info['Subcontractor'],)) -# subcontractor_data = cursor.fetchone() -# -# # new code -# # cursor.callproc('ValidateAndInsertSubcontractor', (file_info['Subcontractor'], 0, '')) -# # -# # for con in cursor.stored_results(): -# # subcontractor_data = con.fetchone() -# # print("subcon:",subcontractor_data) -# # -# # print("subcontractor_data",subcontractor_data) -# -# # Get hold types data from database (for faster lookup) -# # cursor.execute("SELECT hold_type_id, hold_type FROM hold_types") -# # hold_types_data = cursor.fetchall() -# -# cursor.callproc("GetAllHoldTypes") -# for ht in cursor.stored_results(): -# hold_types_data = ht.fetchall() -# -# -# hold_types_lookup = {row['hold_type'].lower(): row['hold_type_id'] for row in hold_types_data if row['hold_type']} -# -# -# cursor.close() -# except mysql.connector.Error as e: -# print(f"Database error: {e}") -# -# # return "Database operation failed", 500 -# return f"{e}",500 -# finally: -# connection.close() -# -# # Extract dynamic variable names from row 5 and detect "hold" columns -# variables = {} -# hold_columns = [] -# hold_counter = 0 -# -# for j in range(1, sheet.max_column + 1): -# col_value = sheet.cell(row=5, column=j).value -# if col_value: -# variables[col_value] = j # Store column name with its position -# -# # Check if the column header contains the word 'hold' -# if 'hold' in str(col_value).lower(): -# hold_counter += 1 -# # Lookup hold type id from database -# hold_type_key = str(col_value).lower().strip() -# hold_type_id = hold_types_lookup.get(hold_type_key, None) -# hold_columns.append({ -# 'column_name': col_value, -# 'column_number': j, -# 'hold_type_id': hold_type_id -# }) -# -# # Extract data dynamically based on row numbers -# for i in range(6, sheet.max_row + 1): -# row_data = {} -# if sheet.cell(row=i, column=1).value: -# row_data["Row Number"] = i # Store row number -# for var_name, col_num in variables.items(): -# row_data[var_name] = sheet.cell(row=i, column=col_num).value -# # Check if at least 4 non-empty cells exist in the row -# if sum(1 for value in row_data.values() if value) >= 4: -# data.append(row_data) -# -# # For debugging or console output, you can print the hold columns info -# for hold in hold_columns: -# if hold['hold_type_id']: -# print( -# f" if Column: {hold['column_name']}, Column Number: {hold['column_number']}, Hold Type ID: {hold['hold_type_id']}") -# else: -# errors.append( -# f"Hold Type not added ! Column name '{hold['column_name']}'.") -# print( -# f" else Column: {hold['column_name']}, Column Number: {hold['column_number']}, Hold Type ID: {hold['hold_type_id']}") -# -# return render_template( -# 'show_excel_file.html', -# file_info=file_info, -# variables=variables, -# data=data, -# subcontractor_data=subcontractor_data, -# state_data=state_data, -# district_data=district_data, -# block_data=block_data, -# errors=errors, -# hold_columns=hold_columns, -# hold_counter=hold_counter -# ) - -# save Excel data -@app.route('/save_data', methods=['POST']) -def save_data(): - # Extract form data - subcontractor_id = request.form.get("subcontractor_data") - state_id = request.form.get("state_data") - district_id = request.form.get("district_data") - block_id = request.form.get("block_data") - - variables = request.form.getlist('variables[]') - hold_columns = request.form.get("hold_columns") - hold_counter = request.form.get("hold_counter") - - # print("Info: ", subcontractor_id, state_id, district_id, block_id) - - if not data: - return jsonify({"error": "No data provided to save"}), 400 - - if data: - # print("Total number of entries in data:", len(data)) - - connection = config.get_db_connection() - cursor = connection.cursor() - - try: - for entry in data: - save_data = { - "PMC_No": entry.get("PMC_No"), - "Invoice_Details": entry.get("Invoice_Details", ''), - "Work_Type": 'none', - "Invoice_Date": entry.get("Invoice_Date").strftime('%Y-%m-%d') if entry.get( - "Invoice_Date") else None, - "Invoice_No": entry.get("Invoice_No", ''), - "Basic_Amount": entry.get("Basic_Amount", 0.00), - "Debit_Amount": entry.get("Debit_Amount", 0.00), - "After_Debit_Amount": entry.get("After_Debit_Amount", 0.00), - "Amount": entry.get("Amount", 0.00), - "GST_Amount": entry.get("GST_Amount", 0.00), - "TDS_Amount": entry.get("TDS_Amount", 0.00), - "SD_Amount": entry.get("SD_Amount", 0.00), - "On_Commission": entry.get("On_Commission", 0.00), - "Hydro_Testing": entry.get("Hydro_Testing", 0.00), - "Hold_Amount": 0, - "GST_SD_Amount": entry.get("GST_SD_Amount", 0.00), - "Final_Amount": entry.get("Final_Amount", 0.00), - "Payment_Amount": entry.get("Payment_Amount", 0.00), - "Total_Amount": entry.get("Total_Amount", 0.00), - "TDS_Payment_Amount": entry.get("TDS_Payment_Amount", 0.00), - "UTR": entry.get("UTR", ''), - } - - village_name, work_type = None, None - village_id = 0 - LogHelper.log_action("Data saved", f"User {current_user.id} Data saved'{ village_name}'") - PMC_No = save_data.get('PMC_No') - Invoice_Details = save_data.get('Invoice_Details') - Invoice_Date = save_data.get('Invoice_Date') - Invoice_No = save_data.get('Invoice_No') - Basic_Amount = save_data.get('Basic_Amount') - Debit_Amount = save_data.get('Debit_Amount') - After_Debit_Amount = save_data.get('After_Debit_Amount') - Amount = save_data.get('Amount') - GST_Amount = save_data.get('GST_Amount') - TDS_Amount = save_data.get('TDS_Amount') - SD_Amount = save_data.get('SD_Amount') - On_Commission = save_data.get('On_Commission') - Hydro_Testing = save_data.get('Hydro_Testing') - GST_SD_Amount = save_data.get('GST_SD_Amount') - Final_Amount = save_data.get('Final_Amount') - - Payment_Amount = save_data.get('Payment_Amount') - Total_Amount = save_data.get('Total_Amount') - TDS_Payment_Amount = save_data.get('TDS_Payment_Amount') - UTR = save_data.get('UTR') - - if Invoice_Details: - words = Invoice_Details.lower().split() - if 'village' in words: - village_pos = words.index('village') - village_name = " ".join(words[:village_pos]) - if 'work' in words: - work_pos = words.index('work') - if village_name: - work_type = " ".join(words[village_pos + 1:work_pos + 1]) - else: - work_type = " ".join(words[:work_pos + 1]) - - if Invoice_Details and 'village' in Invoice_Details.lower() and 'work' in Invoice_Details.lower(): - print("village_name ::", village_name, "|| work_type ::", work_type) - if block_id and village_name: - village_id = None - # cursor.execute("SELECT Village_Id FROM villages WHERE Block_Id = %s AND Village_Name = %s",(block_id, village_name)) - # result = cursor.fetchone() - cursor.callproc("GetVillageId", (block_id, village_name)) - for result in cursor.stored_results(): - result = result.fetchone() - village_id = result[0] if result else None - - if not village_id: - # cursor.execute("INSERT INTO villages (Village_Name, Block_Id) VALUES (%s, %s)", (village_name, block_id)) - - cursor.callproc("SaveVillage", (village_name, block_id)) - # cursor.execute("SELECT Village_Id FROM villages WHERE Block_Id = %s AND Village_Name = %s",(block_id, village_name)) - # result = cursor.fetchone() - cursor.callproc("GetVillageId", (block_id, village_name)) - for result in cursor.stored_results(): - result = result.fetchone() - village_id = result[0] if result else None - - print("village_id :", village_id) - print("block_id :", block_id) - print("invoice :", PMC_No, village_id, work_type, Invoice_Details, Invoice_Date, Invoice_No, - Basic_Amount, Debit_Amount, After_Debit_Amount, Amount, GST_Amount, TDS_Amount, - SD_Amount, On_Commission, Hydro_Testing, GST_SD_Amount, Final_Amount) - # - # cursor.execute("SET @p_invoice_id = 0") - # cursor.callproc("SaveInvoice", ( - # PMC_No, village_id, work_type, Invoice_Details, Invoice_Date, Invoice_No, - # Basic_Amount, Debit_Amount, After_Debit_Amount, Amount, GST_Amount, TDS_Amount, - # SD_Amount, On_Commission, Hydro_Testing, GST_SD_Amount, Final_Amount, - # subcontractor_id, "@p_invoice_id" - # )) - # cursor.execute("SELECT @p_invoice_id") - # invoice_id = cursor.fetchone()[0] - - args = ( - PMC_No, village_id, work_type, Invoice_Details, Invoice_Date, Invoice_No, - Basic_Amount, Debit_Amount, After_Debit_Amount, Amount, GST_Amount, TDS_Amount, - SD_Amount, On_Commission, Hydro_Testing, GST_SD_Amount, Final_Amount, - subcontractor_id, 0 - ) - # for result in cursor.stored_results(): - # invoice_id = result.fetchone()['invoice_id'] - print("All invoice Details ",args) - results = cursor.callproc('SaveInvoice', args) - # cursor.callproc("SaveInvoice",args) - # for re in cursor.stored_results(): - invoice_id = results[-1] - - print("invoice id from the excel ", invoice_id) - - if isinstance(hold_columns, str): - hold_columns = ast.literal_eval(hold_columns) - - # Check if hold_columns is actually a list of dictionaries - if isinstance(hold_columns, list) and all(isinstance(hold, dict) for hold in hold_columns): - for hold in hold_columns: - print(f"Processing hold: {hold}") - hold_column_name = hold.get('column_name') # Get column name - hold_type_id = hold.get('hold_type_id') # Get hold_type_id - - if hold_column_name: - hold_amount = entry.get( - hold_column_name) # Get the value for that specific hold column - if hold_amount is not None: - print(f"Processing hold type: {hold_column_name}, Hold Amount: {hold_amount}") - - # Insert into the invoice_subcontractor_hold_join table - hold_join_data = { - "Contractor_Id": subcontractor_id, - "Invoice_Id": invoice_id, - "hold_type_id": hold_type_id, - "hold_amount": hold_amount - } - - # insert_hold_query = """INSERT INTO invoice_subcontractor_hold_join (Contractor_Id, Invoice_Id, hold_type_id, hold_amount) - # VALUES (%(Contractor_Id)s, %(Invoice_Id)s, %(hold_type_id)s, %(hold_amount)s); - # """ - # cursor.execute(insert_hold_query, hold_join_data) - # print(f"Inserted hold join data: {hold_join_data}") - cursor.callproc('InsertHoldJoinData', [ - hold_join_data['Contractor_Id'], hold_join_data['Invoice_Id'], - hold_join_data['hold_type_id'], hold_join_data['hold_amount'] - ]) - connection.commit() - print(f"Inserted hold join data: {hold_join_data}") - - - else: - print(f"Invalid hold entry: {hold}") - else: - print("Hold columns data is not a valid list of dictionaries.") -#---------------------------------------------Credit Note--------------------------------------------------------------------------- - elif any(keyword in Invoice_Details.lower() for keyword in ['credit note','logging report']): - print("Credit note found:", PMC_No, Invoice_No, Basic_Amount, Debit_Amount, Final_Amount, - After_Debit_Amount, GST_Amount, Amount, Final_Amount, Payment_Amount, Total_Amount, UTR, Invoice_No) - cursor.execute( - """INSERT INTO credit_note (PMC_No, Invoice_Details, Basic_Amount, Debit_Amount, After_Debit_Amount, GST_Amount, Amount, Final_Amount, Payment_Amount, Total_Amount, UTR, Contractor_Id, invoice_no) VALUES (%s,%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,%s)""", - ( PMC_No, Invoice_Details, Basic_Amount, Debit_Amount, After_Debit_Amount, GST_Amount, Amount, Final_Amount, Payment_Amount, Total_Amount, UTR, subcontractor_id, Invoice_No)) -#-----------------------------------------------Hold Amount---------------------------------------------------------------------- - - # Step 1: Normalize Invoice_Details: lowercase, trim, remove extra spaces - normalized_details = re.sub(r'\s+', ' ', Invoice_Details.strip()).lower() - - # Step 2: Define lowercase keywords - keywords = [ - 'excess hold', - 'ht', - 'hold release amount', - 'dpr excess hold amount', - 'excess hold amount', - 'Multi to Single layer bill', - 'hold amount', - 'logging report' - ] - - # Step 3: Matching condition - if any(kw in normalized_details for kw in keywords): - print("✅ Match found. Inserting hold release for:", Invoice_Details) - cursor.execute(""" - INSERT INTO hold_release (PMC_No, Invoice_No, Invoice_Details, Basic_Amount, - Total_Amount, UTR, Contractor_Id) - VALUES (%s, %s, %s, %s, %s, %s, %s) - """, ( - PMC_No, Invoice_No, Invoice_Details, Basic_Amount, Final_Amount, UTR, - subcontractor_id - )) - connection.commit() # ✅ Ensure changes are saved to DB - print("✅ Hold release inserted for:", PMC_No, Invoice_Details) - - #------------------------------------------------------------------------------------------------------------------ - elif Invoice_Details and any( - keyword in Invoice_Details.lower() for keyword in ['gst', 'release', 'note']): - print("Gst rels :", PMC_No, Invoice_No, Basic_Amount, Final_Amount,Total_Amount,UTR, subcontractor_id) - #cursor.callproc("SaveGSTRelease", (PMC_No, Invoice_No, Basic_Amount, Final_Amount,Total_Amount,UTR)) - cursor.execute( - """INSERT INTO gst_release (PMC_No, Invoice_No, Basic_Amount, Final_Amount,Total_Amount,UTR, Contractor_Id) VALUES (%s,%s, %s, %s, %s, %s, %s)""", - (PMC_No, Invoice_No, Basic_Amount, Final_Amount, Total_Amount, UTR, subcontractor_id)) - - # insert_payment = """INSERT INTO payment (PMC_No, Invoice_No, Payment_Amount, TDS_Payment_Amount, Total_Amount, UTR) VALUES (%s, %s, %s, %s, %s, %s)""" - # cursor.execute(insert_payment, - # (PMC_No, Invoice_No, Payment_Amount, TDS_Payment_Amount, Total_Amount, UTR)) - - if PMC_No and Total_Amount and UTR: - print("Payment :", PMC_No, Invoice_No, Payment_Amount, TDS_Payment_Amount, Total_Amount, UTR ) - # insert_payment = """INSERT INTO payment (PMC_No, Invoice_No, Payment_Amount, TDS_Payment_Amount, Total_Amount, UTR) VALUES (%s, %s, %s, %s, %s, %s)""" - # cursor.execute(insert_payment, - # (PMC_No, Invoice_No, Payment_Amount, TDS_Payment_Amount, Total_Amount, UTR)) - - cursor.callproc("SavePayment", - (PMC_No, Invoice_No, Payment_Amount, TDS_Payment_Amount, Total_Amount, UTR )) - - connection.commit() - return jsonify({"success": "Data saved successfully!"}), 200 - # return render_template('uploadExcelFile.html') - except Exception as e: - connection.rollback() - return jsonify({"error": f"An unexpected error occurred: {e}"}), 500 - finally: - cursor.close() - connection.close() - - return render_template('index.html') - - -# ---------------------- Report -------------------------------- -# call report page -@app.route('/report') -def report_page(): - return render_template('report.html') - - -# Search list multiples input and search reports -@app.route('/search_contractor', methods=['POST']) -def search_contractor(): - connection = config.get_db_connection() - cursor = connection.cursor(dictionary=True) - - subcontractor_name = request.form.get('subcontractor_name') - pmc_no = request.form.get('pmc_no') - state = request.form.get('state') - district = request.form.get('district') - block = request.form.get('block') - village = request.form.get('village') - year_from = request.form.get('year_from') - year_to = request.form.get('year_to') - - conditions = [] - params = [] - - LogHelper.log_action("Search contractor", f"User {current_user.id} Search contractor'{ subcontractor_name}'") - if subcontractor_name: - conditions.append("LOWER(s.Contractor_Name) LIKE LOWER(%s)") - params.append(f"%{subcontractor_name}%") - if pmc_no: - conditions.append("i.PMC_No = %s") - params.append(pmc_no) - if state: - conditions.append("LOWER(st.State_Name) LIKE LOWER(%s)") - params.append(f"%{state}%") - if district: - conditions.append("LOWER(d.District_Name) LIKE LOWER(%s)") - params.append(f"%{district}%") - if block: - conditions.append("LOWER(b.Block_Name) LIKE LOWER(%s)") - params.append(f"%{block}%") - if village: - conditions.append("LOWER(v.Village_Name) LIKE LOWER(%s)") - params.append(f"%{village}%") - if year_from and year_to: - conditions.append("i.Invoice_Date BETWEEN %s AND %s") - params.append(year_from) - params.append(year_to) - - if not conditions: - return jsonify({"error": "At least one field is required for search."}), 400 - - # query = f""" - # SELECT DISTINCT s.Contractor_Id, s.Contractor_Name, i.PMC_No, st.State_Name, - # d.District_Name, b.Block_Name, v.Village_Name - # FROM subcontractors s - # INNER JOIN assign_subcontractors asg ON s.Contractor_Id = asg.Contractor_Id - # INNER JOIN villages v ON asg.Village_Id = v.Village_Id - # INNER JOIN invoice i ON i.Village_Id = asg.Village_Id AND i.PMC_No = asg.PMC_No - # LEFT JOIN blocks b ON v.Block_Id = b.Block_Id - # LEFT JOIN districts d ON b.District_id = d.District_id - # LEFT JOIN states st ON d.State_Id = st.State_Id - # WHERE {' AND '.join(conditions)} - # ORDER BY s.Contractor_Name ASC, i.PMC_No ASC - # """ - # cursor.execute(query, tuple(params)) - # data = cursor.fetchall() - cursor.callproc("search_contractor_info", [ - subcontractor_name or None, - pmc_no or None, - state or None, - district or None, - block or None, - village or None, - year_from or None, - year_to or None - ]) - - for result in cursor.stored_results(): - data = result.fetchall() - - return jsonify(data) - - - - -@app.route('/contractor_report/') -def contractor_report(contractor_id): - connection = config.get_db_connection() - cursor = connection.cursor(dictionary=True, buffered=True) - - try: - # Contractor details - cursor.execute(""" - SELECT DISTINCT s.Contractor_Name, st.State_Name, d.District_Name, b.Block_Name, - s.Mobile_No, s.GST_Registration_Type, s.GST_No, s.PAN_No, s.Email, s.Address - FROM subcontractors s - LEFT JOIN assign_subcontractors asg ON s.Contractor_Id = asg.Contractor_Id - LEFT JOIN villages v ON asg.Village_Id = v.Village_Id - LEFT JOIN blocks b ON v.Block_Id = b.Block_Id - LEFT JOIN districts d ON b.District_id = d.District_id - LEFT JOIN states st ON d.State_Id = st.State_Id - WHERE s.Contractor_Id = %s - """, (contractor_id,)) - contInfo = cursor.fetchone() - - # Hold types - cursor.execute(""" - SELECT DISTINCT ht.hold_type_id, ht.hold_type - FROM invoice_subcontractor_hold_join h - JOIN hold_types ht ON h.hold_type_id = ht.hold_type_id - JOIN invoice i ON h.Invoice_Id = i.Invoice_Id - WHERE h.Contractor_Id = %s - """, (contractor_id,)) - hold_types = cursor.fetchall() - - # Invoices - cursor.execute(""" - SELECT DISTINCT i.PMC_No, v.Village_Name, i.Work_Type, i.Invoice_Details, - i.Invoice_Date, i.Invoice_No, i.Basic_Amount, i.Debit_Amount, - i.After_Debit_Amount, i.Amount, i.GST_Amount, i.TDS_Amount, i.SD_Amount, - i.On_Commission, i.Hydro_Testing, i.GST_SD_Amount, - i.Final_Amount, h.hold_amount, ht.hold_type - FROM assign_subcontractors asg - INNER JOIN villages v ON asg.Village_Id = v.Village_Id - INNER JOIN invoice i ON i.Village_Id = v.Village_Id AND i.PMC_No = asg.PMC_No - LEFT JOIN invoice_subcontractor_hold_join h ON i.Invoice_Id = h.Invoice_Id AND h.Contractor_Id = asg.Contractor_Id - LEFT JOIN hold_types ht ON h.hold_type_id = ht.hold_type_id - WHERE asg.Contractor_Id = %s - ORDER BY i.PMC_No ASC - """, (contractor_id,)) - invoices = cursor.fetchall() - - # GST Release - cursor.execute(""" - SELECT gr.pmc_no, gr.invoice_no, gr.basic_amount, gr.final_amount - FROM gst_release gr - INNER JOIN ( - SELECT DISTINCT i.PMC_No, i.Invoice_No - FROM invoice i - JOIN assign_subcontractors a ON i.PMC_No = a.PMC_No AND i.Village_Id = a.Village_Id - WHERE a.Contractor_Id = %s - ) x ON gr.pmc_no = x.PMC_No AND gr.invoice_no = x.Invoice_No - ORDER BY gr.pmc_no ASC - """, (contractor_id,)) - gst_rel = cursor.fetchall() - #Hold - # Hold Release - cursor.execute("SELECT * FROM hold_release WHERE Contractor_Id=%s", (contractor_id,)) - hold_release = cursor.fetchall() - print(hold_release) - - #Credit Note - - cursor.execute("select * from credit_note where Contractor_Id=%s",(contractor_id,)) - credit_note=cursor.fetchall() - print(credit_note) - # Payments (include valid matches and payments with pmc_no but invoice_no is NULL) - cursor.execute(""" - SELECT p.pmc_no, p.invoice_no, p.Payment_Amount, p.TDS_Payment_Amount, p.Total_amount, p.utr - FROM payment p - WHERE - EXISTS ( - SELECT 1 - FROM invoice i - JOIN assign_subcontractors a ON i.PMC_No = a.PMC_No AND i.Village_Id = a.Village_Id - WHERE a.Contractor_Id = %s - AND i.PMC_No = p.pmc_no AND i.Invoice_No = p.invoice_no - ) - OR ( - p.invoice_no IS NULL - AND EXISTS ( - SELECT 1 - FROM assign_subcontractors a - WHERE a.Contractor_Id = %s - AND a.PMC_No = p.pmc_no - ) - ) - ORDER BY p.pmc_no ASC - """, (contractor_id, contractor_id)) - payments = cursor.fetchall() - - # Totals - total = { - "sum_invo_basic_amt": float(sum(row['Basic_Amount'] or 0 for row in invoices)), - "sum_invo_debit_amt": float(sum(row['Debit_Amount'] or 0 for row in invoices)), - "sum_invo_after_debit_amt": float(sum(row['After_Debit_Amount'] or 0 for row in invoices)), - "sum_invo_amt": float(sum(row['Amount'] or 0 for row in invoices)), - "sum_invo_gst_amt": float(sum(row['GST_Amount'] or 0 for row in invoices)), - "sum_invo_tds_amt": float(sum(row['TDS_Amount'] or 0 for row in invoices)), - "sum_invo_ds_amt": float(sum(row['SD_Amount'] or 0 for row in invoices)), - "sum_invo_on_commission": float(sum(row['On_Commission'] or 0 for row in invoices)), - "sum_invo_hydro_test": float(sum(row['Hydro_Testing'] or 0 for row in invoices)), - "sum_invo_gst_sd_amt": float(sum(row['GST_SD_Amount'] or 0 for row in invoices)), - "sum_invo_final_amt": float(sum(row['Final_Amount'] or 0 for row in invoices)), - "sum_invo_hold_amt": float(sum(row['hold_amount'] or 0 for row in invoices)), - - "sum_gst_basic_amt": float(sum(row['basic_amount'] or 0 for row in gst_rel)), - "sum_gst_final_amt": float(sum(row['final_amount'] or 0 for row in gst_rel)), - - "sum_pay_payment_amt": float(sum(row['Payment_Amount'] or 0 for row in payments)), - "sum_pay_tds_payment_amt": float(sum(row['TDS_Payment_Amount'] or 0 for row in payments)), - "sum_pay_total_amt": float(sum(row['Total_amount'] or 0 for row in payments)) - } - - current_date = datetime.now().strftime('%Y-%m-%d') - - except Exception as e: - print(f"Error fetching contractor report: {e}") - return "An error occurred while fetching contractor report", 500 - - finally: - cursor.close() - connection.close() - - return render_template('subcontractor_report.html', - contInfo=contInfo, - contractor_id=contractor_id, - invoices=invoices, - hold_types=hold_types, - gst_rel=gst_rel, - payments=payments, - credit_note=credit_note, - hold_release=hold_release, - total=total, - current_date=current_date) - - - -# # Download report by contractor id -# # Download report by contractor id - - -class FilePathData: - downloadReportFolder = "static/download" - - - -@app.route('/download_report/') -def download_report(contractor_id): - connection = config.get_db_connection() - cursor = connection.cursor(dictionary=True, buffered=True) - - output_folder = FilePathData.downloadReportFolder - os.makedirs(output_folder, exist_ok=True) - output_file = os.path.join(output_folder, f"Contractor_Report_{contractor_id}.xlsx") - - try: - # ---------------- Contractor Info ---------------- - contractor = ContractorInfo(contractor_id) - contInfo = contractor.contInfo - - if not contractor.contInfo: - return "No contractor found", 404 - - # ---------------- Hold Types ---------------- - cursor.execute(""" - SELECT DISTINCT ht.hold_type_id, ht.hold_type - FROM invoice_subcontractor_hold_join h - JOIN hold_types ht ON h.hold_type_id = ht.hold_type_id - WHERE h.Contractor_Id = %s - """, (contractor_id,)) - hold_types = cursor.fetchall() - hold_type_map = {ht['hold_type_id']: ht['hold_type'] for ht in hold_types} - - # ---------------- Invoices ---------------- - cursor.execute(""" - SELECT i.*, v.Village_Name - FROM assign_subcontractors asg - INNER JOIN invoice i ON i.PMC_No = asg.PMC_No AND i.Village_Id = asg.Village_Id - LEFT JOIN villages v ON i.Village_Id = v.Village_Id - WHERE asg.Contractor_Id = %s - ORDER BY i.PMC_No, i.Invoice_No - """, (contractor_id,)) - invoices = cursor.fetchall() - - # Remove duplicate invoices - invoice_ids_seen = set() - unique_invoices = [] - for inv in invoices: - if inv["Invoice_Id"] not in invoice_ids_seen: - invoice_ids_seen.add(inv["Invoice_Id"]) - unique_invoices.append(inv) - invoices = unique_invoices - - # ---------------- Hold Amounts ---------------- - cursor.execute(""" - SELECT h.Invoice_Id, h.hold_type_id, h.hold_amount - FROM invoice_subcontractor_hold_join h - WHERE h.Contractor_Id = %s - """, (contractor_id,)) - hold_amounts = cursor.fetchall() - hold_data = {} - for h in hold_amounts: - hold_data.setdefault(h['Invoice_Id'], {})[h['hold_type_id']] = h['hold_amount'] - - # ---------------- Payments ---------------- - cursor.execute(""" - SELECT DISTINCT p.pmc_no, p.invoice_no, p.Payment_Amount, p.TDS_Payment_Amount, p.Total_amount, p.utr - FROM payment p - INNER JOIN invoice i ON i.PMC_No = p.pmc_no AND i.invoice_no = p.invoice_no - INNER JOIN assign_subcontractors asg ON i.PMC_No = asg.PMC_No AND i.Village_Id = asg.Village_Id - WHERE asg.Contractor_Id = %s - """, (contractor_id,)) - payments = cursor.fetchall() - payments_map = {} - for pay in payments: - key = (str(pay['pmc_no']), str(pay['invoice_no'])) - payments_map.setdefault(key, []).append(pay) - - # ---------------- Extra Payments (no invoice_no) ---------------- - cursor.execute(""" - SELECT pmc_no, Payment_Amount, TDS_Payment_Amount, Total_amount, utr - FROM payment - WHERE (invoice_no IS NULL OR invoice_no = '') - AND Total_amount != 0 - AND pmc_no IS NOT NULL - """) - extra_payments_raw = cursor.fetchall() - extra_payments_map = {} - for pay in extra_payments_raw: - extra_payments_map.setdefault(str(pay['pmc_no']), []).append({ - 'Payment_Amount': pay['Payment_Amount'], - 'TDS_Payment_Amount': pay['TDS_Payment_Amount'], - 'Total_amount': pay['Total_amount'], - 'utr': pay['utr'] - }) - - # ---------------- Credit Notes ---------------- - cursor.execute(""" - SELECT PMC_No, Invoice_No, Invoice_Details, Basic_Amount, Debit_Amount, After_Debit_Amount, - GST_Amount, Amount, Final_Amount, Payment_Amount, Total_Amount, UTR - FROM credit_note - WHERE Contractor_Id = %s - """, (contractor_id,)) - credit_notes = cursor.fetchall() - credit_note_map = {} - for cn in credit_notes: - key = (str(cn['PMC_No']), str(cn['Invoice_No'])) - credit_note_map.setdefault(key, []).append(cn) - - # ---------------- GST Releases ---------------- - cursor.execute(""" - SELECT PMC_No, Invoice_No, Basic_Amount, Final_Amount, Total_Amount, UTR - FROM gst_release - WHERE Contractor_Id = %s - ORDER BY PMC_No, Invoice_No - """, (contractor_id,)) - gst_releases = cursor.fetchall() - gst_release_map = {} - for gr in gst_releases: - key = (str(gr['PMC_No']), str(gr['Invoice_No'])) - gst_release_map.setdefault(key, []).append(gr) - - # ---------------- Excel Workbook ---------------- - workbook = openpyxl.Workbook() - sheet = workbook.active - sheet.title = "Contractor Report" - - # Contractor Info - for field, value in contInfo.items(): - sheet.append([field.replace("_", " "), value]) - sheet.append([]) - - # Headers - base_headers = ["PMC No", "Village", "Work Type", "Invoice Details", "Invoice Date", "Invoice No", - "Basic Amount", "Debit", "After Debit Amount", "GST (18%)", "Amount", "TDS (1%)", - "SD (5%)", "On Commission", "Hydro Testing", "GST SD Amount"] - hold_headers = [ht['hold_type'] for ht in hold_types] - payment_headers = ["Final Amount", "Payment Amount", "TDS Payment", "Total Paid", "UTR"] - all_headers = base_headers + hold_headers + payment_headers - sheet.append(all_headers) - for cell in sheet[sheet.max_row]: - cell.font = Font(bold=True) - cell.fill = PatternFill(start_color="ADD8E6", end_color="ADD8E6", fill_type="solid") - - # ---------------- Data Rows ---------------- - processed_gst_releases = set() - appended_credit_keys = set() - previous_pmc_no = None - - for inv in invoices: - pmc_no = str(inv["PMC_No"]) - invoice_no = str(inv["invoice_no"]) - key = (pmc_no, invoice_no) - - # Yellow separator if PMC_No changes - if previous_pmc_no and pmc_no != previous_pmc_no: - sheet.append([""] * len(all_headers)) - yellow_fill = PatternFill(start_color="FFFF99", end_color="FFFF99", fill_type="solid") - for cell in sheet[sheet.max_row]: - cell.fill = yellow_fill - previous_pmc_no = pmc_no - - # Invoice row - row = [ - pmc_no, inv["Village_Name"], inv["Work_Type"], inv["Invoice_Details"], - inv["Invoice_Date"], invoice_no, inv["Basic_Amount"], inv["Debit_Amount"], - inv["After_Debit_Amount"], inv["GST_Amount"], inv["Amount"], inv["TDS_Amount"], - inv["SD_Amount"], inv["On_Commission"], inv["Hydro_Testing"], inv["GST_SD_Amount"] - ] - # Hold values - invoice_holds = hold_data.get(inv["Invoice_Id"], {}) - for ht_id in hold_type_map: - row.append(invoice_holds.get(ht_id, "")) - - # Payment values - payment = payments_map.get(key, [None])[0] - row += [ - inv["Final_Amount"], - payment["Payment_Amount"] if payment else "", - payment["TDS_Payment_Amount"] if payment else "", - payment["Total_amount"] if payment else "", - payment["utr"] if payment and payment.get("utr") else "" - ] - sheet.append(row) - - # ---------------- Extra Payments for this PMC ---------------- - if pmc_no in extra_payments_map: - for ep in extra_payments_map[pmc_no]: - extra_row = [pmc_no] + [""] * (len(base_headers) - 1) - extra_row += [""] * len(hold_headers) - extra_row += [ - "", ep["Payment_Amount"], ep["TDS_Payment_Amount"], ep["Total_amount"], ep.get("utr", "") - ] - sheet.append(extra_row) - del extra_payments_map[pmc_no] - - # GST Releases - if key in gst_release_map and key not in processed_gst_releases: - for gr in gst_release_map[key]: - gst_row = [ - pmc_no, "", "", "GST Release Note", "", gr["Invoice_No"], - gr["Basic_Amount"], "", "", "", "", "", "", "", "", "" - ] - gst_row += ["" for _ in hold_headers] - gst_row += [gr["Final_Amount"], "", "", gr["Total_Amount"], gr.get("UTR", "")] - sheet.append(gst_row) - processed_gst_releases.add(key) - - # Credit Notes - if key in credit_note_map and key not in appended_credit_keys: - for cn in credit_note_map[key]: - cn_row = [ - pmc_no, "", "", cn.get("Invoice_Details", "Credit Note"), "", cn.get("Invoice_No", ""), - cn.get("Basic_Amount", ""), cn.get("Debit_Amount", ""), cn.get("After_Debit_Amount", ""), - cn.get("GST_Amount", ""), cn.get("Amount", ""), "", "", "", "", "" - ] - cn_row += ["" for _ in hold_headers] - cn_row += [cn.get("Final_Amount", ""), "", "", cn.get("Total_Amount", ""), cn.get("UTR", "")] - sheet.append(cn_row) - appended_credit_keys.add(key) - - # ---------------- Totals ---------------- - total_basic_amount = total_tds_amount = total_sd_amount = total_on_commission = 0 - total_final_amount = total_total_amount = total_hold_amount = 0 - - start_row = 2 # skip headers - for r in sheet.iter_rows(min_row=start_row, max_row=sheet.max_row, values_only=True): - try: - total_basic_amount += float(r[6] or 0) - total_tds_amount += float(r[11] or 0) - total_sd_amount += float(r[12] or 0) - total_on_commission += float(r[13] or 0) - total_final_amount += float(r[-5] or 0) - total_total_amount += float(r[-2] or 0) - total_hold_amount += sum(float(r[i] or 0) for i in range(len(base_headers), len(base_headers) + len(hold_headers))) - except: - continue - - totals_row = [ - "Total", "", "", "", "", "", total_basic_amount, "", "", "", "", total_tds_amount, - total_sd_amount, total_on_commission, "", "" - ] - totals_row += [total_hold_amount for _ in hold_headers] - totals_row += [total_final_amount, "", "", total_total_amount, ""] - sheet.append([]) - sheet.append(totals_row) - for cell in sheet[sheet.max_row]: - cell.font = Font(bold=True) - - # ---------------- Column Width ---------------- - for col in sheet.columns: - max_length = 0 - col_letter = openpyxl.utils.get_column_letter(col[0].column) - for cell in col: - if cell.value: - max_length = max(max_length, len(str(cell.value))) - sheet.column_dimensions[col_letter].width = max_length + 2 - - workbook.save(output_file) - workbook.close() - - finally: - cursor.close() - connection.close() - - return send_from_directory(output_folder, f"Contractor_Report_{contractor_id}.xlsx", as_attachment=True) - - - - -@app.route('/pmc_report/') -def pmc_report(pmc_no): - connection = config.get_db_connection() - cursor = connection.cursor(dictionary=True, buffered=True) - - try: - # 1. Fetch PMC info using stored procedure - # cursor.execute(""" - # SELECT DISTINCT a.PMC_No, a.Village_Id, v.Village_Name, b.Block_Name, - # d.District_Name, s.State_Name, sc.Contractor_Id, sc.Contractor_Name, - # sc.Address, sc.Mobile_No, sc.PAN_No, sc.Email, sc.Gender, - # sc.GST_Registration_Type, sc.GST_No - # FROM assign_subcontractors a - # INNER JOIN villages v ON a.Village_Id = v.Village_Id - # INNER JOIN blocks b ON v.Block_Id = b.Block_Id - # INNER JOIN districts d ON b.District_id = d.District_id - # INNER JOIN states s ON d.State_Id = s.State_Id - # INNER JOIN subcontractors sc ON a.Contractor_Id = sc.Contractor_Id - # WHERE a.pmc_no = %s - # """, (pmc_no,)) - # pmc_info = cursor.fetchone() - - cursor.callproc("GetContractorInfoByPmcNo", (pmc_no,)) - pmc_info = next(cursor.stored_results()).fetchone() - - if not pmc_info: - return "No PMC found with this number", 404 - - # 2. Fetch hold types using stored procedure - # cursor.execute(""" - # SELECT DISTINCT ht.hold_type_id, ht.hold_type - # FROM invoice_subcontractor_hold_join h - # JOIN hold_types ht ON h.hold_type_id = ht.hold_type_id - # JOIN invoice i ON h.Invoice_Id = i.Invoice_Id - # JOIN assign_subcontractors a ON i.PMC_No = a.PMC_No - # WHERE a.PMC_No = %s AND a.Contractor_Id = %s - # """, (pmc_no, pmc_info["Contractor_Id"])) - # hold_types = cursor.fetchall() - cursor.callproc("Get_pmc_hold_types", (pmc_no, pmc_info["Contractor_Id"])) - hold_types = next(cursor.stored_results()).fetchall() - hold_type_ids = [ht['hold_type_id'] for ht in hold_types] - - # 3. Initialize invoice data - invoices = [] - hold_amount_total = 0 - - # 4. Build invoice query - if hold_type_ids: - placeholders = ','.join(['%s'] * len(hold_type_ids)) - query = f""" - SELECT DISTINCT i.PMC_No, v.Village_Name, i.Work_Type, i.Invoice_Details, - i.Invoice_Date, i.Invoice_No, i.Basic_Amount, i.Debit_Amount, - i.After_Debit_Amount, i.Amount, i.GST_Amount, i.TDS_Amount, i.SD_Amount, - i.On_Commission, i.Hydro_Testing, i.GST_SD_Amount, - i.Final_Amount, h.hold_amount, ht.hold_type - FROM invoice i - LEFT JOIN villages v ON i.Village_Id = v.Village_Id - LEFT JOIN assign_subcontractors a ON i.PMC_No = a.PMC_No - LEFT JOIN invoice_subcontractor_hold_join h ON i.Invoice_Id = h.Invoice_Id - LEFT JOIN hold_types ht ON h.hold_type_id = ht.hold_type_id - WHERE a.PMC_No = %s AND a.Contractor_Id = %s - AND (ht.hold_type_id IS NULL OR ht.hold_type_id IN ({placeholders})) - ORDER BY i.Invoice_Date, i.Invoice_No - """ - params = [pmc_no, pmc_info["Contractor_Id"]] + hold_type_ids - else: - query = """ - SELECT DISTINCT i.PMC_No, v.Village_Name, i.Work_Type, i.Invoice_Details, - i.Invoice_Date, i.Invoice_No, i.Basic_Amount, i.Debit_Amount, - i.After_Debit_Amount, i.Amount, i.GST_Amount, i.TDS_Amount, i.SD_Amount, - i.On_Commission, i.Hydro_Testing, i.GST_SD_Amount, i.Final_Amount - FROM invoice i - LEFT JOIN villages v ON i.Village_Id = v.Village_Id - LEFT JOIN assign_subcontractors a ON i.PMC_No = a.PMC_No - WHERE a.PMC_No = %s AND a.Contractor_Id = %s - ORDER BY i.Invoice_Date, i.Invoice_No - """ - params = [pmc_no, pmc_info["Contractor_Id"]] - - cursor.execute(query, params) - invoices = cursor.fetchall() - - if hold_type_ids: - hold_amount_total = sum(row.get('hold_amount', 0) or 0 for row in invoices) - - # 5. Totals from invoices - total_invo_final = sum(row.get('Final_Amount', 0) or 0 for row in invoices) - - # 6. GST release - cursor.execute(""" - SELECT pmc_no, invoice_no, basic_amount, final_amount - FROM gst_release - WHERE pmc_no = %s - ORDER BY invoice_no ASC - """, (pmc_no,)) - gst_rel = cursor.fetchall() - # gst_rel = cursor.fetchall() - # cursor.callproc('GetGSTReleaseByPMC', [pmc_no]) - # - # # Fetch results - # for result in cursor.stored_results(): - # gst_rel = result.fetchall() - - total_gst_basic = sum(row.get('basic_amount', 0) or 0 for row in gst_rel) - total_gst_final = sum(row.get('final_amount', 0) or 0 for row in gst_rel) - - # Hold Release Amount - cursor.execute("""select * from hold_release where pmc_no=%s""", (pmc_no,)) - hold_release = cursor.fetchall() - print("All Hold Release ", hold_release) - - # Credit Note - - cursor.execute("select * from credit_note where pmc_no=%s", (pmc_no,)) - credit_note = cursor.fetchall() - print(credit_note) - - # 7. Payments - cursor.execute(""" - SELECT pmc_no, invoice_no, Payment_Amount, TDS_Payment_Amount, Total_amount, utr - FROM payment - WHERE pmc_no = %s - ORDER BY invoice_no ASC - """, (pmc_no,)) - payments = cursor.fetchall() - # cursor.callproc('GetPaymentByPMC', [pmc_no]) - # - # for result in cursor.stored_results(): - # payments = result.fetchall() - - total_pay_amount = sum(row.get('Payment_Amount', 0) or 0 for row in payments) - total_pay_total = sum(row.get('Total_amount', 0) or 0 for row in payments) - - # 8. Final totals dictionary - totals = { - "sum_invo_basic_amt": sum(row.get('Basic_Amount', 0) or 0 for row in invoices), - "sum_invo_debit_amt": sum(row.get('Debit_Amount', 0) or 0 for row in invoices), - "sum_invo_after_debit_amt": sum(row.get('After_Debit_Amount', 0) or 0 for row in invoices), - "sum_invo_amt": sum(row.get('Amount', 0) or 0 for row in invoices), - "sum_invo_gst_amt": sum(row.get('GST_Amount', 0) or 0 for row in invoices), - "sum_invo_tds_amt": sum(row.get('TDS_Amount', 0) or 0 for row in invoices), - "sum_invo_ds_amt": sum(row.get('SD_Amount', 0) or 0 for row in invoices), - "sum_invo_on_commission": sum(row.get('On_Commission', 0) or 0 for row in invoices), - "sum_invo_hydro_test": sum(row.get('Hydro_Testing', 0) or 0 for row in invoices), - "sum_invo_gst_sd_amt": sum(row.get('GST_SD_Amount', 0) or 0 for row in invoices), - "sum_invo_final_amt": total_invo_final, - "sum_invo_hold_amt": hold_amount_total, - "sum_gst_basic_amt": total_gst_basic, - "sum_gst_final_amt": total_gst_final, - "sum_pay_payment_amt": total_pay_amount, - "sum_pay_tds_payment_amt": sum(row.get('TDS_Payment_Amount', 0) or 0 for row in payments), - "sum_pay_total_amt": total_pay_total - } - - except Exception as e: - print(f"Error fetching PMC report: {e}") - return "An error occurred while fetching PMC report", 500 - - finally: - cursor.close() - connection.close() - - return render_template( - 'pmc_report.html', - info=pmc_info, - invoices=invoices, - hold_types=hold_types, - gst_rel=gst_rel, - payments=payments, - credit_note=credit_note, - hold_release=hold_release, - total=totals - ) - - -# # Download report by PMC No -# @app.route('/download_pmc_report/') -# def download_pmc_report(pmc_no): -# connection = config.get_db_connection() -# output_folder = "static/download" -# output_file = os.path.join(output_folder, f"PMC_Report_{pmc_no}.xlsx") - -# if not os.path.exists(output_folder): -# os.makedirs(output_folder) - -# cursor = connection.cursor(dictionary=True) - -# try: -# # # Fetch Contractor Details using PMC No -# # cursor.execute(""" -# # SELECT DISTINCT s.Contractor_Id, s.Contractor_Name, st.State_Name, d.District_Name, b.Block_Name, -# # s.Mobile_No, s.GST_Registration_Type, s.GST_No, s.PAN_No, s.Email, s.Address -# # FROM subcontractors s -# # LEFT JOIN assign_subcontractors asg ON s.Contractor_Id = asg.Contractor_Id -# # LEFT JOIN villages v ON asg.Village_Id = v.Village_Id -# # LEFT JOIN blocks b ON v.Block_Id = b.Block_Id -# # LEFT JOIN districts d ON b.District_id = d.District_id -# # LEFT JOIN states st ON d.State_Id = st.State_Id -# # WHERE asg.PMC_No = %s -# # """, (pmc_no,)) -# # contractor_info = cursor.fetchone() -# cursor.callproc('GetContractorDetailsByPMC', [pmc_no]) - -# # Now fetch the result: -# for result in cursor.stored_results(): -# contractor_info = result.fetchone() - -# if not contractor_info: -# return "No contractor found for this PMC No", 404 - -# # # Fetch distinct hold types present for the contractor -# # cursor.execute(""" -# # SELECT DISTINCT ht.hold_type_id, ht.hold_type -# # FROM invoice_subcontractor_hold_join h -# # JOIN hold_types ht ON h.hold_type_id = ht.hold_type_id -# # WHERE h.Contractor_Id = %s -# # """, (contractor_info["Contractor_Id"],)) -# # hold_types = cursor.fetchall() -# cursor.callproc('GetHoldTypesByContractor', [contractor_info["Contractor_Id"]]) - -# for result in cursor.stored_results(): -# hold_types = result.fetchall() - -# hold_type_map = {ht['hold_type_id']: ht['hold_type'] for ht in hold_types} - -# # # # Fetch Invoices & GST Releases -# # cursor.execute(""" -# # SELECT DISTINCT i.Invoice_Id, i.PMC_No, v.Village_Name, i.Work_Type, i.Invoice_Details, -# # i.Invoice_Date, i.Invoice_No, i.Basic_Amount, i.Debit_Amount, -# # i.After_Debit_Amount, i.GST_Amount, i.Amount, i.TDS_Amount, i.SD_Amount, -# # i.On_Commission, i.Hydro_Testing, i.GST_SD_Amount, i.Final_Amount, -# # g.pmc_no AS gst_pmc_no, g.invoice_no AS gst_invoice_no, -# # g.basic_amount AS gst_basic_amount, g.final_amount AS gst_final_amount -# # FROM invoice i -# # LEFT JOIN assign_subcontractors asg ON i.PMC_No = asg.PMC_No -# # LEFT JOIN villages v ON i.Village_Id = v.Village_Id -# # LEFT JOIN gst_release g ON i.PMC_No = g.pmc_no AND i.Invoice_No = g.invoice_no -# # WHERE asg.PMC_No = %s -# # ORDER BY i.Invoice_Date, i.Invoice_No -# # """, (pmc_no,)) -# # invoices = cursor.fetchall() - -# cursor.callproc('GetInvoicesAndGstReleaseByPmcNo', [pmc_no]) - -# for result in cursor.stored_results(): -# invoices = result.fetchall() -# print("pmc_report invoice data:",invoices) - -# # cursor.callproc('GetInvoicesAndGSTReleasesByPMC', [pmc_no]) - -# # for result in cursor.stored_results(): -# # invoices = result.fetchall() - -# # # Fetch Hold Amounts separately -# # cursor.execute(""" -# # SELECT h.Invoice_Id, ht.hold_type_id, h.hold_amount -# # FROM invoice_subcontractor_hold_join h -# # JOIN hold_types ht ON h.hold_type_id = ht.hold_type_id -# # WHERE h.Contractor_Id = %s -# # """, (contractor_info["Contractor_Id"],)) -# # hold_amounts = cursor.fetchall() -# cursor.callproc('GetHoldAmountsByContractor', [contractor_info["Contractor_Id"]]) - -# for result in cursor.stored_results(): -# hold_amounts = result.fetchall() - -# # Create a mapping of invoice_id to hold amounts by type -# hold_data = {} -# for h in hold_amounts: -# hold_data.setdefault(h['Invoice_Id'], {})[h['hold_type_id']] = h['hold_amount'] - -# # # Fetch all Payments for the PMC number -# # cursor.execute(""" -# # SELECT pmc_no, invoice_no, Payment_Amount, TDS_Payment_Amount, Total_amount, UTR -# # FROM payment -# # WHERE pmc_no = %s -# # ORDER BY invoice_no -# # """, (pmc_no,)) -# # all_payments = cursor.fetchall() -# cursor.callproc('GetAllPaymentsByPMC', [pmc_no]) - -# for result in cursor.stored_results(): -# all_payments = result.fetchall() - -# # Organize payments by Invoice No (both regular and GST release notes) -# payments_map = {} -# extra_payments = [] -# for pay in all_payments: -# if pay['invoice_no']: -# key = pay['invoice_no'] -# if key not in payments_map: -# payments_map[key] = [] -# payments_map[key].append(pay) -# else: -# extra_payments.append(pay) - -# # Create Excel workbook -# workbook = openpyxl.Workbook() -# sheet = workbook.active -# sheet.title = "PMC Report" - -# # Write Contractor Details -# sheet.append(["", "", "Laxmi Civil Engineering Services PVT. LTD.", "", ""]) -# sheet.append( -# ["Contractor Name", contractor_info["Contractor_Name"], " ", "GST No", contractor_info["GST_No"], " ", -# "GST Type", contractor_info["GST_Registration_Type"]]) -# sheet.append(["State", contractor_info["State_Name"], " ", "PAN No", contractor_info["PAN_No"], " ", "Address", -# contractor_info["Address"]]) -# sheet.append(["District", contractor_info["District_Name"], " ", "Mobile No", contractor_info["Mobile_No"]]) -# sheet.append(["Block", contractor_info["Block_Name"], " ", "Email", contractor_info["Email"]]) -# sheet.append([]) - -# # Table Headers - include all hold types as separate columns -# base_headers = ["PMC No", "Village", "Work Type", "Invoice Details", "Invoice Date", "Invoice No", -# "Basic Amount", "Debit", "After Debit Amount", "GST (18%)", "Amount", "TDS (1%)", -# "SD (5%)", "On Commission", "Hydro Testing", "GST SD Amount"] - -# hold_headers = [ht['hold_type'] for ht in hold_types] - -# payment_headers = ["Final Amount", "Payment Amount", "TDS Payment", "Total Paid", "UTR"] - -# sheet.append(base_headers + hold_headers + payment_headers) - -# seen_invoices = set() -# seen_gst_notes = set() -# processed_payments = set() - -# # Process invoices -# for inv in invoices: -# invoice_no = inv["Invoice_No"] -# payments = payments_map.get(invoice_no, []) - -# # Process invoice row with first payment (if exists) -# if invoice_no not in seen_invoices: -# seen_invoices.add(invoice_no) -# first_payment = payments[0] if len(payments) > 0 else None - -# # Base invoice data -# row = [ -# pmc_no, inv["Village_Name"], inv["Work_Type"], inv["Invoice_Details"], -# inv["Invoice_Date"], invoice_no, inv["Basic_Amount"], inv["Debit_Amount"], -# inv["After_Debit_Amount"], inv["GST_Amount"], inv["Amount"], inv["TDS_Amount"], -# inv["SD_Amount"], inv["On_Commission"], inv["Hydro_Testing"], inv["GST_SD_Amount"] -# ] - -# # Add hold amounts for each hold type -# invoice_holds = hold_data.get(inv["Invoice_Id"], {}) -# for ht_id in hold_type_map.keys(): -# row.append(invoice_holds.get(ht_id, "")) - -# # Add payment information -# row += [ -# inv["Final_Amount"], -# first_payment["Payment_Amount"] if first_payment else "", -# first_payment["TDS_Payment_Amount"] if first_payment else "", -# first_payment["Total_amount"] if first_payment else "", -# first_payment["UTR"] if first_payment else "" -# ] - -# sheet.append(row) - -# if first_payment: -# payment_id = f"{invoice_no}-{first_payment['Payment_Amount']}-{first_payment.get('UTR', '')}" -# processed_payments.add(payment_id) - -# # Process GST release if exists (only if we have a matching GST record) -# if inv["gst_pmc_no"] and inv["gst_invoice_no"] and inv["gst_invoice_no"] not in seen_gst_notes: -# seen_gst_notes.add(inv["gst_invoice_no"]) - -# # Find the payment that matches this GST release -# gst_payment = None -# for payment in payments[1:]: # Skip first payment (already used for invoice) -# if payment['invoice_no'] == inv["gst_invoice_no"]: -# gst_payment = payment -# break - -# # If no payment found in the invoice's payments, check all payments -# if not gst_payment: -# gst_payments = payments_map.get(inv["gst_invoice_no"], []) -# if gst_payments: -# gst_payment = gst_payments[0] - -# # GST release row -# row = [ -# pmc_no, "", "", "GST Release Note", "", inv["gst_invoice_no"], -# inv["gst_basic_amount"], "", "", "", "", "", "", "", "", "" # Empty GST SD Amount -# ] - -# # Empty holds for GST release -# row += ["" for _ in hold_headers] - -# # Add payment information -# row += [ -# inv["gst_final_amount"], -# gst_payment["Payment_Amount"] if gst_payment else "", -# gst_payment["TDS_Payment_Amount"] if gst_payment else "", -# gst_payment["Total_amount"] if gst_payment else "", -# gst_payment["UTR"] if gst_payment else "" -# ] - -# sheet.append(row) - -# if gst_payment: -# payment_id = f"{inv['gst_invoice_no']}-{gst_payment['Payment_Amount']}-{gst_payment.get('UTR', '')}" -# processed_payments.add(payment_id) - -# # Process remaining payments as extra payments -# for payment in payments[1:]: -# payment_id = f"{payment['invoice_no']}-{payment['Payment_Amount']}-{payment.get('UTR', '')}" -# if payment_id not in processed_payments: -# row = [ -# pmc_no, "", "", "", "", payment['invoice_no'], -# "", "", "", "", "", "", "", "", "", "" # Empty GST SD Amount -# ] - -# # Empty holds for extra payments -# row += ["" for _ in hold_headers] - -# # Add payment information -# row += [ -# "", -# payment["Payment_Amount"], -# payment["TDS_Payment_Amount"], -# payment["Total_amount"], -# payment["UTR"] -# ] - -# sheet.append(row) -# processed_payments.add(payment_id) - -# # Process extra payments (null invoice_no) -# for payment in extra_payments: -# payment_id = f"null-{payment['Payment_Amount']}-{payment.get('UTR', '')}" -# if payment_id not in processed_payments: -# row = [ -# pmc_no, "", "", "", "", "", -# "", "", "", "", "", "", "", "", "", "" # Empty GST SD Amount -# ] - -# # Empty holds for null invoice payments -# row += ["" for _ in hold_headers] - -# # Add payment information -# row += [ -# "", -# payment["Payment_Amount"], -# payment["TDS_Payment_Amount"], -# payment["Total_amount"], -# payment["UTR"] -# ] - -# sheet.append(row) -# processed_payments.add(payment_id) - -# # Calculate totals -# total_basic_amount = 0 -# total_tds_amount = 0 -# total_sd_amount = 0 -# total_on_commission = 0 -# total_hold_amount = 0 -# total_final_amount = 0 -# total_payment_amount = 0 -# total_tds_payment_amount = 0 -# total_total_paid = 0 - -# for row in sheet.iter_rows(min_row=2, max_row=sheet.max_row, values_only=True): -# try: -# total_basic_amount += float(row[6] or 0) # Basic_Amount -# total_tds_amount += float(row[11] or 0) # TDS_Amount -# total_sd_amount += float(row[12] or 0) # SD_Amount -# total_on_commission += float(row[13] or 0) # On_Commission -# total_final_amount += float(row[-5] or 0) # Final_Amount -# total_payment_amount += float(row[-4] or 0) # Payment_Amount -# total_tds_payment_amount += float(row[-3] or 0) # TDS_Payment -# total_total_paid += float(row[-2] or 0) # Total_Paid - -# # Sum of hold amounts -# hold_start_col = len(base_headers) -# hold_end_col = hold_start_col + len(hold_headers) -# total_hold_amount += sum(float(row[i] or 0) for i in range(hold_start_col, hold_end_col)) -# except (ValueError, IndexError, TypeError): -# continue - -# # Append totals row -# totals_row = [ -# "TOTAL", "", "", "", "", "", -# total_basic_amount, "", "", "", "", total_tds_amount, total_sd_amount, -# total_on_commission, "", "", # Empty GST SD Amount -# ] - -# # Add hold totals -# totals_row += [total_hold_amount] + [""] * (len(hold_headers) - 1) - -# # Add payment totals -# totals_row += [ -# total_final_amount, -# total_payment_amount, -# total_tds_payment_amount, -# total_total_paid, -# "" # UTR column remains empty -# ] - -# sheet.append([]) -# sheet.append(totals_row) - -# # Make totals row bold -# for cell in sheet[sheet.max_row]: -# cell.font = Font(bold=True) - -# # Save Excel file -# workbook.save(output_file) -# workbook.close() - -# finally: -# cursor.close() -# connection.close() - -# return send_from_directory(output_folder, f"PMC_Report_{pmc_no}.xlsx", as_attachment=True) - - - -# @app.route('/download_pmc_report/') -# def download_pmc_report(pmc_no): -# connection = config.get_db_connection() -# output_folder = "static/download" -# output_file = os.path.join(output_folder, f"PMC_Report_{pmc_no}.xlsx") -# -# if not os.path.exists(output_folder): -# os.makedirs(output_folder) -# -# cursor = connection.cursor(dictionary=True) -# -# try: -# cursor.callproc('GetContractorDetailsByPMC', [pmc_no]) -# -# for result in cursor.stored_results(): -# contractor_info = result.fetchone() -# -# if not contractor_info: -# return "No contractor found for this PMC No", 404 -# -# cursor.callproc('GetHoldTypesByContractor', [contractor_info["Contractor_Id"]]) -# -# for result in cursor.stored_results(): -# hold_types = result.fetchall() -# -# hold_type_map = {ht['hold_type_id']: ht['hold_type'] for ht in hold_types} -# -# cursor.callproc('GetInvoicesAndGstReleaseByPmcNo', [pmc_no]) -# -# for result in cursor.stored_results(): -# invoices = result.fetchall() -# total_tds=Decimal('0.00') -# final_amount=Decimal('0.00') -# # total_hold_amount=Decimal('0.00') -# for data in invoices: -# total_tds=total_tds+data.get('TDS_Amount',Decimal('0.00')) -# final_amount=final_amount+data.get('Final_Amount',Decimal('0.00')) -# -# cursor.callproc('GetHoldAmountsByContractor', [contractor_info["Contractor_Id"]]) -# -# for result in cursor.stored_results(): -# hold_amounts = result.fetchall() -# -# hold_data = {} -# for h in hold_amounts: -# hold_data.setdefault(h['Invoice_Id'], {})[h['hold_type_id']] = h['hold_amount'] -# -# cursor.callproc('GetAllPaymentsByPMC', [pmc_no]) -# -# for result in cursor.stored_results(): -# all_payments = result.fetchall() -# total_amount=Decimal('0.00') -# for d in all_payments: -# total_amount=total_amount+ d.get('Total_Amount',Decimal('0.00')) -# total_amount_paid= final_amount- total_amount; -# payments_map = {} -# extra_payments = [] -# for pay in all_payments: -# if pay['invoice_no']: -# key = pay['invoice_no'] -# if key not in payments_map: -# payments_map[key] = [] -# payments_map[key].append(pay) -# else: -# extra_payments.append(pay) -# -# workbook = openpyxl.Workbook() -# sheet = workbook.active -# sheet.title = "PMC Report" -# -# # Write Contractor Details -# sheet.append(["", "", "Laxmi Civil Engineering Services PVT. LTD.", "", ""]) -# sheet.append( -# ["Contractor Name", contractor_info["Contractor_Name"], " ", "GST No", contractor_info["GST_No"], " ", -# "GST Type", contractor_info["GST_Registration_Type"]]) -# sheet.append(["State", contractor_info["State_Name"], " ", "PAN No", contractor_info["PAN_No"], " ", "Address", -# contractor_info["Address"]]) -# sheet.append(["District", contractor_info["District_Name"], " ", "Mobile No", contractor_info["Mobile_No"]]) -# sheet.append(["Block", contractor_info["Block_Name"], " ", "Email", contractor_info["Email"]]) -# sheet.append([]) -# -# # Table Headers - include all hold types as separate columns -# base_headers = ["PMC No", "Village", "Work Type", "Invoice Details", "Invoice Date", "Invoice No", -# "Basic Amount", "Debit", "After Debit Amount", "GST (18%)", "Amount", "TDS (1%)", -# "SD (5%)", "On Commission", "Hydro Testing", "GST SD Amount"] -# -# hold_headers = [ht['hold_type'] for ht in hold_types] -# -# payment_headers = ["Final Amount", "Payment Amount", "TDS Payment", "Total Paid", "UTR"] -# -# sheet.append(base_headers + hold_headers + payment_headers) -# -# seen_invoices = set() -# seen_gst_notes = set() -# processed_payments = set() -# -# # Process invoices -# for inv in invoices: -# invoice_no = inv["Invoice_No"] -# payments = payments_map.get(invoice_no, []) -# -# # Process invoice row with first payment (if exists) -# if invoice_no not in seen_invoices: -# seen_invoices.add(invoice_no) -# first_payment = payments[0] if len(payments) > 0 else None -# -# # Base invoice data -# row = [ -# pmc_no, inv["Village_Name"], inv["Work_Type"], inv["Invoice_Details"], -# inv["Invoice_Date"], invoice_no, inv["Basic_Amount"], inv["Debit_Amount"], -# inv["After_Debit_Amount"], inv["GST_Amount"], inv["Amount"], inv["TDS_Amount"], -# inv["SD_Amount"], inv["On_Commission"], inv["Hydro_Testing"], inv["GST_SD_Amount"] -# ] -# -# # Add hold amounts for each hold type -# invoice_holds = hold_data.get(inv["Invoice_Id"], {}) -# for ht_id in hold_type_map.keys(): -# row.append(invoice_holds.get(ht_id, "")) -# -# # Add payment information -# row += [ -# inv["Final_Amount"], -# first_payment["Payment_Amount"] if first_payment else "", -# first_payment["TDS_Payment_Amount"] if first_payment else "", -# first_payment["Total_amount"] if first_payment else "", -# first_payment["UTR"] if first_payment else "" -# ] -# -# sheet.append(row) -# -# if first_payment: -# payment_id = f"{invoice_no}-{first_payment['Payment_Amount']}-{first_payment.get('UTR', '')}" -# processed_payments.add(payment_id) -# -# # Process GST release if exists (only if we have a matching GST record) -# if inv["gst_pmc_no"] and inv["gst_invoice_no"] and inv["gst_invoice_no"] not in seen_gst_notes: -# seen_gst_notes.add(inv["gst_invoice_no"]) -# -# # Find the payment that matches this GST release -# gst_payment = None -# for payment in payments[1:]: # Skip first payment (already used for invoice) -# if payment['invoice_no'] == inv["gst_invoice_no"]: -# gst_payment = payment -# break -# -# # If no payment found in the invoice's payments, check all payments -# if not gst_payment: -# gst_payments = payments_map.get(inv["gst_invoice_no"], []) -# if gst_payments: -# gst_payment = gst_payments[0] -# -# # GST release row (this will be in the same row, after the invoice information) -# gst_row = [ -# pmc_no, "", "", "GST Release Note", "", inv["gst_invoice_no"], -# inv["gst_basic_amount"], "", "", "", "", "", "", "", "", "" # Empty GST SD Amount -# ] -# -# # Empty holds for GST release -# gst_row += ["" for _ in hold_headers] -# -# # Add GST payment information (same columns as invoice payment information) -# gst_row += [ -# inv["gst_final_amount"], -# gst_payment["Payment_Amount"] if gst_payment else "", -# gst_payment["TDS_Payment_Amount"] if gst_payment else "", -# gst_payment["Total_amount"] if gst_payment else "", -# gst_payment["UTR"] if gst_payment else "" -# ] -# -# sheet.append(gst_row) -# -# if gst_payment: -# payment_id = f"{inv['gst_invoice_no']}-{gst_payment['Payment_Amount']}-{gst_payment.get('UTR', '')}" -# processed_payments.add(payment_id) -# -# # Process remaining payments as extra payments (if any) -# for payment in payments[1:]: -# payment_id = f"{payment['invoice_no']}-{payment['Payment_Amount']}-{payment.get('UTR', '')}" -# if payment_id not in processed_payments: -# row = [ -# pmc_no, "", "", "", "", payment['invoice_no'], -# "", "", "", "", "", "", "", "", "", "" # Empty GST SD Amount -# ] -# -# # Empty holds for extra payments -# row += ["" for _ in hold_headers] -# -# # Add payment information -# row += [ -# "", -# payment["Payment_Amount"], -# payment["TDS_Payment_Amount"], -# payment["Total_amount"], -# payment["UTR"] -# ] -# -# sheet.append(row) -# processed_payments.add(payment_id) -# -# # Process extra payments (null invoice_no) -# for payment in extra_payments: -# payment_id = f"null-{payment['Payment_Amount']}-{payment.get('UTR', '')}" -# if payment_id not in processed_payments: -# row = [ -# pmc_no, "", "", "", "", "", -# "", "", "", "", "", "", "", "", "", "" # Empty GST SD Amount -# ] -# -# # Empty holds for null invoice payments -# row += ["" for _ in hold_headers] -# -# # Add payment information -# row += [ -# "", -# payment["Payment_Amount"], -# payment["TDS_Payment_Amount"], -# payment["Total_amount"], -# payment["UTR"] -# ] -# -# sheet.append(row) -# processed_payments.add(payment_id) -# -# # Calculate totals -# total_basic_amount = 0 -# total_tds_amount = 0 -# total_sd_amount = 0 -# total_on_commission = 0 -# total_hold_amount = 0 -# total_final_amount = 0 -# total_payment_amount = 0 -# total_tds_payment_amount = 0 -# total_total_paid = 0 -# -# for row in sheet.iter_rows(min_row=2, max_row=sheet.max_row, values_only=True): -# try: -# total_basic_amount += float(row[6] or 0) # Basic_Amount -# total_tds_amount += float(row[11] or 0) # TDS_Amount -# total_sd_amount += float(row[12] or 0) # SD_Amount -# total_on_commission += float(row[13] or 0) # On_Commission -# total_final_amount += float(row[-5] or 0) # Final_Amount -# total_payment_amount += float(row[-4] or 0) # Payment_Amount -# total_tds_payment_amount += float(row[-3] or 0) # TDS_Payment -# total_total_paid += float(row[-2] or 0) # Total_Paid -# -# # Sum of hold amounts -# hold_start_col = len(base_headers) -# hold_end_col = hold_start_col + len(hold_headers) -# total_hold_amount += sum(float(row[i] or 0) for i in range(hold_start_col, hold_end_col)) -# except (ValueError, IndexError, TypeError): -# continue -# -# # Append totals row -# totals_row = [ -# "TOTAL", "", "", "", "", "", -# total_basic_amount, "", "", "", "", total_tds_amount, total_sd_amount, -# total_on_commission, "", "", # Empty GST SD Amount -# ] -# if hold_headers: -# totals_row += [total_hold_amount] + [""] * (len(hold_headers) - 1) -# -# # Add payment totals -# totals_row += [ -# total_final_amount, -# total_payment_amount, -# total_tds_payment_amount, -# total_total_paid, -# "" # UTR column remains empty -# ] -# -# sheet.append([]) -# sheet.append(totals_row) -# #new code added for small chart---summary -# total_hold_amount=Decimal('0.00') -# for d in invoices: -# total_hold_amount = total_hold_amount + d.get('SD_Amount', Decimal('0.00')) + d.get('On_Commission', -# Decimal( -# '0.00')) + d.get( -# 'Hydro_Testing', Decimal('0.00')) -# for data in hold_amounts: -# total_hold_amount = total_hold_amount + data.get('hold_amount', Decimal('0.00')) -# print("Total Hold Amount after adding the hold amount ", total_hold_amount) -# -# # Add payment information -# # Get today's date -# today_date = datetime.today().strftime('%A,%Y-%m-%d') -# # Add headers (optional) -# sheet.append(["Contractor Name", contractor_info["Contractor_Name"]]) -# sheet.append(["Date", today_date]) -# sheet.append(["Description", "Amount"]) -# # Add your values -# sheet.append(["Advance/Surplus", str(total_final_amount-total_payment_amount)]) -# sheet.append(["Total Hold Amount", str(total_hold_amount)]) -# sheet.append(["Amount With TDS", str(total_tds_payment_amount)]) -# # new coded ended here for summary chart -# # Make totals row bold -# for cell in sheet[sheet.max_row]: -# cell.font = Font(bold=True) -# -# # Save Excel file -# workbook.save(output_file) -# workbook.close() -# -# finally: -# cursor.close() -# connection.close() -# -# return send_from_directory(output_folder, f"PMC_Report_{pmc_no}.xlsx", as_attachment=True) - -# @app.route('/download_pmc_report/') -# def download_pmc_report(pmc_no): -# connection = config.get_db_connection() -# output_folder = "static/download" -# output_file = os.path.join(output_folder, f"PMC_Report_{pmc_no}.xlsx") -# -# if not os.path.exists(output_folder): -# os.makedirs(output_folder) -# -# cursor = connection.cursor(dictionary=True) -# -# try: -# cursor.callproc('GetContractorDetailsByPMC', [pmc_no]) -# contractor_info = next(cursor.stored_results()).fetchone() -# -# if not contractor_info: -# return "No contractor found for this PMC No", 404 -# -# cursor.callproc('GetHoldTypesByContractor', [contractor_info["Contractor_Id"]]) -# hold_types = next(cursor.stored_results()).fetchall() -# hold_type_map = {ht['hold_type_id']: ht['hold_type'] for ht in hold_types} -# -# cursor.callproc('GetInvoicesAndGstReleaseByPmcNo', [pmc_no]) -# invoices = next(cursor.stored_results()).fetchall() -# -# cursor.callproc('GetHoldAmountsByContractor', [contractor_info["Contractor_Id"]]) -# hold_amounts = next(cursor.stored_results()).fetchall() -# hold_data = {} -# for h in hold_amounts: -# hold_data.setdefault(h['Invoice_Id'], {})[h['hold_type_id']] = h['hold_amount'] -# -# cursor.callproc('GetAllPaymentsByPMC', [pmc_no]) -# all_payments = next(cursor.stored_results()).fetchall() -# -# payments_map = {} -# extra_payments = [] -# for pay in all_payments: -# if pay['invoice_no']: -# payments_map.setdefault(pay['invoice_no'], []).append(pay) -# else: -# extra_payments.append(pay) -# -# workbook = openpyxl.Workbook() -# sheet = workbook.active -# sheet.title = "PMC Report" -# -# # Write contractor header -# sheet.append(["", "", "Laxmi Civil Engineering Services PVT. LTD."]) -# sheet.append(["Contractor Name", contractor_info["Contractor_Name"], "", "GST No", contractor_info["GST_No"], "", "GST Type", contractor_info["GST_Registration_Type"]]) -# sheet.append(["State", contractor_info["State_Name"], "", "PAN No", contractor_info["PAN_No"], "", "Address", contractor_info["Address"]]) -# sheet.append(["District", contractor_info["District_Name"], "", "Mobile No", contractor_info["Mobile_No"]]) -# sheet.append(["Block", contractor_info["Block_Name"], "", "Email", contractor_info["Email"]]) -# sheet.append([]) -# -# base_headers = ["PMC No", "Village", "Work Type", "Invoice Details", "Invoice Date", "Invoice No", -# "Basic Amount", "Debit", "After Debit Amount", "GST (18%)", "Amount", "TDS (1%)", -# "SD (5%)", "On Commission", "Hydro Testing", "GST SD Amount"] -# -# hold_headers = [ht['hold_type'] for ht in hold_types] -# payment_headers = ["Final Amount", "Payment Amount", "TDS Payment", "Total Paid", "UTR"] -# sheet.append(base_headers + hold_headers + payment_headers) -# # Style the headers -# header_fill=PatternFill(start_color="ADD8E6",end_color="ADD8E6",fill_type="solid") -# header_font=Font(bold=True) -# for cell in sheet[sheet.max_row]: -# cell.font=header_font -# cell.fill=header_fill -# -# seen_invoices = set() -# seen_gst_notes = set() -# processed_payments = set() -# -# for inv in invoices: -# invoice_no = inv["Invoice_No"] -# payments = payments_map.get(invoice_no, []) -# -# if invoice_no not in seen_invoices: -# seen_invoices.add(invoice_no) -# first_payment = payments[0] if payments else None -# -# row = [ -# pmc_no, inv["Village_Name"], inv["Work_Type"], inv["Invoice_Details"], -# inv["Invoice_Date"], invoice_no, inv["Basic_Amount"], inv["Debit_Amount"], -# inv["After_Debit_Amount"], inv["GST_Amount"], inv["Amount"], inv["TDS_Amount"], -# inv["SD_Amount"], inv["On_Commission"], inv["Hydro_Testing"], inv["GST_SD_Amount"] -# ] -# -# invoice_holds = hold_data.get(inv["Invoice_Id"], {}) -# for ht_id in hold_type_map.keys(): -# row.append(invoice_holds.get(ht_id, "")) -# -# row += [ -# inv["Final_Amount"], -# first_payment["Payment_Amount"] if first_payment else "", -# first_payment["TDS_Payment_Amount"] if first_payment else "", -# first_payment["Total_amount"] if first_payment else "", -# first_payment["UTR"] if first_payment else "" -# ] -# -# sheet.append(row) -# -# if first_payment: -# processed_payments.add(f"{invoice_no}-{first_payment['Payment_Amount']}-{first_payment.get('UTR', '')}") -# -# if inv["gst_pmc_no"] and inv["gst_invoice_no"] and inv["gst_invoice_no"] not in seen_gst_notes: -# seen_gst_notes.add(inv["gst_invoice_no"]) -# gst_payment = None -# for payment in payments[1:]: -# if payment['invoice_no'] == inv["gst_invoice_no"]: -# gst_payment = payment -# break -# if not gst_payment: -# gst_payment = payments_map.get(inv["gst_invoice_no"], [None])[0] -# -# gst_row = [ -# pmc_no, "", "", "GST Release Note", "", inv["gst_invoice_no"], -# inv["gst_basic_amount"], "", "", "", "", "", "", "", "", "" -# ] -# gst_row += ["" for _ in hold_headers] -# gst_row += [ -# inv["gst_final_amount"], -# gst_payment["Payment_Amount"] if gst_payment else "", -# gst_payment["TDS_Payment_Amount"] if gst_payment else "", -# gst_payment["Total_amount"] if gst_payment else "", -# gst_payment["UTR"] if gst_payment else "" -# ] -# sheet.append(gst_row) -# if gst_payment: -# processed_payments.add(f"{inv['gst_invoice_no']}-{gst_payment['Payment_Amount']}-{gst_payment.get('UTR', '')}") -# -# for payment in payments[1:]: -# payment_id = f"{payment['invoice_no']}-{payment['Payment_Amount']}-{payment.get('UTR', '')}" -# if payment_id not in processed_payments: -# row = [pmc_no, "", "", "", "", payment['invoice_no']] + [""] * 10 -# row += ["" for _ in hold_headers] -# row += [ -# "", payment["Payment_Amount"], payment["TDS_Payment_Amount"], -# payment["Total_amount"], payment["UTR"] -# ] -# sheet.append(row) -# processed_payments.add(payment_id) -# -# for payment in extra_payments: -# row = [pmc_no, "", "", "", "", ""] + [""] * 10 -# row += ["" for _ in hold_headers] -# row += [ -# "", payment["Payment_Amount"], payment["TDS_Payment_Amount"], -# payment["Total_amount"], payment["UTR"] -# ] -# sheet.append(row) -# -# # Totals -# total_basic_amount = Decimal('0.00') -# total_tds_amount = Decimal('0.00') -# total_sd_amount = Decimal('0.00') -# total_on_commission = Decimal('0.00') -# total_final_amount = Decimal('0.00') -# total_payment_amount = Decimal('0.00') -# total_tds_payment_amount = Decimal('0.00') -# total_total_paid = Decimal('0.00') -# total_hold_amount_dynamic = Decimal('0.00') -# -# for row in sheet.iter_rows(min_row=8, max_row=sheet.max_row, values_only=True): -# try: -# total_basic_amount += Decimal(str(row[6] or 0)) -# total_tds_amount += Decimal(str(row[11] or 0)) -# total_sd_amount += Decimal(str(row[12] or 0)) -# total_on_commission += Decimal(str(row[13] or 0)) -# total_final_amount += Decimal(str(row[-5] or 0)) -# total_payment_amount += Decimal(str(row[-4] or 0)) -# total_tds_payment_amount += Decimal(str(row[-3] or 0)) -# total_total_paid += Decimal(str(row[-2] or 0)) -# -# for i in range(len(base_headers), len(base_headers) + len(hold_headers)): -# total_hold_amount_dynamic += Decimal(str(row[i] or 0)) -# except: -# continue -# -# totals_row = [ -# "TOTAL", "", "", "", "", "", -# total_basic_amount, "", "", "", "", total_tds_amount, total_sd_amount, -# total_on_commission, "", "" -# ] -# totals_row += [total_hold_amount_dynamic] + [""] * (len(hold_headers) - 1) -# totals_row += [ -# total_final_amount, -# total_payment_amount, -# total_tds_payment_amount, -# total_total_paid, -# "" -# ] -# -# sheet.append([]) -# sheet.append(totals_row) -# -# # Summary -# summary_hold = Decimal('0.00') -# for d in invoices: -# summary_hold += Decimal(str(d.get('SD_Amount', 0.00))) + Decimal(str(d.get('On_Commission', 0.00))) + Decimal(str(d.get('Hydro_Testing', 0.00))) -# for h in hold_amounts: -# summary_hold += Decimal(str(h.get('hold_amount', 0.00))) -# -# sheet.append([]) -# today = datetime.today().strftime('%A, %Y-%m-%d') -# sheet.append(["Contractor Name", contractor_info["Contractor_Name"]]) -# sheet.append(["Date", today]) -# sheet.append(["Description", "Amount"]) -# sheet.append(["Advance/Surplus", str(total_final_amount - total_payment_amount)]) -# sheet.append(["Total Hold Amount", str(summary_hold)]) -# sheet.append(["Amount With TDS", str(total_payment_amount + total_tds_payment_amount)]) -# -# for cell in sheet[sheet.max_row]: -# cell.font = Font(bold=True) -# -# workbook.save(output_file) -# workbook.close() -# -# finally: -# cursor.close() -# connection.close() -# -# return send_from_directory(output_folder, f"PMC_Report_{pmc_no}.xlsx", as_attachment=True) - - -@app.route('/download_pmc_report/') -def download_pmc_report(pmc_no): - connection = config.get_db_connection() - output_folder = "static/download" - output_file = os.path.join(output_folder, f"PMC_Report_{pmc_no}.xlsx") - - if not os.path.exists(output_folder): - os.makedirs(output_folder) - - cursor = connection.cursor(dictionary=True) - - try: - cursor.callproc('GetContractorDetailsByPMC', [pmc_no]) - contractor_info = next(cursor.stored_results()).fetchone() - - if not contractor_info: - return "No contractor found for this PMC No", 404 - - cursor.callproc('GetHoldTypesByContractor', [contractor_info["Contractor_Id"]]) - hold_types = next(cursor.stored_results()).fetchall() - hold_type_map = {ht['hold_type_id']: ht['hold_type'] for ht in hold_types} - - cursor.callproc('GetInvoicesAndGstReleaseByPmcNo', [pmc_no]) - invoices = next(cursor.stored_results()).fetchall() - - # Credit Note - # Credit Note Fetch - cursor.execute(""" - SELECT PMC_No, Invoice_Details, Basic_Amount, Debit_Amount, After_Debit_Amount, - GST_Amount, Amount, Final_Amount, Payment_Amount, Total_Amount, UTR, Invoice_No - FROM credit_note - WHERE Contractor_Id = %s - """, (pmc_no,)) - - credit_notes = cursor.fetchall() - - - # Build map by (PMC_No, Invoice_No) - credit_note_map = {} - for cn in credit_notes: - key = (cn["PMC_No"], cn["Invoice_No"]) # Use correct casing! - credit_note_map.setdefault(key, []).append(cn) - - # Track already appended credit notes - appended_credit_keys = set() - - cursor.callproc('GetHoldAmountsByContractor', [contractor_info["Contractor_Id"]]) - hold_amounts = next(cursor.stored_results()).fetchall() - hold_data = {} - for h in hold_amounts: - hold_data.setdefault(h['Invoice_Id'], {})[h['hold_type_id']] = h['hold_amount'] - - cursor.callproc('GetAllPaymentsByPMC', [pmc_no]) - all_payments = next(cursor.stored_results()).fetchall() - - - payments_map = {} - extra_payments = [] - for pay in all_payments: - if pay['invoice_no']: - payments_map.setdefault(pay['invoice_no'], []).append(pay) - else: - extra_payments.append(pay) - - # Fetch GST Release data - cursor.execute(""" - SELECT PMC_No, Invoice_No, Basic_Amount, Final_Amount, Total_Amount, UTR - FROM gst_release - WHERE PMC_No = %s - """, (pmc_no,)) - gst_releases = cursor.fetchall() - gst_release_map = {} - for gr in gst_releases: - invoice_nos = [] - - if gr['Invoice_No']: - cleaned = gr['Invoice_No'].replace(' ', '') - if '&' in cleaned: - invoice_nos = cleaned.split('&') - elif ',' in cleaned: - invoice_nos = cleaned.split(',') - else: - invoice_nos = [cleaned] - - for inv_no in invoice_nos: - gst_release_map.setdefault(inv_no, []).append(gr) - LogHelper.log_action("Download PMC Report", f"User {current_user.id} Download PMC Report'{ pmc_no}'") - workbook = openpyxl.Workbook() - sheet = workbook.active - sheet.title = "PMC Report" - - # Write contractor header - sheet.append(["", "", "Laxmi Civil Engineering Services PVT. LTD."]) - sheet.append(["Contractor Name", contractor_info["Contractor_Name"], "", "GST No", contractor_info["GST_No"], "", "GST Type", contractor_info["GST_Registration_Type"]]) - sheet.append(["State", contractor_info["State_Name"], "", "PAN No", contractor_info["PAN_No"], "", "Address", contractor_info["Address"]]) - sheet.append(["District", contractor_info["District_Name"], "", "Mobile No", contractor_info["Mobile_No"]]) - sheet.append(["Block", contractor_info["Block_Name"], "", "Email", contractor_info["Email"]]) - sheet.append([]) - - base_headers = ["PMC No", "Village", "Work Type", "Invoice Details", "Invoice Date", "Invoice No", - "Basic Amount", "Debit", "After Debit Amount", "GST (18%)", "Amount", "TDS (1%)", - "SD (5%)", "On Commission", "Hydro Testing", "GST SD Amount"] - - hold_headers = [ht['hold_type'] for ht in hold_types] - payment_headers = ["Final Amount", "Payment Amount", "TDS Payment", "Total Paid", "UTR"] - sheet.append(base_headers + hold_headers + payment_headers) - # Style the headers - header_fill=PatternFill(start_color="ADD8E6",end_color="ADD8E6",fill_type="solid") - header_font=Font(bold=True) - for cell in sheet[sheet.max_row]: - cell.font=header_font - cell.fill=header_fill - - seen_invoices = set() - seen_gst_notes = set() - processed_payments = set() - - for inv in invoices: - invoice_no = inv["Invoice_No"] - payments = payments_map.get(invoice_no, []) - - if invoice_no not in seen_invoices: - seen_invoices.add(invoice_no) - first_payment = payments[0] if payments else None - - row = [ - pmc_no, inv["Village_Name"], inv["Work_Type"], inv["Invoice_Details"], - inv["Invoice_Date"], invoice_no, inv["Basic_Amount"], inv["Debit_Amount"], - inv["After_Debit_Amount"], inv["GST_Amount"], inv["Amount"], inv["TDS_Amount"], - inv["SD_Amount"], inv["On_Commission"], inv["Hydro_Testing"], inv["GST_SD_Amount"] - ] - - invoice_holds = hold_data.get(inv["Invoice_Id"], {}) - for ht_id in hold_type_map.keys(): - row.append(invoice_holds.get(ht_id, "")) - - row += [ - inv["Final_Amount"], - first_payment["Payment_Amount"] if first_payment else "", - first_payment["TDS_Payment_Amount"] if first_payment else "", - first_payment["Total_amount"] if first_payment else "", - first_payment["UTR"] if first_payment else "" - ] - - sheet.append(row) - - if first_payment: - processed_payments.add(f"{invoice_no}-{first_payment['Payment_Amount']}-{first_payment.get('UTR', '')}") - - # if inv["gst_pmc_no"] and inv["gst_invoice_no"] and inv["gst_invoice_no"] not in seen_gst_notes: - # seen_gst_notes.add(inv["gst_invoice_no"]) - # gst_payment = None - # for payment in payments[1:]: - # if payment['invoice_no'] == inv["gst_invoice_no"]: - # gst_payment = payment - # break - # if not gst_payment: - # gst_payment = payments_map.get(inv["gst_invoice_no"], [None])[0] - # - # gst_row = [ - # pmc_no, "", "", "GST Release Note", "", inv["gst_invoice_no"], - # inv["gst_basic_amount"], "", "", "", "", "", "", "", "", "" - # ] - # gst_row += ["" for _ in hold_headers] - # gst_row += [ - # inv["gst_final_amount"], - # gst_payment["Payment_Amount"] if gst_payment else "", - # gst_payment["TDS_Payment_Amount"] if gst_payment else "", - # gst_payment["Total_amount"] if gst_payment else "", - # gst_payment["UTR"] if gst_payment else "" - # ] - # sheet.append(gst_row) - # Add GST Release Note(s) for this invoice if any - if invoice_no in gst_release_map: - for gr in gst_release_map[invoice_no]: - gst_row = [ - pmc_no, "", "", "GST Release Note", "", gr["Invoice_No"], - gr["Basic_Amount"], "", "", "", "", "", "", "", "", "" - ] - gst_row += ["" for _ in hold_headers] - gst_row += [ - gr["Final_Amount"], "", "", gr["Total_Amount"], - gr["UTR"] if gr["UTR"] else "" - ] - - # ✅ Ensure proper alignment - while len(gst_row) < len(base_headers) + len(hold_headers) + len(payment_headers): - gst_row.append("") - - sheet.append(gst_row) - - # if gst_payment: - # processed_payments.add(f"{inv['gst_invoice_no']}-{gst_payment['Payment_Amount']}-{gst_payment.get('UTR', '')}") - - for payment in payments[1:]: - payment_id = f"{payment['invoice_no']}-{payment['Payment_Amount']}-{payment.get('UTR', '')}" - if payment_id not in processed_payments: - row = [pmc_no, "", "", "", "", payment['invoice_no']] + [""] * 10 - row += ["" for _ in hold_headers] - row += [ - "", payment["Payment_Amount"], payment["TDS_Payment_Amount"], - payment["Total_amount"], payment["UTR"] - ] - sheet.append(row) - processed_payments.add(payment_id) - - for payment in extra_payments: - row = [pmc_no, "", "", "", "", ""] + [""] * 10 - row += ["" for _ in hold_headers] - row += [ - "", payment["Payment_Amount"], payment["TDS_Payment_Amount"], - payment["Total_amount"], payment["UTR"] - ] - sheet.append(row) - - # Credit Note row(s) - - # Track already appended credit notes - appended_credit_keys = set() - - # While writing invoices - key = (pmc_no, invoice_no) - if key in credit_note_map and key not in appended_credit_keys: - for cn in credit_note_map[key]: - credit_row = [ - pmc_no, "", "", cn.get("Invoice_Details", "Credit Note"), "", cn.get("Invoice_No", ""), - cn.get("Basic_Amount", ""), cn.get("Debit_Amount", ""), - cn.get("After_Debit_Amount", ""), cn.get("GST_Amount", ""), cn.get("Amount", ""), "", "", "", - "", "" - ] - credit_row += ["" for _ in hold_headers] - credit_row += [ - cn.get("Final_Amount", ""), - cn.get("Total_Amount", ""), - cn.get("UTR", "") - ] - - sheet.append(credit_row) - - appended_credit_keys.add(key) - - # Totals - total_basic_amount = Decimal('0.00') - total_tds_amount = Decimal('0.00') - total_sd_amount = Decimal('0.00') - total_on_commission = Decimal('0.00') - total_final_amount = Decimal('0.00') - total_payment_amount = Decimal('0.00') - total_tds_payment_amount = Decimal('0.00') - total_total_paid = Decimal('0.00') - total_hold_amount_dynamic = Decimal('0.00') - - for row in sheet.iter_rows(min_row=8, max_row=sheet.max_row, values_only=True): - try: - total_basic_amount += Decimal(str(row[6] or 0)) - total_tds_amount += Decimal(str(row[11] or 0)) - total_sd_amount += Decimal(str(row[12] or 0)) - total_on_commission += Decimal(str(row[13] or 0)) - total_final_amount += Decimal(str(row[-5] or 0)) - total_payment_amount += Decimal(str(row[-4] or 0)) - total_tds_payment_amount += Decimal(str(row[-3] or 0)) - total_total_paid += Decimal(str(row[-2] or 0)) - - for i in range(len(base_headers), len(base_headers) + len(hold_headers)): - total_hold_amount_dynamic += Decimal(str(row[i] or 0)) - except: - continue - - # totals_row = [ - # "TOTAL", "", "", "", "", "", - # total_basic_amount, "", "", "", "", total_tds_amount, total_sd_amount, - # total_on_commission, "", "" - # ] - # if total_hold_amount_dynamic: - # totals_row += [total_hold_amount_dynamic] + [""] * (len(hold_headers) - 1) - # totals_row += [ - # total_final_amount, - # total_payment_amount, - # total_tds_payment_amount, - # total_total_paid, - # "" - # ] - # Prepare empty totals_row with length of base_headers - totals_row = [""] * len(base_headers) - - # Fill in specific columns - totals_row[0] = "TOTAL" # Column 0: Label - totals_row[6] = total_basic_amount # Column 6: Basic Amount - totals_row[11] = total_tds_amount # Column 11: TDS - totals_row[12] = total_sd_amount # Column 12: SD - totals_row[13] = total_on_commission # Column 13: On Commission - - # Add hold header totals - hold_values = ["" for _ in hold_headers] - if total_hold_amount_dynamic: - hold_values[0] = total_hold_amount_dynamic # Only in first column - totals_row += hold_values - - # Add payment section - totals_row += [ - total_final_amount, - total_payment_amount, - total_tds_payment_amount, - total_total_paid, - "" # UTR - ] - - sheet.append([]) - sheet.append(totals_row) - - # Summary - # summary_hold = Decimal('0.00') - # for d in invoices: - # summary_hold += Decimal(str(d.get('SD_Amount', 0))) + Decimal(str(d.get('On_Commission', 0))) + Decimal(str(d.get('Hydro_Testing', 0))) - # for h in hold_amounts: - # summary_hold += Decimal(str(h.get('hold_amount', 0))) - - sheet.append([]) - today = datetime.today().strftime('%A, %Y-%m-%d') - sheet.append(["Contractor Name", contractor_info["Contractor_Name"]]) - sheet.append(["Date", today]) - sheet.append(["Description", "Amount"]) - sheet.append(["Advance/Surplus", str(total_final_amount - total_total_paid)]) - sheet.append(["Total Hold Amount", str(total_hold_amount_dynamic)]) - sheet.append(["Amount With TDS", str(total_tds_amount)]) - - for cell in sheet[sheet.max_row]: - cell.font = Font(bold=True) - - workbook.save(output_file) - workbook.close() - - finally: - cursor.close() - connection.close() - - return send_from_directory(output_folder, f"PMC_Report_{pmc_no}.xlsx", as_attachment=True) - -# --------- Hold Types Controller -------------------------------------------- -# Route to Add a New Hold Type -@app.route('/add_hold_type', methods=['POST', 'GET']) -def add_hold_type(): - connection = config.get_db_connection() - cursor = connection.cursor(dictionary=True) - - try: - # Fetch all hold types using the stored procedure - cursor.callproc("GetAllHoldTypes") - hold_types = [] - for hold in cursor.stored_results(): - hold_types = hold.fetchall() - - if request.method == 'POST': - hold_type = request.form.get('hold_type', '').strip() - - # Validation: Must start with a letter - if not hold_type or not hold_type[0].isalpha(): - return jsonify({"status": "error", "message": "Hold Type must start with a letter."}), 400 - - # Validation: Check if it already exists (case-insensitive) - # cursor.execute("SELECT COUNT(*) AS count FROM hold_types WHERE LOWER(hold_type) = LOWER(%s)", (hold_type,)) - # if cursor.fetchone()['count'] > 0: - # return jsonify({"status": "error", "message": "This Hold Type already exists."}), 400 - # Call the procedure to check if the hold_type exists - - cursor.callproc('CheckHoldTypeExists', [hold_type]) - - try: - # Insert new hold type into the database - # cursor.execute("INSERT INTO hold_types (hold_type) VALUES (%s)", (hold_type,)) - # connection.commit() - cursor.callproc('SaveHoldType', [hold_type]) - connection.commit() - - return jsonify({"status": "success", "message": "Hold Type added successfully!"}), 201 - except mysql.connector.Error as e: - connection.rollback() - return jsonify({"status": "error", "message": f"Database error: {str(e)}"}), 500 - - except mysql.connector.Error as e: - return jsonify({"status": "error", "message": f"Database error: {str(e)}"}), 500 - - finally: - cursor.close() - connection.close() - - return render_template('add_hold_type.html', Hold_Types_data=hold_types) - - -# Route to Update Hold Type -# @app.route('/update_hold_type/', methods=['POST', 'GET']) -# def update_hold_type(id): -# # GET request: Show the form with the current hold type -# if request.method == 'GET': -# connection = config.get_db_connection() -# cursor = connection.cursor() -# # cursor.execute("SELECT * FROM hold_types WHERE hold_type_id = %s", (id,)) -# # hold_type = cursor.fetchone() -# -# cursor.callproc("GetHoldTypesById", (id,)) -# for hold in cursor.stored_results(): -# hold_type = hold.fetchone() -# -# cursor.close() -# connection.close() -# -# if not hold_type: -# return jsonify({'status': 'error', 'message': 'Hold Type not found.'}), 404 -# -# return render_template('edit_hold_type.html', hold_type=hold_type) -# -# # POST request: Update the hold type -# if request.method == 'POST': -# new_hold_type = request.form.get('hold_type').strip() -# -# # Validation: Must start with a letter -# if not new_hold_type or not new_hold_type[0].isalpha(): -# return jsonify(ResponseHandler.invalid_name('Hold Type')), 400 -# -# connection = config.get_db_connection() -# cursor = connection.cursor() -# -# try: -# # Check if the hold type exists before updating -# # cursor.execute("SELECT * FROM hold_types WHERE hold_type_id = %s", (id,)) -# # hold_type = cursor.fetchone() -# cursor.callproc("GetHoldTypesById", (id,)) -# for hold in cursor.stored_results(): -# hold_type = hold.fetchone() -# -# if not hold_type: -# return jsonify({'status': 'error', 'message': 'Hold Type not found.'}), 404 -# -# # Update the hold type -# # cursor.execute("UPDATE hold_types SET hold_type = %s WHERE hold_type_id = %s", (new_hold_type, id)) -# cursor.callproc("UpdateHoldTypeById", (id,new_hold_type)) -# connection.commit() -# return jsonify(ResponseHandler.update_success('Hold Type')) -# -# except mysql.connector.Error as e: -# connection.rollback() -# return jsonify(ResponseHandler.update_failure('Hold Type')), 500 -# finally: -# cursor.close() -# connection.close() - - -@app.route('/update_hold_type/', methods=['GET', 'POST']) -def update_hold_type(id): - connection = config.get_db_connection() - cursor = connection.cursor() - - if request.method == 'GET': - cursor.callproc("GetHoldTypesById", (id,)) - for hold in cursor.stored_results(): - hold_type = hold.fetchone() - cursor.close() - connection.close() - - if not hold_type: - flash('Hold Type not found.', 'error') - return redirect(url_for('add_hold_type')) - - return render_template('edit_hold_type.html', hold_type=hold_type) - - elif request.method == 'POST': - new_hold_type = request.form.get('hold_type', '').strip() - - if not new_hold_type or not new_hold_type[0].isalpha(): - flash('Invalid hold type name. Must start with a letter.', 'error') - return redirect(url_for('add_hold_type')) - - try: - cursor.callproc("GetHoldTypesById", (id,)) - for h in cursor.stored_results(): - hold_type = h.fetchone() - - if not hold_type: - flash('Hold Type not found.', 'error') - return redirect(url_for('add_hold_type')) - - cursor.callproc("UpdateHoldTypeById", (id, new_hold_type)) - connection.commit() - flash('Hold Type updated successfully!', 'success') - - except mysql.connector.Error as e: - connection.rollback() - flash('Failed to update Hold Type.', 'error') - - finally: - cursor.close() - connection.close() - - return redirect(url_for('add_hold_type')) - - - -# Route to Delete Hold Type -@app.route('/delete_hold_type/', methods=['POST']) -def delete_hold_type(id): - connection = config.get_db_connection() - cursor = connection.cursor() - - try: - # cursor.execute("SELECT * FROM hold_types WHERE hold_type_id = %s", (id,)) - # hold_type = cursor.fetchone() - cursor.callproc("GetHoldTypesById", (id,)) - for hold in cursor.stored_results(): - hold_type = hold.fetchone() - LogHelper.log_action("Delete hold type", f"User {current_user.id} Delete hold type'{ hold_type}'") - if not hold_type: - return jsonify({'status': 'error', 'message': 'Hold Type not found.'}), 404 - - # Proceed with deletion - # cursor.execute("DELETE FROM hold_types WHERE hold_type_id = %s", (id,)) - cursor.callproc("DeleteHoldType", (id,)) - connection.commit() - return jsonify(ResponseHandler.delete_success('Hold Type')) - - except mysql.connector.Error as e: - return jsonify(ResponseHandler.delete_failure('Hold Type')), 500 - finally: - cursor.close() - connection.close() - -@app.route('/unreleased_gst') -def unreleased_gst(): - connection = config.get_db_connection() - cursor = connection.cursor(dictionary=True) - - try: - # Step 1: Fetch invoices - cursor.execute(""" - SELECT - i.PMC_No, i.Invoice_No, i.GST_SD_Amount, i.Invoice_Details, - i.Basic_Amount, i.Final_Amount - FROM `invoice` i - """) - invoices = cursor.fetchall() - - # Step 2: Fetch GST releases - cursor.execute(""" - SELECT Invoice_No, Basic_Amount ,Final_Amount - FROM gst_release - """) - gst_releases = cursor.fetchall() - - # Step 3: Lookup sets - gst_invoice_nos = {g['Invoice_No'] for g in gst_releases if g['Invoice_No']} - gst_basic_amounts = {float(g['Basic_Amount']) for g in gst_releases if g['Basic_Amount'] is not None} - - # Step 4: Filter - unreleased = [] - for inv in invoices: - match_by_invoice = inv['Invoice_No'] in gst_invoice_nos - match_by_gst_amount = float(inv.get('GST_SD_Amount') or 0.0) in gst_basic_amounts - if not (match_by_invoice or match_by_gst_amount): - unreleased.append(inv) - - return render_template("unreleased_gst.html", data=unreleased) - - finally: - cursor.close() - connection.close() - -# -- end hold types controlller -------------------- - - -# -- end hold types controlller -------------------- - -# Route to display the HTML form -@app.route('/add_work_order', methods=['GET']) -def add_work_order(): - # Add database connection - connection = config.get_db_connection() - cursor = connection.cursor(dictionary=True) - - cursor.execute("SELECT Contractor_id, Contractor_Name FROM subcontractors") # Adjust table/column names as needed - subcontractor = cursor.fetchall() - - cursor.close() - connection.close() - - return render_template('add_work_order.html', subcontractor=subcontractor) # This is your HTML form page - - -# Route to handle form submission (from action="/submit_work_order") -@app.route('/submit_work_order', methods=['POST', 'GET']) -def submit_work_order(): - vendor_name = request.form['vendor_name'] - work_order_type = request.form['work_order_type'] - work_order_amount = request.form['work_order_amount'] - boq_amount = request.form['boq_amount'] - work_done_percentage = request.form['work_done_percentage'] - work_order_number = request.form['work_order_number'] - gst_amount = request.form['gst_amount'] - tds_amount = request.form['tds_amount'] - security_deposite = request.form['security_deposite'] - sd_against_gst = request.form['sd_against_gst'] - final_total = request.form['final_total'] - tds_of_gst = request.form['tds_of_gst'] - LogHelper.log_action("Submit Work Order", f"User {current_user.id} Submit Work Order'{ work_order_type}'") - # print("Good Morning How are U") - connection = config.get_db_connection() - cursor = connection.cursor(dictionary=True) - # print("Good morning and how are you") - insert_query = """ - INSERT INTO work_order - (vendor_name, work_order_type, work_order_amount, boq_amount, work_done_percentage,work_order_number,gst_amount,tds_amount - ,security_deposit,sd_against_gst,final_total,tds_of_gst) - VALUES (%s, %s, %s, %s, %s,%s,%s,%s,%s,%s,%s,%s) - """ - cursor.execute(insert_query, - (vendor_name, work_order_type, work_order_amount, boq_amount, work_done_percentage, work_order_number - , gst_amount, tds_amount, security_deposite, sd_against_gst, final_total, tds_of_gst)) - connection.commit() - - # ✅ Fetch all data after insert - select_query = "SELECT * FROM work_order" - cursor.execute(select_query) - wo = cursor.fetchall() - # print("The Work order data is ",wo) - print("The data from work order ", wo) # should now print the data properly - cursor.execute("SELECT Contractor_id, Contractor_Name FROM subcontractors") # Adjust table/column names as needed - subcontractor = cursor.fetchall() - cursor.close() - connection.close() - - return render_template('add_work_order.html', work_order_type=work_order_type, wo=wo, subcontractor=subcontractor) - - -@app.route('/delete_work_order/', methods=['POST']) -def delete_work_order(id): - connection = config.get_db_connection() - cursor = connection.cursor() - cursor.execute("DELETE FROM work_order WHERE work_order_id = %s", (id,)) - connection.commit() - cursor.close() - connection.close() - LogHelper.log_action("delete Work Order", f"User {current_user.id} delete Work Order'{ id}'") - return jsonify({'success': True}) - - -@app.route('/update_work_order/', methods=['GET', 'POST']) -def update_work_order(id): - connection = config.get_db_connection() - cursor = connection.cursor(dictionary=True) - - if request.method == 'POST': - work_order_type = request.form['work_order_type'] - work_order_amount = request.form['work_order_amount'] - boq_amount = request.form['boq_amount'] - work_done_percentage = request.form['work_done_percentage'] - work_order_number = request.form['work_order_number'] - gst_amount = request.form['gst_amount'] - tds_amount = request.form['tds_amount'] - security_deposite = request.form['security_deposite'] - sd_against_gst = request.form['sd_against_gst'] - final_amount = request.form['final_amount'] - tds_of_gst = request.form['tds_of_gst'] - update_query = """ - UPDATE work_order - SET work_order_type = %s, - work_order_amount = %s, - boq_amount = %s, - work_done_percentage = %s, - work_order_number= %s, - gst_amount = %s, - tds_amount= %s, - security_deposite= %s, - sd_against_gst=%s, - final_amount= %s, - tds_of_gst=%s - WHERE work_order_id = %s - """ - cursor.execute(update_query, ( - work_order_type, work_order_amount, boq_amount, work_done_percentage, work_order_number, gst_amount, - tds_amount, security_deposite, sd_against_gst, final_amount, tds_of_gst, id)) - connection.commit() - cursor.close() - connection.close() - - # If GET request: fetch the existing record - cursor.execute("SELECT * FROM work_order WHERE work_order_id = %s", (id,)) - work_order = cursor.fetchone() - cursor.close() - connection.close() - return render_template('update_work_order.html', work_order=work_order) - - -# Optional: Route to show a success message -@app.route('/success') -def success(): - return "Work Order Submitted Successfully!" - - - -logging.basicConfig(level=logging.DEBUG) - - -@app.route('/add_purchase_order', methods=['GET', 'POST']) -def add_purchase_order(): - connection = config.get_db_connection() - cursor = connection.cursor(dictionary=True) - - if request.method == 'POST': - # Fetch form fields - purchase_date = request.form.get('purchase_date') - supplier_name = request.form.get('supplier_name') - purchase_order_no = request.form.get('purchase_order_no') - item_name = request.form.get('item_name') - quantity = request.form.get('quantity') - unit = request.form.get('unit') - rate = request.form.get('rate') - amount = request.form.get('amount') - GST_Amount = request.form.get('GST_Amount') - TDS = request.form.get('TDS') - final_amount = request.form.get('final_amount') - LogHelper.log_action("Add purchase order", f"User {current_user.id} Added puirchase Order'{ purchase_order_no}'") - # Insert into database - insert_query = """ - INSERT INTO purchase_order ( - purchase_date, supplier_name, purchase_order_no, item_name, quantity, unit, rate, amount, - GST_Amount, TDS, final_amount - ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) - """ - cursor.execute(insert_query, ( - purchase_date, supplier_name, purchase_order_no, item_name, quantity, unit, rate, amount, - GST_Amount, TDS, final_amount - )) - connection.commit() - - # ✅ Always fetch updated data - cursor.execute("SELECT * FROM purchase_order") - purchases = cursor.fetchall() - - cursor.close() - connection.close() - - return render_template('add_purchase_order.html', purchases=purchases) - - -# Show all purchases -@app.route('/purchase_orders') -def show_purchase_orders(): - connection = config.get_db_connection() - cursor = connection.cursor(dictionary=True) - cursor.execute("SELECT * FROM purchase_order") - purchases = cursor.fetchall() - cursor.close() - connection.close() - return render_template('add_purchase_order.html', purchases=purchases) - - -# Delete purchase order -@app.route('/delete_purchase/', methods=['POST']) -def delete_purchase(id): - connection = config.get_db_connection() - cursor = connection.cursor() - cursor.execute("DELETE FROM purchase_order WHERE purchase_id = %s", (id,)) - connection.commit() - cursor.close() - connection.close() - LogHelper.log_action("Delete purchase order", f"User {current_user.id} Deleted puirchase Order'{ id}'") - return render_template(('add_purchase_order.html')) - - -# Edit purchase order (form + update logic) -@app.route('/update_purchase/', methods=['GET', 'POST']) -def update_purchase(id): - connection = config.get_db_connection() - cursor = connection.cursor(dictionary=True) - - if request.method == 'POST': - # ✅ Form submitted - update all fields - data = request.form - cursor.execute(""" - UPDATE purchase_order - SET purchase_date = %s, - supplier_name = %s, - purchase_order_no = %s, - item_name = %s, - quantity = %s, - unit = %s, - rate = %s, - amount = %s, - GST_Amount = %s, - TDS = %s, - final_amount = %s - WHERE purchase_id = %s - """, ( - data['purchase_date'], data['supplier_name'], data['purchase_order_no'], data['item_name'], - data['quantity'], - data['unit'], data['rate'], data['amount'], data['GST_Amount'], data['TDS'], data['final_amount'], - id - )) - connection.commit() - cursor.close() - connection.close() - LogHelper.log_action("Delete purchase order", f"User {current_user.id} Deleted puirchase Order'{ id}'") - return redirect(url_for('show_purchase_orders')) - - # Show edit form - cursor.execute("SELECT * FROM purchase_order WHERE purchase_id = %s", (id,)) - purchase = cursor.fetchone() - cursor.close() - connection.close() - return render_template('edit_purchase.html', purchase=purchase) - - -# SHOW all GRNs + ADD form -@app.route('/grn', methods=['GET']) -def grn_page(): - connection = config.get_db_connection() - cursor = connection.cursor(dictionary=True) - - # Fetch purchase orders for dropdown - cursor.execute("SELECT purchase_id, supplier_name FROM purchase_order") - purchase_orders = cursor.fetchall() - - # Fetch all GRNs to display - cursor.execute("SELECT * FROM goods_receive_note") - grns = cursor.fetchall() - print(grns) - cursor.close() - connection.close() - - # Render the template with both datasets - return render_template('grn_form.html', purchase_orders=purchase_orders, grns=grns) - - -# ADD new GRN -@app.route('/add_grn', methods=['POST', 'GET']) -def add_grn(): - data = request.form - connection = config.get_db_connection() - cursor = connection.cursor() - query = """ - INSERT INTO goods_receive_note - (grn_date, purchase_id, supplier_name, item_description, received_quantity, unit, rate, amount, remarks) - VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) \ - """ - cursor.execute(query, ( - data.get('grn_date'), - data.get('purchase_id'), - data.get('supplier_name'), - data.get('item_description'), - data.get('received_quantity'), - data.get('unit'), - data.get('rate'), - data.get('amount'), - data.get('remarks') - )) - connection.commit() - - cursor.execute("SELECT * FROM goods_receive_note") - grns = cursor.fetchall() - print(grns) - query = "select * from purchase_order" - cursor.execute(query) - purchase_orders = cursor.fetchall() - cursor.close() - connection.close() - return render_template('grn_form.html', purchase_orders=purchase_orders, grns=grns) - - -# UPDATE GRN -@app.route('/update_grn/', methods=['GET', 'POST']) -def update_grn(grn_id): - connection = config.get_db_connection() - cursor = connection.cursor(dictionary=True) - - if request.method == 'POST': - data = request.form - query = """ - UPDATE goods_receive_note - SET grn_date=%s, purchase_id=%s, supplier_name=%s, item_description=%s, - received_quantity=%s, unit=%s, rate=%s, amount=%s, remarks=%s - WHERE grn_id=%s - """ - cursor.execute(query, ( - data['grn_date'], data['purchase_id'], data['supplier_name'], - data['item_description'], data['received_quantity'], data['unit'], - data['rate'], data['amount'], data['remarks'], grn_id - )) - connection.commit() - cursor.close() - connection.close() - return redirect(url_for('grns')) - - cursor.execute("SELECT * FROM goods_receive_note WHERE grn_id = %s", (grn_id,)) - grn = cursor.fetchone() - cursor.close() - connection.close() - return render_template("edit_grn.html", grn=grn) - - -# DELETE GRN -@app.route('/delete_grn/', methods=['POST']) -def delete_grn(grn_id): - connection = config.get_db_connection() - cursor = connection.cursor() - cursor.execute("DELETE FROM goods_receive_note WHERE grn_id = %s", (grn_id,)) - connection.commit() - cursor.close() - connection.close() - return render_template("grn_form.html") - - -@app.route('/work_order_report', methods=['GET']) -def work_order_report(): - return render_template('work_order_report.html') - - - - -# ✅ Vendor Name Search (for Select2) -@app.route('/get_vendor_names') -def get_vendor_names(): - query = request.args.get('q', '') - connection = config.get_db_connection() - cursor = connection.cursor() - cursor.execute("SELECT DISTINCT vendor_name FROM work_order WHERE vendor_name LIKE %s", (f"%{query}%",)) - vendors = [row[0] for row in cursor.fetchall()] - cursor.close() - connection.close() - return jsonify(vendors) - - -# ✅ Work Order Number Search (with or without vendor) -@app.route('/get_work_order_numbers') -def get_work_order_numbers(): - vendor = request.args.get('vendor_name', '') - query = request.args.get('q', '') - connection = config.get_db_connection() - cursor = connection.cursor() - - if vendor: - cursor.execute( - "SELECT DISTINCT work_order_number FROM work_order WHERE vendor_name = %s AND work_order_number LIKE %s", - (vendor, f"%{query}%")) - else: - cursor.execute("SELECT DISTINCT work_order_number FROM work_order WHERE work_order_number LIKE %s", - (f"%{query}%",)) - - orders = [row[0] for row in cursor.fetchall()] - cursor.close() - connection.close() - return jsonify(orders) - - -# ✅ Get Work Order Data (Filtered) -@app.route('/get_work_order_data') -def get_work_order_data(): - vendor = request.args.get('vendor_name') - order_number = request.args.get('work_order_number') - - query = "SELECT * FROM work_order WHERE 1=1" - params = [] - - if vendor: - query += " AND vendor_name = %s" - params.append(vendor) - if order_number: - query += " AND work_order_number = %s" - params.append(order_number) - - connection = config.get_db_connection() - cursor = connection.cursor(dictionary=True) - cursor.execute(query, tuple(params)) - data = cursor.fetchall() - cursor.close() - connection.close() - return jsonify(data) - - - - -@app.route('/download_work_order_report') -def download_work_order_report(): - vendor_name = request.args.get('vendor_name') - work_order_number = request.args.get('work_order_number') - - if work_order_number == "null": - work_order_number = None - - query = "SELECT * FROM work_order WHERE 1=1" - params = [] - - if vendor_name: - query += " AND vendor_name = %s" - params.append(vendor_name) - if work_order_number: - query += " AND work_order_number = %s" - params.append(work_order_number) - - conn = config.get_db_connection() - cursor = conn.cursor(dictionary=True) - cursor.execute(query, tuple(params)) - data = cursor.fetchall() - cursor.close() - conn.close() - - if not data: - return "No data found for the selected filters", 404 - - # Convert to DataFrame - df = pd.DataFrame(data) - - output_path = 'static/downloads/work_order_report.xlsx' - os.makedirs(os.path.dirname(output_path), exist_ok=True) - df.to_excel(output_path, index=False, startrow=2) # Leave space for heading - - # Load workbook for styling - wb = load_workbook(output_path) - ws = wb.active - - # Add a merged title - title = "Work Order Report" - ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=len(df.columns)) - title_cell = ws.cell(row=1, column=1) - title_cell.value = title - title_cell.font = Font(size=14, bold=True, color="1F4E78") - title_cell.alignment = Alignment(horizontal="center", vertical="center") - - # Style header row (row 3 because data starts from row 3) - header_font = Font(bold=True) - for col_num, column_name in enumerate(df.columns, 1): - cell = ws.cell(row=3, column=col_num) - cell.font = header_font - cell.alignment = Alignment(horizontal="center", vertical="center") - # Optional: adjust column width - max_length = max(len(str(column_name)), 12) - ws.column_dimensions[get_column_letter(col_num)].width = max_length + 2 - - wb.save(output_path) - return send_file(output_path, as_attachment=True) - - -@app.route('/purchase_order_report', methods=['GET']) -def purchase_order_report(): - return render_template('purchase_order_report.html') - - -@app.route('/get_supplier_names') -def get_supplier_names(): - query = request.args.get('q', '') # Get the search term from Select2 - connection = config.get_db_connection() - cursor = connection.cursor() - - # Fetch distinct supplier names that match the search query - cursor.execute( - "SELECT supplier_name FROM purchase_order WHERE supplier_name LIKE %s", - (f"%{query}%",) - ) - suppliers = [row[0] for row in cursor.fetchall()] - - cursor.close() - connection.close() - return jsonify(suppliers) - - -@app.route('/get_purchase_order_numbers') -def get_purchase_order_numbers(): - supplier = request.args.get('supplier_name', '') - query = request.args.get('q', '') - - connection = config.get_db_connection() - cursor = connection.cursor() - - if supplier: - cursor.execute(""" - SELECT purchase_order_no - FROM purchase_order - WHERE supplier_name = %s AND purchase_order_no LIKE %s - """, (supplier, f"%{query}%")) - else: - cursor.execute(""" - SELECT purchase_order_no - FROM purchase_order - WHERE purchase_order_no LIKE %s - """, (f"%{query}%",)) - - orders = [row[0] for row in cursor.fetchall()] - cursor.close() - connection.close() - return jsonify(orders) - - -@app.route('/get_purchase_order_data') -def get_purchase_order_data(): - supplier = request.args.get('supplier_name') - order_no = request.args.get('purchase_order_no') - - query = "SELECT * FROM purchase_order WHERE 1=1" - params = [] - - if supplier: - query += " AND supplier_name = %s" - params.append(supplier) - if order_no: - query += " AND purchase_order_no = %s" - params.append(order_no) - - connection = config.get_db_connection() - cursor = connection.cursor(dictionary=True) - cursor.execute(query, tuple(params)) - data = cursor.fetchall() - cursor.close() - connection.close() - return jsonify(data) - - -@app.route('/download_purchase_order_report') -def download_purchase_order_report(): - supplier_name = request.args.get('supplier_name') - purchase_order_no = request.args.get('purchase_order_no') - - if purchase_order_no == "null": - purchase_order_no = None - LogHelper.log_action("Download purchase order", f"User {current_user.id} Download puirchase Order'{ purchase_order_no}'") - query = "SELECT * FROM purchase_order WHERE 1=1" - params = [] - - if supplier_name: - query += " AND supplier_name = %s" - params.append(supplier_name) - if purchase_order_no: - query += " AND purchase_order_no = %s" - params.append(purchase_order_no) - - conn = config.get_db_connection() - cursor = conn.cursor(dictionary=True) - cursor.execute(query, tuple(params)) - data = cursor.fetchall() - cursor.close() - conn.close() - - if not data: - return "No data found for the selected filters", 404 - - # Convert to DataFrame - df = pd.DataFrame(data) - - output_path = 'static/downloads/purchase_order_report.xlsx' - os.makedirs(os.path.dirname(output_path), exist_ok=True) - - df.to_excel(output_path, index=False, startrow=2) # Reserve space for heading - - # Load workbook for styling - wb = load_workbook(output_path) - ws = wb.active - - # Add a merged title - title = "Purchase Order Report" - ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=len(df.columns)) - title_cell = ws.cell(row=1, column=1) - title_cell.value = title - title_cell.font = Font(size=14, bold=True, color="1F4E78") - title_cell.alignment = Alignment(horizontal="center", vertical="center") - - # Style header row (row 3 because data starts from row 3) - header_font = Font(bold=True) - for col_num, column_name in enumerate(df.columns, 1): - cell = ws.cell(row=3, column=col_num) - cell.font = header_font - cell.alignment = Alignment(horizontal="center", vertical="center") - max_length = max(len(str(column_name)), 12) - ws.column_dimensions[get_column_letter(col_num)].width = max_length + 2 - - wb.save(output_path) - return send_file(output_path, as_attachment=True) - - + return render_template("index.html") + +# ---------------- Register Blueprints ---------------- + +app.register_blueprint(auth_bp) +app.register_blueprint(log_bp) +app.register_blueprint(state_bp) +app.register_blueprint(district_bp) +app.register_blueprint(block_bp) +app.register_blueprint(village_bp) +app.register_blueprint(invoice_bp) +app.register_blueprint(subcontractor_bp) +app.register_blueprint(payment_bp) +app.register_blueprint(gst_release_bp) +app.register_blueprint(excel_bp) +app.register_blueprint(report_bp) +app.register_blueprint(pmc_report_bp) +app.register_blueprint(hold_bp) + +# ---------------- Run App ---------------- if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True) + + diff --git a/model/Auth.py b/model/Auth.py new file mode 100644 index 0000000..e1b9cd4 --- /dev/null +++ b/model/Auth.py @@ -0,0 +1,63 @@ +import os +from dotenv import load_dotenv +from flask_login import UserMixin +from ldap3 import Server, Connection, ALL +from ldap3.core.exceptions import LDAPBindError + +# Load .env +load_dotenv() + + +class DefaultCredentials: + username = os.getenv("DEFAULT_USERNAME") + password = os.getenv("DEFAULT_PASSWORD") + + +class LoginLDAP: + + def __init__(self, request): + + self.username = request.form.get("username", "").strip() + self.password = request.form.get("password", "") + + self.isDefaultCredentials = False + self.isValidLogin = False + self.errorMessage = "" + + ldap_server = "ldap://localhost:389" + ldap_user_dn = f"uid={self.username},ou=users,dc=lcepl,dc=org" + + # fallback admin login + if ( + self.username == DefaultCredentials.username + and self.password == DefaultCredentials.password + ): + self.isDefaultCredentials = True + self.isValidLogin = True + return + + try: + + server = Server(ldap_server, get_info=ALL) + + conn = Connection( + server, + user=ldap_user_dn, + password=self.password, + auto_bind=True + ) + + if conn.bound: + self.isValidLogin = True + + except LDAPBindError: + self.errorMessage = "Invalid LDAP credentials" + + except Exception as e: + self.errorMessage = str(e) + + +class User(UserMixin): + + def __init__(self, username): + self.id = username \ No newline at end of file diff --git a/model/Block.py b/model/Block.py new file mode 100644 index 0000000..c963d28 --- /dev/null +++ b/model/Block.py @@ -0,0 +1,165 @@ +from flask import Flask, render_template, request, redirect, url_for, send_from_directory, flash, jsonify, json +from flask import current_app + +from datetime import datetime +from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user + +from model.Utilities import RegEx, ResponseHandler, HtmlHelper, ItemCRUDType +from model.Log import LogData, LogHelper + +import os +import config +import re + +import mysql.connector +from mysql.connector import Error + +from model.ItemCRUD import ItemCRUD, itemCRUDMapping + + +class Block: + + isSuccess = False + resultMessage = "" + + def __init__(self): + self.isSuccess = False + self.resultMessage = "" + + # ---------------------------------------------------------- + # Add Block + # ---------------------------------------------------------- + def AddBlock(self, request): + + block = ItemCRUD(itemType=ItemCRUDType.Block) + + district_id = request.form.get('district_Id') + block_name = request.form.get('block_Name', '').strip() + + block.AddItem(request=request, parentid=district_id, childname=block_name, storedprocfetch="GetBlockByNameAndDistricts", storedprocadd="SaveBlock" ) + self.isSuccess = block.isSuccess + self.resultMessage = block.resultMessage + return + + # ---------------------------------------------------------- + # Get All Blocks + # ---------------------------------------------------------- + # def GetAllBlocks(self): + + # block = ItemCRUD(itemType=ItemCRUDType.Block) + # blocksdata = block.GetAllData(request=request, storedproc="GetAllBlock") + # self.isSuccess = block.isSuccess + # self.resultMessage = block.resultMessage + # return blocksdata + + def GetAllBlocks(self, request): + + block = ItemCRUD(itemType=ItemCRUDType.Block) + blocksdata = block.GetAllData(request=request, storedproc="GetAllBlock") + + self.isSuccess = block.isSuccess + self.resultMessage = block.resultMessage + return blocksdata + + # ---------------------------------------------------------- + # Check Block Exists + # ---------------------------------------------------------- + + # def CheckBlock(self, request): + # block = ItemCRUD(itemType=ItemCRUDType.Block) + # block_name = request.json.get('block_Name', '').strip() + # district_id = request.json.get('district_Id') + # result = block.CheckItem(request=request, parentid=district_id, childname=block_name, storedprocfetch="GetBlockByNameAndDistrict") + # self.isSuccess = block.isSuccess + # self.resultMessage = block.resultMessage + # return result + def CheckBlock(self, request): + block = ItemCRUD(itemType=ItemCRUDType.Block) + data = request.get_json(silent=True) or request.form + block_name = (data.get('block_Name') or '').strip() + district_id = data.get('district_Id') + + result = block.CheckItem( + request=request, + parentid=district_id, + childname=block_name, + storedprocfetch="GetBlockByNameAndDistrict" + ) + self.isSuccess = block.isSuccess + self.resultMessage = block.resultMessage + return result + + # ---------------------------------------------------------- + # Get Block By ID + # ---------------------------------------------------------- + # def GetBlockByID(self, id): + + # block = ItemCRUD(itemType=ItemCRUDType.Village) + # blockdata = block.GetAllData(id=id,storedproc="GetBlockDataByID") + # self.isSuccess = block.isSuccess + # self.resultMessage = block.resultMessage + # print("akash"+blockdata) + # return blockdata + + # def GetBlockByID(self,request,id): + # block = ItemCRUD(itemType=ItemCRUDType.Block) + # blockdata = block.GetDataByID(request=request,id=id,storedproc="GetBlockDataByID") + # self.isSuccess = block.isSuccess + # self.resultMessage = block.resultMessage + # return blockdata + def GetBlockByID(self, id): + + block = ItemCRUD(itemType=ItemCRUDType.Block) + + blockdata = block.GetDataByID( + id=id, + storedproc="GetBlockDataByID" + ) + + self.isSuccess = block.isSuccess + self.resultMessage = block.resultMessage + + return blockdata + # ---------------------------------------------------------- + # Update Block + # ---------------------------------------------------------- + # def EditBlock(self, request, block_id): + + # block = ItemCRUD(itemType=ItemCRUDType.Block) + + # district_id = request.form.get('district_Id') + # block_name = request.form.get('block_Name', '').strip() + + # block.EditItem(request=request, childid=block_id, parentid=district_id, childname=block_name, storedprocadd="UpdateBlockById" ) + # self.isSuccess = block.isSuccess + # self.resultMessage = block.resultMessage + # return + def EditBlock(self, request, block_id): + + block = ItemCRUD(itemType=ItemCRUDType.Block) + + district_id = request.form.get('district_Id') + block_name = request.form.get('block_Name', '').strip() + + block.EditItem( + request=request, + childid=block_id, + parentid=district_id, + childname=block_name, + storedprocupdate="UpdateBlockById" + ) + + self.isSuccess = block.isSuccess + self.resultMessage = block.resultMessage + return render_template('add_block.html') + + # ---------------------------------------------------------- + # Delete Block + # --------------------------------------------------------- + def DeleteBlock(self,request, id): + block = ItemCRUD(itemType=ItemCRUDType.Block) + + block.DeleteItem(request=request,itemID=id, storedprocDelete="DeleteBlock") + self.isSuccess = block.isSuccess + self.resultMessage = block.resultMessage + return \ No newline at end of file diff --git a/AppCode/ContractorInfo.py b/model/ContractorInfo.py similarity index 57% rename from AppCode/ContractorInfo.py rename to model/ContractorInfo.py index 88fa7f5..a4eb36b 100644 --- a/AppCode/ContractorInfo.py +++ b/model/ContractorInfo.py @@ -19,12 +19,10 @@ class ContractorInfo: def fetchData(self): try: connection = config.get_db_connection() - cursor = connection.cursor(dictionary=True, buffered=True) - print("here", flush=True) - - cursor.callproc('GetContractorInfoById', [self.contInfo]) - #self.contInfo = next(cursor.stored_results()).fetchone() - self.contInfo = cursor.fetchone() + cursor = connection.cursor(dictionary=True, buffered=True) + cursor.callproc('GetContractorInfoById', [self.ID]) + for result in cursor.stored_results(): + self.contInfo = result.fetchone() print(self.contInfo,flush=True) finally: @@ -40,25 +38,23 @@ class ContractorInfo: # ---------------- Hold Types ---------------- - cursor.execute(""" - SELECT DISTINCT ht.hold_type_id, ht.hold_type - FROM invoice_subcontractor_hold_join h - JOIN hold_types ht ON h.hold_type_id = ht.hold_type_id - WHERE h.Contractor_Id = %s - """, (self.contractor_id,)) - hold_types = cursor.fetchall() + cursor = connection.cursor(dictionary=True) + + cursor.callproc('GetHoldTypesByContractor', [self.ID]) + + hold_types = [] + for result in cursor.stored_results(): + hold_types = result.fetchall() hold_type_map = {ht['hold_type_id']: ht['hold_type'] for ht in hold_types} # ---------------- Invoices ---------------- - cursor.execute(""" - SELECT i.*, v.Village_Name - FROM assign_subcontractors asg - INNER JOIN invoice i ON i.PMC_No = asg.PMC_No AND i.Village_Id = asg.Village_Id - LEFT JOIN villages v ON i.Village_Id = v.Village_Id - WHERE asg.Contractor_Id = %s - ORDER BY i.PMC_No, i.Invoice_No - """, (self.contractor_id,)) - invoices = cursor.fetchall() + cursor = connection.cursor(dictionary=True) + + cursor.callproc('GetInvoicesByContractor', [self.ID]) + + invoices = [] + for result in cursor.stored_results(): + invoices = result.fetchall() # Remove duplicate invoices invoice_ids_seen = set() diff --git a/AppCode/District.py b/model/District.py similarity index 63% rename from AppCode/District.py rename to model/District.py index 4402b3e..14ecea5 100644 --- a/AppCode/District.py +++ b/model/District.py @@ -4,8 +4,8 @@ from flask import current_app from datetime import datetime from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user -from AppCode.Utilities import RegEx, ResponseHandler, HtmlHelper, ItemCRUDType -from AppCode.Log import LogData, LogHelper +from model.Utilities import RegEx, ResponseHandler, HtmlHelper, ItemCRUDType +from model.Log import LogData, LogHelper import os import config @@ -14,7 +14,7 @@ import re import mysql.connector from mysql.connector import Error -from AppCode.ItemCRUD import ItemCRUD +from model.ItemCRUD import ItemCRUD class District: isSuccess = False @@ -31,13 +31,13 @@ class District: district_name = request.form['district_Name'].strip() state_id = request.form['state_Id'] - district.EditItem(request=request, childid=district_id, parentid=state_id, childname=district_name, storedprocadd="UpdateBlockById" ) + district.EditItem(request=request, childid=district_id, parentid=state_id, childname=district_name,storedprocupdate="UpdateDistrict" ) self.isSuccess = district.isSuccess self.resultMessage = district.resultMessage return - def AddDistrict(self, request): + def AddDistrict(self, request): district = ItemCRUD(ItemCRUDType.District) @@ -69,11 +69,32 @@ class District: return result - def GetDistrictByID(self, request, id): - district = ItemCRUD(itemType=ItemCRUDType.Village) - districtdata = district.GetAllData("GetDistrictDataByID") - self.isSuccess = district.isSuccess - self.resultMessage = district.resultMessage + # def GetDistrictByID(self, request,district_id): + # district = ItemCRUD(itemType=ItemCRUDType.District) + # districtdata = district.GetAllData(id=district_id,storedproc="GetDistrictDataByID") + # self.isSuccess = district.isSuccess + # self.resultMessage = district.resultMessage + # return districtdata + def GetDistrictByID(self, request, district_id): + district = ItemCRUD(itemType=ItemCRUDType.District) + + districtdata = district.GetDataByID( + id=district_id, + storedproc="GetDistrictDataByID" + ) + + if districtdata: + self.isSuccess = True + else: + self.isSuccess = False + self.resultMessage = "District not found" + return districtdata - \ No newline at end of file + +#Delete District + def DeleteDistrict(self, request, district_id): + district = ItemCRUD(itemType=ItemCRUDType.District) + district.DeleteItem(request=request,itemID=district_id,storedprocDelete="DeleteDistrict") + self.isSuccess = district.isSuccess + self.resultMessage = str(district.resultMessage) \ No newline at end of file diff --git a/model/FolderAndFile.py b/model/FolderAndFile.py new file mode 100644 index 0000000..65dedef --- /dev/null +++ b/model/FolderAndFile.py @@ -0,0 +1,39 @@ +import os +from flask import current_app + + +class FolderAndFile: + + # ----------------------------- + # BASE FOLDER METHODS + # ----------------------------- + @staticmethod + def get_download_folder(): + folder = os.path.join(current_app.root_path, "static", "downloads") + + if not os.path.exists(folder): + os.makedirs(folder) + + os.makedirs(folder, exist_ok=True) + return folder + + @staticmethod + def get_upload_folder(): + folder = os.path.join(current_app.root_path, "static", "uploads") + + if not os.path.exists(folder): + os.makedirs(folder) + + os.makedirs(folder, exist_ok=True) + return folder + + # ----------------------------- + # FILE PATH METHODS + # ----------------------------- + @staticmethod + def get_download_path(filename): + return os.path.join(FolderAndFile.get_download_folder(), filename) + + @staticmethod + def get_upload_path(filename): + return os.path.join(FolderAndFile.get_upload_folder(), filename) \ No newline at end of file diff --git a/model/GST.py b/model/GST.py new file mode 100644 index 0000000..a908dd9 --- /dev/null +++ b/model/GST.py @@ -0,0 +1,55 @@ +import config + +class GST: + + @staticmethod + def get_unreleased_gst(): + + connection = config.get_db_connection() + cursor = connection.cursor(dictionary=True) + + try: + # ----------- Invoices ----------- + cursor.callproc('GetAllInvoicesBasic') + invoices = [] + for result in cursor.stored_results(): + invoices = result.fetchall() + + + # ----------- GST Releases ----------- + cursor.callproc('GetAllGSTReleasesBasic') + gst_releases = [] + for result in cursor.stored_results(): + gst_releases = result.fetchall() + + gst_invoice_nos = { + g['Invoice_No'] + for g in gst_releases + if g['Invoice_No'] + } + + gst_basic_amounts = { + float(g['Basic_Amount']) + for g in gst_releases + if g['Basic_Amount'] is not None + } + + unreleased = [] + + for inv in invoices: + + match_by_invoice = inv['Invoice_No'] in gst_invoice_nos + + match_by_gst_amount = float( + inv.get('GST_SD_Amount') or 0 + ) in gst_basic_amounts + + if not (match_by_invoice or match_by_gst_amount): + unreleased.append(inv) + + return unreleased + + finally: + cursor.close() + connection.close() + \ No newline at end of file diff --git a/model/HoldTypes.py b/model/HoldTypes.py new file mode 100644 index 0000000..b8a9a46 --- /dev/null +++ b/model/HoldTypes.py @@ -0,0 +1,90 @@ +from flask import request +from model.ItemCRUD import ItemCRUD +from model.Utilities import ItemCRUDType + +class HoldTypes: + """CRUD operations for Hold Types using ItemCRUD""" + + def __init__(self): + self.isSuccess = False + self.resultMessage = "" + + # ------------------- Add Hold Type ------------------- + def AddHoldType(self, request): + hold = ItemCRUD(itemType=ItemCRUDType.HoldType) + hold_name = request.form.get('hold_type', '').strip() + + hold.AddItem( + request=request, + parentid=None, + childname=hold_name, + storedprocfetch="CheckHoldTypeExists", + storedprocadd="SaveHoldType" + ) + + self.isSuccess = hold.isSuccess + self.resultMessage = hold.resultMessage + + # ------------------- Get All Hold Types ------------------- + def GetAllHoldTypes(self, request=None): + hold = ItemCRUD(itemType=ItemCRUDType.HoldType) + rows = hold.GetAllData(request=request, storedproc="GetAllHoldTypes") + + self.isSuccess = hold.isSuccess + self.resultMessage = hold.resultMessage + + # ✅ Convert tuple → dictionary + data = [] + for row in rows: + data.append({ + "hold_type_id": row[0], + "hold_type": row[1] + }) + + return data + + # ------------------- Get Hold Type By ID ------------------- + def GetHoldTypeByID(self, hold_type_id): + hold = ItemCRUD(itemType=ItemCRUDType.HoldType) + row = hold.GetDataByID(hold_type_id, storedproc="GetHoldTypesById") + + self.isSuccess = hold.isSuccess + self.resultMessage = hold.resultMessage + + # ✅ Convert tuple → dictionary + if row: + return { + "hold_type_id": row[0], + "hold_type": row[1] + } + + return None + + # ------------------- Update Hold Type ------------------- + def EditHoldType(self, request, hold_type_id): + hold = ItemCRUD(itemType=ItemCRUDType.HoldType) + hold_name = request.form.get('hold_type', '').strip() + + hold.EditItem( + request=request, + childid=hold_type_id, + parentid=None, + childname=hold_name, + storedprocupdate="UpdateHoldTypeById" + ) + + self.isSuccess = hold.isSuccess + self.resultMessage = hold.resultMessage + + # ------------------- Delete Hold Type ------------------- + def DeleteHoldType(self, request, hold_type_id): + hold = ItemCRUD(itemType=ItemCRUDType.HoldType) + + hold.DeleteItem( + request=request, + itemID=hold_type_id, + storedprocDelete="DeleteHoldType" + ) + + self.isSuccess = hold.isSuccess + self.resultMessage = hold.resultMessage \ No newline at end of file diff --git a/model/Invoice.py b/model/Invoice.py new file mode 100644 index 0000000..ac148ac --- /dev/null +++ b/model/Invoice.py @@ -0,0 +1,379 @@ +import config +import mysql.connector + +# ------------------- Helper ------------------- +def clear_results(cursor): + for r in cursor.stored_results(): + r.fetchall() + + +# ------------------- Get Village Id ------------------- +def get_village_id(village_name): + connection = config.get_db_connection() + cursor = connection.cursor(dictionary=True) + + cursor.callproc("GetVillageIdByName", (village_name,)) + village_result = None + + for rs in cursor.stored_results(): + village_result = rs.fetchone() + + cursor.close() + connection.close() + return village_result + + +# ------------------- Insert Invoice ------------------- +def insert_invoice(data, village_id): + connection = config.get_db_connection() + cursor = connection.cursor(dictionary=True) + + try: + # 1. Insert Invoice + cursor.callproc('InsertInvoice', [ + data.get('pmc_no'), + village_id, + data.get('work_type'), + data.get('invoice_details'), + data.get('invoice_date'), + data.get('invoice_no'), + float(data.get('basic_amount') or 0), + float(data.get('debit_amount') or 0), + float(data.get('after_debit_amount') or 0), + float(data.get('amount') or 0), + float(data.get('gst_amount') or 0), + float(data.get('tds_amount') or 0), + float(data.get('sd_amount') or 0), + float(data.get('on_commission') or 0), + float(data.get('hydro_testing') or 0), + float(data.get('gst_sd_amount') or 0), + float(data.get('final_amount') or 0) + ]) + + invoice_id = None + for result in cursor.stored_results(): + row = result.fetchone() + if row: + invoice_id = row.get('invoice_id') + + if not invoice_id: + raise Exception("Invoice ID not returned") + + # 2. Insert Inpayment + cursor.callproc('InsertInpayment', [ + data.get('pmc_no'), + village_id, + data.get('work_type'), + data.get('invoice_details'), + data.get('invoice_date'), + data.get('invoice_no'), + float(data.get('basic_amount') or 0), + float(data.get('debit_amount') or 0), + float(data.get('after_debit_amount') or 0), + float(data.get('amount') or 0), + float(data.get('gst_amount') or 0), + float(data.get('tds_amount') or 0), + float(data.get('sd_amount') or 0), + float(data.get('on_commission') or 0), + float(data.get('hydro_testing') or 0), + float(data.get('gst_sd_amount') or 0), + float(data.get('final_amount') or 0), + data.get('subcontractor_id') + ]) + clear_results(cursor) + + connection.commit() + return invoice_id + + except Exception as e: + connection.rollback() + raise e + + finally: + cursor.close() + connection.close() + + +# ------------------- Assign Subcontractor ------------------- +def assign_subcontractor(data, village_id): + connection = config.get_db_connection() + cursor = connection.cursor(dictionary=True) + + try: + cursor.callproc('AssignSubcontractor', [ + data.get('pmc_no'), + data.get('subcontractor_id'), + village_id + ]) + clear_results(cursor) + + connection.commit() + + except Exception as e: + connection.rollback() + raise e + + finally: + cursor.close() + connection.close() + + +# ------------------- Insert Hold Types ------------------- +def insert_hold_types(data, invoice_id): + connection = config.get_db_connection() + cursor = connection.cursor(dictionary=True) + + try: + hold_types = data.getlist('hold_type[]') + hold_amounts = data.getlist('hold_amount[]') + + for hold_type, hold_amount in zip(hold_types, hold_amounts): + if not hold_type: + continue + + cursor.callproc('GetHoldTypeIdByName', [hold_type]) + hold_type_result = None + + for result in cursor.stored_results(): + hold_type_result = result.fetchone() + + if not hold_type_result: + cursor.callproc('InsertHoldType', [hold_type, 0]) + cursor.execute("SELECT @_InsertHoldType_1") + hold_type_id = cursor.fetchone()[0] + else: + hold_type_id = hold_type_result['hold_type_id'] + + hold_amount = float(hold_amount or 0) + + cursor.callproc('InsertInvoiceSubcontractorHold', [ + data.get('subcontractor_id'), + invoice_id, + hold_type_id, + hold_amount + ]) + clear_results(cursor) + + connection.commit() + + except Exception as e: + connection.rollback() + raise e + + finally: + cursor.close() + connection.close() + + +# ------------------- Get All Invoices ------------------- +def get_all_invoice_details(): + connection = config.get_db_connection() + cursor = connection.cursor(dictionary=True) + + cursor.callproc('GetAllInvoiceDetails') + invoices = [] + + for result in cursor.stored_results(): + invoices = result.fetchall() + + cursor.close() + connection.close() + return invoices + + +# ------------------- Get All Villages ------------------- +def get_all_villages(): + connection = config.get_db_connection() + cursor = connection.cursor(dictionary=True) + + cursor.callproc("GetAllVillages") + villages = [] + + for result in cursor.stored_results(): + villages = result.fetchall() + + cursor.close() + connection.close() + return villages + + +# ------------------- Search Contractors ------------------- +def search_contractors(sub_query): + connection = config.get_db_connection() + cursor = connection.cursor(dictionary=True) + + cursor.callproc('SearchContractorsByName', [sub_query]) + results = [] + + for result in cursor.stored_results(): + results = result.fetchall() + + cursor.close() + connection.close() + return results + + +# ------------------- Get All Hold Types ------------------- +def get_all_hold_types(): + connection = config.get_db_connection() + cursor = connection.cursor(dictionary=True) + + cursor.callproc("GetAllHoldTypes") + hold_types = [] + + for result in cursor.stored_results(): + hold_types = result.fetchall() + + cursor.close() + connection.close() + return hold_types + + +# ------------------- Get Invoice By Id ------------------- +def get_invoice_by_id(invoice_id): + connection = config.get_db_connection() + cursor = connection.cursor(dictionary=True) + + cursor.callproc('GetInvoiceDetailsById', [invoice_id]) + invoice = None + + for result in cursor.stored_results(): + invoice = result.fetchone() + + cursor.callproc('GetHoldAmountsByInvoiceId', [invoice_id]) + hold_amounts = [] + + for result in cursor.stored_results(): + hold_amounts = result.fetchall() + + if invoice: + invoice["hold_amounts"] = hold_amounts + + cursor.close() + connection.close() + return invoice + + +# ------------------- Update Invoice ------------------- +def update_invoice(data, invoice_id): + connection = config.get_db_connection() + cursor = connection.cursor(dictionary=True) + + try: + cursor.callproc("GetVillageIdByName", (data.get('village'),)) + village = None + + for rs in cursor.stored_results(): + village = rs.fetchone() + + village_id = village['Village_Id'] + + numeric = [ + float(data.get('basic_amount') or 0), + float(data.get('debit_amount') or 0), + float(data.get('after_debit_amount') or 0), + float(data.get('amount') or 0), + float(data.get('gst_amount') or 0), + float(data.get('tds_amount') or 0), + float(data.get('sd_amount') or 0), + float(data.get('on_commission') or 0), + float(data.get('hydro_testing') or 0), + float(data.get('gst_sd_amount') or 0), + float(data.get('final_amount') or 0), + ] + + cursor.callproc('UpdateInvoice', [ + data.get('pmc_no'), + village_id, + data.get('work_type'), + data.get('invoice_details'), + data.get('invoice_date'), + data.get('invoice_no'), + *numeric, + invoice_id + ]) + clear_results(cursor) + + connection.commit() + + except Exception as e: + connection.rollback() + raise e + + finally: + cursor.close() + connection.close() + + +# ------------------- Update Inpayment ------------------- +def update_inpayment(data): + connection = config.get_db_connection() + cursor = connection.cursor(dictionary=True) + + try: + numeric = [ + float(data.get('basic_amount') or 0), + float(data.get('debit_amount') or 0), + float(data.get('after_debit_amount') or 0), + float(data.get('amount') or 0), + float(data.get('gst_amount') or 0), + float(data.get('tds_amount') or 0), + float(data.get('sd_amount') or 0), + float(data.get('on_commission') or 0), + float(data.get('hydro_testing') or 0), + float(data.get('gst_sd_amount') or 0), + float(data.get('final_amount') or 0), + ] + + cursor.callproc('UpdateInpayment', [ + data.get('work_type'), + data.get('invoice_details'), + data.get('invoice_date'), + *numeric, + data.get('pmc_no'), + data.get('invoice_no') + ]) + clear_results(cursor) + + connection.commit() + + except Exception as e: + connection.rollback() + raise e + + finally: + cursor.close() + connection.close() + + +# ------------------- Delete Invoice ------------------- +def delete_invoice_data(invoice_id, user_id): + connection = config.get_db_connection() + cursor = connection.cursor(dictionary=True) + + try: + cursor.callproc('GetInvoicePMCById', [invoice_id]) + + record = {} + for result in cursor.stored_results(): + record = result.fetchone() or {} + if not record: + raise Exception("Invoice not found") + + cursor.callproc("DeleteInvoice", (invoice_id,)) + clear_results(cursor) + + cursor.callproc( + 'DeleteInpaymentByPMCInvoice', + [record['PMC_No'], record['invoice_no']] + ) + + connection.commit() + + except Exception as e: + connection.rollback() + raise e + + finally: + cursor.close() + connection.close() \ No newline at end of file diff --git a/model/ItemCRUD.py b/model/ItemCRUD.py new file mode 100644 index 0000000..19f8dfb --- /dev/null +++ b/model/ItemCRUD.py @@ -0,0 +1,359 @@ +from flask_login import current_user +from model.Utilities import RegEx, ResponseHandler, HtmlHelper, ItemCRUDType +from model.Log import LogHelper + +import config +import re +import mysql.connector + + +# ---------------------------------------------------------- +# Mapping Class +# ---------------------------------------------------------- +class itemCRUDMapping: + + def __init__(self, itemType): + if itemType is ItemCRUDType.Village: + self.name = "Village" + elif itemType is ItemCRUDType.Block: + self.name = "Block" + elif itemType is ItemCRUDType.State: + self.name = "State" + elif itemType is ItemCRUDType.HoldType: + self.name = "Hold Type" + elif itemType is ItemCRUDType.Subcontractor: + self.name = "Subcontractor" + else: + self.name = "Item" + + +# ---------------------------------------------------------- +# Generic CRUD Class +# ---------------------------------------------------------- +class ItemCRUD: + + def __init__(self, itemType): + self.isSuccess = False + self.resultMessage = "" + self.itemCRUDType = itemType + self.itemCRUDMapping = itemCRUDMapping(itemType) + + # ---------------------------------------------------------- + # DELETE + # ---------------------------------------------------------- + def DeleteItem(self, request, itemID, storedprocDelete): + + connection = config.get_db_connection() + cursor = connection.cursor() + + LogHelper.log_action( + f"Delete {self.itemCRUDMapping.name}", + f"User {current_user.id} deleted {self.itemCRUDMapping.name} '{itemID}'" + ) + + try: + cursor.callproc(storedprocDelete, (itemID,)) + connection.commit() + + self.isSuccess = True + self.resultMessage = HtmlHelper.json_response( + ResponseHandler.delete_success(self.itemCRUDMapping.name), 200 + ) + + except mysql.connector.Error as e: + print(f"Error deleting {self.itemCRUDMapping.name}: {e}") + self.isSuccess = False + self.resultMessage = HtmlHelper.json_response( + ResponseHandler.delete_failure(self.itemCRUDMapping.name), 500 + ) + + finally: + cursor.close() + connection.close() + + # ---------------------------------------------------------- + # ADD + # ---------------------------------------------------------- + def AddItem(self, request, parentid=None, childname=None, storedprocfetch=None, storedprocadd=None, data=None): + + connection = config.get_db_connection() + if not connection: + self.isSuccess = False + self.resultMessage = HtmlHelper.json_response( + ResponseHandler.db_connection_failure(), 500 + ) + return + + cursor = connection.cursor() + + LogHelper.log_action( + f"Add {self.itemCRUDMapping.name}", + f"User {current_user.id} adding '{childname if childname else (data.get('Contractor_Name') if data else '')}'" + ) + + try: + # ====================================================== + # SUBCONTRACTOR (MULTI-FIELD) + # ====================================================== + if data: + + # Duplicate check + cursor.callproc(storedprocfetch, (data['Contractor_Name'],)) + + existing_item = None + for rs in cursor.stored_results(): + existing_item = rs.fetchone() + + if existing_item: + self.isSuccess = False + self.resultMessage = HtmlHelper.json_response( + ResponseHandler.already_exists(self.itemCRUDMapping.name), 409 + ) + return + + # Insert + cursor.callproc(storedprocadd, ( + data['Contractor_Name'], + data['Address'], + data['Mobile_No'], + data['PAN_No'], + data['Email'], + data['Gender'], + data['GST_Registration_Type'], + data['GST_No'], + data['Contractor_password'] + )) + + connection.commit() + + self.isSuccess = True + self.resultMessage = HtmlHelper.json_response( + ResponseHandler.add_success(self.itemCRUDMapping.name), 200 + ) + return + + # ====================================================== + # NORMAL (Village / Block / State) + # ====================================================== + if not re.match(RegEx.patternAlphabetOnly, childname): + self.isSuccess = False + self.resultMessage = HtmlHelper.json_response( + ResponseHandler.invalid_name(self.itemCRUDMapping.name), 400 + ) + return + + # Duplicate check + if parentid is None: + cursor.callproc(storedprocfetch, (childname,)) + else: + cursor.callproc(storedprocfetch, (childname, parentid)) + + existing_item = None + for rs in cursor.stored_results(): + existing_item = rs.fetchone() + + if existing_item: + self.isSuccess = False + self.resultMessage = HtmlHelper.json_response( + ResponseHandler.already_exists(self.itemCRUDMapping.name), 409 + ) + return + + # Insert + if parentid is None: + cursor.callproc(storedprocadd, (childname,)) + else: + cursor.callproc(storedprocadd, (childname, parentid)) + + connection.commit() + + self.isSuccess = True + self.resultMessage = HtmlHelper.json_response( + + ResponseHandler.add_success(self.itemCRUDMapping.name), 200 + ) + + except mysql.connector.Error as e: + print(f"Database Error: {e}") + self.isSuccess = False + self.resultMessage = HtmlHelper.json_response( + ResponseHandler.add_failure(self.itemCRUDMapping.name), 500 + ) + + finally: + cursor.close() + connection.close() + + # ---------------------------------------------------------- + # EDIT + # ---------------------------------------------------------- + def EditItem(self, request, childid, parentid=None, childname=None, storedprocupdate=None, data=None): + + connection = config.get_db_connection() + cursor = connection.cursor() + + LogHelper.log_action( + f"Edit {self.itemCRUDMapping.name}", + f"User {current_user.id} edited '{childid}'" + ) + + try: + # ====================================================== + # SUBCONTRACTOR (MULTI-FIELD) + # ====================================================== + if data: + cursor.callproc(storedprocupdate, ( + childid, + data['Contractor_Name'], + data['Address'], + data['Mobile_No'], + data['PAN_No'], + data['Email'], + data['Gender'], + data['GST_Registration_Type'], + data['GST_No'], + data['Contractor_password'] + )) + + connection.commit() + + self.isSuccess = True + self.resultMessage = HtmlHelper.json_response( + ResponseHandler.update_success(self.itemCRUDMapping.name), 200 + ) + return + + # ====================================================== + # NORMAL + # ====================================================== + if not re.match(RegEx.patternAlphabetOnly, childname): + self.isSuccess = False + self.resultMessage = ResponseHandler.update_failure(self.itemCRUDMapping.name)['message'] + return + + if parentid is None: + cursor.callproc(storedprocupdate, (childid, childname)) + else: + cursor.callproc(storedprocupdate, (childid, parentid, childname)) + + connection.commit() + + self.isSuccess = True + self.resultMessage = ResponseHandler.update_success(self.itemCRUDMapping.name)['message'] + + except mysql.connector.Error as e: + print(f"Error updating {self.itemCRUDMapping.name}: {e}") + self.isSuccess = False + self.resultMessage = HtmlHelper.json_response( + ResponseHandler.update_failure(self.itemCRUDMapping.name), 500 + ) + + finally: + cursor.close() + connection.close() + + # ---------------------------------------------------------- + # GET ALL + # ---------------------------------------------------------- + def GetAllData(self, request, storedproc): + + data = [] + connection = config.get_db_connection() + + if not connection: + return [] + + cursor = connection.cursor() + + try: + cursor.callproc(storedproc) + + for result in cursor.stored_results(): + data = result.fetchall() + + self.isSuccess = True + + except mysql.connector.Error as e: + print(f"Error fetching {self.itemCRUDMapping.name}: {e}") + self.isSuccess = False + self.resultMessage = HtmlHelper.json_response( + ResponseHandler.fetch_failure(self.itemCRUDMapping.name), 500 + ) + return [] + + finally: + cursor.close() + connection.close() + + return data + + # ---------------------------------------------------------- + # GET BY ID + # ---------------------------------------------------------- + def GetDataByID(self, id, storedproc): + + data = None + connection = config.get_db_connection() + cursor = connection.cursor() + + try: + cursor.callproc(storedproc, (id,)) + + for rs in cursor.stored_results(): + data = rs.fetchone() + + except mysql.connector.Error as e: + print(f"Error fetching {self.itemCRUDMapping.name}: {e}") + + finally: + cursor.close() + connection.close() + + return data + + # ---------------------------------------------------------- + # CHECK ITEM + # ---------------------------------------------------------- + def CheckItem(self, request, parentid, childname, storedprocfetch): + + connection = config.get_db_connection() + cursor = connection.cursor() + + LogHelper.log_action( + f"Check {self.itemCRUDMapping.name}", + f"User {current_user.id} checked '{childname}'" + ) + + if not re.match(RegEx.patternAlphabetOnly, childname): + return HtmlHelper.json_response( + ResponseHandler.invalid_name(self.itemCRUDMapping.name), 400 + ) + + try: + if parentid is None: + cursor.callproc(storedprocfetch, (childname,)) + else: + cursor.callproc(storedprocfetch, (childname, parentid)) + + existing_item = None + for rs in cursor.stored_results(): + existing_item = rs.fetchone() + + if existing_item: + return HtmlHelper.json_response( + ResponseHandler.already_exists(self.itemCRUDMapping.name), 409 + ) + + return HtmlHelper.json_response( + ResponseHandler.is_available(self.itemCRUDMapping.name), 200 + ) + + except mysql.connector.Error as e: + print(f"Error checking {self.itemCRUDMapping.name}: {e}") + return HtmlHelper.json_response( + ResponseHandler.fetch_failure(self.itemCRUDMapping.name), 500 + ) + + finally: + cursor.close() + connection.close() \ No newline at end of file diff --git a/AppCode/Log.py b/model/Log.py similarity index 100% rename from AppCode/Log.py rename to model/Log.py diff --git a/model/PmcReport.py b/model/PmcReport.py new file mode 100644 index 0000000..c8ec69e --- /dev/null +++ b/model/PmcReport.py @@ -0,0 +1,456 @@ +import openpyxl +from openpyxl.styles import Font, PatternFill +import config +from flask_login import current_user +from model.Log import LogHelper + +from model.Report import ReportHelper +from model.FolderAndFile import FolderAndFile + +class PmcReport: + + @staticmethod + def get_pmc_report(pmc_no): + + connection = config.get_db_connection() + cursor = connection.cursor(dictionary=True, buffered=True) + + try: + + # cursor.callproc("GetContractorInfoByPmcNo", (pmc_no,)) + # pmc_info = next(cursor.stored_results()).fetchone() + pmc_info = ReportHelper.execute_sp(cursor, 'GetContractorInfoByPmcNo', [pmc_no], True) + + if not pmc_info: + return None + + cursor.callproc("Get_pmc_hold_types", (pmc_no, pmc_info["Contractor_Id"])) + hold_types = next(cursor.stored_results()).fetchall() + + # Extract hold_type_ids + hold_type_ids = [ht['hold_type_id'] for ht in hold_types] + + invoices = [] + hold_amount_total = 0 + if hold_type_ids: + hold_type_ids_str = ",".join(map(str, hold_type_ids)) + cursor.callproc( + 'GetInvoices_WithHold', + [pmc_no, pmc_info["Contractor_Id"], hold_type_ids_str] + ) + else: + cursor.callproc( + 'GetInvoices_NoHold', + [pmc_no, pmc_info["Contractor_Id"]] + ) + for result in cursor.stored_results(): + invoices = result.fetchall() + + if hold_type_ids: + hold_amount_total = sum(row.get('hold_amount', 0) or 0 for row in invoices) + + total_invo_final = sum(row.get('Final_Amount', 0) or 0 for row in invoices) + + + # GST RELEASE + # cursor.callproc('GetGSTReleaseByPMC', [pmc_no]) + # gst_rel = [] + # for result in cursor.stored_results(): + # gst_rel = result.fetchall() + + gst_rel = ReportHelper.execute_sp(cursor, 'GetGSTReleaseByPMC', [pmc_no]) + + total_gst_basic = sum(row.get('basic_amount', 0) or 0 for row in gst_rel) + total_gst_final = sum(row.get('final_amount', 0) or 0 for row in gst_rel) + + # ---------------- HOLD RELEASE ---------------- + # cursor.callproc('GetHoldReleaseByPMC', [pmc_no]) + # hold_release = [] + # for result in cursor.stored_results(): + # hold_release = result.fetchall() + + hold_release = ReportHelper.execute_sp(cursor, 'GetHoldReleaseByPMC', [pmc_no]) + + # ---------------- CREDIT NOTE ---------------- + # cursor.callproc('GetCreditNoteByPMC', [pmc_no]) + # credit_note = [] + # for result in cursor.stored_results(): + # credit_note = result.fetchall() + + credit_note = ReportHelper.execute_sp(cursor, 'GetCreditNoteByPMC', [pmc_no]) + + payments = ReportHelper.execute_sp(cursor, 'GetPaymentsByPMC', [pmc_no]) + + + # ---------------- PAYMENTS ---------------- + # cursor.callproc('GetPaymentsByPMC', [pmc_no]) + # payments = [] + # for result in cursor.stored_results(): + # payments = result.fetchall() + + + + total_pay_amount = sum(row.get('Payment_Amount', 0) or 0 for row in payments) + total_pay_total = sum(row.get('Total_amount', 0) or 0 for row in payments) + + totals = { + "sum_invo_basic_amt": sum(row.get('Basic_Amount', 0) or 0 for row in invoices), + "sum_invo_debit_amt": sum(row.get('Debit_Amount', 0) or 0 for row in invoices), + "sum_invo_after_debit_amt": sum(row.get('After_Debit_Amount', 0) or 0 for row in invoices), + "sum_invo_amt": sum(row.get('Amount', 0) or 0 for row in invoices), + "sum_invo_gst_amt": sum(row.get('GST_Amount', 0) or 0 for row in invoices), + "sum_invo_tds_amt": sum(row.get('TDS_Amount', 0) or 0 for row in invoices), + "sum_invo_ds_amt": sum(row.get('SD_Amount', 0) or 0 for row in invoices), + "sum_invo_on_commission": sum(row.get('On_Commission', 0) or 0 for row in invoices), + "sum_invo_hydro_test": sum(row.get('Hydro_Testing', 0) or 0 for row in invoices), + "sum_invo_gst_sd_amt": sum(row.get('GST_SD_Amount', 0) or 0 for row in invoices), + "sum_invo_final_amt": total_invo_final, + "sum_invo_hold_amt": hold_amount_total, + "sum_gst_basic_amt": total_gst_basic, + "sum_gst_final_amt": total_gst_final, + "sum_pay_payment_amt": total_pay_amount, + "sum_pay_tds_payment_amt": sum(row.get('TDS_Payment_Amount', 0) or 0 for row in payments), + "sum_pay_total_amt": total_pay_total + } + + return { + "info": pmc_info, + "invoices": invoices, + "hold_types": hold_types, + "gst_rel": gst_rel, + "payments": payments, + "credit_note": credit_note, + "hold_release": hold_release, + "total": totals + } + + finally: + cursor.close() + connection.close() + + + @staticmethod + def download_pmc_report(pmc_no): + + connection = config.get_db_connection() + if not connection: + return None + + cursor = connection.cursor(dictionary=True) + + try: + # filename + filename = f"PMC_Report_{pmc_no}.xlsx" + + output_folder = FolderAndFile.get_download_folder() + output_file = FolderAndFile.get_download_path(filename) + + # ================= DATA FETCH ================= + + contractor_info = ReportHelper.execute_sp(cursor, 'GetContractorDetailsByPMC', [pmc_no], "one") + + if not contractor_info: + return None + + hold_types = ReportHelper.execute_sp(cursor, 'GetHoldTypesByContractor', [contractor_info["Contractor_Id"]]) + + hold_type_map = {ht['hold_type_id']: ht['hold_type'] for ht in hold_types} + + invoices = ReportHelper.execute_sp(cursor, 'GetInvoicesAndGstReleaseByPmcNo', [pmc_no]) + + credit_notes = ReportHelper.execute_sp(cursor, 'GetCreditNoteByContractor', [contractor_info["Contractor_Id"]]) + + hold_amounts = ReportHelper.execute_sp(cursor, 'GetHoldAmountsByContractor', [contractor_info["Contractor_Id"]]) + + all_payments = ReportHelper.execute_sp(cursor, 'GetAllPaymentsByPMC', [pmc_no]) + + gst_releases = ReportHelper.execute_sp(cursor, 'GetGSTReleaseDetailsByPMC', [pmc_no]) + + # ================= DATA MAPPING ================= + + hold_data = {} + for h in hold_amounts: + hold_data.setdefault(h['Invoice_Id'], {})[h['hold_type_id']] = h['hold_amount'] + + payments_map = {} + for pay in all_payments: + if pay['invoice_no']: + payments_map.setdefault(pay['invoice_no'], []).append(pay) + + # ================= LOG ================= + LogHelper.log_action( + "Download PMC Report", + f"User {current_user.id} Download PMC Report '{pmc_no}'" + ) + + # ================= EXCEL ================= + workbook = openpyxl.Workbook() + sheet = workbook.active + sheet.title = "PMC Report" + + # HEADER INFO + sheet.append(["", "", "Laxmi Civil Engineering Services PVT. LTD."]) + sheet.append(["Contractor Name", contractor_info["Contractor_Name"]]) + sheet.append(["State", contractor_info["State_Name"]]) + sheet.append(["District", contractor_info["District_Name"]]) + sheet.append(["Block", contractor_info["Block_Name"]]) + sheet.append([]) + + base_headers = [ + "PMC No","Village","Work Type","Invoice Details","Invoice Date","Invoice No", + "Basic Amount","Debit","After Debit Amount","GST","Amount","TDS", + "SD","On Commission","Hydro Testing","GST SD Amount" + ] + + hold_headers = [ht['hold_type'] for ht in hold_types] + + payment_headers = [ + "Final Amount","Payment Amount","TDS Payment","Total Paid","UTR" + ] + + headers = base_headers + hold_headers + payment_headers + sheet.append(headers) + + # STYLE + for cell in sheet[sheet.max_row]: + cell.font = Font(bold=True) + + # DATA + seen_invoices = set() + + for inv in invoices: + + invoice_no = inv["Invoice_No"] + payments = payments_map.get(invoice_no, []) + + if invoice_no in seen_invoices: + continue + + seen_invoices.add(invoice_no) + + first_payment = payments[0] if payments else None + + row = [ + pmc_no, + inv["Village_Name"], + inv["Work_Type"], + inv["Invoice_Details"], + inv["Invoice_Date"], + invoice_no, + inv["Basic_Amount"], + inv["Debit_Amount"], + inv["After_Debit_Amount"], + inv["GST_Amount"], + inv["Amount"], + inv["TDS_Amount"], + inv["SD_Amount"], + inv["On_Commission"], + inv["Hydro_Testing"], + inv["GST_SD_Amount"] + ] + + # HOLD DATA + invoice_holds = hold_data.get(inv["Invoice_Id"], {}) + for ht_id in hold_type_map.keys(): + row.append(invoice_holds.get(ht_id, "")) + + # PAYMENT DATA + row += [ + inv["Final_Amount"], + first_payment["Payment_Amount"] if first_payment else "", + first_payment["TDS_Payment_Amount"] if first_payment else "", + first_payment["Total_amount"] if first_payment else "", + first_payment["UTR"] if first_payment else "" + ] + + sheet.append(row) + + # AUTO WIDTH + for col in sheet.columns: + max_len = max((len(str(cell.value)) for cell in col if cell.value), default=0) + sheet.column_dimensions[col[0].column_letter].width = max_len + 2 + + # SAVE + workbook.save(output_file) + workbook.close() + + return output_folder, filename + + except Exception as e: + print(f"Error generating PMC report: {e}") + return None + + finally: + cursor.close() + connection.close() + + # @staticmethod + # def download_pmc_report(pmc_no): + + # connection = config.get_db_connection() + # cursor = connection.cursor(dictionary=True) + + # # output_folder = "static/download" + # # output_file = os.path.join(output_folder, f"PMC_Report_{pmc_no}.xlsx") + # output_folder = FolderAndFile.get_download_folder + # filename = f"PMC_Report_{pmc_no}.xlsx" + # output_file = FolderAndFile.get_download_path(filename) + + # try: + + # cursor.callproc('GetContractorDetailsByPMC', [pmc_no]) + # contractor_info = next(cursor.stored_results()).fetchone() + + # if not contractor_info: + # return None + + # cursor.callproc('GetHoldTypesByContractor', [contractor_info["Contractor_Id"]]) + # hold_types = next(cursor.stored_results()).fetchall() + + # hold_type_map = {ht['hold_type_id']: ht['hold_type'] for ht in hold_types} + + # cursor.callproc('GetInvoicesAndGstReleaseByPmcNo', [pmc_no]) + # invoices = next(cursor.stored_results()).fetchall() + + # cursor.callproc('GetCreditNoteByContractor',[contractor_info["Contractor_Id"]]) + + # credit_notes = [] + # for result in cursor.stored_results(): + # credit_notes = result.fetchall() + + # credit_note_map = {} + # for cn in credit_notes: + # key = (cn["PMC_No"], cn["Invoice_No"]) + # credit_note_map.setdefault(key, []).append(cn) + + # cursor.callproc('GetHoldAmountsByContractor', [contractor_info["Contractor_Id"]]) + # hold_amounts = next(cursor.stored_results()).fetchall() + + # hold_data = {} + # for h in hold_amounts: + # hold_data.setdefault(h['Invoice_Id'], {})[h['hold_type_id']] = h['hold_amount'] + + # cursor.callproc('GetAllPaymentsByPMC', [pmc_no]) + # all_payments = next(cursor.stored_results()).fetchall() + + # payments_map = {} + # extra_payments = [] + + # for pay in all_payments: + # if pay['invoice_no']: + # payments_map.setdefault(pay['invoice_no'], []).append(pay) + # else: + # extra_payments.append(pay) + + # # ---------------- GST RELEASE DETAILS ---------------- + # cursor.callproc('GetGSTReleaseDetailsByPMC', [pmc_no]) + + # gst_releases = [] + # for result in cursor.stored_results(): + # gst_releases = result.fetchall() + + # gst_release_map = {} + + # for gr in gst_releases: + + # invoice_nos = [] + + # if gr['Invoice_No']: + + # cleaned = gr['Invoice_No'].replace(' ', '') + + # if '&' in cleaned: + # invoice_nos = cleaned.split('&') + + # elif ',' in cleaned: + # invoice_nos = cleaned.split(',') + + # else: + # invoice_nos = [cleaned] + + # for inv_no in invoice_nos: + # gst_release_map.setdefault(inv_no, []).append(gr) + + # LogHelper.log_action( + # "Download PMC Report", + # f"User {current_user.id} Download PMC Report '{pmc_no}'" + # ) + + # workbook = openpyxl.Workbook() + # sheet = workbook.active + # sheet.title = "PMC Report" + + # sheet.append(["", "", "Laxmi Civil Engineering Services PVT. LTD."]) + # sheet.append(["Contractor Name", contractor_info["Contractor_Name"], "", "GST No", contractor_info["GST_No"], "", "GST Type", contractor_info["GST_Registration_Type"]]) + # sheet.append(["State", contractor_info["State_Name"], "", "PAN No", contractor_info["PAN_No"], "", "Address", contractor_info["Address"]]) + # sheet.append(["District", contractor_info["District_Name"], "", "Mobile No", contractor_info["Mobile_No"]]) + # sheet.append(["Block", contractor_info["Block_Name"], "", "Email", contractor_info["Email"]]) + # sheet.append([]) + + # base_headers = [ + # "PMC No","Village","Work Type","Invoice Details","Invoice Date","Invoice No", + # "Basic Amount","Debit","After Debit Amount","GST (18%)","Amount","TDS (1%)", + # "SD (5%)","On Commission","Hydro Testing","GST SD Amount" + # ] + + # hold_headers = [ht['hold_type'] for ht in hold_types] + + # payment_headers = [ + # "Final Amount","Payment Amount","TDS Payment","Total Paid","UTR" + # ] + + # sheet.append(base_headers + hold_headers + payment_headers) + + # header_fill = PatternFill(start_color="ADD8E6",end_color="ADD8E6",fill_type="solid") + # header_font = Font(bold=True) + + # for cell in sheet[sheet.max_row]: + # cell.font = header_font + # cell.fill = header_fill + + # seen_invoices = set() + # processed_payments = set() + + # for inv in invoices: + + # invoice_no = inv["Invoice_No"] + # payments = payments_map.get(invoice_no, []) + + # if invoice_no not in seen_invoices: + + # seen_invoices.add(invoice_no) + # first_payment = payments[0] if payments else None + + # row = [ + # pmc_no, inv["Village_Name"], inv["Work_Type"], + # inv["Invoice_Details"], inv["Invoice_Date"], invoice_no, + # inv["Basic_Amount"], inv["Debit_Amount"], + # inv["After_Debit_Amount"], inv["GST_Amount"], + # inv["Amount"], inv["TDS_Amount"], inv["SD_Amount"], + # inv["On_Commission"], inv["Hydro_Testing"], inv["GST_SD_Amount"] + # ] + + # invoice_holds = hold_data.get(inv["Invoice_Id"], {}) + + # for ht_id in hold_type_map.keys(): + # row.append(invoice_holds.get(ht_id, "")) + + # row += [ + # inv["Final_Amount"], + # first_payment["Payment_Amount"] if first_payment else "", + # first_payment["TDS_Payment_Amount"] if first_payment else "", + # first_payment["Total_amount"] if first_payment else "", + # first_payment["UTR"] if first_payment else "" + # ] + + # sheet.append(row) + + # workbook.save(output_file) + # workbook.close() + + # return output_folder, filename + + # finally: + + # cursor.close() + # connection.close() \ No newline at end of file diff --git a/model/Report.py b/model/Report.py new file mode 100644 index 0000000..41d5ad2 --- /dev/null +++ b/model/Report.py @@ -0,0 +1,275 @@ +import config +from datetime import datetime +from flask import send_file +import openpyxl +from openpyxl.styles import Font + +from model.FolderAndFile import FolderAndFile + +class ReportHelper: + isSuccess = False + resultMessage = "" + data=[] + + def __init__(self): + self.isSuccess = False + self.resultMessage = "" + self.data = [] + + @staticmethod + def execute_sp(cursor, proc_name, params=[], fetch_one=False): + cursor.callproc(proc_name, params) + return ( + ReportHelper.fetch_one_result(cursor) + if fetch_one else + ReportHelper.fetch_all_results(cursor) + ) + + @staticmethod + def fetch_all_results(cursor): + data = [] + for result in cursor.stored_results(): + data = result.fetchall() + return data + + + @staticmethod + def fetch_one_result(cursor): + data = None + for result in cursor.stored_results(): + data = result.fetchone() + return data + + + @staticmethod + def search_contractor(request): + subcontractor_name = request.form.get("subcontractor_name") + pmc_no = request.form.get("pmc_no") + state = request.form.get("state") + district = request.form.get("district") + block = request.form.get("block") + village = request.form.get("village") + year_from = request.form.get("year_from") + year_to = request.form.get("year_to") + + connection = config.get_db_connection() + if not connection: + return [] + + cursor = connection.cursor(dictionary=True) + + try: + data = ReportHelper.execute_sp( + cursor, + "search_contractor_info", + [ + subcontractor_name or None, + pmc_no or None, + state or None, + district or None, + block or None, + village or None, + year_from or None, + year_to or None + ] + ) + + except Exception as e: + print(f"Error in search_contractor: {e}") + data = [] + + finally: + cursor.close() + connection.close() + + return data + + + @staticmethod + def get_contractor_report(contractor_id): + connection = config.get_db_connection() + cursor = connection.cursor(dictionary=True, buffered=True) + + try: + # Contractor Info (only one fetch) + contInfo = ReportHelper.execute_sp(cursor, 'GetContractorInfo', [contractor_id], True) + # Hold Types + hold_types = ReportHelper.execute_sp(cursor, 'GetContractorHoldTypes', [contractor_id]) + # Invoices + invoices = ReportHelper.execute_sp(cursor, 'GetContractorInvoices', [contractor_id]) + # GST Release + gst_rel = ReportHelper.execute_sp(cursor, 'GetGSTRelease', [contractor_id]) + # Hold Release + hold_release = ReportHelper.execute_sp(cursor, 'GetHoldRelease', [contractor_id]) + # Credit Note + credit_note = ReportHelper.execute_sp(cursor, 'GetCreditNote', [contractor_id]) + # Payments + payments = ReportHelper.execute_sp(cursor, 'GetPayments', [contractor_id]) + + # Totals + total = { + "sum_invo_basic_amt": float(sum(row['Basic_Amount'] or 0 for row in invoices)), + "sum_invo_debit_amt": float(sum(row['Debit_Amount'] or 0 for row in invoices)), + "sum_invo_after_debit_amt": float(sum(row['After_Debit_Amount'] or 0 for row in invoices)), + "sum_invo_amt": float(sum(row['Amount'] or 0 for row in invoices)), + "sum_invo_gst_amt": float(sum(row['GST_Amount'] or 0 for row in invoices)), + "sum_invo_tds_amt": float(sum(row['TDS_Amount'] or 0 for row in invoices)), + "sum_invo_ds_amt": float(sum(row['SD_Amount'] or 0 for row in invoices)), + "sum_invo_on_commission": float(sum(row['On_Commission'] or 0 for row in invoices)), + "sum_invo_hydro_test": float(sum(row['Hydro_Testing'] or 0 for row in invoices)), + "sum_invo_gst_sd_amt": float(sum(row['GST_SD_Amount'] or 0 for row in invoices)), + "sum_invo_final_amt": float(sum(row['Final_Amount'] or 0 for row in invoices)), + "sum_invo_hold_amt": float(sum(row['hold_amount'] or 0 for row in invoices)), + + "sum_gst_basic_amt": float(sum(row['basic_amount'] or 0 for row in gst_rel)), + "sum_gst_final_amt": float(sum(row['final_amount'] or 0 for row in gst_rel)), + + "sum_pay_payment_amt": float(sum(row['Payment_Amount'] or 0 for row in payments)), + "sum_pay_tds_payment_amt": float(sum(row['TDS_Payment_Amount'] or 0 for row in payments)), + "sum_pay_total_amt": float(sum(row['Total_amount'] or 0 for row in payments)) + } + + current_date = datetime.now().strftime('%Y-%m-%d') + + finally: + cursor.close() + connection.close() + + return { + "contInfo": contInfo, + "invoices": invoices, + "hold_types": hold_types, + "gst_rel": gst_rel, + "payments": payments, + "credit_note": credit_note, + "hold_release": hold_release, + "total": total, + "current_date": current_date + } + + @staticmethod + def download_report(contractor_id): + try: + connection = config.get_db_connection() + cursor = connection.cursor(dictionary=True) + + # -------- Contractor Info -------- + contInfo = ReportHelper.execute_sp(cursor, 'GetContractorInfo', [contractor_id], True) + + if not contInfo: + return "No contractor found", 404 + + # -------- Invoice Data -------- + cursor.callproc('FetchInvoicesByContractor', [contractor_id]) + + invoices = [] + for result in cursor.stored_results(): + invoices.extend(result.fetchall()) + + if not invoices: + return "No invoice data found" + + # -------- Create Workbook -------- + workbook = openpyxl.Workbook() + sheet = workbook.active + sheet.title = "Contractor Report" + + # ================= CONTRACTOR DETAILS ================= + sheet.append(["SUB CONTRACTOR DETAILS"]) + sheet.cell(row=sheet.max_row, column=1).font = Font(bold=True) + sheet.append([]) + + sheet.append(["Name", contInfo.get("Contractor_Name") or ""]) + sheet.append(["Mobile No", contInfo.get("Mobile_No") or ""]) + sheet.append(["Email", contInfo.get("Email") or ""]) + sheet.append(["Village", contInfo.get("Village_Name") or ""]) + sheet.append(["Block", contInfo.get("Block_Name") or ""]) + sheet.append(["District", contInfo.get("District_Name") or ""]) + sheet.append(["State", contInfo.get("State_Name") or ""]) + sheet.append(["Address", contInfo.get("Address") or ""]) + sheet.append(["GST No", contInfo.get("GST_No") or ""]) + sheet.append(["PAN No", contInfo.get("PAN_No") or ""]) + sheet.append([]) + sheet.append([]) + + # ================= TABLE HEADERS ================= + headers = [ + "PMC No", "Village", "Invoice No", "Invoice Date", "Work Type","Invoice_Details", + "Basic Amount", "Debit Amount", "After Debit Amount", + "Amount", "GST Amount", "TDS Amount", "SD Amount", + "On Commission", "Hydro Testing", "Hold Amount", + "GST SD Amount", "Final Amount", + "Payment Amount", "TDS Payment", + "Total Amount", "UTR" + ] + sheet.append(headers) + for col in range(1, len(headers) + 1): + sheet.cell(row=sheet.max_row, column=col).font = Font(bold=True) + + # ================= DATA ================= + total_final = 0 + total_payment = 0 + total_amount = 0 + + for inv in invoices: + row = [ + inv.get("PMC_No"), + inv.get("Village_Name"), + inv.get("invoice_no"), + inv.get("Invoice_Date"), + inv.get("Work_Type"), + inv.get("Invoice_Details"), + inv.get("Basic_Amount"), + inv.get("Debit_Amount"), + inv.get("After_Debit_Amount"), + inv.get("Amount"), + inv.get("GST_Amount"), + inv.get("TDS_Amount"), + inv.get("SD_Amount"), + inv.get("On_Commission"), + inv.get("Hydro_Testing"), + inv.get("Hold_Amount"), + inv.get("GST_SD_Amount"), + inv.get("Final_Amount"), + inv.get("Payment_Amount"), + inv.get("TDS_Payment_Amount"), + inv.get("Total_Amount"), + inv.get("UTR") + ] + + total_final += float(inv.get("Final_Amount") or 0) + total_payment += float(inv.get("Payment_Amount") or 0) + total_amount += float(inv.get("Total_Amount") or 0) + + sheet.append(row) + + # ================= TOTAL ROW ================= + sheet.append([]) + sheet.append([ + "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", + "TOTAL", + total_final, + total_payment, + "", + total_amount, + "" + ]) + + # ================= AUTO WIDTH ================= + for column in sheet.columns: + max_length = 0 + column_letter = column[0].column_letter + for cell in column: + if cell.value: + max_length = max(max_length, len(str(cell.value))) + sheet.column_dimensions[column_letter].width = max_length + 2 + + # ================= SAVE FILE ================= + filename = f"Contractor_Report_{contInfo.get('Contractor_Name')}.xlsx" + output_file = FolderAndFile.get_download_path(filename) + workbook.save(output_file) + + return send_file(output_file, as_attachment=True) + + except Exception as e: + return str(e) \ No newline at end of file diff --git a/model/State.py b/model/State.py new file mode 100644 index 0000000..9245a99 --- /dev/null +++ b/model/State.py @@ -0,0 +1,168 @@ +from flask import request, redirect, url_for +from flask_login import current_user + +from model.Utilities import RegEx, ResponseHandler, HtmlHelper, ItemCRUDType +from model.Log import LogHelper +from model.ItemCRUD import ItemCRUD + +import config +import re +import mysql.connector + + +class State: + + def __init__(self): + self.isSuccess = False + self.resultMessage = "" + + # ---------------------------------------------------------- + # ADD STATE (USING ITEM CRUD) + # ---------------------------------------------------------- + def AddState(self, request): + + state_name = request.form['state_Name'].strip() + + crud = ItemCRUD(ItemCRUDType.State) + + crud.AddItem( + request=request, + childname=state_name, + storedprocfetch="CheckStateExists", + storedprocadd="SaveState" + ) + + self.isSuccess = crud.isSuccess + self.resultMessage = crud.resultMessage + + return self.resultMessage + + + # ---------------------------------------------------------- + # GET ALL STATES (NO CHANGE - THIS IS CORRECT) + # ---------------------------------------------------------- + def GetAllStates(self, request): + + connection = config.get_db_connection() + data = [] + + if not connection: + return [] + + cursor = connection.cursor() + + try: + cursor.callproc("GetAllStates") + for res in cursor.stored_results(): + data = res.fetchall() + + self.isSuccess = True + + except mysql.connector.Error as e: + print(f"Error fetching states: {e}") + self.isSuccess = False + self.resultMessage = HtmlHelper.json_response( + ResponseHandler.fetch_failure("state"), 500 + ) + return [] + + finally: + cursor.close() + connection.close() + + return data + + + # ---------------------------------------------------------- + # CHECK STATE (USING ITEM CRUD) + # ---------------------------------------------------------- + def CheckState(self, request): + + state_name = request.json.get('state_Name', '').strip() + + crud = ItemCRUD(ItemCRUDType.State) + + return crud.CheckItem( + request=request, + parentid=None, + childname=state_name, + storedprocfetch="CheckStateExists" + ) + + + # ---------------------------------------------------------- + # DELETE STATE (USING ITEM CRUD) + # ---------------------------------------------------------- + def DeleteState(self, request, id): + + crud = ItemCRUD(ItemCRUDType.State) + + crud.DeleteItem( + request=request, + itemID=id, + storedprocDelete="DeleteState" + ) + + self.isSuccess = crud.isSuccess + self.resultMessage = crud.resultMessage + + return self.resultMessage + + + # ---------------------------------------------------------- + # EDIT STATE (USING ITEM CRUD) + # ---------------------------------------------------------- + def EditState(self, request, id): + + state_name = request.form['state_Name'].strip() + + crud = ItemCRUD(ItemCRUDType.State) + + crud.EditItem( + request=request, + childid=id, + parentid=None, + childname=state_name, + storedprocupdate="UpdateStateById" + ) + + self.isSuccess = crud.isSuccess + self.resultMessage = crud.resultMessage + + return redirect(url_for('state.add_state')) + + + # ---------------------------------------------------------- + # GET STATE BY ID (KEEP SAME) + # ---------------------------------------------------------- + def GetStateByID(self, request, id): + + connection = config.get_db_connection() + data = None + + if not connection: + return None + + cursor = connection.cursor() + + try: + cursor.callproc("GetStateByID", (id,)) + for res in cursor.stored_results(): + data = res.fetchone() + + if data: + self.isSuccess = True + self.resultMessage = "Success" + else: + self.isSuccess = False + self.resultMessage = "Not Found" + + except mysql.connector.Error as e: + print(f"Error fetching state: {e}") + self.isSuccess = False + + finally: + cursor.close() + connection.close() + + return data \ No newline at end of file diff --git a/model/Subcontractor.py b/model/Subcontractor.py new file mode 100644 index 0000000..0ec659d --- /dev/null +++ b/model/Subcontractor.py @@ -0,0 +1,140 @@ +from model.Utilities import ItemCRUDType +from model.ItemCRUD import ItemCRUD + + +class Subcontractor: + def __init__(self): + self.isSuccess = False + self.resultMessage = "" + + # ---------------------------------------------------------- + # ADD + # ---------------------------------------------------------- + def AddSubcontractor(self, request): + + subcontractor = ItemCRUD(itemType=ItemCRUDType.Subcontractor) + + data = { + "Contractor_Name": request.form.get('Contractor_Name', '').strip(), + "Address": request.form.get('Address', '').strip(), + "Mobile_No": request.form.get('Mobile_No', '').strip(), + "PAN_No": request.form.get('PAN_No', '').strip(), + "Email": request.form.get('Email', '').strip(), + "Gender": request.form.get('Gender', '').strip(), + "GST_Registration_Type": request.form.get('GST_Registration_Type', '').strip(), + "GST_No": request.form.get('GST_No', '').strip(), + "Contractor_password": request.form.get('Contractor_password', '').strip() + } + + subcontractor.AddItem( + request=request, + data=data, + storedprocfetch="GetSubcontractorByName", + storedprocadd="SaveContractor" + ) + + self.isSuccess = subcontractor.isSuccess + self.resultMessage = subcontractor.resultMessage + return + + # ---------------------------------------------------------- + # GET ALL + # ---------------------------------------------------------- + def GetAllSubcontractors(self, request): + + subcontractor = ItemCRUD(itemType=ItemCRUDType.Subcontractor) + + data = subcontractor.GetAllData( + request=request, + storedproc="GetAllSubcontractors" + ) + + self.isSuccess = subcontractor.isSuccess + self.resultMessage = subcontractor.resultMessage + return data + + # ---------------------------------------------------------- + # GET BY ID + # ---------------------------------------------------------- + def GetSubcontractorByID(self, id): + + subcontractor = ItemCRUD(itemType=ItemCRUDType.Subcontractor) + + data = subcontractor.GetDataByID( + id=id, + storedproc="GetSubcontractorById" + ) + + if data: + self.isSuccess = True + else: + self.isSuccess = False + self.resultMessage = "Subcontractor not found" + + return data + + # ---------------------------------------------------------- + # CHECK (Duplicate) + # ---------------------------------------------------------- + def CheckSubcontractor(self, request): + + subcontractor = ItemCRUD(itemType=ItemCRUDType.Subcontractor) + + name = request.form.get('Contractor_Name', '').strip() + + result = subcontractor.CheckItem( + request=request, + childname=name, + storedprocfetch="GetSubcontractorByName" + ) + + self.isSuccess = subcontractor.isSuccess + self.resultMessage = subcontractor.resultMessage + return result + + # ---------------------------------------------------------- + # EDIT + # ---------------------------------------------------------- + def EditSubcontractor(self, request, subcontractor_id): + + subcontractor = ItemCRUD(itemType=ItemCRUDType.Subcontractor) + + data = { + "Contractor_Name": request.form.get('Contractor_Name', '').strip(), + "Address": request.form.get('Address', '').strip(), + "Mobile_No": request.form.get('Mobile_No', '').strip(), + "PAN_No": request.form.get('PAN_No', '').strip(), + "Email": request.form.get('Email', '').strip(), + "Gender": request.form.get('Gender', '').strip(), + "GST_Registration_Type": request.form.get('GST_Registration_Type', '').strip(), + "GST_No": request.form.get('GST_No', '').strip(), + "Contractor_password": request.form.get('Contractor_password', '').strip() + } + + subcontractor.EditItem( + request=request, + childid=subcontractor_id, + data=data, + storedprocupdate="UpdateSubcontractor" + ) + + self.isSuccess = subcontractor.isSuccess + self.resultMessage = subcontractor.resultMessage + return + + # ---------------------------------------------------------- + # DELETE + # ---------------------------------------------------------- + def DeleteSubcontractor(self, request, subcontractor_id): + + subcontractor = ItemCRUD(itemType=ItemCRUDType.Subcontractor) + + subcontractor.DeleteItem( + request=request, + itemID=subcontractor_id, + storedprocDelete="DeleteSubcontractor" + ) + + self.isSuccess = subcontractor.isSuccess + self.resultMessage = subcontractor.resultMessage + return \ No newline at end of file diff --git a/AppCode/Utilities.py b/model/Utilities.py similarity index 97% rename from AppCode/Utilities.py rename to model/Utilities.py index f6228b7..83868ba 100644 --- a/AppCode/Utilities.py +++ b/model/Utilities.py @@ -6,7 +6,9 @@ class ItemCRUDType(Enum): Block = 2 District = 3 State = 4 - + HoldType = 5 + Subcontractor = 6 + class RegEx: patternAlphabetOnly = "^[A-Za-z ]+$" diff --git a/AppCode/Village.py b/model/Village.py similarity index 77% rename from AppCode/Village.py rename to model/Village.py index 79b98db..3b7b0cb 100644 --- a/AppCode/Village.py +++ b/model/Village.py @@ -1,15 +1,15 @@ from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user -from AppCode.Utilities import RegEx, ResponseHandler, HtmlHelper, ItemCRUDType -from AppCode.Log import LogData, LogHelper +from model.Utilities import RegEx, ResponseHandler, HtmlHelper, ItemCRUDType +from model.Log import LogData, LogHelper import config import mysql.connector from mysql.connector import Error -from AppCode.ItemCRUD import ItemCRUD, itemCRUDMapping +from model.ItemCRUD import ItemCRUD class Village: @@ -60,25 +60,36 @@ class Village: return def EditVillage(self, request, village_id): - + corsor=None village = ItemCRUD(itemType=ItemCRUDType.Village) block_id = request.form.get('block_Id') village_name = request.form.get('Village_Name', '').strip() - village.EditItem(request=request, childid=village_id, parentid=block_id, childname=village_name, storedprocadd="UpdateVillage" ) + village.EditItem(request=request,childid=village_id,parentid=block_id,childname=village_name,storedprocupdate="UpdateVillage" ) + self.isSuccess = village.isSuccess self.resultMessage = village.resultMessage return + # def GetVillageByID(self, request, id): + + # village = ItemCRUD(itemType=ItemCRUDType.Village) + # villagedetailsdata = village.GetAllData(request=request, storedproc="GetVillageDetailsById") + # self.isSuccess = village.isSuccess + # self.resultMessage = village.resultMessage + # return villagedetailsdata + def GetVillageByID(self, request, id): - village = ItemCRUD(itemType=ItemCRUDType.Village) - villagedetailsdata = village.GetAllData(request=request, storedproc="GetVillageDetailsById") - self.isSuccess = village.isSuccess - self.resultMessage = village.resultMessage - return villagedetailsdata + villagedetailsdata = village.GetDataByID(id=id,storedproc="GetVillageDetailsById") + if villagedetailsdata: + self.isSuccess = True + else: + self.isSuccess = False + self.resultMessage = "Village not found" + return villagedetailsdata def GetAllBlocks(self, request): diff --git a/model/__pycache__/Auth.cpython-313.pyc b/model/__pycache__/Auth.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..152a9c7966d3c5af4ff60b5c230fa9b28b4c59a2 GIT binary patch literal 2724 zcmbVO&2Jk;6rcUHovaf#B*jVV)P|PCB59$aRT9!ds8f}EQF$>`ilx=oUYn(x-7ssn zG^a`+WDuwzRMDP_1P6NKz?lpE0~)0eYoMYMz4VqUMIgBF-mL995DqYs-^}}%H}l@S z_gim8qa6gsm!E!XYbqhXAk!R5W3lxyEUplp=)xSKIE!<_tVqQquBAC?R;Dsv%X7-C zO4ZpA4b6sWn9G#8$ZQAgNRqtx1Ub@hnIv)Y1ku$2qKA%%fsl4`Sr}xImQ3Ta4v=-` z> z()9d1*hS|v)2Cmu-Qotvu~=s2YuVgbfApQso8-{BxTZX|p3}e|VRvlbxhVfR_bQ+p&!zkI*uQ;}AxgMxR&u41c z!kn>i6*7i~EWBd+{soT~S)XC}6|-U&46{<9_StI1@(qJ_!OgZXPIm6ZWd3!Jp36H0 zYsE26Q}0d4ub)3{UR;JalGG}AZozhJaN@c7MeD+|`ISItY}XmSSE^hvsWp1|+0kdl zMh`!8_-G#Du$=sKwerTnm5UTp&IxA5PXc+5)Vp_GzWBk#Tm4V3caK~;wT*q}6pc5x zYg^Iq9@u}1F4z_!q+Qdklaf*;n%)_IU#Avqz7 z(!Y&tvx?f}x&U?3@RV)#SDU`sA35T*cK-+O(j)KSL2f$MW#C$;aSnPt7`g~LjE4_h zaYbFtk!(xf)DY4Iy*ehj($3l?OcEbE?VUVBh&f6K>L`*1Gz`oA*%>HuS|}OPprySyLfE1f5fXE!zTAfiiKmm zVR7Nn@~Z%ZVSoX{c1s@MrHoeGBa4{Tif5b!fLx8g?4CCryO_jwNERL;RXWDxl1G=B z1U{JJ15&K;YXvTPZU3L>#x&c14ZZOOV;Y?2(FM!*&9W6VI+iNMnxvRyrkwSvZjs`X zvB=EZ05<`Q!)fUdlYPr6u@JT1s#<;pJ_e8m1A;)**EoYA8AX?=4XF&ki&aGSr1tFaR87@3{aI91=9!nwb`caYC_caPLM>-~wV#p}!0maiJOMrLk3ezK-* zhDmhS<%RbbZYK_HB*xbgapZ@7N$x|7T+!S|Z#-*Em5u}5$%$Rg@Oo4S;p+jIq4fy%|`Qi=LMa~bIxzE{bnpY75woyPG1CX_?7iv00M;}2tSg@ z9Wr=_q<$vpHIlwV4n0tbpuHRYO%;XF2L#A(9cf|c0VHwg;Tb^^_S7bRB{1=@{sK{Y BImQ40 literal 0 HcmV?d00001 diff --git a/model/__pycache__/Auth.cpython-314.pyc b/model/__pycache__/Auth.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d9bfd24d221be6d3348ce47f72c9fe4d923fb522 GIT binary patch literal 2846 zcmb6bOKjWNaVSz0WmC4|wG+j$VhhgN5!OypC)hYyCmS_#V57EcFo|~$N(e@zv&7P* z_VFpZF?#S}(W*t!^aHfXDFxa?bL*k!Ub?-A5c6H4L{B1vuAIISd0(08KsuTIyUPBJ%E#rXVfd zrl}#V3u)cAq;q0RGNTEjP*nd;%cY_abUU75>ZZpn_Z}|OH+_pO+Mn5OQ4Hkk7Q1J$ zpzEsVx>l9jo*Rgzg$0z$ER;*jU$ zpLL1@StFbiA{v*!QYO)sNogWiGzslU27P5~)u=oCDzi+><+kDYLAE@5t#otYhJN$< zthQ8IoZX`=m#$y`;G0_c2^yac6kT_XHA~lnE?r;q%(_Fks_UQB4X4HF)%8`I`P{Kx z%k_|*@ccl&YmvhOrH<|*!9b}QzW=Gm%wRy*eQt2OsvDd$`%ax(zOJ(j8n{bF2H(1R zu|jV4E2Z+{{8FWC`5$wy)}Cv|{WUa2XI9m7tF~jKGSAh0ZsgWWM?8=Wtm<>F!R@Kuihm$U>=t^k)c5#LJ+(hJ^Q+okBKwt^RT1Elc7 zz)^O8iqs8Q5tc*pey|*eRtH(5;Av@Ej?=D#AE(U%20rVwTeHoPb1!4`@tBkKcT12|nr+gH&o^qnKsn@F01|tGgznXAIJ_pW*eR+K;CdQ-S1L&T5ci)+cq#=h#8e4>}q_ ztwIeKWRh;Wf!v}>SjgN!uzWgVY;1%EqX3`^Y`Zqcvf9@!45wmRA4>RxSZ6liMQ-@9 z`bgc*o!rcw{MF#EwP(5M?cC+f+~sGvD-G%UndSikM1OIj*pRlALUX7<^U>ze@o;`| zc%sqWJeq%KK3;pY_E3L1G5hq$wT8N*K<3bc#qTa||5FFpQp;Ml`&{AP4(;N8Zxmv7~p*~86& z0-p0Y|0v%aJpMYJ?C)umcX}Y(|6u*WdSktL;^YsdKV}QBM92<)`C5i-{xt~c_nIRY z8|7^^|FSnn>+!#gjQ+gz3pu3yLCybbM?~U3{kPFaze|JS#UyLdQCj^3% z*;!Hiy_iJ2m}Df&MhToCKw%t)&eD?<>!C$20V+ETYXdMWFTf5X2oC9b)xid5+Ere) z97orm;6FeI5qlo>gDc^yGRn{pkS?FJe)%1I#5dA-a(o96L@kBHs`$0XSGSaz|I$(< zYNq?T2~{Sn_R20r&<1>vb=ya1fNe-v!s{n&vzTB-4ft{Brrpa4krl7V>^H#(m)c$9!56}kjoxCJ`k`$#s^6$x3+|W^6DJQcqPqW}UL)T=m1`jgmWjjqo<~4-%akCErZ+ z&vThwK?XVBe;)xxmLU8Y($C@eb13{3iknb;4yRwqAgG^b-pC1I@)aO>(^VA4U!gb0 SUw@i(554oW{SwrPxW-uc#!SnzGbO|Iq z2ti#2Syw>OLlDx#5Y|;t^$0{VWGkU<6rw^8PQ>&W#Dp$S#Pts7&=Zi*HPG}<=+wKQ zOYeqm(XUMG(vy(XQ;^crkk)&kN3@3~di6f&)BB-cT!$xi!*0A!cwzts#CZ?~#d!}5 z;aQ#7qh}x^Y$FqU^?k5Ue+gd7uwJ&08PR@b#Ewh;3Ss|jI2-?#F0v7>oOkl>4X%Mz zDq3KA)_ldudlnCab#uXTJ+6XPw81KPJiGvoIa3C{%SB^z2FmlM-&KZ1uFRmLIUat; zEtl+>MJ|()4e(fD0noW;<|`GvREy}{v*)dBh{rFLXYJDEd?`O`0gvQd3ohI5*(Dxz zsKZ>q1|CDbyx{p43BQBphI1~#Ux{xmw9dr`C;LST1 zET@8c{30&$>{M?2>SD#pN<56S7|(lo9zn&onZ$`I=K#up{c6*mYj$JgWn7D0nl1Pb z)R%$A#gp@>e9Wd91Plg}5rCi}foudpG30&*AwxkKHbN*>BaAX)s3@aG1Z9k31#u$= z9kcOl2UpHG<-(0`lj!q2XNBfX=w1}gApW0ZP#|CfIp(0|ABKciE$dm94Njhi4EB8{ z@rdmj3x$H^y1WA{cfs*4qmD7aRm-mB%(yf#!=1CQPQO`(8`Dm~syOBqD8GX+aHp^2 z7w0jc83aYSRInX8@7d+j^rZFfk?D%xXKwE^-SUk0ZXT>dM~@sDedW;6(W57(=gURQ znHK9CsVs8UH0_e@nI=#8ZO!XGoQ&IrVu{uJ_AJXECF+Ai%kmd${}XIvg$AriXDa^1 zGue+z_?TU5J%MX2tEuLTE(Z9*2#f_vQZ6tR+S1riry}ibhIG8)xfFbXQ9(m)zJyb; zcCW`9O(i)eO?9;QzqKnT8;TJ!!edH?p-rOQ2BRKswxUI6Q{OzJ1wAEkATHF&&+w%r zVAJcsV@2Ebz%F>^#UhVhC-9k*`FTrVhxh;3@LO4rPEqFA08AH&tUK_L!yXa;wX=p+75=Sn$pJnAq_yJz=7-HtMfGBxOgyqc$!x;JXL+avol6Prtn-Ps~xueUw#hpjCww*+CmuMOz+u3{n zU~S*AD*if;FDvy_zaP~<&pte~k{Vt~U0IH<_3gPk^eJ)93?9E;N5;TV*bgQ;@d8hB_vfLeHlpwCLVC-q#UbEgH^+5M2=;T~~3- z3c0p}Gy@}ODrgE0p~m|#;v)2IT$Pmt3gHM1I7;Fb6xrz0KvZWf&!j8{=rO*@YGbGPR{`Q>W*U@d+0fmBP4Ra0ZDsn?dH z&AwZ!=|ju0wXXD?q1!{qUG-F^p3c+v7gU^epJ7mjCa$sk4a@ zBAYXd7soN(lk6h=2%mR5F%gqB4cnRN3XbuaDRP~*&1Vo(1CkfVw-8gSt%N?@NfhnG zw3h6vC5NlY;kwpU@5!z!EOlU=1yj-G$a?gdQCib_SJYmgpoox~2T_I~s8?T{pk5^7 z+02vV|3RLlAj$2JCn4oXIXS0Lt|T(0;Bzyj;PW!2YrC3{&ImUt4{1pCX-F{6CIN9R zSHbZ6*$~32*vyE+A`e1N6>V=j83ApG z2;gBnHF7cG(8OEQ<|znCP?Jj;ui&Bv8AZsO03_GMX{6QgA)2}$q1XYP>A^eBZKsw# zP)#4Whum^}Iac46BsmYxtfo%ZQmiipiaOekZCa7Q_McFB!Ssl>%^lLCx4OmV8^rB4xYKsKEjh0|;v3ee;tFLLU%>fH)Q?YXM4^feJpr zl#KyV7LU>Ak{hP@o&i+I0VPk+NrH-9pezW8b+{%f<&m6+rM72VF3|iTMIudtY9$`= zjqno&^im^L&0H$GJcLF1j6KUGz~XCu(Y@)6U`q)rdHj;2hz95<lz(ik1{A_4| ze9?kojs(>rjk4*KAI3Mp#WqSR1#Z@Zfk5C9E7w^03s(M`4gQl2K4LR9HuDX;w8}0$ jVlO{tkuO=tmn`y_rN3r-zhTGz9r}qB&^8!Jaf|-|A6+5G literal 0 HcmV?d00001 diff --git a/model/__pycache__/Block.cpython-314.pyc b/model/__pycache__/Block.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..77bf4019508d30af321171907e79616eb55a17a4 GIT binary patch literal 5189 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 (5W5$O`RfQ?9=)FZ`Ci=8?_tYR z*fOz$(FKF$?(o=7K{lV?k61d6ZBJv{;DvmC4_c)iw9+@xa010!AS^_16BZ-T*aaZ( zaJ8|f(&N}_1w@7Xv#tAgleL~x)t*xuZEsZWKTGyM{P4ktYwtcuo~b3zSCi*Al9QE> z>TRiqqYp-Z^TCt06JQif)Pl)sFu4}|>k=Sw(RWdRk0#s})`Lj^r2M{6R|ad!iK=p< z!hZG3dhg*{?|8L$yuuy_Q(pvQ`vHlIu&MM@dtR?Wr2;_3qaf&{k>-4wPZK5@;0MAL zW)b{*6400E7J-Ew$GLgu3l=y>JI(p==A&uI@B|>_LZRU9vy~PcN(t|{8>nUvIfWaI zATnrTByo?AclgHwj?7v(Lv*ywIdlp~SKM*O_DlmYVr4+=XB*o!BUNSO^Q-IXkJpuv z4do|5HZNz8dZKq-?%nS*+-=)u<3J0qb^X1BMh+ql^r9PQ9z+S=h4>O;Ij~P45w}m9 zY9MOgn{FZ1h?+EkyAKJj(pS<5ShP6S%@PpgA#cvUB5edYY=c3RO*wQDw6ti0XopVd zJ6IapgCmMpI`=Oc%;AmDr8`FM?jGi7y65&V2M;)5TH`u{+pKY&#q54uXz?yr8{1Wm zQ?fn=aS+7p3$3OcuPVns8~F1fxX)_JrK)miL%Hl-Y_;~`YWwio4?h<++E1^__109a zb-3C({MprYO$S1uc*C_0L)_IiBdi9#6``H04N?I>ddVY|-`lf%4N~z#v~v)t2)jr{ zoD+{V2F(L?^b*a#i%`U2qKVBVnzV~fpdL8FrjvF!$L@CH!{OpeW(wXBf|;k6VTc=7 zK4^2R2a6>(xKLUy=Jyj!1iNW8Z(^9{TjVTsX9I#q^NrxlW*OWxJGOB`8{{{4M4ZfS z%4x2dfri$PKU~42Q=iOihDi%h zP0wbK^bcb}edz4!)Z=LXvqTRRI}Ft9V2s0T2Ktjd?ElZfx7ut6H^#EVV(;w2P>s~= z(s?huJ~)JSLnmnCh0+?{BckK@_>@sFngU_JbBzQrgoa&j3V9vFnRIXu8}P7Ge?ENOT8}+i_*DG0>W87c9%Zv{M@)p5o=ULfhUk6NGMVd zMc`3<0#Xc1Y|f|nMx%IJ@d*AJxvSg%E+{_CXaWV^KNO#_6z59YLGXnA5+1wB;7!d; z!=fEn)rq1DMG}odjuHH>pb8eIuuv#jJOHop1#^)L)PSB=tb2tqC@I3v8~AG@*##6I ztvv4ZfH1^AAr4A$t&fPa?!#y$Pdbi%hn66Vn{1)?HvK-I?{@O_RdGU1FQJF3J@mjeHx3+ngFm6cNI?Uf$fe?9@PRAu?XFY1Nn2Dd9C*^cnfK;5 zZ{NPJ?PjwX0`%?g-`TGx3Hc3$9)TQE_cd7V5S{3>L^!TeiLOXoDv)t9MfAi;q9+%m z2+R{3v?xD9VYNubP`SyuPlYMduWVugPb%m!lQbko{mprFp@%P-ylI>WJWUx5k11b{mbR~*DT#nyJ$fcfl z2O7{J`U$$M^c0uW-tO_LQ(am>`#|pVtPe%Fe*Pg@0Kbr4jTTZlLHg|s$LhXiiafqm z_y<@$p(o#z3UF(h6y;T+_<v>A3vucg7@DtKK6LcaLQK0VQbvGYLx@u zSTi^4P$~s(l|8p?J2rUYxyGv1I&UFC zNd4KyrWo0<0@nK~7C9tN55kUsV0-v$z`h{+xskhLx5w`0Z|CpV_9o_cC+5HTky7)fcfTKR zYby`cmH$Lo_ylZxUg$1`}n0PIjSF6`znwDXG(uC=v` z57modGW(2-ugTlOf9SKctj=qMLwS z2X~$0N9e{w9hFNo2I97(#MSqi!8n0_t%e?(pkdM4DjqgcGd g&%!NiCw~lBCqXIwnWTRur~Xo==raA2AP?>T4PU0S7ytkO literal 0 HcmV?d00001 diff --git a/model/__pycache__/FolderorFile.cpython-313.pyc b/model/__pycache__/FolderorFile.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..214f09ebad1f47d6b7245e1b56ee8daa1f369be3 GIT binary patch literal 2254 zcmeHI&2Jk;6rWjp*J~&4$_;6v;wEmCAdE=VCIkseKnN*nVi85%@F8`j(PFRDt=GF| zb}cuesyLyhm(WAi9(v%K8wU=(!Jp7zq@V##F7`Kt%$cu0+v?%kditxzzLS zPy;$dKSP(5p5l_)+dW-%s!NM#AISYH>q8N)pMO9W!7rqjqlHvXk$yYFvAS=WB2RA> z{svZ0=*c&v0^FJ=MR`pqeqaW6S)?mo%XK`n;&1Fyp<35$KVV*MHzg#`7s-YhToK8u zp6zn{K#}I27qC#Pgjipwb+cwwZ0^H-r5Yx}$ImGU|GhVjk33#8oU+w$*gE&FTIIku z*3GRtluChHWzQ|!jt!o8uCZpd&Kr&Bj18VK->U{KlUuX%=V#9?%+8;if8D5i70WTA z)O#E=KiAk2Bb!#hdLP9if5h2=0+~Aog5BY-1N)pD(-Gl9yUqX&c}pt0B{z}`2}FmAaN61-@bnT{r&vXUVf>a zf9JdLwzm3EUHwmlg-?K|&k4+naKtBp^+!bOE37acXP#SDcszsG3&4&<sA zidPA9%0vG#9ef&?7-cNNFf$0a{Yf*he8%`o2*3&r8z(9WtP6bn4}pC_IvJvzd?Kaf zQ{4pYI=JhcJVrMj>Zn|zF%Y*MCDyJ)TKKujD>Ql~zNDB8%TmZy$MkFA?<4YJ&@-XF k6~*FhJ_on3o%}Iiodl)yCzAe!ocUduqAT<_f;_bU2d?3=7%Q6n^XVI<~V;;@EK;$HdNmOdaym7STVVw3HNR(#o#Op%$x+y-r;=yJ2@t z6$z=FP%0r2+D5de_J9!FICA1p%H1Xxg)d{~DRq?mU=gT2rXDMuJm>_Lag(3nLg?tDlm z^H~!zqm_sHAkNnz0~(V8&5>d>Hshok2Q9Y7rC80i=!|oOolTv4$RfT=sRaQF)*~3# z;eu5MkhIr$iU9hvbrncM68mZ%DJJDO#3)QJq?shuZW`DdV?r?)2gQLbPZI^F_9_85 z6#!7!G@ERFK1*@TbpfSnNno( zK0j66)4h7C8je-MqbG6Az0E?;RLu*%fLU_jK7X*<^U~_+ zrjqE;?JKvgY(yun@r{ygFNBBdoHN??Tldh~Of?W(%dAh=Iq=8Un6<=>_FuY(Y8?0? zH9*dGhso}H#(u9H7^L+04Q@@m*;WpXY<7%VMZH+m*H+^p99ez)!JgSf%$j~aSMVXE=htwyn=wYr6|*9UJyPgin5hy7leF4(+#WV`FhM~y46U$GG$dURv?0vZ@dsKz4fOwlM!-)o==ddpPhgAl zZ#yQqXq^RDXPrK237ZyoX!NsS)=@1pMYV|gsA>0gZ49UMo2z$XsId&g{0hFu&izdE J5nyQ4{0$CK?7aX0 literal 0 HcmV?d00001 diff --git a/model/__pycache__/GST.cpython-314.pyc b/model/__pycache__/GST.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5f258d618515aa35d7c875feefb9b2977e5d3f46 GIT binary patch literal 2343 zcmb_dO>7fK6n^{X*k&EuiQ|yO#Q7l!E)FCiEl?^#T1bG<5Sidh4Xcg42`e_$)Fr()tw+PlC zk^%6pO`yFB8HWfk;f7lG04C}RYmkNsEO2{EVNnMu+pAG6kftWA2-{kU9&|8q*asSo zDBm^+4&7KG7^w%)m^3r7Z%Dsr0@r){MWKqlbELCq}~w`SDd z?P%9%9Im-HP2;+LfV#f4Z)>J$&ta6){F_vL6G1n^#yjP4)sIkHtBd2|cIVPHRka;y zzl+J)c^|_S*s;xb8hzf0@J0-`m-e}OaTvSc?6Vj55KqST=AN6lbw>Ylz7uyVj*rI4 zn|f_9`5N8H*XYV{gxLi$m8h9aP{f~;v!<-(MSYIck4omSqMXWqq{>;zcvm#!EQ#7q z{4`UNB8i6U_d76QTqS%JDh$u6g}g}uh!xHqcZLZ+@nqHfv^+z?GmH2JzTZgr6}?l(mZ>)C>t)4hJUWRRa@5$COpJAXaNJh`*3`C^^C) zmV{kxv^>fWVyL*xp{x7dH>_){-USc(tc;?_RwNW@`iVnftG=a z9|H9@W$+RIX1Qh8{F!Z(c-NgPx34V5`)=@6%BmllyVp22Ui({9S8=#pA1P*T4qYF5 z#zDBPNEMSegT>%4Oq^6r)MG5`P>UliPFwJ%gLcqa;O|>U5<2>B3*a0k0Woa*2B)WN)#fk6_2$9 zXRe+3e7wv@m-$$Uk1g@7FP|R4)L%a3F_#yWcC;Wpi2573B FzX7WV2QL5s literal 0 HcmV?d00001 diff --git a/model/__pycache__/HoldTypes.cpython-313.pyc b/model/__pycache__/HoldTypes.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3cc1ffaf52acd3e899eb028598b263d43d1c7130 GIT binary patch literal 3679 zcmd5<&2Jl35P$pS?~gQb(j?T$HjP9L%2!cZN(;0krEStil^4UImR4rHNw(eC?%OpX z2&p|GM?$I)swgLl)Jt!iIq)|mDkx^5QU#~poCM01nYUiAlLq=BA@P*V&U-ua-gxHs zW@fkJ@d$x&_sbuQP2hgVi8sV1vwaenTSOrWH%n$X%E`#{v;2%e1;z`r;*3Nkne>t& zqKN%OktTUpn+6sFncyRwWH={C^=nn#vVjeqxAmo|Y;GF(n8&}jQqeV6(WT8@$o46i z-y(TJIfYPO;i#bSR8)k1LM1$r29&50=qFb=8dQQX4=Ev_VI>SSqC{wPLCVCO@VT;C z#8a$C672ZN2wEVQD>^OMM!96k^JOZdjLd}Os%4ZGWY0W}bFE?&O%vdMTLuE|;c~8!PG#zi{0C)v zHE(Kq#Z)iR@)cdPt^B3J%938PWvXlCl4h6&vpYYhFHhtv?mKnoJJl-B+sg&2j~tm8 z8Gmi$$oP??`K5AEH}frLj8;~hkg6Ib!&X(N(_i=2LSU4&56BHt?;EIzpC{_+!J7C} zsDB%Ou;#@HCn(Dt(*Wicx!iKY<(5>|!xqXnhr%7_N_>vX2D}yjnGO5A$0@ubcqq$8 zy;c7?*;4>c^mTfsPrSOsNtz{&N-g80~h*`-ticVNt zG|XbDu%tUl%Pv#BSfOQYUbnSHC+;s4ip5NrqGu^$+X;C)IwGKY$q^QG+mS4r8Wksa zx>!V1(-?}QcQPWypYNV6qSZ~?@mvdcm0{2%FisS-Tbp-#oHg3Ej9XfGA8wFell$); z*f@A}9sZKXYGOV0(pMM0xUi8LUQZ3*x%}PG{nX?}>co2L#QoHpweZ7G>}G5=22%Cz zzIxAK{lG-M|8OHp!rhxB5RNoDNdLe$179~$cyJ>d>kdP<=0E@#2KbiswCwW%4Pn%{dHW}WHY{bV4#q0P8 zw}HGA!-zASsz#Caz+xuoV~K)s#EH0jP8aNgj~gdYs8sY)F%$6dLx)gYq~&GH5sM)1 z2*_hB5JXKF(U(!GuVv!sE|c+0s|sWf2qn92USGX_XL;?!h1&JCfX=iJ1TTTSioys0Iu2x4=0I3_XQKM9@VCDjLv0Pn9^p@P zZX^cQ69czDU3-0IEitf`n5_x_h=9id5=%bZ9rIM80WnWBA`osj*ce=I;Js$Set{w; z{0Bu0a>SmQfy?nw#zZV*tcVFuEn>nm6tRGtMOYqi>sDv30vEYeq!?V9g(}F(6J(7( z@EPp~;&k{~rpol%3ljqo9YA5oT!hM~5js;e>|GT}YRf1-st$p;g~0YRi4#L@4?Iqf z$62H8>!(ohg-cR=HI5}IB{!>5dIC#Q&oh-ItsPmdigX+7dCoK<01Ec3NPq&YNdi;> zA^WtNBs{GqL6Q)gbtEyZo4OzO47B`UwiCbx2d-~hpk2Wyvqi*xu`9N1-3D#Nc4xAn zZ{tb5elT~MatXtE7RZzN0b7XVY7*nsHCRuNG=e1DyGcZ79%BFAJWxoS@S!pXp9;_q zW9tL`a70z1U#^K20jGxD=|Y)j-Nc`Fi^#{ZC$ zQfg~B0b~fKf65VQ+|@_bxlGWJ=FNh2)eTsJb^N0k1^nb;DcwoBi#Z#opL+A&`H+=Z_zne!=Ayyi{218j(b4*9*{#1$l<@l9LJ4s5u{A_Z=t;IiU0rr literal 0 HcmV?d00001 diff --git a/model/__pycache__/HoldTypes.cpython-314.pyc b/model/__pycache__/HoldTypes.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3a51958a67e3a78e8d07314cdfddb549861e3537 GIT binary patch literal 3843 zcmcha-ES0C6u|H7%~7mtNR8fyv*&)Cd+*%) zyXTxeoQOvWl-pnYXg&t*SFHF&@|2BJpxh)^h$2jo3j!DVP!CK5E{I%oba6tuAal8o zbdY|cNL@scj|E(79#nz{NG|jnR&#8?8t?ZrQ?e1qe0wAwp2D~ft#fw%g&W(7Q3@fe|CwP#rdqiOHF9nBJNNm zB)qX7l$%5$c~Y!*D+1K&qm=-xE1t8-)SZAGG1=uMO*0s?(!R+{723)C1~ zESSaF=M6e~WN_q_!J{KbkIycabij3vd8-wGpAzTWr+ z4)E)pqKNn=?!X%`;SC^Rc4PryQ#q&mnXho`~_kMZt^NZ{0gKO!7tMC4>|6cml_4J9g^oe`v zH!G1x;n=O%N(@F-JG!f#ebs$Osy&BlZ6wn1lmsKuT8i{!zRi5?G1zLA!Ru)wZa$Rm9LPOdm6JS60``-EyICh-oxr6lZOb2kPOMAbChX2*xAYR20vq2 z^^3~|+-r|EV6tO~fJ3fo>a+vQa-jxXbRW(Wb>|r`@InJ5J6I@}jiR0lHh`l2I9#Ho z>&%vP7;cNGpZ}q}?FwRKpo9Id;v#&B2PIL!cQHeP|IrtDAgFRFj*WP=hnf z=@>rUfkse^p|y{b-RWy=sJZ<#8tyo(#aH4O*3wxovh^Opu-5rPVNGl13>~M#+4rFA zD5}BBZynWu35;vrYLFO-(@6D}vMH{K&x>nLVkEi#BQb7RMgxMJ=!TvkrcCgv=S&9N#D$)OCVNlgYB%p?8V$~a*zP|tDM_v*@%GyLs=b4? z5Q*%0N+d`}V*i|uP>lIGVJa8Ld>fM~$hV`a3iNspp<9fk8z9jeMW{6)~z0*4Im{UvTNu->JrnpI#-H#M#q zmZhrjHzeNF2xgcnoOTjB8p8Qd;M-t_LHzM4oW{@6=o2X*XP-pl^2pO{QcgcPm;kXK zs?V}fd89@_IPTD~p2~%6c@EaS>RxWb`SUS^7lwbxoakUD-NqRX|82NwFgIM_;&+HU sm6L0RPr4^{ie7>)+$+P}rzQ%5@Q`#rBnKXnL(ilcK^WN}C>`6s0Ab_@Hvj+t literal 0 HcmV?d00001 diff --git a/model/__pycache__/Invoice.cpython-313.pyc b/model/__pycache__/Invoice.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9c747128e8450c0e87a1f3f07754e3de4143f6b9 GIT binary patch literal 16386 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 Qm|n)c?PWR`H+kg$0#R4AK>z>% literal 0 HcmV?d00001 diff --git a/model/__pycache__/ItemCRUD.cpython-313.pyc b/model/__pycache__/ItemCRUD.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4cc7be4600436d1c27ca5cbe02058f4efab58e77 GIT binary patch literal 15291 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 literal 0 HcmV?d00001 diff --git a/model/__pycache__/ItemCRUD.cpython-314.pyc b/model/__pycache__/ItemCRUD.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..df8d5358566eb310cb9f9138e84cc67f003e51d1 GIT binary patch literal 15647 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 ZqWqrTsIY%afOqfU2(7T+CD0do{4X7u8bkm9 literal 0 HcmV?d00001 diff --git a/model/__pycache__/Log.cpython-313.pyc b/model/__pycache__/Log.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2ace267fcbe714041b0eb5c1c20d5f442c5f4f8f GIT binary patch literal 5486 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 S2#|+m{We?UeS*|@yZ-@N7gaU@ literal 0 HcmV?d00001 diff --git a/model/__pycache__/Log.cpython-314.pyc b/model/__pycache__/Log.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4c5491b024bd08e664efeb87443ebb63b921a953 GIT binary patch literal 5730 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 b(fF6h`j{6O@7D;(>PG(u%N&4q literal 0 HcmV?d00001 diff --git a/model/__pycache__/PmcReport.cpython-313.pyc b/model/__pycache__/PmcReport.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..332434c799eee13d1e5139fa561f9bac39ad95d6 GIT binary patch literal 14273 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 PUrkJgL>F literal 0 HcmV?d00001 diff --git a/model/__pycache__/State.cpython-313.pyc b/model/__pycache__/State.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f6fe37d82066d388b1cad3423a215d289e1b6cf2 GIT binary patch literal 5654 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 literal 0 HcmV?d00001 diff --git a/model/__pycache__/State.cpython-314.pyc b/model/__pycache__/State.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7e0b85edb56afc1e28637ce8963029fbf92ccaec GIT binary patch literal 5778 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 OJ?(V#?+~O+;r{`bYgyU= literal 0 HcmV?d00001 diff --git a/model/__pycache__/Subcontractor.cpython-313.pyc b/model/__pycache__/Subcontractor.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5d4373538b8e1b0897fb38ed2762a2b7568b5ad7 GIT binary patch literal 6056 zcmeHL&2JmW6(8;{KP74Z)WFx&#vF^BT!tQ|GoH&VM6|ijaK2BmEAe0JRl+w9q*EN9MloRI`b~`4ole> z870Sw$c_<_JI7cnD7WeqTt8w%a2WkJRC(=6IymuWR@6AfL zHK~Q>3(b;o~uAWnL_ne}q z=M=p?r|9cB#lDxLDE7~K%WQ^8`#WVe; zMF=B|A-sk#jxYgW1h31g%@AGQ#G@JmV(zzco4&vqZYqDeAuFoE6>@6M;1#gha$%iT z@+-2Mzhn5@9l1h5@KUrq6wMPwJ7aJw6}o1ytFmf1;d_ef=1riRp=eVm8nbPVBCqHq zF7PY+-2*)ieG1ZyL7~VlV-T*=c6cLVYt9}VN8(BW%;Y`tuhH1w-qdH7H2536TJ<+v zBsTTs))!lP^pqAo^7gktH1){)c)k`|&_hWr zl&pm=*2A$c$G;fY!-5tT9=-9H*20T=_$@8`RxNzF9-Mesx?j?RC$!*+uYULVSS@%~ z4=!rK#ab{~4~##|-p}fRgceAAwfczG0`q#{ycRfL3oIVKGV$g&*{`#D;+&Q^_hjMb zx3rc>>xmDw#D}%SM~5$s&wP{oI;qFcXz?>o&ffTTz7|jE@ntQ(T#LW|Y}hyGt8$G| z;tlGa3C%N6A9<}l6sV7%t&hz#yu=%MMx0(>V}OiJ>ESsoJZHJ78GYisHgUenedigg zdj=?aF`K_a4{!Rbze456{)$QOX(KFemW%_v(*?%V^F3W`jS>PHy5|8whs`*uJ&HA$ zf#)(yhJBp~T#7=FMT`+pjZ3AHO`;Tmv4qptmUh8)P}F_a$|cZ~wro=Yh}s31&f-4D zZ9B|%+w~1x`fGqrxqZry>-?0)Pa%^+CmT*OINczu$wA*?4C05YeTT5>9>xVBy2)q#B9yk&Kaow3I*sUGQmG@{b@_* zqZ%K55<3mfiC@t9q{b&}{KYEwbSR<^O>0BbXd{ASV8}sij)7Eh$9z>+qyJDd5vckt zGlAmMDnBRlV2_X|yvjU!lW)DsJbH7FN69?64EvnSgUN81dzhIA`@+nFeTmG2?=W#M znE=oTsI)PhS%;urwwWMsfMyu(Hti!gR`h?yU_0*FFxR}$UV5$;it6*tB~aWS=1Z_q z+uI(nn?p#-c1R;?YtD8|xDAJX74awQM~L_%nkRyeV&ruU`X}Zw;*TC7;@|Hl_QH;z z5b?91KV2jK3}eRpEadHM`b9B6`=Xd1^7dB5KfEND?K)H`%+uF1X5{=`GI`kZB(&IyGVCA(>)aG6hN0yz!T6tG0hW04i3kWeQx5Nd`37( z*ba5{&4i6&ij)%i@ud=D7W%%=!r z!0JWb;A^*)x#1F;M!=NSdRy=$t#7_aGth@Gs|r4+G+2kj@dKIufyDpky3RQG9fD{s F{6F`*V|D-l literal 0 HcmV?d00001 diff --git a/model/__pycache__/Subcontractor.cpython-314.pyc b/model/__pycache__/Subcontractor.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..838bcdf5e32f6c86c729a6e921a1c2b3c262c8a4 GIT binary patch literal 6217 zcmeGgO>Y~=b;#Z2mq@O@NScZyQepH#JCYSyu?&?kEQ_|IB4&lP!~_MK1x>DGA{5EY zuHvfDL6@Q|5TG`CaB^uBplEEM2mgcin4=doy6gf0478Wt2q}Pzp8DSMLt2TJL^qcL zp2OqJ?3?$7^S<8&ug67T*nj_*+*}_a|G-6au#L(73`};&Cq%F;kq<12C4_V9lJx^d zVM4@5Mu@-!h``QT>n9Xe;36by`xh>w7Tvw1NE?g7YJ6p@B*EIzTxaU|b!cfU_J0bK zG%)-HQ7i&cteKqUGbi8K z%w!~4);&~`H}lGEn5WnA@3FF!Uz73NnEva<`P8Z`Q8~2`zrB=5#U=SurC4hHmeN}r zQb7@^1Q*QY^0~BaVJdccJ=aCIq3KqfI)U4&Mx1W=&Eu_jJ#}Mmn2KM4c2LjDjB`6y1r=t`+Hq&a6v{&cO|HCF8$gyrCHA zDEf~lEOp~NgagaboC4UaSZioEyT=>uL zYUktX`S|m5H_D!xjd+H?y#Mv~SKC_Xq8hsRY2mjHbp|$&7XQYvis4usc%x+==8g3jz_=EiR)f>^ICWkdx}pwUDYMTV1AljTDO!aoU~#Zv1o=P0pHN7sO_j7xaue4U_}RtPsf3%S-a`G$meYnhpBd4FW}Li0XVMIFZe-?A6NNtlviM~ z#*yxceZm;Rbe$lpD&JS`Izd{0Fu1)=dT~Z+G58)hvLKT|NMcQr0yzlm;T?g4m03=< z138(V@cPzLHfq(axh$Oo`YSho6)=4d7m7-Ft+-jp9v58~JU_|}Xv7a>7i~B#%*E*h zf-nHHFb#b%@j)7GqF5>R44vm!KUnFWS(%oO~ zs@chK1lQG(`EvZZd-z3rA)~W4O!7Yl)7KL6kDfuE7@rw|ox?Z-qr`%O{m;=k$%3JC zh62%kplXC-Is`K?e*x^ss3;bsPiTf=mIl(;oltwtu^=@gbEDJqy1F(Tu;W z@iCQ;RrsrA_C?RI)-$2@OrY=Z4}h-+4eA92V|!g?Tg`o<`|y=Lb@$Pb3*6wxs7{zS z)rs+vCeXGrdsCelZ**c+`u|g%uu0|>)rmF9VtsO`I$=)rf6S@Xi3j|j_o)BF<^uI2 z2p~9zAc$Z9!CMFh0qA|JB`6UjlWT$)Y4}aM;Wr`nh7q&-AWzyf&Kxfrjyh7ujciVN zwI2nNhl>UgI2}afxNf#+3edu!IwA$7WNoa*Z1<~LJWk0igS_Q$JRzV0@L9q4Av@b5@ zrK4ujNH1*I6@dKkkb%^(6UUQMSN+Qon>bWR)9dgyh8Z++(y@9*L2sfBj+j-x&82n# zO-=x4lS4Rsnj@?_!YI$a2ny3qoFhLF7K*`>GK6L^NU^J!hpHF#euT6lDdvAsI2P zK-1ywrXtBexJMK-d1!)Ua~UNg<@2Hl-C@t6O*n=v(Q8Jd!DwI@09}ZTb;G}Z1JAK% zwQ}J-Z;<2nVqIMSUYG|k;pO~$W3cx!=H~bs0bry;#`#2)(|My4JhiGoPg%)Hve6^a zz4cubBlhF#RU@kR6H-5JbUYlf`rBNktMCbLD5Ep3F&2yEdouAoiM+JkvRe3kg3vhl EUlF8-$p8QV literal 0 HcmV?d00001 diff --git a/model/__pycache__/Utilities.cpython-313.pyc b/model/__pycache__/Utilities.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..68460ee42a60390b77a8768f4902a7a3b8fa31bd GIT binary patch literal 3969 zcmb_fOK{Ux6n(NSTXAeF;S(}}Qt?ac(wM{{Our1D5b`5sBEqL}A{6;05h#}XBm;3b znduB`Hf*};mK}>O*fmR*@?-&{X{ViOH{L(fJ~^%1W(omvS?k_w$z@Cm2pur=<yZHmq1E*wj)KCW1W`_z^dM_N&Yllikc18qFKZ zMP}Zhnr&q+s;etBXG@I2ZklfBu!)(=Owro|nY^!4T6KzLF50(MM*GqOef@)d>HhSI z3?QUNX2ymMY@J%E{3;XRwH`rlSrU-D{uIZ8tvNs(*?G-wq*Ob zb%T2QxGjZ}yvtA1rE_=6ahO$%&Cj0h`(EuUN^?gKdv2IYhFJ&h--%=glAS=@oq6yW zWx3Nvepy|h_IEjBmGz(+o#&gh4@diJ1YckwI>6KO=|*UeKMD0Mc30DyRDK0btKFH9 z9OxlKAjgOCz(ax5`7rRICO!f@luBJM03UAR+kl6%sq3S_3r&1G@K8c^eGK?$6CVe@ zy@^lAF(xj>lL=RxrdHm}S#(s*Wev*8=sm4gNoYJq7Ym@W3kVzroE(@T2`Ko%pd3mC z4wEE5AA*!v`Xwv;}*kv``P zj5s5gonu!@;#Ehu`oTd)tb(RNn2Nh3HJhbbsmd=F3}e+t%8r3uTL8Ahbfr4k-Ma-{ zLy09>hj!@Ui6?`@rNQBi!4W5Y-Z?+(^k+)qoFmL_h4wZX%8tPH#l(oJ8(_R`A_~dn zQeKSwlVEdbJj)khL4ncjEw3u^V6ZSUe#Rvp-FmdVt~#mFk~roFV_OY=&xC7JZ-G_n zfpJ6$ivG|hDJ(5BO61b&has4EaRl-O5(gD z%ztQf>^KIhOM=f0wcjDn68?gEs%nPzWn;f|pUl=9m>Duu9nWyb zv0dY)k{rWAN(S8aYU`z#3pd<$zaLXH==Ck!TdH;-R_1RsTR#iSYu)T zwMPCCyYBtCAs+f=))D(1p}#rRBHN`Jy9n#p6(rjw%`_^IAaL(vdzaKxVZh58^!I@O z9-KIpjQV%Sn+$#9A!c}qb3+tvjtsAI{vF0^{4qk_3pQN~T;Uu4c6XA^!W49+g*H43 zavb+3=`E4oKgjMEp)l8V@AwM>e=m1%T=F>q@>2MOo8+Doq%SXVJU8$!0pi*CAJ5Zy AEdT%j literal 0 HcmV?d00001 diff --git a/model/__pycache__/Utilities.cpython-314.pyc b/model/__pycache__/Utilities.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d4ac3392843744d8097a92365cecca05a3e58719 GIT binary patch literal 4100 zcmcInO>Eo96&{MBM9Y$8XJaSbzc5YVjhb4KWhcdU+iiYqM@k%b!A;yvBa5M_am)?% z)8Qx^xfc~E+Up({J^8w~9CNFWdz>Hz5)df1Kzr#effh}VeUFqxDt5MMn_(!^q{FH8EeB*@gEO!Z}BAWIEpWBT|Bl1u*sr{!c% z*>X&G+e?1xHXHU<$BW}2CwcMZM!RkWYX?;7Grc?vx@3cJNhds3jdQsw@pv`Hm8#4W zz$dE;SE~s=rb|4f$KZ9cmo9T!zo=hdy4GpYFUeYN+*1vsVb-Z(cqzlEH*0MN$7#d( zb=!3Mnyg`L+05mR-Jp#osFUlqvR%$>i+jq)+~m|#OHHSSFYz)T zw>Pb3gEQ0OP3BD-h6@(9WtbdhY_>Ud4TFsVnjS*)etYpJmFq5LZsq*a)$&?piMn@q zvo-v0nVmXqaD!1;)v_HM7Hc*Px4FeXGZ~$$n|7n}pNxw0bA^SuVxf4p0+6Xwxz1rF zZc{hk>M#}j@JSp7t_I}yi>uQpsx=2F;-u8drmK)J(@| zenxA@{jhu~c?rl}n|EAvVwT}sy|f*y;@6?~_6zRjquN7Y@4n@``q#;$U%a>b{(beq z%mY=NyCw3b$ZiUC(+}$iNIdL(kOhBc0CJst6x|5W;4|1&fG-zgKKwX)2NdA=J*aV) z9Rp@(5^ynU);d}sJr2lHmf5m7Y{w2|8W=?(PV~)H6 z*t_VaAGV1V({=y`Zz3Melk&l7p`k00jPx;Vfo+@7gE91#xU?G_Zi9TBcyRZ@_MR#7 zC6O%)wY=Z;*OHE7=3Vg5+=PPmp}pZ3pk_nW4HPd4P}+8lq0Is17SVnm;e~m4YPWNL z{qdQVhi6tEohgaMvM5(XaYJOQLalx`IN2$1AwW2z!H0_SSIfeQz;>93`wDJUkD1&H zfgX^H8=_DZSwpDCexUzTE>4BH82F;G(}>mod}%O>CgcFlASYi2r?3AZ!WlUxMdAG3 z?B3C@rbPaV$gT=?bw7;Pk(VR!YPV|83%$~*F%j6lJYJ)EZ2s8$NU-r3Iqd)6U<5Dv z)?oB~d1)|4^+j~^XRpuK$uM!YD7Uu1&DD%=VGKNd=f&fAM12-~9R0Y8!nwVfJ@qR| zt^;MNs7@C3Aq$$l4}D)G8gqD~9C#KBs)xE%b{-rF$f zlFG1gS|^MB@w&8ty`r?1lUN+Aaxrhb-zFN(%}>4YpoKInxNf+(_H4iV%ivWHZZkoT z>KzST$d70~ggGwue_itT?9AuCd6Ye|dsAc!LM=p-Qavas*(I3AK0<=&#Y#xZK%z;s zib5oqmG1X|_(?QOjd?$daR29n7ozQ2E)`r}?=ajXA3DQ>)QeNNx-vYCg8P(z5AhS# zf6?eI#Z!M8Ts=Qz*I)+jsf%trl_g2~n#?{VvwtDe-^3Hr%;$^W5cqq3NRo2T2$1LM Vd(x`(j39k}O;V)!9s%O}_;1uln#lkF literal 0 HcmV?d00001 diff --git a/model/__pycache__/Village.cpython-313.pyc b/model/__pycache__/Village.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..39c6b3696f38a90afe4a63e51e003f5f165038bd GIT binary patch literal 5940 zcmds5U2q%K6~5XZX(j8&l5NR${3F?Jf)Wxt2^8!Gh#ir}vg1%KN>c2yDALM^tXJml zIw1^0+?oD}4l|H;($J^ofrs$MV;?#l%J9%f35QCvleV6rEyG)I=(JNF+H-cblKj&I z&+ybc_R-yQ&%Jy1o^!wRbI<4V5NPlI;&+9O03n~^gWb4#=Uxdq*NI3(#|%k2s3XZx zCdpDZ$x$xJQ$FdW&M1yEGp?jSg`}IhlOF0xw$V0|&(3(0KI%*Qso#9&W&$(-^4v^2 z4Z<%!)1C~`kU8d@=}30c&M4_7Lqv4-5>a@Rv35ec7P+|lb9{(94exAmv7nyLsF_8X z8t$|v)6<1l3aa5za6noChS3K7;)-rP_^kmdzr0eQa?bE(S16TLYc$Rod`ez?`V}LP zlC|Zcs>!D^YEFSgJg4*}<&>-}L*F~C%S$Ix>BO^FmgP8OxL_BFjGi&v(6MCZ;tqp< zn$jYLj$q4Yt#7cXqyBJj7CP6-0-+9(P)2l6R%9qAvXmFOUP7HB57Z?(feLsS)GZ1? zJ)#?Eo9F@R72AONL@)I(`r>}Wb+(`=a3-IFKFLXgWbK#XJMko0_`eqg0y=f1i2^;V zi3~h7q!&q?ojXZ8VeCD|a2GUjC7YEs&G1oKTT%4W(1+K+H*%V+e|9HnJY_h)KMyDi)yx@6f%0Ds4mROmk%#2TVvAZF-a@t^~)J5 zA3SpS;MmcFN5+nPdts@Vla+<~7am%^VhEB{Pz$;w8SOUL=5)Y@v>=dGQtKHgac}x- z{eva$6QTDWFiEyy|8L7K}f$)@=j-QSh4vZjidXeQ&Twa6EGRh@CK&-uFT%ZDrp0Vt*0V1fz9u7+ z%f;Okw@tlBLO={IU!+S0yC~}huj#a~Y`7+KIlPfHfymYM?9YMs zd+RI-x1jML6~#t-QQiZ~)pu`%>YYPP9==3d1+q%+1bf~+Rvmt<0)N5BS1+%JdTu;? z?cr)@s1h2wIk6TxwE9Xd+N}S|SSaUSze~s&W`S9~`UP0;^GvOKu-YB3bjMfqAH81d8?N>puJj#V)o;6cYAzq` zhfQoUc>EC4;NOAHt__|^HH-zO*IU{lG9o+9fN3+ShN;-2gn$7xt`JC=j;A?_J&?eC znJvwZkT^F-2jR)^g8@w{O8tbjIAf{H)6{?5xmEcdYYu{Qi?+F@w&@{U+Sad@13?Wp z*ibKh1O>vjUN`Sy7{;_|AfVe&@4K_rk;g0W7aA|QYLS6zBwmTc-``h0dA1yhmm}v& z-dazz+OxmXv)@9Zk!t^uO8=1(cU$PK1@@GMJ=>Lphx~t!O5>c$e-oIL=D}cGmch^% zoE#lOg8s|s0o~QrxEX-7IA@v?-HWpOkURur8nUeFqR@zmnBMBO%-SCr4BmE_Gc8Mfxn%xOW&WB7SVZm9k4@467K}Z75%El32 zLRMtG;c!?BMx5xyrV~bc6HY9-9YDnz*rNCrP%%80EEggbawfoRft2HLNJf{f=S|x| zH?ExkvP%BC)mymyRaZ~N)pK*-AFh_M_rli!{8lwJBAmTugd-A_-Bpys?IK=jMmhX0 zW_{f#$C>7KjB-Nf&tjBg#yH#qV;t^l#5n&0O?>HPa25?K;w>w3nSlyuy#Xby2DM^N zc_6L`Pv;8y7VC1gz)Awm$C)istw>GtYGRCr-ok_y8(i9OF&kStY@2IgK8Y4*K}#*r zQF3l|Wb3=fmvtdf6}l@z_sz%$=K)kMJ5DlXp&Nn}_bF!mJ}yx3TmG-rW*b4u+py7Z z23pcgJxJlw93nlo3`FW8w<$c~Mj$I~<%rHr!3m7pzQ*WkI+uj3X9|iob!9pSxgi6_ zfS3B&ldlHrryo^|dNf~LQFGg|r481>Xa!~`S|XN~_>?{at65=7=#m99IqQvP@Pfx> z8h{m?V$f87EiSn!kZIwIFiT4T*}=c`LzFUB=^uN4syy}ma{pMl|GBcz`{%a)T7RsR zcysQqhqMosh1gve85l-JE%rz$aoanv9*6;ZdqmT477FB5*U2bRA?byah1Xkca}GMs z+5DX{tRd3Ke87>|+RVYqqhthK#|{}-V+J=s07rJnrbO1x(ZK!eh&1*Z9pn_`m=L}2 zDa^IEG+Rn#PInQ~MxeTZHr9}WwJ}QB((GBL>%0FrGeTlyzTI9Rg(=ULmcr;~OLLLT z8Q8UR9ToyWpS zfFH8jAk>aj=cIG)XV!1tkta{EgCy^Wp$6`M3he#I?Uf*3wQ3oL7p?{55@!CXJ^O0J ztkgu!gtDM6M$J4{n}`~`*(-br+l&XB_Yu)o%@-Dp;G(QcxeF3Js!-y9Y6c9gATS$3 zHlrw*rW$@Lq=3=}mP|B5Fy8~C2ETMgds%_>)|w7*jy7cQ%e0{C6z$URy`&XYoN891 zC?-sX&s;#t1I(;Y+2FHEQIq2>F2ZnHUtoBe8>7#`RC)~OJLGMCf}X{JM(o*oJQ9p( z{{gZ})`Q?6TI(o~x7_L4Q|&tVao54MuF;a`?}46LM^|ZjJ-EjTG1r14CFfnv>mICm zLvNjVcfED>jjPqbNF^|GkFf4V#~p95+%t|z0zBLH z)=#_x|JE=F|2=tXDn>qx4J7>RhfhuN2{-!@$0vg1BR3DMkNhn5gF}g(*=*AHa@Vi-^~yGqq~<6TMHu6#KT5gUdd<|C9|qZ(Qi|P zYl?1&BG6NeNGJxLM!+oG9ZKs^`O^!sMiH7#i#AQ&tl1cDO9%?76suSi%T)BdH61U@ zq2^tA9*4{eb_JV#_liXnVc z4;BGR(qepEK2&>|$MmBtnLX?1~XtI?7oqVG&svC2{vZ@GKt|l{uTplLzB-u(nspd;i_obM+a#B8p~CDC*hWG@f zMG6(Esh_pJ!jcaA;r7!|$-wqLOPE6@jC(G~_$VpDW-~$Nm?-lx!kn@I(j|)^CD{qm zja$P!vINpAyFvP74@kf4Wq}34g0c^`%5S(9w0s`+=afOR&3EFfaZ7nZq}wASMy6c4jnfx zpFEz%GttviiPNdsbVAk7vtp_BU&>rs!M&$ywphq&c`d`VVnNZ1OYCBXss~mwS|NQe zrOCqwCXO7KoS6JhdZn0C^J)8g50)+&lA>q@jVX%J(QIwD;J4@@5M|Qn8K?+v1{(c? z72zYP_ck<=tc^IV6A6CiR!zcpl}Wm_G3l1poM38h5i&VuYnHeL$DDIZr};4F-q|KQ zjy6Z}_c4l-`Q|N|^X{DWsNGZ&7PvY8&i=*D1wj^MQFa~^t=>Tbnt`2~01Q{N6*M}U z_050Kz#TJ->;mXW@4_X};L_lQF{E?(V)nd}g3D(giZYv7QB8Ci-QQK2-8y{wVEN@nwEt@UO1>T)uSLhNz4jhmiyp5>AFo9pUyDwcU)^v8 zUcdaq%Wq$NKV8kec9W1ZT$(Fiz5`f(j%#!e*1O}i?s%E~=(R@QXua=nt?zJ|t-E?Y zcKK-^EMg1%@k31bzhf@%8^NEOYk>wPvCD!(cF5c@`x2FSfKv-NO&x-~)Vf@tVVbMW zUaWzJ4O*!4$W?RznvDQ}X)2$$H(QT$*2#G`u@HA|$AQNhgM8k0@WST7(*rnl^QiS4 zDC)RAJZkf#=@C?jHjjAg8b+Z{Pl14A4)?w@Qy+V%HulhZ_~DAH5gVw-;#@DH*xu`b-*RiQN9wVuT5M`9b`k&?>aK@IYN3(q z$Ex3d7O17ZJwhDgIf5$KU0>2x-EQU>fvEKy-jlUh%`@q%X%J!F8w5kGWpwfi{c8@T|G5d&$YdOzFc+nth=87I!2-W&{{qb zdMoHYmiVu27#uCXNVp#lr{x!k_c4l>g|FooITwUo{i5*V!Zxo+xTjYn+|w%}03z4y zu1w=UTLPQX0+nci%4PaCfPogxwBu{l9CA<2k)FtDY#Y{GZIpvxF`U^V)r-^w+a~|$ zXjI1AiN}kRHF>AAiNLnTM-hJ(?KFa&73X$)w{id6vmpiRQg==2z7~7`X`r5qj#FG! z>IS#Qo#ZyY+ERWj1uQe!j$jei2hY;&UqR&?dG%jGI}6+vw?~>Tf%DQwWPXcF z0}-t(w71CO7Ka8SUdt#MkxeU(iG~IS4^HKnA+_^QT@3*+(%{D6*lY;%Xtc zlXko?4|+H-I??9kw7IwR49sS^I^hcz1LdqYnhp={orwm^w?#+Gel1S9#rbJ#e+owF zGKgIaQ9pP#6Se+{_ol1U&s6&-*7|=?m3lw%_BZ;6D~UH}Z+b|_NL3oX=^_K8*w7e0 zT1l+?1~!7j(7e+NYFU*M_?KmJkd(N)#kgq!)@7F60<~e%3ZUT7o5#5f@acn@N71tE z63Vs42zv<7%PwJ=%r!|3tkbRp)SRP(oB~G#{2Xx((lpykQ>(;18G*|{1}u=Z)L?G( zWww>(DAO|Dy&p0FLnMtclUe34X4+Qfy0$gIqm$(3aF0sPGzK_kk%f7uk4(0GxlHEU zat~>YvCJ~p+pKN=50f+oC}x>MFMnHsl#$rnQ`y<1ewZK3lvVm6sZ?@4=A8Md^{+WJ z@(ABgG{+E`zsEk!^LO9J=1g(P%9a>DxETdu* zHSG?payA~?ypu@Bg(Yp-2ra8j$(>W+v7lx#PXb-5$kc|E&E)eK>KXyd<$#0=CS`O( zGG7F}hPZM`e<=?kvNas|9Z_V6C8`w|MPwTO7j;O!z)&;sMKR7Y{N@D85>U-5RShwl zFY0Q%&1M)*>k|ym=ECSRFqA%m;~lDPI3NsehDT?yqvfc!?~ejK`oBSx$wmk)Mtfog z;+-3jJ@v?e4IF* z?@+z>P_6gSN4=GdU&bMaXm;qk#MeDx4ny=5~ z7I0X z!{LX2cc^}R@x$Yb)u)xUH+dy9z}s{ z*lHf@K1F$HHIuh{f{L=FQJv+rf?6ml$|lw*UN<^{;s}ZpDA2*APohBLrsm;A6h&u{ ztIH}G2C0&Bt4!75Jt0NO<}U%^ z=5+C>;a!4toL9_TFGZVAk=-d;EsA!5qBld)g*Ad^((vF}2I)tpsX9d-H^Fb-3Nu4w zxGg0}?^4X2QA~@`MQb?TtAm@@>RIeE?^eJXg#H#Li>*Whe)PM82;7f`9vygZX#ZjF39>T^igV_!g zMscv+SkB-56vOW#1UrW4Cdx|QpaeiG#0?-s%#6` ze3fklTd1;aU|XteJJ{AL+Yz_nrX_pCp*vrU&&K48oK)rI?1G&6nhftiFFDBEe*vQR z&^?Ha5HtI4X25T_+-C(P5Sbwo78rz{%u8S@a!N~Q zl?bCdDwYc0p^%QFV{>=1_(yY@l)RFWZsY6^p|m=8JGr(jD_RK4@JTA2Nhe7ab2IYm z!*eT!Ppb7vYIae3J&EOki^Bs$BLf$QE?%Ca4_ewo2Lu4 zehYL|?S|-Q=&!!6J>S5ed;>ea!F9)1PJhAEUU2&JHvc~OYv)#g3_7>FFw&a2CAoFM zTn!2uL<&=x-DC#A+{Q>9s*nb}WZO)sSB z@=oG4z4b*|o5*A;>q&j1XBy)y4lT-B>eaNe6apVCYPuHHu~4K5lTC@qq{G%(#cCGo zwp22cS;5(q-lA$*EHA*~RaY~bs@rHa;MIlYHTAWOZZ9ok_)4G+>B0(5E1E8(GFerQ z2$=lSb+Kv(sMt*#GwReCStYA0L;@yc)Z1ty60{;!f2Q|WhDEp4g~sKYp$_$r5RvWD zx91z#_KocLM%NvMcK^EgRgfQvK<)tJTQ9ydZ9P+oAFiey?3V*mO-m|X2I@k zT$XEC)_N2-LI`?hJBk_5n)PV^68|4k;8~!6y@~=t!rp)a_6hr)llBnKZUyS8rpQU_ zEK|fXb6xk|P*fRfl?`@F$*k!fBlm_{Iwd8Sv#Y=qaWSnVGi6K5 zY88uH)v(?SG}3S~1<;bx3lu#@A9|n(m23ry85$X^aT~*^28|xBOoPFhgJr^`KBBtn z8AFfcjw0J;50){r<2wt4v7lzp+qd4jRdDqeTH8OK`$KnrHko(Le=Bg!EkF{}d%A(w zftuU*#GY-jXIBiukza%i&)YZOy7`Nlg4nhvc5jQ_pNRoN8<+3xH{W`5&)K)_?Au4I z-T$T7n)eRpPUSp#_vo&8`3q0SBhh{ENyzghZACnL1Dt6Xb&)m zkDf-GJwi0hZH76LJ{_L2a;ehU4<8{!jsVJ_t*2oSlVEBXgoY4R8%!e(mDC8)57qRe z%@Zw>K!Z#bf4HNVQqm%DrXR%(g{^22v!6+f$mvc_pt=y}O>J<9UpU5ZImT~2#&0{u zZ$HNGF#Us8g}uYY$o8!Z=2-y5WsdQ)RC$&4RwI=aBPyXTl$4inudMyzO$ng^bA<_E zCJxMm-x_9~u-|27iZip=37w;YtAdNkwHph13)oWW+9QE&x&3lfngOoeC~*za;w0tT zM2TzHDc4MEmU69Raf#X38{xw>TxfnRlwJTKCpO|P_L5E-sx7O?ItMq4M);*%j#%Jk zQn{&^1l9S4q?R-|N^T*T!jxP}444`qZ8yYN_4o5@P=$V`GCkF*?LfhCs8Brv*l37U zxaG92^R-;9$TtEsKx@8xFn1w8Jh3ZIKI#nZbq;QK4u0M_eBV;&I`z@`2je?k=iiwE z<<@oaZ%z*Y+2?=w(u0@sJp()53k7cnG2Xws1>iFa=mowg45 zeZJUnkfOU{py2eZHyxxjV}FiP`f~2ITq^G#+Y_&Di=YNMa1c${i1h}LKaR3fJlf&| zQ)Ap#xMgaT+Zqz4uA!|_f#i&FBz-MB9Xv!7x}FBe|4mWozI*BJ4*<$fA_}48AYTRV z{|iF2^uHE_uEYxLdbzT9b+3_A7K4*(K&II@x(zg~lHv!%Ant`t9dY0*1RJMG)K8)_ zBqC=fCifall*JQiHQI=Lv{pM<<`t-3{WC-j!2(*%Q?J!ZI4QtY&Qxmic~{C%YE*ie zI=Gbn zGsKZ}Z+J?m6=~(?qOYEY3H2h4TofA%G&atW=b}R%i}+fB21hpsC5dpX3#Q0;LXpOm zMH**{w1qGtm&S%N&8(eDm6W);o>)QF11mtpL>cM2ZLswqeXo+l=&vQ`U1lY{WeEg$6I z5#kI#0}UD1;y#jUbT_sd042MD$}rtRUY_v+rgDtplWH<@^D#78=_SogyK@XN50?1} z)T3TKOo|1HtiOtw6%|%6RoKPcEBQ-PyW(`g=U=~GsFh;=;77p^f;;|m?^vi5`-cU2 zI}7dMVyL0cDF%_p|Gw|RzjeTIDk{jvi53r zfj;Nr09h~RNpzJ&FOcX(h;*MM8KtG9rs4GbswS(Fgl~`n1RGS*8`lXQO!B9aHSu%w zNND|r<%Q0D4q4nql7g2Di~AeaEp(UiAXPj|^E!oKDGyS`XK7xy;4S4rs`wnuYcH3B zRPiFs>nY{!vyd`6MY<8;Fkk zRa>P6VxSLIfC90T7EYd$7AV@h5&Z#0E7>o7q9P??H$h{x1&X{eu+aqZOV8|uONx}} z)a_db(Cj&LIdjp>H{UsYrqk&_kbd*dAF`)B2>l%^Mxu4$;TQ&o$lbQaz_HmEN6om+7;IeFUL6{C!{1{wU7~V zO_QY%vOQdY>D)p|eNqU;jAhhH5Nv`m$s$Ui<9f|a=s$osGz?`hAh8@F6-&C1zmdJE zblntWKC{AunitY?wvdkyio?_-0*r)|pGIerS0q7{l5-0$U0hBs2-5fELb3KOrq)-5 zyv&ONbWLY-*%UU#ONATq4^yHrzM9JBlkFN!o*q9nH9mQ2@@!JwhvZEOTU0k@qPVVf z;<=hrQ-=2K+eBl%1!gV{Kz0lL!`Hv(8~?~RzT-Q&;rQGY_>0G1aRth@z(c%84I9ot zfx6*;g{3!aIF#;B=5|k@B4KWMriiyZfnwO&zQn=VeHL#kq$1gm5@gJ(Y4tfrPU~%g zNVQGI?Dh4UY>e+lOK@X~RDz0i9Nel%e9woF9Sz|zLE4`TYe78o!|UO0CEzcFlB{7R z)@exQH|92FL$aVu0PjwbPQskE#8GjYMU!=F9z+~nq-W_%3GJIDvgkZDh_XZo#oNq2 z-e&fOr3e%M5Ken0s~-H6xUO`)AjosMoO!UM*9R9xu^@(S2y*(>Z2o2l>O%UVlx9N9 zkq(iDHHa9x;8v}PQbAN~=~OOP6boskQ<4j!kbz??t>t7%v8i^TR$*4xrPp$bT|dy! zi%~~Xn4*}?%LF_+ferTa4qwF=-1CiW`$kGf%f68v-?JM_ zRl37DTygi@b=`55LsPr%=?B3hx7o_j(Y>Lm?V+hphR)nxstiYdJNv8Io#E-XFMUa& z;MppRy8L(9JM1qyEAGBMcWB!k+I5F3uKu#Ef8!^Ref_8dc>{N6@67IbMz=kqj}YNJ z|C!rY?mbgVm0m9QoZEBHY`bS3S1G7?T(vYC{Ma4-MvB04em^tsMw{+n)Jkoh?2MAs z=JQO{fwo8nO17*NmL1{f2`XKGAMrm{#e*RL+$Y8ZYr@(Lzi`*Btq{SucST#m))EVd ztHjmetBVQCgw=T8;EuL#OSWulJq$g92xewG3^`yl>*4hr^&g|bjEV;KIy9i;_9ir7 ziuPIiwKlVix0@wAfShWn9#5XuY7I{;ON#em9^OgWylI#7xpl>(m0ry2Zy+v=8_<$f zd+fs&fR+y(CbXzmUhIamh@bA&)vPQ6h$0cAwAh0)KhAm~i#QZZaW&273yLF~|3M*} z7O>2&q@-+`PpuZ#0A1J{*?cNj)8v9|YSfx6Do(&9Sxlt?zW8iL1tDz?_(hfQr5Mpt z4RyM;b<{(S5;pfiL!9O|VQlXU$o8X+`b_s$$sT*K1~@yuQNS7ta(dpn{O09~d!*9k z{~-CtfpR=mcCS2QC}$^Nicx#4MXdul{d?@-Haob>1}h$a6$(6WU3&A<&zCD~&mKFl z%?^CR4q@DJ``&uv%{TU3N4H%^A0g5n_>Ao;dry~+lsx6`GrR2BPd&X4*zV@ekEywK>rl4O5#J=;#>Q3Nui$b)3TTSa^Y@(mD@- zPWbWG(deUZgGNMxXhx%x8v7txAreP$WR0U>R#A_L>O>H5?F5=2>hOp<3}bYLLMQ0M zaHL3@48@pnMpC&H8N=_=zQoZ6O9DpIs-9_6Z)sC+ZBuV+Q*Uom?=b2otVQw~6~ni9 zUou{YAc_-h>PZ!YYdowK`-))<*_UdJj7ip5AJ-9QnlKm@nOWvioKwFI2>O&WuOTSA z92fEYRVHSF$Nc)mjM51@uRBUS=$6<^QGOXv?xK!zAhYu-%FXF0x1^$+p~Y2{(>3Z! zoO?ZdFu4n@tcS9hnNWmng~B4Ht~#45*<4e@SuiLa2M}oqu;2%EZP1M_)^69xh3C_-VZWa9EI81g&dyE3=bj_DNKp48 zoeLMJd(Se9G`e?zfs*?K1@e75yf|_IXYBhnDE;5&jNMnKuOGuMcQYwT7EFb43x;sE9*+4nE>hoE^j2 z2+r_BDvsg|dqGb`<L?zo9kd+`Ws{|kp`%oXFrwG@udPjpF!kS%I>sA{rLGcH?w;Od&paK6bId!@rZX5lMs1$xjE1Pq89F+Own7Y) zY#pPpJQ`kbG;+9Fgz4*VgAUCc&OVI&w$}WOmd4c`(8`)%8aONwa~YP+ zuQ@(PCYUL_pNGz}{VE`CR*Q_aVq`I|Il3VMOFOT6%9dn}C33d7rIDp^_B9e>XGGC5 zYq{2Dp1>y$T?hEes+|U=_mnnkOwX}oP(3dM-Aw!gu)}OmJOk@iybBQitw%?d2zod{ z668jJH1HAP=Rz|+M#!b*Lx`Ms76#KCYB;E^y=tq5tvDK2i@&7{Vn$S{n~JO|f5V=V z#^3a4T(>%1e;}xqYscvkjE1nYfYmpi128fEv~eM6cQ3hW@I*9X#0Iejc{+gE?RHJP}yv@)*77NB3O}$oIVzmi^(ybJT}uIpS1oJfBak3Ouhk zcpfPCS`N!B&%d^o%IP&Op1+Y5B{`SP3;6=BmSlLZ)2o8~Y9XUWiN!&Dgr30}M81%S z`MJ0Rnd0Mltu>#LWih+5CJPdn=;HZwE+t7gPMsDYsLR7|G|sw0=!Rni-&Lc(nzurO zj{l?|L-zI0;6;AGbbU$EOy5HaS-PuOf=G#_`%AK$8PLl>s*b8l8ApEP^T23MeN!rE;a5wQ44!(UK5_H_#)2K4mixA o*e+^F11u< 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 literal 0 HcmV?d00001 diff --git a/model/__pycache__/payment.cpython-314.pyc b/model/__pycache__/payment.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9792ad9da071d7b44d4a06f4df82be738f40ca35 GIT binary patch literal 8203 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 R)*sA2GZDdEgmY!!zW`z^10?_e literal 0 HcmV?d00001 diff --git a/model/gst_release.py b/model/gst_release.py new file mode 100644 index 0000000..732f9d7 --- /dev/null +++ b/model/gst_release.py @@ -0,0 +1,150 @@ +import config +import mysql.connector + +class GSTReleasemodel: + + @staticmethod + def get_connection(): + connection = config.get_db_connection() + if not connection: + return None + return connection + + @staticmethod + def fetch_all_gst_releases(): + connection = GSTReleasemodel.get_connection() + gst_releases = [] + if connection: + cursor = connection.cursor(dictionary=True) + + try: + cursor.callproc('GetAllGSTReleases') + + gst_releases = [] + for result in cursor.stored_results(): # change to procedure + gst_releases = result.fetchall() + + except mysql.connector.Error as e: + print(f"Error fetching GST releases: {e}") + finally: + cursor.close() + connection.close() + return gst_releases + + @staticmethod + def insert_gst_release(pmc_no, invoice_no, basic_amount, final_amount, total_amount, utr, contractor_id): + connection = GSTReleasemodel.get_connection() + if not connection: + return False + try: + cursor = connection.cursor() + + # Insert into gst_release + cursor.callproc( + 'InsertGSTReleaseOnly', + [pmc_no, invoice_no, basic_amount, final_amount, total_amount, utr, contractor_id] + ) + + # Insert into inpayment + cursor.callproc( + 'InsertInpaymentOnly', + [pmc_no, invoice_no, basic_amount, final_amount, total_amount, utr, contractor_id] + ) + + connection.commit() + return True + except mysql.connector.Error as e: + print(f"Error inserting GST release: {e}") + return False + finally: + cursor.close() + connection.close() + + @staticmethod + def fetch_gst_release_by_id(gst_release_id): + connection = GSTReleasemodel.get_connection() + if not connection: + return None + data = {} + try: + cursor = connection.cursor(dictionary=True) + + cursor.callproc('GetGSTReleaseById', [gst_release_id]) + + for result in cursor.stored_results(): + data = result.fetchone() + if data: + # Convert to array for template + data = [ + data.get('GST_Release_Id'), + data.get('PMC_No'), + data.get('Invoice_No'), + data.get('Basic_Amount'), + data.get('Final_Amount'), + data.get('Total_Amount'), + data.get('UTR') + ] + except mysql.connector.Error as e: + print(f"Error fetching GST release by id: {e}") + finally: + cursor.close() + connection.close() + return data + + @staticmethod + def update_gst_release(gst_release_id, pmc_no, invoice_no, basic_amount, final_amount, total_amount, utr): + connection = GSTReleasemodel.get_connection() + if not connection: + return False + try: + cursor = connection.cursor() + # Update gst_release + cursor.callproc( + 'UpdateGSTRelease', + [pmc_no, invoice_no, basic_amount, final_amount, total_amount, utr, gst_release_id] + ) + # Update inpayment + cursor.callproc( + 'UpdateInpaymentByUTR', + [basic_amount, final_amount, total_amount, utr] + ) + connection.commit() + return True + except mysql.connector.Error as e: + print(f"Error updating GST release: {e}") + return False + finally: + cursor.close() + connection.close() + + @staticmethod + def delete_gst_release(gst_release_id): + connection = GSTReleasemodel.get_connection() + if not connection: + return False, None + try: + cursor = connection.cursor(dictionary=True) + cursor.callproc('GetGSTReleaseUTRById', [gst_release_id]) + record = None + for result in cursor.stored_results(): + record = result.fetchone() + + if not record: + return False, None + + utr = record['UTR'] + + # Step 1: Delete gst_release + cursor.callproc('DeleteGSTReleaseById', [gst_release_id]) + + # Step 2: Reset inpayment using UTR + cursor.callproc('ResetInpaymentByUTR', [utr]) + + connection.commit() + return True, utr + except mysql.connector.Error as e: + print(f"Error deleting GST release: {e}") + return False, None + finally: + cursor.close() + connection.close() \ No newline at end of file diff --git a/model/payment.py b/model/payment.py new file mode 100644 index 0000000..90bd738 --- /dev/null +++ b/model/payment.py @@ -0,0 +1,158 @@ +import config +import mysql.connector + +class Paymentmodel: + + @staticmethod + def get_connection(): + connection = config.get_db_connection() + if not connection: + return None + return connection + + @staticmethod + def fetch_all_payments(): + connection = Paymentmodel.get_connection() + payments = [] + if connection: + cursor = connection.cursor(dictionary=True) + try: + cursor.callproc('GetAllPayments') + for result in cursor.stored_results(): + payments = result.fetchall() + except mysql.connector.Error as e: + print(f"Error fetching payment history: {e}") + finally: + cursor.close() + connection.close() + return payments + + @staticmethod + def insert_payment(pmc_no, invoice_no, amount, tds_amount, total_amount, utr): + connection = Paymentmodel.get_connection() + if not connection: + return False + try: + cursor = connection.cursor() + cursor.callproc('InsertPayments', [pmc_no, invoice_no, amount, tds_amount, total_amount, utr]) + connection.commit() + return True + except mysql.connector.Error as e: + print(f"Error inserting payment: {e}") + return False + finally: + cursor.close() + connection.close() + + @staticmethod + def update_inpayment(subcontractor_id, pmc_no, invoice_no, amount, tds_amount, total_amount, utr): + connection = Paymentmodel.get_connection() + if not connection: + return False + try: + cursor = connection.cursor() + cursor.callproc('UpdateInpaymentRecord', [ + subcontractor_id, + pmc_no, + invoice_no, + amount, + tds_amount, + total_amount, + utr + ]) + connection.commit() + return True + except mysql.connector.Error as e: + print(f"Error updating inpayment: {e}") + return False + finally: + cursor.close() + connection.close() + + @staticmethod + def fetch_payment_by_id(payment_id): + connection = Paymentmodel.get_connection() + if not connection: + return None + payment_data = {} + try: + cursor = connection.cursor(dictionary=True) + cursor.callproc("GetPaymentById", (payment_id,)) + for result in cursor.stored_results(): + payment_data = result.fetchone() + # Convert to array for template + if payment_data: + payment_data = [ + payment_data.get('Payment_Id'), + payment_data.get('PMC_No'), + payment_data.get('Invoice_No'), + payment_data.get('Payment_Amount'), + payment_data.get('TDS_Payment_Amount'), + payment_data.get('Total_Amount'), + payment_data.get('UTR') + ] + except mysql.connector.Error as e: + print(f"Error fetching payment data: {e}") + finally: + cursor.close() + connection.close() + return payment_data + + @staticmethod + def call_update_payment_proc(payment_id, pmc_no, invoice_no, amount, tds_amount, total_amount, utr): + connection = Paymentmodel.get_connection() + if not connection: + return False + try: + cursor = connection.cursor() + cursor.callproc("UpdatePayment", (payment_id, pmc_no, invoice_no, amount, tds_amount, total_amount, utr)) + connection.commit() + return True + except mysql.connector.Error as e: + print(f"Error updating payment: {e}") + return False + finally: + cursor.close() + connection.close() + + @staticmethod + def delete_payment(payment_id): + """ + Deletes a payment and resets the related inpayment fields in one go. + Returns (success, pmc_no, invoice_no) + """ + connection = Paymentmodel.get_connection() + if not connection: + return False, None, None + + try: + cursor = connection.cursor(dictionary=True) + + cursor.callproc('GetPaymentPMCInvoiceById', [payment_id]) + + record = {} + for result in cursor.stored_results(): + record = result.fetchone() or {} + if not record: + return False, None, None + + pmc_no = record['PMC_No'] + invoice_no = record['Invoice_No'] + + # Step 2: Delete the payment using the stored procedure + cursor.callproc("DeletePayment", (payment_id,)) + connection.commit() + + # Step 3: Reset inpayment fields using the stored procedure + cursor.callproc("ResetInpayment", [pmc_no, invoice_no]) + connection.commit() + + return True, pmc_no, invoice_no + + except mysql.connector.Error as e: + print(f"Error deleting payment: {e}") + return False, None, None + + finally: + cursor.close() + connection.close() \ No newline at end of file diff --git a/static/js/block.js b/static/js/block.js index 801f38d..2448888 100644 --- a/static/js/block.js +++ b/static/js/block.js @@ -1,87 +1,116 @@ window.onload = function () { - document.getElementById('block_Name').focus(); - }; - + document.getElementById('block_Name').focus(); +}; $(document).ready(function () { - $("#block_Name").on("input", function () { - let blockName = $(this).val(); - let cleanedName = blockName.replace(/[^A-Za-z ]/g, ""); - $(this).val(cleanedName); - }); - $("#block_Name, #district_Id").on("input change", function () { - let blockName = $("#block_Name").val().trim(); - let districtId = $("#district_Id").val(); + $("#block_Name").on("input", function () { + let blockName = $(this).val(); + let cleanedName = blockName.replace(/[^A-Za-z ]/g, ""); + $(this).val(cleanedName); + }); - if (blockName === "" || districtId === "") { - $("#blockMessage").text("").css("color", ""); - $("#submitButton").prop("disabled", true); - return; - } + $("#block_Name, #district_Id").on("input change", function () { - $.ajax({ - url: "/check_block", - type: "POST", - contentType: "application/json", - data: JSON.stringify({ block_Name: blockName, district_Id: districtId }), - success: function (response) { - if (response.status === "available") { - $("#blockMessage").text(response.message).css("color", "green"); - $("#submitButton").prop("disabled", false); - } - }, - error: function (xhr) { - if (xhr.status === 409) { - $("#blockMessage").text("Block already exists!").css("color", "red"); - $("#submitButton").prop("disabled", true); - } else if (xhr.status === 400) { - $("#blockMessage").text("Invalid block name! Only letters are allowed.").css("color", "red"); - $("#submitButton").prop("disabled", true); - } + let blockName = $("#block_Name").val().trim(); + let districtId = $("#district_Id").val(); + + if (blockName === "" || districtId === "") { + $("#blockMessage").text("").css("color", ""); + $("#submitButton").prop("disabled", true); + return; + } + + $.ajax({ + url: "/check_block", + type: "POST", + contentType: "application/json", + data: JSON.stringify({ + block_Name: blockName, + district_Id: districtId + }), + success: function (response) { + + if (response.status === "available") { + $("#blockMessage").text(response.message).css("color", "green"); + $("#submitButton").prop("disabled", false); } - }); - }); + }, + error: function (xhr) { - $("#blockForm").on("submit", function (event) { - event.preventDefault(); - $.ajax({ - url: "/add_block", - type: "POST", - data: $(this).serialize(), - success: function (response) { - alert(response.message); - location.reload(); - }, - error: function (xhr) { - alert(xhr.responseJSON.message); + if (xhr.status === 409) { + $("#blockMessage").text("Block already exists!").css("color", "red"); + $("#submitButton").prop("disabled", true); + } + else if (xhr.status === 400) { + $("#blockMessage").text("Invalid block name! Only letters allowed.").css("color", "red"); + $("#submitButton").prop("disabled", true); } - }); - }); - - $('#state_Id').change(function() { - var stateId = $(this).val(); - if (stateId) { - $.ajax({ - url: '/get_districts/' + stateId, - type: 'GET', - success: function(data) { - var districtDropdown = $('#district_Id'); - districtDropdown.empty(); - districtDropdown.append(''); - - data.districts.forEach(function(district) { - districtDropdown.append(''); - }); - - districtDropdown.prop('disabled', false); - }, - error: function() { - alert('Error fetching districts. Please try again.'); - } - }); - } else { - $('#district_Id').prop('disabled', true); } }); - }); \ No newline at end of file + }); + + + $("#blockForm").on("submit", function (event) { + + event.preventDefault(); + + $.ajax({ + url: "/add_block", + type: "POST", + data: $(this).serialize(), + + success: function (response) { + alert(response.message); + location.reload(); + }, + + error: function (xhr) { + alert(xhr.responseJSON.message); + } + }); + }); + + + + // ✅ FETCH DISTRICTS WHEN STATE CHANGES + $('#state_Id').change(function () { + + var stateId = $(this).val(); + + if (stateId) { + + $.ajax({ + url: '/get_districts/' + stateId, + type: 'GET', + + success: function (data) { + + var districtDropdown = $('#district_Id'); + + districtDropdown.empty(); + districtDropdown.append(''); + + data.forEach(function (district) { + + districtDropdown.append( + '' + ); + + }); + + districtDropdown.prop('disabled', false); + }, + + error: function () { + alert('Error fetching districts. Please try again.'); + } + }); + + } else { + $('#district_Id').prop('disabled', true); + } + + }); + +}); \ No newline at end of file diff --git a/static/js/village.js b/static/js/village.js index 7bcefcf..e07e455 100644 --- a/static/js/village.js +++ b/static/js/village.js @@ -1,102 +1,198 @@ - window.onload = function () { - document.getElementById('Village_Name').focus(); - }; + document.getElementById('Village_Name').focus(); +}; $(document).ready(function () { - $('#state_Id').change(function () { - var stateId = $(this).val(); - if (stateId) { - $.ajax({ - url: '/get_districts/' + stateId, - type: 'GET', - success: function (data) { - var districtDropdown = $('#district_Id'); - districtDropdown.empty().append(''); - data.districts.forEach(function (district) { - districtDropdown.append(''); - }); - districtDropdown.prop('disabled', false); - } - }); - } - }); - $('#district_Id').change(function () { - var districtId = $(this).val(); - if (districtId) { - $.ajax({ - url: '/get_blocks/' + districtId, - type: 'GET', - success: function (data) { - var blockDropdown = $('#block_Id'); - blockDropdown.empty().append(''); - data.blocks.forEach(function (block) { - blockDropdown.append(''); - }); - blockDropdown.prop('disabled', false); - } - }); - } - }); + // STATE → DISTRICT + $('#state_Id').change(function () { - $('#Village_Name').on('input', function () { - var villageName = $(this).val(); - var validPattern = /^[A-Za-z ]*$/; + var stateId = $(this).val(); - if (!validPattern.test(villageName)) { - $('#villageMessage').text('Only letters and spaces are allowed!').css('color', 'red'); - $('#submitVillage').prop('disabled', true); - } else { - $('#villageMessage').text(''); - $('#submitVillage').prop('disabled', false); - } - }); - - $('#Village_Name, #block_Id').on('change keyup', function () { - var blockId = $('#block_Id').val(); - var villageName = $('#Village_Name').val().trim(); - - if (blockId && villageName) { - $.ajax({ - url: '/check_village', - type: 'POST', - data: { block_Id: blockId, Village_Name: villageName }, - success: function (response) { - if (response.status === 'exists') { - $('#villageMessage').text(response.message).css('color', 'red'); - $('#submitVillage').prop('disabled', true); - } else { - $('#villageMessage').text(response.message).css('color', 'green'); - $('#submitVillage').prop('disabled', false); - } - }, - error: function () { - $('#villageMessage').text('Error checking village name').css('color', 'red'); - $('#submitVillage').prop('disabled', true); - } - }); - } - }); - - $('#villageForm').submit(function (event) { - event.preventDefault(); // Prevent default form submission + if (stateId) { $.ajax({ - url: '/add_village', - type: 'POST', - data: $(this).serialize(), - success: function (response) { - if (response.status === 'success') { - alert('Village added successfully!'); - location.reload(); // Refresh the page to show the updated list - } else { - alert('Error adding village. Please try again.'); - } - }, - error: function () { - alert('An error occurred. Please try again.'); + url: '/get_districts/' + stateId, + type: 'GET', + + success: function (data) { + + var districtDropdown = $('#district_Id'); + + districtDropdown.empty(); + districtDropdown.append(''); + + data.forEach(function (district) { + + districtDropdown.append( + '' + ); + + }); + + districtDropdown.prop('disabled', false); + } + }); + + } + + }); + + + // DISTRICT → BLOCK + $('#district_Id').change(function () { + + var districtId = $(this).val(); + + if (districtId) { + + $.ajax({ + url: '/get_blocks/' + districtId, + type: 'GET', + + success: function (data) { + + var blockDropdown = $('#block_Id'); + + blockDropdown.empty(); + blockDropdown.append(''); + + data.forEach(function (block) { + + blockDropdown.append( + '' + ); + + }); + + blockDropdown.prop('disabled', false); + + } + + }); + + } + + }); + + + // VILLAGE NAME VALIDATION + $('#Village_Name').on('input', function () { + + var villageName = $(this).val(); + var validPattern = /^[A-Za-z ]*$/; + + if (!validPattern.test(villageName)) { + + $('#villageMessage') + .text('Only letters and spaces are allowed!') + .css('color', 'red'); + + $('#submitVillage').prop('disabled', true); + + } else { + + $('#villageMessage').text(''); + $('#submitVillage').prop('disabled', false); + + } + + }); + + + // CHECK DUPLICATE VILLAGE + $('#Village_Name, #block_Id').on('change keyup', function () { + + var blockId = $('#block_Id').val(); + var villageName = $('#Village_Name').val().trim(); + + if (blockId && villageName) { + + $.ajax({ + + url: '/check_village', + type: 'POST', + + data: { + block_Id: blockId, + Village_Name: villageName + }, + + success: function (response) { + + if (response.status === 'exists') { + + $('#villageMessage') + .text(response.message) + .css('color', 'red'); + + $('#submitVillage').prop('disabled', true); + + } else { + + $('#villageMessage') + .text(response.message) + .css('color', 'green'); + + $('#submitVillage').prop('disabled', false); + + } + + }, + + error: function () { + + $('#villageMessage') + .text('Error checking village name') + .css('color', 'red'); + + $('#submitVillage').prop('disabled', true); + + } + + }); + + } + + }); + + + // ADD VILLAGE + $('#villageForm').submit(function (event) { + + event.preventDefault(); + + $.ajax({ + + url: '/add_village', + type: 'POST', + data: $(this).serialize(), + + success: function (response) { + + if (response.status === 'success') { + + alert('Village added successfully!'); + location.reload(); + + } else { + + alert('Error adding village. Please try again.'); + + } + + }, + + error: function () { + + alert('An error occurred. Please try again.'); + + } + }); - }); \ No newline at end of file + + }); + +}); \ No newline at end of file diff --git a/templates/add_block.html b/templates/add_block.html index e3ba5e4..4bcf1f7 100644 --- a/templates/add_block.html +++ b/templates/add_block.html @@ -1,5 +1,6 @@ {% extends 'base.html' %} {% block content %} + @@ -8,90 +9,91 @@ + - -
    - - -
    + +
    + + +
    -