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.
@@ -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,5 @@
1
+ from .ERPNext import ERPNextLibrary
2
+
3
+ __version__ = "1.0.0"
4
+ __author__ = "Mahara"
5
+ __all__ = ["ERPNextLibrary"]
@@ -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,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
+ ```
@@ -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,4 @@
1
+ requests>=2.28.0
2
+ robotframework>=6.0
3
+ pandas>=2.0.0
4
+ openpyxl>=3.1.0
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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()