rpa-erpnext 1.0.1__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/LICENSE +21 -0
- rpa_erpnext-1.0.1/PKG-INFO +27 -0
- rpa_erpnext-1.0.1/RPA/ERPNext.py +696 -0
- rpa_erpnext-1.0.1/RPA/__init__.py +5 -0
- rpa_erpnext-1.0.1/pyproject.toml +30 -0
- rpa_erpnext-1.0.1/readme.md +9 -0
- rpa_erpnext-1.0.1/rpa_erpnext.egg-info/PKG-INFO +27 -0
- rpa_erpnext-1.0.1/rpa_erpnext.egg-info/SOURCES.txt +11 -0
- rpa_erpnext-1.0.1/rpa_erpnext.egg-info/dependency_links.txt +1 -0
- rpa_erpnext-1.0.1/rpa_erpnext.egg-info/requires.txt +4 -0
- rpa_erpnext-1.0.1/rpa_erpnext.egg-info/top_level.txt +1 -0
- rpa_erpnext-1.0.1/setup.cfg +4 -0
- rpa_erpnext-1.0.1/test/test_functions.py +77 -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,27 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rpa-erpnext
|
|
3
|
+
Version: 1.0.1
|
|
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
|
+
Requires-Dist: pandas>=2.0.0
|
|
16
|
+
Requires-Dist: openpyxl>=3.1.0
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# RPA.ERPNext
|
|
20
|
+
|
|
21
|
+
Library for automating [ERPNext](https://erpnext.com/) using REST API, built for [Robot Framework](https://robotframework.org/).
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install rpa-erpnext
|
|
27
|
+
```
|
|
@@ -0,0 +1,696 @@
|
|
|
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=None, api_key=None, api_secret=None):
|
|
8
|
+
self.base_url = None
|
|
9
|
+
self.session = None
|
|
10
|
+
if base_url and api_key and api_secret:
|
|
11
|
+
self.connect(base_url, api_key, api_secret)
|
|
12
|
+
|
|
13
|
+
def connect(self, base_url, api_key=None, api_secret=None, access_token=None):
|
|
14
|
+
"""
|
|
15
|
+
Kết nối tới ERPNext server.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
base_url: URL của ERPNext server
|
|
19
|
+
api_key: API Key (cho Token Auth)
|
|
20
|
+
api_secret: API Secret (cho Token Auth)
|
|
21
|
+
access_token: OAuth2 Access Token (Bearer Auth)
|
|
22
|
+
"""
|
|
23
|
+
self.base_url = base_url.rstrip("/")
|
|
24
|
+
self.session = requests.Session()
|
|
25
|
+
|
|
26
|
+
headers = {
|
|
27
|
+
"Accept": "application/json",
|
|
28
|
+
"Content-Type": "application/json"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if access_token:
|
|
32
|
+
headers["Authorization"] = f"Bearer {access_token}"
|
|
33
|
+
elif api_key and api_secret:
|
|
34
|
+
headers["Authorization"] = f"token {api_key}:{api_secret}"
|
|
35
|
+
|
|
36
|
+
self.session.headers.update(headers)
|
|
37
|
+
return {"status": "connected", "base_url": self.base_url}
|
|
38
|
+
|
|
39
|
+
@keyword("Setup ERPNext Connection")
|
|
40
|
+
def setup_erpnext_connection(self, token_file_path: str):
|
|
41
|
+
"""
|
|
42
|
+
Thiết lập kết nối với ERPNext từ file credential JSON.
|
|
43
|
+
|
|
44
|
+
Hỗ trợ 2 format:
|
|
45
|
+
1. Token Auth (API Key/Secret):
|
|
46
|
+
{
|
|
47
|
+
"base_url": "...",
|
|
48
|
+
"api_key": "...",
|
|
49
|
+
"api_secret": "..."
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
2. OAuth2 (Access Token):
|
|
53
|
+
{
|
|
54
|
+
"base_url": "...",
|
|
55
|
+
"access_token": "..."
|
|
56
|
+
}
|
|
57
|
+
"""
|
|
58
|
+
with open(token_file_path, 'r', encoding='utf-8') as f:
|
|
59
|
+
data = json.load(f)
|
|
60
|
+
|
|
61
|
+
base_url = data.get("base_url")
|
|
62
|
+
api_key = data.get("api_key")
|
|
63
|
+
api_secret = data.get("api_secret")
|
|
64
|
+
access_token = data.get("access_token")
|
|
65
|
+
|
|
66
|
+
if not base_url:
|
|
67
|
+
raise Exception("Token file must contain base_url")
|
|
68
|
+
|
|
69
|
+
if access_token:
|
|
70
|
+
return self.connect(base_url, access_token=access_token)
|
|
71
|
+
elif api_key and api_secret:
|
|
72
|
+
return self.connect(base_url, api_key=api_key, api_secret=api_secret)
|
|
73
|
+
else:
|
|
74
|
+
raise Exception("Token file must contain credentials (api_key+api_secret OR access_token)")
|
|
75
|
+
|
|
76
|
+
# ===========================================================
|
|
77
|
+
# Utility Methods
|
|
78
|
+
# ===========================================================
|
|
79
|
+
|
|
80
|
+
def create_resource(self, doctype, data):
|
|
81
|
+
res = self.session.post(f"{self.base_url}/api/resource/{doctype}", json=data)
|
|
82
|
+
if not res.ok:
|
|
83
|
+
raise Exception(f"HTTP {res.status_code}: {res.text}")
|
|
84
|
+
return res.json().get("data", res.json())
|
|
85
|
+
|
|
86
|
+
def get_doc(self, doctype, name):
|
|
87
|
+
res = self.session.get(f"{self.base_url}/api/resource/{doctype}/{name}")
|
|
88
|
+
res.raise_for_status()
|
|
89
|
+
return res.json().get("data")
|
|
90
|
+
|
|
91
|
+
def exists(self, doctype, name):
|
|
92
|
+
"""Kiểm tra xem tài nguyên có tồn tại hay không."""
|
|
93
|
+
res = self.session.get(f"{self.base_url}/api/resource/{doctype}/{name}")
|
|
94
|
+
return res.ok
|
|
95
|
+
|
|
96
|
+
@keyword("Get Document")
|
|
97
|
+
def get_document(self, doctype, name):
|
|
98
|
+
"""
|
|
99
|
+
Lấy thông tin chi tiết của một document.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
doctype: Loại document (Item, Customer, Sales Order, etc.)
|
|
103
|
+
name: Tên/ID của document
|
|
104
|
+
|
|
105
|
+
Example:
|
|
106
|
+
| ${doc}= | Get Document | Customer | CUST-00001 |
|
|
107
|
+
"""
|
|
108
|
+
return self.get_doc(doctype, name)
|
|
109
|
+
|
|
110
|
+
@keyword("List Documents")
|
|
111
|
+
def list_documents(self, doctype, filters=None, fields=None, limit=20):
|
|
112
|
+
"""
|
|
113
|
+
Lấy danh sách documents với filters.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
doctype: Loại document
|
|
117
|
+
filters: Điều kiện lọc (dict hoặc JSON string)
|
|
118
|
+
fields: Các trường cần lấy (list hoặc JSON string)
|
|
119
|
+
limit: Số lượng kết quả tối đa
|
|
120
|
+
|
|
121
|
+
Example:
|
|
122
|
+
| ${items}= | List Documents | Item | {"item_group": "Products"} | ["item_code", "item_name"] | 10 |
|
|
123
|
+
"""
|
|
124
|
+
params = {"limit_page_length": limit}
|
|
125
|
+
|
|
126
|
+
if filters:
|
|
127
|
+
if isinstance(filters, str):
|
|
128
|
+
filters = json.loads(filters)
|
|
129
|
+
params["filters"] = json.dumps(filters)
|
|
130
|
+
|
|
131
|
+
if fields:
|
|
132
|
+
if isinstance(fields, str):
|
|
133
|
+
fields = json.loads(fields)
|
|
134
|
+
params["fields"] = json.dumps(fields)
|
|
135
|
+
|
|
136
|
+
res = self.session.get(f"{self.base_url}/api/resource/{doctype}", params=params)
|
|
137
|
+
res.raise_for_status()
|
|
138
|
+
return res.json().get("data", [])
|
|
139
|
+
|
|
140
|
+
@keyword("Update Document")
|
|
141
|
+
def update_document(self, doctype, name, data):
|
|
142
|
+
"""
|
|
143
|
+
Cập nhật một document.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
doctype: Loại document
|
|
147
|
+
name: Tên/ID của document
|
|
148
|
+
data: Dữ liệu cần cập nhật (dict hoặc JSON string)
|
|
149
|
+
|
|
150
|
+
Example:
|
|
151
|
+
| Update Document | Item | ITEM-001 | {"description": "New description"} |
|
|
152
|
+
"""
|
|
153
|
+
if isinstance(data, str):
|
|
154
|
+
data = json.loads(data)
|
|
155
|
+
|
|
156
|
+
res = self.session.put(f"{self.base_url}/api/resource/{doctype}/{name}", json=data)
|
|
157
|
+
if not res.ok:
|
|
158
|
+
raise Exception(f"HTTP {res.status_code}: {res.text}")
|
|
159
|
+
return res.json().get("data", res.json())
|
|
160
|
+
|
|
161
|
+
@keyword("Delete Document")
|
|
162
|
+
def delete_document(self, doctype, name):
|
|
163
|
+
"""
|
|
164
|
+
Xóa một document.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
doctype: Loại document
|
|
168
|
+
name: Tên/ID của document
|
|
169
|
+
|
|
170
|
+
Example:
|
|
171
|
+
| Delete Document | Item | ITEM-001 |
|
|
172
|
+
"""
|
|
173
|
+
res = self.session.delete(f"{self.base_url}/api/resource/{doctype}/{name}")
|
|
174
|
+
if not res.ok:
|
|
175
|
+
raise Exception(f"HTTP {res.status_code}: {res.text}")
|
|
176
|
+
return {"status": "deleted", "doctype": doctype, "name": name}
|
|
177
|
+
|
|
178
|
+
# ===========================================================
|
|
179
|
+
# Ensure Functions - Master Data
|
|
180
|
+
# ===========================================================
|
|
181
|
+
|
|
182
|
+
@keyword("Ensure Company Exist")
|
|
183
|
+
def ensure_company_exist(self, company: str, abbr: str):
|
|
184
|
+
"""Đảm bảo Company tồn tại."""
|
|
185
|
+
if self.exists("Company", company):
|
|
186
|
+
return {"status": "exists", "name": company}
|
|
187
|
+
|
|
188
|
+
data = {
|
|
189
|
+
"doctype": "Company",
|
|
190
|
+
"company_name": company,
|
|
191
|
+
"abbr": abbr,
|
|
192
|
+
"default_currency": "VND",
|
|
193
|
+
"country": "Vietnam",
|
|
194
|
+
"company_logo": None
|
|
195
|
+
}
|
|
196
|
+
return self.create_resource("Company", data)
|
|
197
|
+
|
|
198
|
+
@keyword("Ensure Warehouse Exist")
|
|
199
|
+
def ensure_warehouse_exist(self, warehouse: str, company: str):
|
|
200
|
+
"""Đảm bảo Warehouse tồn tại."""
|
|
201
|
+
if self.exists("Warehouse", warehouse):
|
|
202
|
+
return {"status": "exists", "name": warehouse}
|
|
203
|
+
|
|
204
|
+
data = {
|
|
205
|
+
"doctype": "Warehouse",
|
|
206
|
+
"warehouse_name": warehouse,
|
|
207
|
+
"company": company,
|
|
208
|
+
}
|
|
209
|
+
return self.create_resource("Warehouse", data)
|
|
210
|
+
|
|
211
|
+
@keyword("Ensure Supplier Exist")
|
|
212
|
+
def ensure_supplier_exist(self, supplier_name: str):
|
|
213
|
+
"""Đảm bảo Supplier tồn tại."""
|
|
214
|
+
if self.exists("Supplier", supplier_name):
|
|
215
|
+
return {"status": "exists", "name": supplier_name}
|
|
216
|
+
|
|
217
|
+
data = {
|
|
218
|
+
"doctype": "Supplier",
|
|
219
|
+
"supplier_name": supplier_name,
|
|
220
|
+
"supplier_type": "Company",
|
|
221
|
+
"country": "Vietnam",
|
|
222
|
+
}
|
|
223
|
+
return self.create_resource("Supplier", data)
|
|
224
|
+
|
|
225
|
+
@keyword("Ensure Items Exist")
|
|
226
|
+
def ensure_items_exist(self, items_json):
|
|
227
|
+
"""Đảm bảo tất cả item tồn tại."""
|
|
228
|
+
if isinstance(items_json, str):
|
|
229
|
+
items = json.loads(items_json)
|
|
230
|
+
else:
|
|
231
|
+
items = items_json
|
|
232
|
+
|
|
233
|
+
created_items = []
|
|
234
|
+
for i in items:
|
|
235
|
+
code = i["item_code"]
|
|
236
|
+
if self.exists("Item", code):
|
|
237
|
+
created_items.append({"status": "exists", "item_code": code})
|
|
238
|
+
continue
|
|
239
|
+
|
|
240
|
+
data = {
|
|
241
|
+
"doctype": "Item",
|
|
242
|
+
"item_code": code,
|
|
243
|
+
"item_name": code,
|
|
244
|
+
"description": i.get("description", code),
|
|
245
|
+
"stock_uom": "Nos",
|
|
246
|
+
"is_stock_item": 1,
|
|
247
|
+
"item_group": "All Item Groups",
|
|
248
|
+
}
|
|
249
|
+
res = self.create_resource("Item", data)
|
|
250
|
+
created_items.append(res)
|
|
251
|
+
return created_items
|
|
252
|
+
|
|
253
|
+
@keyword("Ensure Customer Exist")
|
|
254
|
+
def ensure_customer_exist(self, customer_name: str, customer_group="Commercial", territory="Vietnam"):
|
|
255
|
+
"""
|
|
256
|
+
Đảm bảo Customer tồn tại.
|
|
257
|
+
|
|
258
|
+
API: /api/resource/Customer
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
customer_name: Tên khách hàng
|
|
262
|
+
customer_group: Nhóm khách hàng (mặc định: Commercial)
|
|
263
|
+
territory: Khu vực (mặc định: Vietnam)
|
|
264
|
+
"""
|
|
265
|
+
if self.exists("Customer", customer_name):
|
|
266
|
+
return {"status": "exists", "name": customer_name}
|
|
267
|
+
|
|
268
|
+
data = {
|
|
269
|
+
"doctype": "Customer",
|
|
270
|
+
"customer_name": customer_name,
|
|
271
|
+
"customer_type": "Company",
|
|
272
|
+
"customer_group": customer_group,
|
|
273
|
+
"territory": territory,
|
|
274
|
+
}
|
|
275
|
+
return self.create_resource("Customer", data)
|
|
276
|
+
|
|
277
|
+
# ===========================================================
|
|
278
|
+
# Sales APIs - Quy trình bán hàng
|
|
279
|
+
# ===========================================================
|
|
280
|
+
|
|
281
|
+
@keyword("Create Sales Order")
|
|
282
|
+
def create_sales_order(self, customer: str, items: list, delivery_date: str, company: str):
|
|
283
|
+
"""
|
|
284
|
+
Tạo Sales Order (Đơn hàng bán).
|
|
285
|
+
|
|
286
|
+
API: /api/resource/Sales Order
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
customer: Tên khách hàng
|
|
290
|
+
items: Danh sách items [{"item_code": "ITEM-001", "qty": 10, "rate": 1000}]
|
|
291
|
+
delivery_date: Ngày giao hàng (format: YYYY-MM-DD)
|
|
292
|
+
company: Tên công ty
|
|
293
|
+
|
|
294
|
+
Example:
|
|
295
|
+
| ${items}= | Create List | {"item_code": "ITEM-001", "qty": 10, "rate": 1000} |
|
|
296
|
+
| Create Sales Order | Customer A | ${items} | 2025-12-01 | My Company |
|
|
297
|
+
"""
|
|
298
|
+
self.ensure_customer_exist(customer)
|
|
299
|
+
|
|
300
|
+
order_items = []
|
|
301
|
+
for item in items:
|
|
302
|
+
order_items.append({
|
|
303
|
+
"item_code": item["item_code"],
|
|
304
|
+
"qty": item["qty"],
|
|
305
|
+
"rate": item.get("rate", 0),
|
|
306
|
+
"delivery_date": delivery_date,
|
|
307
|
+
"uom": item.get("uom", "Nos"),
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
data = {
|
|
311
|
+
"doctype": "Sales Order",
|
|
312
|
+
"customer": customer,
|
|
313
|
+
"company": company,
|
|
314
|
+
"delivery_date": delivery_date,
|
|
315
|
+
"items": order_items
|
|
316
|
+
}
|
|
317
|
+
return self.create_resource("Sales Order", data)
|
|
318
|
+
|
|
319
|
+
@keyword("Create Sales Invoice")
|
|
320
|
+
def create_sales_invoice(self, customer: str, items: list, company: str, posting_date=None):
|
|
321
|
+
"""
|
|
322
|
+
Tạo Sales Invoice (Hóa đơn bán hàng).
|
|
323
|
+
|
|
324
|
+
API: /api/resource/Sales Invoice
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
customer: Tên khách hàng
|
|
328
|
+
items: Danh sách items [{"item_code": "ITEM-001", "qty": 10, "rate": 1000}]
|
|
329
|
+
company: Tên công ty
|
|
330
|
+
posting_date: Ngày lập hóa đơn (mặc định: ngày hiện tại)
|
|
331
|
+
"""
|
|
332
|
+
self.ensure_customer_exist(customer)
|
|
333
|
+
|
|
334
|
+
invoice_items = []
|
|
335
|
+
for item in items:
|
|
336
|
+
invoice_items.append({
|
|
337
|
+
"item_code": item["item_code"],
|
|
338
|
+
"qty": item["qty"],
|
|
339
|
+
"rate": item.get("rate", 0),
|
|
340
|
+
"uom": item.get("uom", "Nos"),
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
data = {
|
|
344
|
+
"doctype": "Sales Invoice",
|
|
345
|
+
"customer": customer,
|
|
346
|
+
"company": company,
|
|
347
|
+
"items": invoice_items
|
|
348
|
+
}
|
|
349
|
+
if posting_date:
|
|
350
|
+
data["posting_date"] = posting_date
|
|
351
|
+
|
|
352
|
+
return self.create_resource("Sales Invoice", data)
|
|
353
|
+
|
|
354
|
+
# ===========================================================
|
|
355
|
+
# Stock/Inventory APIs - Quản lý kho
|
|
356
|
+
# ===========================================================
|
|
357
|
+
|
|
358
|
+
@keyword("Create Stock Entry")
|
|
359
|
+
def create_stock_entry(self, purpose: str, items: list, company: str, posting_date=None):
|
|
360
|
+
"""
|
|
361
|
+
Tạo Stock Entry (Phiếu nhập/xuất kho).
|
|
362
|
+
|
|
363
|
+
API: /api/resource/Stock Entry
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
purpose: Mục đích (Material Receipt, Material Issue, Material Transfer, etc.)
|
|
367
|
+
items: Danh sách items với warehouse
|
|
368
|
+
company: Tên công ty
|
|
369
|
+
posting_date: Ngày lập phiếu
|
|
370
|
+
|
|
371
|
+
Purpose types:
|
|
372
|
+
- Material Receipt: Nhập kho
|
|
373
|
+
- Material Issue: Xuất kho
|
|
374
|
+
- Material Transfer: Chuyển kho
|
|
375
|
+
- Manufacture: Sản xuất
|
|
376
|
+
- Repack: Đóng gói lại
|
|
377
|
+
|
|
378
|
+
Example:
|
|
379
|
+
| ${items}= | Create List | {"item_code": "ITEM-001", "qty": 100, "t_warehouse": "Stores - EDU"} |
|
|
380
|
+
| Create Stock Entry | Material Receipt | ${items} | My Company |
|
|
381
|
+
"""
|
|
382
|
+
stock_items = []
|
|
383
|
+
for item in items:
|
|
384
|
+
stock_items.append({
|
|
385
|
+
"item_code": item["item_code"],
|
|
386
|
+
"qty": item["qty"],
|
|
387
|
+
"s_warehouse": item.get("s_warehouse"), # Source warehouse
|
|
388
|
+
"t_warehouse": item.get("t_warehouse"), # Target warehouse
|
|
389
|
+
"uom": item.get("uom", "Nos"),
|
|
390
|
+
"stock_uom": item.get("stock_uom", "Nos"),
|
|
391
|
+
"conversion_factor": item.get("conversion_factor", 1.0),
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
data = {
|
|
395
|
+
"doctype": "Stock Entry",
|
|
396
|
+
"purpose": purpose,
|
|
397
|
+
"company": company,
|
|
398
|
+
"items": stock_items
|
|
399
|
+
}
|
|
400
|
+
if posting_date:
|
|
401
|
+
data["posting_date"] = posting_date
|
|
402
|
+
|
|
403
|
+
return self.create_resource("Stock Entry", data)
|
|
404
|
+
|
|
405
|
+
@keyword("Create Purchase Receipt")
|
|
406
|
+
def create_purchase_receipt(self, supplier: str, items: list, company: str, posting_date=None):
|
|
407
|
+
"""
|
|
408
|
+
Tạo Purchase Receipt (Phiếu nhận hàng mua).
|
|
409
|
+
|
|
410
|
+
API: /api/resource/Purchase Receipt
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
supplier: Tên nhà cung cấp
|
|
414
|
+
items: Danh sách items [{"item_code": "ITEM-001", "qty": 100, "rate": 1000, "warehouse": "Stores - EDU"}]
|
|
415
|
+
company: Tên công ty
|
|
416
|
+
posting_date: Ngày nhận hàng
|
|
417
|
+
"""
|
|
418
|
+
self.ensure_supplier_exist(supplier)
|
|
419
|
+
|
|
420
|
+
receipt_items = []
|
|
421
|
+
for item in items:
|
|
422
|
+
receipt_items.append({
|
|
423
|
+
"item_code": item["item_code"],
|
|
424
|
+
"qty": item["qty"],
|
|
425
|
+
"rate": item.get("rate", 0),
|
|
426
|
+
"warehouse": item.get("warehouse"),
|
|
427
|
+
"uom": item.get("uom", "Nos"),
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
data = {
|
|
431
|
+
"doctype": "Purchase Receipt",
|
|
432
|
+
"supplier": supplier,
|
|
433
|
+
"company": company,
|
|
434
|
+
"items": receipt_items
|
|
435
|
+
}
|
|
436
|
+
if posting_date:
|
|
437
|
+
data["posting_date"] = posting_date
|
|
438
|
+
|
|
439
|
+
return self.create_resource("Purchase Receipt", data)
|
|
440
|
+
|
|
441
|
+
@keyword("Create Purchase Order")
|
|
442
|
+
def create_purchase_order(self, supplier: str, items: list, company: str, schedule_date=None):
|
|
443
|
+
"""
|
|
444
|
+
Tạo Purchase Order (Đơn hàng mua).
|
|
445
|
+
|
|
446
|
+
API: /api/resource/Purchase Order
|
|
447
|
+
|
|
448
|
+
Args:
|
|
449
|
+
supplier: Tên nhà cung cấp
|
|
450
|
+
items: Danh sách items [{"item_code": "ITEM-001", "qty": 100, "rate": 1000}]
|
|
451
|
+
company: Tên công ty
|
|
452
|
+
schedule_date: Ngày cần hàng
|
|
453
|
+
"""
|
|
454
|
+
self.ensure_supplier_exist(supplier)
|
|
455
|
+
|
|
456
|
+
po_items = []
|
|
457
|
+
for item in items:
|
|
458
|
+
po_items.append({
|
|
459
|
+
"item_code": item["item_code"],
|
|
460
|
+
"qty": item["qty"],
|
|
461
|
+
"rate": item.get("rate", 0),
|
|
462
|
+
"schedule_date": item.get("schedule_date", schedule_date),
|
|
463
|
+
"warehouse": item.get("warehouse"),
|
|
464
|
+
"uom": item.get("uom", "Nos"),
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
data = {
|
|
468
|
+
"doctype": "Purchase Order",
|
|
469
|
+
"supplier": supplier,
|
|
470
|
+
"company": company,
|
|
471
|
+
"items": po_items,
|
|
472
|
+
"schedule_date": schedule_date
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return self.create_resource("Purchase Order", data)
|
|
476
|
+
|
|
477
|
+
# ===========================================================
|
|
478
|
+
# Accounting APIs - Kế toán
|
|
479
|
+
# ===========================================================
|
|
480
|
+
|
|
481
|
+
@keyword("Create Purchase Invoice")
|
|
482
|
+
def create_purchase_invoice(self, supplier: str, items: list, company: str, posting_date=None):
|
|
483
|
+
"""
|
|
484
|
+
Tạo Purchase Invoice (Hóa đơn mua hàng).
|
|
485
|
+
|
|
486
|
+
API: /api/resource/Purchase Invoice
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
supplier: Tên nhà cung cấp
|
|
490
|
+
items: Danh sách items [{"item_code": "ITEM-001", "qty": 100, "rate": 1000}]
|
|
491
|
+
company: Tên công ty
|
|
492
|
+
posting_date: Ngày lập hóa đơn
|
|
493
|
+
"""
|
|
494
|
+
self.ensure_supplier_exist(supplier)
|
|
495
|
+
|
|
496
|
+
invoice_items = []
|
|
497
|
+
for item in items:
|
|
498
|
+
invoice_items.append({
|
|
499
|
+
"item_code": item["item_code"],
|
|
500
|
+
"qty": item["qty"],
|
|
501
|
+
"rate": item.get("rate", 0),
|
|
502
|
+
"uom": item.get("uom", "Nos"),
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
data = {
|
|
506
|
+
"doctype": "Purchase Invoice",
|
|
507
|
+
"supplier": supplier,
|
|
508
|
+
"company": company,
|
|
509
|
+
"items": invoice_items
|
|
510
|
+
}
|
|
511
|
+
if posting_date:
|
|
512
|
+
data["posting_date"] = posting_date
|
|
513
|
+
|
|
514
|
+
return self.create_resource("Purchase Invoice", data)
|
|
515
|
+
|
|
516
|
+
@keyword("Create Payment Entry")
|
|
517
|
+
def create_payment_entry(self, payment_type: str, party_type: str, party: str,
|
|
518
|
+
paid_amount: float, company: str, posting_date=None):
|
|
519
|
+
"""
|
|
520
|
+
Tạo Payment Entry (Phiếu thanh toán).
|
|
521
|
+
|
|
522
|
+
API: /api/resource/Payment Entry
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
payment_type: Loại thanh toán (Receive, Pay, Internal Transfer)
|
|
526
|
+
party_type: Loại đối tác (Customer, Supplier, Employee)
|
|
527
|
+
party: Tên đối tác
|
|
528
|
+
paid_amount: Số tiền thanh toán
|
|
529
|
+
company: Tên công ty
|
|
530
|
+
posting_date: Ngày thanh toán
|
|
531
|
+
|
|
532
|
+
Example:
|
|
533
|
+
| Create Payment Entry | Receive | Customer | Customer A | 10000000 | My Company |
|
|
534
|
+
| Create Payment Entry | Pay | Supplier | Supplier B | 5000000 | My Company |
|
|
535
|
+
"""
|
|
536
|
+
data = {
|
|
537
|
+
"doctype": "Payment Entry",
|
|
538
|
+
"payment_type": payment_type,
|
|
539
|
+
"party_type": party_type,
|
|
540
|
+
"party": party,
|
|
541
|
+
"paid_amount": paid_amount,
|
|
542
|
+
"received_amount": paid_amount if payment_type == "Receive" else 0,
|
|
543
|
+
"company": company,
|
|
544
|
+
}
|
|
545
|
+
if posting_date:
|
|
546
|
+
data["posting_date"] = posting_date
|
|
547
|
+
|
|
548
|
+
return self.create_resource("Payment Entry", data)
|
|
549
|
+
|
|
550
|
+
# ===========================================================
|
|
551
|
+
# Procurement - Quy trình mua hàng (đã có sẵn)
|
|
552
|
+
# ===========================================================
|
|
553
|
+
|
|
554
|
+
@keyword("Load Excel Request")
|
|
555
|
+
def load_excel_request(self, path: str):
|
|
556
|
+
"""Đọc file Excel chứa company, itemcode, quantity."""
|
|
557
|
+
df = pd.read_excel(path)
|
|
558
|
+
return df.to_dict(orient="records")
|
|
559
|
+
|
|
560
|
+
@keyword("Create Material Request From Excel")
|
|
561
|
+
def create_material_request_from_excel(self, path: str, schedule_date: str):
|
|
562
|
+
"""Tạo Material Request từ file Excel."""
|
|
563
|
+
rows = self.load_excel_request(path)
|
|
564
|
+
if not rows:
|
|
565
|
+
raise Exception("Excel file không có dữ liệu.")
|
|
566
|
+
|
|
567
|
+
company = rows[0]["company"]
|
|
568
|
+
self.ensure_company_exist(company, "EDU")
|
|
569
|
+
self.ensure_warehouse_exist(f"Stores - EDU", company)
|
|
570
|
+
|
|
571
|
+
items = []
|
|
572
|
+
for row in rows:
|
|
573
|
+
self.ensure_items_exist([{
|
|
574
|
+
"item_code": row["itemcode"],
|
|
575
|
+
"description": row["itemcode"]
|
|
576
|
+
}])
|
|
577
|
+
items.append({
|
|
578
|
+
"item_code": row["itemcode"],
|
|
579
|
+
"description": row["itemcode"],
|
|
580
|
+
"qty": float(row["quantity"]),
|
|
581
|
+
"schedule_date": schedule_date,
|
|
582
|
+
"warehouse": f"Stores - EDU",
|
|
583
|
+
"uom": "Nos",
|
|
584
|
+
"stock_uom": "Nos",
|
|
585
|
+
"conversion_factor": 1.0
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
data = {
|
|
589
|
+
"doctype": "Material Request",
|
|
590
|
+
"material_request_type": "Purchase",
|
|
591
|
+
"company": company,
|
|
592
|
+
"schedule_date": schedule_date,
|
|
593
|
+
"items": items
|
|
594
|
+
}
|
|
595
|
+
return self.create_resource("Material Request", data)
|
|
596
|
+
|
|
597
|
+
# ===========================================================
|
|
598
|
+
# Step 2: Gửi RFQ
|
|
599
|
+
# ===========================================================
|
|
600
|
+
|
|
601
|
+
@keyword("Send RFQ From Material Request")
|
|
602
|
+
def send_rfq_from_mr(self, mr_name: str, suppliers: list):
|
|
603
|
+
"""Tạo RFQ dựa trên Material Request."""
|
|
604
|
+
mr_doc = self.get_doc("Material Request", mr_name)
|
|
605
|
+
company = mr_doc["company"]
|
|
606
|
+
self.ensure_warehouse_exist(f"Stores - EDU", company)
|
|
607
|
+
for s in suppliers:
|
|
608
|
+
self.ensure_supplier_exist(s)
|
|
609
|
+
|
|
610
|
+
items = []
|
|
611
|
+
for d in mr_doc["items"]:
|
|
612
|
+
items.append({
|
|
613
|
+
"item_code": d["item_code"],
|
|
614
|
+
"qty": d["qty"],
|
|
615
|
+
"uom": d.get("uom", "Nos"),
|
|
616
|
+
"stock_uom": d.get("stock_uom", "Nos"),
|
|
617
|
+
"conversion_factor": 1.0,
|
|
618
|
+
"warehouse": d["warehouse"],
|
|
619
|
+
})
|
|
620
|
+
|
|
621
|
+
rfq_data = {
|
|
622
|
+
"doctype": "Request for Quotation",
|
|
623
|
+
"company": company,
|
|
624
|
+
"transaction_date": mr_doc["schedule_date"],
|
|
625
|
+
"suppliers": [{"supplier": s} for s in suppliers],
|
|
626
|
+
"items": items,
|
|
627
|
+
"message_for_supplier": "Xin vui lòng gửi báo giá."
|
|
628
|
+
}
|
|
629
|
+
return self.create_resource("Request for Quotation", rfq_data)
|
|
630
|
+
|
|
631
|
+
# ===========================================================
|
|
632
|
+
# Step 3: Supplier gửi báo giá
|
|
633
|
+
# ===========================================================
|
|
634
|
+
|
|
635
|
+
@keyword("Receive Supplier Quotation")
|
|
636
|
+
def receive_supplier_quotation(self, rfq_name: str, supplier_name: str, quotation_data: list):
|
|
637
|
+
rfq_doc = self.get_doc("Request for Quotation", rfq_name)
|
|
638
|
+
company = rfq_doc["company"]
|
|
639
|
+
self.ensure_supplier_exist(supplier_name)
|
|
640
|
+
|
|
641
|
+
sq_data = {
|
|
642
|
+
"doctype": "Supplier Quotation",
|
|
643
|
+
"supplier": supplier_name,
|
|
644
|
+
"company": company,
|
|
645
|
+
"transaction_date": "2026-01-28",
|
|
646
|
+
"items": [],
|
|
647
|
+
}
|
|
648
|
+
|
|
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
|
+
return self.create_resource("Supplier Quotation", sq_data)
|
|
661
|
+
|
|
662
|
+
# ===========================================================
|
|
663
|
+
# Step 4: Tạo Purchase Order
|
|
664
|
+
# ===========================================================
|
|
665
|
+
|
|
666
|
+
@keyword("Create Purchase Order From Quotation")
|
|
667
|
+
def create_purchase_order_from_quotation(self, quotation_name: str):
|
|
668
|
+
sq_doc = self.get_doc("Supplier Quotation", quotation_name)
|
|
669
|
+
supplier = sq_doc["supplier"]
|
|
670
|
+
company = sq_doc["company"]
|
|
671
|
+
|
|
672
|
+
self.ensure_supplier_exist(supplier)
|
|
673
|
+
self.ensure_warehouse_exist(f"Stores - EDU", company)
|
|
674
|
+
|
|
675
|
+
items = []
|
|
676
|
+
for d in sq_doc["items"]:
|
|
677
|
+
items.append({
|
|
678
|
+
"item_code": d["item_code"],
|
|
679
|
+
"qty": d["qty"],
|
|
680
|
+
"rate": d["rate"],
|
|
681
|
+
"schedule_date": "2026-02-28",
|
|
682
|
+
"warehouse": f"Stores - EDU",
|
|
683
|
+
"uom": d.get("uom", "Nos"),
|
|
684
|
+
"conversion_factor": 1.0
|
|
685
|
+
})
|
|
686
|
+
|
|
687
|
+
po_data = {
|
|
688
|
+
"doctype": "Purchase Order",
|
|
689
|
+
"supplier": supplier,
|
|
690
|
+
"company": company,
|
|
691
|
+
"schedule_date": "2026-02-28",
|
|
692
|
+
"items": items
|
|
693
|
+
}
|
|
694
|
+
return self.create_resource("Purchase Order", po_data)
|
|
695
|
+
|
|
696
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
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.1"
|
|
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
|
+
"pandas>=2.0.0",
|
|
20
|
+
"openpyxl>=3.1.0"
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://github.com/yourusername/rpa-erpnext"
|
|
25
|
+
Source = "https://github.com/yourusername/rpa-erpnext"
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.packages.find]
|
|
28
|
+
where = ["."]
|
|
29
|
+
include = ["RPA*"]
|
|
30
|
+
exclude = ["test*", "tests*", "*.tests"]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rpa-erpnext
|
|
3
|
+
Version: 1.0.1
|
|
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
|
+
Requires-Dist: pandas>=2.0.0
|
|
16
|
+
Requires-Dist: openpyxl>=3.1.0
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# RPA.ERPNext
|
|
20
|
+
|
|
21
|
+
Library for automating [ERPNext](https://erpnext.com/) using REST API, built for [Robot Framework](https://robotframework.org/).
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install rpa-erpnext
|
|
27
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
pyproject.toml
|
|
3
|
+
readme.md
|
|
4
|
+
RPA/ERPNext.py
|
|
5
|
+
RPA/__init__.py
|
|
6
|
+
rpa_erpnext.egg-info/PKG-INFO
|
|
7
|
+
rpa_erpnext.egg-info/SOURCES.txt
|
|
8
|
+
rpa_erpnext.egg-info/dependency_links.txt
|
|
9
|
+
rpa_erpnext.egg-info/requires.txt
|
|
10
|
+
rpa_erpnext.egg-info/top_level.txt
|
|
11
|
+
test/test_functions.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
RPA
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
|
|
2
|
+
import sys
|
|
3
|
+
import os
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
# Add parent directory to path to import RPA.ERPNext
|
|
7
|
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
8
|
+
|
|
9
|
+
from RPA.ERPNext import ERPNextLibrary
|
|
10
|
+
|
|
11
|
+
# Configuration
|
|
12
|
+
BASE_URL = "http://4.216.192.221:8080"
|
|
13
|
+
API_KEY = "j47s948g5i"
|
|
14
|
+
API_SECRET = "c0b0693403"
|
|
15
|
+
|
|
16
|
+
def test_connection():
|
|
17
|
+
print(f"\n--- Testing Connection to {BASE_URL} ---")
|
|
18
|
+
erp = ERPNextLibrary(BASE_URL, API_KEY, API_SECRET)
|
|
19
|
+
print("Connection initialized.")
|
|
20
|
+
return erp
|
|
21
|
+
|
|
22
|
+
def test_get_document(erp):
|
|
23
|
+
print("\n--- Testing Get Document (User) ---")
|
|
24
|
+
try:
|
|
25
|
+
# Assuming Administrator or Guest exists, or we list first
|
|
26
|
+
users = erp.list_documents("User", limit=1)
|
|
27
|
+
if users:
|
|
28
|
+
first_user = users[0]
|
|
29
|
+
print(f"Found user: {first_user['name']}")
|
|
30
|
+
doc = erp.get_document("User", first_user['name'])
|
|
31
|
+
print("Document details retrieved successfully.")
|
|
32
|
+
# print(json.dumps(doc, indent=2))
|
|
33
|
+
else:
|
|
34
|
+
print("No users found to test get_document.")
|
|
35
|
+
except Exception as e:
|
|
36
|
+
print(f"Error in test_get_document: {e}")
|
|
37
|
+
|
|
38
|
+
def test_procurement_flow(erp):
|
|
39
|
+
print("\n--- Testing Procurement Flow Utilities ---")
|
|
40
|
+
|
|
41
|
+
company = "EduRPA Co."
|
|
42
|
+
supplier = "Supplier Demo"
|
|
43
|
+
item_code = "TEMP_SENSOR"
|
|
44
|
+
|
|
45
|
+
# 1. Ensure exists check
|
|
46
|
+
print(f"Checking if company exists: {company}")
|
|
47
|
+
exists = erp.ensure_company_exist(company, "EDU")
|
|
48
|
+
print(f"Company exists: {exists}")
|
|
49
|
+
|
|
50
|
+
print(f"Checking if supplier exists: {supplier}")
|
|
51
|
+
sup_exists = erp.ensure_supplier_exist(supplier)
|
|
52
|
+
print(f"Supplier exists: {sup_exists}")
|
|
53
|
+
|
|
54
|
+
# 2. Test create material request (Simulated data)
|
|
55
|
+
items = [{"item_code": item_code, "qty": 10, "schedule_date": "2025-11-05", "warehouse": "Stores - EDU"}]
|
|
56
|
+
print(f"Creating Material Request for items: {items}")
|
|
57
|
+
try:
|
|
58
|
+
mr = erp.create_material_request(company, items, "2025-11-05")
|
|
59
|
+
print(f"Material Request Created: {mr.get('name')}")
|
|
60
|
+
|
|
61
|
+
# 3. Create RFQ
|
|
62
|
+
print(f"Creating RFQ from MR: {mr.get('name')}")
|
|
63
|
+
rfq = erp.create_request_for_quotation(company, [{"supplier": supplier}], items)
|
|
64
|
+
print(f"RFQ Created: {rfq.get('name')}")
|
|
65
|
+
|
|
66
|
+
except Exception as e:
|
|
67
|
+
print(f"Error in procurement flow: {e}")
|
|
68
|
+
|
|
69
|
+
def main():
|
|
70
|
+
erp = test_connection()
|
|
71
|
+
|
|
72
|
+
# Uncomment functions to test specific logic
|
|
73
|
+
test_get_document(erp)
|
|
74
|
+
test_procurement_flow(erp)
|
|
75
|
+
|
|
76
|
+
if __name__ == "__main__":
|
|
77
|
+
main()
|