rpa-erpnext 1.0.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rpa-erpnext
3
- Version: 1.0.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,787 @@
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
+ 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)")
110
+
111
+ # ===========================================================
112
+ # Utility Methods
113
+ # ===========================================================
114
+
115
+ def create_resource(self, doctype, data):
116
+ res = self.session.post(f"{self.base_url}/api/resource/{doctype}", json=data)
117
+ if not res.ok:
118
+ raise Exception(f"HTTP {res.status_code}: {res.text}")
119
+ return res.json().get("data", res.json())
120
+
121
+ def get_doc(self, doctype, name):
122
+ res = self.session.get(f"{self.base_url}/api/resource/{doctype}/{name}")
123
+ res.raise_for_status()
124
+ return res.json().get("data")
125
+
126
+ def exists(self, doctype, name):
127
+ """Kiểm tra xem tài nguyên có tồn tại hay không."""
128
+ res = self.session.get(f"{self.base_url}/api/resource/{doctype}/{name}")
129
+ return res.ok
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
+
213
+ # ===========================================================
214
+ # Ensure Functions - Master Data
215
+ # ===========================================================
216
+
217
+ @keyword("Ensure Company Exist")
218
+ def ensure_company_exist(self, company: str, abbr: str):
219
+ """Đảm bảo Company tồn tại."""
220
+ if self.exists("Company", company):
221
+ return {"status": "exists", "name": company}
222
+
223
+ data = {
224
+ "doctype": "Company",
225
+ "company_name": company,
226
+ "abbr": abbr,
227
+ "default_currency": "VND",
228
+ "country": "Vietnam",
229
+ "company_logo": None
230
+ }
231
+ return self.create_resource("Company", data)
232
+
233
+ @keyword("Ensure Warehouse Exist")
234
+ def ensure_warehouse_exist(self, warehouse: str, company: str):
235
+ """Đảm bảo Warehouse tồn tại."""
236
+ if self.exists("Warehouse", warehouse):
237
+ return {"status": "exists", "name": warehouse}
238
+
239
+ data = {
240
+ "doctype": "Warehouse",
241
+ "warehouse_name": warehouse,
242
+ "company": company,
243
+ }
244
+ return self.create_resource("Warehouse", data)
245
+
246
+ @keyword("Ensure Supplier Exist")
247
+ def ensure_supplier_exist(self, supplier_name: str):
248
+ """Đảm bảo Supplier tồn tại."""
249
+ if self.exists("Supplier", supplier_name):
250
+ return {"status": "exists", "name": supplier_name}
251
+
252
+ data = {
253
+ "doctype": "Supplier",
254
+ "supplier_name": supplier_name,
255
+ "supplier_type": "Company",
256
+ "country": "Vietnam",
257
+ }
258
+ return self.create_resource("Supplier", data)
259
+
260
+ @keyword("Ensure Items Exist")
261
+ def ensure_items_exist(self, items_json):
262
+ """Đảm bảo tất cả item tồn tại."""
263
+ if isinstance(items_json, str):
264
+ items = json.loads(items_json)
265
+ else:
266
+ items = items_json
267
+
268
+ created_items = []
269
+ for i in items:
270
+ code = i["item_code"]
271
+ if self.exists("Item", code):
272
+ created_items.append({"status": "exists", "item_code": code})
273
+ continue
274
+
275
+ data = {
276
+ "doctype": "Item",
277
+ "item_code": code,
278
+ "item_name": code,
279
+ "description": i.get("description", code),
280
+ "stock_uom": "Nos",
281
+ "is_stock_item": 1,
282
+ "item_group": "All Item Groups",
283
+ }
284
+ res = self.create_resource("Item", data)
285
+ created_items.append(res)
286
+ return created_items
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
+
526
+ # ===========================================================
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)
601
+ # ===========================================================
602
+
603
+ @keyword("Load Excel Request")
604
+ def load_excel_request(self, path: str):
605
+ """Đọc file Excel chứa company, itemcode, quantity."""
606
+ df = pd.read_excel(path)
607
+ return df.to_dict(orient="records")
608
+
609
+ @keyword("Create Material Request From Excel")
610
+ def create_material_request_from_excel(self, path: str, schedule_date: str):
611
+ """Tạo Material Request từ file Excel."""
612
+ rows = self.load_excel_request(path)
613
+ if not rows:
614
+ raise Exception("Excel file không có dữ liệu.")
615
+
616
+ company = rows[0]["company"]
617
+ self.ensure_company_exist(company, "EDU")
618
+ self.ensure_warehouse_exist(f"Stores - EDU", company)
619
+
620
+ items = []
621
+ for row in rows:
622
+ self.ensure_items_exist([{
623
+ "item_code": row["itemcode"],
624
+ "description": row["itemcode"]
625
+ }])
626
+ items.append({
627
+ "item_code": row["itemcode"],
628
+ "description": row["itemcode"],
629
+ "qty": float(row["quantity"]),
630
+ "schedule_date": schedule_date,
631
+ "warehouse": f"Stores - EDU",
632
+ "uom": "Nos",
633
+ "stock_uom": "Nos",
634
+ "conversion_factor": 1.0
635
+ })
636
+
637
+ data = {
638
+ "doctype": "Material Request",
639
+ "material_request_type": "Purchase",
640
+ "company": company,
641
+ "schedule_date": schedule_date,
642
+ "items": items
643
+ }
644
+ return self.create_resource("Material Request", data)
645
+
646
+ # ===========================================================
647
+ # Step 2: Gửi RFQ
648
+ # ===========================================================
649
+
650
+ @keyword("Send RFQ From Material Request")
651
+ def send_rfq_from_mr(self, mr_name: str, suppliers: list):
652
+ """Tạo RFQ dựa trên Material Request."""
653
+ if isinstance(suppliers, str):
654
+ suppliers = json.loads(suppliers)
655
+
656
+ mr_doc = self.get_doc("Material Request", mr_name)
657
+ company = mr_doc["company"]
658
+ self.ensure_warehouse_exist(f"Stores - EDU", company)
659
+ for s in suppliers:
660
+ self.ensure_supplier_exist(s)
661
+
662
+ items = []
663
+ for d in mr_doc["items"]:
664
+ items.append({
665
+ "item_code": d["item_code"],
666
+ "qty": d["qty"],
667
+ "uom": d.get("uom", "Nos"),
668
+ "stock_uom": d.get("stock_uom", "Nos"),
669
+ "conversion_factor": 1.0,
670
+ "warehouse": d["warehouse"],
671
+ })
672
+
673
+ rfq_data = {
674
+ "doctype": "Request for Quotation",
675
+ "company": company,
676
+ "transaction_date": mr_doc["schedule_date"],
677
+ "suppliers": [{"supplier": s} for s in suppliers],
678
+ "items": items,
679
+ "message_for_supplier": "Xin vui lòng gửi báo giá."
680
+ }
681
+ return self.create_resource("Request for Quotation", rfq_data)
682
+
683
+ # ===========================================================
684
+ # Step 3: Supplier gửi báo giá
685
+ # ===========================================================
686
+
687
+ @keyword("Receive Supplier Quotation")
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
+ """
699
+ rfq_doc = self.get_doc("Request for Quotation", rfq_name)
700
+ company = rfq_doc["company"]
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
+ })
742
+
743
+ sq_data = {
744
+ "doctype": "Supplier Quotation",
745
+ "supplier": supplier_name,
746
+ "company": company,
747
+ "transaction_date": "2026-01-28",
748
+ "items": items_list,
749
+ }
750
+
751
+ return self.create_resource("Supplier Quotation", sq_data)
752
+
753
+ # ===========================================================
754
+ # Step 4: Tạo Purchase Order
755
+ # ===========================================================
756
+
757
+ @keyword("Create Purchase Order From Quotation")
758
+ def create_purchase_order_from_quotation(self, quotation_name: str):
759
+ sq_doc = self.get_doc("Supplier Quotation", quotation_name)
760
+ supplier = sq_doc["supplier"]
761
+ company = sq_doc["company"]
762
+
763
+ self.ensure_supplier_exist(supplier)
764
+ self.ensure_warehouse_exist(f"Stores - EDU", company)
765
+
766
+ items = []
767
+ for d in sq_doc["items"]:
768
+ items.append({
769
+ "item_code": d["item_code"],
770
+ "qty": d["qty"],
771
+ "rate": d["rate"],
772
+ "schedule_date": "2026-02-28",
773
+ "warehouse": f"Stores - EDU",
774
+ "uom": d.get("uom", "Nos"),
775
+ "conversion_factor": 1.0
776
+ })
777
+
778
+ po_data = {
779
+ "doctype": "Purchase Order",
780
+ "supplier": supplier,
781
+ "company": company,
782
+ "schedule_date": "2026-02-28",
783
+ "items": items
784
+ }
785
+ return self.create_resource("Purchase Order", po_data)
786
+
787
+
@@ -0,0 +1,5 @@
1
+ from .ERPNext import ERPNextLibrary
2
+
3
+ __version__ = "1.0.0"
4
+ __author__ = "Mahara"
5
+ __all__ = ["ERPNextLibrary"]
@@ -4,9 +4,9 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rpa-erpnext"
7
- version = "1.0.0"
7
+ version = "1.0.4"
8
8
  description = "Robot Framework library for automating ERPNext using REST API"
9
- readme = "README.md"
9
+ readme = "readme.md"
10
10
  requires-python = ">=3.8"
11
11
  license = {text = "MIT"}
12
12
  authors = [
@@ -15,9 +15,16 @@ authors = [
15
15
  keywords = ["robotframework", "erpnext", "rpa", "automation"]
16
16
  dependencies = [
17
17
  "requests>=2.28.0",
18
- "robotframework>=6.0"
18
+ "robotframework>=6.0",
19
+ "pandas>=2.0.0",
20
+ "openpyxl>=3.1.0"
19
21
  ]
20
22
 
21
23
  [project.urls]
22
24
  Homepage = "https://github.com/yourusername/rpa-erpnext"
23
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,9 @@
1
+ # RPA.ERPNext
2
+
3
+ Library for automating [ERPNext](https://erpnext.com/) using REST API, built for [Robot Framework](https://robotframework.org/).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install rpa-erpnext
9
+ ```
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rpa-erpnext
3
- Version: 1.0.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
+ ```
@@ -1,9 +1,11 @@
1
1
  LICENSE
2
2
  pyproject.toml
3
+ readme.md
3
4
  RPA/ERPNext.py
4
- RPA/init.py
5
+ RPA/__init__.py
5
6
  rpa_erpnext.egg-info/PKG-INFO
6
7
  rpa_erpnext.egg-info/SOURCES.txt
7
8
  rpa_erpnext.egg-info/dependency_links.txt
8
9
  rpa_erpnext.egg-info/requires.txt
9
- rpa_erpnext.egg-info/top_level.txt
10
+ rpa_erpnext.egg-info/top_level.txt
11
+ test/test_functions.py
@@ -1,2 +1,4 @@
1
1
  requests>=2.28.0
2
2
  robotframework>=6.0
3
+ pandas>=2.0.0
4
+ openpyxl>=3.1.0
@@ -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()
@@ -1,253 +0,0 @@
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
-
@@ -1,6 +0,0 @@
1
- # src/RPA/__init__.py
2
- from .ERPNext import ERPNext
3
-
4
- __version__ = "1.0.0"
5
- __author__ = "Mahara"
6
- __all__ = ["ERPNext"]
File without changes
File without changes