Compare commits
47 Commits
clean-inst
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 02d1329746 | |||
| f4864dae63 | |||
| 4ae49f097c | |||
| 0868e90916 | |||
| 8464fcef87 | |||
| 15ee0459b5 | |||
| 6d0593a33d | |||
| d87e70097b | |||
| 17fb06d6d3 | |||
| 77694443aa | |||
| 35d02da993 | |||
| 237341ccf8 | |||
| d3f6cb4675 | |||
| d7d62aaf44 | |||
| 93be7bd9c4 | |||
| ab5965235f | |||
| 15f57d57d7 | |||
| 2152583560 | |||
| bbde69f3d9 | |||
| a3663e17b3 | |||
| b07e9fd4b4 | |||
| 0e519e3b7d | |||
| 0183909f8e | |||
| b9c1abf15a | |||
| ac19e9c6bb | |||
| 7c4d1c43ff | |||
| ee9ce9ece5 | |||
| 22d102dee8 | |||
| d1d2f44c25 | |||
| bd487adca2 | |||
| 33966decd9 | |||
| beb873a9b1 | |||
| 772fcb86c2 | |||
| 321d402b81 | |||
| ae0365a42a | |||
| a6bb81bbf8 | |||
| 1610905a43 | |||
| e2746b83bb | |||
| 8c818f8dde | |||
| 8ebd77540c | |||
| cf577f3ac7 | |||
| 49617c39c4 | |||
| caa4bc2dca | |||
| b3e6e4f6a2 | |||
|
|
07af3c52ea | ||
|
|
69286e8977 | ||
|
|
a24aac3262 |
1
clean_slate_backup.sh
Normal file
1
clean_slate_backup.sh
Normal file
@ -0,0 +1 @@
|
||||
bench --site snw-erp.localhost backup --exclude 'Customer,Address,Lead,Quotation,Project,Sales Order,Sales Invoice,On-Site Meeting,Service Address 2,Project Task Link,Bid Meeting Note,Service Appointment,Customer Contact Link,Customer Address Link,Customer Company Link,Address Task Link,Customer Task Link,Address Company Link,Lead Quotation Link,Lead Contact Link,Lead Address Link,Customer Sales Order Link,Customer Quotation Link,Customer Project Link,Customer On-Site Meeting Link,Address Contact Link,Lead On-Site Meeting Link,Contact Address Link,Address Sales Order Link,Address On-Site Meeting Link,Address Quotation Link,Address Project Link,Lead Companies Link,Lead Company Link'
|
||||
BIN
csv/Item (1).xlsx
Normal file
BIN
csv/Item (1).xlsx
Normal file
Binary file not shown.
147
csv/Item Price-import-ready.csv
Normal file
147
csv/Item Price-import-ready.csv
Normal file
@ -0,0 +1,147 @@
|
||||
"name","item_code","uom","price_list","price_list_rate","packing_unit","item_name","brand","item_description","customer","supplier","batch_no","buying","selling","currency","valid_from","lead_time_days","valid_upto","note","reference"
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
"","","","","","","","","","","","","","","","","","","",""
|
||||
|
3
csv/Item Price-template.csv
Normal file
3
csv/Item Price-template.csv
Normal file
@ -0,0 +1,3 @@
|
||||
"ID","Item Code","UOM","Price List","Rate","Packing Unit","Item Name","Brand","Item Description","Customer","Supplier","Batch No","Buying","Selling","Currency","Valid From","Lead Time in days","Valid Upto","Note","Reference"
|
||||
|
||||
|
||||
|
166
csv/Item Price.csv
Normal file
166
csv/Item Price.csv
Normal file
@ -0,0 +1,166 @@
|
||||
"Data Import Template"
|
||||
"Table:","Item Price"
|
||||
""
|
||||
""
|
||||
"Notes:"
|
||||
"Please do not change the template headings."
|
||||
"First data column must be blank."
|
||||
"If you are uploading new records, leave the ""name"" (ID) column blank."
|
||||
"If you are uploading new records, ""Naming Series"" becomes mandatory, if present."
|
||||
"Only mandatory fields are necessary for new records. You can delete non-mandatory columns if you wish."
|
||||
"For updating, you can update only selective columns."
|
||||
"You can only upload upto 5000 records in one go. (may be less in some cases)"
|
||||
""
|
||||
"DocType:","Item Price","","","","","","","","","","","","","","","","","","","","",""
|
||||
"Column Labels:","ID","Item Code","UOM","Price List","Rate","Created On","Created By","Packing Unit","Item Name","Brand","Item Description","Customer","Supplier","Batch No","Buying","Selling","Currency","Valid From","Lead Time in days","Valid Upto","Note","Reference"
|
||||
"Column Name:","name","item_code","uom","price_list","price_list_rate","creation","owner","packing_unit","item_name","brand","item_description","customer","supplier","batch_no","buying","selling","currency","valid_from","lead_time_days","valid_upto","note","reference"
|
||||
"Mandatory:","Yes","Yes","Yes","Yes","Yes","No","No","No","No","No","No","No","No","No","No","No","No","No","No","No","No","No"
|
||||
"Type:","Data","Link","Link","Link","Currency","Datetime","Link","Int","Data","Link","Text","Link","Link","Link","Check","Check","Link","Date","Int","Date","Text","Data"
|
||||
"Info:","","Valid Item","Valid UOM","Valid Price List","","mm-dd-yyyy","Valid User","Integer","","Valid Brand","","Valid Customer","Valid Supplier","Valid Batch","0 or 1","0 or 1","Valid Currency","mm-dd-yyyy","Integer","mm-dd-yyyy","",""
|
||||
"Start entering data below this line"
|
||||
"","""028bea2a8e""","SERV-0004","Each","Standard Buying",65.0,"2025-03-05 06:40:31.079375","Administrator",0,"Start Up","","<div><p>Start Up Sprinklers</p></div>","","","",1,0,"USD","03-05-2025",0,"","",""
|
||||
"","""04d409e574""","DL-0045","Yard","Standard Selling",54.0,"2024-12-11 10:26:06.281663","lbrown@sprinklersnorthwest.com",0,"Large Nugget","","Large Nugget","","","",0,1,"USD","",0,"","",""
|
||||
"","""0751d07aa2""","DL-0033","Yard","Standard Selling",30.0,"2024-12-11 10:26:05.956546","lbrown@sprinklersnorthwest.com",0,"Granite A-Rock","","Granite A-Rock","","","",0,1,"USD","",0,"","",""
|
||||
"","""0986e9be33""","DL-0019","Yard","Standard Selling",42.0,"2024-12-11 10:26:05.591816","lbrown@sprinklersnorthwest.com",0,"3/4"" Minus Basalt","","3/4"" Minus Basalt","","","",0,1,"USD","",0,"","",""
|
||||
"","""0e00cf42b2""","BLDR - Sample Package 1","Each","Commerical Installation",1000.0,"2025-02-07 11:13:35.048005","kris@sprinklersnorthwest.com",0,"BLDR - Sample Package 1","","BLDR - Sample Package 1","","","",0,1,"USD","02-07-2025",0,"","",""
|
||||
"","""0eface13a2""","DL-0020","Yard","Standard Selling",97.0,"2024-12-11 10:26:05.618573","lbrown@sprinklersnorthwest.com",0,"Red Valley","","Red Valley","","","",0,1,"USD","",0,"","",""
|
||||
"","""10a6ba6d26""","DL-0008","Yard","Standard Selling",65.0,"2024-12-11 10:26:05.296632","lbrown@sprinklersnorthwest.com",0,"Mt Grey","","Mt Grey","","","",0,1,"USD","",0,"","",""
|
||||
"","""1683a6757e""","DL-0029","Yard","Standard Selling",37.0,"2024-12-11 10:26:05.846856","lbrown@sprinklersnorthwest.com",0,"Curb Sand","","Curb Sand","","","",0,1,"USD","",0,"","",""
|
||||
"","""1706c2e18c""","BLDR-Woodridge 4 Falcon Ridge Gallery","Each","Standard Selling",12300.0,"2025-02-21 12:49:14.903409","kris@sprinklersnorthwest.com",0,"Woodridge 4 Falcon Ridge Gallery Landscape Installation ","","Woodridge 4 Falcon Ridge Gallery Landscape Installation ","","","",0,1,"USD","02-21-2025",0,"","",""
|
||||
"","""17ed4513b6""","BLDR-Front Yard Landscape Installation","Each","Standard Selling",4500.0,"2025-01-28 14:40:09.616449","kris@sprinklersnorthwest.com",0,"Builder Landscape Installation","","Builder Landscape Installation","","","",0,1,"USD","01-28-2025",0,"","",""
|
||||
"","""18802dcb0d""","SNW-I-0017","Each","Commerical Installation",500.0,"2025-02-13 11:39:45.976852","courtney@sprinklersnorthwest.com",0,"Plant Install","","<div><p>Installation of designated plant package to jobsite.</p></div>","","","",0,1,"USD","02-13-2025",0,"","",""
|
||||
"","""19008402d9""","SNW-I-0025","Each","Standard Selling",750.0,"2025-08-15 21:02:02.922642","courtney@sprinklersnorthwest.com",0,"Bark & Weed Barrier","","Bark & Weed Barrier to be installed in all designated areas.","","","",0,1,"USD","08-15-2025",0,"","",""
|
||||
"","""1a4044e8fd""","DL-0024","Yard","Standard Selling",95.0,"2024-12-11 10:26:05.725117","lbrown@sprinklersnorthwest.com",0,"Champaign","","Champaign","","","",0,1,"USD","",0,"","",""
|
||||
"","""1aea533e41""","DL-0037","Yard","Standard Selling",38.0,"2024-12-11 10:26:06.063979","lbrown@sprinklersnorthwest.com",0,"1/2 Shale","","1/2 Shale","","","",0,1,"USD","",0,"","",""
|
||||
"","""1d81cfcf5d""","DL-0006","Yard","Standard Selling",37.0,"2024-12-11 10:26:05.245640","lbrown@sprinklersnorthwest.com",0,"Container Blend","","Sand/Bartek","","","",0,1,"USD","",0,"","",""
|
||||
"","""1e73c035f1""","SNW-I-0006","Each","Standard Buying",185.0,"2025-01-16 13:45:08.778857","kris@sprinklersnorthwest.com",0,"Equipment Operator","","Equipment Operator","","","",1,0,"USD","01-16-2025",0,"","",""
|
||||
"","""1e914222e1""","DL-0040","Yard","Standard Selling",37.0,"2024-12-11 10:26:06.150211","lbrown@sprinklersnorthwest.com",0,"Sand & Gravel","","Sand & Gravel","","","",0,1,"USD","",0,"","",""
|
||||
"","""298d8d530a""","BLDR-Woodridge 4 Falcon Ridge Gallery","Each","Standard Selling",12300.0,"2025-02-24 11:50:07.853732","kris@sprinklersnorthwest.com",0,"Woodridge 4 Falcon Ridge Gallery Landscape Installation ","","Woodridge 4 Falcon Ridge Gallery Landscape Installation ","","","",0,1,"USD","02-24-2025",0,"","",""
|
||||
"","""2e5b7ddad0""","SERV-0005","Each","Residential",60.0,"2025-02-12 15:35:49.280265","courtney@sprinklersnorthwest.com",0,"Additional Backflow Test","","<div><p>Price Per Additional Backflow Test (when more than one backflow is required to be tested)</p></div>","","","",0,1,"USD","02-12-2025",0,"","",""
|
||||
"","""2f2f841aa7""","BLDR-Monogram Front Yard","Each","Standard Selling",4300.0,"2025-01-28 12:50:39.208311","kris@sprinklersnorthwest.com",0,"Monogram Front Yard Package ","","<div><p>Installation of front yard builder package. </p></div>","","","",0,1,"USD","01-28-2025",0,"","",""
|
||||
"","""2fb9dd4af5""","DL-0046","Yard","Standard Selling",42.0,"2024-12-11 10:26:06.308121","lbrown@sprinklersnorthwest.com",0,"Dark Fines","","Dark Fines","","","",0,1,"USD","",0,"","",""
|
||||
"","""30ef963682""","SNW-I-0050","Each","Standard Selling",1800.0,"2024-12-12 09:53:24.191598","lbrown@sprinklersnorthwest.com",0,"Pump Truck","","Concrete pump truck with extendable arm that allows for Precise placement of concrete.","","","",0,1,"USD","",0,"","",""
|
||||
"","""3111b26fbe""","SNW-I-0030","Each","Standard Selling",5.5,"2024-12-12 09:53:23.880263","lbrown@sprinklersnorthwest.com",0,"Metal Edging","","Metal Edging to be installed to designated areas.","","","",0,1,"USD","",0,"","",""
|
||||
"","""33f4fce6ec""","SERV-0012","Each","Residential",120.0,"2025-02-14 12:37:16.541406","courtney@sprinklersnorthwest.com",0,"Startup/Backflow","","<div><p>Start Up System And Perform Backflow Test.</p></div>","","","",0,1,"USD","02-14-2025",0,"","",""
|
||||
"","""39444b3676""","DL-0001","Yard","Standard Buying",28.0,"2025-01-16 13:45:08.755303","kris@sprinklersnorthwest.com",0,"Topsoil","","Topsoil","","","",1,0,"USD","01-16-2025",0,"","",""
|
||||
"","""3c63f24be4""","SNW-I-0054","Each","Standard Selling",500.0,"2025-01-24 10:14:28.850325","courtney@sprinklersnorthwest.com",0,"Drip Zone install","","Installation of drip zone system to cover all desiganated areas.","","","",0,1,"USD","01-24-2025",0,"","",""
|
||||
"","""3eca1d3e99""","SERV-0012","Each","Commerical Installation",120.0,"2025-02-12 14:46:29.599224","courtney@sprinklersnorthwest.com",0,"Startup/Backflow","","<div><p>Start Up System And Perform Backflow Test.</p></div>","","","",0,1,"USD","02-12-2025",0,"","",""
|
||||
"","""4046cb9970""","DL-0013","Yard","Standard Selling",85.0,"2024-12-11 10:26:05.432515","lbrown@sprinklersnorthwest.com",0,"Lg. Green Omak","","Lg. Green Omak","","","",0,1,"USD","",0,"","",""
|
||||
"","""41d8f86449""","SNW-I-0025","Each","Standard Buying",42.0,"2025-08-18 14:18:15.914070","courtney@sprinklersnorthwest.com",0,"Bark & Weed Barrier","","Bark & Weed Barrier to be installed in all designated areas.","","","",1,0,"USD","08-18-2025",0,"","",""
|
||||
"","""41e307e76a""","BLDR-Meadowlane Greens","Each","Standard Selling",11300.0,"2025-02-21 12:41:18.296600","kris@sprinklersnorthwest.com",0,"Meadowlane Greens Full Yard Landscape Package ","","Meadowlane Greens Full Yard Landscape Package ","","","",0,1,"USD","02-21-2025",0,"","",""
|
||||
"","""46656517ae""","SERV-0017","Each","Residential",160.0,"2025-02-14 12:55:02.906307","courtney@sprinklersnorthwest.com",0,"Startup/Backflow/Winterize","","<div><p>Startup/ Backflow Test/ Prepay Fall Winterization</p></div>","","","",0,1,"USD","02-14-2025",0,"","",""
|
||||
"","""46c6400432""","BLDR-Lennar Plant Package","Each","Standard Selling",700.0,"2025-01-27 13:00:44.563964","kris@sprinklersnorthwest.com",0,"BLDR-Lennar Plant Package ","","<div><p>Plant package for Lennar Homes</p></div>","","","",0,1,"USD","01-27-2025",0,"","",""
|
||||
"","""471671e8ff""","DL-0015","Yard","Standard Selling",42.0,"2024-12-11 10:26:05.485870","lbrown@sprinklersnorthwest.com",0,"2-4"" Basalt Cobble","","2-4"" Basalt Cobble","","","",0,1,"USD","",0,"","",""
|
||||
"","""48076b1740""","DL-0004","Yard","Standard Selling",43.0,"2024-12-11 10:26:05.192354","lbrown@sprinklersnorthwest.com",0,"3-1 Mix","","Topsoil/Bartek/Sandyloam","","","",0,1,"USD","",0,"","",""
|
||||
"","""48520c1901""","DL-0032","Yard","Standard Selling",42.0,"2024-12-11 10:26:05.929739","lbrown@sprinklersnorthwest.com",0,"Sandy Loam","","Sandy Loam","","","",0,1,"USD","",0,"","",""
|
||||
"","""49a186d603""","DL-0018","Yard","Standard Buying",42.0,"2025-08-18 14:18:15.877139","courtney@sprinklersnorthwest.com",0,"3/4"" Basalt","","3/4"" Basalt A-Rock","","","",1,0,"USD","08-18-2025",0,"","",""
|
||||
"","""4b858992df""","DL-0005","Yard","Standard Selling",31.0,"2024-12-11 10:26:05.219701","lbrown@sprinklersnorthwest.com",0,"Garden Mix","","Dark fines/Topsoil","","","",0,1,"USD","",0,"","",""
|
||||
"","""4db08a4b3d""","DL-0003","Yard","Standard Selling",5.0,"2024-12-11 10:26:05.165505","lbrown@sprinklersnorthwest.com",0,"Dirty Drain Rock","","Drain rock material","","","",0,1,"USD","",0,"","",""
|
||||
"","""4e0cedd652""","SBE075","Each","Standard Selling",1.0,"2025-05-08 11:57:41.979246","peter@shilohcode.com",0,"Rain Bird Spiral Barb Elbow 3/4 in. x 1/2 in. MIPT x Barb","","Rain Bird Spiral Barb Elbow 3/4 in. x 1/2 in. MIPT x Barb","","","",0,1,"USD","05-08-2025",0,"","",""
|
||||
"","""4e2d24b4d8""","BLDR - Sample Package 1","Each","Standard Selling",4500.0,"2025-01-28 15:08:22.865509","kris@sprinklersnorthwest.com",0,"BLDR - Sample Package 1","","BLDR - Sample Package 1","","","",0,1,"USD","01-28-2025",0,"","",""
|
||||
"","""4e7a5baf93""","DL-0050","Hour","Standard Buying",135.0,"2025-01-17 10:15:24.745401","kris@sprinklersnorthwest.com",0,"12-15 Yard Dump ","","12-15 Yard Dump ","","","",1,0,"USD","01-17-2025",0,"","",""
|
||||
"","""4l0jeavd8r""","SNW-I-0027","Each","Standard Selling",700.0,"2025-01-03 09:33:47.591569","courtney@sprinklersnorthwest.com",0,"Hydroseeding","","Hydroseeding to be applied onto all designated areas.","","","",0,1,"USD","01-03-2025",0,"","",""
|
||||
"","""4l0jkasmia""","SNW-I-0018","Yard","Markup Price List",60.0,"2025-01-03 09:33:47.575336","courtney@sprinklersnorthwest.com",0,"Topsoil Application","","Topsoil Application","","","",0,1,"USD","01-03-2025",0,"12-31-2025","",""
|
||||
"","""4l0k6masv9""","SNW-I-0017","Each","Standard Selling",65.0,"2025-01-03 09:33:47.613707","courtney@sprinklersnorthwest.com",0,"Plant Install","","<div><p>Installation of designated plant package to jobsite.</p></div>","","","",0,1,"USD","01-03-2025",0,"","",""
|
||||
"","""4l0kcta6un""","PSMS147060","Each","Standard Selling",25.0,"2025-01-03 09:33:47.627881","courtney@sprinklersnorthwest.com",0,"Paver Leiden Multi Stone Jamestown Blend","","(Sold by the layer)","","","",0,1,"USD","01-03-2025",0,"","",""
|
||||
"","""520a2496e9""","BLDR- Small Plant Package","Each","Standard Selling",250.0,"2025-01-27 12:52:42.784151","kris@sprinklersnorthwest.com",0,"BLDR- Small Plant Package","","BLDR- Small Plant Package","","","",0,1,"USD","01-27-2025",0,"","",""
|
||||
"","""52kh3l1c6o""","SNW-I-DOOT","Nos","Standard Selling",30.0,"2026-02-05 12:37:21.999118","casey@shilohcode.com",0,"Doot","","Blah","","","",0,1,"USD","02-05-2026",0,"","",""
|
||||
"","""544bd7e4a4""","DL-0042","Yard","Standard Selling",79.0,"2024-12-11 10:26:06.204691","lbrown@sprinklersnorthwest.com",0,"1.5"" Rainbow","","1.5"" Rainbow","","","",0,1,"USD","",0,"","",""
|
||||
"","""54a631121f""","BLDR - Sample Package 1","Each","Standard Buying",4500.0,"2025-02-03 15:24:57.624602","courtney@sprinklersnorthwest.com",0,"BLDR - Sample Package 1","","BLDR - Sample Package 1","","","",1,0,"USD","02-03-2025",0,"","",""
|
||||
"","""5a94b4899b""","SERV-0003","Each","Standard Buying",55.0,"2025-03-05 06:40:31.128339","Administrator",0,"Service Call Minimum","","<div><p>Service Call Minimum (half hour or less)</p></div>","","","",1,0,"USD","03-05-2025",0,"","",""
|
||||
"","""5d53679192""","DL-0028","Yard","Standard Selling",38.0,"2024-12-11 10:26:05.820107","lbrown@sprinklersnorthwest.com",0,"Pea Gravel","","Pea Gravel","","","",0,1,"USD","",0,"","",""
|
||||
"","""5df9a9a9e8""","DL-0007","Yard","Standard Selling",49.0,"2024-12-11 10:26:05.270449","lbrown@sprinklersnorthwest.com",0,"Bartek","","Compost soil 90% Forestry 10%Organics","","","",0,1,"USD","",0,"","",""
|
||||
"","""5e1c628bc8""","SERV-0002","Each","Residential",55.0,"2025-02-14 12:37:16.576546","courtney@sprinklersnorthwest.com",0,"Fall System Winterize","","<div><p>Winterization of irrigation system</p></div>","","","",0,1,"USD","02-14-2025",0,"","",""
|
||||
"","""5ie73htc9k""","Tan Vinyl 1.5x5.5x8 Rail","Each","Standard Selling",18.74,"2024-12-18 15:44:51.995881","courtney@sprinklersnorthwest.com",0,"Tan Vinyl 1.5x5.5x8 Rail","","Tan Vinyl 1.5x5.5x8 Rail","","","",0,1,"USD","12-18-2024",0,"","",""
|
||||
"","""5ie77ll7ff""","Veka Almond 7/8""X6""X61.5"" T&G","Nos","Standard Selling",15.63,"2024-12-18 15:44:51.981282","courtney@sprinklersnorthwest.com",0,"Veka Almond 7/8""X6""X61.5"" T&G","","Veka Almond 7/8""X6""X61.5"" T&G","","","",0,1,"USD","12-18-2024",0,"","",""
|
||||
"","""5ie7f8g3ve""","Tan Vinyl 11.3"" T&G","Each","Standard Selling",14.07,"2024-12-18 15:44:51.957833","courtney@sprinklersnorthwest.com",0,"Tan Vinyl 11.3"" T&G","","Tan Vinyl 11.3"" T&G","","","",0,1,"USD","12-18-2024",0,"","",""
|
||||
"","""5ie8edsmhd""","Labor","Nos","Standard Selling",165.0,"2024-12-18 15:44:52.023891","courtney@sprinklersnorthwest.com",0,"Labor","","Labor","","","",0,1,"USD","12-18-2024",0,"","",""
|
||||
"","""5ie8sk3ho1""","Vinyl Gate Hinge","Per Set","Standard Selling",30.0,"2024-12-18 15:44:52.009456","courtney@sprinklersnorthwest.com",0,"Vinyl Gate Hinge","","Vinyl Gate Hinge","","","",0,1,"USD","12-18-2024",0,"","",""
|
||||
"","""61025f73b4""","SNW-I-0034","Hour","Commerical Installation",150.0,"2025-02-13 11:39:46.018748","courtney@sprinklersnorthwest.com",0,"General Labor","","General Labor","","","",0,1,"USD","02-13-2025",0,"","",""
|
||||
"","""681a353a50""","DL-0011","Yard","Standard Selling",42.0,"2024-12-11 10:26:05.374863","lbrown@sprinklersnorthwest.com",0,"4"" Cobble","","4"" Cobble","","","",0,1,"USD","",0,"","",""
|
||||
"","""6892aeec3d""","DL-0022","Yard","Standard Selling",95.0,"2024-12-11 10:26:05.671388","lbrown@sprinklersnorthwest.com",0,"China White","","China White","","","",0,1,"USD","",0,"","",""
|
||||
"","""698b85bbdb""","SERV-0005","Each","Standard Buying",60.0,"2025-02-12 15:58:17.080872","courtney@sprinklersnorthwest.com",0,"Additional Backflow Test","","<div><p>Price Per Additional Backflow Test (when more than one backflow is required to be tested)</p></div>","","","",1,0,"USD","02-12-2025",0,"","",""
|
||||
"","""6e91bea59c""","DL-0021","Yard","Standard Selling",65.0,"2024-12-11 10:26:05.645251","lbrown@sprinklersnorthwest.com",0,"Purple Rage","","Purple Rage","","","",0,1,"USD","",0,"","",""
|
||||
"","""7545a350fb""","DL-0002","Yard","Standard Selling",20.0,"2024-12-11 10:26:05.135602","lbrown@sprinklersnorthwest.com",0,"Daniels Base","","Base Material","","","",0,1,"USD","",0,"","",""
|
||||
"","""763ce91518""","BLDR-Hayden Canyon Gold Front","Each","Commerical Installation",9800.0,"2025-02-07 10:44:32.604088","kris@sprinklersnorthwest.com",0,"BLDR-Hayden Canyon Gold Front","","BLDR-Hayden Canyon Gold Front","","","",0,1,"USD","02-07-2025",0,"","",""
|
||||
"","""7e05f0c35f""","White Vinyl 1.5x5.5x8 Rail","Each","Standard Selling",12.0,"2025-01-24 11:28:06.610161","courtney@sprinklersnorthwest.com",0,"White Vinyl 1.5x5.5x8 Rail","","White Vinyl 1.5x5.5x8 Rail","","","",0,1,"USD","01-24-2025",0,"","",""
|
||||
"","""7f8a67ac25""","DL-0034","Yard","Standard Selling",30.0,"2024-12-11 10:26:05.983444","lbrown@sprinklersnorthwest.com",0,"Granite B-Rock","","Granite B-Rock","","","",0,1,"USD","",0,"","",""
|
||||
"","""8229b74ad9""","BLDR-Front Yard Irrigation Installation","Each","Standard Selling",2500.0,"2025-01-27 15:11:18.074654","kris@sprinklersnorthwest.com",0,"BLDR-Front Yard Irrigation Installation ","","<div><p>Front yard Irrigation Installation: </p><p>3-4 Irrigation zones including 1 drip zone</p><p>All turf zones zoned with head to head coverage. </p><p>Drip zone to be installed underground with water going to each plant.</p></div>","","","",0,1,"USD","01-27-2025",0,"","",""
|
||||
"","""86b976de4f""","SNW-I-0018","Yard","Standard Buying",16.0,"2025-08-18 14:18:15.815276","courtney@sprinklersnorthwest.com",0,"Topsoil Application","","Topsoil Application","","","",1,0,"USD","08-18-2025",0,"","",""
|
||||
"","""88426a107u""","BLDR-WOW","Nos","Standard Selling",11300.0,"2026-02-05 12:42:46.854232","casey@shilohcode.com",0,"Wow","","wow","","","",0,1,"USD","02-05-2026",0,"","",""
|
||||
"","""8suh4u50ra""","VHS-0018","Each","Standard Buying",45.0,"2024-12-17 13:39:00.893309","courtney@sprinklersnorthwest.com",0,"Paver Spikes","","Spikes","","","",1,0,"USD","12-17-2024",0,"","",""
|
||||
"","""903e0cd87e""","DL-0038","Yard","Standard Selling",38.0,"2024-12-11 10:26:06.090089","lbrown@sprinklersnorthwest.com",0,"3/4 Shale","","3/4 Shale","","","",0,1,"USD","",0,"","",""
|
||||
"","""92bc03f7ec""","BLDR-Meadowlane Greens","Each","Commerical Installation",11300.0,"2025-02-24 14:37:05.766490","kris@sprinklersnorthwest.com",0,"Meadowlane Greens Full Yard Landscape Package ","","Meadowlane Greens Full Yard Landscape Package ","","","",0,1,"USD","02-24-2025",0,"","",""
|
||||
"","""9743f3e2a2""","DL-0041","Yard","Standard Selling",79.0,"2024-12-11 10:26:06.177971","lbrown@sprinklersnorthwest.com",0,"3/4 Rainbow","","3/4 Rainbow","","","",0,1,"USD","",0,"","",""
|
||||
"","""97c9841ad7""","SERV-0001","","Standard Buying",0.0,"2025-01-31 10:37:59.732968","kris@sprinklersnorthwest.com",0,"Backflow Test Only","","<div><p>Backflow Test Only</p></div>","","","",1,0,"USD","01-31-2025",0,"","",""
|
||||
"","""99b4ecbde4""","DL-0035","Yard","Standard Selling",65.0,"2024-12-11 10:26:06.008990","lbrown@sprinklersnorthwest.com",0,"Iron Mt","","Iron Mt","","","",0,1,"USD","",0,"","",""
|
||||
"","""9a509a80a6""","DL-0031","Yard","Standard Selling",32.0,"2024-12-11 10:26:05.900179","lbrown@sprinklersnorthwest.com",0,"Course Sand","","Course Sand","","","",0,1,"USD","",0,"","",""
|
||||
"","""a09261b096""","SNW-I-0056","","Standard Selling",485.0,"2025-01-22 14:23:57.953335","kris@sprinklersnorthwest.com",0,"Backflow Installation","","Installation of Backflow","","","",0,1,"USD","01-22-2025",0,"","",""
|
||||
"","""a102ba8219""","SERV-0005","Each","Commerical Installation",30.0,"2025-02-13 11:13:58.044160","courtney@sprinklersnorthwest.com",0,"Additional Backflow Test","","<div><p>Price Per Additional Backflow Test (when more than one backflow is required to be tested)</p></div>","","","",0,1,"USD","02-13-2025",0,"","",""
|
||||
"","""a1fe8dc563""","SNW-I-0012","Each","Standard Selling",500.0,"2025-01-29 11:22:05.752885","courtney@sprinklersnorthwest.com",0,"Excavator","","Excavator","","","",0,1,"USD","01-29-2025",0,"","",""
|
||||
"","""a29c769843""","DL-0050","Hour","Standard Selling",270.0,"2025-01-27 11:29:59.079130","courtney@sprinklersnorthwest.com",0,"12-15 Yard Dump ","","12-15 Yard Dump ","","","",0,1,"USD","01-27-2025",0,"","",""
|
||||
"","""a30f9fd663""","SNW-I-0058","Each","Standard Selling",800.0,"2025-01-24 10:14:28.881529","courtney@sprinklersnorthwest.com",0,"Rotor zone install","","Rotor zone to be installed to all designated areas with head to head coverage.","","","",0,1,"USD","01-24-2025",0,"","",""
|
||||
"","""a913677251""","SERV-0001","Each","Commerical Installation",60.0,"2025-02-13 11:13:58.011002","courtney@sprinklersnorthwest.com",0,"Backflow Test Only","","<div><p>Backflow Test Only</p></div>","","","",0,1,"USD","02-13-2025",0,"","",""
|
||||
"","""a944e3ac05""","SNW-I-0028","Each","Standard Selling",7.25,"2024-12-12 09:53:23.813831","lbrown@sprinklersnorthwest.com",0,"Curbing","","Concrete curbing to be installed to all designated areas. (100"" minimum)","","","",0,1,"USD","",0,"","",""
|
||||
"","""a9b4f75d88""","DL-0039","Yard","Standard Selling",38.0,"2024-12-11 10:26:06.121909","lbrown@sprinklersnorthwest.com",0,"2-3"" Shale","","2-3"" Shale","","","",0,1,"USD","",0,"","",""
|
||||
"","""aade78bb71""","SNW-I-0036","Each","Standard Selling",4.25,"2024-12-12 09:53:23.979191","lbrown@sprinklersnorthwest.com",0,"Plastic Edging","","Plastic edging to be installed to all designated areas.","","","",0,1,"USD","",0,"","",""
|
||||
"","""aae3510267""","DL-0009","Yard","Standard Selling",61.0,"2024-12-11 10:26:05.323949","lbrown@sprinklersnorthwest.com",0,"Small Speckled Granite","","Small Speckled Granite","","","",0,1,"USD","",0,"","",""
|
||||
"","""aee6df53a9""","DL-0016","Yard","Standard Selling",42.0,"2024-12-11 10:26:05.513666","lbrown@sprinklersnorthwest.com",0,"4-8"" Basalt Cobble","","4-8"" Basalt Cobble","","","",0,1,"USD","",0,"","",""
|
||||
"","""b19def7015""","DL-0012","Yard","Standard Selling",85.0,"2024-12-11 10:26:05.403703","lbrown@sprinklersnorthwest.com",0,"Sm. Green Omak","","Sm. Green Omak","","","",0,1,"USD","",0,"","",""
|
||||
"","""b1aca5ed08""","BLDR-Lennar Package","Each","Commerical Installation",9800.0,"2025-02-07 12:10:17.843290","kris@sprinklersnorthwest.com",0,"BLDR-Lennar Package","","BLD","","","",0,1,"USD","02-07-2025",0,"","",""
|
||||
"","""b687960f3d""","DL-0030","Yard","Standard Selling",37.0,"2024-12-11 10:26:05.873711","lbrown@sprinklersnorthwest.com",0,"Fine Sand","","Fine Sand","","","",0,1,"USD","",0,"","",""
|
||||
"","""bdec0ddb6a""","DL-0026","Yard","Standard Selling",42.0,"2024-12-11 10:26:05.777881","lbrown@sprinklersnorthwest.com",0,"1.5"" PHD","","1.5"" PHD","","","",0,1,"USD","",0,"","",""
|
||||
"","""bkne7f8o3s""","BLDR-BIG-BIG-BUILDERS","Nos","Standard Selling",12700.0,"2026-02-05 12:48:34.347978","casey@shilohcode.com",0,"Big Big Builders","","Big Builders","","","",0,1,"USD","02-05-2026",0,"","",""
|
||||
"","""c211b2c087""","SNW-I-0018","Yard","Standard Selling",65.0,"2025-01-17 13:24:41.574304","kris@sprinklersnorthwest.com",0,"Topsoil Application","","Topsoil Application","","","",0,1,"USD","01-17-2025",0,"","",""
|
||||
"","""c2b6ee2472""","DL-0018","Yard","Standard Selling",42.0,"2024-12-11 10:26:05.566110","lbrown@sprinklersnorthwest.com",0,"3/4"" Basalt","","3/4"" Basalt A-Rock","","","",0,1,"USD","",0,"","",""
|
||||
"","""c4db4ab59e""","DL-0001","Yard","Standard Selling",16.0,"2024-12-11 10:26:05.062927","lbrown@sprinklersnorthwest.com",0,"Topsoil","","Topsoil","","","",0,1,"USD","",0,"","",""
|
||||
"","""c4f5ba1700""","SNW-I-0024","Each","Standard Selling",2250.0,"2025-01-27 11:29:59.038039","courtney@sprinklersnorthwest.com",0,"French Drain","","French Drain","","","",0,1,"USD","01-27-2025",0,"","",""
|
||||
"","""c50e8f6577""","Dump Fees","Each","Standard Selling",1000.0,"2025-01-29 11:27:26.128108","courtney@sprinklersnorthwest.com",0,"Dump Fees","","<div><p>Dump Fees</p></div>","","","",0,1,"USD","01-29-2025",0,"","",""
|
||||
"","""c82018d25a""","DL-0044","Yard","Standard Selling",66.0,"2024-12-11 10:26:06.256209","lbrown@sprinklersnorthwest.com",0,"Small Nugget","","Small Nugget","","","",0,1,"USD","",0,"","",""
|
||||
"","""cb8d088bd0""","DL-0017","Yard","Standard Selling",42.0,"2024-12-11 10:26:05.540059","lbrown@sprinklersnorthwest.com",0,"2"" Basalt","","2"" Basalt","","","",0,1,"USD","",0,"","",""
|
||||
"","""cj0snnhj1f""","SNW-I-0034","Each","Standard Selling",35.0,"2024-12-17 17:00:31.603919","courtney@sprinklersnorthwest.com",0,"General Labor","","General Labor","","","",0,1,"USD","12-17-2024",0,"","",""
|
||||
"","""d07dc72e6b""","SNW-I-0037","Each","Standard Selling",0.9,"2024-12-12 09:53:24.007122","lbrown@sprinklersnorthwest.com",0,"Sod Install","","Sod to be installed to designated areas as specified.","","","",0,1,"USD","",0,"","",""
|
||||
"","""d68759f4ae""","BLDR-Hayden Canyon Townhomes Bronze","Each","Standard Selling",4400.0,"2025-02-11 08:30:41.276902","kris@sprinklersnorthwest.com",0,"BLDR-Hayden Canyon Townhomes Bronze ","","BLDR-Hayden Canyon Townhomes Bronze ","","","",0,1,"USD","02-11-2025",0,"","",""
|
||||
"","""d72eb32f2e""","DL-0048","Yard","Standard Selling",42.0,"2024-12-11 10:26:06.361540","lbrown@sprinklersnorthwest.com",0,"Shredded","","Shredded","","","",0,1,"USD","",0,"","",""
|
||||
"","""dc1fd7d474""","BLDR-Medium Plant Package","Each","Standard Selling",450.0,"2025-01-27 12:55:08.839894","kris@sprinklersnorthwest.com",0,"BLDR-Medium Plant Package ","","BLDR-Medium Plant Package ","","","",0,1,"USD","01-27-2025",0,"","",""
|
||||
"","""dc4e2eb529""","BLDR-Tangled Ridge","Each","Standard Selling",11350.0,"2025-02-11 08:49:27.936095","kris@sprinklersnorthwest.com",0,"BLDR-Tangled Ridge","","BLDR-Tangled Ridge","","","",0,1,"USD","02-11-2025",0,"","",""
|
||||
"","""dd451a1ae6""","DL-0036","Yard","Standard Selling",90.0,"2024-12-11 10:26:06.036579","lbrown@sprinklersnorthwest.com",0,"Decomp Granite","","Decomp Granite","","","",0,1,"USD","",0,"","",""
|
||||
"","""dd73018ac8""","DL-0051","","Standard Buying",150.0,"2025-01-17 10:21:14.010729","kris@sprinklersnorthwest.com",0,"20-24 Yard Dump","","20-24 Yard Dump","","","",1,0,"USD","01-17-2025",0,"","",""
|
||||
"","""de1rmums4j""","SNW-I-0055","Each","Standard Selling",1450.0,"2025-01-08 14:03:56.387517","kris@sprinklersnorthwest.com",0,"POC Installation","","installation of point of conection.","","","",0,1,"USD","01-08-2025",0,"","",""
|
||||
"","""de1spo8bcn""","SNW-I-0005","Each","Standard Selling",625.0,"2025-01-08 14:03:56.419385","kris@sprinklersnorthwest.com",0,"Grading And Labor","","Grading And Labor","","","",0,1,"USD","01-08-2025",0,"","",""
|
||||
"","""de9e25ff8b""","DL-0049","Hour","Standard Buying",95.0,"2025-01-17 09:27:12.011460","kris@sprinklersnorthwest.com",0,"Small 6 yard Dump Truck ","","Small 6 yard Dump Truck ","","Daniels Landscape Supplies","",1,0,"USD","01-17-2025",0,"","","Daniels Landscape Supplies"
|
||||
"","""dp1ssltjgs""","SNW-I-DOOTER","Nos","Standard Selling",2030.0,"2026-02-05 12:52:12.960575","casey@shilohcode.com",0,"Dooter","","Doot doot","","","",0,1,"USD","02-05-2026",0,"","",""
|
||||
"","""e0cf8f09de""","DL-0047","Yard","Standard Selling",46.0,"2024-12-11 10:26:06.333886","lbrown@sprinklersnorthwest.com",0,"Bright Fines","","Bright Fines","","","",0,1,"USD","",0,"","",""
|
||||
"","""e39d799e7e""","DL-0014","Yard","Standard Selling",143.0,"2024-12-11 10:26:05.459391","lbrown@sprinklersnorthwest.com",0,"Scoria","","Scoria","","","",0,1,"USD","",0,"","",""
|
||||
"","""e543944bcf""","SNW-I-0029","Each","Standard Selling",8.25,"2024-12-12 09:53:23.852907","lbrown@sprinklersnorthwest.com",0,"Curbing Color","","Color to be added to curbing to be installed.","","","",0,1,"USD","",0,"","",""
|
||||
"","""e60d37ea58""","DL-0010","Yard","Standard Selling",61.0,"2024-12-11 10:26:05.349308","lbrown@sprinklersnorthwest.com",0,"Large Speckled Granite","","Large Speckled Granite","","","",0,1,"USD","",0,"","",""
|
||||
"","""e884241450""","DL-0043","Yard","Standard Selling",79.0,"2024-12-11 10:26:06.230380","lbrown@sprinklersnorthwest.com",0,"2-4"" Rainbow","","2-4"" Rainbow","","","",0,1,"USD","",0,"","",""
|
||||
"","""e8f24ceaea""","SERV-0001","Each","Residential",85.0,"2025-01-31 10:10:11.531811","kris@sprinklersnorthwest.com",0,"Backflow Test Only","","<div><p>Backflow Test Only</p></div>","","","",0,1,"USD","01-31-2025",0,"","",""
|
||||
"","""ee7c270ecf""","BLDR-Tangled Ridge","Each","Commerical Installation",11350.0,"2025-02-11 10:10:52.269052","kris@sprinklersnorthwest.com",0,"BLDR-Tangled Ridge","","BLDR-Tangled Ridge","","","",0,1,"USD","02-11-2025",0,"","",""
|
||||
"","""ef1377ef15""","SERV-0015","Each","Residential",140.0,"2025-04-17 11:20:53.896920","peter@shilohcode.com",0,"Spring Fertilize","","<div><p>Spring Fertilizer Application</p></div>","","","",0,1,"USD","04-17-2025",0,"","",""
|
||||
"","""f24fec277c""","DL-0023","Yard","Standard Selling",97.0,"2024-12-11 10:26:05.698538","lbrown@sprinklersnorthwest.com",0,"Chewelah Yellow","","Chewelah Yellow","","","",0,1,"USD","",0,"","",""
|
||||
"","""f6a5e1b7dd""","DL-0025","Yard","Standard Selling",37.0,"2024-12-11 10:26:05.751945","lbrown@sprinklersnorthwest.com",0,"3/4 Washed Round","","3/4 Washed Round","","","",0,1,"USD","",0,"","",""
|
||||
"","""f6c2996a29""","SNW-I-0006","Each","Standard Selling",185.0,"2025-01-16 13:19:57.699427","kris@sprinklersnorthwest.com",0,"Equipment Operator","","Equipment Operator","","","",0,1,"USD","01-16-2025",0,"","",""
|
||||
"","""fe3a6a4f3d""","SNW-I-0001","Each","Commerical Installation",125.0,"2025-02-13 11:39:46.054895","courtney@sprinklersnorthwest.com",0,"Excavation","","Excavation","","","",0,1,"USD","02-13-2025",0,"","",""
|
||||
"","""gp961regkt""","SERV-0002","Each","Standard Selling",55.0,"2024-12-09 08:56:16.696237","lbrown@sprinklersnorthwest.com",1,"Fall System Winterize","","<div><p>Winterization of irrigation system</p></div>","","","",0,1,"USD","12-09-2024",0,"","",""
|
||||
"","""hbvha1kiqv""","SERV-0003","Each","Standard Selling",55.0,"2024-12-09 09:28:11.295470","lbrown@sprinklersnorthwest.com",0,"Service Call Minimum","","<div><p>Service Call Minimum (half hour or less)</p></div>","","","",0,1,"USD","12-09-2024",0,"","",""
|
||||
"","""hghbpap5of""","SERV-0004","Each","Standard Selling",65.0,"2024-12-09 09:35:57.943100","lbrown@sprinklersnorthwest.com",0,"Start Up","","<div><p>Start Up Sprinklers</p></div>","","","",0,1,"USD","12-09-2024",0,"","",""
|
||||
"","""hjeg7705nm""","SERV-0005","Each","Standard Selling",30.0,"2024-12-09 09:40:56.064168","lbrown@sprinklersnorthwest.com",0,"Additional Backflow Test","","<div><p>Price Per Additional Backflow Test (when more than one backflow is required to be tested)</p></div>","","","",0,1,"USD","12-09-2024",0,"","",""
|
||||
"","""hpfpp4ph0o""","SERV-0006","Each","Standard Selling",70.0,"2024-12-09 09:51:14.538879","lbrown@sprinklersnorthwest.com",0,"Additional Service Time","","<div><p>Charge $70 per 15min when service call runs over the allowed time.</p></div>","","","",0,1,"USD","12-09-2024",0,"","",""
|
||||
"","""iafjtq1heh""","SERV-0007","Each","Standard Selling",75.0,"2024-12-09 10:20:14.734917","lbrown@sprinklersnorthwest.com",0,"Outlying Backflow Test","","<div><p>Backflow Test Outlying Area</p></div>","","","",0,1,"USD","12-09-2024",0,"","",""
|
||||
"","""idmm50lt36""","SERV-0008","Each","Standard Selling",65.0,"2024-12-09 10:25:44.599739","lbrown@sprinklersnorthwest.com",0,"Outlying Winterize","","<div><p>Fall Winterization-Outlying Area</p></div>","","","",0,1,"USD","12-09-2024",0,"","",""
|
||||
"","""ihndqvf7ev""","SERV-0001","Each","Standard Selling",65.0,"2024-12-09 10:32:36.553612","lbrown@sprinklersnorthwest.com",0,"Backflow Test Only","","<div><p>Backflow Test Only</p></div>","","","",0,1,"USD","12-09-2024",0,"","",""
|
||||
"","""isb7rsh7iv""","SERV-0009","Each","Standard Selling",75.0,"2024-12-09 10:50:43.976626","lbrown@sprinklersnorthwest.com",0,"Outlying Startup","","<div><p>Spring Start Up-Outlying Area</p></div>","","","",0,1,"USD","12-09-2024",0,"","",""
|
||||
"","""ivnthihgun""","SERV-0010","Each","Standard Selling",130.0,"2024-12-09 10:56:31.729945","lbrown@sprinklersnorthwest.com",0,"Outlying Startup/Backflow","","<div><p>Outlying Areas Spring Turn on/Backflow Test</p></div>","","","",0,1,"USD","12-09-2024",0,"","",""
|
||||
"","""j4uviejsud""","SERV-0011","Each","Standard Selling",70.0,"2024-12-09 11:05:26.384150","lbrown@sprinklersnorthwest.com",0,"Service Call","","<div><p>Service Call (Billed at $70 Per Hour Per Tech)</p></div>","","","",0,1,"USD","12-09-2024",0,"","",""
|
||||
"","""jai9q37en1""","SERV-0012","Each","Standard Selling",120.0,"2024-12-09 11:15:00.105518","lbrown@sprinklersnorthwest.com",0,"Startup/Backflow","","<div><p>Start Up System And Perform Backflow Test.</p></div>","","","",0,1,"USD","12-09-2024",0,"","",""
|
||||
"","""jflu3b6fqe""","SERV-0013","Each","Standard Selling",170.0,"2024-12-09 11:23:43.830160","lbrown@sprinklersnorthwest.com",0,"Stoneridge Startup/Backflow/Winterize","","<div><p>Stoneridge Spring turn on/ Backflow test/ Prepay Fall Winterization.</p></div>","","","",0,1,"USD","12-09-2024",0,"","",""
|
||||
"","""jhifdikd83""","BLDR-BIG-TEST-KIT","Nos","Standard Selling",11700.0,"2026-02-05 13:02:03.478033","casey@shilohcode.com",0,"Big Test Kit","","This is just a test","","","",0,1,"USD","02-05-2026",0,"","",""
|
||||
"","""jmqt2928nj""","SERV-0014","Each","Standard Selling",225.0,"2024-12-09 11:35:56.560417","lbrown@sprinklersnorthwest.com",0,"Mid Season Check Adjustments","","<div><p>Midseason Check adjustments</p></div>","","","",0,1,"USD","12-09-2024",0,"","",""
|
||||
"","""jv3k43j5vj""","SERV-0015","Each","Standard Selling",350.0,"2024-12-09 11:50:03.620159","lbrown@sprinklersnorthwest.com",1,"Spring Fertilize","","<div><p>Spring Fertilizer Application</p></div>","","","",0,1,"USD","12-09-2024",0,"","",""
|
||||
"","""ntvjsadrkq""","BLDR-TEST-HOUSES-PACKAGE","Nos","Standard Selling",900.0,"2026-02-11 11:09:13.536751","casey@shilohcode.com",0,"Test houses package","","Test package","","","",0,1,"USD","02-11-2026",0,"","",""
|
||||
"","""ost4kd4fiu""","SNW-I-0053","Each","Standard Selling",2200.0,"2024-12-31 12:36:38.804512","courtney@sprinklersnorthwest.com",0,"Installation Sprinkler System","","Installation of sprinkler system zoned with head to head coverage in all turf areas.","","","",0,1,"USD","12-31-2024",0,"","",""
|
||||
"","""ou2r02dsg4""","SNW-I-BOB-THE-BUILDER-PACKAGE","Nos","Standard Selling",1400.0,"2026-02-05 12:16:38.605960","casey@shilohcode.com",0,"Bob The Builder Package","","This is a test","","","",0,1,"USD","02-05-2026",0,"","",""
|
||||
"","""q6etqmej10""","VHS-0018","Each","Standard Selling",45.0,"2024-12-09 17:30:17.300988","courtney@sprinklersnorthwest.com",0,"Paver Spikes","","Spikes","","","",0,1,"USD","12-09-2024",0,"","",""
|
||||
"","""skl3m36h2m""","BLDR- Full Yard Sprinkler Installation","Each","Standard Selling",8000.0,"2026-02-06 08:24:27.790550","casey@shilohcode.com",0,"BLDR- Full Yard Sprinkler Installation","","BLDR- Full Yard Sprinkler Installation","","","",0,1,"USD","02-06-2026",0,"","",""
|
||||
"","""upoveng8h9""","BLDR-BIG-BUILDER-INSTALL-PACKAGE","Nos","Standard Selling",2000.0,"2026-02-05 12:26:39.275545","casey@shilohcode.com",0,"Big Builder Install Package","","This is a test","","","",0,1,"USD","02-05-2026",0,"","",""
|
||||
|
Can't render this file because it has a wrong number of fields in line 2.
|
BIN
csv/Item Price.xlsx
Normal file
BIN
csv/Item Price.xlsx
Normal file
Binary file not shown.
2314
csv/Item-import-ready.csv
Normal file
2314
csv/Item-import-ready.csv
Normal file
File diff suppressed because it is too large
Load Diff
1
csv/Item-template.csv
Normal file
1
csv/Item-template.csv
Normal file
@ -0,0 +1 @@
|
||||
"Item Code","Item Group","Default Unit of Measure","Series","Item Name","Markup Percentage","Disabled","Allow Alternative Item","Maintain Stock","Has Variants","Opening Stock","Valuation Rate","Standard Selling Rate","Is Fixed Asset","Auto Create Assets on Purchase","Create Grouped Asset","Asset Category","Asset Naming Series","Over Delivery/Receipt Allowance (%)","Over Billing Allowance (%)","Image","Description","Brand","Shelf Life In Days","End of Life","Default Material Request Type","Valuation Method","Warranty Period (in days)","Weight Per Unit","Weight UOM","Allow Negative Stock","Has Batch No","Automatically Create New Batch","Batch Number Series","Has Expiry Date","Retain Sample","Max Sample Quantity","Has Serial No","Serial Number Series","Variant Of","Variant Based On","Enable Deferred Expense","No of Months (Expense)","Enable Deferred Revenue","No of Months (Revenue)","Default Purchase Unit of Measure","Minimum Order Qty","Safety Stock","Allow Purchase","Lead Time in days","Last Purchase Rate","Is Customer Provided Item","Customer","Delivered by Supplier (Drop Ship)","Country of Origin","Customs Tariff Number","Default Sales Unit of Measure","Grant Commission","Allow Sales","Max Discount (%)","Inspection Required before Purchase","Quality Inspection Template","Inspection Required before Delivery","Include Item In Manufacturing","Supply Raw Materials for Purchase","Default BOM","Customer Code","Default Item Manufacturer","Default Manufacturer Part No","Total Projected Qty","ID (Barcodes)","Barcode (Barcodes)","Barcode Type (Barcodes)","UOM (Barcodes)","ID (Reorder level based on Warehouse)","Check in (group) (Reorder level based on Warehouse)","Material Request Type (Reorder level based on Warehouse)","Re-order Level (Reorder level based on Warehouse)","Re-order Qty (Reorder level based on Warehouse)","Request for (Reorder level based on Warehouse)","ID (UOMs)","Conversion Factor (UOMs)","UOM (UOMs)","ID (Variant Attributes)","Attribute (Variant Attributes)","Attribute Value (Variant Attributes)","Disabled (Variant Attributes)","From Range (Variant Attributes)","Increment (Variant Attributes)","Numeric Values (Variant Attributes)","To Range (Variant Attributes)","Variant Of (Variant Attributes)","ID (Item Defaults)","Company (Item Defaults)","Default Buying Cost Center (Item Defaults)","Default Discount Account (Item Defaults)","Default Expense Account (Item Defaults)","Default Income Account (Item Defaults)","Default Price List (Item Defaults)","Default Provisional Account (Item Defaults)","Default Selling Cost Center (Item Defaults)","Default Supplier (Item Defaults)","Default Warehouse (Item Defaults)","Deferred Expense Account (Item Defaults)","Deferred Revenue Account (Item Defaults)","ID (Supplier Items)","Supplier (Supplier Items)","Supplier Part Number (Supplier Items)","ID (Customer Items)","Customer Group (Customer Items)","Customer Name (Customer Items)","Ref Code (Customer Items)","ID (Taxes)","Item Tax Template (Taxes)","Maximum Net Rate (Taxes)","Minimum Net Rate (Taxes)","Tax Category (Taxes)","Valid From (Taxes)"
|
||||
|
2333
csv/Item.csv
Normal file
2333
csv/Item.csv
Normal file
File diff suppressed because it is too large
Load Diff
2328
csv/Item2.csv
Normal file
2328
csv/Item2.csv
Normal file
File diff suppressed because it is too large
Load Diff
58
csv/data.py
Normal file
58
csv/data.py
Normal file
@ -0,0 +1,58 @@
|
||||
import csv
|
||||
|
||||
# FILES
|
||||
export_file = "Item Price.csv" # ERPNext export
|
||||
output_file = "Item Price-import-ready.csv" # clean import CSV
|
||||
|
||||
# Template columns (exact Column Name values for ERPNext import)
|
||||
template_columns = [
|
||||
"name","item_code","uom","price_list","price_list_rate","packing_unit",
|
||||
"item_name","brand","item_description","customer","supplier","batch_no",
|
||||
"buying","selling","currency","valid_from","lead_time_days","valid_upto",
|
||||
"note","reference"
|
||||
]
|
||||
|
||||
# Which row has the Column Name row in ERPNext export? Usually 20th (0-index 19)
|
||||
COLUMN_NAME_ROW = 19
|
||||
DATA_START_ROW = 21 # 0-indexed row where actual data starts
|
||||
|
||||
def clean_cell(cell):
|
||||
# Remove extra quotes around the data
|
||||
if cell.startswith('"""') and cell.endswith('"""'):
|
||||
return cell[3:-3]
|
||||
elif cell.startswith('"') and cell.endswith('"'):
|
||||
return cell[1:-1]
|
||||
return cell
|
||||
|
||||
# Read the export
|
||||
with open(export_file, newline='', encoding='utf-8') as f:
|
||||
reader = list(csv.reader(f))
|
||||
|
||||
export_columns = [clean_cell(c) for c in reader[COLUMN_NAME_ROW]]
|
||||
data_rows = reader[DATA_START_ROW-1:]
|
||||
|
||||
# Build column index map
|
||||
col_indexes = []
|
||||
for col in template_columns:
|
||||
if col in export_columns:
|
||||
col_indexes.append(export_columns.index(col))
|
||||
else:
|
||||
col_indexes.append(None) # fill missing columns with empty string
|
||||
|
||||
# Write clean CSV
|
||||
with open(output_file, "w", newline='', encoding='utf-8') as f_out:
|
||||
writer = csv.writer(f_out, quoting=csv.QUOTE_ALL)
|
||||
|
||||
# Header row: template
|
||||
writer.writerow(template_columns)
|
||||
|
||||
for row in data_rows:
|
||||
clean_row = []
|
||||
for idx in col_indexes:
|
||||
if idx is not None and idx < len(row):
|
||||
clean_row.append(clean_cell(row[idx]))
|
||||
else:
|
||||
clean_row.append("")
|
||||
writer.writerow(clean_row)
|
||||
|
||||
print(f"Clean Item Price CSV written to {output_file}")
|
||||
BIN
csv/item.xlsx
Normal file
BIN
csv/item.xlsx
Normal file
Binary file not shown.
@ -3,6 +3,32 @@ import json
|
||||
from custom_ui.db_utils import build_error_response, build_success_response
|
||||
from custom_ui.services import ClientService, AddressService, ContactService
|
||||
|
||||
@frappe.whitelist()
|
||||
def check_addresses_exist(addresses):
|
||||
"""Check if any of the provided addresses already exist in the system."""
|
||||
if isinstance(addresses, str):
|
||||
addresses = json.loads(addresses)
|
||||
print(f"DEBUG: check_addresses_exist called with addresses: {addresses}")
|
||||
existing_addresses = []
|
||||
for address in addresses:
|
||||
filters = {
|
||||
"doctype": "Address",
|
||||
"address_line1": address.get("address_line1"),
|
||||
"city": address.get("city"),
|
||||
# "state": address.get("state"),
|
||||
"pincode": address.get("pincode")
|
||||
}
|
||||
if address.get("address_line2"):
|
||||
filters["address_line2"] = address.get("address_line2")
|
||||
print(f"DEBUG: Checking existence for address with filters: {filters}")
|
||||
if frappe.db.exists(filters):
|
||||
print("DEBUG: Address exists:", filters)
|
||||
existing_addresses.append(address)
|
||||
else:
|
||||
print("DEBUG: Address does not exist:", filters)
|
||||
|
||||
return build_success_response(existing_addresses)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_address_by_full_address(full_address):
|
||||
"""Get address by full_address, including associated contacts."""
|
||||
|
||||
@ -9,6 +9,120 @@ from custom_ui.services import AddressService, ContactService, ClientService
|
||||
# CLIENT MANAGEMENT API METHODS
|
||||
# ===============================================================================
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_addresses_contacts(client_name, company_name, addresses=[], contacts=[]):
|
||||
if isinstance(addresses, str):
|
||||
addresses = json.loads(addresses)
|
||||
if isinstance(contacts, str):
|
||||
contacts = json.loads(contacts)
|
||||
print(f"DEBUG: add_addresses_contacts called with client_name: {client_name}, addresses: {addresses}, contacts: {contacts}")
|
||||
try:
|
||||
client_doc = ClientService.get_client_or_throw(client_name)
|
||||
if contacts:
|
||||
contact_docs = [frappe.get_doc("Contact", contact.contact) for contact in client_doc.contacts]
|
||||
for contact in contacts:
|
||||
contact_doc = None
|
||||
if frappe.db.exists("Contact", {"email_id": contact.get("email")}):
|
||||
contact_doc = frappe.get_doc("Contact", {"email_id": contact.get("email")})
|
||||
else:
|
||||
contact_doc = ContactService.create({
|
||||
"first_name": contact.get("first_name"),
|
||||
"last_name": contact.get("last_name"),
|
||||
"email_id": contact.get("email"),
|
||||
"role": contact.get("role"),
|
||||
"phone": contact.get("phone"),
|
||||
"custom_email": contact.get("email"),
|
||||
"is_primary_contact": 0,
|
||||
"customer_type": client_doc.doctype,
|
||||
"customer_name": client_doc.name,
|
||||
"email_ids": [{
|
||||
"email": contact.get("email"),
|
||||
"is_primary": 1
|
||||
}],
|
||||
"phone_nos": [{
|
||||
"phone": contact.get("phone"),
|
||||
"is_primary_phone": 1,
|
||||
"is_primary_mobile_no": 1
|
||||
}]
|
||||
})
|
||||
contact_doc.insert()
|
||||
ClientService.append_link_v2(client_doc.name, "contacts", {"contact": contact_doc.name})
|
||||
ContactService.link_contact_to_customer(contact_doc, client_doc.doctype, client_doc.name)
|
||||
contact_docs.append(contact_doc)
|
||||
address_docs = [frappe.get_doc("Address", link.address) for link in client_doc.properties]
|
||||
for address in addresses:
|
||||
address_doc = None
|
||||
if frappe.db.exists("Address", {
|
||||
"address_line1": address.get("address_line1"),
|
||||
"address_line2": address.get("address_line2"),
|
||||
"city": address.get("city"),
|
||||
# "state": address.get("state"),
|
||||
"pincode": address.get("pincode")
|
||||
}):
|
||||
address_doc = frappe.get_doc("Address", {
|
||||
"address_line1": address.get("address_line1"),
|
||||
"address_line2": address.get("address_line2"),
|
||||
"city": address.get("city"),
|
||||
# "state": address.get("state"),
|
||||
"pincode": address.get("pincode")
|
||||
})
|
||||
else:
|
||||
address_doc = AddressService.create({
|
||||
"address_title": AddressService.build_address_title(customer_name=client_name, address_data=address),
|
||||
"address_line1": address.get("address_line1"),
|
||||
"address_line2": address.get("address_line2"),
|
||||
"city": address.get("city"),
|
||||
"state": address.get("state"),
|
||||
"pincode": address.get("pincode"),
|
||||
"country": "United States",
|
||||
"address_type": "Service",
|
||||
"custom_billing_address": 0,
|
||||
"is_primary_address": 0,
|
||||
"is_service_address": 1,
|
||||
"customer_type": client_doc.doctype,
|
||||
"customer_name": client_doc.name
|
||||
})
|
||||
address_doc.insert()
|
||||
if company_name not in [company.company for company in address_doc.companies]:
|
||||
address_doc.append("companies", {"company": company_name})
|
||||
address_doc.save(ignore_permissions=True)
|
||||
AddressService.link_address_to_customer(address_doc, client_doc.doctype, client_doc.name)
|
||||
for contact_to_link_idx in address.get("contacts", []):
|
||||
contact_doc = contact_docs[contact_to_link_idx]
|
||||
AddressService.link_address_to_contact(address_doc, contact_doc.name)
|
||||
ContactService.link_contact_to_address(contact_doc, address_doc.name)
|
||||
primary_contact = contact_docs[address.get("primary_contact", 0)]
|
||||
AddressService.set_primary_contact(address_doc.name, primary_contact.name)
|
||||
ClientService.append_link_v2(client_doc.name, "properties", {"address": address_doc.name})
|
||||
address_docs.append(address_doc)
|
||||
|
||||
return build_success_response({
|
||||
"contacts": [contact.as_dict() for contact in contact_docs],
|
||||
"addresses": [address.as_dict() for address in address_docs],
|
||||
"message": "Addresses and contacts added successfully."
|
||||
})
|
||||
except Exception as e:
|
||||
return build_error_response(str(e), 500)
|
||||
|
||||
@frappe.whitelist()
|
||||
def check_client_exists(client_name):
|
||||
"""Check if a client exists as either a Customer or a Lead.
|
||||
Additionally, return a list of potential matches based on the client name."""
|
||||
print("DEBUG: check_client_exists called with client_name:", client_name)
|
||||
try:
|
||||
exact_customer_match = frappe.db.exists("Customer", client_name)
|
||||
exact_lead_match = frappe.db.exists("Lead", {"custom_customer_name": client_name})
|
||||
customer_matches = frappe.get_all("Customer", pluck="name", filters={"name": ["like", f"%{client_name}%"]})
|
||||
lead_matches = frappe.get_all("Lead", pluck="custom_customer_name", filters={"custom_customer_name": ["like", f"%{client_name}%"]})
|
||||
# remove duplicates from potential matches between customers and leads
|
||||
|
||||
return build_success_response({
|
||||
"exact_match": exact_customer_match or exact_lead_match,
|
||||
"potential_matches": list(set(customer_matches + lead_matches))
|
||||
})
|
||||
except Exception as e:
|
||||
return build_error_response(str(e), 500)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_client_status_counts(weekly=False, week_start_date=None, week_end_date=None):
|
||||
"""Get counts of clients by status categories with optional weekly filtering."""
|
||||
@ -363,15 +477,15 @@ def upsert_client(data):
|
||||
client_doc = check_and_get_client_doc(customer_name)
|
||||
if client_doc:
|
||||
return build_error_response(f"Client with name '{customer_name}' already exists.", 400)
|
||||
for address in addresses:
|
||||
if address_exists(
|
||||
address.get("address_line1"),
|
||||
address.get("address_line2"),
|
||||
address.get("city"),
|
||||
address.get("state"),
|
||||
address.get("pincode")
|
||||
):
|
||||
return build_error_response("This address already exists. Please use a different address or search for the address to find the associated client.", 400)
|
||||
# for address in addresses:
|
||||
# if address_exists(
|
||||
# address.get("address_line1"),
|
||||
# address.get("address_line2"),
|
||||
# address.get("city"),
|
||||
# address.get("state"),
|
||||
# address.get("pincode")
|
||||
# ):
|
||||
# return build_error_response("This address already exists. Please use a different address or search for the address to find the associated client.", 400)
|
||||
|
||||
# Handle customer creation/update
|
||||
|
||||
@ -444,25 +558,36 @@ def upsert_client(data):
|
||||
# Handle address creation
|
||||
address_docs = []
|
||||
for address in addresses:
|
||||
|
||||
is_billing = True if address.get("is_billing_address") else False
|
||||
is_service = True if address.get("is_service_address") else False
|
||||
print("#####DEBUG: Creating address with data:", address)
|
||||
address_doc = AddressService.create_address({
|
||||
"address_title": AddressService.build_address_title(customer_name, address),
|
||||
address_exists = frappe.db.exists("Address", {
|
||||
"address_line1": address.get("address_line1"),
|
||||
"address_line2": address.get("address_line2"),
|
||||
"address_type": "Billing" if is_billing else "Service",
|
||||
"custom_billing_address": is_billing,
|
||||
"is_service_address": is_service,
|
||||
"is_primary_address": is_billing,
|
||||
"city": address.get("city"),
|
||||
"state": address.get("state"),
|
||||
"country": "United States",
|
||||
"pincode": address.get("pincode"),
|
||||
"customer_type": "Lead",
|
||||
"customer_name": client_doc.name,
|
||||
"companies": [{ "company": data.get("company_name") }]
|
||||
"pincode": address.get("pincode")
|
||||
})
|
||||
address_doc = None
|
||||
if address_exists:
|
||||
address_doc = frappe.get_doc("Address", address_exists)
|
||||
else:
|
||||
print("#####DEBUG: Creating address with data:", address)
|
||||
address_doc = AddressService.create_address({
|
||||
"address_title": AddressService.build_address_title(customer_name, address),
|
||||
"address_line1": address.get("address_line1"),
|
||||
"address_line2": address.get("address_line2"),
|
||||
"address_type": "Billing" if is_billing else "Service",
|
||||
"custom_billing_address": is_billing,
|
||||
"is_service_address": is_service,
|
||||
"is_primary_address": is_billing,
|
||||
"city": address.get("city"),
|
||||
"state": address.get("state"),
|
||||
"country": "United States",
|
||||
"pincode": address.get("pincode"),
|
||||
"customer_type": "Lead",
|
||||
"customer_name": client_doc.name,
|
||||
"companies": [{ "company": data.get("company_name") }]
|
||||
})
|
||||
AddressService.link_address_to_customer(address_doc, "Lead", client_doc.name)
|
||||
address_doc.reload()
|
||||
if is_billing:
|
||||
|
||||
@ -1,4 +1,23 @@
|
||||
import frappe
|
||||
import json
|
||||
from custom_ui.db_utils import build_error_response, build_success_response
|
||||
|
||||
@frappe.whitelist()
|
||||
def check_contacts_exist(contacts):
|
||||
"""Check if any of the provided contacts already exist in the system."""
|
||||
if isinstance(contacts, str):
|
||||
contacts = json.loads(contacts)
|
||||
print(f"DEBUG: check_contacts_exist called with contacts: {contacts}")
|
||||
existing_contacts = []
|
||||
for contact in contacts:
|
||||
if frappe.db.exists("Contact", {
|
||||
"first_name": contact.get("first_name"),
|
||||
"last_name": contact.get("last_name"),
|
||||
"email_id": contact.get("email"),
|
||||
"phone": contact.get("phone_number")
|
||||
}):
|
||||
existing_contacts.append(contact)
|
||||
return build_success_response(existing_contacts)
|
||||
|
||||
def existing_contact_name(first_name: str, last_name: str, email: str, phone: str) -> str:
|
||||
"""Check if a contact exists based on provided details."""
|
||||
|
||||
@ -15,6 +15,13 @@ def get_estimate_table_data_v2(filters={}, sortings=[], page=1, page_size=10):
|
||||
"""Get paginated estimate table data with filtering and sorting."""
|
||||
print("DEBUG: Raw estimate options received:", filters, sortings, page, page_size)
|
||||
filters, sortings, page, page_size = DbUtils.process_datatable_request(filters, sortings, page, page_size)
|
||||
if filters.get("address"):
|
||||
# change the key from address to custom_job_address
|
||||
filters["custom_job_address"] = filters.pop("address")
|
||||
filters["custom_job_address"] = ["like", f"%{filters['custom_job_address']['value']}%"]
|
||||
if filters.get("customer"):
|
||||
filters["actual_customer_name"] = ["like", f"%{filters.pop('customer')['value']}%"]
|
||||
print("DEBUG: Processed estimate options - Filters:", filters, "Sortings:", sortings, "Page:", page, "Page Size:", page_size)
|
||||
sortings = "modified desc" if not sortings else sortings
|
||||
count = frappe.db.count("Quotation", filters=filters)
|
||||
print(f"DEBUG: Number of estimates returned: {count}")
|
||||
@ -145,7 +152,7 @@ def get_estimate(estimate_name):
|
||||
est_dict["address_details"] = address_doc
|
||||
|
||||
est_dict["history"] = get_doc_history("Quotation", estimate_name)
|
||||
est_dict["items"] = [ItemService.get_full_dict(item.item_code) for item in estimate.items]
|
||||
est_dict["items"] = [{**ItemService.get_full_dict(item.item_code), **item.as_dict()} for item in estimate.items]
|
||||
|
||||
return build_success_response(est_dict)
|
||||
except Exception as e:
|
||||
@ -462,6 +469,7 @@ def upsert_estimate(data):
|
||||
estimate.requires_half_payment = data.get("requires_half_payment", 0)
|
||||
estimate.custom_project_template = project_template
|
||||
estimate.custom_quotation_template = data.get("quotation_template", None)
|
||||
estimate.remarks = data.get("remarks", "")
|
||||
# estimate.company = data.get("company")
|
||||
# estimate.contact_email = data.get("contact_email")
|
||||
# estimate.quotation_to = client_doctype
|
||||
@ -515,7 +523,8 @@ def upsert_estimate(data):
|
||||
"letter_head": data.get("company"),
|
||||
"custom_project_template": data.get("project_template", None),
|
||||
"custom_quotation_template": data.get("quotation_template", None),
|
||||
"from_onsite_meeting": data.get("from_onsite_meeting", None)
|
||||
"from_onsite_meeting": data.get("from_onsite_meeting", None),
|
||||
"remarks": data.get("remarks", "")
|
||||
})
|
||||
for item in data.get("items", []):
|
||||
item = json.loads(item) if isinstance(item, str) else item
|
||||
@ -534,7 +543,7 @@ def upsert_estimate(data):
|
||||
# ClientService.append_link(data.get("customer"), "quotations", "quotation", new_estimate.name)
|
||||
print("DEBUG: New estimate created with name:", new_estimate.name)
|
||||
dict = new_estimate.as_dict()
|
||||
dict["items"] = [ItemService.get_full_dict(item.item_code) for item in new_estimate.items]
|
||||
dict["items"] = [{**item.as_dict(), **ItemService.get_full_dict(item.item_code)} for item in new_estimate.items]
|
||||
return build_success_response(dict)
|
||||
except Exception as e:
|
||||
print(f"DEBUG: Error in upsert_estimate: {str(e)}")
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import frappe, json
|
||||
from custom_ui.db_utils import process_query_conditions, build_datatable_dict, get_count_or_filters, build_success_response, build_error_response
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
|
||||
from custom_ui.services import SalesOrderService, EmailService
|
||||
|
||||
# ===============================================================================
|
||||
# INVOICES API METHODS
|
||||
@ -10,15 +11,40 @@ from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
|
||||
@frappe.whitelist()
|
||||
def create_invoice_for_job(job_name):
|
||||
"""Create the invoice from a sales order of a job."""
|
||||
print("DEBUG: create_invoice_for_job called with job_name:", job_name)
|
||||
try:
|
||||
project = frappe.get_doc("Project", job_name)
|
||||
sales_order = project.sales_order
|
||||
invoice = make_sales_invoice(sales_order)
|
||||
invoice.save()
|
||||
sales_order = frappe.get_value("Project", job_name, "sales_order")
|
||||
if not sales_order:
|
||||
return build_error_response("No sales order found for this job.", 404)
|
||||
invoice = SalesOrderService.create_sales_invoice_from_sales_order(sales_order)
|
||||
return build_success_response(invoice.as_dict())
|
||||
except Exception as e:
|
||||
return build_error_response(str(e), 500)
|
||||
|
||||
@frappe.whitelist()
|
||||
def submit_and_send_invoice(invoice_name):
|
||||
"""Submit the invoice and send email to customer."""
|
||||
print("DEBUG: submit_and_send_invoice called with invoice_name:", invoice_name)
|
||||
try:
|
||||
invoice_doc = frappe.get_doc("Sales Invoice", invoice_name)
|
||||
if invoice_doc.docstatus == 0:
|
||||
print("DEBUG: Submitting invoice:", invoice_name)
|
||||
invoice_doc.submit()
|
||||
else:
|
||||
print("DEBUG: Invoice already submitted:", invoice_name)
|
||||
# Send invoice email to customer
|
||||
try:
|
||||
print("DEBUG: Preparing to send invoice email for", invoice_name)
|
||||
EmailService.send_invoice_email(invoice_name)
|
||||
print("DEBUG: Invoice email sent successfully for", invoice_name)
|
||||
frappe.set_value("Project", invoice_doc.project, "invoice_status", "Invoice Sent")
|
||||
except Exception as e:
|
||||
print(f"ERROR: Failed to send invoice email: {str(e)}")
|
||||
# Don't raise the exception - we don't want to block the invoice submission
|
||||
frappe.log_error(f"Failed to send invoice email for {invoice_name}: {str(e)}", "Invoice Email Error")
|
||||
return build_success_response(invoice_doc.as_dict())
|
||||
except Exception as e:
|
||||
return build_error_response(str(e), 500)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_invoices_late_count():
|
||||
|
||||
@ -61,7 +61,7 @@ def get_unscheduled_service_appointments(companies):
|
||||
return build_error_response(str(e), 500)
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_service_appointment_scheduled_dates(service_appointment_name: str, start_date, end_date, crew_lead_name, start_time=None, end_time=None):
|
||||
def update_service_appointment_scheduled_dates(service_appointment_name: str, start_date, end_date, crew_lead_name, skipped_days=[], start_time=None, end_time=None):
|
||||
"""Update scheduled dates for a Service Appointment."""
|
||||
print(f"DEBUG: Updating scheduled dates for Service Appointment {service_appointment_name} to start: {start_date}, end: {end_date}, crew lead: {crew_lead_name}, start time: {start_time}, end time: {end_time}")
|
||||
try:
|
||||
@ -71,7 +71,8 @@ def update_service_appointment_scheduled_dates(service_appointment_name: str, st
|
||||
start_date,
|
||||
end_date,
|
||||
start_time,
|
||||
end_time
|
||||
end_time,
|
||||
skip_days=skipped_days
|
||||
)
|
||||
return build_success_response(updated_service_appointment.as_dict())
|
||||
except Exception as e:
|
||||
|
||||
@ -4,6 +4,7 @@ from datetime import datetime
|
||||
from frappe.utils.data import flt
|
||||
from custom_ui.services import DbService, StripeService, PaymentService
|
||||
from custom_ui.models import PaymentData
|
||||
from werkzeug.wrappers import Response
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def half_down_stripe_payment(sales_order):
|
||||
@ -36,7 +37,11 @@ def invoice_stripe_payment(sales_invoice):
|
||||
if si.docstatus != 1:
|
||||
frappe.throw("Sales Invoice must be submitted to proceed with payment.")
|
||||
if si.outstanding_amount <= 0:
|
||||
frappe.throw("This invoice has already been paid.")
|
||||
html = frappe.render_template("custom_ui/templates/invoices/already_paid.html", {
|
||||
"invoice_number": si.name,
|
||||
"company": frappe.get_doc("Company", si.company)
|
||||
})
|
||||
return Response(html, content_type='text/html')
|
||||
stripe_session = StripeService.create_checkout_session(
|
||||
company=si.company,
|
||||
amount=si.outstanding_amount,
|
||||
|
||||
@ -4,6 +4,7 @@ import subprocess
|
||||
import frappe
|
||||
from custom_ui.utils import create_module
|
||||
from custom_ui.api.db.general import search_any_field
|
||||
from custom_ui.install import create_companies, create_project_templates, create_task_types, create_tasks, create_bid_meeting_note_form_templates
|
||||
|
||||
@click.command("update-data")
|
||||
@click.option("--site", default=None, help="Site to update data for")
|
||||
@ -100,5 +101,9 @@ def build_frontend(site):
|
||||
def create_module_command():
|
||||
create_module()
|
||||
click.echo("✅ Custom UI module created or already exists.")
|
||||
|
||||
def setup_custom_ui():
|
||||
pass
|
||||
|
||||
|
||||
commands = [build_frontend, create_module_command]
|
||||
@ -0,0 +1,37 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2026-02-18 05:53:09.935695",
|
||||
"custom": 1,
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"company"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-18 13:24:00.502036",
|
||||
"modified_by": "casey@shilohcode.com",
|
||||
"module": "Custom UI",
|
||||
"name": "Address Company Link",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"rows_threshold_for_grid_search": 20,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2026-02-18 05:53:10.533385",
|
||||
"custom": 1,
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"customer_type",
|
||||
"customer_name",
|
||||
"relation",
|
||||
"is_active"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"default": "Customer",
|
||||
"fieldname": "customer_type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Customer Type",
|
||||
"options": "Customer\nLead",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "customer_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Customer Name",
|
||||
"options": "customer_type",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "relation",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Relation",
|
||||
"options": "Owner\nBuilder",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "is_active",
|
||||
"fieldtype": "Check",
|
||||
"label": "Active"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-18 05:53:10.573936",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom UI",
|
||||
"name": "Address Customer Link",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"rows_threshold_for_grid_search": 20,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@ -1,7 +1,8 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2026-01-30 07:21:48.230423",
|
||||
"creation": "2026-02-18 05:53:08.779777",
|
||||
"custom": 1,
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
@ -29,7 +30,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-30 07:52:45.496993",
|
||||
"modified": "2026-02-18 05:53:08.816451",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom UI",
|
||||
"name": "Address Task Link",
|
||||
|
||||
@ -0,0 +1,104 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2026-02-18 05:53:07.884371",
|
||||
"custom": 1,
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"section_break_m7ec",
|
||||
"property_address",
|
||||
"technician",
|
||||
"test_date",
|
||||
"result",
|
||||
"test_report",
|
||||
"notes",
|
||||
"water_district_submitted",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "section_break_m7ec",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "property_address",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Property Address",
|
||||
"options": "Address",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "technician",
|
||||
"fieldtype": "Link",
|
||||
"label": "Technician",
|
||||
"options": "Employee"
|
||||
},
|
||||
{
|
||||
"fieldname": "test_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Test Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "result",
|
||||
"fieldtype": "Select",
|
||||
"label": "Result",
|
||||
"options": "Pass\nFail"
|
||||
},
|
||||
{
|
||||
"fieldname": "test_report",
|
||||
"fieldtype": "Attach",
|
||||
"label": "Test Report"
|
||||
},
|
||||
{
|
||||
"fieldname": "notes",
|
||||
"fieldtype": "Text",
|
||||
"label": "Notes"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "water_district_submitted",
|
||||
"fieldtype": "Check",
|
||||
"label": "Water District Submitted"
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Backflow Test Form",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-18 10:48:33.908921",
|
||||
"modified_by": "casey@shilohcode.com",
|
||||
"module": "Custom UI",
|
||||
"name": "Backflow Test Form",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@ -25,7 +25,7 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Project Template",
|
||||
"options": "Project Template",
|
||||
"reqd": 1
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "notes",
|
||||
|
||||
@ -0,0 +1,123 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_auto_repeat": 1,
|
||||
"allow_events_in_timeline": 1,
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "naming_series:",
|
||||
"color": "Blue",
|
||||
"creation": "2026-02-18 05:53:07.971371",
|
||||
"custom": 1,
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Other",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"naming_series",
|
||||
"route_name",
|
||||
"service_date",
|
||||
"route_description",
|
||||
"areazone",
|
||||
"crew_leader",
|
||||
"assigned_technicians",
|
||||
"assigned_addresses",
|
||||
"route_map"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Select",
|
||||
"label": "Route Number",
|
||||
"options": "Route - .#####"
|
||||
},
|
||||
{
|
||||
"fieldname": "route_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Route Name"
|
||||
},
|
||||
{
|
||||
"fieldname": "service_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Service Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "route_description",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Route Description"
|
||||
},
|
||||
{
|
||||
"fieldname": "areazone",
|
||||
"fieldtype": "Link",
|
||||
"label": "Area/Zone",
|
||||
"options": "Territory"
|
||||
},
|
||||
{
|
||||
"fieldname": "crew_leader",
|
||||
"fieldtype": "Link",
|
||||
"label": "Crew Leader",
|
||||
"options": "Employee"
|
||||
},
|
||||
{
|
||||
"fieldname": "assigned_technicians",
|
||||
"fieldtype": "Table",
|
||||
"label": "Assigned Technicians",
|
||||
"options": "Route Technician Assignment"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 1,
|
||||
"allow_in_quick_entry": 1,
|
||||
"columns": 5,
|
||||
"fieldname": "assigned_addresses",
|
||||
"fieldtype": "Table",
|
||||
"label": "Assigned Addresses",
|
||||
"options": "Assigned Address"
|
||||
},
|
||||
{
|
||||
"fieldname": "route_map",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Route Map"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-18 11:06:00.945057",
|
||||
"modified_by": "casey@shilohcode.com",
|
||||
"module": "Custom UI",
|
||||
"name": "Pre-Built Routes",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Sales User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "route_name, service_date",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
// Copyright (c) 2026, Shiloh Code LLC and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Service Address 2", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@ -1,7 +1,8 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2026-01-30 07:01:57.571003",
|
||||
"creation": "2026-02-18 05:53:10.416092",
|
||||
"custom": 1,
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@ -19,7 +20,9 @@
|
||||
"customer",
|
||||
"company",
|
||||
"service_address",
|
||||
"foreman"
|
||||
"foreman",
|
||||
"ready_to_schedule",
|
||||
"skip_days"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -113,12 +116,24 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Foreman",
|
||||
"options": "Employee"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "ready_to_schedule",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ready To Schedule"
|
||||
},
|
||||
{
|
||||
"fieldname": "skip_days",
|
||||
"fieldtype": "Table",
|
||||
"label": "Skip Days",
|
||||
"options": "Skip Day"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-30 07:15:39.410145",
|
||||
"modified": "2026-02-18 05:53:10.467828",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom UI",
|
||||
"name": "Service Address 2",
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
# Copyright (c) 2026, Shiloh Code LLC and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class ServiceAddress2(Document):
|
||||
pass
|
||||
@ -1,9 +0,0 @@
|
||||
# Copyright (c) 2026, Shiloh Code LLC and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestServiceAddress2(FrappeTestCase):
|
||||
pass
|
||||
@ -2,7 +2,7 @@
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "format:SA-{MM}-{YYYY}-{####}",
|
||||
"creation": "2026-01-30 07:01:56.861733",
|
||||
"creation": "2026-02-11 05:12:39.498845",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@ -110,7 +110,7 @@
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-30 07:18:16.297996",
|
||||
"modified": "2026-02-13 03:39:36.214833",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom UI",
|
||||
"name": "Service Appointment",
|
||||
|
||||
@ -75,9 +75,9 @@ def on_update_after_submit(doc, method):
|
||||
new_sales_order.customer_address = doc.customer_address
|
||||
# new_sales_order.custom_installation_address = doc.custom_installation_address
|
||||
new_sales_order.custom_job_address = doc.custom_job_address
|
||||
new_sales_order.payment_schedule = []
|
||||
new_sales_order.set("payment_schedule", [])
|
||||
print("DEBUG: Setting payment schedule for Sales Order")
|
||||
new_sales_order.set_payment_schedule()
|
||||
# new_sales_order.set_payment_schedule()
|
||||
print("DEBUG: Inserting Sales Order:", new_sales_order.as_dict())
|
||||
new_sales_order.delivery_date = new_sales_order.transaction_date
|
||||
# backup = new_sales_order.customer_address
|
||||
|
||||
@ -31,6 +31,8 @@ def after_insert(doc, method):
|
||||
"project_template": doc.project_template
|
||||
})
|
||||
doc.service_appointment = service_apt.name
|
||||
if doc.requires_half_payment:
|
||||
service_apt.ready_to_schedule = 0
|
||||
doc.save(ignore_permissions=True)
|
||||
print("DEBUG: Created Service Appointment:", service_apt.name)
|
||||
except Exception as e:
|
||||
@ -63,6 +65,9 @@ def before_insert(doc, method):
|
||||
def before_save(doc, method):
|
||||
print("DEBUG: Before Save Triggered for Project:", doc.name)
|
||||
print("DEBUG: Checking status: ", doc.status)
|
||||
if doc.percent_complete == 100.0 and doc.invoice_status == "Not Ready":
|
||||
print("DEBUG: Project is 100% complete and invoice status is Not Ready, setting invoice status to Ready to Invoice")
|
||||
doc.invoice_status = "Ready to Invoice"
|
||||
if doc.expected_start_date and doc.expected_end_date:
|
||||
print("DEBUG: Project has expected start and end dates, marking as scheduled")
|
||||
doc.is_scheduled = 1
|
||||
@ -82,11 +87,18 @@ def before_save(doc, method):
|
||||
|
||||
def after_save(doc, method):
|
||||
print("DEBUG: After Save Triggered for Project:", doc.name)
|
||||
if doc.status == "Completed":
|
||||
print("DEBUG: Project marked as Completed. Generating and sending final invoice.")
|
||||
sales_order_status = frappe.get_value("Sales Order", doc.sales_order, "billing_status")
|
||||
if sales_order_status == "Not Billed":
|
||||
SalesOrderService.create_sales_invoice_from_sales_order(doc.sales_order)
|
||||
if doc.ready_to_schedule and doc.service_appointment:
|
||||
service_apt_ready_to_schedule = frappe.get_value("Service Address 2", doc.service_appointment, "ready_to_schedule")
|
||||
if not service_apt_ready_to_schedule:
|
||||
print("DEBUG: Project is ready to schedule, setting Service Appointment to ready to schedule.")
|
||||
service_apt_doc = frappe.get_doc("Service Address 2", doc.service_appointment)
|
||||
service_apt_doc.ready_to_schedule = 1
|
||||
service_apt_doc.save(ignore_permissions=True)
|
||||
# if doc.status == "Completed":
|
||||
|
||||
# sales_order_status = frappe.get_value("Sales Order", doc.sales_order, "billing_status")
|
||||
# if sales_order_status == "Not Billed":
|
||||
# SalesOrderService.create_sales_invoice_from_sales_order(doc.sales_order)
|
||||
if doc.ready_to_schedule:
|
||||
service_apt_ready_to_schedule = frappe.get_value("Service Address 2", doc.service_appointment, "ready_to_schedule")
|
||||
if not service_apt_ready_to_schedule:
|
||||
|
||||
@ -1,22 +1,23 @@
|
||||
import frappe
|
||||
from custom_ui.services.email_service import EmailService
|
||||
from custom_ui.services import ProjectService
|
||||
|
||||
def on_submit(doc, method):
|
||||
print("DEBUG: On Submit Triggered for Sales Invoice:", doc.name)
|
||||
|
||||
# Send invoice email to customer
|
||||
try:
|
||||
print("DEBUG: Preparing to send invoice email for", doc.name)
|
||||
EmailService.send_invoice_email(doc.name)
|
||||
print("DEBUG: Invoice email sent successfully for", doc.name)
|
||||
frappe.set_value("Project", doc.project, "invoice_status", "Invoice Sent")
|
||||
except Exception as e:
|
||||
print(f"ERROR: Failed to send invoice email: {str(e)}")
|
||||
# Don't raise the exception - we don't want to block the invoice submission
|
||||
frappe.log_error(f"Failed to send invoice email for {doc.name}: {str(e)}", "Invoice Email Error")
|
||||
# # Send invoice email to customer
|
||||
# try:
|
||||
# print("DEBUG: Preparing to send invoice email for", doc.name)
|
||||
# EmailService.send_invoice_email(doc.name)
|
||||
# print("DEBUG: Invoice email sent successfully for", doc.name)
|
||||
# frappe.set_value("Project", doc.project, "invoice_status", "Invoice Sent")
|
||||
# except Exception as e:
|
||||
# print(f"ERROR: Failed to send invoice email: {str(e)}")
|
||||
# # Don't raise the exception - we don't want to block the invoice submission
|
||||
# frappe.log_error(f"Failed to send invoice email for {doc.name}: {str(e)}", "Invoice Email Error")
|
||||
|
||||
def after_insert(doc, method):
|
||||
print("DEBUG: After Insert Triggered for Sales Invoice:", doc.name)
|
||||
# Additional logic can be added here if needed after invoice creation
|
||||
frappe.set_value("Project", doc.project, "invoice_status", "Invoice Created")
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import frappe
|
||||
from custom_ui.services import DbService, AddressService, ClientService
|
||||
from custom_ui.services import DbService, AddressService, ClientService, EmailService
|
||||
|
||||
|
||||
def before_save(doc, method):
|
||||
@ -76,7 +76,6 @@ def after_insert(doc, method):
|
||||
if doc.requires_half_payment:
|
||||
try:
|
||||
print("DEBUG: Sales Order requires half payment, preparing to send down payment email")
|
||||
from custom_ui.services.email_service import EmailService
|
||||
|
||||
# Use EmailService to send the down payment email
|
||||
EmailService.send_downpayment_email(doc.name)
|
||||
|
||||
@ -4,6 +4,13 @@ from custom_ui.services import AddressService, ClientService, TaskService
|
||||
def before_insert(doc, method):
|
||||
"""Set values before inserting a Task."""
|
||||
print("DEBUG: Before Insert Triggered for Task")
|
||||
if doc.type:
|
||||
task_type_weight = frappe.get_value("Task Type", doc.type, "weight") or 0
|
||||
print(f"DEBUG: Setting Task weight to {task_type_weight} based on Task Type {doc.type}")
|
||||
doc.task_weight = task_type_weight
|
||||
if doc.status == "Template":
|
||||
print("DEBUG: Task is a Template, skipping project linking")
|
||||
return
|
||||
project_doc = frappe.get_doc("Project", doc.project)
|
||||
doc.project_template = project_doc.project_template
|
||||
doc.customer = project_doc.customer
|
||||
@ -12,6 +19,9 @@ def before_insert(doc, method):
|
||||
|
||||
def after_insert(doc, method):
|
||||
print("DEBUG: After Insert Triggered for Task")
|
||||
if doc.status == "Template":
|
||||
print("DEBUG: Task is a Template, skipping linking to Customer and Address")
|
||||
return
|
||||
print("DEBUG: Linking Task to Customer and Address")
|
||||
AddressService.append_link_v2(
|
||||
doc.custom_property, "tasks", {"task": doc.name, "project_template": doc.project_template }
|
||||
@ -27,25 +37,32 @@ def after_insert(doc, method):
|
||||
|
||||
def before_save(doc, method):
|
||||
print("DEBUG: Before Save Triggered for Task:", doc.name)
|
||||
task_type_weight = frappe.get_value("Task Type", doc.type, "weight") or 0
|
||||
if doc.task_weight != task_type_weight:
|
||||
print(f"DEBUG: Updating Task weight from {doc.task_weight} to {task_type_weight}")
|
||||
doc.task_weight = task_type_weight
|
||||
event = TaskService.determine_event(doc)
|
||||
if event:
|
||||
task_names = [task.name for task in TaskService.get_tasks_by_project(doc.project)]
|
||||
TaskService.fire_task_triggers(task_names, event, current_triggering_dict=doc.as_dict())
|
||||
if doc.type:
|
||||
task_type_weight = frappe.get_value("Task Type", doc.type, "weight") or 0
|
||||
if doc.task_weight != task_type_weight:
|
||||
print(f"DEBUG: Updating Task weight from {doc.task_weight} to {task_type_weight}")
|
||||
doc.task_weight = task_type_weight
|
||||
if doc.status != "Template":
|
||||
event = TaskService.determine_event(doc)
|
||||
if event:
|
||||
task_names = [task.name for task in TaskService.get_tasks_by_project(doc.project)]
|
||||
TaskService.fire_task_triggers(task_names, event, current_triggering_dict=doc.as_dict())
|
||||
|
||||
def after_save(doc, method):
|
||||
print("DEBUG: After Save Triggered for Task:", doc.name)
|
||||
if doc.status == "Template":
|
||||
print("DEBUG: Task is a Template, skipping after save logic")
|
||||
return
|
||||
if doc.project and doc.status == "Completed":
|
||||
print("DEBUG: Task is completed, checking if project has calculated 100% Progress.")
|
||||
project_doc = frappe.get_doc("Project", doc.project)
|
||||
if project_doc.percent_complete == 100:
|
||||
print("DEBUG: Current Project percent_complete:", project_doc.percent_complete)
|
||||
if project_doc.percent_complete == 100.0:
|
||||
print("DEBUG: Project percent_complete is 100%, checking if we need to update project status or invoice status.")
|
||||
project_update_required = False
|
||||
if project_doc.status == "Completed" and project_doc.customCompletionDate is None:
|
||||
if project_doc.status == "Completed" and project_doc.custom_completion_date is None:
|
||||
print("DEBUG: Project is marked as Completed but customCompletionDate is not set, updating customCompletionDate.")
|
||||
project_doc.customCompletionDate = frappe.utils.nowdate()
|
||||
project_doc.custom_completion_date = frappe.utils.nowdate()
|
||||
project_update_required = True
|
||||
if project_doc.invoice_status == "Not Ready":
|
||||
project_doc.invoice_status = "Ready to Invoice"
|
||||
|
||||
153
custom_ui/fixtures/bid_meeting_note_form.json
Normal file
153
custom_ui/fixtures/bid_meeting_note_form.json
Normal file
@ -0,0 +1,153 @@
|
||||
[
|
||||
{
|
||||
"company": "Sprinklers Northwest",
|
||||
"docstatus": 0,
|
||||
"doctype": "Bid Meeting Note Form",
|
||||
"fields": [
|
||||
{
|
||||
"column": 1,
|
||||
"conditional_on_field": null,
|
||||
"conditional_on_value": null,
|
||||
"default_value": null,
|
||||
"doctype_for_select": null,
|
||||
"doctype_label_field": null,
|
||||
"help_text": "Indicate if a locate is needed for this project.",
|
||||
"include_options": 0,
|
||||
"label": "Locate Needed",
|
||||
"options": null,
|
||||
"order": 0,
|
||||
"parent": "SNW Install Bid Meeting Notes",
|
||||
"parentfield": "fields",
|
||||
"parenttype": "Bid Meeting Note Form",
|
||||
"read_only": 0,
|
||||
"required": 0,
|
||||
"row": 1,
|
||||
"type": "Check"
|
||||
},
|
||||
{
|
||||
"column": 2,
|
||||
"conditional_on_field": null,
|
||||
"conditional_on_value": null,
|
||||
"default_value": null,
|
||||
"doctype_for_select": null,
|
||||
"doctype_label_field": null,
|
||||
"help_text": "Indicate if a permit is needed for this project.",
|
||||
"include_options": 0,
|
||||
"label": "Permit Needed",
|
||||
"options": null,
|
||||
"order": 0,
|
||||
"parent": "SNW Install Bid Meeting Notes",
|
||||
"parentfield": "fields",
|
||||
"parenttype": "Bid Meeting Note Form",
|
||||
"read_only": 0,
|
||||
"required": 0,
|
||||
"row": 1,
|
||||
"type": "Check"
|
||||
},
|
||||
{
|
||||
"column": 3,
|
||||
"conditional_on_field": null,
|
||||
"conditional_on_value": null,
|
||||
"default_value": null,
|
||||
"doctype_for_select": null,
|
||||
"doctype_label_field": null,
|
||||
"help_text": "Indicate if a backflow test is required after installation.",
|
||||
"include_options": 0,
|
||||
"label": "Back Flow Test Required",
|
||||
"options": null,
|
||||
"order": 0,
|
||||
"parent": "SNW Install Bid Meeting Notes",
|
||||
"parentfield": "fields",
|
||||
"parenttype": "Bid Meeting Note Form",
|
||||
"read_only": 0,
|
||||
"required": 0,
|
||||
"row": 1,
|
||||
"type": "Check"
|
||||
},
|
||||
{
|
||||
"column": 1,
|
||||
"conditional_on_field": null,
|
||||
"conditional_on_value": null,
|
||||
"default_value": null,
|
||||
"doctype_for_select": null,
|
||||
"doctype_label_field": null,
|
||||
"help_text": null,
|
||||
"include_options": 0,
|
||||
"label": "Machine Access",
|
||||
"options": null,
|
||||
"order": 0,
|
||||
"parent": "SNW Install Bid Meeting Notes",
|
||||
"parentfield": "fields",
|
||||
"parenttype": "Bid Meeting Note Form",
|
||||
"read_only": 0,
|
||||
"required": 0,
|
||||
"row": 2,
|
||||
"type": "Check"
|
||||
},
|
||||
{
|
||||
"column": 2,
|
||||
"conditional_on_field": "Machine Access",
|
||||
"conditional_on_value": null,
|
||||
"default_value": null,
|
||||
"doctype_for_select": null,
|
||||
"doctype_label_field": null,
|
||||
"help_text": null,
|
||||
"include_options": 1,
|
||||
"label": "Machines",
|
||||
"options": "MT, Skip Steer, Excavator-E-50, Link Belt, Tre?, Forks, Auger, Backhoe, Loader, Duzer",
|
||||
"order": 0,
|
||||
"parent": "SNW Install Bid Meeting Notes",
|
||||
"parentfield": "fields",
|
||||
"parenttype": "Bid Meeting Note Form",
|
||||
"read_only": 0,
|
||||
"required": 0,
|
||||
"row": 2,
|
||||
"type": "Multi-Select"
|
||||
},
|
||||
{
|
||||
"column": 0,
|
||||
"conditional_on_field": null,
|
||||
"conditional_on_value": null,
|
||||
"default_value": null,
|
||||
"doctype_for_select": null,
|
||||
"doctype_label_field": null,
|
||||
"help_text": null,
|
||||
"include_options": 0,
|
||||
"label": "Materials Required",
|
||||
"options": null,
|
||||
"order": 0,
|
||||
"parent": "SNW Install Bid Meeting Notes",
|
||||
"parentfield": "fields",
|
||||
"parenttype": "Bid Meeting Note Form",
|
||||
"read_only": 0,
|
||||
"required": 0,
|
||||
"row": 3,
|
||||
"type": "Check"
|
||||
},
|
||||
{
|
||||
"column": 0,
|
||||
"conditional_on_field": "Materials Required",
|
||||
"conditional_on_value": null,
|
||||
"default_value": null,
|
||||
"doctype_for_select": "Item",
|
||||
"doctype_label_field": "itemName",
|
||||
"help_text": null,
|
||||
"include_options": 0,
|
||||
"label": "Materials",
|
||||
"options": null,
|
||||
"order": 0,
|
||||
"parent": "SNW Install Bid Meeting Notes",
|
||||
"parentfield": "fields",
|
||||
"parenttype": "Bid Meeting Note Form",
|
||||
"read_only": 0,
|
||||
"required": 0,
|
||||
"row": 4,
|
||||
"type": "Multi-Select w/ Quantity"
|
||||
}
|
||||
],
|
||||
"modified": "2026-02-18 05:52:37.304228",
|
||||
"name": "SNW Install Bid Meeting Notes",
|
||||
"notes": null,
|
||||
"title": "SNW Install Bid Meeting Notes"
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,12 +0,0 @@
|
||||
[
|
||||
{
|
||||
"docstatus": 0,
|
||||
"doctype": "Email Template",
|
||||
"modified": "2026-01-24 07:18:15.939258",
|
||||
"name": "Customer Invoice",
|
||||
"response": "<div class=\"ql-editor read-mode\"><p>-- Copywriting goes here --</p><p>-- Customized Payment Link goes here --</p><p>-- In the meantime --</p><p>Invoice number: {{ name }}</p><p>Amount: {{ grand_total }}</p><p>https://sprinklersnorthwest.com/product/bill-pay/</p></div>",
|
||||
"response_html": null,
|
||||
"subject": "Your Invoice is Ready",
|
||||
"use_html": 0
|
||||
}
|
||||
]
|
||||
@ -1,12 +1,13 @@
|
||||
[
|
||||
{
|
||||
"bid_meeting_note_form": null,
|
||||
"calendar_color": null,
|
||||
"bid_meeting_note_form": "SNW Install Bid Meeting Notes",
|
||||
"calendar_color": "#c1dec5",
|
||||
"company": "Sprinklers Northwest",
|
||||
"custom__complete_method": "Task Weight",
|
||||
"docstatus": 0,
|
||||
"doctype": "Project Template",
|
||||
"item_groups": null,
|
||||
"modified": "2026-01-29 09:51:46.681553",
|
||||
"item_groups": "SNW-I, SNW-S, SNW-LS",
|
||||
"modified": "2026-02-16 03:59:53.719382",
|
||||
"name": "SNW Install",
|
||||
"project_type": "External",
|
||||
"tasks": [
|
||||
@ -51,26 +52,20 @@
|
||||
"parenttype": "Project Template",
|
||||
"subject": "Curbing",
|
||||
"task": "TASK-2025-00006"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"bid_meeting_note_form": null,
|
||||
"calendar_color": null,
|
||||
"company": "Sprinklers Northwest",
|
||||
"docstatus": 0,
|
||||
"doctype": "Project Template",
|
||||
"item_groups": null,
|
||||
"modified": "2026-01-08 10:36:39.245470",
|
||||
"name": "Other",
|
||||
"project_type": null,
|
||||
"tasks": [
|
||||
},
|
||||
{
|
||||
"parent": "Other",
|
||||
"parent": "SNW Install",
|
||||
"parentfield": "tasks",
|
||||
"parenttype": "Project Template",
|
||||
"subject": "Primary Job",
|
||||
"task": "TASK-2025-00004"
|
||||
"subject": "15-Day QA",
|
||||
"task": "TASK-2025-00007"
|
||||
},
|
||||
{
|
||||
"parent": "SNW Install",
|
||||
"parentfield": "tasks",
|
||||
"parenttype": "Project Template",
|
||||
"subject": "Permit Close-out",
|
||||
"task": "TASK-2025-00008"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
14994
custom_ui/fixtures/property_setter.json
Normal file
14994
custom_ui/fixtures/property_setter.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -56,25 +56,6 @@
|
||||
"script": "\r\n\r\n\r\n# Helper function to calculate the legal start date (2 business days after the locate date)\r\ndef calculate_legal_start_date(locate_date):\r\n # Add 2 business days to the locate date (exclude weekends)\r\n current_date = getdate(locate_date)\r\n while True:\r\n current_date = add_days(current_date, 1)\r\n if current_date.weekday() < 5: # Weekdays are 0-4 (Monday to Friday)\r\n if current_date.weekday() == 4: # If it's Friday, skip to Monday\r\n current_date = add_days(current_date, 2)\r\n break\r\n return current_date\r\n\r\n# Helper function to calculate the expiration date based on the state\r\ndef calculate_expiration_date(state, locate_date):\r\n expiration_days = 45 if state == 'Washington' else 28\r\n expiration_date = add_days(locate_date, expiration_days)\r\n return expiration_date\r\n \r\n \r\n\r\n # Calculate the legal start date and expiration date\r\n legal_start_date = calculate_legal_start_date(locate_date)\r\n expiration_date = calculate_expiration_date(state, locate_date)\r\n\r\n ",
|
||||
"script_type": "DocType Event"
|
||||
},
|
||||
{
|
||||
"allow_guest": 0,
|
||||
"api_method": null,
|
||||
"cron_format": null,
|
||||
"disabled": 1,
|
||||
"docstatus": 0,
|
||||
"doctype": "Server Script",
|
||||
"doctype_event": "Before Save",
|
||||
"enable_rate_limit": 0,
|
||||
"event_frequency": "All",
|
||||
"modified": "2024-12-18 11:41:19.792616",
|
||||
"module": "ERPNext Integrations",
|
||||
"name": "Automate Job queue creation",
|
||||
"rate_limit_count": 5,
|
||||
"rate_limit_seconds": 86400,
|
||||
"reference_doctype": "Sales Order",
|
||||
"script": "# Trigger: Before Save\r\nif doc.company == \"Lowe Fencing\":\r\n # Check if a Fencing Job Queue entry already exists for this Sales Order\r\n existing_job = frappe.db.exists(\"Fencing Job Queue\", {\"sales_order\": doc.name})\r\n \r\n if not existing_job:\r\n # Create a new Fencing Job Queue entry\r\n new_job = frappe.get_doc({\r\n \"doctype\": \"Fencing Job Queue\",\r\n \"sales_order\": doc.name,\r\n \"customer\": doc.customer,\r\n \"job_description\": doc.get(\"job_description\", \"No description provided\"), # Modify if necessary\r\n \"status\": \"Pending\"\r\n })\r\n new_job.insert()\r\n frappe.msgprint(f\"Fencing Job Queue entry created for Sales Order {doc.name}.\")\r\n",
|
||||
"script_type": "DocType Event"
|
||||
},
|
||||
{
|
||||
"allow_guest": 0,
|
||||
"api_method": null,
|
||||
|
||||
@ -1,49 +1,4 @@
|
||||
[
|
||||
{
|
||||
"act_end_date": null,
|
||||
"act_start_date": null,
|
||||
"actual_time": 0.0,
|
||||
"closing_date": null,
|
||||
"color": null,
|
||||
"company": "Sprinklers Northwest",
|
||||
"completed_by": null,
|
||||
"completed_on": null,
|
||||
"custom_foreman": "HR-EMP-00014",
|
||||
"custom_property": null,
|
||||
"customer": null,
|
||||
"department": null,
|
||||
"depends_on": [],
|
||||
"depends_on_tasks": "",
|
||||
"description": null,
|
||||
"docstatus": 0,
|
||||
"doctype": "Task",
|
||||
"duration": 0,
|
||||
"exp_end_date": null,
|
||||
"exp_start_date": null,
|
||||
"expected_time": 0.0,
|
||||
"is_group": 0,
|
||||
"is_milestone": 0,
|
||||
"is_template": 1,
|
||||
"issue": null,
|
||||
"modified": "2025-05-13 06:34:23.580282",
|
||||
"name": "TASK-2025-00007",
|
||||
"old_parent": "",
|
||||
"parent_task": null,
|
||||
"priority": "Low",
|
||||
"progress": 0.0,
|
||||
"project": null,
|
||||
"project_template": null,
|
||||
"review_date": null,
|
||||
"start": 0,
|
||||
"status": "Template",
|
||||
"subject": "15-Day QA",
|
||||
"task_weight": 0.0,
|
||||
"template_task": null,
|
||||
"total_billing_amount": 0.0,
|
||||
"total_costing_amount": 0.0,
|
||||
"total_expense_claim": 0.0,
|
||||
"type": "QA"
|
||||
},
|
||||
{
|
||||
"act_end_date": null,
|
||||
"act_start_date": null,
|
||||
@ -104,7 +59,7 @@
|
||||
"department": null,
|
||||
"depends_on": [],
|
||||
"depends_on_tasks": "",
|
||||
"description": null,
|
||||
"description": "Utility locate request prior to installation start.",
|
||||
"docstatus": 0,
|
||||
"doctype": "Task",
|
||||
"duration": 0,
|
||||
@ -115,7 +70,7 @@
|
||||
"is_milestone": 0,
|
||||
"is_template": 1,
|
||||
"issue": null,
|
||||
"modified": "2025-04-24 14:57:03.402721",
|
||||
"modified": "2026-02-12 12:31:42.805351",
|
||||
"name": "TASK-2025-00002",
|
||||
"old_parent": "",
|
||||
"parent_task": null,
|
||||
@ -127,12 +82,12 @@
|
||||
"start": 0,
|
||||
"status": "Template",
|
||||
"subject": "811/Locate call in",
|
||||
"task_weight": 0.0,
|
||||
"task_weight": 7.0,
|
||||
"template_task": null,
|
||||
"total_billing_amount": 0.0,
|
||||
"total_costing_amount": 0.0,
|
||||
"total_expense_claim": 0.0,
|
||||
"type": "Admin"
|
||||
"type": "811/Locate"
|
||||
},
|
||||
{
|
||||
"act_end_date": null,
|
||||
@ -323,7 +278,7 @@
|
||||
"company": "Sprinklers Northwest",
|
||||
"completed_by": null,
|
||||
"completed_on": null,
|
||||
"custom_foreman": null,
|
||||
"custom_foreman": "HR-EMP-00014",
|
||||
"custom_property": null,
|
||||
"customer": null,
|
||||
"department": null,
|
||||
@ -340,7 +295,52 @@
|
||||
"is_milestone": 0,
|
||||
"is_template": 1,
|
||||
"issue": null,
|
||||
"modified": "2025-05-10 05:06:24.653035",
|
||||
"modified": "2026-01-23 02:29:45.172285",
|
||||
"name": "TASK-2025-00007",
|
||||
"old_parent": "",
|
||||
"parent_task": null,
|
||||
"priority": "Low",
|
||||
"progress": 0.0,
|
||||
"project": null,
|
||||
"project_template": null,
|
||||
"review_date": null,
|
||||
"start": 0,
|
||||
"status": "Template",
|
||||
"subject": "15-Day QA",
|
||||
"task_weight": 0.0,
|
||||
"template_task": null,
|
||||
"total_billing_amount": 0.0,
|
||||
"total_costing_amount": 0.0,
|
||||
"total_expense_claim": 0.0,
|
||||
"type": "15 Day Warranty Follow-Up"
|
||||
},
|
||||
{
|
||||
"act_end_date": null,
|
||||
"act_start_date": null,
|
||||
"actual_time": 0.0,
|
||||
"closing_date": null,
|
||||
"color": null,
|
||||
"company": "Sprinklers Northwest",
|
||||
"completed_by": null,
|
||||
"completed_on": null,
|
||||
"custom_foreman": null,
|
||||
"custom_property": null,
|
||||
"customer": null,
|
||||
"department": null,
|
||||
"depends_on": [],
|
||||
"depends_on_tasks": "",
|
||||
"description": "Task tracking for the main service job.",
|
||||
"docstatus": 0,
|
||||
"doctype": "Task",
|
||||
"duration": 0,
|
||||
"exp_end_date": null,
|
||||
"exp_start_date": null,
|
||||
"expected_time": 0.0,
|
||||
"is_group": 0,
|
||||
"is_milestone": 0,
|
||||
"is_template": 1,
|
||||
"issue": null,
|
||||
"modified": "2026-02-12 12:32:42.996899",
|
||||
"name": "TASK-2025-00004",
|
||||
"old_parent": "",
|
||||
"parent_task": null,
|
||||
@ -352,11 +352,11 @@
|
||||
"start": 0,
|
||||
"status": "Template",
|
||||
"subject": "Primary Job",
|
||||
"task_weight": 0.0,
|
||||
"task_weight": 25.0,
|
||||
"template_task": null,
|
||||
"total_billing_amount": 0.0,
|
||||
"total_costing_amount": 0.0,
|
||||
"total_expense_claim": 0.0,
|
||||
"type": "Labor"
|
||||
"type": "Main Job"
|
||||
}
|
||||
]
|
||||
527
custom_ui/fixtures/task_type.json
Normal file
527
custom_ui/fixtures/task_type.json
Normal file
@ -0,0 +1,527 @@
|
||||
[
|
||||
{
|
||||
"base_date": "Start",
|
||||
"calculate_from": "Project",
|
||||
"custom_completion_trigger": null,
|
||||
"custom_completion_trigger_doctype": null,
|
||||
"custom_target_percent": 0,
|
||||
"days": 0,
|
||||
"description": "Collect half down payment on project creation.",
|
||||
"docstatus": 0,
|
||||
"doctype": "Task Type",
|
||||
"logic_key": null,
|
||||
"modified": "2026-01-26 12:59:50.869932",
|
||||
"name": "1/2 Down Payment",
|
||||
"no_due_date": 0,
|
||||
"offset_days": 0,
|
||||
"offset_direction": "Before",
|
||||
"skip_holidays": 1,
|
||||
"skip_weekends": 0,
|
||||
"task_type_calculate_from": null,
|
||||
"title": "1/2 Down Payment",
|
||||
"trigger": "Created",
|
||||
"triggering_doctype": "Project",
|
||||
"weight": 0.0,
|
||||
"work_type": "Admin"
|
||||
},
|
||||
{
|
||||
"base_date": "Start",
|
||||
"calculate_from": "Service Address 2",
|
||||
"custom_completion_trigger": null,
|
||||
"custom_completion_trigger_doctype": null,
|
||||
"custom_target_percent": 0,
|
||||
"days": 0,
|
||||
"description": "Stage machinery one day before installation start.",
|
||||
"docstatus": 0,
|
||||
"doctype": "Task Type",
|
||||
"logic_key": null,
|
||||
"modified": "2026-01-26 13:00:47.389623",
|
||||
"name": "Machine Staging",
|
||||
"no_due_date": 0,
|
||||
"offset_days": 1,
|
||||
"offset_direction": "Before",
|
||||
"skip_holidays": 1,
|
||||
"skip_weekends": 0,
|
||||
"task_type_calculate_from": null,
|
||||
"title": "Machine Staging",
|
||||
"trigger": "Scheduled",
|
||||
"triggering_doctype": "Service Address 2",
|
||||
"weight": 0.0,
|
||||
"work_type": "Admin"
|
||||
},
|
||||
{
|
||||
"base_date": "Completion",
|
||||
"calculate_from": "Service Address 2",
|
||||
"custom_completion_trigger": null,
|
||||
"custom_completion_trigger_doctype": null,
|
||||
"custom_target_percent": 0,
|
||||
"days": 0,
|
||||
"description": "Send final invoice within 5 days of job completion.",
|
||||
"docstatus": 0,
|
||||
"doctype": "Task Type",
|
||||
"logic_key": null,
|
||||
"modified": "2026-01-26 13:01:14.573788",
|
||||
"name": "Final Invoice",
|
||||
"no_due_date": 0,
|
||||
"offset_days": 5,
|
||||
"offset_direction": "After",
|
||||
"skip_holidays": 1,
|
||||
"skip_weekends": 0,
|
||||
"task_type_calculate_from": null,
|
||||
"title": "Final Invoice",
|
||||
"trigger": "Completed",
|
||||
"triggering_doctype": "Service Address 2",
|
||||
"weight": 0.0,
|
||||
"work_type": "Admin"
|
||||
},
|
||||
{
|
||||
"base_date": "Completion",
|
||||
"calculate_from": "Service Address 2",
|
||||
"custom_completion_trigger": null,
|
||||
"custom_completion_trigger_doctype": null,
|
||||
"custom_target_percent": 0,
|
||||
"days": 0,
|
||||
"description": "Backflow test after job completion if quoted.",
|
||||
"docstatus": 0,
|
||||
"doctype": "Task Type",
|
||||
"logic_key": null,
|
||||
"modified": "2026-01-26 13:01:34.781732",
|
||||
"name": "Backflow Test",
|
||||
"no_due_date": 0,
|
||||
"offset_days": 0,
|
||||
"offset_direction": "After",
|
||||
"skip_holidays": 1,
|
||||
"skip_weekends": 0,
|
||||
"task_type_calculate_from": null,
|
||||
"title": "Backflow Test",
|
||||
"trigger": "Completed",
|
||||
"triggering_doctype": "Service Address 2",
|
||||
"weight": 0.0,
|
||||
"work_type": "Admin"
|
||||
},
|
||||
{
|
||||
"base_date": "Completion",
|
||||
"calculate_from": "Service Address 2",
|
||||
"custom_completion_trigger": null,
|
||||
"custom_completion_trigger_doctype": null,
|
||||
"custom_target_percent": 0,
|
||||
"days": 0,
|
||||
"description": "Schedule permit inspection 5 days after completion.",
|
||||
"docstatus": 0,
|
||||
"doctype": "Task Type",
|
||||
"logic_key": null,
|
||||
"modified": "2026-01-26 13:01:44.550215",
|
||||
"name": "Schedule Permit Inspection",
|
||||
"no_due_date": 0,
|
||||
"offset_days": 5,
|
||||
"offset_direction": "After",
|
||||
"skip_holidays": 1,
|
||||
"skip_weekends": 0,
|
||||
"task_type_calculate_from": null,
|
||||
"title": "Schedule Permit Inspection",
|
||||
"trigger": "Completed",
|
||||
"triggering_doctype": "Service Address 2",
|
||||
"weight": 0.0,
|
||||
"work_type": "Admin"
|
||||
},
|
||||
{
|
||||
"base_date": "Completion",
|
||||
"calculate_from": "Service Address 2",
|
||||
"custom_completion_trigger": null,
|
||||
"custom_completion_trigger_doctype": null,
|
||||
"custom_target_percent": 0,
|
||||
"days": 0,
|
||||
"description": "30-day warranty check after completion.",
|
||||
"docstatus": 0,
|
||||
"doctype": "Task Type",
|
||||
"logic_key": null,
|
||||
"modified": "2026-01-26 13:02:08.165742",
|
||||
"name": "30 Day Warranty Check",
|
||||
"no_due_date": 0,
|
||||
"offset_days": 30,
|
||||
"offset_direction": "After",
|
||||
"skip_holidays": 1,
|
||||
"skip_weekends": 0,
|
||||
"task_type_calculate_from": null,
|
||||
"title": "30 Day Warranty Check",
|
||||
"trigger": "Completed",
|
||||
"triggering_doctype": "Service Address 2",
|
||||
"weight": 0.0,
|
||||
"work_type": "Admin"
|
||||
},
|
||||
{
|
||||
"base_date": "Completion",
|
||||
"calculate_from": "Service Address 2",
|
||||
"custom_completion_trigger": null,
|
||||
"custom_completion_trigger_doctype": null,
|
||||
"custom_target_percent": 0,
|
||||
"days": 0,
|
||||
"description": "Payment reminder sent 30 days after completion.",
|
||||
"docstatus": 0,
|
||||
"doctype": "Task Type",
|
||||
"logic_key": null,
|
||||
"modified": "2026-01-26 13:02:21.758218",
|
||||
"name": "30 Day Payment Reminder",
|
||||
"no_due_date": 0,
|
||||
"offset_days": 30,
|
||||
"offset_direction": "After",
|
||||
"skip_holidays": 1,
|
||||
"skip_weekends": 0,
|
||||
"task_type_calculate_from": null,
|
||||
"title": "30 Day Payment Reminder",
|
||||
"trigger": "Completed",
|
||||
"triggering_doctype": "Service Address 2",
|
||||
"weight": 0.0,
|
||||
"work_type": "Admin"
|
||||
},
|
||||
{
|
||||
"base_date": "Completion",
|
||||
"calculate_from": "Service Address 2",
|
||||
"custom_completion_trigger": null,
|
||||
"custom_completion_trigger_doctype": null,
|
||||
"custom_target_percent": 0,
|
||||
"days": 0,
|
||||
"description": "Late payment notification at 60 days post completion.",
|
||||
"docstatus": 0,
|
||||
"doctype": "Task Type",
|
||||
"logic_key": null,
|
||||
"modified": "2026-01-26 13:02:33.693892",
|
||||
"name": "60 Day Late Payment Notice",
|
||||
"no_due_date": 0,
|
||||
"offset_days": 60,
|
||||
"offset_direction": "After",
|
||||
"skip_holidays": 1,
|
||||
"skip_weekends": 0,
|
||||
"task_type_calculate_from": null,
|
||||
"title": "60 Day Late Payment Notice",
|
||||
"trigger": "Completed",
|
||||
"triggering_doctype": "Service Address 2",
|
||||
"weight": 0.0,
|
||||
"work_type": "Admin"
|
||||
},
|
||||
{
|
||||
"base_date": "Completion",
|
||||
"calculate_from": "Service Address 2",
|
||||
"custom_completion_trigger": null,
|
||||
"custom_completion_trigger_doctype": null,
|
||||
"custom_target_percent": 0,
|
||||
"days": 0,
|
||||
"description": "Lien notice if payment is still late after 80 days.",
|
||||
"docstatus": 0,
|
||||
"doctype": "Task Type",
|
||||
"logic_key": null,
|
||||
"modified": "2026-01-26 13:02:42.742371",
|
||||
"name": "80 Day Lien Notice",
|
||||
"no_due_date": 0,
|
||||
"offset_days": 80,
|
||||
"offset_direction": "After",
|
||||
"skip_holidays": 1,
|
||||
"skip_weekends": 0,
|
||||
"task_type_calculate_from": null,
|
||||
"title": "80 Day Lien Notice",
|
||||
"trigger": "Completed",
|
||||
"triggering_doctype": "Service Address 2",
|
||||
"weight": 0.0,
|
||||
"work_type": "Admin"
|
||||
},
|
||||
{
|
||||
"base_date": "Completion",
|
||||
"calculate_from": "Service Address 2",
|
||||
"custom_completion_trigger": null,
|
||||
"custom_completion_trigger_doctype": null,
|
||||
"custom_target_percent": 0,
|
||||
"days": 0,
|
||||
"description": "One-year warranty call or walk-through.",
|
||||
"docstatus": 0,
|
||||
"doctype": "Task Type",
|
||||
"logic_key": null,
|
||||
"modified": "2026-01-26 13:02:51.999530",
|
||||
"name": "365 Day Warranty Call / Walk",
|
||||
"no_due_date": 0,
|
||||
"offset_days": 365,
|
||||
"offset_direction": "After",
|
||||
"skip_holidays": 1,
|
||||
"skip_weekends": 0,
|
||||
"task_type_calculate_from": null,
|
||||
"title": "365 Day Warranty Call / Walk",
|
||||
"trigger": "Completed",
|
||||
"triggering_doctype": "Service Address 2",
|
||||
"weight": 0.0,
|
||||
"work_type": "Admin"
|
||||
},
|
||||
{
|
||||
"base_date": "Start",
|
||||
"calculate_from": "Service Address 2",
|
||||
"custom_completion_trigger": null,
|
||||
"custom_completion_trigger_doctype": null,
|
||||
"custom_target_percent": 0,
|
||||
"days": 0,
|
||||
"description": "Locate utilities for digging",
|
||||
"docstatus": 0,
|
||||
"doctype": "Task Type",
|
||||
"logic_key": null,
|
||||
"modified": "2026-01-26 13:00:26.110307",
|
||||
"name": "Locate",
|
||||
"no_due_date": 0,
|
||||
"offset_days": 7,
|
||||
"offset_direction": "Before",
|
||||
"skip_holidays": 1,
|
||||
"skip_weekends": 0,
|
||||
"task_type_calculate_from": null,
|
||||
"title": "Locate",
|
||||
"trigger": "Scheduled",
|
||||
"triggering_doctype": "Service Address 2",
|
||||
"weight": 0.0,
|
||||
"work_type": "Admin"
|
||||
},
|
||||
{
|
||||
"base_date": "Start",
|
||||
"calculate_from": "Service Address 2",
|
||||
"custom_completion_trigger": null,
|
||||
"custom_completion_trigger_doctype": null,
|
||||
"custom_target_percent": 0,
|
||||
"days": 0,
|
||||
"description": "Utility locate request prior to installation start.",
|
||||
"docstatus": 0,
|
||||
"doctype": "Task Type",
|
||||
"logic_key": null,
|
||||
"modified": "2026-02-08 01:46:40.122526",
|
||||
"name": "811/Locate",
|
||||
"no_due_date": 0,
|
||||
"offset_days": 7,
|
||||
"offset_direction": "Before",
|
||||
"skip_holidays": 1,
|
||||
"skip_weekends": 0,
|
||||
"task_type_calculate_from": null,
|
||||
"title": "811/Locate",
|
||||
"trigger": "Scheduled",
|
||||
"triggering_doctype": "Service Address 2",
|
||||
"weight": 7.0,
|
||||
"work_type": "Admin"
|
||||
},
|
||||
{
|
||||
"base_date": "Project Start",
|
||||
"calculate_from": "Service Appointment",
|
||||
"custom_completion_trigger": null,
|
||||
"custom_completion_trigger_doctype": null,
|
||||
"custom_target_percent": 0,
|
||||
"days": 0,
|
||||
"description": null,
|
||||
"docstatus": 0,
|
||||
"doctype": "Task Type",
|
||||
"logic_key": null,
|
||||
"modified": "2024-12-20 16:48:23.131743",
|
||||
"name": "Topshot",
|
||||
"no_due_date": 0,
|
||||
"offset_days": 7,
|
||||
"offset_direction": "Before",
|
||||
"skip_holidays": 1,
|
||||
"skip_weekends": 1,
|
||||
"task_type_calculate_from": null,
|
||||
"title": null,
|
||||
"trigger": null,
|
||||
"triggering_doctype": "Service Address 2",
|
||||
"weight": 0.0,
|
||||
"work_type": "Admin"
|
||||
},
|
||||
{
|
||||
"base_date": "Completion",
|
||||
"calculate_from": "Service Address 2",
|
||||
"custom_completion_trigger": null,
|
||||
"custom_completion_trigger_doctype": null,
|
||||
"custom_target_percent": 0,
|
||||
"days": 0,
|
||||
"description": "15-day warranty follow-up after completion.",
|
||||
"docstatus": 0,
|
||||
"doctype": "Task Type",
|
||||
"logic_key": null,
|
||||
"modified": "2026-01-26 13:01:56.949793",
|
||||
"name": "15 Day Warranty Follow-Up",
|
||||
"no_due_date": 0,
|
||||
"offset_days": 15,
|
||||
"offset_direction": "After",
|
||||
"skip_holidays": 1,
|
||||
"skip_weekends": 0,
|
||||
"task_type_calculate_from": null,
|
||||
"title": "15 Day Warranty Follow-Up",
|
||||
"trigger": "Completed",
|
||||
"triggering_doctype": "Service Address 2",
|
||||
"weight": 0.0,
|
||||
"work_type": "Admin"
|
||||
},
|
||||
{
|
||||
"base_date": "Project Start",
|
||||
"calculate_from": "Service Appointment",
|
||||
"custom_completion_trigger": null,
|
||||
"custom_completion_trigger_doctype": null,
|
||||
"custom_target_percent": 0,
|
||||
"days": 0,
|
||||
"description": null,
|
||||
"docstatus": 0,
|
||||
"doctype": "Task Type",
|
||||
"logic_key": null,
|
||||
"modified": "2024-12-17 14:18:26.429960",
|
||||
"name": "Subcontractor",
|
||||
"no_due_date": 0,
|
||||
"offset_days": 7,
|
||||
"offset_direction": "Before",
|
||||
"skip_holidays": 1,
|
||||
"skip_weekends": 1,
|
||||
"task_type_calculate_from": null,
|
||||
"title": null,
|
||||
"trigger": null,
|
||||
"triggering_doctype": "Service Address 2",
|
||||
"weight": 0.0,
|
||||
"work_type": "Admin"
|
||||
},
|
||||
{
|
||||
"base_date": "End",
|
||||
"calculate_from": "Service Address 2",
|
||||
"custom_completion_trigger": "Completed",
|
||||
"custom_completion_trigger_doctype": "Service Address 2",
|
||||
"custom_target_percent": 0,
|
||||
"days": 0,
|
||||
"description": "Task tracking for the main service job.",
|
||||
"docstatus": 0,
|
||||
"doctype": "Task Type",
|
||||
"logic_key": null,
|
||||
"modified": "2026-02-08 02:35:42.343305",
|
||||
"name": "Main Job",
|
||||
"no_due_date": 0,
|
||||
"offset_days": 0,
|
||||
"offset_direction": "After",
|
||||
"skip_holidays": 1,
|
||||
"skip_weekends": 0,
|
||||
"task_type_calculate_from": null,
|
||||
"title": "Main Job",
|
||||
"trigger": "Scheduled",
|
||||
"triggering_doctype": "Service Address 2",
|
||||
"weight": 25.0,
|
||||
"work_type": "Labor"
|
||||
},
|
||||
{
|
||||
"base_date": "Start",
|
||||
"calculate_from": "Service Address 2",
|
||||
"custom_completion_trigger": null,
|
||||
"custom_completion_trigger_doctype": null,
|
||||
"custom_target_percent": 0,
|
||||
"days": 0,
|
||||
"description": "Permits required prior to installation start.",
|
||||
"docstatus": 0,
|
||||
"doctype": "Task Type",
|
||||
"logic_key": null,
|
||||
"modified": "2026-02-08 01:48:15.012387",
|
||||
"name": "Permit",
|
||||
"no_due_date": 0,
|
||||
"offset_days": 7,
|
||||
"offset_direction": "Before",
|
||||
"skip_holidays": 1,
|
||||
"skip_weekends": 0,
|
||||
"task_type_calculate_from": null,
|
||||
"title": "Permit",
|
||||
"trigger": "Scheduled",
|
||||
"triggering_doctype": "Service Address 2",
|
||||
"weight": 7.0,
|
||||
"work_type": "Admin"
|
||||
},
|
||||
{
|
||||
"base_date": "Project Start",
|
||||
"calculate_from": "Service Appointment",
|
||||
"custom_completion_trigger": null,
|
||||
"custom_completion_trigger_doctype": null,
|
||||
"custom_target_percent": 0,
|
||||
"days": 0,
|
||||
"description": null,
|
||||
"docstatus": 0,
|
||||
"doctype": "Task Type",
|
||||
"logic_key": null,
|
||||
"modified": "2025-05-10 03:44:23.619864",
|
||||
"name": "QA",
|
||||
"no_due_date": 0,
|
||||
"offset_days": 7,
|
||||
"offset_direction": "Before",
|
||||
"skip_holidays": 1,
|
||||
"skip_weekends": 1,
|
||||
"task_type_calculate_from": null,
|
||||
"title": null,
|
||||
"trigger": null,
|
||||
"triggering_doctype": "Service Address 2",
|
||||
"weight": 0.0,
|
||||
"work_type": "Admin"
|
||||
},
|
||||
{
|
||||
"base_date": "Project Start",
|
||||
"calculate_from": "Service Appointment",
|
||||
"custom_completion_trigger": null,
|
||||
"custom_completion_trigger_doctype": null,
|
||||
"custom_target_percent": 0,
|
||||
"days": 0,
|
||||
"description": null,
|
||||
"docstatus": 0,
|
||||
"doctype": "Task Type",
|
||||
"logic_key": null,
|
||||
"modified": "2025-04-24 14:56:51.838933",
|
||||
"name": "Admin",
|
||||
"no_due_date": 0,
|
||||
"offset_days": 7,
|
||||
"offset_direction": "Before",
|
||||
"skip_holidays": 1,
|
||||
"skip_weekends": 1,
|
||||
"task_type_calculate_from": null,
|
||||
"title": null,
|
||||
"trigger": null,
|
||||
"triggering_doctype": "Service Address 2",
|
||||
"weight": 0.0,
|
||||
"work_type": "Admin"
|
||||
},
|
||||
{
|
||||
"base_date": "Project Start",
|
||||
"calculate_from": "Service Appointment",
|
||||
"custom_completion_trigger": null,
|
||||
"custom_completion_trigger_doctype": null,
|
||||
"custom_target_percent": 0,
|
||||
"days": 0,
|
||||
"description": null,
|
||||
"docstatus": 0,
|
||||
"doctype": "Task Type",
|
||||
"logic_key": null,
|
||||
"modified": "2024-04-16 11:55:50.883266",
|
||||
"name": "Scheduling",
|
||||
"no_due_date": 0,
|
||||
"offset_days": 7,
|
||||
"offset_direction": "Before",
|
||||
"skip_holidays": 1,
|
||||
"skip_weekends": 1,
|
||||
"task_type_calculate_from": null,
|
||||
"title": null,
|
||||
"trigger": null,
|
||||
"triggering_doctype": "Service Address 2",
|
||||
"weight": 0.0,
|
||||
"work_type": "Admin"
|
||||
},
|
||||
{
|
||||
"base_date": "Project Start",
|
||||
"calculate_from": "Service Appointment",
|
||||
"custom_completion_trigger": null,
|
||||
"custom_completion_trigger_doctype": null,
|
||||
"custom_target_percent": 0,
|
||||
"days": 0,
|
||||
"description": null,
|
||||
"docstatus": 0,
|
||||
"doctype": "Task Type",
|
||||
"logic_key": null,
|
||||
"modified": "2024-04-19 12:51:25.954969",
|
||||
"name": "Labor",
|
||||
"no_due_date": 0,
|
||||
"offset_days": 7,
|
||||
"offset_direction": "Before",
|
||||
"skip_holidays": 1,
|
||||
"skip_weekends": 1,
|
||||
"task_type_calculate_from": null,
|
||||
"title": null,
|
||||
"trigger": null,
|
||||
"triggering_doctype": "Service Address 2",
|
||||
"weight": 0.0,
|
||||
"work_type": "Admin"
|
||||
}
|
||||
]
|
||||
@ -9,7 +9,8 @@ app_license = "mit"
|
||||
|
||||
after_install = "custom_ui.install.after_install"
|
||||
after_migrate = "custom_ui.install.after_migrate"
|
||||
after_uninstall = "custom_ui.install.after_uninstall"
|
||||
after_build = "custom_ui.commands.build_frontend"
|
||||
# after_uninstall = "custom_ui.install.after_uninstall"
|
||||
# on_session_creation = "custom_ui.utils.on_login_redirect"
|
||||
# on_login = "custom_ui.utils.on_login_redirect"
|
||||
page_js = {
|
||||
@ -196,7 +197,8 @@ doc_events = {
|
||||
"Task": {
|
||||
"before_insert": "custom_ui.events.task.before_insert",
|
||||
"after_insert": "custom_ui.events.task.after_insert",
|
||||
"before_save": "custom_ui.events.task.before_save"
|
||||
"before_save": "custom_ui.events.task.before_save",
|
||||
"on_update": "custom_ui.events.task.after_save"
|
||||
},
|
||||
"Bid Meeting Note Form": {
|
||||
"after_insert": "custom_ui.events.general.attach_bid_note_form_to_project_template"
|
||||
@ -210,28 +212,52 @@ doc_events = {
|
||||
"on_submit": "custom_ui.events.payments.on_submit"
|
||||
},
|
||||
"Sales Invoice": {
|
||||
"on_submit": "custom_ui.events.sales_invoice.on_submit"
|
||||
"on_submit": "custom_ui.events.sales_invoice.on_submit",
|
||||
"after_insert": "custom_ui.events.sales_invoice.after_insert"
|
||||
}
|
||||
}
|
||||
|
||||
# custom_ui/hooks.py (or a separate utils.py in the app)
|
||||
def remove_fencing_job_queue_links(doc):
|
||||
"""Remove links to the deleted 'Fencing Job Queue' doctype"""
|
||||
links = doc.get("links", [])
|
||||
doc["links"] = [l for l in links if l.get("link_doctype") != "Fencing Job Queue"]
|
||||
|
||||
|
||||
fixtures = [
|
||||
{
|
||||
"dt": "Custom Field",
|
||||
"filters": [["module", "in", ["Custom UI"]]]
|
||||
},
|
||||
{
|
||||
"dt": "Email Template",
|
||||
"filters": [
|
||||
["name", "in", ["Customer Invoice"]]
|
||||
]
|
||||
"filters": [["module", "in", ["Custom UI"]], ["dt", "not in", ["Service Appointment"]]]
|
||||
},
|
||||
# {
|
||||
# "dt": "Email Template",
|
||||
# "filters": [
|
||||
# ["name", "in", ["Customer Invoice"]]
|
||||
# ]
|
||||
# },
|
||||
# {
|
||||
# "dt": "DocType",
|
||||
# "filters": [
|
||||
# ["custom", "=", 1],
|
||||
# ["name", "!=", "Fencing Job Queue"],
|
||||
# ["name", "!=", "Locate Log"], # <-- skip the deleted/removed doctype
|
||||
# ]
|
||||
# },
|
||||
{ "dt": "Task Type" },
|
||||
{
|
||||
"dt": "Task",
|
||||
"filters": [["status", "=", "Template"]]
|
||||
},
|
||||
{
|
||||
"dt": "Project Template"
|
||||
"filters": [
|
||||
["is_template", "=", 1]
|
||||
]
|
||||
},
|
||||
{ "dt": "Project Template" },
|
||||
{ "dt": "Bid Meeting Note Form" },
|
||||
# { "dt": "Holiday List" },
|
||||
|
||||
# These don't have reliable flags → export all
|
||||
{"dt": "Custom Field"},
|
||||
{"dt": "Property Setter",
|
||||
"filters": [["name", "not in", ["Service Appointment-service_address", "Service Appointment-customer", "Service Appointment-project", "Service Appointment-project_template", "Service Appointment"]],]},
|
||||
{"dt": "Client Script"},
|
||||
{"dt": "Server Script"},
|
||||
]
|
||||
|
||||
1036
custom_ui/install.py
1036
custom_ui/install.py
File diff suppressed because it is too large
Load Diff
20
custom_ui/services/db_restore_service.py
Normal file
20
custom_ui/services/db_restore_service.py
Normal file
@ -0,0 +1,20 @@
|
||||
import frappe
|
||||
|
||||
class DBRestoreService:
|
||||
@staticmethod
|
||||
def massage_customer_address_contact_links():
|
||||
"""Fixes the links between Customer, Address, and Contacts from legacy data that may have been imported without proper linking."""
|
||||
# use emojis in print statments to make it more fun and visually distinct in the logs
|
||||
print("DEBUG: 🛠️ Starting to massage customer, address, and contact links")
|
||||
all_addresses = frappe.get_all("Address", pluck="name")
|
||||
print(f"DEBUG: Found {len(all_addresses)} addresses to process")
|
||||
all_customers = frappe.get_all("Customer", pluck="name")
|
||||
print(f"DEBUG: Found {len(all_customers)} customers to process")
|
||||
all_leads = frappe.get_all("Lead", pluck="name")
|
||||
print(f"DEBUG: Found {len(all_leads)} leads to process")
|
||||
all_contacts = frappe.get_all("Contact", pluck="name")
|
||||
print(f"DEBUG: Found {len(all_contacts)} contacts to process")
|
||||
|
||||
# query all customer doctypes that don't have an empty array for custom_select_address. This field is a child table so get_all cannot be used. We need to use a custom sql query
|
||||
# the child table is a doctype called "Custom "
|
||||
# print(f"DEBUG: Found {len(customers_with_addresses)} customers with addresses to process")
|
||||
@ -8,7 +8,8 @@ class SalesOrderService:
|
||||
def create_sales_invoice_from_sales_order(sales_order_name):
|
||||
try:
|
||||
sales_order_doc = frappe.get_doc("Sales Order", sales_order_name)
|
||||
sales_invoice = make_sales_invoice(sales_order_doc.name)
|
||||
sales_invoice = make_sales_invoice(sales_order_doc.name, ignore_permissions=True)
|
||||
print("DEBUG: Sales Invoice created from Sales Order:", sales_invoice.name)
|
||||
sales_invoice.project = sales_order_doc.project
|
||||
sales_invoice.posting_date = today()
|
||||
sales_invoice.due_date = today()
|
||||
@ -21,8 +22,9 @@ class SalesOrderService:
|
||||
sales_invoice.calculate_taxes_and_totals()
|
||||
|
||||
sales_invoice.insert()
|
||||
sales_invoice.submit()
|
||||
return sales_invoice.name
|
||||
print("DEBUG: Sales Invoice: ", sales_invoice.as_dict())
|
||||
# sales_invoice.submit()
|
||||
return sales_invoice
|
||||
except Exception as e:
|
||||
print("ERROR creating Sales Invoice from Sales Order:", str(e))
|
||||
return None
|
||||
@ -1,5 +1,6 @@
|
||||
import frappe
|
||||
from custom_ui.services import ContactService, AddressService, ClientService, DbService
|
||||
import json
|
||||
|
||||
class ServiceAppointmentService:
|
||||
|
||||
@ -23,13 +24,17 @@ class ServiceAppointmentService:
|
||||
service_appointment["service_address"] = AddressService.get_or_throw(service_appointment["service_address"]).as_dict()
|
||||
service_appointment["customer"] = ClientService.get_client_or_throw(service_appointment["customer"]).as_dict()
|
||||
service_appointment["project"] = DbService.get_or_throw("Project", service_appointment["project"]).as_dict()
|
||||
service_appointment["color"] = frappe.get_value("Project Template", service_appointment["project_template"], "calendar_color")
|
||||
|
||||
return service_appointment
|
||||
|
||||
@staticmethod
|
||||
def update_scheduled_dates(service_appointment_name: str, crew_lead_name: str,start_date, end_date, start_time=None, end_time=None):
|
||||
def update_scheduled_dates(service_appointment_name: str, crew_lead_name: str = None, start_date=None, end_date=None, start_time=None, end_time=None, skip_days=[]):
|
||||
"""Update the scheduled start and end dates of a Service Appointment."""
|
||||
print(f"DEBUG: Updating scheduled dates for Service Appointment {service_appointment_name} to start: {start_date}, end: {end_date}")
|
||||
if isinstance(skip_days, str):
|
||||
skip_days = json.loads(skip_days)
|
||||
print(f"DEBUG: Parsed skip_days: {skip_days}")
|
||||
service_appointment = DbService.get_or_throw("Service Address 2", service_appointment_name)
|
||||
service_appointment.expected_start_date = start_date
|
||||
service_appointment.expected_end_date = end_date
|
||||
@ -38,6 +43,22 @@ class ServiceAppointmentService:
|
||||
service_appointment.expected_start_time = start_time
|
||||
if end_time:
|
||||
service_appointment.expected_end_time = end_time
|
||||
if skip_days:
|
||||
print(f"DEBUG: Updating skip days for Service Appointment {service_appointment_name}. Current skip days: {[skip_day.date for skip_day in service_appointment.skip_days]}, New skip days: {skip_days}")
|
||||
# Compare skip_days with the current skip_days and remove/add as needed
|
||||
current_skip_days = [skip_day.date for skip_day in service_appointment.skip_days]
|
||||
# Remove skip days that are no longer needed
|
||||
for skip_day in current_skip_days:
|
||||
if skip_day not in skip_days:
|
||||
skip_day_doc = service_appointment.skip_days.find(lambda d: d.date == skip_day)
|
||||
if skip_day_doc:
|
||||
service_appointment.skip_days.remove(skip_day_doc.name)
|
||||
print(f"DEBUG: Removed skip day {skip_day} from Service Appointment {service_appointment_name}")
|
||||
# Add new skip days
|
||||
for skip_day in skip_days:
|
||||
if skip_day not in current_skip_days:
|
||||
service_appointment.append("skip_days", {"date": skip_day["date"]})
|
||||
print(f"DEBUG: Added new skip day {skip_day} for Service Appointment {service_appointment_name}")
|
||||
service_appointment.save()
|
||||
print(f"DEBUG: Updated scheduled dates for Service Appointment {service_appointment_name}")
|
||||
return service_appointment
|
||||
|
||||
@ -56,14 +56,17 @@ class StripeService:
|
||||
Returns:
|
||||
stripe.checkout.Session object
|
||||
"""
|
||||
print(f"DEBUG: Creating Stripe Checkout Session for company with details - Company: {company}, Amount: {amount}, Service: {service}, Order Num: {order_num}, Currency: {currency}, For Advance Payment: {for_advance_payment}, Sales Invoice: {sales_invoice}")
|
||||
stripe.api_key = StripeService.get_api_key(company)
|
||||
|
||||
# Determine payment description
|
||||
if for_advance_payment:
|
||||
description = f"Advance payment for {company}{' - ' + service if service else ''}"
|
||||
else:
|
||||
print("DEBUG: Creating description for full payment")
|
||||
description = f"Invoice payment for {company}{' - ' + service if service else ''}"
|
||||
if sales_invoice:
|
||||
print(f"DEBUG: Sales Invoice provided for full payment: {sales_invoice}")
|
||||
description = f"Invoice {sales_invoice} - {company}"
|
||||
|
||||
# Use custom line items if provided and not an advance payment, otherwise create default line item
|
||||
@ -88,7 +91,7 @@ class StripeService:
|
||||
if for_advance_payment:
|
||||
metadata["sales_order"] = order_num
|
||||
else:
|
||||
metadata["sales_invoice"] = sales_invoice or order_num
|
||||
metadata["sales_invoice"] = sales_invoice if sales_invoice else order_num
|
||||
if sales_invoice:
|
||||
# Check if there's a related sales order
|
||||
invoice_doc = frappe.get_doc("Sales Invoice", sales_invoice)
|
||||
|
||||
191
custom_ui/templates/invoices/already_paid.html
Normal file
191
custom_ui/templates/invoices/already_paid.html
Normal file
@ -0,0 +1,191 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Invoice Already Paid</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #333;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.message-container {
|
||||
text-align: center;
|
||||
background-color: #fff;
|
||||
padding: 50px;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
position: relative;
|
||||
}
|
||||
.message-icon {
|
||||
font-size: 5rem;
|
||||
color: #00b894;
|
||||
margin-bottom: 30px;
|
||||
animation: checkmarkAnimation 1.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes checkmarkAnimation {
|
||||
0% {
|
||||
transform: scale(0) rotate(-180deg);
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2) rotate(0deg);
|
||||
opacity: 1;
|
||||
}
|
||||
70% {
|
||||
transform: scale(0.9) rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1) rotate(0deg);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.message-title {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.message-text {
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 20px;
|
||||
color: #666;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.invoice-number {
|
||||
background-color: #e3f2fd;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
border-left: 4px solid #2196f3;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.contact-section {
|
||||
background-color: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
margin-top: 30px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.contact-section h3 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #333;
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.contact-section > p {
|
||||
margin: 0 0 15px 0;
|
||||
color: #666;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.contact-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.contact-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 5px 0;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.contact-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.contact-label {
|
||||
font-weight: 500;
|
||||
color: #495057;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.contact-value {
|
||||
font-weight: 400;
|
||||
color: #6c757d;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.contact-value a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.contact-value a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="message-container">
|
||||
<div class="message-icon">✓</div>
|
||||
<h1 class="message-title">Invoice Already Paid</h1>
|
||||
<p class="message-text">This invoice has already been paid in full.</p>
|
||||
|
||||
{% if invoice_number %}
|
||||
<div class="invoice-number">
|
||||
Invoice #{{ invoice_number }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if company %}
|
||||
<div class="contact-section">
|
||||
<h3>Have Questions?</h3>
|
||||
<p>We're here to help! Contact us if you need assistance.</p>
|
||||
<div class="contact-details">
|
||||
{% if company.company_name %}
|
||||
<div class="contact-row">
|
||||
<span class="contact-label">Company:</span>
|
||||
<span class="contact-value">{{ company.company_name }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if company.phone_no %}
|
||||
<div class="contact-row">
|
||||
<span class="contact-label">Phone:</span>
|
||||
<span class="contact-value">{{ company.phone_no }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if company.email %}
|
||||
<div class="contact-row">
|
||||
<span class="contact-label">Email:</span>
|
||||
<span class="contact-value"><a href="mailto:{{ company.email }}">{{ company.email }}</a></span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if company.website %}
|
||||
<div class="contact-row">
|
||||
<span class="contact-label">Website:</span>
|
||||
<span class="contact-value"><a href="{{ company.website }}" target="_blank">{{ company.website }}</a></span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -5,4 +5,5 @@ services:
|
||||
ports:
|
||||
- "8025:8025" # MailHog web UI
|
||||
- "1025:1025" # SMTP server
|
||||
restart: unless-stopped
|
||||
restart: unless-stopped
|
||||
|
||||
@ -46,6 +46,7 @@ const FRAPPE_GET_INVOICES_METHOD = "custom_ui.api.db.invoices.get_invoice_table_
|
||||
const FRAPPE_UPSERT_INVOICE_METHOD = "custom_ui.api.db.invoices.upsert_invoice";
|
||||
const FRAPPE_GET_INVOICES_LATE_METHOD = "custom_ui.api.db.invoices.get_invoices_late_count";
|
||||
const FRAPPE_CREATE_INVOICE_FOR_JOB = "custom_ui.api.db.invoices.create_invoice_for_job";
|
||||
const FRAPPE_SUBMIT_AND_SEND_INVOICE_METHOD = "custom_ui.api.db.invoices.submit_and_send_invoice";
|
||||
// Warranty methods
|
||||
const FRAPPE_GET_WARRANTY_CLAIMS_METHOD = "custom_ui.api.db.warranties.get_warranty_claims";
|
||||
// On-Site Meeting methods
|
||||
@ -55,7 +56,10 @@ const FRAPPE_GET_BID_MEETING_NOTE_FORM_METHOD = "custom_ui.api.db.bid_meetings.g
|
||||
const FRAPPE_GET_ONSITE_MEETINGS_METHOD = "custom_ui.api.db.bid_meetings.get_bid_meetings";
|
||||
const FRAPPE_SUBMIT_BID_MEETING_NOTE_FORM_METHOD = "custom_ui.api.db.bid_meetings.submit_bid_meeting_note_form";
|
||||
// Address methods
|
||||
const FRAPPE_CHECK_ADDRESSES_EXIST_METHOD = "custom_ui.api.db.addresses.check_addresses_exist";
|
||||
const FRAPPE_GET_ADDRESSES_METHOD = "custom_ui.api.db.addresses.get_addresses";
|
||||
// Contact methods
|
||||
const FRAPPE_CHECK_CONTACTS_EXIST_METHOD = "custom_ui.api.db.contacts.check_contacts_exist";
|
||||
// Client methods
|
||||
const FRAPPE_UPSERT_CLIENT_METHOD = "custom_ui.api.db.clients.upsert_client";
|
||||
const FRAPPE_GET_CLIENT_STATUS_COUNTS_METHOD = "custom_ui.api.db.clients.get_client_status_counts";
|
||||
@ -63,6 +67,8 @@ const FRAPPE_GET_CLIENT_TABLE_DATA_METHOD = "custom_ui.api.db.clients.get_client
|
||||
const FRAPPE_GET_CLIENT_TABLE_DATA_V2_METHOD = "custom_ui.api.db.clients.get_clients_table_data_v2";
|
||||
const FRAPPE_GET_CLIENT_METHOD = "custom_ui.api.db.clients.get_client_v2";
|
||||
const FRAPPE_GET_CLIENT_NAMES_METHOD = "custom_ui.api.db.clients.get_client_names";
|
||||
const FRAPPE_CHECK_CLIENT_EXISTS_METHOD = "custom_ui.api.db.clients.check_client_exists";
|
||||
const FRAPPE_ADD_ADDRESSES_CONTACTS_METHOD = "custom_ui.api.db.clients.add_addresses_contacts";
|
||||
// Employee methods
|
||||
const FRAPPE_GET_EMPLOYEES_METHOD = "custom_ui.api.db.employees.get_employees";
|
||||
const FRAPPE_GET_EMPLOYEES_ORGANIZED_METHOD = "custom_ui.api.db.employees.get_employees_organized";
|
||||
@ -76,7 +82,7 @@ const FRAPPE_UPDATE_SERVICE_APPOINTMENT_SCHEDULED_DATES_METHOD = "custom_ui.api.
|
||||
const FRAPPE_UPDATE_SERVICE_APPOINTMENT_STATUS_METHOD = "custom_ui.api.db.service_appointments.update_service_appointment_status";
|
||||
class Api {
|
||||
// ============================================================================
|
||||
// CORE REQUEST METHOPD
|
||||
// CORE REQUEST METHOD
|
||||
// ============================================================================
|
||||
|
||||
static async request(frappeMethod, args = {}) {
|
||||
@ -104,6 +110,10 @@ class Api {
|
||||
// CLIENT METHODS
|
||||
// ============================================================================
|
||||
|
||||
static async checkCustomerExists(clientName) {
|
||||
return await this.request(FRAPPE_CHECK_CLIENT_EXISTS_METHOD, { clientName });
|
||||
}
|
||||
|
||||
static async getClientStatusCounts(params = {}) {
|
||||
return await this.request(FRAPPE_GET_CLIENT_STATUS_COUNTS_METHOD, params);
|
||||
}
|
||||
@ -174,6 +184,10 @@ class Api {
|
||||
return result;
|
||||
}
|
||||
|
||||
static async addAddressesAndContacts(clientName, companyName, addresses = [], contacts = []) {
|
||||
return await this.request(FRAPPE_ADD_ADDRESSES_CONTACTS_METHOD, { clientName, companyName, addresses, contacts });
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ON-SITE MEETING METHODS
|
||||
// ============================================================================
|
||||
@ -467,12 +481,13 @@ class Api {
|
||||
return await this.request(FRAPPE_GET_SERVICE_APPOINTMENTS_METHOD, { companies, filters });
|
||||
}
|
||||
|
||||
static async updateServiceAppointmentScheduledDates(serviceAppointmentName, startDate, endDate, crewLeadName, startTime = null, endTime = null) {
|
||||
static async updateServiceAppointmentScheduledDates(serviceAppointmentName, startDate = null, endDate = null, crewLeadName = null, skippedDays = [],startTime = null, endTime = null) {
|
||||
return await this.request(FRAPPE_UPDATE_SERVICE_APPOINTMENT_SCHEDULED_DATES_METHOD, {
|
||||
serviceAppointmentName,
|
||||
startDate,
|
||||
endDate,
|
||||
crewLeadName,
|
||||
skippedDays,
|
||||
startTime,
|
||||
endTime
|
||||
})
|
||||
@ -574,8 +589,12 @@ class Api {
|
||||
|
||||
}
|
||||
|
||||
static async sendInvoice(invoiceName) {
|
||||
return await this.request(FRAPPE_SUBMIT_AND_SEND_INVOICE_METHOD, { invoiceName });
|
||||
}
|
||||
|
||||
static async createInvoice(invoiceData) {
|
||||
const payload = DataUtils.toSnakeCaseObject(invoiceData);
|
||||
// const payload = DataUtils.toSnakeCaseObject(invoiceData);
|
||||
const result = await this.request(FRAPPE_UPSERT_INVOICE_METHOD, { data: payload });
|
||||
console.log("DEBUG: API - Created Invoice: ", result);
|
||||
return result;
|
||||
@ -639,10 +658,22 @@ class Api {
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CONTACT METHODS
|
||||
// ============================================================================
|
||||
|
||||
static async checkContactsExist(contacts) {
|
||||
return await this.request(FRAPPE_CHECK_CONTACTS_EXIST_METHOD, { contacts });
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ADDRESS METHODS
|
||||
// ============================================================================
|
||||
|
||||
static async checkAddressesExist(addresses) {
|
||||
return await this.request(FRAPPE_CHECK_ADDRESSES_EXIST_METHOD, { addresses });
|
||||
}
|
||||
|
||||
static async getAddressByFullAddress(fullAddress) {
|
||||
return await this.request("custom_ui.api.db.addresses.get_address_by_full_address", {
|
||||
full_address: fullAddress,
|
||||
|
||||
@ -101,11 +101,13 @@
|
||||
dragOverSlot?.date === day.date &&
|
||||
dragOverSlot?.time === timeSlot.time,
|
||||
weekend: day.isWeekend,
|
||||
'has-multiple-meetings': getMeetingsForTimeSlot(day.date, timeSlot.time).length > 1,
|
||||
}"
|
||||
@click="selectTimeSlot(day.date, timeSlot.time)"
|
||||
@dragover="handleDragOver($event, day.date, timeSlot.time)"
|
||||
@dragleave="handleDragLeave"
|
||||
@drop="handleDrop($event, day.date, timeSlot.time)"
|
||||
:ref="el => dayCells[`${day.date}-${timeSlot.time}`] = el"
|
||||
>
|
||||
<!-- Meetings in this time slot -->
|
||||
<div
|
||||
@ -119,7 +121,7 @@
|
||||
:draggable="meeting.status !== 'Completed'"
|
||||
@dragstart="handleMeetingDragStart($event, meeting)"
|
||||
@dragend="handleDragEnd($event)"
|
||||
@click.stop="showMeetingDetails(meeting)"
|
||||
@drop="handleDrop($event, day.date, timeSlot.time)"
|
||||
>
|
||||
<div class="event-time">
|
||||
{{ formatTimeDisplay(meeting.scheduledTime) }}
|
||||
@ -135,6 +137,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slot Popup for Multiple Meetings -->
|
||||
<div v-if="showSlotPopup" class="slot-popup-overlay" @click="showSlotPopup = false">
|
||||
<div class="slot-popup" :style="popupStyle">
|
||||
<div class="popup-header">
|
||||
{{ formatTimeDisplay(popupPosition.time) }} - {{ formatDate(popupPosition.date) }}
|
||||
</div>
|
||||
<div
|
||||
v-for="meeting in popupMeetings"
|
||||
:key="meeting.id"
|
||||
class="popup-meeting-item"
|
||||
@click.stop="showMeetingDetails(meeting); showSlotPopup = false"
|
||||
>
|
||||
<div class="meeting-address">{{ meeting.address?.fullAddress || meeting.address }}</div>
|
||||
<div class="meeting-client">{{ meeting.client }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Sidebar for Unscheduled Meetings -->
|
||||
<div class="sidebar sidebar-right" :class="{ collapsed: isSidebarCollapsed }">
|
||||
<div class="sidebar-header">
|
||||
@ -274,6 +294,13 @@ const draggedMeeting = ref(null);
|
||||
const originalMeetingForReschedule = ref(null); // Store original meeting data for reschedules
|
||||
const isUnscheduledDragOver = ref(false);
|
||||
|
||||
// Slot popup state
|
||||
const showSlotPopup = ref(false);
|
||||
const popupMeetings = ref([]);
|
||||
const popupPosition = ref({});
|
||||
const popupStyle = ref({});
|
||||
const dayCells = ref({});
|
||||
|
||||
// Sidebar state
|
||||
const isSidebarCollapsed = ref(false);
|
||||
|
||||
@ -313,7 +340,7 @@ const weekDays = computed(() => {
|
||||
const isWeekend = day.getDay() === 0 || day.getDay() === 6;
|
||||
|
||||
days.push({
|
||||
date: day.toISOString().split("T")[0],
|
||||
date: `${day.getFullYear()}-${String(day.getMonth() + 1).padStart(2, '0')}-${String(day.getDate()).padStart(2, '0')}`,
|
||||
dayName: day.toLocaleDateString("en-US", { weekday: "short" }),
|
||||
displayDate: day.toLocaleDateString("en-US", { month: "short", day: "numeric" }),
|
||||
isToday,
|
||||
@ -472,7 +499,25 @@ const getAddressText = (address) => {
|
||||
};
|
||||
|
||||
const selectTimeSlot = (date, time) => {
|
||||
console.log("Selected time slot:", date, time);
|
||||
const meetingsInSlot = getMeetingsForTimeSlot(date, time);
|
||||
if (meetingsInSlot.length === 1) {
|
||||
showMeetingDetails(meetingsInSlot[0]);
|
||||
} else if (meetingsInSlot.length > 1) {
|
||||
// Show popup with list of meetings
|
||||
const cellKey = `${date}-${time}`;
|
||||
const cell = dayCells.value[cellKey];
|
||||
if (cell) {
|
||||
const rect = cell.getBoundingClientRect();
|
||||
popupStyle.value = {
|
||||
top: `${rect.bottom + window.scrollY + 5}px`,
|
||||
left: `${rect.left + window.scrollX}px`,
|
||||
};
|
||||
}
|
||||
popupMeetings.value = meetingsInSlot;
|
||||
popupPosition.value = { date, time };
|
||||
showSlotPopup.value = true;
|
||||
}
|
||||
// If no meetings, do nothing for now
|
||||
};
|
||||
|
||||
const showMeetingDetails = (meeting) => {
|
||||
@ -964,15 +1009,8 @@ const loadWeekMeetings = async () => {
|
||||
meetings.value = apiResult
|
||||
.map((meeting) => {
|
||||
// Extract date and time from startTime
|
||||
const startDateTime = meeting.startTime
|
||||
? new Date(meeting.startTime)
|
||||
: null;
|
||||
const date = startDateTime
|
||||
? startDateTime.toISOString().split("T")[0]
|
||||
: null;
|
||||
const time = startDateTime
|
||||
? `${startDateTime.getHours().toString().padStart(2, "0")}:${startDateTime.getMinutes().toString().padStart(2, "0")}`
|
||||
: null;
|
||||
const date = meeting.startTime ? meeting.startTime.split(' ')[0] : null;
|
||||
const time = meeting.startTime ? meeting.startTime.split(' ')[1].substring(0,5) : null;
|
||||
|
||||
// Return the full meeting object with calendar-specific fields added
|
||||
return {
|
||||
@ -1108,8 +1146,8 @@ const findAndDisplayMeetingByName = async () => {
|
||||
|
||||
// Parse the start time to get date and time
|
||||
const startDateTime = new Date(meetingData.startTime);
|
||||
const meetingDate = startDateTime.toISOString().split("T")[0];
|
||||
const meetingTime = `${startDateTime.getHours().toString().padStart(2, "0")}:${startDateTime.getMinutes().toString().padStart(2, "0")}`;
|
||||
const meetingDate = meetingData.startTime.split(' ')[0];
|
||||
const meetingTime = meetingData.startTime.split(' ')[1].substring(0,5);
|
||||
|
||||
// Navigate to the week containing this meeting
|
||||
currentWeekStart.value = new Date(
|
||||
@ -1636,24 +1674,60 @@ watch(
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.main-layout {
|
||||
flex-direction: column;
|
||||
}
|
||||
.day-column.has-multiple-meetings {
|
||||
border: 2px solid #ff9800;
|
||||
background-color: #fff3e0;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
padding-right: 0;
|
||||
padding-bottom: 20px;
|
||||
max-height: 200px;
|
||||
}
|
||||
.slot-popup-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 1000;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.calendar-header-row,
|
||||
.time-row {
|
||||
grid-template-columns: 60px repeat(7, 1fr);
|
||||
}
|
||||
.slot-popup {
|
||||
position: absolute;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
padding: 8px;
|
||||
min-width: 250px;
|
||||
max-width: 300px;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.popup-meeting-item {
|
||||
padding: 4px 0;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.popup-meeting-item:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.popup-meeting-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.meeting-address {
|
||||
font-size: 0.8em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.meeting-client {
|
||||
font-size: 0.7em;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -9,6 +9,7 @@
|
||||
v-for="(address, index) in localFormData.addresses"
|
||||
:key="index"
|
||||
class="address-item"
|
||||
:class="{ 'existing-highlight': isExistingAddress(address) }"
|
||||
>
|
||||
<div class="address-header">
|
||||
<div class="address-title">
|
||||
@ -169,6 +170,14 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
existingAddresses: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
contactOptions: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:formData"]);
|
||||
@ -200,7 +209,7 @@ const localFormData = computed({
|
||||
|
||||
const contactOptions = computed(() => {
|
||||
if (!localFormData.value.contacts || localFormData.value.contacts.length === 0) {
|
||||
return [];
|
||||
return props.contactOptions;
|
||||
}
|
||||
return localFormData.value.contacts.map((contact, index) => ({
|
||||
label: `${contact.firstName || ""} ${contact.lastName || ""}`.trim() || `Contact ${index + 1}`,
|
||||
@ -226,28 +235,6 @@ onMounted(() => {
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
const addAddress = () => {
|
||||
localFormData.value.addresses.push({
|
||||
addressLine1: "",
|
||||
addressLine2: "",
|
||||
isBillingAddress: false,
|
||||
isServiceAddress: true,
|
||||
pincode: "",
|
||||
city: "",
|
||||
state: "",
|
||||
contacts: [],
|
||||
primaryContact: null,
|
||||
zipcodeLookupDisabled: true,
|
||||
});
|
||||
};
|
||||
|
||||
const removeAddress = (index) => {
|
||||
if (localFormData.value.addresses.length > 1) {
|
||||
localFormData.value.addresses.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
const formatAddressLine = (index, field, event) => {
|
||||
const value = event.target.value;
|
||||
if (!value) return;
|
||||
@ -310,48 +297,53 @@ const handleServiceChange = (selectedIndex) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getFullAddress = (address) => {
|
||||
return `${address.addressLine1 || ''} ${address.addressLine2 || ''} ${address.city || ''} ${address.state || ''} ${address.pincode || ''}`.trim().replace(/\s+/g, ' ');
|
||||
};
|
||||
|
||||
const handleZipcodeInput = async (index, event) => {
|
||||
const input = event.target.value;
|
||||
|
||||
// Only allow digits
|
||||
const digitsOnly = input.replace(/\D/g, "");
|
||||
|
||||
// Limit to 5 digits
|
||||
if (digitsOnly.length > 5) {
|
||||
return;
|
||||
}
|
||||
|
||||
localFormData.value.addresses[index].pincode = digitsOnly;
|
||||
|
||||
// Reset city/state if zipcode is not complete
|
||||
if (digitsOnly.length < 5 && localFormData.value.addresses[index].zipcodeLookupDisabled) {
|
||||
localFormData.value.addresses[index].city = "";
|
||||
localFormData.value.addresses[index].state = "";
|
||||
localFormData.value.addresses[index].zipcodeLookupDisabled = false;
|
||||
}
|
||||
|
||||
// Fetch city/state when 5 digits entered
|
||||
if (digitsOnly.length === 5) {
|
||||
const value = event.target.value;
|
||||
localFormData.value.addresses[index].pincode = value;
|
||||
if (value.length === 5) {
|
||||
try {
|
||||
console.log("DEBUG: Looking up city/state for zip code:", digitsOnly);
|
||||
const places = await Api.getCityStateByZip(digitsOnly);
|
||||
console.log("DEBUG: Retrieved places:", places);
|
||||
if (places && places.length > 0) {
|
||||
// Auto-populate city and state
|
||||
localFormData.value.addresses[index].city = places[0]["city"];
|
||||
localFormData.value.addresses[index].state = places[0]["state"];
|
||||
localFormData.value.addresses[index].zipcodeLookupDisabled = true;
|
||||
notificationStore.addSuccess(`Found: ${places[0]["city"]}, ${places[0]["state"]}`);
|
||||
const zipInfo = await Api.getCityStateByZip(value);
|
||||
console.log("Zipcode lookup result:", zipInfo);
|
||||
if (zipInfo && zipInfo.length > 0) {
|
||||
localFormData.value.addresses[index].city = zipInfo[0].city;
|
||||
localFormData.value.addresses[index].state = zipInfo[0].state;
|
||||
localFormData.value.addresses[index].zipcodeLookupDisabled = false;
|
||||
} else {
|
||||
throw new Error("No data returned");
|
||||
}
|
||||
} catch (error) {
|
||||
// Enable manual entry if lookup fails
|
||||
localFormData.value.addresses[index].zipcodeLookupDisabled = false;
|
||||
notificationStore.addWarning(
|
||||
"Could not find city/state for this zip code. Please enter manually.",
|
||||
);
|
||||
console.error("Zipcode lookup failed:", error);
|
||||
localFormData.value.addresses[index].zipcodeLookupDisabled = true;
|
||||
localFormData.value.addresses[index].city = '';
|
||||
localFormData.value.addresses[index].state = '';
|
||||
notificationStore.addError("Invalid zipcode or lookup failed");
|
||||
}
|
||||
} else {
|
||||
localFormData.value.addresses[index].zipcodeLookupDisabled = true;
|
||||
localFormData.value.addresses[index].city = '';
|
||||
localFormData.value.addresses[index].state = '';
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeAddressString = (s = '') => {
|
||||
return (s || '')
|
||||
.toString()
|
||||
.replace(/,/g, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
};
|
||||
|
||||
const isExistingAddress = (address) => {
|
||||
const fullAddr = getFullAddress(address);
|
||||
const normFull = normalizeAddressString(fullAddr);
|
||||
if (!props.existingAddresses || props.existingAddresses.length === 0) return false;
|
||||
return props.existingAddresses.some((ea) => normalizeAddressString(ea) === normFull);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -479,13 +471,8 @@ const handleZipcodeInput = async (index, event) => {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.form-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.address-item.existing-highlight {
|
||||
border-color: var(--red-500);
|
||||
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -28,13 +28,12 @@
|
||||
class="w-full"
|
||||
/>
|
||||
<Button
|
||||
label="Check"
|
||||
size="small"
|
||||
icon="pi pi-check-circle"
|
||||
class="check-btn"
|
||||
@click="checkCustomerExists"
|
||||
:disabled="isSubmitting"
|
||||
/>
|
||||
>
|
||||
<i class="pi pi-check-circle"></i> Check
|
||||
</Button>
|
||||
<Button
|
||||
v-if="!isNewClient && !isEditMode"
|
||||
@click="searchCustomers"
|
||||
@ -52,7 +51,7 @@
|
||||
<Dialog
|
||||
:visible="showCustomerSearchModal"
|
||||
@update:visible="showCustomerSearchModal = $event"
|
||||
header="Select Customer"
|
||||
header="Potential Matches"
|
||||
:modal="true"
|
||||
class="search-dialog"
|
||||
>
|
||||
@ -61,15 +60,18 @@
|
||||
<i class="pi pi-info-circle"></i>
|
||||
<p>No customers found matching your search.</p>
|
||||
</div>
|
||||
<div v-else class="results-list">
|
||||
<div
|
||||
v-for="(customerName, index) in customerSearchResults"
|
||||
:key="index"
|
||||
class="result-item"
|
||||
@click="selectCustomer(customerName)"
|
||||
>
|
||||
<strong>{{ customerName }}</strong>
|
||||
<i class="pi pi-chevron-right"></i>
|
||||
<div v-else>
|
||||
<p class="potential-matches-message">Here are potential matches for your search. Click on a customer to view their details.</p>
|
||||
<div class="results-list">
|
||||
<div
|
||||
v-for="(customerName, index) in customerSearchResults"
|
||||
:key="index"
|
||||
class="result-item"
|
||||
@click="router.push(`/client?client=${encodeURIComponent(customerName)}`)"
|
||||
>
|
||||
<strong>{{ customerName }}</strong>
|
||||
<i class="pi pi-chevron-right"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -77,12 +79,30 @@
|
||||
<Button label="Cancel" severity="secondary" @click="showCustomerSearchModal = false" />
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<!-- Exact Match Modal -->
|
||||
<Dialog
|
||||
:visible="showExactMatchModal"
|
||||
@update:visible="showExactMatchModal = $event"
|
||||
header="Customer Already Exists"
|
||||
:modal="true"
|
||||
>
|
||||
<p>The customer "{{ exactMatchClient }}" already exists.</p>
|
||||
<template #footer>
|
||||
<Button label="Cancel" severity="secondary" @click="showExactMatchModal = false" />
|
||||
<Button label="Go to Customer" @click="goToCustomer(exactMatchClient)" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template><script setup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, computed } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import InputText from "primevue/inputtext";
|
||||
import Select from "primevue/select";
|
||||
import Dialog from "primevue/dialog";
|
||||
import Button from "primevue/button";
|
||||
import Api from "../../api";
|
||||
import { useNotificationStore } from "../../stores/notifications-primevue";
|
||||
|
||||
@ -104,6 +124,7 @@ const props = defineProps({
|
||||
const emit = defineEmits(["update:formData", "newClientToggle", "customerSelected"]);
|
||||
|
||||
const notificationStore = useNotificationStore();
|
||||
const router = useRouter();
|
||||
|
||||
const localFormData = computed({
|
||||
get: () => props.formData,
|
||||
@ -114,6 +135,8 @@ const isNewClient = ref(true);
|
||||
const showCustomerSearchModal = ref(false);
|
||||
const customerSearchResults = ref([]);
|
||||
const customerTypeOptions = ["Individual", "Partnership", "Company"];
|
||||
const showExactMatchModal = ref(false);
|
||||
const exactMatchClient = ref(null);
|
||||
|
||||
const mapContactsFromClient = (contacts = []) => {
|
||||
if (!Array.isArray(contacts) || contacts.length === 0) {
|
||||
@ -191,58 +214,26 @@ const checkCustomerExists = async () => {
|
||||
notificationStore.addWarning("Please ensure a customer name is entered before checking.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const client = await Api.getClient(searchTerm);
|
||||
|
||||
if (!client) {
|
||||
notificationStore.addInfo("Customer is not in our system yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
localFormData.value.customerName = client.customerName || searchTerm;
|
||||
localFormData.value.customerType = client.customerType || localFormData.value.customerType;
|
||||
localFormData.value.contacts = mapContactsFromClient(client.contacts);
|
||||
|
||||
isNewClient.value = false;
|
||||
showCustomerSearchModal.value = false;
|
||||
|
||||
emit("customerSelected", client);
|
||||
notificationStore.addSuccess(
|
||||
`Customer ${localFormData.value.customerName} found and loaded from system.`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error checking customer:", error);
|
||||
const message =
|
||||
typeof error?.message === "string" &&
|
||||
error.message.toLowerCase().includes("not found")
|
||||
? "Customer is not in our system yet."
|
||||
: "Failed to check customer. Please try again.";
|
||||
|
||||
if (message.includes("not in our system")) {
|
||||
notificationStore.addInfo(message);
|
||||
const result = await Api.checkCustomerExists(searchTerm);
|
||||
if (result.exactMatch) {
|
||||
exactMatchClient.value = result.exactMatch;
|
||||
showExactMatchModal.value = true;
|
||||
} else if (result.potentialMatches && result.potentialMatches.length > 0) {
|
||||
customerSearchResults.value = result.potentialMatches;
|
||||
showCustomerSearchModal.value = true;
|
||||
} else {
|
||||
notificationStore.addError(message);
|
||||
notificationStore.addInfo("No matching customers found.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error checking customer exists:", error);
|
||||
notificationStore.addError("Failed to check customer existence. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
const selectCustomer = async (customerName) => {
|
||||
try {
|
||||
// Fetch full customer data
|
||||
const clientData = await Api.getClient(customerName);
|
||||
|
||||
localFormData.value.customerName = clientData.customerName;
|
||||
localFormData.value.customerType = clientData.customerType;
|
||||
localFormData.value.contacts = mapContactsFromClient(clientData.contacts);
|
||||
showCustomerSearchModal.value = false;
|
||||
|
||||
// Pass the full client data including contacts
|
||||
emit("customerSelected", clientData);
|
||||
} catch (error) {
|
||||
console.error(`Error fetching client ${customerName}:`, error);
|
||||
notificationStore.addError("Failed to load customer details. Please try again.");
|
||||
}
|
||||
const goToCustomer = (clientName) => {
|
||||
router.push(`/client?client=${encodeURIComponent(clientName)}`);
|
||||
showExactMatchModal.value = false;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
@ -330,7 +321,8 @@ defineExpose({
|
||||
}
|
||||
|
||||
.search-dialog {
|
||||
max-width: 500px;
|
||||
max-width: 800px;
|
||||
width: 90vw;
|
||||
}
|
||||
|
||||
.search-results {
|
||||
@ -374,15 +366,14 @@ defineExpose({
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.customer-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.customer-type {
|
||||
font-size: 0.85rem;
|
||||
.potential-matches-message {
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.75rem;
|
||||
background-color: var(--surface-section);
|
||||
border: 1px solid var(--surface-border);
|
||||
border-radius: 4px;
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.iconoir-btn {
|
||||
@ -412,6 +403,9 @@ defineExpose({
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
height: 100%;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
v-for="(contact, index) in localFormData.contacts"
|
||||
:key="index"
|
||||
class="contact-item"
|
||||
:class="{ 'existing-highlight': isExistingContact(contact) }"
|
||||
>
|
||||
<div class="contact-header">
|
||||
<div class="contact-title">
|
||||
@ -139,29 +140,33 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
existingContacts: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:formData"]);
|
||||
|
||||
const localFormData = computed({
|
||||
get: () => {
|
||||
if (!props.formData.contacts || props.formData.contacts.length === 0) {
|
||||
props.formData.contacts = [
|
||||
{
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
phoneNumber: "",
|
||||
email: "",
|
||||
contactRole: "",
|
||||
isPrimary: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
return props.formData;
|
||||
},
|
||||
get: () => props.formData,
|
||||
set: (value) => emit("update:formData", value),
|
||||
});
|
||||
|
||||
// Ensure at least one contact always exists
|
||||
if (!localFormData.value.contacts || localFormData.value.contacts.length === 0) {
|
||||
localFormData.value.contacts = [
|
||||
{
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
phoneNumber: "",
|
||||
email: "",
|
||||
contactRole: "",
|
||||
isPrimary: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const roleOptions = ref([
|
||||
{ label: "Owner", value: "Owner" },
|
||||
{ label: "Property Manager", value: "Property Manager" },
|
||||
@ -278,7 +283,14 @@ const handlePhoneKeydown = (event, index) => {
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({});
|
||||
const getFullName = (contact) => {
|
||||
return `${contact.firstName || ''} ${contact.lastName || ''}`.trim();
|
||||
};
|
||||
|
||||
const isExistingContact = (contact) => {
|
||||
const fullName = getFullName(contact);
|
||||
return props.existingContacts.includes(fullName);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -406,15 +418,8 @@ defineExpose({});
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.contact-item.existing-highlight {
|
||||
border-color: var(--red-500);
|
||||
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<Dialog :visible="visible" @update:visible="val => emit('update:visible', val)" modal :closable="false" :style="{ width: '700px', maxWidth: '95vw' }">
|
||||
<template #header>
|
||||
<span class="modal-title">Add Contact/Address</span>
|
||||
</template>
|
||||
<div class="modal-body">
|
||||
<ContactInformationForm
|
||||
:formData="contactFormData.value"
|
||||
@update:formData="val => contactFormData.value = val"
|
||||
:isSubmitting="isSubmitting"
|
||||
:existingContacts="existingContacts"
|
||||
/>
|
||||
<AddressInformationForm
|
||||
:formData="addressFormData.value"
|
||||
@update:formData="val => addressFormData.value = val"
|
||||
:isSubmitting="isSubmitting"
|
||||
:contactOptions="allContactOptions"
|
||||
:existingAddresses="existingAddresses"
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button label="Cancel" @click="close" severity="secondary" />
|
||||
<Button label="Create" @click="create" severity="primary" :loading="isSubmitting" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import Button from 'primevue/button';
|
||||
import ContactInformationForm from '../clientSubPages/ContactInformationForm.vue';
|
||||
import AddressInformationForm from '../clientSubPages/AddressInformationForm.vue';
|
||||
|
||||
const props = defineProps({
|
||||
visible: Boolean,
|
||||
clientContacts: { type: Array, default: () => [] },
|
||||
existingContacts: { type: Array, default: () => [] },
|
||||
existingAddresses: { type: Array, default: () => [] },
|
||||
isSubmitting: { type: Boolean, default: false },
|
||||
});
|
||||
const emit = defineEmits(['update:visible', 'created']);
|
||||
|
||||
const contactFormData = ref({ contacts: [] });
|
||||
const addressFormData = ref({ addresses: [], contacts: [] });
|
||||
|
||||
// Keep addressFormData.contacts in sync with new contacts
|
||||
watch(
|
||||
() => contactFormData.value.contacts,
|
||||
(newContacts) => {
|
||||
addressFormData.value.contacts = newContacts || [];
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// All contact options = clientContacts + new contacts
|
||||
const allContactOptions = computed(() => {
|
||||
const clientOpts = (props.clientContacts || []).map((c, idx) => ({
|
||||
label: `${c.firstName || ''} ${c.lastName || ''}`.trim() || `Contact ${idx + 1}`,
|
||||
value: `client-${idx}`,
|
||||
...c,
|
||||
}));
|
||||
const newOpts = (contactFormData.value.contacts || []).map((c, idx) => ({
|
||||
label: `${c.firstName || ''} ${c.lastName || ''}`.trim() || `Contact ${idx + 1}`,
|
||||
value: `new-${idx}`,
|
||||
...c,
|
||||
}));
|
||||
return [...clientOpts, ...newOpts];
|
||||
});
|
||||
|
||||
function close() {
|
||||
emit('update:visible', false);
|
||||
}
|
||||
function create() {
|
||||
// Dummy create handler
|
||||
console.log('Create clicked', {
|
||||
contacts: contactFormData.value.contacts,
|
||||
addresses: addressFormData.value.addresses,
|
||||
});
|
||||
emit('created', {
|
||||
contacts: contactFormData.value.contacts,
|
||||
addresses: addressFormData.value.addresses,
|
||||
});
|
||||
close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.modal-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
</style>
|
||||
@ -1,116 +1,123 @@
|
||||
<template>
|
||||
<div class="general-client-info">
|
||||
<div class="info-grid">
|
||||
<!-- Lead Badge -->
|
||||
<div v-if="isLead" class="lead-badge-container">
|
||||
<Badge value="LEAD" severity="warn" size="large" />
|
||||
<div class="action-buttons">
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
@click="addAddress"
|
||||
>
|
||||
<v-icon left size="small">mdi-map-marker-plus</v-icon>
|
||||
Add Address
|
||||
</v-btn>
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
@click="addContact"
|
||||
>
|
||||
<v-icon left size="small">mdi-account-plus</v-icon>
|
||||
Add Contact
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Client Name (only show for Company type) -->
|
||||
<div v-if="clientData.customerType === 'Company'" class="info-section">
|
||||
<label>Company Name</label>
|
||||
<span class="info-value large">{{ displayClientName }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Client Type -->
|
||||
<div class="info-section">
|
||||
<label>Client Type</label>
|
||||
<span class="info-value">{{ clientData.customerType || "N/A" }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Associated Companies -->
|
||||
<div v-if="associatedCompanies.length > 0" class="info-section">
|
||||
<label>Associated Companies</label>
|
||||
<div class="companies-list">
|
||||
<Tag
|
||||
v-for="company in associatedCompanies"
|
||||
:key="company"
|
||||
:value="company"
|
||||
severity="info"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Billing Address -->
|
||||
<div v-if="billingAddress" class="info-section">
|
||||
<label>Billing Address</label>
|
||||
<span class="info-value">{{ billingAddress }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Primary Contact Info -->
|
||||
<div v-if="primaryContact" class="info-section primary-contact">
|
||||
<label>{{ clientData.customerType === 'Individual' ? 'Contact Information' : 'Primary Contact' }}</label>
|
||||
<div class="contact-details">
|
||||
<div class="contact-item">
|
||||
<i class="pi pi-user"></i>
|
||||
<span>{{ primaryContact.fullName || primaryContact.name || "N/A" }}</span>
|
||||
</div>
|
||||
<div class="contact-item">
|
||||
<i class="pi pi-envelope"></i>
|
||||
<span>{{ primaryContact.emailId || primaryContact.customEmail || "N/A" }}</span>
|
||||
</div>
|
||||
<div class="contact-item">
|
||||
<i class="pi pi-phone"></i>
|
||||
<span>{{ primaryContact.phone || primaryContact.mobileNo || "N/A" }}</span>
|
||||
<div>
|
||||
<div class="general-client-info">
|
||||
<div class="info-grid">
|
||||
<!-- Add Contact/Address Button (always visible) -->
|
||||
<div class="lead-badge-container">
|
||||
<template v-if="isLead">
|
||||
<Badge value="LEAD" severity="warn" size="large" />
|
||||
</template>
|
||||
<div class="action-buttons">
|
||||
<Button size="small" variant="outlined" color="primary" @click="openAddModal">
|
||||
<v-icon left size="small">mdi-account-plus</v-icon>
|
||||
Add Contact/Address
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics -->
|
||||
<div class="info-section stats">
|
||||
<label>Overview</label>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<i class="pi pi-map-marker"></i>
|
||||
<span class="stat-value">{{ addressCount }}</span>
|
||||
<span class="stat-label">Addresses</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<i class="pi pi-users"></i>
|
||||
<span class="stat-value">{{ contactCount }}</span>
|
||||
<span class="stat-label">Contacts</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<i class="pi pi-briefcase"></i>
|
||||
<span class="stat-value">{{ projectCount }}</span>
|
||||
<span class="stat-label">Projects</span>
|
||||
<!-- Client Name (only show for Company type) -->
|
||||
<div v-if="clientData.customerType === 'Company'" class="info-section">
|
||||
<label>Company Name</label>
|
||||
<span class="info-value large">{{ displayClientName }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Client Type -->
|
||||
<div class="info-section">
|
||||
<label>Client Type</label>
|
||||
<span class="info-value">{{ clientData.customerType || "N/A" }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Associated Companies -->
|
||||
<div v-if="associatedCompanies.length > 0" class="info-section">
|
||||
<label>Associated Companies</label>
|
||||
<div class="companies-list">
|
||||
<Tag
|
||||
v-for="company in associatedCompanies"
|
||||
:key="company"
|
||||
:value="company"
|
||||
severity="info"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Creation Date -->
|
||||
<div class="info-section">
|
||||
<label>Created</label>
|
||||
<span class="info-value">{{ formattedCreationDate }}</span>
|
||||
<!-- Billing Address -->
|
||||
<div v-if="billingAddress" class="info-section">
|
||||
<label>Billing Address</label>
|
||||
<span class="info-value">{{ billingAddress }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Primary Contact Info -->
|
||||
<div v-if="primaryContact" class="info-section primary-contact">
|
||||
<label>{{ clientData.customerType === 'Individual' ? 'Contact Information' : 'Primary Contact' }}</label>
|
||||
<div class="contact-details">
|
||||
<div class="contact-item">
|
||||
<i class="pi pi-user"></i>
|
||||
<span>{{ primaryContact.fullName || primaryContact.name || "N/A" }}</span>
|
||||
</div>
|
||||
<div class="contact-item">
|
||||
<i class="pi pi-envelope"></i>
|
||||
<span>{{ primaryContact.emailId || primaryContact.customEmail || "N/A" }}</span>
|
||||
</div>
|
||||
<div class="contact-item">
|
||||
<i class="pi pi-phone"></i>
|
||||
<span>{{ primaryContact.phone || primaryContact.mobileNo || "N/A" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics -->
|
||||
<div class="info-section stats">
|
||||
<label>Overview</label>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<i class="pi pi-map-marker"></i>
|
||||
<span class="stat-value">{{ addressCount }}</span>
|
||||
<span class="stat-label">Addresses</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<i class="pi pi-users"></i>
|
||||
<span class="stat-value">{{ contactCount }}</span>
|
||||
<span class="stat-label">Contacts</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<i class="pi pi-briefcase"></i>
|
||||
<span class="stat-value">{{ projectCount }}</span>
|
||||
<span class="stat-label">Projects</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Creation Date -->
|
||||
<div class="info-section">
|
||||
<label>Created</label>
|
||||
<span class="info-value">{{ formattedCreationDate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AddContactAddressModal
|
||||
:visible="showAddModal"
|
||||
@update:visible="showAddModal = $event"
|
||||
:clientContacts="clientData.contacts || []"
|
||||
:existingContacts="clientData.contacts?.map(c => c.fullName || c.name) || []"
|
||||
:existingAddresses="clientData.addresses?.map(a => a.addressLine1) || []"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import Badge from "primevue/badge";
|
||||
import Tag from "primevue/tag";
|
||||
import AddContactAddressModal from './AddContactAddressModal.vue';
|
||||
import Button from 'primevue/button';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const showAddModal = ref(false);
|
||||
const openAddModal = () => {
|
||||
showAddModal.value = true;
|
||||
};
|
||||
|
||||
const props = defineProps({
|
||||
clientData: {
|
||||
@ -166,16 +173,8 @@ const formattedCreationDate = computed(() => {
|
||||
});
|
||||
});
|
||||
|
||||
// Placeholder methods for adding address and contact
|
||||
const addAddress = () => {
|
||||
console.log("Add Address modal would open here");
|
||||
// TODO: Open add address modal
|
||||
};
|
||||
|
||||
const addContact = () => {
|
||||
console.log("Add Contact modal would open here");
|
||||
// TODO: Open add contact modal
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -1,64 +1,124 @@
|
||||
<template>
|
||||
<div class="client-page">
|
||||
<!-- Client Header -->
|
||||
<GeneralClientInfo
|
||||
v-if="client.customerName"
|
||||
:client-data="client"
|
||||
/>
|
||||
<AdditionalInfoBar :address="client.addresses[selectedAddressIdx]" v-if="client.customerName" />
|
||||
<!-- New Client Form -->
|
||||
<div v-if="isNew">
|
||||
<ClientInformationForm
|
||||
:formData="client"
|
||||
:is-submitting="isSubmitting"
|
||||
@update:formData="handleClientUpdate"
|
||||
@newClientToggle="handleNewClientToggle"
|
||||
@customerSelected="handleCustomerSelected"
|
||||
/>
|
||||
<ContactInformationForm
|
||||
:formData="client"
|
||||
:is-submitting="isSubmitting"
|
||||
:existing-contacts="existingContacts.map(contact => `${contact.firstName || ''} ${contact.lastName || ''}`.trim() || contact.email || 'Unknown Contact')"
|
||||
@update:formData="handleClientUpdate"
|
||||
/>
|
||||
<AddressInformationForm
|
||||
:formData="client"
|
||||
:is-submitting="isSubmitting"
|
||||
:existing-addresses="existingAddresses.map(addr => DataUtils.calculateFullAddress(addr))"
|
||||
@update:formData="handleClientUpdate"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Address Selector (only shows if multiple addresses) -->
|
||||
<AddressSelector
|
||||
v-if="!isNew && client.addresses && client.addresses.length > 1"
|
||||
:addresses="client.addresses"
|
||||
:selected-address-idx="selectedAddressIdx"
|
||||
:contacts="client.contacts"
|
||||
@update:selected-address-idx="handleAddressChange"
|
||||
/>
|
||||
<!-- Existing Client View -->
|
||||
<div v-else>
|
||||
<!-- Client Header -->
|
||||
<GeneralClientInfo
|
||||
v-if="client.customerName"
|
||||
:client-data="client"
|
||||
/>
|
||||
<AdditionalInfoBar :address="client.addresses[selectedAddressIdx]" v-if="client.customerName" />
|
||||
|
||||
<!-- Main Content Tabs -->
|
||||
<Tabs value="0" class="overview-tabs">
|
||||
<TabList>
|
||||
<Tab value="0">Overview</Tab>
|
||||
<Tab value="1">Projects</Tab>
|
||||
<Tab value="2">Financials</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<!-- Overview Tab -->
|
||||
<TabPanel value="0">
|
||||
<Overview
|
||||
:selected-address="selectedAddressData"
|
||||
:all-contacts="client.contacts"
|
||||
:edit-mode="editMode"
|
||||
:is-new="isNew"
|
||||
:full-address="fullAddress"
|
||||
:client="client"
|
||||
@edit-mode-enabled="enableEditMode"
|
||||
@update:address-contacts="handleAddressContactsUpdate"
|
||||
@update:primary-contact="handlePrimaryContactUpdate"
|
||||
@update:client="handleClientUpdate"
|
||||
/>
|
||||
</TabPanel>
|
||||
<!-- Address Selector (only shows if multiple addresses) -->
|
||||
<AddressSelector
|
||||
v-if="!isNew && client.addresses && client.addresses.length > 1"
|
||||
:addresses="client.addresses"
|
||||
:selected-address-idx="selectedAddressIdx"
|
||||
:contacts="client.contacts"
|
||||
@update:selected-address-idx="handleAddressChange"
|
||||
/>
|
||||
|
||||
<!-- Projects Tab -->
|
||||
<TabPanel value="1">
|
||||
<div class="coming-soon-section">
|
||||
<i class="pi pi-wrench"></i>
|
||||
<h3>Projects</h3>
|
||||
<p>Section coming soon</p>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<!-- Main Content Tabs -->
|
||||
<Tabs value="0" class="overview-tabs">
|
||||
<TabList>
|
||||
<Tab value="0">Overview</Tab>
|
||||
<Tab value="1">Projects</Tab>
|
||||
<Tab value="2">Financials</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<!-- Overview Tab -->
|
||||
<TabPanel value="0">
|
||||
<Overview
|
||||
:selected-address="selectedAddressData"
|
||||
:all-contacts="client.contacts"
|
||||
:edit-mode="editMode"
|
||||
:is-new="isNew"
|
||||
:full-address="fullAddress"
|
||||
:client="client"
|
||||
@edit-mode-enabled="enableEditMode"
|
||||
@update:address-contacts="handleAddressContactsUpdate"
|
||||
@update:primary-contact="handlePrimaryContactUpdate"
|
||||
@update:client="handleClientUpdate"
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<!-- Financials Tab -->
|
||||
<TabPanel value="2">
|
||||
<div class="coming-soon-section">
|
||||
<i class="pi pi-dollar"></i>
|
||||
<h3>Financials</h3>
|
||||
<p>Section coming soon</p>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
<!-- Projects Tab -->
|
||||
<TabPanel value="1">
|
||||
<div class="coming-soon-section">
|
||||
<i class="pi pi-wrench"></i>
|
||||
<h3>Projects</h3>
|
||||
<p>Section coming soon</p>
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
||||
<!-- Financials Tab -->
|
||||
<TabPanel value="2">
|
||||
<div class="coming-soon-section">
|
||||
<i class="pi pi-dollar"></i>
|
||||
<h3>Financials</h3>
|
||||
<p>Section coming soon</p>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<!-- Existing Addresses/Contacts Modal -->
|
||||
<Dialog
|
||||
:visible="showExistingModal"
|
||||
@update:visible="showExistingModal = $event"
|
||||
header="Existing Addresses and Contacts Found"
|
||||
:modal="true"
|
||||
class="existing-modal"
|
||||
>
|
||||
<div class="modal-content">
|
||||
<p>The following addresses and/or contacts already exist in the system:</p>
|
||||
<div v-if="existingAddresses && existingAddresses.length > 0" class="existing-section">
|
||||
<h4>Existing Addresses:</h4>
|
||||
<ul>
|
||||
<li v-for="addr in existingAddresses" :key="addr">
|
||||
{{ addr.addressLine1 }} {{ addr.addressLine2 }}, {{ addr.city }}, {{ addr.state }} {{ addr.pincode }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="existingContacts && existingContacts.length > 0" class="existing-section">
|
||||
<h4>Existing Contacts:</h4>
|
||||
<ul>
|
||||
<li v-for="contact in existingContacts" :key="contact">
|
||||
{{ contact.firstName }} {{ contact.lastName }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>Would you like to link these existing addresses/contacts with this new client, or cancel the creation?</p>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button label="Cancel" severity="secondary" @click="cancelExisting" />
|
||||
<Button label="Continue and Link" @click="continueWithExisting" />
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<!-- Form Actions (for edit mode or new client) -->
|
||||
<div class="form-actions" v-if="editMode || isNew">
|
||||
@ -84,6 +144,7 @@ import Tab from "primevue/tab";
|
||||
import TabPanels from "primevue/tabpanels";
|
||||
import TabPanel from "primevue/tabpanel";
|
||||
import Button from "primevue/button";
|
||||
import Dialog from "primevue/dialog";
|
||||
import Api from "../../api";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useLoadingStore } from "../../stores/loading";
|
||||
@ -94,6 +155,9 @@ import AddressSelector from "../clientView/AddressSelector.vue";
|
||||
import GeneralClientInfo from "../clientView/GeneralClientInfo.vue";
|
||||
import AdditionalInfoBar from "../clientView/AdditionalInfoBar.vue";
|
||||
import Overview from "../clientView/Overview.vue";
|
||||
import ClientInformationForm from "../clientSubPages/ClientInformationForm.vue";
|
||||
import AddressInformationForm from "../clientSubPages/AddressInformationForm.vue";
|
||||
import ContactInformationForm from "../clientSubPages/ContactInformationForm.vue";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@ -128,6 +192,9 @@ const nextVisitDate = ref(null); // Placeholder, update as needed
|
||||
// Tab and edit state
|
||||
const editMode = ref(false);
|
||||
const isSubmitting = ref(false);
|
||||
const showExistingModal = ref(false);
|
||||
const existingAddresses = ref([]);
|
||||
const existingContacts = ref([]);
|
||||
|
||||
const selectedAddressIdx = computed({
|
||||
get: () => addresses.value.indexOf(selectedAddress.value),
|
||||
@ -154,17 +221,17 @@ const fullAddress = computed(() => {
|
||||
return DataUtils.calculateFullAddress(selectedAddressData.value);
|
||||
});
|
||||
|
||||
const getClientNames = async (type) => {
|
||||
loadingStore.setLoading(true);
|
||||
try {
|
||||
const names = await Api.getClientNames(type);
|
||||
clientNames.value = names;
|
||||
} catch (error) {
|
||||
console.error("Error fetching client names in Client.vue: ", error.message || error);
|
||||
} finally {
|
||||
loadingStore.setLoading(false);
|
||||
}
|
||||
};
|
||||
// const getClientNames = async (type) => {
|
||||
// loadingStore.setLoading(true);
|
||||
// try {
|
||||
// const names = await Api.getClientNames(type);
|
||||
// clientNames.value = names;
|
||||
// } catch (error) {
|
||||
// console.error("Error fetching client names in Client.vue: ", error.message || error);
|
||||
// } finally {
|
||||
// loadingStore.setLoading(false);
|
||||
// }
|
||||
// };
|
||||
|
||||
const getClient = async (name) => {
|
||||
loadingStore.setLoading(true);
|
||||
@ -315,6 +382,21 @@ const handleSubmit = async () => {
|
||||
isSubmitting.value = true;
|
||||
try {
|
||||
if (isNew.value) {
|
||||
const clientExists = await Api.checkCustomerExists(client.value.customerName);
|
||||
if (clientExists.exactMatch) {
|
||||
notificationStore.addError("A client with this name already exists. Please choose a different name.");
|
||||
return;
|
||||
}
|
||||
const addressesExist = await Api.checkAddressesExist(client.value.addresses);
|
||||
const contactsExist = await Api.checkContactsExist(client.value.contacts);
|
||||
console.log("Address existence check:", addressesExist);
|
||||
console.log("Contact existence check:", contactsExist);
|
||||
if (addressesExist.length > 0 || contactsExist.length > 0) {
|
||||
// existingAddresses.value = Array.isArray(addressesExist) ? addressesExist : [];
|
||||
// existingContacts.value = Array.isArray(contactsExist) ? contactsExist : [];
|
||||
showExistingModal.value = true;
|
||||
return;
|
||||
}
|
||||
const createdClient = await Api.createClient(client.value);
|
||||
console.log("Created client:", createdClient);
|
||||
notificationStore.addSuccess("Client created successfully!");
|
||||
@ -350,6 +432,35 @@ const handlePrimaryContactUpdate = (contactName) => {
|
||||
const handleClientUpdate = (newClientData) => {
|
||||
client.value = { ...client.value, ...newClientData };
|
||||
};
|
||||
|
||||
const cancelExisting = () => {
|
||||
showExistingModal.value = false;
|
||||
// TODO: Highlight existing addresses/contacts with red outline
|
||||
};
|
||||
|
||||
const continueWithExisting = async () => {
|
||||
showExistingModal.value = false;
|
||||
try {
|
||||
const createdClient = await Api.createClient(client.value);
|
||||
console.log("Created client:", createdClient);
|
||||
notificationStore.addSuccess("Client created successfully!");
|
||||
const strippedName = createdClient.name.split("-#-")[0].trim();
|
||||
// Navigate to the created client
|
||||
router.push('/client?client=' + encodeURIComponent(strippedName));
|
||||
} catch (error) {
|
||||
console.error("Error creating client:", error);
|
||||
notificationStore.addError("Failed to create client");
|
||||
}
|
||||
};
|
||||
|
||||
const handleNewClientToggle = (isNewClient) => {
|
||||
// Handle toggle if needed
|
||||
};
|
||||
|
||||
const handleCustomerSelected = (clientData) => {
|
||||
// Handle customer selected from search
|
||||
client.value = { ...client.value, ...clientData };
|
||||
};
|
||||
</script>
|
||||
<style lang="css">
|
||||
.tab-info-alert {
|
||||
@ -420,4 +531,32 @@ const handleClientUpdate = (newClientData) => {
|
||||
box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.existing-modal {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.existing-section {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.existing-section h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: var(--text-color);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.existing-section ul {
|
||||
margin: 0;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.existing-section li {
|
||||
margin-bottom: 0.25rem;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -90,7 +90,17 @@
|
||||
<span>Loading available items...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="remarks-section">
|
||||
<label for="remarks" class="field-label">Remarks</label>
|
||||
<Textarea
|
||||
id="remarks"
|
||||
v-model="formData.remarks"
|
||||
placeholder="Additional notes or instructions for the estimate"
|
||||
:disabled="!isEditable"
|
||||
:rows="6"
|
||||
fluid
|
||||
/>
|
||||
</div>
|
||||
<!-- Items Section -->
|
||||
<div class="items-section">
|
||||
<div class="items-header">
|
||||
@ -422,6 +432,7 @@ import Button from "primevue/button";
|
||||
import Select from "primevue/select";
|
||||
import Tooltip from "primevue/tooltip";
|
||||
import Drawer from "primevue/drawer";
|
||||
import Textarea from "primevue/textarea";
|
||||
import Api from "../../api";
|
||||
import DataUtils from "../../utils";
|
||||
import { useLoadingStore } from "../../stores/loading";
|
||||
@ -784,6 +795,7 @@ const saveDraft = async () => {
|
||||
requiresHalfPayment: formData.requiresHalfPayment,
|
||||
projectTemplate: formData.projectTemplate,
|
||||
fromOnsiteMeeting: formData.fromOnsiteMeeting,
|
||||
remarks: formData.remarks,
|
||||
company: company.currentCompany
|
||||
};
|
||||
estimate.value = await Api.createEstimate(data);
|
||||
@ -878,12 +890,7 @@ const isPackageItem = (item) => {
|
||||
};
|
||||
|
||||
const onTabClick = () => {
|
||||
console.log('Bid notes tab clicked');
|
||||
console.log('Current showDrawer value:', showDrawer.value);
|
||||
console.log('bidMeeting:', bidMeeting.value);
|
||||
console.log('bidMeeting?.bidNotes:', bidMeeting.value?.bidNotes);
|
||||
showDrawer.value = true;
|
||||
console.log('Set showDrawer to true');
|
||||
};
|
||||
|
||||
const totalCost = computed(() => {
|
||||
@ -910,17 +917,11 @@ watch(() => formData.projectTemplate, async (newValue) => {
|
||||
isLoadingQuotationItems.value = true;
|
||||
try {
|
||||
quotationItems.value = await Api.getItemsByProjectTemplate(newValue);
|
||||
console.log("DEBUG: quotationItems after API call:", quotationItems.value);
|
||||
console.log("DEBUG: quotationItems type:", typeof quotationItems.value);
|
||||
console.log("DEBUG: quotationItems keys length:", quotationItems.value ? Object.keys(quotationItems.value).length : 0);
|
||||
console.log("DEBUG: hasQuotationItems computed value:", hasQuotationItems.value);
|
||||
} catch (error) {
|
||||
console.error("Error fetching items by project template:", error);
|
||||
notificationStore.addNotification("Failed to load items for selected project template", "error");
|
||||
quotationItems.value = {};
|
||||
} finally {
|
||||
isLoadingQuotationItems.value = false;
|
||||
console.log("DEBUG: Loading finished, isLoadingQuotationItems:", isLoadingQuotationItems.value);
|
||||
}
|
||||
})
|
||||
|
||||
@ -1015,6 +1016,7 @@ watch(
|
||||
});
|
||||
}
|
||||
formData.requiresHalfPayment = Boolean(estimate.value.requiresHalfPayment || estimate.value.custom_requires_half_payment);
|
||||
formData.remarks = estimate.value.remarks;
|
||||
// If estimate has fromOnsiteMeeting, fetch bid meeting
|
||||
if (estimate.value.fromOnsiteMeeting) {
|
||||
try {
|
||||
@ -1172,6 +1174,7 @@ onMounted(async () => {
|
||||
});
|
||||
}
|
||||
formData.requiresHalfPayment = Boolean(estimate.value.requiresHalfPayment || estimate.value.custom_requires_half_payment);
|
||||
formData.remarks = estimate.value.remarks;
|
||||
// If estimate has fromOnsiteMeeting, fetch bid meeting
|
||||
if (estimate.value.fromOnsiteMeeting) {
|
||||
try {
|
||||
|
||||
@ -26,10 +26,30 @@
|
||||
<span class="btn-icon">📧</span>
|
||||
Email Customer
|
||||
</button>
|
||||
<button class="btn btn-primary" @click="createInvoiceForJob()">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click="createInvoiceForJob()"
|
||||
:disabled="!canCreateInvoice"
|
||||
>
|
||||
<span class="btn-icon">💰</span>
|
||||
Create Invoice
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
@click="viewInvoice()"
|
||||
:disabled="!canViewInvoice"
|
||||
>
|
||||
<span class="btn-icon">👁️</span>
|
||||
View Invoice
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
@click="sendInvoice()"
|
||||
:disabled="!canSendInvoice"
|
||||
>
|
||||
<span class="btn-icon">📤</span>
|
||||
{{ sendInvoiceButtonText }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -446,6 +466,26 @@ const filteredTasks = computed(() => {
|
||||
return job.value.tasks;
|
||||
});
|
||||
|
||||
const canCreateInvoice = computed(() => {
|
||||
const status = job.value?.invoiceStatus;
|
||||
return status === 'Ready to Invoice';
|
||||
});
|
||||
|
||||
const canViewInvoice = computed(() => {
|
||||
const status = job.value?.invoiceStatus;
|
||||
return status === 'Invoice Created' || status === 'Invoice Sent';
|
||||
});
|
||||
|
||||
const canSendInvoice = computed(() => {
|
||||
const status = job.value?.invoiceStatus;
|
||||
return status === 'Invoice Created' || status === 'Invoice Sent';
|
||||
});
|
||||
|
||||
const sendInvoiceButtonText = computed(() => {
|
||||
const status = job.value?.invoiceStatus;
|
||||
return status === 'Invoice Sent' ? 'Resend Invoice' : 'Send Invoice';
|
||||
});
|
||||
|
||||
const getProgressOffset = (percent) => {
|
||||
const circumference = 2 * Math.PI * 45;
|
||||
const offset = circumference - (percent / 100) * circumference;
|
||||
@ -536,7 +576,9 @@ const initializeMap = async () => {
|
||||
const createInvoiceForJob = async () => {
|
||||
if (!job.value) return;
|
||||
try {
|
||||
await Api.createInvoiceForJob(job.value.name);
|
||||
const invoice = await Api.createInvoiceForJob(job.value.name);
|
||||
job.value.invoiceStatus = "Invoice Created";
|
||||
job.value.invoice = invoice;
|
||||
notifications.addSuccess("Invoice created successfully");
|
||||
} catch (error) {
|
||||
console.error("Error creating invoice:", error);
|
||||
@ -544,6 +586,19 @@ const createInvoiceForJob = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const viewInvoice = () => {
|
||||
// Placeholder method for viewing invoice
|
||||
console.log("View Invoice clicked");
|
||||
notifications.addInfo("View Invoice functionality - Coming soon!");
|
||||
};
|
||||
|
||||
const sendInvoice = async () => {
|
||||
// Placeholder method for sending invoice
|
||||
await Api.sendInvoice(job.value.invoice.name)
|
||||
job.value.invoiceStatus = "Invoice Sent";
|
||||
console.log("Send Invoice clicked");
|
||||
};
|
||||
|
||||
const navigateToCalendar = () => {
|
||||
router.push('/calendar?tab=projects');
|
||||
};
|
||||
@ -771,8 +826,20 @@ onMounted(async () => {
|
||||
border: 1px solid #ced4da;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #f8f9fa;
|
||||
.btn:disabled {
|
||||
background: #e9ecef;
|
||||
color: #adb5bd;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn:disabled:hover {
|
||||
background: #e9ecef;
|
||||
color: #adb5bd;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
@ -989,24 +1056,24 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.financial-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 6px 12px;
|
||||
margin-top: 12px;
|
||||
padding-top: 0;
|
||||
border-top: none;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.financial-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
font-weight: 500;
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.item-value {
|
||||
@ -1014,6 +1081,7 @@ onMounted(async () => {
|
||||
font-weight: 600;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.item-value.paid {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
Loading…
x
Reference in New Issue
Block a user