From 6a5f2a3ebb01b72f2bcfd0e64ccd09b2a234b163 Mon Sep 17 00:00:00 2001 From: Westech Admin Date: Wed, 20 May 2026 22:14:10 +0000 Subject: [PATCH] feat: Customer Records page + CRM data import (7,966 records) --- .../__pycache__/crm_import.cpython-312.pyc | Bin 0 -> 6584 bytes .../crm_import_fix.cpython-312.pyc | Bin 0 -> 6567 bytes westech_r2/crm_import.py | 129 ++++++++++++++ westech_r2/crm_import_fix.py | 142 ++++++++++++++++ westech_r2/westech_r2/doctype/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 181 bytes .../doctype/customer_record/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 197 bytes .../customer_record.cpython-312.pyc | Bin 0 -> 446 bytes .../customer_record/customer_record.js | 8 + .../customer_record/customer_record.json | 158 ++++++++++++++++++ .../customer_record/customer_record.py | 9 + .../customer_record/test_customer_record.py | 9 + 13 files changed, 455 insertions(+) create mode 100644 westech_r2/__pycache__/crm_import.cpython-312.pyc create mode 100644 westech_r2/__pycache__/crm_import_fix.cpython-312.pyc create mode 100644 westech_r2/crm_import.py create mode 100644 westech_r2/crm_import_fix.py create mode 100644 westech_r2/westech_r2/doctype/__init__.py create mode 100644 westech_r2/westech_r2/doctype/__pycache__/__init__.cpython-312.pyc create mode 100644 westech_r2/westech_r2/doctype/customer_record/__init__.py create mode 100644 westech_r2/westech_r2/doctype/customer_record/__pycache__/__init__.cpython-312.pyc create mode 100644 westech_r2/westech_r2/doctype/customer_record/__pycache__/customer_record.cpython-312.pyc create mode 100644 westech_r2/westech_r2/doctype/customer_record/customer_record.js create mode 100644 westech_r2/westech_r2/doctype/customer_record/customer_record.json create mode 100644 westech_r2/westech_r2/doctype/customer_record/customer_record.py create mode 100644 westech_r2/westech_r2/doctype/customer_record/test_customer_record.py diff --git a/westech_r2/__pycache__/crm_import.cpython-312.pyc b/westech_r2/__pycache__/crm_import.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..34b6c8f872e094460cd6ea947edad093a3c4031c GIT binary patch literal 6584 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 literal 0 HcmV?d00001 diff --git a/westech_r2/__pycache__/crm_import_fix.cpython-312.pyc b/westech_r2/__pycache__/crm_import_fix.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bafc85c36bce5e11838d3b35167ad93ec0d5a3f4 GIT binary patch literal 6567 zcmbtYeN0WW@%{YS<0t*f<-U;C|S=2L{1D%azU!B4R<>DSHu6a zez38MBqUI?l&Ek>NF=t=qwK5VwDXW$cjQ$?D&4g#CXVs6&D@jnImCz-MP+9s$2SWT`OVszl2MJX| zouDQ9cVeR}^w5ePO;JxH$W{|8J3XrFZUPPgH(JVEi zeiuzszN+})?*w>(V{QfbP;^RQaNIGYQGPNY3d|@Uon*vuj_Dh@#0W8-3$VgC$BCDH zYFQl#L}WS=jd0LTg~QLO?k5<-6B-&>b+AWP^&EVHqwq;jk#gvy&nO#1LMS4Lfk=?! zF}F-(gR)u>d07KZC?;#d(P<9PV&RY|tHWGGHi85%3ezERTvmr7qE9QUZw11#CWgtf zW-=PVPuRe$OwV!wUe@E*51axHXr}mkKG;1To#eVl`9Lhjb#r_y!p(>WKHwt3@opdq z-P4>Pa=~#w-%~jZ@{|71WGu>y{?X7(S8P@`f?0v{W8^#o`tk7+bP&9A=-;X=d8?zK zt4?}9naSyxyw$!mvoP~fJfm+YQH7k&mA6zcg%&~|-I!N@W3?xPtD)u4%8kt4-VJAO z-ch$zWl4G$PUUOrR)?2|Q^A$X^8*FoJNi*P_0C^>nD=&U*qEdyDWv-Ib*|O;a(qQf z(!X*zlj6$BH1+YBH2X06Ao`fiwY`ydxzl^nH`9E&+1j7HN4>(&blsUYA$YTk;(SkZ4Fh;eB*LI7fj%gf82Al--UkWQuVvlL6|E)oJ{c6 zKO2G+x>}KbSmmfw6^DXV!u}{qAW=zW6H2O5G(##PM> zyo-LMSORV(olW4yy!2yfRx?JinlH5E*d261ZWF!_7RZ^8eE!K&(vYyQGQFi;3mw>1tgs)T0o9>{6<17h)| zkGUtBh|Equ;ocdO5qSl^5PvpFa~HAIKT@IqY*GDa$sAOTp`hx0h`5AVGP8E<8cBVN z0>o94d0Dk+p;5w;Fjwrcqavyl?4Z4-QakLvbqpnJlBFLdRqww8`m5o^vlHVC!MoG5 zJ1;SvR9(iR^DD1Rsd~2vU~7s0W+(w)aGa9PZzoz6Jv3YcYLuDIuf$RXt%gXkbrbu@ zc&C4b{rWNG%o8MlK#9CRNLRzIy6OmqKS)>OuDa@pu2-zaMb=Q^&7JeI`wleiBGdR9 zJI%YuDCa{|R{RQ-EiX%xIe1<cie6wMS75KCb(ZK%@8TlgLKR9E!|3_NDtD= zX@0G350N50NHfItD}3f9ycIj!`Wl%%yU2K6BeQoG8LuMq`(#0DISX1PE87-_w=Qce zUU&~wWTAG+3OT2vGUxm;xiYC*yhfuxMneU*14V3ImDnU3f3Sk1yCR3$h`(Sx2Vazd z`^2F}1aCliXC~|w@;#FM#rOE4{k7;Xkv-VyD>AO5Li=IK@uGJ2h-BRjvE*Qn-lMM~ zFFFK;;WT95cj2;6p(MTdVd=Ub1X@s)Fi!)II7BS(LHzdXspOR5)xCHgcqjmm}0 zvCHw+)2B}tE0y8mZJoIgnqk_QKBx^sktvRWr}5fuaWdA8Z}dDDjPk6|H6cVJap$$D z7zi`PYdy=9ADv?2*1jo0gqne8MvClE=>#S@pGww_hPW^bl}9W*6}%C*p9|yKXqG9} z2^@Qhk@Z)nVo4J}zO}M!5 z2SKaIv9cuyiwFc^SS&hZ%V;zVRg!-yR+^i_0YUV~ri!y`@cFIx0EJ_jc=i7SBb!U>_D7~BKLFIV#*!OLcqap)2p+|&2zWp%n}X5FSRgX% z$8M34Oc`Zs87ejnt`%f02eM&7Hpj-HRxE25WL-v&VW zcHBU{k9Z$6vL+H0IYHKg!ATCvI@vlNo#F+56wCxfSYPN{SYKSrFo27Ry8tDDT;5J) zZ{n&`%oBXMh*w{WMwv;#&u}~+! z1{M&)mLYJ6iSmBM^#eqo%6IAszBI;F3=?lEh1p9%B`}fbGz5$)dIL6Iiybiq{vms> zEn$;2B92jCJH#9hg{@4((xCvy@hj8eXn^JMB?lt2Sl-IIF;4Uc!eJ<_Ik7Zjqd{2> zWFFu7d3-4=R1Pswo zgg=P6%*D<2m-m)07XCJcpOBAWtw(Wl3^%*%H-8*IIDwm!(8z{?nIIR#-j=nvdXKQO zCK#Q9F99_&H7e`M8zAe+5tB`1ws)Je(^mhgSig4!}3M;CLW12IQ*Z z;VU0;6+6n~E0?S-r3abfWrGr3{27e#EPk#AWirL_Z^A^t@m2cl@M6p3TLrLAMfgz| z?Zv$R0AEbzP=PjNj0bXb*SA_^v}7!Mvxc^9L}}b>tp&St)wk?Rom<&IultP}XuzW} zW=x*X4yUI-?M-)OjE8b`4^VB+rP~X)GcDcg&UO7)+MM;u+)x2N49u*Yo_iacFtywh z(!E*t!A#4cjIk$29|kFB!#(HymOCwL#`O#9C%!tCYZ%GaUY)yKGGjcRqfg{b=DC5q z!~GQ1XzJ#51xw9RWFeAi?s}}tT29QX^LC&<)uOsTq2{ad_U5O^XzZy6>(#sr|;$fIoB$E)S;8r?t77_I!(Hc@VxR)Yg}Y zbgpJ^iC7CdpVlLjBX6=U4J{0<(epz&(}BFHI{D7RTMGl*J^=UY{YY;v=q*cz1;f|+ z`VD=3PTyG2o0Hn)RB{XuXDr=0{XwwMyyu^7myTOlK!CA1^uXZhWWmfz6RTyJ7XFZ*N$S0#o|z zhWl_*{fQ~x=3DFDXgifu=WO0BPuoM^1K(QTj|V>=%p4i`lF#mcE9<$4mALcFo`;49 zhBfbx+dprA+_8Q>+j%z2yje2)!n5u#d$OGuvdm!VnYZoX@dwA(PG-GFH@$DH+cA~N z*EOzAFHfg#t$esyw|@uC7hjk;fSG+j-C4hwTsAGo5c_J-wTr^Ixj4HTTv_VdF^NTzGqaaLejUITjLW zYSY?Uu+^rtsp(WG(|$hV?918u3pVGfVcGDtje)OjIh*Idwg)KK_HVCINK^Ia!+$pX zHC?ko*W_qtF=X6nU1~Pd(U&py=jefNbP$T>r2d{Gb^Wd@WloP|EFC$$FJE1kQm5=m z*Hc6pn&!_HAcW5+ZzhAuj=#LJtp*x6((G6oUKn1wvT!BSbRcW$nj0uu=vb>=(`AeY zbM&DtXJdN$r{49^pX|$g=erwK*E96>-(18Q;@3EJ{LjL{T73@H9SSM!I?MN zmo6<_N?lF6H<}M+ntC34{_g5}_1|89Jd|lXoim-mUmA~n^n=vMrqvDJ$khA3@UD$M z+PBu6IX3)N>sOA9^HR=s`Co0d|EWPX=D)W!p!KQgIQ{cl8nxm@uE~ xdncppRG2%%hk%A}G6F77!BG_TOJx2fs`?ky{FH8?Y|q}LLKO8qiXVsP{|C-qzvKV_ literal 0 HcmV?d00001 diff --git a/westech_r2/crm_import.py b/westech_r2/crm_import.py new file mode 100644 index 0000000..1625a3a --- /dev/null +++ b/westech_r2/crm_import.py @@ -0,0 +1,129 @@ +import frappe +import json + +def run(): + print("=== CRM Import Status Check ===") + print("Customer Record DocType exists:", bool(frappe.db.exists("DocType", "Customer Record"))) + + if not frappe.db.exists("DocType", "Customer Record"): + print("Customer Record DocType does not exist! Creating it...") + doctype_def = { + "name": "Customer Record", + "doctype": "DocType", + "module": "Westech R2", + "custom": 0, + "autoname": "field:record_number", + "naming_rule": "By fieldname", + "engine": "InnoDB", + "document_type": "Document", + "fields": [ + {"fieldname": "record_number", "fieldtype": "Data", "label": "Record Number", "reqd": 1, "unique": 1, "in_list_view": 1}, + {"fieldname": "company_name", "fieldtype": "Data", "label": "Company Name", "in_list_view": 1}, + {"fieldname": "supplier", "fieldtype": "Link", "label": "Supplier", "options": "Supplier", "in_list_view": 1}, + {"fieldname": "customer_number", "fieldtype": "Data", "label": "Customer Number", "in_list_view": 1}, + {"fieldname": "contact_persons", "fieldtype": "Text", "label": "Contact Persons"}, + {"fieldname": "email_address", "fieldtype": "Text", "label": "Email Addresses"}, + {"fieldname": "phone_numbers", "fieldtype": "Text", "label": "Phone Numbers"}, + {"fieldname": "customer_address", "fieldtype": "Data", "label": "Address"}, + {"fieldname": "city", "fieldtype": "Data", "label": "City"}, + {"fieldname": "state", "fieldtype": "Data", "label": "State"}, + {"fieldname": "zip", "fieldtype": "Data", "label": "Zip"}, + {"fieldname": "date_created", "fieldtype": "Date", "label": "Date Created"}, + {"fieldname": "contacted_date", "fieldtype": "Date", "label": "Contacted Date"}, + {"fieldname": "follow_up_date", "fieldtype": "Date", "label": "Follow Up Date"}, + {"fieldname": "last_pu_date", "fieldtype": "Date", "label": "Last PU Date"}, + {"fieldname": "notes", "fieldtype": "Text", "label": "Notes"}, + {"fieldname": "comments", "fieldtype": "Text", "label": "Comments"}, + {"fieldname": "hours_operation", "fieldtype": "Data", "label": "Hours of Operation"}, + ], + "permissions": [ + {"role": "System Manager", "read": 1, "write": 1, "create": 1, "delete": 1, "export": 1, "print": 1, "share": 1, "email": 1}, + {"role": "All", "read": 1, "write": 1, "create": 1, "delete": 0, "export": 0, "print": 1, "share": 0, "email": 0} + ], + "quick_entry": 0, + "track_changes": 1, + } + doc = frappe.get_doc(doctype_def) + doc.insert(ignore_permissions=True) + frappe.db.commit() + print("Customer Record DocType created!") + + # Check existing count + existing = frappe.db.count("Customer Record") + print(f"Records in Customer Record: {existing}") + + if existing > 0: + print("Data already imported. Skipping.") + return {"status": "already_done", "count": existing} + + # Load parsed data + with open("/tmp/crm_records.json") as f: + records = json.load(f) + + print(f"Parsed records from xlsx: {len(records)}") + + # Get supplier names + supplier_names = set(frappe.get_all("Supplier", fields=["name"], pluck="name")) + print(f"Total suppliers: {len(supplier_names)}") + + # Count matches + matched = sum(1 for r in records if r["record_number"] in supplier_names) + print(f"Records matching suppliers: {matched}") + + print(f"Importing {len(records)} records...") + created = 0 + errors = [] + + for rec in records: + try: + doc = frappe.new_doc("Customer Record") + doc.record_number = rec["record_number"] + doc.company_name = rec.get("company_name") or "" + + # Match supplier + if rec["record_number"] in supplier_names: + doc.supplier = rec["record_number"] + doc.customer_number = rec["record_number"] + + doc.contact_persons = "\n".join(rec.get("contact_persons", [])) + doc.email_address = "\n".join(rec.get("emails", [])) + doc.phone_numbers = "\n".join(rec.get("phone_numbers", [])) + doc.customer_address = rec.get("address") or "" + doc.city = rec.get("city") or "" + doc.state = rec.get("state") or "" + doc.zip = str(rec.get("zip") or "") + + if rec.get("date_created"): + doc.date_created = rec["date_created"] + if rec.get("contacted_date"): + doc.contacted_date = rec["contacted_date"] + if rec.get("follow_up_date"): + doc.follow_up_date = rec["follow_up_date"] + if rec.get("last_pu_date"): + doc.last_pu_date = rec["last_pu_date"] + + doc.notes = rec.get("notes") or "" + doc.comments = rec.get("comments") or "" + doc.hours_operation = rec.get("hours_operation") or "" + + doc.save(ignore_permissions=True) + created += 1 + + if created % 500 == 0: + print(f" ... {created} records imported") + frappe.db.commit() + + except Exception as e: + errors.append(f"{rec['record_number']}: {str(e)}") + if len(errors) > 10: + print(f"Too many errors, stopping. Last error: {e}") + break + + frappe.db.commit() + print(f"\nImport complete: {created} records created") + if errors: + print(f"Errors ({len(errors)}):") + for e in errors[:10]: + print(f" {e}") + + return {"status": "ok", "created": created, "errors": len(errors), "matched": matched} diff --git a/westech_r2/crm_import_fix.py b/westech_r2/crm_import_fix.py new file mode 100644 index 0000000..ebe7a2a --- /dev/null +++ b/westech_r2/crm_import_fix.py @@ -0,0 +1,142 @@ +import frappe +import json +from datetime import datetime + +def parse_date(val): + """Parse various date formats from the CRM spreadsheet""" + if not val: + return None + if isinstance(val, datetime): + return val.date() + if isinstance(val, str): + val = val.strip() + if not val or val.lower() in ['nan', 'none', 'null']: + return None + # Handle format like "9/8/0206" -> year 2006 + if '/' in val: + parts = val.split('/') + if len(parts) == 3: + month, day, year = parts + year = year.strip() + # Fix 4-digit years starting with 02xx -> 20xx + if len(year) == 4 and year.startswith('02'): + year = '20' + year[2:] + # Fix 2-digit years + elif len(year) == 2: + year_int = int(year) + if year_int < 50: + year = '20' + year + else: + year = '19' + year + try: + return datetime(int(year), int(month), int(day)).date() + except: + return None + return None + +def run(): + print("=== CRM Import Fix & Continue ===") + + # Load parsed data + with open("/tmp/crm_records.json") as f: + records = json.load(f) + + print(f"Total parsed records: {len(records)}") + + # Get already imported records + existing = frappe.get_all("Customer Record", fields=["name"], pluck="name") + existing_set = set(existing) + print(f"Already imported: {len(existing_set)}") + + # Get supplier names + supplier_names = set(frappe.get_all("Supplier", fields=["name"], pluck="name")) + + # Fix existing bad records + print("\nFixing existing records with bad dates...") + fixed = 0 + for rec_num in existing_set: + doc = frappe.get_doc("Customer Record", rec_num) + changed = False + + # Check and fix dates + date_fields = ['date_created', 'contacted_date', 'follow_up_date', 'last_pu_date'] + for df in date_fields: + val = doc.get(df) + if val and isinstance(val, str): + parsed = parse_date(val) + if parsed: + doc.set(df, parsed) + changed = True + else: + # Can't parse, clear it + doc.set(df, None) + changed = True + + if changed: + doc.save(ignore_permissions=True) + fixed += 1 + + if fixed: + frappe.db.commit() + print(f"Fixed {fixed} existing records") + + # Import remaining records + remaining = [r for r in records if r["record_number"] not in existing_set] + print(f"\nImporting remaining {len(remaining)} records...") + + created = 0 + errors = [] + + for rec in remaining: + try: + doc = frappe.new_doc("Customer Record") + doc.record_number = rec["record_number"] + doc.company_name = rec.get("company_name") or "" + + # Match supplier + if rec["record_number"] in supplier_names: + doc.supplier = rec["record_number"] + doc.customer_number = rec["record_number"] + + doc.contact_persons = "\n".join(rec.get("contact_persons", [])) + doc.email_address = "\n".join(rec.get("emails", [])) + doc.phone_numbers = "\n".join(rec.get("phone_numbers", [])) + doc.customer_address = rec.get("address") or "" + doc.city = rec.get("city") or "" + doc.state = rec.get("state") or "" + doc.zip = str(rec.get("zip") or "") + + # Parse dates properly + doc.date_created = parse_date(rec.get("date_created")) + doc.contacted_date = parse_date(rec.get("contacted_date")) + doc.follow_up_date = parse_date(rec.get("follow_up_date")) + doc.last_pu_date = parse_date(rec.get("last_pu_date")) + + doc.notes = rec.get("notes") or "" + doc.comments = rec.get("comments") or "" + doc.hours_operation = rec.get("hours_operation") or "" + + doc.save(ignore_permissions=True) + created += 1 + + if created % 500 == 0: + print(f" ... {created} remaining records imported") + frappe.db.commit() + + except Exception as e: + errors.append(f"{rec['record_number']}: {str(e)}") + if len(errors) > 20: + print(f"Too many errors ({len(errors)}), stopping. Last: {e}") + break + + frappe.db.commit() + print(f"\nImport complete: {created} additional records created") + if errors: + print(f"Errors ({len(errors)}):") + for e in errors[:10]: + print(f" {e}") + + total = frappe.db.count("Customer Record") + print(f"Total Customer Records now: {total}") + + return {"status": "ok", "created": created, "errors": len(errors), "total": total} diff --git a/westech_r2/westech_r2/doctype/__init__.py b/westech_r2/westech_r2/doctype/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/westech_r2/westech_r2/doctype/__pycache__/__init__.cpython-312.pyc b/westech_r2/westech_r2/doctype/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d2f66bb7facebf39b443a87d3f3d9ccef489aaf1 GIT binary patch literal 181 zcmX@j%ge<81Vx5?Ss?l`h(HIQS%4zb87dhx8U0o=6fpsLpFwJVh3IGG=cekX6(trF zr0S;@738H>l;|d<<|Sw71BHt9%TtR>Qj;^{i;QqEQ}UBbDuH_A<1_OzOXB183Mzkb f*yQG?l;)(`6|n*>VFco05aS~=BO_xGGmr%U5%Mno literal 0 HcmV?d00001 diff --git a/westech_r2/westech_r2/doctype/customer_record/__init__.py b/westech_r2/westech_r2/doctype/customer_record/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/westech_r2/westech_r2/doctype/customer_record/__pycache__/__init__.cpython-312.pyc b/westech_r2/westech_r2/doctype/customer_record/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..00f58a235a1a8043515e0b32ce78dda13aa8d2ba GIT binary patch literal 197 zcmX@j%ge<81Vx5?Ss?l`h(HIQS%4zb87dhx8U0o=6fpsLpFwJVrRZnm=cekX6(trF zr0S;@738H>l;|d<<|Sw71BHt9%TtR>Qj;^{i;QqEQ}UBbDuH^FON&c@Mis>ur6%VW urRc}UXXa&=#K-FuRQ}?y$<0qG%}KQ@Vg*{t2*kx8#z$sGM#ds$APWG!{xyyO literal 0 HcmV?d00001 diff --git a/westech_r2/westech_r2/doctype/customer_record/__pycache__/customer_record.cpython-312.pyc b/westech_r2/westech_r2/doctype/customer_record/__pycache__/customer_record.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..241c131be24f52068f31e5192b7656cb9496257f GIT binary patch literal 446 zcmZ`#yH3ME5Ztv%WI~b-J^{%>*n%3-fJ8?@h1{IX-J%Hm;%p1j=L6{S4T$fc;s+wP zRCGutO)B<$ra@why_vn6)!e*KCL`d|N7K7EK0gfktw9gkeFjTVpfG_HBSdiE0+h1= zUa^qppUR+m+tlD;CDYjx?n zjVDAFrzAu(5+co2ofte8;-QWcd-DRV*}1t7XRHlxbzBq_k}fiOst#``le3UVW%x*C zMRF$e@jp=IvYNA2_BMp>eEj7>G1n%Uu6Ybf!xpf@fi_uob7OwjdI{J!B>`{m;?O27 qyI|X(PO`&j^* literal 0 HcmV?d00001 diff --git a/westech_r2/westech_r2/doctype/customer_record/customer_record.js b/westech_r2/westech_r2/doctype/customer_record/customer_record.js new file mode 100644 index 0000000..e9c44e0 --- /dev/null +++ b/westech_r2/westech_r2/doctype/customer_record/customer_record.js @@ -0,0 +1,8 @@ +// Copyright (c) 2026, Westech and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Customer Record", { +// refresh(frm) { + +// }, +// }); diff --git a/westech_r2/westech_r2/doctype/customer_record/customer_record.json b/westech_r2/westech_r2/doctype/customer_record/customer_record.json new file mode 100644 index 0000000..9141732 --- /dev/null +++ b/westech_r2/westech_r2/doctype/customer_record/customer_record.json @@ -0,0 +1,158 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:record_number", + "creation": "2026-05-20 15:10:58.374067", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "record_number", + "company_name", + "supplier", + "customer_number", + "contact_persons", + "email_address", + "phone_numbers", + "customer_address", + "city", + "state", + "zip", + "date_created", + "contacted_date", + "follow_up_date", + "last_pu_date", + "notes", + "comments", + "hours_operation" + ], + "fields": [ + { + "fieldname": "record_number", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Record Number", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "company_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Company Name" + }, + { + "fieldname": "supplier", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Supplier", + "options": "Supplier" + }, + { + "fieldname": "customer_number", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Customer Number" + }, + { + "fieldname": "contact_persons", + "fieldtype": "Text", + "label": "Contact Persons" + }, + { + "fieldname": "email_address", + "fieldtype": "Text", + "label": "Email Addresses" + }, + { + "fieldname": "phone_numbers", + "fieldtype": "Text", + "label": "Phone Numbers" + }, + { + "fieldname": "customer_address", + "fieldtype": "Data", + "label": "Address" + }, + { + "fieldname": "city", + "fieldtype": "Data", + "label": "City" + }, + { + "fieldname": "state", + "fieldtype": "Data", + "label": "State" + }, + { + "fieldname": "zip", + "fieldtype": "Data", + "label": "Zip" + }, + { + "fieldname": "date_created", + "fieldtype": "Date", + "label": "Date Created" + }, + { + "fieldname": "contacted_date", + "fieldtype": "Date", + "label": "Contacted Date" + }, + { + "fieldname": "follow_up_date", + "fieldtype": "Date", + "label": "Follow Up Date" + }, + { + "fieldname": "last_pu_date", + "fieldtype": "Date", + "label": "Last PU Date" + }, + { + "fieldname": "notes", + "fieldtype": "Text", + "label": "Notes" + }, + { + "fieldname": "comments", + "fieldtype": "Text", + "label": "Comments" + }, + { + "fieldname": "hours_operation", + "fieldtype": "Data", + "label": "Hours of Operation" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2026-05-20 15:10:58.374067", + "modified_by": "Administrator", + "module": "Westech R2", + "name": "Customer Record", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/westech_r2/westech_r2/doctype/customer_record/customer_record.py b/westech_r2/westech_r2/doctype/customer_record/customer_record.py new file mode 100644 index 0000000..d1bb1ea --- /dev/null +++ b/westech_r2/westech_r2/doctype/customer_record/customer_record.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, Westech and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class CustomerRecord(Document): + pass diff --git a/westech_r2/westech_r2/doctype/customer_record/test_customer_record.py b/westech_r2/westech_r2/doctype/customer_record/test_customer_record.py new file mode 100644 index 0000000..6f327c6 --- /dev/null +++ b/westech_r2/westech_r2/doctype/customer_record/test_customer_record.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, Westech and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestCustomerRecord(FrappeTestCase): + pass