rpa-erpnext 1.0.0__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.0/LICENSE +21 -0
- rpa_erpnext-1.0.0/PKG-INFO +15 -0
- rpa_erpnext-1.0.0/RPA/ERPNext.py +253 -0
- rpa_erpnext-1.0.0/RPA/init.py +6 -0
- rpa_erpnext-1.0.0/pyproject.toml +23 -0
- rpa_erpnext-1.0.0/rpa_erpnext.egg-info/PKG-INFO +15 -0
- rpa_erpnext-1.0.0/rpa_erpnext.egg-info/SOURCES.txt +9 -0
- rpa_erpnext-1.0.0/rpa_erpnext.egg-info/dependency_links.txt +1 -0
- rpa_erpnext-1.0.0/rpa_erpnext.egg-info/requires.txt +2 -0
- rpa_erpnext-1.0.0/rpa_erpnext.egg-info/top_level.txt +1 -0
- rpa_erpnext-1.0.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Mahara
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rpa-erpnext
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Robot Framework library for automating ERPNext using REST API
|
|
5
|
+
Author-email: Your Name <youremail@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/yourusername/rpa-erpnext
|
|
8
|
+
Project-URL: Source, https://github.com/yourusername/rpa-erpnext
|
|
9
|
+
Keywords: robotframework,erpnext,rpa,automation
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: requests>=2.28.0
|
|
14
|
+
Requires-Dist: robotframework>=6.0
|
|
15
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import requests
|
|
4
|
+
from robot.api.deco import keyword, library
|
|
5
|
+
@library(scope='GLOBAL', auto_keywords=False)
|
|
6
|
+
class ERPNextLibrary:
|
|
7
|
+
def __init__(self, base_url, api_key, api_secret):
|
|
8
|
+
self.base_url = base_url.rstrip("/")
|
|
9
|
+
self.session = requests.Session()
|
|
10
|
+
self.session.auth = (api_key, api_secret)
|
|
11
|
+
|
|
12
|
+
# ===========================================================
|
|
13
|
+
# Utility
|
|
14
|
+
# ===========================================================
|
|
15
|
+
|
|
16
|
+
def create_resource(self, doctype, data):
|
|
17
|
+
res = self.session.post(f"{self.base_url}/api/resource/{doctype}", json=data)
|
|
18
|
+
if not res.ok:
|
|
19
|
+
raise Exception(f"HTTP {res.status_code}: {res.text}")
|
|
20
|
+
return res.json().get("data", res.json())
|
|
21
|
+
|
|
22
|
+
def get_doc(self, doctype, name):
|
|
23
|
+
res = self.session.get(f"{self.base_url}/api/resource/{doctype}/{name}")
|
|
24
|
+
res.raise_for_status()
|
|
25
|
+
return res.json().get("data")
|
|
26
|
+
|
|
27
|
+
def exists(self, doctype, name):
|
|
28
|
+
"""Kiểm tra xem tài nguyên có tồn tại hay không."""
|
|
29
|
+
res = self.session.get(f"{self.base_url}/api/resource/{doctype}/{name}")
|
|
30
|
+
return res.ok
|
|
31
|
+
|
|
32
|
+
# ===========================================================
|
|
33
|
+
# Ensure Hàm nền tảng
|
|
34
|
+
# ===========================================================
|
|
35
|
+
|
|
36
|
+
@keyword("Ensure Company Exist")
|
|
37
|
+
def ensure_company_exist(self, company: str, abbr: str):
|
|
38
|
+
"""Đảm bảo Company tồn tại."""
|
|
39
|
+
if self.exists("Company", company):
|
|
40
|
+
return {"status": "exists", "name": company}
|
|
41
|
+
|
|
42
|
+
data = {
|
|
43
|
+
"doctype": "Company",
|
|
44
|
+
"company_name": company,
|
|
45
|
+
"abbr": abbr,
|
|
46
|
+
"default_currency": "VND",
|
|
47
|
+
"country": "Vietnam",
|
|
48
|
+
"company_logo": None
|
|
49
|
+
}
|
|
50
|
+
return self.create_resource("Company", data)
|
|
51
|
+
|
|
52
|
+
@keyword("Ensure Warehouse Exist")
|
|
53
|
+
def ensure_warehouse_exist(self, warehouse: str, company: str):
|
|
54
|
+
"""Đảm bảo Warehouse tồn tại."""
|
|
55
|
+
if self.exists("Warehouse", warehouse):
|
|
56
|
+
return {"status": "exists", "name": warehouse}
|
|
57
|
+
|
|
58
|
+
data = {
|
|
59
|
+
"doctype": "Warehouse",
|
|
60
|
+
"warehouse_name": warehouse,
|
|
61
|
+
"company": company,
|
|
62
|
+
}
|
|
63
|
+
return self.create_resource("Warehouse", data)
|
|
64
|
+
|
|
65
|
+
@keyword("Ensure Supplier Exist")
|
|
66
|
+
def ensure_supplier_exist(self, supplier_name: str):
|
|
67
|
+
"""Đảm bảo Supplier tồn tại."""
|
|
68
|
+
if self.exists("Supplier", supplier_name):
|
|
69
|
+
return {"status": "exists", "name": supplier_name}
|
|
70
|
+
|
|
71
|
+
data = {
|
|
72
|
+
"doctype": "Supplier",
|
|
73
|
+
"supplier_name": supplier_name,
|
|
74
|
+
"supplier_type": "Company",
|
|
75
|
+
"country": "Vietnam",
|
|
76
|
+
}
|
|
77
|
+
return self.create_resource("Supplier", data)
|
|
78
|
+
|
|
79
|
+
@keyword("Ensure Items Exist")
|
|
80
|
+
def ensure_items_exist(self, items_json):
|
|
81
|
+
"""Đảm bảo tất cả item tồn tại."""
|
|
82
|
+
if isinstance(items_json, str):
|
|
83
|
+
items = json.loads(items_json)
|
|
84
|
+
else:
|
|
85
|
+
items = items_json
|
|
86
|
+
|
|
87
|
+
created_items = []
|
|
88
|
+
for i in items:
|
|
89
|
+
code = i["item_code"]
|
|
90
|
+
if self.exists("Item", code):
|
|
91
|
+
created_items.append({"status": "exists", "item_code": code})
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
data = {
|
|
95
|
+
"doctype": "Item",
|
|
96
|
+
"item_code": code,
|
|
97
|
+
"item_name": code,
|
|
98
|
+
"description": i.get("description", code),
|
|
99
|
+
"stock_uom": "Nos",
|
|
100
|
+
"is_stock_item": 1,
|
|
101
|
+
"item_group": "All Item Groups",
|
|
102
|
+
}
|
|
103
|
+
res = self.create_resource("Item", data)
|
|
104
|
+
created_items.append(res)
|
|
105
|
+
return created_items
|
|
106
|
+
|
|
107
|
+
# ===========================================================
|
|
108
|
+
# Step 1: Đọc file Excel & tạo Material Request
|
|
109
|
+
# ===========================================================
|
|
110
|
+
|
|
111
|
+
@keyword("Load Excel Request")
|
|
112
|
+
def load_excel_request(self, path: str):
|
|
113
|
+
"""Đọc file Excel chứa company, itemcode, quantity."""
|
|
114
|
+
df = pd.read_excel(path)
|
|
115
|
+
return df.to_dict(orient="records")
|
|
116
|
+
|
|
117
|
+
@keyword("Create Material Request From Excel")
|
|
118
|
+
def create_material_request_from_excel(self, path: str, schedule_date: str):
|
|
119
|
+
"""Tạo Material Request từ file Excel."""
|
|
120
|
+
rows = self.load_excel_request(path)
|
|
121
|
+
if not rows:
|
|
122
|
+
raise Exception("Excel file không có dữ liệu.")
|
|
123
|
+
|
|
124
|
+
company = rows[0]["company"]
|
|
125
|
+
self.ensure_company_exist(company, "EDU")
|
|
126
|
+
self.ensure_warehouse_exist(f"Stores - EDU", company)
|
|
127
|
+
|
|
128
|
+
items = []
|
|
129
|
+
for row in rows:
|
|
130
|
+
self.ensure_items_exist([{
|
|
131
|
+
"item_code": row["itemcode"],
|
|
132
|
+
"description": row["itemcode"]
|
|
133
|
+
}])
|
|
134
|
+
items.append({
|
|
135
|
+
"item_code": row["itemcode"],
|
|
136
|
+
"description": row["itemcode"],
|
|
137
|
+
"qty": float(row["quantity"]),
|
|
138
|
+
"schedule_date": schedule_date,
|
|
139
|
+
"warehouse": f"Stores - EDU",
|
|
140
|
+
"uom": "Nos",
|
|
141
|
+
"stock_uom": "Nos",
|
|
142
|
+
"conversion_factor": 1.0
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
data = {
|
|
146
|
+
"doctype": "Material Request",
|
|
147
|
+
"material_request_type": "Purchase",
|
|
148
|
+
"company": company,
|
|
149
|
+
"schedule_date": schedule_date,
|
|
150
|
+
"items": items
|
|
151
|
+
}
|
|
152
|
+
return self.create_resource("Material Request", data)
|
|
153
|
+
|
|
154
|
+
# ===========================================================
|
|
155
|
+
# Step 2: Gửi RFQ
|
|
156
|
+
# ===========================================================
|
|
157
|
+
|
|
158
|
+
@keyword("Send RFQ From Material Request")
|
|
159
|
+
def send_rfq_from_mr(self, mr_name: str, suppliers: list):
|
|
160
|
+
"""Tạo RFQ dựa trên Material Request."""
|
|
161
|
+
mr_doc = self.get_doc("Material Request", mr_name)
|
|
162
|
+
company = mr_doc["company"]
|
|
163
|
+
self.ensure_warehouse_exist(f"Stores - EDU", company)
|
|
164
|
+
for s in suppliers:
|
|
165
|
+
self.ensure_supplier_exist(s)
|
|
166
|
+
|
|
167
|
+
items = []
|
|
168
|
+
for d in mr_doc["items"]:
|
|
169
|
+
items.append({
|
|
170
|
+
"item_code": d["item_code"],
|
|
171
|
+
"qty": d["qty"],
|
|
172
|
+
"uom": d.get("uom", "Nos"),
|
|
173
|
+
"stock_uom": d.get("stock_uom", "Nos"),
|
|
174
|
+
"conversion_factor": 1.0,
|
|
175
|
+
"warehouse": d["warehouse"],
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
rfq_data = {
|
|
179
|
+
"doctype": "Request for Quotation",
|
|
180
|
+
"company": company,
|
|
181
|
+
"transaction_date": "2025-10-28",
|
|
182
|
+
"suppliers": [{"supplier": s} for s in suppliers],
|
|
183
|
+
"items": items,
|
|
184
|
+
"message_for_supplier": "Xin vui lòng gửi báo giá."
|
|
185
|
+
}
|
|
186
|
+
return self.create_resource("Request for Quotation", rfq_data)
|
|
187
|
+
|
|
188
|
+
# ===========================================================
|
|
189
|
+
# Step 3: Supplier gửi báo giá
|
|
190
|
+
# ===========================================================
|
|
191
|
+
|
|
192
|
+
@keyword("Receive Supplier Quotation")
|
|
193
|
+
def receive_supplier_quotation(self, rfq_name: str, supplier_name: str, quotation_data: list):
|
|
194
|
+
rfq_doc = self.get_doc("Request for Quotation", rfq_name)
|
|
195
|
+
company = rfq_doc["company"]
|
|
196
|
+
self.ensure_supplier_exist(supplier_name)
|
|
197
|
+
|
|
198
|
+
sq_data = {
|
|
199
|
+
"doctype": "Supplier Quotation",
|
|
200
|
+
"supplier": supplier_name,
|
|
201
|
+
"company": company,
|
|
202
|
+
"transaction_date": "2025-10-28",
|
|
203
|
+
"items": [],
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
for q in quotation_data:
|
|
207
|
+
sq_data["items"].append({
|
|
208
|
+
"item_code": q["item_code"],
|
|
209
|
+
"qty": q["qty"],
|
|
210
|
+
"rate": q["rate"],
|
|
211
|
+
"uom": "Nos",
|
|
212
|
+
"stock_uom": "Nos",
|
|
213
|
+
"conversion_factor": 1.0,
|
|
214
|
+
"warehouse": f"Stores - EDU"
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
return self.create_resource("Supplier Quotation", sq_data)
|
|
218
|
+
|
|
219
|
+
# ===========================================================
|
|
220
|
+
# Step 4: Tạo Purchase Order
|
|
221
|
+
# ===========================================================
|
|
222
|
+
|
|
223
|
+
@keyword("Create Purchase Order From Quotation")
|
|
224
|
+
def create_purchase_order_from_quotation(self, quotation_name: str):
|
|
225
|
+
sq_doc = self.get_doc("Supplier Quotation", quotation_name)
|
|
226
|
+
supplier = sq_doc["supplier"]
|
|
227
|
+
company = sq_doc["company"]
|
|
228
|
+
|
|
229
|
+
self.ensure_supplier_exist(supplier)
|
|
230
|
+
self.ensure_warehouse_exist(f"Stores - EDU", company)
|
|
231
|
+
|
|
232
|
+
items = []
|
|
233
|
+
for d in sq_doc["items"]:
|
|
234
|
+
items.append({
|
|
235
|
+
"item_code": d["item_code"],
|
|
236
|
+
"qty": d["qty"],
|
|
237
|
+
"rate": d["rate"],
|
|
238
|
+
"schedule_date": "2025-11-05",
|
|
239
|
+
"warehouse": f"Stores - EDU",
|
|
240
|
+
"uom": d.get("uom", "Nos"),
|
|
241
|
+
"conversion_factor": 1.0
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
po_data = {
|
|
245
|
+
"doctype": "Purchase Order",
|
|
246
|
+
"supplier": supplier,
|
|
247
|
+
"company": company,
|
|
248
|
+
"schedule_date": "2025-11-05",
|
|
249
|
+
"items": items
|
|
250
|
+
}
|
|
251
|
+
return self.create_resource("Purchase Order", po_data)
|
|
252
|
+
|
|
253
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "rpa-erpnext"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Robot Framework library for automating ERPNext using REST API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [
|
|
13
|
+
{ name="Your Name", email="youremail@example.com" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["robotframework", "erpnext", "rpa", "automation"]
|
|
16
|
+
dependencies = [
|
|
17
|
+
"requests>=2.28.0",
|
|
18
|
+
"robotframework>=6.0"
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://github.com/yourusername/rpa-erpnext"
|
|
23
|
+
Source = "https://github.com/yourusername/rpa-erpnext"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rpa-erpnext
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Robot Framework library for automating ERPNext using REST API
|
|
5
|
+
Author-email: Your Name <youremail@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/yourusername/rpa-erpnext
|
|
8
|
+
Project-URL: Source, https://github.com/yourusername/rpa-erpnext
|
|
9
|
+
Keywords: robotframework,erpnext,rpa,automation
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: requests>=2.28.0
|
|
14
|
+
Requires-Dist: robotframework>=6.0
|
|
15
|
+
Dynamic: license-file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
RPA
|