rpa-erpnext 1.0.1__tar.gz → 1.0.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rpa_erpnext-1.0.1 → rpa_erpnext-1.0.4}/PKG-INFO +1 -1
- {rpa_erpnext-1.0.1 → rpa_erpnext-1.0.4}/RPA/ERPNext.py +115 -24
- {rpa_erpnext-1.0.1 → rpa_erpnext-1.0.4}/pyproject.toml +1 -1
- {rpa_erpnext-1.0.1 → rpa_erpnext-1.0.4}/rpa_erpnext.egg-info/PKG-INFO +1 -1
- {rpa_erpnext-1.0.1 → rpa_erpnext-1.0.4}/LICENSE +0 -0
- {rpa_erpnext-1.0.1 → rpa_erpnext-1.0.4}/RPA/__init__.py +0 -0
- {rpa_erpnext-1.0.1 → rpa_erpnext-1.0.4}/readme.md +0 -0
- {rpa_erpnext-1.0.1 → rpa_erpnext-1.0.4}/rpa_erpnext.egg-info/SOURCES.txt +0 -0
- {rpa_erpnext-1.0.1 → rpa_erpnext-1.0.4}/rpa_erpnext.egg-info/dependency_links.txt +0 -0
- {rpa_erpnext-1.0.1 → rpa_erpnext-1.0.4}/rpa_erpnext.egg-info/requires.txt +0 -0
- {rpa_erpnext-1.0.1 → rpa_erpnext-1.0.4}/rpa_erpnext.egg-info/top_level.txt +0 -0
- {rpa_erpnext-1.0.1 → rpa_erpnext-1.0.4}/setup.cfg +0 -0
- {rpa_erpnext-1.0.1 → rpa_erpnext-1.0.4}/test/test_functions.py +0 -0
|
@@ -41,37 +41,72 @@ class ERPNextLibrary:
|
|
|
41
41
|
"""
|
|
42
42
|
Thiết lập kết nối với ERPNext từ file credential JSON.
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
Args:
|
|
45
|
+
token_file_path: Đường dẫn đến file token JSON
|
|
46
|
+
|
|
47
|
+
Format 1 (Standard ERPNext):
|
|
46
48
|
{
|
|
47
49
|
"base_url": "...",
|
|
48
50
|
"api_key": "...",
|
|
49
51
|
"api_secret": "..."
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
2
|
|
54
|
+
Format 2 (Google-like / Moodle structure):
|
|
53
55
|
{
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
+
"access_token": "http://erpnext.example.com", # Mapping base_url here
|
|
57
|
+
"refresh_token": "key:secret" # Mapping credentials here
|
|
56
58
|
}
|
|
57
59
|
"""
|
|
60
|
+
if not token_file_path:
|
|
61
|
+
raise Exception("Token file path is required")
|
|
62
|
+
|
|
63
|
+
# Fix for potential argument naming issue from Robot Framework (connection=...)
|
|
64
|
+
if token_file_path.startswith("connection="):
|
|
65
|
+
token_file_path = token_file_path.split("=", 1)[1]
|
|
66
|
+
|
|
58
67
|
with open(token_file_path, 'r', encoding='utf-8') as f:
|
|
59
68
|
data = json.load(f)
|
|
60
69
|
|
|
61
|
-
base_url =
|
|
62
|
-
api_key =
|
|
63
|
-
api_secret =
|
|
64
|
-
access_token =
|
|
70
|
+
base_url = None
|
|
71
|
+
api_key = None
|
|
72
|
+
api_secret = None
|
|
73
|
+
access_token = None
|
|
74
|
+
|
|
75
|
+
# Logic to parse different formats
|
|
76
|
+
if 'access_token' in data and 'refresh_token' in data:
|
|
77
|
+
# Format 2: Google-like structure
|
|
78
|
+
# access_token field holds the URL
|
|
79
|
+
# refresh_token field holds the credentials (key:secret)
|
|
80
|
+
base_url = data.get('access_token')
|
|
81
|
+
creds = data.get('refresh_token')
|
|
82
|
+
|
|
83
|
+
if ":" in creds:
|
|
84
|
+
parts = creds.split(":")
|
|
85
|
+
api_key = parts[0]
|
|
86
|
+
api_secret = parts[1]
|
|
87
|
+
else:
|
|
88
|
+
# If no colon, assume it's a bearer token? Or just raw key?
|
|
89
|
+
# For consistency with user request "y chang moodle", we assume this structure.
|
|
90
|
+
# But ERPNext needs key:secret. If it's a single string, maybe it's bearer?
|
|
91
|
+
# Let's support bearer if it's not key:secret
|
|
92
|
+
access_token = creds
|
|
93
|
+
|
|
94
|
+
else:
|
|
95
|
+
# Format 1: Standard
|
|
96
|
+
base_url = data.get("base_url")
|
|
97
|
+
api_key = data.get("api_key")
|
|
98
|
+
api_secret = data.get("api_secret")
|
|
99
|
+
access_token = data.get("access_token") # Real OAuth2 access token
|
|
65
100
|
|
|
66
101
|
if not base_url:
|
|
67
|
-
raise Exception("
|
|
102
|
+
raise Exception("Could not find base_url in token file")
|
|
68
103
|
|
|
69
104
|
if access_token:
|
|
70
105
|
return self.connect(base_url, access_token=access_token)
|
|
71
106
|
elif api_key and api_secret:
|
|
72
107
|
return self.connect(base_url, api_key=api_key, api_secret=api_secret)
|
|
73
108
|
else:
|
|
74
|
-
raise Exception("Token file must contain credentials (api_key+api_secret OR access_token)")
|
|
109
|
+
raise Exception("Token file must contain credentials (api_key+api_secret OR access_token/refresh_token)")
|
|
75
110
|
|
|
76
111
|
# ===========================================================
|
|
77
112
|
# Utility Methods
|
|
@@ -297,6 +332,9 @@ class ERPNextLibrary:
|
|
|
297
332
|
"""
|
|
298
333
|
self.ensure_customer_exist(customer)
|
|
299
334
|
|
|
335
|
+
if isinstance(items, str):
|
|
336
|
+
items = json.loads(items)
|
|
337
|
+
|
|
300
338
|
order_items = []
|
|
301
339
|
for item in items:
|
|
302
340
|
order_items.append({
|
|
@@ -331,6 +369,9 @@ class ERPNextLibrary:
|
|
|
331
369
|
"""
|
|
332
370
|
self.ensure_customer_exist(customer)
|
|
333
371
|
|
|
372
|
+
if isinstance(items, str):
|
|
373
|
+
items = json.loads(items)
|
|
374
|
+
|
|
334
375
|
invoice_items = []
|
|
335
376
|
for item in items:
|
|
336
377
|
invoice_items.append({
|
|
@@ -417,6 +458,9 @@ class ERPNextLibrary:
|
|
|
417
458
|
"""
|
|
418
459
|
self.ensure_supplier_exist(supplier)
|
|
419
460
|
|
|
461
|
+
if isinstance(items, str):
|
|
462
|
+
items = json.loads(items)
|
|
463
|
+
|
|
420
464
|
receipt_items = []
|
|
421
465
|
for item in items:
|
|
422
466
|
receipt_items.append({
|
|
@@ -438,6 +482,8 @@ class ERPNextLibrary:
|
|
|
438
482
|
|
|
439
483
|
return self.create_resource("Purchase Receipt", data)
|
|
440
484
|
|
|
485
|
+
|
|
486
|
+
|
|
441
487
|
@keyword("Create Purchase Order")
|
|
442
488
|
def create_purchase_order(self, supplier: str, items: list, company: str, schedule_date=None):
|
|
443
489
|
"""
|
|
@@ -453,6 +499,9 @@ class ERPNextLibrary:
|
|
|
453
499
|
"""
|
|
454
500
|
self.ensure_supplier_exist(supplier)
|
|
455
501
|
|
|
502
|
+
if isinstance(items, str):
|
|
503
|
+
items = json.loads(items)
|
|
504
|
+
|
|
456
505
|
po_items = []
|
|
457
506
|
for item in items:
|
|
458
507
|
po_items.append({
|
|
@@ -601,6 +650,9 @@ class ERPNextLibrary:
|
|
|
601
650
|
@keyword("Send RFQ From Material Request")
|
|
602
651
|
def send_rfq_from_mr(self, mr_name: str, suppliers: list):
|
|
603
652
|
"""Tạo RFQ dựa trên Material Request."""
|
|
653
|
+
if isinstance(suppliers, str):
|
|
654
|
+
suppliers = json.loads(suppliers)
|
|
655
|
+
|
|
604
656
|
mr_doc = self.get_doc("Material Request", mr_name)
|
|
605
657
|
company = mr_doc["company"]
|
|
606
658
|
self.ensure_warehouse_exist(f"Stores - EDU", company)
|
|
@@ -633,30 +685,69 @@ class ERPNextLibrary:
|
|
|
633
685
|
# ===========================================================
|
|
634
686
|
|
|
635
687
|
@keyword("Receive Supplier Quotation")
|
|
636
|
-
def receive_supplier_quotation(self, rfq_name: str, supplier_name: str, quotation_data
|
|
688
|
+
def receive_supplier_quotation(self, rfq_name: str, supplier_name: str, quotation_data):
|
|
689
|
+
"""
|
|
690
|
+
Nhận báo giá từ Supplier.
|
|
691
|
+
|
|
692
|
+
Args:
|
|
693
|
+
rfq_name: Mã Request for Quotation
|
|
694
|
+
supplier_name: Tên nhà cung cấp
|
|
695
|
+
quotation_data:
|
|
696
|
+
- Đường dẫn file Excel (.xlsx) chứa các cột: item_code, qty, rate
|
|
697
|
+
- Hoặc List/JSON string chứa thông tin items
|
|
698
|
+
"""
|
|
637
699
|
rfq_doc = self.get_doc("Request for Quotation", rfq_name)
|
|
638
700
|
company = rfq_doc["company"]
|
|
639
701
|
self.ensure_supplier_exist(supplier_name)
|
|
702
|
+
|
|
703
|
+
items_list = []
|
|
704
|
+
|
|
705
|
+
# Case 1: Input is Excel file path
|
|
706
|
+
if isinstance(quotation_data, str) and (quotation_data.endswith('.xlsx') or quotation_data.endswith('.xls')):
|
|
707
|
+
df = pd.read_excel(quotation_data)
|
|
708
|
+
# Clean column names (optional: lowercase)
|
|
709
|
+
df.columns = [c.lower().strip() for c in df.columns]
|
|
710
|
+
|
|
711
|
+
for _, row in df.iterrows():
|
|
712
|
+
# Flexible column mapping
|
|
713
|
+
item_code = row.get("item_code") or row.get("itemcode")
|
|
714
|
+
qty = row.get("qty") or row.get("quantity")
|
|
715
|
+
rate = row.get("rate") or row.get("price") or 0
|
|
716
|
+
|
|
717
|
+
items_list.append({
|
|
718
|
+
"item_code": item_code,
|
|
719
|
+
"qty": float(qty),
|
|
720
|
+
"rate": float(rate),
|
|
721
|
+
"uom": "Nos",
|
|
722
|
+
"stock_uom": "Nos",
|
|
723
|
+
"conversion_factor": 1.0,
|
|
724
|
+
"warehouse": f"Stores - EDU"
|
|
725
|
+
})
|
|
726
|
+
|
|
727
|
+
# Case 2: Input is List or JSON String
|
|
728
|
+
else:
|
|
729
|
+
if isinstance(quotation_data, str):
|
|
730
|
+
quotation_data = json.loads(quotation_data)
|
|
731
|
+
|
|
732
|
+
for q in quotation_data:
|
|
733
|
+
items_list.append({
|
|
734
|
+
"item_code": q["item_code"],
|
|
735
|
+
"qty": q["qty"],
|
|
736
|
+
"rate": q["rate"],
|
|
737
|
+
"uom": "Nos",
|
|
738
|
+
"stock_uom": "Nos",
|
|
739
|
+
"conversion_factor": 1.0,
|
|
740
|
+
"warehouse": f"Stores - EDU"
|
|
741
|
+
})
|
|
640
742
|
|
|
641
743
|
sq_data = {
|
|
642
744
|
"doctype": "Supplier Quotation",
|
|
643
745
|
"supplier": supplier_name,
|
|
644
746
|
"company": company,
|
|
645
747
|
"transaction_date": "2026-01-28",
|
|
646
|
-
"items":
|
|
748
|
+
"items": items_list,
|
|
647
749
|
}
|
|
648
750
|
|
|
649
|
-
for q in quotation_data:
|
|
650
|
-
sq_data["items"].append({
|
|
651
|
-
"item_code": q["item_code"],
|
|
652
|
-
"qty": q["qty"],
|
|
653
|
-
"rate": q["rate"],
|
|
654
|
-
"uom": "Nos",
|
|
655
|
-
"stock_uom": "Nos",
|
|
656
|
-
"conversion_factor": 1.0,
|
|
657
|
-
"warehouse": f"Stores - EDU"
|
|
658
|
-
})
|
|
659
|
-
|
|
660
751
|
return self.create_resource("Supplier Quotation", sq_data)
|
|
661
752
|
|
|
662
753
|
# ===========================================================
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|