from django.test import Client from app.workflow_registry import register_action from app.workflow_utils import resolve_value from app.document_generator import DocumentGenerator from app.jobemail import EmailJobRunner from app.payment import account_entry_api from django.apps import apps import re import datetime client = Client() # ============================ # Logic xử lý Map Expression ($map) # ============================ def handle_map_expression(expression, context): """ Xử lý biểu thức đặc biệt để biến đổi danh sách dữ liệu. Cú pháp: $map(data.installments, {amount: amount, due_date: $add_days(created_at, gap)}) """ # Regex tách nguồn dữ liệu và template match = re.match(r"^\$map\(([^,]+),\s*\{(.*)\}\)$", expression.strip()) if not match: return [] source_path = match.group(1).strip() template_content = match.group(2).strip() # Lấy danh sách dữ liệu gốc từ context (ví dụ: data.installments) source_data = resolve_value(source_path, context) if not isinstance(source_data, list): return [] # Tìm các cặp key: value trong template định nghĩa # Hỗ trợ cả trường hợp value là một hàm lồng như $add_days pairs = re.findall(r"(\w+):\s*(\$add_days\([^)]+\)|[^{},]+)", template_content) results = [] for index, item in enumerate(source_data): # Tạo context riêng cho từng item để resolve item_context = {**context, "item": item, "index": index} processed_row = {} for key, val_expr in pairs: val_expr = val_expr.strip() # 1. Xử lý biến chỉ mục ($index) if val_expr == "$index": processed_row[key] = index # 2. Xử lý hàm cộng ngày ($add_days) elif "$add_days" in val_expr: m = re.search(r"\$add_days\(([^,]+),\s*([^)]+)\)", val_expr) if m: base_key = m.group(1).strip() days_key = m.group(2).strip() # Tìm giá trị ngày gốc và số ngày cần cộng base_date = item.get(base_key) if base_key in item else resolve_value(base_key, item_context) days = item.get(days_key) if days_key in item else resolve_value(days_key, item_context) try: # Chuyển đổi string sang date object if isinstance(base_date, str): base_date = datetime.datetime.strptime(base_date[:10], "%Y-%m-%d").date() new_date = base_date + datetime.timedelta(days=int(days or 0)) processed_row[key] = new_date.isoformat() except Exception: processed_row[key] = str(base_date) # Trả về bản gốc nếu lỗi # 3. Xử lý lấy giá trị từ item hiện tại hoặc context chung else: if val_expr == "$item": processed_row[key] = item elif val_expr == "$index": processed_row[key] = index elif val_expr in item: processed_row[key] = item[val_expr] else: processed_row[key] = resolve_value(val_expr, item_context) results.append(processed_row) return results # ============================ # CRUD thông qua API có sẵn # ============================ def deep_resolve_values(data, context): if isinstance(data, dict): return {k: deep_resolve_values(v, context) for k, v in data.items()} elif isinstance(data, list): return [deep_resolve_values(item, context) for item in data] elif isinstance(data, str): # Workaround for resolver bug: handle strings that are only a placeholder match = re.fullmatch(r"\{([^}]+)\}", data) if match: # The path is the content inside the braces, e.g., "transaction_detail.id" path = match.group(1) # resolve_value works on raw paths, so call it directly return resolve_value(path, context) else: # This handles complex strings like "/prefix/{path}/" or normal strings return resolve_value(data, context) else: return data @register_action("API_CALL", schema={"required": ["method", "url"]}) def api_call_action(params, context): """Thực hiện gọi API nội bộ bằng Django Test Client""" method = params["method"].upper() url = resolve_value(params["url"], context) save_as = params.get("save_as") raw_body = params.get("body") # ============================ # Resolve body # ============================ if isinstance(raw_body, str) and raw_body.startswith("$map"): body = handle_map_expression(raw_body, context) elif isinstance(raw_body, dict): body = deep_resolve_values(raw_body, context) elif raw_body is None: body = None else: body = resolve_value(raw_body, context) print(f" [API_CALL] {method} {url}") print(f" [API_CALL] Resolved Body: {body}") # ============================ # Execute request # ============================ if method == "POST": resp = client.post(url, body, content_type="application/json") elif method == "PATCH": resp = client.patch(url, body, content_type="application/json") elif method == "PUT": resp = client.put(url, body, content_type="application/json") elif method == "DELETE": resp = client.delete(url) else: resp = client.get(url) print(f" [API_CALL] Status Code: {resp.status_code}") # ============================ # Handle error # ============================ if resp.status_code >= 400: error_content = resp.content.decode("utf-8") if resp.content else "" print(f" [API_CALL] Error: {error_content}") raise Exception(f"API Call failed: {error_content}") # ============================ # Handle response safely # ============================ if resp.status_code == 204 or not resp.content: # DELETE / No Content result = {"deleted": True} else: try: result = resp.json() except ValueError: # Fallback nếu response không phải JSON result = resp.content.decode("utf-8") print(f" [API_CALL] Result: {result}") if save_as: context[save_as] = result return result # ============================ # Gọi Utility / API bên ngoài # ============================ @register_action("CALL_UTILITY", schema={"required": ["utility_code"]}) def call_utility_action(params, context): Utility = apps.get_model("app", "Utility") util = Utility.objects.get(code=params["utility_code"]) module_path = util.integration_module if not module_path: return {"error": "utility has no module"} from django.utils.module_loading import import_string func = import_string(module_path) resolved_params = {k: resolve_value(v, context) for k,v in params.get("params", {}).items()} return func(**resolved_params) @register_action("SEND_EMAIL", schema={"required": ["template"]}) def send_email_action(params, context): tpl_name = params["template"] tpl_pks = {k: resolve_value(v, context) for k,v in params.get("context_pks", {}).items()} Email_Template = apps.get_model("app", "Email_Template") try: template_obj = Email_Template.objects.get(name=tpl_name) except Email_Template.DoesNotExist: raise Exception(f"Email template '{tpl_name}' not found") runner = EmailJobRunner(template=template_obj, context_pks=tpl_pks) success = runner.run() return {"sent": success} # ============================ # Tạo Document # ============================ @register_action("GENERATE_DOCUMENT", schema={"required": ["document_code"]}) def generate_document_action(params, context): code = resolve_value(params["document_code"], context) pks = {k: str(resolve_value(v, context)) for k, v in params.get("context_pks", {}).items()} save_as = params.get("save_as") print(f" [GEN_DOC] Generating for code: {code}") gen = DocumentGenerator(document_code=code, context_pks=pks) result = gen.generate() formatted_result = [{ "pdf": result.get("pdf"), "file": result.get("file"), "name": result.get("name"), "code": code }] if save_as: context[save_as] = formatted_result print(f" [GEN_DOC] Success: Saved to context as '{save_as}'") return formatted_result # ============================ # Hạch toán # ============================ @register_action("ACCOUNT_ENTRY", schema={"required": ["amount", "category_code"]}) def account_entry_action(params, context): amount = resolve_value(params["amount"], context) content = params.get("content", "") userid = resolve_value(params.get("userid"), context) return account_entry_api( code=params.get("internal_account", "HOAC02VND"), amount=amount, content=content, type=params.get("type", "CR"), category=apps.get_model("app", "Entry_Category").objects.get(code=params["category_code"]).id, userid=userid, ref=params.get("ref") ) # ============================ # Tìm bản ghi # ============================ @register_action("LOOKUP_DATA", schema={"required": ["model_name", "lookup_field", "lookup_value"]}) def lookup_data_action(params, context): model_name = params["model_name"] field = params["lookup_field"] save_as = params.get("save_as") # Lấy giá trị thực tế (ví dụ: "reserved") value = resolve_value(params["lookup_value"], context) print(f" [LOOKUP] Searching {model_name} where {field} = '{value}'") try: Model = apps.get_model("app", model_name) obj = Model.objects.filter(**{field: value}).first() if not obj: print(f" [LOOKUP] ERROR: Not found!") raise Exception(f"Lookup failed: {model_name} with {field}={value} not found.") if save_as: context[save_as] = obj print(f" [LOOKUP] Success: Found ID {obj.id}, saved to context as '{save_as}'") return obj.id except Exception as e: print(f" [LOOKUP] EXCEPTION: {str(e)}") raise e # ============================ # Quét và phân bổ toàn bộ bút toán còn phần dư # ============================ @register_action("ALLOCATE_ALL_PENDING", schema={}) def allocate_all_pending_action(params, context): """ Quét toàn bộ Internal_Entry có allocation_remain > 0 (type CR), group by product_id, gọi phân bổ cho từng product cho đến khi hết. """ from app.payment import allocate_payment_to_schedules, allocate_penalty_reduction from decimal import Decimal Internal_Entry = apps.get_model("app", "Internal_Entry") Payment_Schedule = apps.get_model("app", "Payment_Schedule") Product_Booked = apps.get_model("app", "Product_Booked") Transaction_Current = apps.get_model("app", "Transaction_Current") Transaction_Detail = apps.get_model("app", "Transaction_Detail") # ---------- Lấy toàn bộ product_id còn entry chưa phân bổ hết ---------- product_ids = list( Internal_Entry.objects.filter( type__code="CR", allocation_remain__gt=0, product__isnull=False ) .values_list("product_id", flat=True) .distinct() ) print(f" [ALLOCATE_ALL] Tìm được {len(product_ids)} product có entry còn phần dư") if not product_ids: return {"total_products": 0, "results": []} # ---------- DEBUG: dump trạng thái trước khi phân bổ ---------- for pid in product_ids: print(f"\n [DEBUG] ===== Product {pid} — trạng thái TRƯỚC phân bổ =====") # Entries entries = Internal_Entry.objects.filter( product_id=pid, type__code="CR", allocation_remain__gt=0 ).order_by("date", "create_time") for e in entries: print(f" Entry id={e.id} | account_id={e.account_id} | amount={e.amount} | allocation_remain={e.allocation_remain} | date={e.date}") # Lấy txn_detail của product booked = Product_Booked.objects.filter(product_id=pid).first() if not booked or not booked.transaction: print(f" !! Không có Product_Booked / Transaction") continue txn = booked.transaction txn_detail = None try: current = Transaction_Current.objects.get(transaction=txn) txn_detail = current.detail except Exception: txn_detail = Transaction_Detail.objects.filter(transaction=txn).order_by("-create_time").first() if not txn_detail: print(f" !! Không có Transaction_Detail") continue # Schedules all_schedules = Payment_Schedule.objects.filter(txn_detail=txn_detail).order_by("cycle", "from_date") unpaid = all_schedules.filter(status__id=1) print(f" Tổng schedule: {all_schedules.count()} | Chưa thanh toán (status=1): {unpaid.count()}") for s in all_schedules: print(f" Schedule id={s.id} | cycle={s.cycle} | status_id={s.status_id} | amount_remain={s.amount_remain} | penalty_remain={s.penalty_remain} | remain_amount={s.remain_amount}") # ---------- Chạy phân bổ ---------- results = [] for product_id in product_ids: try: normal = allocate_payment_to_schedules(product_id) reduction = allocate_penalty_reduction(product_id) results.append({"product_id": product_id, "normal": normal, "reduction": reduction}) print(f" [ALLOCATE_ALL] Product {product_id}: OK — normal={normal}") except Exception as e: print(f" [ALLOCATE_ALL] Product {product_id}: ERROR - {str(e)}") results.append({"product_id": product_id, "error": str(e)}) return {"total_products": len(product_ids), "results": results}