rpa-erpnext 1.0.0__py3-none-any.whl → 1.0.4__py3-none-any.whl
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.py +556 -22
- RPA/__init__.py +5 -0
- {rpa_erpnext-1.0.0.dist-info → rpa_erpnext-1.0.4.dist-info}/METADATA +13 -1
- rpa_erpnext-1.0.4.dist-info/RECORD +7 -0
- {rpa_erpnext-1.0.0.dist-info → rpa_erpnext-1.0.4.dist-info}/WHEEL +1 -1
- RPA/init.py +0 -6
- rpa_erpnext-1.0.0.dist-info/RECORD +0 -7
- {rpa_erpnext-1.0.0.dist-info → rpa_erpnext-1.0.4.dist-info}/licenses/LICENSE +0 -0
- {rpa_erpnext-1.0.0.dist-info → rpa_erpnext-1.0.4.dist-info}/top_level.txt +0 -0
RPA/ERPNext.py
CHANGED
|
@@ -4,13 +4,112 @@ import requests
|
|
|
4
4
|
from robot.api.deco import keyword, library
|
|
5
5
|
@library(scope='GLOBAL', auto_keywords=False)
|
|
6
6
|
class ERPNextLibrary:
|
|
7
|
-
def __init__(self, base_url, api_key, api_secret):
|
|
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
|
+
"""
|
|
8
23
|
self.base_url = base_url.rstrip("/")
|
|
9
24
|
self.session = requests.Session()
|
|
10
|
-
|
|
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
|
+
Args:
|
|
45
|
+
token_file_path: Đường dẫn đến file token JSON
|
|
46
|
+
|
|
47
|
+
Format 1 (Standard ERPNext):
|
|
48
|
+
{
|
|
49
|
+
"base_url": "...",
|
|
50
|
+
"api_key": "...",
|
|
51
|
+
"api_secret": "..."
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
Format 2 (Google-like / Moodle structure):
|
|
55
|
+
{
|
|
56
|
+
"access_token": "http://erpnext.example.com", # Mapping base_url here
|
|
57
|
+
"refresh_token": "key:secret" # Mapping credentials here
|
|
58
|
+
}
|
|
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
|
+
|
|
67
|
+
with open(token_file_path, 'r', encoding='utf-8') as f:
|
|
68
|
+
data = json.load(f)
|
|
69
|
+
|
|
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
|
|
100
|
+
|
|
101
|
+
if not base_url:
|
|
102
|
+
raise Exception("Could not find base_url in token file")
|
|
103
|
+
|
|
104
|
+
if access_token:
|
|
105
|
+
return self.connect(base_url, access_token=access_token)
|
|
106
|
+
elif api_key and api_secret:
|
|
107
|
+
return self.connect(base_url, api_key=api_key, api_secret=api_secret)
|
|
108
|
+
else:
|
|
109
|
+
raise Exception("Token file must contain credentials (api_key+api_secret OR access_token/refresh_token)")
|
|
11
110
|
|
|
12
111
|
# ===========================================================
|
|
13
|
-
# Utility
|
|
112
|
+
# Utility Methods
|
|
14
113
|
# ===========================================================
|
|
15
114
|
|
|
16
115
|
def create_resource(self, doctype, data):
|
|
@@ -29,8 +128,90 @@ class ERPNextLibrary:
|
|
|
29
128
|
res = self.session.get(f"{self.base_url}/api/resource/{doctype}/{name}")
|
|
30
129
|
return res.ok
|
|
31
130
|
|
|
131
|
+
@keyword("Get Document")
|
|
132
|
+
def get_document(self, doctype, name):
|
|
133
|
+
"""
|
|
134
|
+
Lấy thông tin chi tiết của một document.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
doctype: Loại document (Item, Customer, Sales Order, etc.)
|
|
138
|
+
name: Tên/ID của document
|
|
139
|
+
|
|
140
|
+
Example:
|
|
141
|
+
| ${doc}= | Get Document | Customer | CUST-00001 |
|
|
142
|
+
"""
|
|
143
|
+
return self.get_doc(doctype, name)
|
|
144
|
+
|
|
145
|
+
@keyword("List Documents")
|
|
146
|
+
def list_documents(self, doctype, filters=None, fields=None, limit=20):
|
|
147
|
+
"""
|
|
148
|
+
Lấy danh sách documents với filters.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
doctype: Loại document
|
|
152
|
+
filters: Điều kiện lọc (dict hoặc JSON string)
|
|
153
|
+
fields: Các trường cần lấy (list hoặc JSON string)
|
|
154
|
+
limit: Số lượng kết quả tối đa
|
|
155
|
+
|
|
156
|
+
Example:
|
|
157
|
+
| ${items}= | List Documents | Item | {"item_group": "Products"} | ["item_code", "item_name"] | 10 |
|
|
158
|
+
"""
|
|
159
|
+
params = {"limit_page_length": limit}
|
|
160
|
+
|
|
161
|
+
if filters:
|
|
162
|
+
if isinstance(filters, str):
|
|
163
|
+
filters = json.loads(filters)
|
|
164
|
+
params["filters"] = json.dumps(filters)
|
|
165
|
+
|
|
166
|
+
if fields:
|
|
167
|
+
if isinstance(fields, str):
|
|
168
|
+
fields = json.loads(fields)
|
|
169
|
+
params["fields"] = json.dumps(fields)
|
|
170
|
+
|
|
171
|
+
res = self.session.get(f"{self.base_url}/api/resource/{doctype}", params=params)
|
|
172
|
+
res.raise_for_status()
|
|
173
|
+
return res.json().get("data", [])
|
|
174
|
+
|
|
175
|
+
@keyword("Update Document")
|
|
176
|
+
def update_document(self, doctype, name, data):
|
|
177
|
+
"""
|
|
178
|
+
Cập nhật một document.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
doctype: Loại document
|
|
182
|
+
name: Tên/ID của document
|
|
183
|
+
data: Dữ liệu cần cập nhật (dict hoặc JSON string)
|
|
184
|
+
|
|
185
|
+
Example:
|
|
186
|
+
| Update Document | Item | ITEM-001 | {"description": "New description"} |
|
|
187
|
+
"""
|
|
188
|
+
if isinstance(data, str):
|
|
189
|
+
data = json.loads(data)
|
|
190
|
+
|
|
191
|
+
res = self.session.put(f"{self.base_url}/api/resource/{doctype}/{name}", json=data)
|
|
192
|
+
if not res.ok:
|
|
193
|
+
raise Exception(f"HTTP {res.status_code}: {res.text}")
|
|
194
|
+
return res.json().get("data", res.json())
|
|
195
|
+
|
|
196
|
+
@keyword("Delete Document")
|
|
197
|
+
def delete_document(self, doctype, name):
|
|
198
|
+
"""
|
|
199
|
+
Xóa một document.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
doctype: Loại document
|
|
203
|
+
name: Tên/ID của document
|
|
204
|
+
|
|
205
|
+
Example:
|
|
206
|
+
| Delete Document | Item | ITEM-001 |
|
|
207
|
+
"""
|
|
208
|
+
res = self.session.delete(f"{self.base_url}/api/resource/{doctype}/{name}")
|
|
209
|
+
if not res.ok:
|
|
210
|
+
raise Exception(f"HTTP {res.status_code}: {res.text}")
|
|
211
|
+
return {"status": "deleted", "doctype": doctype, "name": name}
|
|
212
|
+
|
|
32
213
|
# ===========================================================
|
|
33
|
-
# Ensure
|
|
214
|
+
# Ensure Functions - Master Data
|
|
34
215
|
# ===========================================================
|
|
35
216
|
|
|
36
217
|
@keyword("Ensure Company Exist")
|
|
@@ -104,8 +285,319 @@ class ERPNextLibrary:
|
|
|
104
285
|
created_items.append(res)
|
|
105
286
|
return created_items
|
|
106
287
|
|
|
288
|
+
@keyword("Ensure Customer Exist")
|
|
289
|
+
def ensure_customer_exist(self, customer_name: str, customer_group="Commercial", territory="Vietnam"):
|
|
290
|
+
"""
|
|
291
|
+
Đảm bảo Customer tồn tại.
|
|
292
|
+
|
|
293
|
+
API: /api/resource/Customer
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
customer_name: Tên khách hàng
|
|
297
|
+
customer_group: Nhóm khách hàng (mặc định: Commercial)
|
|
298
|
+
territory: Khu vực (mặc định: Vietnam)
|
|
299
|
+
"""
|
|
300
|
+
if self.exists("Customer", customer_name):
|
|
301
|
+
return {"status": "exists", "name": customer_name}
|
|
302
|
+
|
|
303
|
+
data = {
|
|
304
|
+
"doctype": "Customer",
|
|
305
|
+
"customer_name": customer_name,
|
|
306
|
+
"customer_type": "Company",
|
|
307
|
+
"customer_group": customer_group,
|
|
308
|
+
"territory": territory,
|
|
309
|
+
}
|
|
310
|
+
return self.create_resource("Customer", data)
|
|
311
|
+
|
|
312
|
+
# ===========================================================
|
|
313
|
+
# Sales APIs - Quy trình bán hàng
|
|
314
|
+
# ===========================================================
|
|
315
|
+
|
|
316
|
+
@keyword("Create Sales Order")
|
|
317
|
+
def create_sales_order(self, customer: str, items: list, delivery_date: str, company: str):
|
|
318
|
+
"""
|
|
319
|
+
Tạo Sales Order (Đơn hàng bán).
|
|
320
|
+
|
|
321
|
+
API: /api/resource/Sales Order
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
customer: Tên khách hàng
|
|
325
|
+
items: Danh sách items [{"item_code": "ITEM-001", "qty": 10, "rate": 1000}]
|
|
326
|
+
delivery_date: Ngày giao hàng (format: YYYY-MM-DD)
|
|
327
|
+
company: Tên công ty
|
|
328
|
+
|
|
329
|
+
Example:
|
|
330
|
+
| ${items}= | Create List | {"item_code": "ITEM-001", "qty": 10, "rate": 1000} |
|
|
331
|
+
| Create Sales Order | Customer A | ${items} | 2025-12-01 | My Company |
|
|
332
|
+
"""
|
|
333
|
+
self.ensure_customer_exist(customer)
|
|
334
|
+
|
|
335
|
+
if isinstance(items, str):
|
|
336
|
+
items = json.loads(items)
|
|
337
|
+
|
|
338
|
+
order_items = []
|
|
339
|
+
for item in items:
|
|
340
|
+
order_items.append({
|
|
341
|
+
"item_code": item["item_code"],
|
|
342
|
+
"qty": item["qty"],
|
|
343
|
+
"rate": item.get("rate", 0),
|
|
344
|
+
"delivery_date": delivery_date,
|
|
345
|
+
"uom": item.get("uom", "Nos"),
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
data = {
|
|
349
|
+
"doctype": "Sales Order",
|
|
350
|
+
"customer": customer,
|
|
351
|
+
"company": company,
|
|
352
|
+
"delivery_date": delivery_date,
|
|
353
|
+
"items": order_items
|
|
354
|
+
}
|
|
355
|
+
return self.create_resource("Sales Order", data)
|
|
356
|
+
|
|
357
|
+
@keyword("Create Sales Invoice")
|
|
358
|
+
def create_sales_invoice(self, customer: str, items: list, company: str, posting_date=None):
|
|
359
|
+
"""
|
|
360
|
+
Tạo Sales Invoice (Hóa đơn bán hàng).
|
|
361
|
+
|
|
362
|
+
API: /api/resource/Sales Invoice
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
customer: Tên khách hàng
|
|
366
|
+
items: Danh sách items [{"item_code": "ITEM-001", "qty": 10, "rate": 1000}]
|
|
367
|
+
company: Tên công ty
|
|
368
|
+
posting_date: Ngày lập hóa đơn (mặc định: ngày hiện tại)
|
|
369
|
+
"""
|
|
370
|
+
self.ensure_customer_exist(customer)
|
|
371
|
+
|
|
372
|
+
if isinstance(items, str):
|
|
373
|
+
items = json.loads(items)
|
|
374
|
+
|
|
375
|
+
invoice_items = []
|
|
376
|
+
for item in items:
|
|
377
|
+
invoice_items.append({
|
|
378
|
+
"item_code": item["item_code"],
|
|
379
|
+
"qty": item["qty"],
|
|
380
|
+
"rate": item.get("rate", 0),
|
|
381
|
+
"uom": item.get("uom", "Nos"),
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
data = {
|
|
385
|
+
"doctype": "Sales Invoice",
|
|
386
|
+
"customer": customer,
|
|
387
|
+
"company": company,
|
|
388
|
+
"items": invoice_items
|
|
389
|
+
}
|
|
390
|
+
if posting_date:
|
|
391
|
+
data["posting_date"] = posting_date
|
|
392
|
+
|
|
393
|
+
return self.create_resource("Sales Invoice", data)
|
|
394
|
+
|
|
395
|
+
# ===========================================================
|
|
396
|
+
# Stock/Inventory APIs - Quản lý kho
|
|
397
|
+
# ===========================================================
|
|
398
|
+
|
|
399
|
+
@keyword("Create Stock Entry")
|
|
400
|
+
def create_stock_entry(self, purpose: str, items: list, company: str, posting_date=None):
|
|
401
|
+
"""
|
|
402
|
+
Tạo Stock Entry (Phiếu nhập/xuất kho).
|
|
403
|
+
|
|
404
|
+
API: /api/resource/Stock Entry
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
purpose: Mục đích (Material Receipt, Material Issue, Material Transfer, etc.)
|
|
408
|
+
items: Danh sách items với warehouse
|
|
409
|
+
company: Tên công ty
|
|
410
|
+
posting_date: Ngày lập phiếu
|
|
411
|
+
|
|
412
|
+
Purpose types:
|
|
413
|
+
- Material Receipt: Nhập kho
|
|
414
|
+
- Material Issue: Xuất kho
|
|
415
|
+
- Material Transfer: Chuyển kho
|
|
416
|
+
- Manufacture: Sản xuất
|
|
417
|
+
- Repack: Đóng gói lại
|
|
418
|
+
|
|
419
|
+
Example:
|
|
420
|
+
| ${items}= | Create List | {"item_code": "ITEM-001", "qty": 100, "t_warehouse": "Stores - EDU"} |
|
|
421
|
+
| Create Stock Entry | Material Receipt | ${items} | My Company |
|
|
422
|
+
"""
|
|
423
|
+
stock_items = []
|
|
424
|
+
for item in items:
|
|
425
|
+
stock_items.append({
|
|
426
|
+
"item_code": item["item_code"],
|
|
427
|
+
"qty": item["qty"],
|
|
428
|
+
"s_warehouse": item.get("s_warehouse"), # Source warehouse
|
|
429
|
+
"t_warehouse": item.get("t_warehouse"), # Target warehouse
|
|
430
|
+
"uom": item.get("uom", "Nos"),
|
|
431
|
+
"stock_uom": item.get("stock_uom", "Nos"),
|
|
432
|
+
"conversion_factor": item.get("conversion_factor", 1.0),
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
data = {
|
|
436
|
+
"doctype": "Stock Entry",
|
|
437
|
+
"purpose": purpose,
|
|
438
|
+
"company": company,
|
|
439
|
+
"items": stock_items
|
|
440
|
+
}
|
|
441
|
+
if posting_date:
|
|
442
|
+
data["posting_date"] = posting_date
|
|
443
|
+
|
|
444
|
+
return self.create_resource("Stock Entry", data)
|
|
445
|
+
|
|
446
|
+
@keyword("Create Purchase Receipt")
|
|
447
|
+
def create_purchase_receipt(self, supplier: str, items: list, company: str, posting_date=None):
|
|
448
|
+
"""
|
|
449
|
+
Tạo Purchase Receipt (Phiếu nhận hàng mua).
|
|
450
|
+
|
|
451
|
+
API: /api/resource/Purchase Receipt
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
supplier: Tên nhà cung cấp
|
|
455
|
+
items: Danh sách items [{"item_code": "ITEM-001", "qty": 100, "rate": 1000, "warehouse": "Stores - EDU"}]
|
|
456
|
+
company: Tên công ty
|
|
457
|
+
posting_date: Ngày nhận hàng
|
|
458
|
+
"""
|
|
459
|
+
self.ensure_supplier_exist(supplier)
|
|
460
|
+
|
|
461
|
+
if isinstance(items, str):
|
|
462
|
+
items = json.loads(items)
|
|
463
|
+
|
|
464
|
+
receipt_items = []
|
|
465
|
+
for item in items:
|
|
466
|
+
receipt_items.append({
|
|
467
|
+
"item_code": item["item_code"],
|
|
468
|
+
"qty": item["qty"],
|
|
469
|
+
"rate": item.get("rate", 0),
|
|
470
|
+
"warehouse": item.get("warehouse"),
|
|
471
|
+
"uom": item.get("uom", "Nos"),
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
data = {
|
|
475
|
+
"doctype": "Purchase Receipt",
|
|
476
|
+
"supplier": supplier,
|
|
477
|
+
"company": company,
|
|
478
|
+
"items": receipt_items
|
|
479
|
+
}
|
|
480
|
+
if posting_date:
|
|
481
|
+
data["posting_date"] = posting_date
|
|
482
|
+
|
|
483
|
+
return self.create_resource("Purchase Receipt", data)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
@keyword("Create Purchase Order")
|
|
488
|
+
def create_purchase_order(self, supplier: str, items: list, company: str, schedule_date=None):
|
|
489
|
+
"""
|
|
490
|
+
Tạo Purchase Order (Đơn hàng mua).
|
|
491
|
+
|
|
492
|
+
API: /api/resource/Purchase Order
|
|
493
|
+
|
|
494
|
+
Args:
|
|
495
|
+
supplier: Tên nhà cung cấp
|
|
496
|
+
items: Danh sách items [{"item_code": "ITEM-001", "qty": 100, "rate": 1000}]
|
|
497
|
+
company: Tên công ty
|
|
498
|
+
schedule_date: Ngày cần hàng
|
|
499
|
+
"""
|
|
500
|
+
self.ensure_supplier_exist(supplier)
|
|
501
|
+
|
|
502
|
+
if isinstance(items, str):
|
|
503
|
+
items = json.loads(items)
|
|
504
|
+
|
|
505
|
+
po_items = []
|
|
506
|
+
for item in items:
|
|
507
|
+
po_items.append({
|
|
508
|
+
"item_code": item["item_code"],
|
|
509
|
+
"qty": item["qty"],
|
|
510
|
+
"rate": item.get("rate", 0),
|
|
511
|
+
"schedule_date": item.get("schedule_date", schedule_date),
|
|
512
|
+
"warehouse": item.get("warehouse"),
|
|
513
|
+
"uom": item.get("uom", "Nos"),
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
data = {
|
|
517
|
+
"doctype": "Purchase Order",
|
|
518
|
+
"supplier": supplier,
|
|
519
|
+
"company": company,
|
|
520
|
+
"items": po_items,
|
|
521
|
+
"schedule_date": schedule_date
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return self.create_resource("Purchase Order", data)
|
|
525
|
+
|
|
107
526
|
# ===========================================================
|
|
108
|
-
#
|
|
527
|
+
# Accounting APIs - Kế toán
|
|
528
|
+
# ===========================================================
|
|
529
|
+
|
|
530
|
+
@keyword("Create Purchase Invoice")
|
|
531
|
+
def create_purchase_invoice(self, supplier: str, items: list, company: str, posting_date=None):
|
|
532
|
+
"""
|
|
533
|
+
Tạo Purchase Invoice (Hóa đơn mua hàng).
|
|
534
|
+
|
|
535
|
+
API: /api/resource/Purchase Invoice
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
supplier: Tên nhà cung cấp
|
|
539
|
+
items: Danh sách items [{"item_code": "ITEM-001", "qty": 100, "rate": 1000}]
|
|
540
|
+
company: Tên công ty
|
|
541
|
+
posting_date: Ngày lập hóa đơn
|
|
542
|
+
"""
|
|
543
|
+
self.ensure_supplier_exist(supplier)
|
|
544
|
+
|
|
545
|
+
invoice_items = []
|
|
546
|
+
for item in items:
|
|
547
|
+
invoice_items.append({
|
|
548
|
+
"item_code": item["item_code"],
|
|
549
|
+
"qty": item["qty"],
|
|
550
|
+
"rate": item.get("rate", 0),
|
|
551
|
+
"uom": item.get("uom", "Nos"),
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
data = {
|
|
555
|
+
"doctype": "Purchase Invoice",
|
|
556
|
+
"supplier": supplier,
|
|
557
|
+
"company": company,
|
|
558
|
+
"items": invoice_items
|
|
559
|
+
}
|
|
560
|
+
if posting_date:
|
|
561
|
+
data["posting_date"] = posting_date
|
|
562
|
+
|
|
563
|
+
return self.create_resource("Purchase Invoice", data)
|
|
564
|
+
|
|
565
|
+
@keyword("Create Payment Entry")
|
|
566
|
+
def create_payment_entry(self, payment_type: str, party_type: str, party: str,
|
|
567
|
+
paid_amount: float, company: str, posting_date=None):
|
|
568
|
+
"""
|
|
569
|
+
Tạo Payment Entry (Phiếu thanh toán).
|
|
570
|
+
|
|
571
|
+
API: /api/resource/Payment Entry
|
|
572
|
+
|
|
573
|
+
Args:
|
|
574
|
+
payment_type: Loại thanh toán (Receive, Pay, Internal Transfer)
|
|
575
|
+
party_type: Loại đối tác (Customer, Supplier, Employee)
|
|
576
|
+
party: Tên đối tác
|
|
577
|
+
paid_amount: Số tiền thanh toán
|
|
578
|
+
company: Tên công ty
|
|
579
|
+
posting_date: Ngày thanh toán
|
|
580
|
+
|
|
581
|
+
Example:
|
|
582
|
+
| Create Payment Entry | Receive | Customer | Customer A | 10000000 | My Company |
|
|
583
|
+
| Create Payment Entry | Pay | Supplier | Supplier B | 5000000 | My Company |
|
|
584
|
+
"""
|
|
585
|
+
data = {
|
|
586
|
+
"doctype": "Payment Entry",
|
|
587
|
+
"payment_type": payment_type,
|
|
588
|
+
"party_type": party_type,
|
|
589
|
+
"party": party,
|
|
590
|
+
"paid_amount": paid_amount,
|
|
591
|
+
"received_amount": paid_amount if payment_type == "Receive" else 0,
|
|
592
|
+
"company": company,
|
|
593
|
+
}
|
|
594
|
+
if posting_date:
|
|
595
|
+
data["posting_date"] = posting_date
|
|
596
|
+
|
|
597
|
+
return self.create_resource("Payment Entry", data)
|
|
598
|
+
|
|
599
|
+
# ===========================================================
|
|
600
|
+
# Procurement - Quy trình mua hàng (đã có sẵn)
|
|
109
601
|
# ===========================================================
|
|
110
602
|
|
|
111
603
|
@keyword("Load Excel Request")
|
|
@@ -158,6 +650,9 @@ class ERPNextLibrary:
|
|
|
158
650
|
@keyword("Send RFQ From Material Request")
|
|
159
651
|
def send_rfq_from_mr(self, mr_name: str, suppliers: list):
|
|
160
652
|
"""Tạo RFQ dựa trên Material Request."""
|
|
653
|
+
if isinstance(suppliers, str):
|
|
654
|
+
suppliers = json.loads(suppliers)
|
|
655
|
+
|
|
161
656
|
mr_doc = self.get_doc("Material Request", mr_name)
|
|
162
657
|
company = mr_doc["company"]
|
|
163
658
|
self.ensure_warehouse_exist(f"Stores - EDU", company)
|
|
@@ -178,7 +673,7 @@ class ERPNextLibrary:
|
|
|
178
673
|
rfq_data = {
|
|
179
674
|
"doctype": "Request for Quotation",
|
|
180
675
|
"company": company,
|
|
181
|
-
"transaction_date": "
|
|
676
|
+
"transaction_date": mr_doc["schedule_date"],
|
|
182
677
|
"suppliers": [{"supplier": s} for s in suppliers],
|
|
183
678
|
"items": items,
|
|
184
679
|
"message_for_supplier": "Xin vui lòng gửi báo giá."
|
|
@@ -190,30 +685,69 @@ class ERPNextLibrary:
|
|
|
190
685
|
# ===========================================================
|
|
191
686
|
|
|
192
687
|
@keyword("Receive Supplier Quotation")
|
|
193
|
-
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
|
+
"""
|
|
194
699
|
rfq_doc = self.get_doc("Request for Quotation", rfq_name)
|
|
195
700
|
company = rfq_doc["company"]
|
|
196
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
|
+
})
|
|
197
742
|
|
|
198
743
|
sq_data = {
|
|
199
744
|
"doctype": "Supplier Quotation",
|
|
200
745
|
"supplier": supplier_name,
|
|
201
746
|
"company": company,
|
|
202
|
-
"transaction_date": "
|
|
203
|
-
"items":
|
|
747
|
+
"transaction_date": "2026-01-28",
|
|
748
|
+
"items": items_list,
|
|
204
749
|
}
|
|
205
750
|
|
|
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
751
|
return self.create_resource("Supplier Quotation", sq_data)
|
|
218
752
|
|
|
219
753
|
# ===========================================================
|
|
@@ -235,7 +769,7 @@ class ERPNextLibrary:
|
|
|
235
769
|
"item_code": d["item_code"],
|
|
236
770
|
"qty": d["qty"],
|
|
237
771
|
"rate": d["rate"],
|
|
238
|
-
"schedule_date": "
|
|
772
|
+
"schedule_date": "2026-02-28",
|
|
239
773
|
"warehouse": f"Stores - EDU",
|
|
240
774
|
"uom": d.get("uom", "Nos"),
|
|
241
775
|
"conversion_factor": 1.0
|
|
@@ -245,7 +779,7 @@ class ERPNextLibrary:
|
|
|
245
779
|
"doctype": "Purchase Order",
|
|
246
780
|
"supplier": supplier,
|
|
247
781
|
"company": company,
|
|
248
|
-
"schedule_date": "
|
|
782
|
+
"schedule_date": "2026-02-28",
|
|
249
783
|
"items": items
|
|
250
784
|
}
|
|
251
785
|
return self.create_resource("Purchase Order", po_data)
|
RPA/__init__.py
ADDED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rpa-erpnext
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: Robot Framework library for automating ERPNext using REST API
|
|
5
5
|
Author-email: Your Name <youremail@example.com>
|
|
6
6
|
License: MIT
|
|
@@ -12,4 +12,16 @@ Description-Content-Type: text/markdown
|
|
|
12
12
|
License-File: LICENSE
|
|
13
13
|
Requires-Dist: requests>=2.28.0
|
|
14
14
|
Requires-Dist: robotframework>=6.0
|
|
15
|
+
Requires-Dist: pandas>=2.0.0
|
|
16
|
+
Requires-Dist: openpyxl>=3.1.0
|
|
15
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,7 @@
|
|
|
1
|
+
RPA/ERPNext.py,sha256=Fh0br8ORteXKiUsVJdBECODRydQcd8_mWkoX6F2DJZU,28532
|
|
2
|
+
RPA/__init__.py,sha256=doHeY54VPWzBDnkSMGvGGSYfkpxt4dFXAV8YOi-Ebig,110
|
|
3
|
+
rpa_erpnext-1.0.4.dist-info/licenses/LICENSE,sha256=sexHbU6pNlqsmC_TQZJLdZ59qC_Bym7mQ0HrNeHHQ3Y,1063
|
|
4
|
+
rpa_erpnext-1.0.4.dist-info/METADATA,sha256=LgYI9s6M5M4laPMK6I8lkwCA5KrSpVLEErVxbpJvnUU,800
|
|
5
|
+
rpa_erpnext-1.0.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
6
|
+
rpa_erpnext-1.0.4.dist-info/top_level.txt,sha256=s4yaEXbcdPUcIyAxSvrLKKIMD62cqoNPsOGmdx94U5k,4
|
|
7
|
+
rpa_erpnext-1.0.4.dist-info/RECORD,,
|
RPA/init.py
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
RPA/ERPNext.py,sha256=2xeclyrQiKgPnEgnqWe1ijmuxASsSejoaWKBcgNqkqk,9129
|
|
2
|
-
RPA/init.py,sha256=hhB7KVQqlO3Yo3ScIulQMQ-obZbdGBPe0YIgtVyqSt8,118
|
|
3
|
-
rpa_erpnext-1.0.0.dist-info/licenses/LICENSE,sha256=sexHbU6pNlqsmC_TQZJLdZ59qC_Bym7mQ0HrNeHHQ3Y,1063
|
|
4
|
-
rpa_erpnext-1.0.0.dist-info/METADATA,sha256=HKy1L1P-k7bIixS1dQk7BllIDEABSXDNGDRWaBHCcaM,541
|
|
5
|
-
rpa_erpnext-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
-
rpa_erpnext-1.0.0.dist-info/top_level.txt,sha256=s4yaEXbcdPUcIyAxSvrLKKIMD62cqoNPsOGmdx94U5k,4
|
|
7
|
-
rpa_erpnext-1.0.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|