erpclaw You are a Full-Stack ERP Controller for ERPClaw, an AI-native ERP system. You handle all core business operations: company setup, chart of accounts, journal entries, payments, tax, financial reports, customers, sales orders, invoices, suppliers, purchase orders, inventory, usage-based billing, HR (employees, leave, attendance, expenses), and US payroll (salary structures, FICA, income tax withholding, W-2 generation, garnishments). All data lives in a single local SQLite database with full double-entry accounting and immutable audit trail. Security Model - Local-first : All data in .…

)\n if not time_re.match(start_time):\n err(f\"Invalid start_time '{start_time}'. Use HH:MM or HH:MM:SS format\")\n if not time_re.match(end_time):\n err(f\"Invalid end_time '{end_time}'. Use HH:MM or HH:MM:SS format\")\n\n # Check company exists\n co_t = Table(\"company\")\n co_q = Q.from_(co_t).select(co_t.id).where(co_t.id == P())\n if not conn.execute(co_q.get_sql(), (company_id,)).fetchone():\n err(f\"Company {company_id} not found\")\n\n # Check unique name\n st_t = Table(\"shift_type\")\n uniq_q = Q.from_(st_t).select(st_t.id).where(st_t.name == P())\n if conn.execute(uniq_q.get_sql(), (name,)).fetchone():\n err(f\"Shift type '{name}' already exists\")\n\n now = datetime.now(timezone.utc).isoformat()\n shift_id = str(uuid.uuid4())\n\n ins_sql, _ = insert_row(\"shift_type\", {\n \"id\": P(), \"name\": P(), \"start_time\": P(), \"end_time\": P(),\n \"company_id\": P(), \"status\": P(), \"created_at\": P(), \"updated_at\": P(),\n })\n conn.execute(ins_sql, (shift_id, name, start_time, end_time,\n company_id, status, now, now))\n conn.commit()\n\n ok({\n \"shift_type_id\": shift_id,\n \"name\": name,\n \"start_time\": start_time,\n \"end_time\": end_time,\n \"company_id\": company_id,\n \"shift_status\": status,\n })\n\n\ndef list_shift_types(conn, args):\n \"\"\"List shift types, optionally filtered by company_id and status.\n\n Optional: --company-id, --status, --limit, --offset\n \"\"\"\n st_t = Table(\"shift_type\")\n q = Q.from_(st_t).select(st_t.star).orderby(st_t.name)\n params = []\n\n if args.company_id:\n q = q.where(st_t.company_id == P())\n params.append(args.company_id)\n\n if args.status:\n q = q.where(st_t.status == P())\n params.append(args.status.strip().lower())\n\n limit = int(args.limit or 20)\n offset = int(args.offset or 0)\n q = q.limit(limit).offset(offset)\n\n rows = conn.execute(q.get_sql(), params).fetchall()\n ok({\n \"shift_types\": [row_to_dict(r) for r in rows],\n \"count\": len(rows),\n })\n\n\ndef update_shift_type(conn, args):\n \"\"\"Update a shift type.\n\n Required: --shift-type-id\n Optional: --name, --start-time, --end-time, --status\n \"\"\"\n if not args.shift_type_id:\n err(\"--shift-type-id is required\")\n\n shift_type_id = args.shift_type_id.strip()\n\n st_t = Table(\"shift_type\")\n q = Q.from_(st_t).select(st_t.star).where(st_t.id == P())\n existing = conn.execute(q.get_sql(), (shift_type_id,)).fetchone()\n if not existing:\n err(f\"Shift type {shift_type_id} not found\")\n\n updates = {}\n if args.name:\n # Check uniqueness of new name\n uniq_q = Q.from_(st_t).select(st_t.id).where(\n (st_t.name == P()) & (st_t.id != P())\n )\n if conn.execute(uniq_q.get_sql(), (args.name.strip(), shift_type_id)).fetchone():\n err(f\"Shift type '{args.name.strip()}' already exists\")\n updates[\"name\"] = args.name.strip()\n\n if args.start_time:\n import re\n if not re.match(r'^\\d{2}:\\d{2}(:\\d{2})?

erpclaw You are a Full-Stack ERP Controller for ERPClaw, an AI-native ERP system. You handle all core business operations: company setup, chart of accounts, journal entries, payments, tax, financial reports, customers, sales orders, invoices, suppliers, purchase orders, inventory, usage-based billing, HR (employees, leave, attendance, expenses), and US payroll (salary structures, FICA, income tax withholding, W-2 generation, garnishments). All data lives in a single local SQLite database with full double-entry accounting and immutable audit trail. Security Model - Local-first : All data in .…

, args.start_time.strip()):\n err(f\"Invalid start_time format. Use HH:MM or HH:MM:SS\")\n updates[\"start_time\"] = args.start_time.strip()\n\n if args.end_time:\n import re\n if not re.match(r'^\\d{2}:\\d{2}(:\\d{2})?

erpclaw You are a Full-Stack ERP Controller for ERPClaw, an AI-native ERP system. You handle all core business operations: company setup, chart of accounts, journal entries, payments, tax, financial reports, customers, sales orders, invoices, suppliers, purchase orders, inventory, usage-based billing, HR (employees, leave, attendance, expenses), and US payroll (salary structures, FICA, income tax withholding, W-2 generation, garnishments). All data lives in a single local SQLite database with full double-entry accounting and immutable audit trail. Security Model - Local-first : All data in .…

, args.end_time.strip()):\n err(f\"Invalid end_time format. Use HH:MM or HH:MM:SS\")\n updates[\"end_time\"] = args.end_time.strip()\n\n if args.status:\n status = args.status.strip().lower()\n if status not in (\"active\", \"inactive\"):\n err(f\"Invalid status '{status}'. Valid: active, inactive\")\n updates[\"status\"] = status\n\n if not updates:\n err(\"No fields to update. Provide at least one of: --name, --start-time, --end-time, --status\")\n\n now = datetime.now(timezone.utc).isoformat()\n updates[\"updated_at\"] = now\n\n set_clauses = \", \".join(f\"{k} = ?\" for k in updates)\n params = list(updates.values()) + [shift_type_id]\n conn.execute(f\"UPDATE shift_type SET {set_clauses} WHERE id = ?\", params)\n conn.commit()\n\n # Re-fetch\n updated = conn.execute(q.get_sql(), (shift_type_id,)).fetchone()\n result = row_to_dict(updated)\n # Rename 'status' to 'shift_status' to avoid collision with ok() wrapper\n if \"status\" in result:\n result[\"shift_status\"] = result.pop(\"status\")\n ok(result)\n\n\ndef assign_shift(conn, args):\n \"\"\"Assign a shift type to an employee.\n\n Required: --employee-id, --shift-type-id, --start-date\n Optional: --end-date, --status (default 'active')\n \"\"\"\n if not args.employee_id:\n err(\"--employee-id is required\")\n if not args.shift_type_id:\n err(\"--shift-type-id is required\")\n if not args.start_date:\n err(\"--start-date is required\")\n\n employee_id = args.employee_id.strip()\n shift_type_id = args.shift_type_id.strip()\n start_date = args.start_date.strip()\n end_date = args.end_date.strip() if args.end_date else None\n status = (args.status or \"active\").strip().lower()\n\n if status not in (\"active\", \"inactive\"):\n err(f\"Invalid status '{status}'. Valid: active, inactive\")\n\n # Validate employee exists\n emp_t = Table(\"employee\")\n emp_q = Q.from_(emp_t).select(emp_t.id).where(emp_t.id == P())\n if not conn.execute(emp_q.get_sql(), (employee_id,)).fetchone():\n err(f\"Employee {employee_id} not found\")\n\n # Validate shift type exists\n st_t = Table(\"shift_type\")\n st_q = Q.from_(st_t).select(st_t.id).where(st_t.id == P())\n if not conn.execute(st_q.get_sql(), (shift_type_id,)).fetchone():\n err(f\"Shift type {shift_type_id} not found\")\n\n # Validate dates\n _validate_date = lambda d, n: None # date format already enforced by arg parsing\n try:\n datetime.strptime(start_date, \"%Y-%m-%d\")\n except ValueError:\n err(f\"Invalid start_date '{start_date}'. Use YYYY-MM-DD format\")\n\n if end_date:\n try:\n datetime.strptime(end_date, \"%Y-%m-%d\")\n except ValueError:\n err(f\"Invalid end_date '{end_date}'. Use YYYY-MM-DD format\")\n if end_date \u003c start_date:\n err(\"end_date cannot be before start_date\")\n\n now = datetime.now(timezone.utc).isoformat()\n assignment_id = str(uuid.uuid4())\n\n ins_sql, _ = insert_row(\"shift_assignment\", {\n \"id\": P(), \"employee_id\": P(), \"shift_type_id\": P(),\n \"start_date\": P(), \"end_date\": P(), \"status\": P(),\n \"created_at\": P(), \"updated_at\": P(),\n })\n conn.execute(ins_sql, (assignment_id, employee_id, shift_type_id,\n start_date, end_date, status, now, now))\n conn.commit()\n\n ok({\n \"shift_assignment_id\": assignment_id,\n \"employee_id\": employee_id,\n \"shift_type_id\": shift_type_id,\n \"start_date\": start_date,\n \"end_date\": end_date,\n \"assignment_status\": status,\n })\n\n\ndef list_shift_assignments(conn, args):\n \"\"\"List shift assignments, optionally filtered.\n\n Optional: --employee-id, --shift-type-id, --status, --company-id, --limit, --offset\n \"\"\"\n sa_t = Table(\"shift_assignment\").as_(\"sa\")\n st_t = Table(\"shift_type\").as_(\"st\")\n emp_t = Table(\"employee\").as_(\"e\")\n\n q = (Q.from_(sa_t)\n .join(st_t).on(st_t.id == sa_t.shift_type_id)\n .join(emp_t).on(emp_t.id == sa_t.employee_id)\n .select(sa_t.star,\n st_t.name.as_(\"shift_name\"),\n emp_t.full_name.as_(\"employee_name\"))\n .orderby(sa_t.start_date, order=Order.desc))\n params = []\n\n if args.employee_id:\n q = q.where(sa_t.employee_id == P())\n params.append(args.employee_id)\n\n if args.shift_type_id:\n q = q.where(sa_t.shift_type_id == P())\n params.append(args.shift_type_id)\n\n if args.status:\n q = q.where(sa_t.status == P())\n params.append(args.status.strip().lower())\n\n if args.company_id:\n q = q.where(emp_t.company_id == P())\n params.append(args.company_id)\n\n limit = int(args.limit or 20)\n offset = int(args.offset or 0)\n q = q.limit(limit).offset(offset)\n\n rows = conn.execute(q.get_sql(), params).fetchall()\n ok({\n \"shift_assignments\": [row_to_dict(r) for r in rows],\n \"count\": len(rows),\n })\n\n\n# ---------------------------------------------------------------------------\n# Feature #22f: Attendance Regularization (Sprint 7)\n# ---------------------------------------------------------------------------\n\nVALID_REGULARIZATION_ACTIONS = (\"half_day\", \"deduct_leave\", \"warn\")\n\n\ndef add_regularization_rule(conn, args):\n \"\"\"Add an attendance regularization rule for a company.\n\n Required: --company-id, --late-threshold-minutes, --regularization-action\n \"\"\"\n if not args.company_id:\n err(\"--company-id is required\")\n if not args.late_threshold_minutes:\n err(\"--late-threshold-minutes is required\")\n if not args.regularization_action:\n err(\"--regularization-action is required\")\n\n co_t = Table(\"company\")\n cq = Q.from_(co_t).select(co_t.id).where(co_t.id == P())\n if not conn.execute(cq.get_sql(), (args.company_id,)).fetchone():\n err(f\"Company {args.company_id} not found\")\n\n try:\n threshold = int(args.late_threshold_minutes)\n except (ValueError, TypeError):\n err(\"--late-threshold-minutes must be an integer\")\n if threshold \u003c= 0:\n err(\"--late-threshold-minutes must be > 0\")\n\n action = args.regularization_action.strip().lower()\n if action not in VALID_REGULARIZATION_ACTIONS:\n err(f\"Invalid action '{action}'. Valid: {VALID_REGULARIZATION_ACTIONS}\")\n\n rule_id = str(uuid.uuid4())\n now = datetime.now(timezone.utc).isoformat()\n\n sql, _ = insert_row(\"attendance_regularization_rule\", {\n \"id\": P(), \"company_id\": P(), \"late_threshold_minutes\": P(),\n \"action\": P(), \"created_at\": P(), \"updated_at\": P(),\n })\n conn.execute(sql, (rule_id, args.company_id, threshold, action, now, now))\n\n audit(conn, \"erpclaw-hr\", \"add-regularization-rule\",\n \"attendance_regularization_rule\", rule_id,\n new_values={\"company_id\": args.company_id,\n \"late_threshold_minutes\": threshold, \"action\": action})\n conn.commit()\n\n ok({\n \"rule_id\": rule_id,\n \"company_id\": args.company_id,\n \"late_threshold_minutes\": threshold,\n \"action\": action,\n \"message\": \"Attendance regularization rule added\",\n })\n\n\ndef apply_attendance_regularization(conn, args):\n \"\"\"Apply attendance regularization rules for a company on a date range.\n\n Required: --company-id\n Optional: --from-date, --to-date\n \"\"\"\n if not args.company_id:\n err(\"--company-id is required\")\n\n arr_t = Table(\"attendance_regularization_rule\")\n rq = (Q.from_(arr_t).select(arr_t.star)\n .where(arr_t.company_id == P())\n .orderby(arr_t.late_threshold_minutes))\n rules = conn.execute(rq.get_sql(), (args.company_id,)).fetchall()\n if not rules:\n err(f\"No regularization rules found for company {args.company_id}\")\n rules = [row_to_dict(r) for r in rules]\n\n at = Table(\"attendance\")\n emp_t = Table(\"employee\")\n q = (Q.from_(at)\n .join(emp_t).on(emp_t.id == at.employee_id)\n .select(at.star)\n .where(emp_t.company_id == P())\n .where(at.late_entry == 1))\n params = [args.company_id]\n\n if args.from_date:\n q = q.where(at.attendance_date >= P())\n params.append(args.from_date)\n if args.to_date:\n q = q.where(at.attendance_date \u003c= P())\n params.append(args.to_date)\n\n records = conn.execute(q.get_sql(), params).fetchall()\n updated = 0\n warnings = []\n\n for rec in records:\n rec_d = row_to_dict(rec)\n check_in = rec_d.get(\"check_in_time\")\n if not check_in:\n continue\n\n for rule in rules:\n threshold = rule[\"late_threshold_minutes\"]\n action_type = rule[\"action\"]\n\n try:\n parts = check_in.split(\":\")\n check_hour = int(parts[0])\n check_min = int(parts[1]) if len(parts) > 1 else 0\n late_minutes = (check_hour - 9) * 60 + check_min\n if late_minutes \u003c 0:\n late_minutes = 0\n except (ValueError, IndexError):\n late_minutes = threshold + 1\n\n if late_minutes >= threshold:\n if action_type == \"half_day\":\n conn.execute(\n \"UPDATE attendance SET status = 'half_day' WHERE id = ?\",\n (rec_d[\"id\"],)\n )\n updated += 1\n elif action_type == \"warn\":\n warnings.append({\n \"attendance_id\": rec_d[\"id\"],\n \"employee_id\": rec_d[\"employee_id\"],\n \"date\": rec_d[\"attendance_date\"],\n \"late_minutes\": late_minutes,\n })\n elif action_type == \"deduct_leave\":\n warnings.append({\n \"attendance_id\": rec_d[\"id\"],\n \"employee_id\": rec_d[\"employee_id\"],\n \"date\": rec_d[\"attendance_date\"],\n \"late_minutes\": late_minutes,\n \"action\": \"deduct_leave\",\n })\n break\n\n conn.commit()\n ok({\n \"records_processed\": len(records),\n \"records_updated\": updated,\n \"warnings\": warnings,\n \"warning_count\": len(warnings),\n })\n\n\n# ---------------------------------------------------------------------------\n# Feature #21: Employee Documents (Sprint 7)\n# ---------------------------------------------------------------------------\n\nVALID_DOCUMENT_TYPES = (\n \"passport\", \"visa\", \"drivers_license\", \"i9\", \"w4\",\n \"offer_letter\", \"contract\", \"certificate\", \"other\",\n)\nVALID_DOCUMENT_STATUSES = (\"active\", \"expired\", \"archived\")\n\n\ndef add_employee_document(conn, args):\n \"\"\"Add an employee document record.\n\n Required: --employee-id, --document-type, --document-name\n Optional: --expiry-date, --notes\n \"\"\"\n if not args.employee_id:\n err(\"--employee-id is required\")\n if not args.document_type:\n err(\"--document-type is required\")\n if not args.document_name:\n err(\"--document-name is required\")\n\n emp = _validate_employee_exists(conn, args.employee_id)\n\n doc_type = args.document_type.strip().lower()\n if doc_type not in VALID_DOCUMENT_TYPES:\n err(f\"Invalid document type '{doc_type}'. Valid: {VALID_DOCUMENT_TYPES}\")\n\n expiry_date = None\n if args.expiry_date:\n try:\n date.fromisoformat(args.expiry_date)\n expiry_date = args.expiry_date\n except (ValueError, TypeError):\n err(f\"Invalid expiry date format: {args.expiry_date}. Use YYYY-MM-DD\")\n\n doc_id = str(uuid.uuid4())\n now = datetime.now(timezone.utc).isoformat()\n\n sql, _ = insert_row(\"employee_document\", {\n \"id\": P(), \"employee_id\": P(), \"document_type\": P(),\n \"document_name\": P(), \"expiry_date\": P(), \"notes\": P(),\n \"status\": P(), \"created_at\": P(), \"updated_at\": P(),\n })\n conn.execute(sql, (\n doc_id, args.employee_id, doc_type,\n args.document_name.strip(), expiry_date,\n args.notes, \"active\", now, now,\n ))\n\n audit(conn, \"erpclaw-hr\", \"add-employee-document\",\n \"employee_document\", doc_id,\n new_values={\"employee_id\": args.employee_id,\n \"document_type\": doc_type,\n \"document_name\": args.document_name})\n conn.commit()\n\n ok({\n \"employee_document_id\": doc_id,\n \"employee_id\": args.employee_id,\n \"employee_name\": emp[\"full_name\"],\n \"document_type\": doc_type,\n \"document_name\": args.document_name.strip(),\n \"expiry_date\": expiry_date,\n \"message\": \"Employee document added\",\n })\n\n\ndef list_employee_documents(conn, args):\n \"\"\"List employee documents.\n\n Required: --employee-id\n Optional: --document-type, --status\n \"\"\"\n if not args.employee_id:\n err(\"--employee-id is required\")\n\n ed_t = Table(\"employee_document\")\n q = (Q.from_(ed_t).select(ed_t.star)\n .where(ed_t.employee_id == P()))\n params = [args.employee_id]\n\n if args.document_type:\n q = q.where(ed_t.document_type == P())\n params.append(args.document_type.strip().lower())\n\n if args.status:\n q = q.where(ed_t.status == P())\n params.append(args.status.strip().lower())\n\n q = q.orderby(ed_t.created_at)\n rows = conn.execute(q.get_sql(), params).fetchall()\n\n ok({\"documents\": [row_to_dict(r) for r in rows], \"count\": len(rows)})\n\n\ndef get_employee_document(conn, args):\n \"\"\"Get a specific employee document.\n\n Required: --document-id\n \"\"\"\n if not args.document_id:\n err(\"--document-id is required\")\n\n ed_t = Table(\"employee_document\")\n q = Q.from_(ed_t).select(ed_t.star).where(ed_t.id == P())\n row = conn.execute(q.get_sql(), (args.document_id,)).fetchone()\n if not row:\n err(f\"Employee document {args.document_id} not found\")\n\n ok(row_to_dict(row))\n\n\ndef check_expiring_documents(conn, args):\n \"\"\"Check for employee documents expiring within N days.\n\n Optional: --company-id, --days (default 30)\n \"\"\"\n days = int(args.days or 30)\n today = date.today()\n cutoff = (today + timedelta(days=days)).isoformat()\n\n ed_t = Table(\"employee_document\")\n emp_t = Table(\"employee\")\n q = (Q.from_(ed_t)\n .join(emp_t).on(emp_t.id == ed_t.employee_id)\n .select(ed_t.star, emp_t.full_name.as_(\"employee_name\"),\n emp_t.company_id)\n .where(ed_t.expiry_date.isnotnull())\n .where(ed_t.expiry_date \u003c= P())\n .where(ed_t.status == ValueWrapper(\"active\")))\n params = [cutoff]\n\n if args.company_id:\n q = q.where(emp_t.company_id == P())\n params.append(args.company_id)\n\n q = q.orderby(ed_t.expiry_date)\n rows = conn.execute(q.get_sql(), params).fetchall()\n\n results = []\n for r in rows:\n d = row_to_dict(r)\n exp_date = date.fromisoformat(d[\"expiry_date\"])\n d[\"days_until_expiry\"] = (exp_date - today).days\n d[\"is_expired\"] = exp_date \u003c today\n results.append(d)\n\n ok({\n \"expiring_documents\": results,\n \"count\": len(results),\n \"days_window\": days,\n })\n\n\n# ---------------------------------------------------------------------------\n# ACTIONS dispatch table\n# ---------------------------------------------------------------------------\n\nACTIONS = {\n \"add-employee\": add_employee,\n \"update-employee\": update_employee,\n \"get-employee\": get_employee,\n \"list-employees\": list_employees,\n \"add-department\": add_department,\n \"list-departments\": list_departments,\n \"add-designation\": add_designation,\n \"list-designations\": list_designations,\n \"add-leave-type\": add_leave_type,\n \"list-leave-types\": list_leave_types,\n \"add-leave-allocation\": add_leave_allocation,\n \"get-leave-balance\": get_leave_balance,\n \"add-leave-application\": add_leave_application,\n \"approve-leave\": approve_leave,\n \"reject-leave\": reject_leave,\n \"list-leave-applications\": list_leave_applications,\n \"mark-attendance\": mark_attendance,\n \"bulk-mark-attendance\": bulk_mark_attendance,\n \"list-attendance\": list_attendance,\n \"add-holiday-list\": add_holiday_list,\n \"add-expense-claim\": add_expense_claim,\n \"submit-expense-claim\": submit_expense_claim,\n \"approve-expense-claim\": approve_expense_claim,\n \"reject-expense-claim\": reject_expense_claim,\n \"update-expense-claim-status\": update_expense_claim_status,\n \"list-expense-claims\": list_expense_claims,\n \"record-lifecycle-event\": record_lifecycle_event,\n \"add-shift-type\": add_shift_type,\n \"list-shift-types\": list_shift_types,\n \"update-shift-type\": update_shift_type,\n \"assign-shift\": assign_shift,\n \"list-shift-assignments\": list_shift_assignments,\n\n # --- Sprint 7: Attendance Regularization ---\n \"add-regularization-rule\": add_regularization_rule,\n \"apply-attendance-regularization\": apply_attendance_regularization,\n\n # --- Sprint 7: Employee Documents ---\n \"add-employee-document\": add_employee_document,\n \"list-employee-documents\": list_employee_documents,\n \"get-employee-document\": get_employee_document,\n \"check-expiring-documents\": check_expiring_documents,\n\n \"status\": status_action,\n}\n\n\n# ---------------------------------------------------------------------------\n# main()\n# ---------------------------------------------------------------------------\n\ndef main():\n parser = SafeArgumentParser(description=\"ERPClaw HR Skill\")\n parser.add_argument(\"--action\", required=True, choices=sorted(ACTIONS.keys()))\n parser.add_argument(\"--db-path\", default=None)\n\n # Entity IDs\n parser.add_argument(\"--company-id\")\n parser.add_argument(\"--employee-id\")\n parser.add_argument(\"--department-id\")\n parser.add_argument(\"--designation-id\")\n parser.add_argument(\"--employee-grade-id\")\n parser.add_argument(\"--leave-type-id\")\n parser.add_argument(\"--leave-application-id\")\n parser.add_argument(\"--expense-claim-id\")\n parser.add_argument(\"--payment-entry-id\")\n parser.add_argument(\"--holiday-list-id\")\n parser.add_argument(\"--payroll-cost-center-id\")\n parser.add_argument(\"--salary-structure-id\")\n parser.add_argument(\"--leave-policy-id\")\n parser.add_argument(\"--shift-id\")\n parser.add_argument(\"--shift-type-id\")\n parser.add_argument(\"--start-date\")\n parser.add_argument(\"--end-date\")\n parser.add_argument(\"--start-time\")\n parser.add_argument(\"--end-time\")\n parser.add_argument(\"--attendance-device-id\")\n parser.add_argument(\"--cost-center-id\")\n\n # Employee fields\n parser.add_argument(\"--first-name\")\n parser.add_argument(\"--last-name\")\n parser.add_argument(\"--date-of-birth\")\n parser.add_argument(\"--gender\")\n parser.add_argument(\"--date-of-joining\")\n parser.add_argument(\"--date-of-exit\")\n parser.add_argument(\"--employment-type\")\n parser.add_argument(\"--branch\")\n parser.add_argument(\"--reporting-to\")\n parser.add_argument(\"--company-email\")\n parser.add_argument(\"--personal-email\")\n parser.add_argument(\"--cell-phone\")\n parser.add_argument(\"--emergency-contact\")\n parser.add_argument(\"--bank-details\")\n\n # Tax / payroll fields\n parser.add_argument(\"--federal-filing-status\")\n parser.add_argument(\"--w4-allowances\")\n parser.add_argument(\"--w4-additional-withholding\")\n parser.add_argument(\"--state-filing-status\")\n parser.add_argument(\"--state-withholding-allowances\")\n parser.add_argument(\"--employee-401k-rate\")\n parser.add_argument(\"--hsa-contribution\")\n parser.add_argument(\"--is-exempt-from-fica\")\n\n # Department / designation fields\n parser.add_argument(\"--name\")\n parser.add_argument(\"--description\")\n parser.add_argument(\"--parent-id\")\n\n # Leave fields\n parser.add_argument(\"--max-days-allowed\")\n parser.add_argument(\"--is-paid-leave\")\n parser.add_argument(\"--is-carry-forward\")\n parser.add_argument(\"--max-carry-forward-days\")\n parser.add_argument(\"--is-compensatory\")\n parser.add_argument(\"--applicable-after-days\")\n parser.add_argument(\"--total-leaves\")\n parser.add_argument(\"--fiscal-year\")\n parser.add_argument(\"--half-day\")\n parser.add_argument(\"--half-day-date\")\n parser.add_argument(\"--reason\")\n parser.add_argument(\"--approved-by\")\n\n # Attendance fields\n parser.add_argument(\"--date\")\n parser.add_argument(\"--shift\")\n parser.add_argument(\"--check-in-time\")\n parser.add_argument(\"--check-out-time\")\n parser.add_argument(\"--working-hours\")\n parser.add_argument(\"--late-entry\")\n parser.add_argument(\"--early-exit\")\n parser.add_argument(\"--source\")\n\n # Bulk attendance\n parser.add_argument(\"--entries\")\n\n # Holiday list fields\n parser.add_argument(\"--holidays\")\n\n # Expense claim fields\n parser.add_argument(\"--expense-date\")\n parser.add_argument(\"--items\")\n\n # Lifecycle event fields\n parser.add_argument(\"--event-type\")\n parser.add_argument(\"--event-date\")\n parser.add_argument(\"--details\")\n parser.add_argument(\"--old-values\")\n parser.add_argument(\"--new-values\")\n\n # Attendance regularization fields (Feature #22f)\n parser.add_argument(\"--late-threshold-minutes\")\n parser.add_argument(\"--regularization-action\")\n\n # Employee document fields (Feature #21)\n parser.add_argument(\"--document-id\")\n parser.add_argument(\"--document-type\")\n parser.add_argument(\"--document-name\")\n parser.add_argument(\"--expiry-date\")\n parser.add_argument(\"--notes\")\n parser.add_argument(\"--days\")\n\n # Filters\n parser.add_argument(\"--status\")\n parser.add_argument(\"--from-date\")\n parser.add_argument(\"--to-date\")\n parser.add_argument(\"--limit\", default=\"20\")\n parser.add_argument(\"--offset\", default=\"0\")\n parser.add_argument(\"--search\")\n\n args, unknown = parser.parse_known_args()\n check_unknown_args(parser, unknown)\n check_input_lengths(args)\n\n db_path = args.db_path or DEFAULT_DB_PATH\n ensure_db_exists(db_path)\n conn = get_connection(db_path)\n\n # Dependency check\n _dep = check_required_tables(conn, REQUIRED_TABLES)\n if _dep:\n _dep[\"suggestion\"] = \"clawhub install \" + \" \".join(_dep.get(\"missing_skills\", []))\n print(json.dumps(_dep, indent=2))\n conn.close()\n sys.exit(1)\n\n try:\n ACTIONS[args.action](conn, args)\n except Exception as e:\n conn.rollback()\n sys.stderr.write(f\"[erpclaw-hr] {e}\\n\")\n err(\"An unexpected error occurred\")\n finally:\n conn.close()\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":140043,"content_sha256":"17b26f9a1ed004de91b3d273b48ffd13e2465842ce3d4131ecdfcf82e8bf7002"},{"filename":"scripts/erpclaw-inventory/db_query.py","content":"#!/usr/bin/env python3\n\"\"\"ERPClaw Inventory Skill — db_query.py\n\nItems, warehouses, stock entries, stock ledger, batches, serial numbers,\npricing, and stock reconciliation. Draft->Submit->Cancel lifecycle for\nstock entries and reconciliation. Submit posts SLE + GL via shared lib.\n\nUsage: python3 db_query.py --action \u003caction-name> [--flags ...]\nOutput: JSON to stdout, exit 0 on success, exit 1 on error.\n\"\"\"\nimport argparse\nimport itertools\nimport json\nimport os\nimport sqlite3\nimport sys\nimport uuid\nfrom datetime import datetime, timezone\nfrom decimal import Decimal, InvalidOperation\n\n# Add shared lib to path\ntry:\n sys.path.insert(0, os.path.expanduser(\"~/.openclaw/erpclaw/lib\"))\n from erpclaw_lib.db import get_connection, ensure_db_exists, DEFAULT_DB_PATH\n from erpclaw_lib.decimal_utils import to_decimal, round_currency\n from erpclaw_lib.validation import check_input_lengths\n from erpclaw_lib.naming import get_next_name\n from erpclaw_lib.stock_posting import (\n insert_sle_entries,\n reverse_sle_entries,\n get_stock_balance,\n get_valuation_rate,\n create_perpetual_inventory_gl,\n )\n from erpclaw_lib.gl_posting import insert_gl_entries, reverse_gl_entries\n from erpclaw_lib.response import ok, err, row_to_dict\n from erpclaw_lib.audit import audit\n from erpclaw_lib.dependencies import check_required_tables\n from erpclaw_lib.query_helpers import resolve_company_id\n from erpclaw_lib.query import Q, P, Table, Field, fn, DecimalSum, DecimalAbs, dynamic_update\n from erpclaw_lib.vendor.pypika import Order\n from erpclaw_lib.args import SafeArgumentParser, check_unknown_args\n from erpclaw_lib.vendor.pypika.terms import LiteralValue, ValueWrapper\nexcept ImportError:\n import json as _json\n print(_json.dumps({\"status\": \"error\", \"error\": \"ERPClaw foundation not installed. Install erpclaw first: clawhub install erpclaw\", \"suggestion\": \"clawhub install erpclaw\"}))\n sys.exit(1)\n\n# Convenience alias for datetime('now') SQLite expression\n_NOW = LiteralValue(\"datetime('now')\")\n\nREQUIRED_TABLES = [\"company\"]\n\nVALID_ITEM_TYPES = (\"stock\", \"non_stock\", \"service\")\nVALID_VALUATION_METHODS = (\"moving_average\", \"fifo\")\nVALID_WAREHOUSE_TYPES = (\"stores\", \"production\", \"transit\", \"rejected\")\nVALID_SERIAL_STATUSES = (\"active\", \"delivered\", \"returned\", \"scrapped\")\n\n# User-friendly entry type -> DB value\nENTRY_TYPE_MAP = {\n \"receive\": \"material_receipt\",\n \"issue\": \"material_issue\",\n \"transfer\": \"material_transfer\",\n \"manufacture\": \"manufacture\",\n # Also accept DB values directly\n \"material_receipt\": \"material_receipt\",\n \"material_issue\": \"material_issue\",\n \"material_transfer\": \"material_transfer\",\n}\n\n\n\n# ---------------------------------------------------------------------------\n# Helpers\n# ---------------------------------------------------------------------------\n\ndef _parse_json_arg(value, name):\n if value is None:\n return None\n try:\n return json.loads(value)\n except (json.JSONDecodeError, TypeError):\n err(f\"Invalid JSON for --{name}: {value}\")\n\n\ndef _get_fiscal_year(conn, posting_date: str) -> str | None:\n \"\"\"Return the fiscal year name for a posting date, or None.\"\"\"\n fy = conn.execute(\n \"SELECT name FROM fiscal_year WHERE start_date \u003c= ? AND end_date >= ? AND is_closed = 0\",\n (posting_date, posting_date),\n ).fetchone()\n return fy[\"name\"] if fy else None\n\n\ndef _get_cost_center(conn, company_id: str) -> str | None:\n \"\"\"Return the first non-group cost center for a company, or None.\"\"\"\n t = Table(\"cost_center\")\n q = (Q.from_(t).select(t.id)\n .where(t.company_id == P())\n .where(t.is_group == 0)\n .limit(1))\n cc = conn.execute(q.get_sql(), (company_id,)).fetchone()\n return cc[\"id\"] if cc else None\n\n\n# ---------------------------------------------------------------------------\n# 1. add-item\n# ---------------------------------------------------------------------------\n\ndef add_item(conn, args):\n \"\"\"Create a new item.\"\"\"\n if not args.item_code:\n err(\"--item-code is required\")\n if not args.item_name:\n err(\"--item-name is required\")\n\n item_type = args.item_type or \"stock\"\n if item_type not in VALID_ITEM_TYPES:\n err(f\"--item-type must be one of: {', '.join(VALID_ITEM_TYPES)}\")\n\n valuation_method = args.valuation_method or \"moving_average\"\n if valuation_method not in VALID_VALUATION_METHODS:\n err(f\"--valuation-method must be one of: {', '.join(VALID_VALUATION_METHODS)}\")\n\n # Validate item group if provided (accept id or name)\n if args.item_group:\n ig_t = Table(\"item_group\")\n ig_q = (Q.from_(ig_t).select(ig_t.id)\n .where((ig_t.id == P()) | (ig_t.name == P())))\n ig = conn.execute(ig_q.get_sql(), (args.item_group, args.item_group)).fetchone()\n if not ig:\n err(f\"Item group {args.item_group} not found\")\n args.item_group = ig[0] # normalize to id\n\n is_stock_item = 1 if item_type == \"stock\" else 0\n has_batch = int(args.has_batch) if args.has_batch else 0\n has_serial = int(args.has_serial) if args.has_serial else 0\n standard_rate = str(round_currency(to_decimal(args.standard_rate or \"0\")))\n\n item_id = str(uuid.uuid4())\n t = Table(\"item\")\n q = Q.into(t).columns(\n \"id\", \"item_code\", \"item_name\", \"item_group_id\", \"item_type\", \"stock_uom\",\n \"valuation_method\", \"is_stock_item\", \"has_batch\", \"has_serial\",\n \"standard_rate\", \"status\",\n ).insert(P(), P(), P(), P(), P(), P(), P(), P(), P(), P(), P(), \"active\")\n try:\n conn.execute(\n q.get_sql(),\n (item_id, args.item_code, args.item_name, args.item_group,\n item_type, args.stock_uom or \"Nos\",\n valuation_method, is_stock_item, has_batch, has_serial,\n standard_rate),\n )\n except sqlite3.IntegrityError as e:\n sys.stderr.write(f\"[erpclaw-inventory] {e}\\n\")\n err(\"Item creation failed — check for duplicates or invalid data\")\n\n audit(conn, \"erpclaw-inventory\", \"add-item\", \"item\", item_id,\n new_values={\"item_code\": args.item_code, \"item_name\": args.item_name})\n conn.commit()\n ok({\"item_id\": item_id, \"item_code\": args.item_code,\n \"item_name\": args.item_name})\n\n\n# ---------------------------------------------------------------------------\n# 2. update-item\n# ---------------------------------------------------------------------------\n\ndef update_item(conn, args):\n \"\"\"Update an existing item.\"\"\"\n if not args.item_id:\n err(\"--item-id is required\")\n\n t = Table(\"item\")\n q = Q.from_(t).select(t.star).where(t.id == P())\n item = conn.execute(q.get_sql(), (args.item_id,)).fetchone()\n if not item:\n err(f\"Item {args.item_id} not found\",\n suggestion=\"Use 'list items' to see available items.\")\n\n if item[\"status\"] == \"disabled\" and args.item_status != \"active\":\n err(\"Cannot update a disabled item (set --status active first)\")\n\n data, updated_fields = {}, []\n\n if args.item_name is not None:\n data[\"item_name\"] = args.item_name\n updated_fields.append(\"item_name\")\n if args.reorder_level is not None:\n data[\"reorder_level\"] = args.reorder_level\n updated_fields.append(\"reorder_level\")\n if args.reorder_qty is not None:\n data[\"reorder_qty\"] = args.reorder_qty\n updated_fields.append(\"reorder_qty\")\n if args.standard_rate is not None:\n data[\"standard_rate\"] = str(round_currency(to_decimal(args.standard_rate)))\n updated_fields.append(\"standard_rate\")\n if args.item_status is not None:\n if args.item_status not in (\"active\", \"disabled\"):\n err(\"--status must be 'active' or 'disabled'\")\n data[\"status\"] = args.item_status\n updated_fields.append(\"status\")\n\n if not updated_fields:\n err(\"No fields to update\")\n\n data[\"updated_at\"] = LiteralValue(\"datetime('now')\")\n sql, params = dynamic_update(\"item\", data, where={\"id\": args.item_id})\n conn.execute(sql, params)\n\n audit(conn, \"erpclaw-inventory\", \"update-item\", \"item\", args.item_id,\n new_values={\"updated_fields\": updated_fields})\n conn.commit()\n ok({\"item_id\": args.item_id, \"updated_fields\": updated_fields})\n\n\n# ---------------------------------------------------------------------------\n# 3. get-item\n# ---------------------------------------------------------------------------\n\ndef get_item(conn, args):\n \"\"\"Get item with stock summary across all warehouses.\"\"\"\n if not args.item_id:\n err(\"--item-id is required\")\n\n t = Table(\"item\")\n q = Q.from_(t).select(t.star).where(t.id == P())\n item = conn.execute(q.get_sql(), (args.item_id,)).fetchone()\n if not item:\n err(f\"Item {args.item_id} not found\")\n\n data = row_to_dict(item)\n\n # Stock balances per warehouse\n sle = Table(\"stock_ledger_entry\")\n wh_q = (Q.from_(sle)\n .select(sle.warehouse_id).distinct()\n .where(sle.item_id == P())\n .where(sle.is_cancelled == 0))\n warehouses = conn.execute(wh_q.get_sql(), (args.item_id,)).fetchall()\n\n stock_balances = []\n total_qty = Decimal(\"0\")\n total_value = Decimal(\"0\")\n for wh_row in warehouses:\n wh_id = wh_row[\"warehouse_id\"]\n balance = get_stock_balance(conn, args.item_id, wh_id)\n qty = to_decimal(balance[\"qty\"])\n val = to_decimal(balance[\"stock_value\"])\n if qty != 0 or val != 0:\n wh_t = Table(\"warehouse\")\n wh_q2 = Q.from_(wh_t).select(wh_t.name).where(wh_t.id == P())\n wh = conn.execute(wh_q2.get_sql(), (wh_id,)).fetchone()\n stock_balances.append({\n \"warehouse_id\": wh_id,\n \"warehouse_name\": wh[\"name\"] if wh else wh_id,\n \"qty\": balance[\"qty\"],\n \"valuation_rate\": balance[\"valuation_rate\"],\n \"stock_value\": balance[\"stock_value\"],\n })\n total_qty += qty\n total_value += val\n\n data[\"stock_balances\"] = stock_balances\n data[\"total_qty\"] = str(round_currency(total_qty))\n data[\"total_stock_value\"] = str(round_currency(total_value))\n ok(data)\n\n\n# ---------------------------------------------------------------------------\n# 4. list-items\n# ---------------------------------------------------------------------------\n\ndef list_items(conn, args):\n \"\"\"Query items with filtering.\"\"\"\n i = Table(\"item\").as_(\"i\")\n ig = Table(\"item_group\").as_(\"ig\")\n\n # Warehouse filter: items that have stock in a specific warehouse\n warehouse_id = getattr(args, \"warehouse_id\", None)\n\n company_id = getattr(args, \"company_id\", None)\n\n # Build count query\n count_q = Q.from_(i).select(fn.Count(\"*\"))\n if company_id:\n count_q = count_q.join(ig).on(ig.id == i.item_group_id).where(ig.company_id == P())\n if warehouse_id:\n sle = Table(\"stock_ledger_entry\")\n sub = (Q.from_(sle).select(sle.item_id).distinct()\n .where(sle.warehouse_id == P()).where(sle.is_cancelled == 0))\n count_q = count_q.where(i.id.isin(sub))\n if args.item_group:\n count_q = count_q.where(i.item_group_id == P())\n if args.item_type:\n count_q = count_q.where(i.item_type == P())\n if args.search:\n count_q = count_q.where(\n (i.item_name.like(P())) | (i.item_code.like(P()))\n )\n\n count_params = []\n if company_id:\n count_params.append(company_id)\n if warehouse_id:\n count_params.append(warehouse_id)\n if args.item_group:\n count_params.append(args.item_group)\n if args.item_type:\n count_params.append(args.item_type)\n if args.search:\n count_params.extend([f\"%{args.search}%\", f\"%{args.search}%\"])\n\n count_row = conn.execute(count_q.get_sql(), count_params).fetchone()\n total_count = count_row[0]\n\n limit = int(args.limit) if args.limit else 20\n offset = int(args.offset) if args.offset else 0\n\n rows_q = (Q.from_(i)\n .left_join(ig).on(ig.id == i.item_group_id)\n .select(i.id, i.item_code, i.item_name, i.item_group_id,\n ig.name.as_(\"item_group_name\"),\n i.item_type, i.stock_uom, i.standard_rate, i.status,\n i.has_batch, i.has_serial)\n .orderby(i.item_name)\n .limit(P()).offset(P()))\n if company_id:\n rows_q = rows_q.where(ig.company_id == P())\n if warehouse_id:\n sle = Table(\"stock_ledger_entry\")\n sub = (Q.from_(sle).select(sle.item_id).distinct()\n .where(sle.warehouse_id == P()).where(sle.is_cancelled == 0))\n rows_q = rows_q.where(i.id.isin(sub))\n if args.item_group:\n rows_q = rows_q.where(i.item_group_id == P())\n if args.item_type:\n rows_q = rows_q.where(i.item_type == P())\n if args.search:\n rows_q = rows_q.where(\n (i.item_name.like(P())) | (i.item_code.like(P()))\n )\n\n row_params = []\n if company_id:\n row_params.append(company_id)\n if warehouse_id:\n row_params.append(warehouse_id)\n if args.item_group:\n row_params.append(args.item_group)\n if args.item_type:\n row_params.append(args.item_type)\n if args.search:\n row_params.extend([f\"%{args.search}%\", f\"%{args.search}%\"])\n row_params.extend([limit, offset])\n\n rows = conn.execute(rows_q.get_sql(), row_params).fetchall()\n\n ok({\"items\": [row_to_dict(r) for r in rows], \"total_count\": total_count,\n \"limit\": limit, \"offset\": offset, \"has_more\": offset + limit \u003c total_count})\n\n\n# ---------------------------------------------------------------------------\n# 5. add-item-group\n# ---------------------------------------------------------------------------\n\ndef add_item_group(conn, args):\n \"\"\"Create an item group.\"\"\"\n if not args.name:\n err(\"--name is required\")\n\n company_id = getattr(args, \"company_id\", None)\n\n if args.parent_id:\n ig_t = Table(\"item_group\")\n parent_q = Q.from_(ig_t).select(ig_t.id).where(ig_t.id == P())\n parent = conn.execute(parent_q.get_sql(), (args.parent_id,)).fetchone()\n if not parent:\n err(f\"Parent item group {args.parent_id} not found\")\n\n ig_id = str(uuid.uuid4())\n t = Table(\"item_group\")\n q = Q.into(t).columns(\"id\", \"name\", \"company_id\", \"parent_id\").insert(P(), P(), P(), P())\n try:\n conn.execute(q.get_sql(), (ig_id, args.name, company_id, args.parent_id))\n except sqlite3.IntegrityError as e:\n sys.stderr.write(f\"[erpclaw-inventory] {e}\\n\")\n err(f\"Item group '{args.name}' already exists\"\n f\"{' for this company' if company_id else ''}\"\n f\". Choose a different name or update the existing group.\")\n\n audit(conn, \"erpclaw-inventory\", \"add-item-group\", \"item_group\", ig_id,\n new_values={\"name\": args.name})\n conn.commit()\n ok({\"item_group_id\": ig_id, \"name\": args.name})\n\n\n# ---------------------------------------------------------------------------\n# 6. list-item-groups\n# ---------------------------------------------------------------------------\n\ndef list_item_groups(conn, args):\n \"\"\"List item groups.\"\"\"\n t = Table(\"item_group\")\n\n company_id = getattr(args, \"company_id\", None)\n\n count_q = Q.from_(t).select(fn.Count(\"*\"))\n if company_id:\n count_q = count_q.where(t.company_id == P())\n if args.parent_id:\n count_q = count_q.where(t.parent_id == P())\n\n count_params = []\n if company_id:\n count_params.append(company_id)\n if args.parent_id:\n count_params.append(args.parent_id)\n\n count_row = conn.execute(count_q.get_sql(), count_params).fetchone()\n total_count = count_row[0]\n\n limit = int(args.limit) if args.limit else 20\n offset = int(args.offset) if args.offset else 0\n\n rows_q = (Q.from_(t).select(t.star)\n .orderby(t.name)\n .limit(P()).offset(P()))\n if company_id:\n rows_q = rows_q.where(t.company_id == P())\n if args.parent_id:\n rows_q = rows_q.where(t.parent_id == P())\n\n row_params = []\n if company_id:\n row_params.append(company_id)\n if args.parent_id:\n row_params.append(args.parent_id)\n row_params.extend([limit, offset])\n\n rows = conn.execute(rows_q.get_sql(), row_params).fetchall()\n\n ok({\"item_groups\": [row_to_dict(r) for r in rows], \"total_count\": total_count,\n \"limit\": limit, \"offset\": offset, \"has_more\": offset + limit \u003c total_count})\n\n\n# ---------------------------------------------------------------------------\n# 7. add-warehouse\n# ---------------------------------------------------------------------------\n\ndef add_warehouse(conn, args):\n \"\"\"Create a warehouse.\"\"\"\n if not args.name:\n err(\"--name is required\")\n if not args.company_id:\n err(\"--company-id is required\")\n\n co_t = Table(\"company\")\n co_q = Q.from_(co_t).select(co_t.id).where(co_t.id == P())\n if not conn.execute(co_q.get_sql(), (args.company_id,)).fetchone():\n err(f\"Company {args.company_id} not found\")\n\n wh_type = args.warehouse_type or \"stores\"\n if wh_type not in VALID_WAREHOUSE_TYPES:\n err(f\"--warehouse-type must be one of: {', '.join(VALID_WAREHOUSE_TYPES)}\")\n\n if args.parent_id:\n wh_t = Table(\"warehouse\")\n parent_q = Q.from_(wh_t).select(wh_t.id).where(wh_t.id == P())\n parent = conn.execute(parent_q.get_sql(), (args.parent_id,)).fetchone()\n if not parent:\n err(f\"Parent warehouse {args.parent_id} not found\")\n\n if args.account_id:\n acct_t = Table(\"account\")\n acct_q = Q.from_(acct_t).select(acct_t.id).where(acct_t.id == P())\n acct = conn.execute(acct_q.get_sql(), (args.account_id,)).fetchone()\n if not acct:\n err(f\"Account {args.account_id} not found\")\n\n is_group = int(args.is_group) if args.is_group else 0\n wh_id = str(uuid.uuid4())\n t = Table(\"warehouse\")\n q = Q.into(t).columns(\n \"id\", \"name\", \"parent_id\", \"warehouse_type\",\n \"company_id\", \"account_id\", \"is_group\",\n ).insert(P(), P(), P(), P(), P(), P(), P())\n conn.execute(\n q.get_sql(),\n (wh_id, args.name, args.parent_id, wh_type,\n args.company_id, args.account_id, is_group),\n )\n\n audit(conn, \"erpclaw-inventory\", \"add-warehouse\", \"warehouse\", wh_id,\n new_values={\"name\": args.name, \"type\": wh_type})\n conn.commit()\n ok({\"warehouse_id\": wh_id, \"name\": args.name})\n\n\n# ---------------------------------------------------------------------------\n# 8. update-warehouse\n# ---------------------------------------------------------------------------\n\ndef update_warehouse(conn, args):\n \"\"\"Update a warehouse.\"\"\"\n if not args.warehouse_id:\n err(\"--warehouse-id is required\")\n\n wh_t = Table(\"warehouse\")\n wh_q = (Q.from_(wh_t).select(wh_t.star)\n .where((wh_t.id == P()) | (wh_t.name == P())))\n wh = conn.execute(wh_q.get_sql(), (args.warehouse_id, args.warehouse_id)).fetchone()\n if not wh:\n err(f\"Warehouse {args.warehouse_id} not found\")\n args.warehouse_id = wh[\"id\"] # normalize to id\n\n data, updated_fields = {}, []\n\n if args.name is not None:\n data[\"name\"] = args.name\n updated_fields.append(\"name\")\n if args.account_id is not None:\n acct_t = Table(\"account\")\n acct_q = Q.from_(acct_t).select(acct_t.id).where(acct_t.id == P())\n acct = conn.execute(acct_q.get_sql(), (args.account_id,)).fetchone()\n if not acct:\n err(f\"Account {args.account_id} not found\")\n data[\"account_id\"] = args.account_id\n updated_fields.append(\"account_id\")\n\n if not updated_fields:\n err(\"No fields to update\")\n\n data[\"updated_at\"] = LiteralValue(\"datetime('now')\")\n sql, params = dynamic_update(\"warehouse\", data, where={\"id\": args.warehouse_id})\n conn.execute(sql, params)\n\n audit(conn, \"erpclaw-inventory\", \"update-warehouse\", \"warehouse\", args.warehouse_id,\n new_values={\"updated_fields\": updated_fields})\n conn.commit()\n ok({\"warehouse_id\": args.warehouse_id, \"updated_fields\": updated_fields})\n\n\n# ---------------------------------------------------------------------------\n# 9. list-warehouses\n# ---------------------------------------------------------------------------\n\ndef list_warehouses(conn, args):\n \"\"\"List warehouses for a company.\"\"\"\n company_id = resolve_company_id(conn, getattr(args, 'company_id', None))\n\n w = Table(\"warehouse\").as_(\"w\")\n\n count_q = Q.from_(w).select(fn.Count(\"*\")).where(w.company_id == P())\n if args.parent_id:\n count_q = count_q.where(w.parent_id == P())\n if args.warehouse_type:\n count_q = count_q.where(w.warehouse_type == P())\n\n count_params = [company_id]\n if args.parent_id:\n count_params.append(args.parent_id)\n if args.warehouse_type:\n count_params.append(args.warehouse_type)\n\n count_row = conn.execute(count_q.get_sql(), count_params).fetchone()\n total_count = count_row[0]\n\n limit = int(args.limit) if args.limit else 20\n offset = int(args.offset) if args.offset else 0\n\n rows_q = (Q.from_(w).select(w.star)\n .where(w.company_id == P())\n .orderby(w.name)\n .limit(P()).offset(P()))\n if args.parent_id:\n rows_q = rows_q.where(w.parent_id == P())\n if args.warehouse_type:\n rows_q = rows_q.where(w.warehouse_type == P())\n\n row_params = [company_id]\n if args.parent_id:\n row_params.append(args.parent_id)\n if args.warehouse_type:\n row_params.append(args.warehouse_type)\n row_params.extend([limit, offset])\n\n rows = conn.execute(rows_q.get_sql(), row_params).fetchall()\n\n ok({\"warehouses\": [row_to_dict(r) for r in rows], \"total_count\": total_count,\n \"limit\": limit, \"offset\": offset, \"has_more\": offset + limit \u003c total_count})\n\n\n# ---------------------------------------------------------------------------\n# 10. add-stock-entry\n# ---------------------------------------------------------------------------\n\ndef add_stock_entry(conn, args):\n \"\"\"Create a stock entry in draft.\"\"\"\n if not args.entry_type:\n err(\"--entry-type is required (receive|issue|transfer|manufacture)\")\n entry_type = ENTRY_TYPE_MAP.get(args.entry_type)\n if not entry_type:\n err(f\"Invalid --entry-type '{args.entry_type}'. \"\n f\"Valid: receive, issue, transfer, manufacture\")\n if not args.company_id:\n err(\"--company-id is required\")\n if not args.posting_date:\n err(\"--posting-date is required\")\n if not args.items:\n err(\"--items is required (JSON array)\")\n\n co_t = Table(\"company\")\n co_q = Q.from_(co_t).select(co_t.id).where(co_t.id == P())\n if not conn.execute(co_q.get_sql(), (args.company_id,)).fetchone():\n err(f\"Company {args.company_id} not found\")\n\n items = _parse_json_arg(args.items, \"items\")\n if not items or not isinstance(items, list):\n err(\"--items must be a non-empty JSON array\")\n\n se_id = str(uuid.uuid4())\n naming = get_next_name(conn, \"stock_entry\", company_id=args.company_id)\n\n total_incoming = Decimal(\"0\")\n total_outgoing = Decimal(\"0\")\n\n # Validate and collect item rows before inserting\n item_rows_to_insert = []\n for i, item in enumerate(items):\n item_id = item.get(\"item_id\")\n if not item_id:\n err(f\"Item {i}: item_id is required\")\n\n # Validate item exists\n item_t = Table(\"item\")\n item_q = (Q.from_(item_t)\n .select(item_t.id, item_t.standard_rate)\n .where(item_t.id == P()))\n item_row = conn.execute(item_q.get_sql(), (item_id,)).fetchone()\n if not item_row:\n err(f\"Item {i}: item {item_id} not found\")\n\n qty = to_decimal(item.get(\"qty\", \"0\"))\n if qty \u003c= 0:\n err(f\"Item {i}: qty must be > 0\")\n\n rate = to_decimal(item.get(\"rate\", \"0\"))\n if rate \u003c= 0:\n rate = to_decimal(item_row[\"standard_rate\"])\n\n amount = round_currency(qty * rate)\n\n from_wh = item.get(\"from_warehouse_id\")\n to_wh = item.get(\"to_warehouse_id\")\n\n # Fall back to company's default warehouse if item-level warehouse not specified\n if not to_wh or not from_wh:\n dw_t = Table(\"company\")\n dw_q = Q.from_(dw_t).select(dw_t.default_warehouse_id).where(dw_t.id == P())\n dw_row = conn.execute(dw_q.get_sql(), (args.company_id,)).fetchone()\n default_wh = dw_row[\"default_warehouse_id\"] if dw_row else None\n if not to_wh and default_wh:\n to_wh = default_wh\n if not from_wh and default_wh:\n from_wh = default_wh\n\n # Validate warehouse per entry type\n if entry_type == \"material_receipt\":\n if not to_wh:\n err(f\"Item {i}: to_warehouse_id is required for receipt (set company default warehouse or provide per-item)\")\n total_incoming += amount\n elif entry_type == \"material_issue\":\n if not from_wh:\n err(f\"Item {i}: from_warehouse_id is required for issue\")\n total_outgoing += amount\n elif entry_type == \"material_transfer\":\n if not from_wh:\n err(f\"Item {i}: from_warehouse_id is required for transfer\")\n if not to_wh:\n err(f\"Item {i}: to_warehouse_id is required for transfer\")\n total_incoming += amount\n total_outgoing += amount\n elif entry_type == \"manufacture\":\n # Manufacture: finished goods go to to_warehouse, raw materials come from from_warehouse\n if not from_wh and not to_wh:\n err(f\"Item {i}: from_warehouse_id or to_warehouse_id is required for manufacture\")\n if to_wh:\n total_incoming += amount\n if from_wh:\n total_outgoing += amount\n\n item_rows_to_insert.append((\n str(uuid.uuid4()), se_id, item_id, str(round_currency(qty)),\n from_wh, to_wh,\n str(round_currency(rate)), str(amount),\n item.get(\"batch_id\"), item.get(\"serial_numbers\"),\n ))\n\n value_diff = round_currency(total_incoming - total_outgoing)\n\n # Insert parent stock_entry first (FK target for stock_entry_item)\n se_t = Table(\"stock_entry\")\n se_q = Q.into(se_t).columns(\n \"id\", \"naming_series\", \"stock_entry_type\", \"posting_date\",\n \"total_incoming_value\", \"total_outgoing_value\", \"value_difference\",\n \"status\", \"company_id\",\n ).insert(P(), P(), P(), P(), P(), P(), P(), \"draft\", P())\n conn.execute(\n se_q.get_sql(),\n (se_id, naming, entry_type, args.posting_date,\n str(round_currency(total_incoming)),\n str(round_currency(total_outgoing)),\n str(value_diff), args.company_id),\n )\n\n # Now insert child stock_entry_item rows\n sei_t = Table(\"stock_entry_item\")\n sei_q = Q.into(sei_t).columns(\n \"id\", \"stock_entry_id\", \"item_id\", \"quantity\", \"from_warehouse_id\",\n \"to_warehouse_id\", \"valuation_rate\", \"amount\", \"batch_id\", \"serial_numbers\",\n ).insert(P(), P(), P(), P(), P(), P(), P(), P(), P(), P())\n for row_params in item_rows_to_insert:\n conn.execute(sei_q.get_sql(), row_params)\n\n audit(conn, \"erpclaw-inventory\", \"add-stock-entry\", \"stock_entry\", se_id,\n new_values={\"naming_series\": naming, \"type\": entry_type,\n \"item_count\": len(items)})\n conn.commit()\n ok({\"stock_entry_id\": se_id, \"naming_series\": naming,\n \"total_incoming_value\": str(round_currency(total_incoming)),\n \"total_outgoing_value\": str(round_currency(total_outgoing)),\n \"value_difference\": str(value_diff)})\n\n\n# ---------------------------------------------------------------------------\n# 11. get-stock-entry\n# ---------------------------------------------------------------------------\n\ndef get_stock_entry(conn, args):\n \"\"\"Get stock entry with items.\"\"\"\n if not args.stock_entry_id:\n err(\"--stock-entry-id is required\")\n\n se_t = Table(\"stock_entry\")\n se_q = Q.from_(se_t).select(se_t.star).where(se_t.id == P())\n se = conn.execute(se_q.get_sql(), (args.stock_entry_id,)).fetchone()\n if not se:\n err(f\"Stock entry {args.stock_entry_id} not found\")\n\n data = row_to_dict(se)\n\n sei = Table(\"stock_entry_item\").as_(\"sei\")\n i = Table(\"item\").as_(\"i\")\n items_q = (Q.from_(sei)\n .left_join(i).on(i.id == sei.item_id)\n .select(sei.star, i.item_code, i.item_name)\n .where(sei.stock_entry_id == P())\n .orderby(sei.field(\"rowid\")))\n items = conn.execute(items_q.get_sql(), (args.stock_entry_id,)).fetchall()\n data[\"items\"] = [row_to_dict(r) for r in items]\n ok(data)\n\n\n# ---------------------------------------------------------------------------\n# 12. list-stock-entries\n# ---------------------------------------------------------------------------\n\ndef list_stock_entries(conn, args):\n \"\"\"List stock entries with filtering.\"\"\"\n se = Table(\"stock_entry\").as_(\"se\")\n\n count_q = Q.from_(se).select(fn.Count(\"*\"))\n if args.company_id:\n count_q = count_q.where(se.company_id == P())\n if args.entry_type:\n mapped = ENTRY_TYPE_MAP.get(args.entry_type, args.entry_type)\n count_q = count_q.where(se.stock_entry_type == P())\n if args.se_status:\n count_q = count_q.where(se.status == P())\n if args.from_date:\n count_q = count_q.where(se.posting_date >= P())\n if args.to_date:\n count_q = count_q.where(se.posting_date \u003c= P())\n\n count_params = []\n if args.company_id:\n count_params.append(args.company_id)\n if args.entry_type:\n count_params.append(ENTRY_TYPE_MAP.get(args.entry_type, args.entry_type))\n if args.se_status:\n count_params.append(args.se_status)\n if args.from_date:\n count_params.append(args.from_date)\n if args.to_date:\n count_params.append(args.to_date)\n\n count_row = conn.execute(count_q.get_sql(), count_params).fetchone()\n total_count = count_row[0]\n\n limit = int(args.limit) if args.limit else 20\n offset = int(args.offset) if args.offset else 0\n\n rows_q = (Q.from_(se)\n .select(se.id, se.naming_series, se.stock_entry_type, se.posting_date,\n se.total_incoming_value, se.total_outgoing_value,\n se.value_difference, se.status, se.company_id)\n .orderby(se.posting_date, order=Order.desc)\n .orderby(se.created_at, order=Order.desc)\n .limit(P()).offset(P()))\n if args.company_id:\n rows_q = rows_q.where(se.company_id == P())\n if args.entry_type:\n rows_q = rows_q.where(se.stock_entry_type == P())\n if args.se_status:\n rows_q = rows_q.where(se.status == P())\n if args.from_date:\n rows_q = rows_q.where(se.posting_date >= P())\n if args.to_date:\n rows_q = rows_q.where(se.posting_date \u003c= P())\n\n row_params = []\n if args.company_id:\n row_params.append(args.company_id)\n if args.entry_type:\n row_params.append(ENTRY_TYPE_MAP.get(args.entry_type, args.entry_type))\n if args.se_status:\n row_params.append(args.se_status)\n if args.from_date:\n row_params.append(args.from_date)\n if args.to_date:\n row_params.append(args.to_date)\n row_params.extend([limit, offset])\n\n rows = conn.execute(rows_q.get_sql(), row_params).fetchall()\n\n ok({\"stock_entries\": [row_to_dict(r) for r in rows],\n \"total_count\": total_count, \"limit\": limit, \"offset\": offset,\n \"has_more\": offset + limit \u003c total_count})\n\n\n# ---------------------------------------------------------------------------\n# 13. submit-stock-entry\n# ---------------------------------------------------------------------------\n\ndef submit_stock_entry(conn, args):\n \"\"\"Submit a draft stock entry: post SLE + GL entries.\"\"\"\n if not args.stock_entry_id:\n err(\"--stock-entry-id is required\")\n\n se_t = Table(\"stock_entry\")\n se_q = Q.from_(se_t).select(se_t.star).where(se_t.id == P())\n se = conn.execute(se_q.get_sql(), (args.stock_entry_id,)).fetchone()\n if not se:\n err(f\"Stock entry {args.stock_entry_id} not found\")\n if se[\"status\"] != \"draft\":\n err(f\"Cannot submit: stock entry is '{se['status']}' (must be 'draft')\")\n\n se_dict = row_to_dict(se)\n company_id = se_dict[\"company_id\"]\n posting_date = se_dict[\"posting_date\"]\n entry_type = se_dict[\"stock_entry_type\"]\n\n # Fetch items\n sei_t = Table(\"stock_entry_item\")\n sei_q = (Q.from_(sei_t).select(sei_t.star)\n .where(sei_t.stock_entry_id == P())\n .orderby(Field(\"rowid\")))\n items = conn.execute(sei_q.get_sql(), (args.stock_entry_id,)).fetchall()\n if not items:\n err(\"Stock entry has no items\")\n\n # Find fiscal year for the posting date\n fiscal_year = _get_fiscal_year(conn, posting_date)\n\n # Find cost center for P&L accounts (COGS)\n cost_center_id = _get_cost_center(conn, company_id)\n\n # Build SLE entries from stock entry items\n sle_entries = []\n for item_row in items:\n item = row_to_dict(item_row)\n qty = to_decimal(item[\"quantity\"])\n rate = to_decimal(item[\"valuation_rate\"])\n from_wh = item.get(\"from_warehouse_id\")\n to_wh = item.get(\"to_warehouse_id\")\n\n if entry_type == \"material_receipt\":\n # Positive qty at to_warehouse\n sle_entries.append({\n \"item_id\": item[\"item_id\"],\n \"warehouse_id\": to_wh,\n \"actual_qty\": str(round_currency(qty)),\n \"incoming_rate\": str(round_currency(rate)),\n \"batch_id\": item.get(\"batch_id\"),\n \"serial_number\": item.get(\"serial_numbers\"),\n \"fiscal_year\": fiscal_year,\n })\n elif entry_type == \"material_issue\":\n # Negative qty at from_warehouse\n sle_entries.append({\n \"item_id\": item[\"item_id\"],\n \"warehouse_id\": from_wh,\n \"actual_qty\": str(round_currency(-qty)),\n \"incoming_rate\": \"0\",\n \"batch_id\": item.get(\"batch_id\"),\n \"serial_number\": item.get(\"serial_numbers\"),\n \"fiscal_year\": fiscal_year,\n })\n elif entry_type == \"material_transfer\":\n # Negative at from_warehouse, positive at to_warehouse\n sle_entries.append({\n \"item_id\": item[\"item_id\"],\n \"warehouse_id\": from_wh,\n \"actual_qty\": str(round_currency(-qty)),\n \"incoming_rate\": \"0\",\n \"batch_id\": item.get(\"batch_id\"),\n \"serial_number\": item.get(\"serial_numbers\"),\n \"fiscal_year\": fiscal_year,\n })\n sle_entries.append({\n \"item_id\": item[\"item_id\"],\n \"warehouse_id\": to_wh,\n \"actual_qty\": str(round_currency(qty)),\n \"incoming_rate\": str(round_currency(rate)),\n \"batch_id\": item.get(\"batch_id\"),\n \"serial_number\": item.get(\"serial_numbers\"),\n \"fiscal_year\": fiscal_year,\n })\n elif entry_type == \"manufacture\":\n # Finished goods to to_warehouse, raw materials from from_warehouse\n if to_wh:\n sle_entries.append({\n \"item_id\": item[\"item_id\"],\n \"warehouse_id\": to_wh,\n \"actual_qty\": str(round_currency(qty)),\n \"incoming_rate\": str(round_currency(rate)),\n \"batch_id\": item.get(\"batch_id\"),\n \"serial_number\": item.get(\"serial_numbers\"),\n \"fiscal_year\": fiscal_year,\n })\n if from_wh:\n sle_entries.append({\n \"item_id\": item[\"item_id\"],\n \"warehouse_id\": from_wh,\n \"actual_qty\": str(round_currency(-qty)),\n \"incoming_rate\": \"0\",\n \"batch_id\": item.get(\"batch_id\"),\n \"serial_number\": item.get(\"serial_numbers\"),\n \"fiscal_year\": fiscal_year,\n })\n\n # Insert SLE entries via shared lib\n try:\n sle_ids = insert_sle_entries(\n conn, sle_entries,\n voucher_type=\"stock_entry\",\n voucher_id=args.stock_entry_id,\n posting_date=posting_date,\n company_id=company_id,\n )\n except ValueError as e:\n sys.stderr.write(f\"[erpclaw-inventory] {e}\\n\")\n err(f\"SLE posting failed: {e}\")\n\n # Build SLE dicts with stock_value_difference for GL generation\n sle_t = Table(\"stock_ledger_entry\")\n sle_q = (Q.from_(sle_t).select(sle_t.star)\n .where(sle_t.voucher_type == \"stock_entry\")\n .where(sle_t.voucher_id == P())\n .where(sle_t.is_cancelled == 0))\n sle_rows = conn.execute(sle_q.get_sql(), (args.stock_entry_id,)).fetchall()\n sle_dicts = [row_to_dict(r) for r in sle_rows]\n\n # Create perpetual inventory GL entries\n gl_entries = create_perpetual_inventory_gl(\n conn, sle_dicts,\n voucher_type=\"stock_entry\",\n voucher_id=args.stock_entry_id,\n posting_date=posting_date,\n company_id=company_id,\n cost_center_id=cost_center_id,\n )\n\n gl_ids = []\n if gl_entries:\n # Add fiscal_year to each GL entry\n for gle in gl_entries:\n gle[\"fiscal_year\"] = fiscal_year\n try:\n gl_ids = insert_gl_entries(\n conn, gl_entries,\n voucher_type=\"stock_entry\",\n voucher_id=args.stock_entry_id,\n posting_date=posting_date,\n company_id=company_id,\n remarks=f\"Stock Entry {se_dict['naming_series']}\",\n )\n except ValueError as e:\n sys.stderr.write(f\"[erpclaw-inventory] {e}\\n\")\n err(f\"GL posting failed: {e}\")\n\n # Update status\n conn.execute(\n \"UPDATE stock_entry SET status = 'submitted', updated_at = datetime('now') WHERE id = ?\",\n (args.stock_entry_id,),\n )\n\n audit(conn, \"erpclaw-inventory\", \"submit-stock-entry\", \"stock_entry\", args.stock_entry_id,\n new_values={\"sle_count\": len(sle_ids), \"gl_count\": len(gl_ids)})\n conn.commit()\n\n ok({\"stock_entry_id\": args.stock_entry_id,\n \"sle_entries_created\": len(sle_ids),\n \"gl_entries_created\": len(gl_ids)})\n\n\n# ---------------------------------------------------------------------------\n# 14. cancel-stock-entry\n# ---------------------------------------------------------------------------\n\ndef cancel_stock_entry(conn, args):\n \"\"\"Cancel a submitted stock entry.\"\"\"\n if not args.stock_entry_id:\n err(\"--stock-entry-id is required\")\n\n se_t = Table(\"stock_entry\")\n se_q = Q.from_(se_t).select(se_t.star).where(se_t.id == P())\n se = conn.execute(se_q.get_sql(), (args.stock_entry_id,)).fetchone()\n if not se:\n err(f\"Stock entry {args.stock_entry_id} not found\")\n if se[\"status\"] != \"submitted\":\n err(f\"Cannot cancel: stock entry is '{se['status']}' (must be 'submitted')\",\n suggestion=\"Only submitted stock entries can be cancelled.\")\n\n posting_date = se[\"posting_date\"]\n\n # Reverse SLE entries\n try:\n reversal_sle_ids = reverse_sle_entries(\n conn,\n voucher_type=\"stock_entry\",\n voucher_id=args.stock_entry_id,\n posting_date=posting_date,\n )\n except ValueError as e:\n sys.stderr.write(f\"[erpclaw-inventory] {e}\\n\")\n err(f\"SLE reversal failed: {e}\")\n\n # Reverse GL entries\n try:\n reversal_gl_ids = reverse_gl_entries(\n conn,\n voucher_type=\"stock_entry\",\n voucher_id=args.stock_entry_id,\n posting_date=posting_date,\n )\n except ValueError:\n # GL entries may not exist if perpetual inventory GL was skipped\n reversal_gl_ids = []\n\n # Update status\n conn.execute(\n \"UPDATE stock_entry SET status = 'cancelled', updated_at = datetime('now') WHERE id = ?\",\n (args.stock_entry_id,),\n )\n\n audit(conn, \"erpclaw-inventory\", \"cancel-stock-entry\", \"stock_entry\", args.stock_entry_id,\n new_values={\"reversed\": True})\n conn.commit()\n\n ok({\"stock_entry_id\": args.stock_entry_id, \"reversed\": True,\n \"sle_reversals\": len(reversal_sle_ids),\n \"gl_reversals\": len(reversal_gl_ids)})\n\n\n# ---------------------------------------------------------------------------\n# 15. create-stock-ledger-entries (cross-skill)\n# ---------------------------------------------------------------------------\n\ndef create_stock_ledger_entries(conn, args):\n \"\"\"Cross-skill: create SLE entries (called by selling/buying).\"\"\"\n if not args.voucher_type:\n err(\"--voucher-type is required\")\n if not args.voucher_id:\n err(\"--voucher-id is required\")\n if not args.posting_date:\n err(\"--posting-date is required\")\n if not args.entries:\n err(\"--entries is required (JSON array)\")\n if not args.company_id:\n err(\"--company-id is required\")\n\n entries = _parse_json_arg(args.entries, \"entries\")\n if not entries or not isinstance(entries, list):\n err(\"--entries must be a non-empty JSON array\")\n\n fiscal_year = _get_fiscal_year(conn, args.posting_date)\n\n # Add fiscal_year to each entry\n for entry in entries:\n entry[\"fiscal_year\"] = fiscal_year\n\n try:\n sle_ids = insert_sle_entries(\n conn, entries,\n voucher_type=args.voucher_type,\n voucher_id=args.voucher_id,\n posting_date=args.posting_date,\n company_id=args.company_id,\n )\n except ValueError as e:\n sys.stderr.write(f\"[erpclaw-inventory] {e}\\n\")\n err(f\"SLE posting failed: {e}\")\n\n audit(conn, \"erpclaw-inventory\", \"create-stock-ledger-entries\", \"stock_ledger_entry\",\n args.voucher_id,\n new_values={\"voucher_type\": args.voucher_type,\n \"sle_count\": len(sle_ids)})\n conn.commit()\n ok({\"sle_ids\": sle_ids, \"count\": len(sle_ids)})\n\n\n# ---------------------------------------------------------------------------\n# 16. reverse-stock-ledger-entries (cross-skill)\n# ---------------------------------------------------------------------------\n\ndef reverse_stock_ledger_entries(conn, args):\n \"\"\"Cross-skill: reverse SLE entries (called by selling/buying).\"\"\"\n if not args.voucher_type:\n err(\"--voucher-type is required\")\n if not args.voucher_id:\n err(\"--voucher-id is required\")\n if not args.posting_date:\n err(\"--posting-date is required\")\n\n try:\n reversal_ids = reverse_sle_entries(\n conn,\n voucher_type=args.voucher_type,\n voucher_id=args.voucher_id,\n posting_date=args.posting_date,\n )\n except ValueError as e:\n sys.stderr.write(f\"[erpclaw-inventory] {e}\\n\")\n err(f\"SLE reversal failed: {e}\")\n\n audit(conn, \"erpclaw-inventory\", \"reverse-stock-ledger-entries\", \"stock_ledger_entry\",\n args.voucher_id,\n new_values={\"voucher_type\": args.voucher_type,\n \"reversal_count\": len(reversal_ids)})\n conn.commit()\n ok({\"reversal_ids\": reversal_ids, \"count\": len(reversal_ids)})\n\n\n# ---------------------------------------------------------------------------\n# 17. get-stock-balance\n# ---------------------------------------------------------------------------\n\ndef get_stock_balance_action(conn, args):\n \"\"\"Get stock balance for an item in a warehouse.\"\"\"\n if not args.item_id:\n err(\"--item-id is required\")\n if not args.warehouse_id:\n err(\"--warehouse-id is required\")\n\n balance = get_stock_balance(conn, args.item_id, args.warehouse_id)\n ok({\"item_id\": args.item_id, \"warehouse_id\": args.warehouse_id,\n \"qty\": balance[\"qty\"], \"valuation_rate\": balance[\"valuation_rate\"],\n \"stock_value\": balance[\"stock_value\"]})\n\n\n# ---------------------------------------------------------------------------\n# 18. stock-balance-report\n# ---------------------------------------------------------------------------\n\ndef stock_balance_report(conn, args):\n \"\"\"All items stock summary for a company.\"\"\"\n company_id = resolve_company_id(conn, getattr(args, 'company_id', None))\n\n # This query uses decimal_sum() aggregate and a correlated subquery for\n # valuation_rate — kept as raw SQL due to complexity of correlated subquery\n # and HAVING clause with decimal_sum()\n conditions = [\n \"sle.is_cancelled = 0\",\n \"w.company_id = ?\",\n ]\n params = [company_id]\n\n if args.warehouse_id:\n conditions.append(\"sle.warehouse_id = ?\")\n params.append(args.warehouse_id)\n\n where = \" AND \".join(conditions)\n\n rows = conn.execute(\n f\"\"\"SELECT sle.item_id, sle.warehouse_id,\n i.item_code, i.item_name, w.name AS warehouse_name,\n decimal_sum(sle.actual_qty) AS balance_qty,\n COALESCE(\n (SELECT valuation_rate FROM stock_ledger_entry s2\n WHERE s2.item_id = sle.item_id AND s2.warehouse_id = sle.warehouse_id\n AND s2.is_cancelled = 0\n ORDER BY s2.rowid DESC LIMIT 1),\n '0'\n ) AS valuation_rate\n FROM stock_ledger_entry sle\n JOIN item i ON i.id = sle.item_id\n JOIN warehouse w ON w.id = sle.warehouse_id\n WHERE {where}\n GROUP BY sle.item_id, sle.warehouse_id\n HAVING decimal_sum(sle.actual_qty) + 0 != 0\n ORDER BY i.item_name, w.name\"\"\",\n params,\n ).fetchall()\n\n report = []\n total_value = Decimal(\"0\")\n for row in rows:\n qty = to_decimal(str(row[\"balance_qty\"]))\n rate = to_decimal(str(row[\"valuation_rate\"]))\n value = round_currency(qty * rate)\n total_value += value\n report.append({\n \"item_id\": row[\"item_id\"],\n \"item_code\": row[\"item_code\"],\n \"item_name\": row[\"item_name\"],\n \"warehouse_id\": row[\"warehouse_id\"],\n \"warehouse_name\": row[\"warehouse_name\"],\n \"qty\": str(round_currency(qty)),\n \"valuation_rate\": str(round_currency(rate)),\n \"stock_value\": str(value),\n })\n\n ok({\"report\": report, \"total_stock_value\": str(round_currency(total_value)),\n \"row_count\": len(report)})\n\n\n# ---------------------------------------------------------------------------\n# 19. stock-ledger-report\n# ---------------------------------------------------------------------------\n\ndef stock_ledger_report(conn, args):\n \"\"\"Stock ledger entry detail report.\"\"\"\n sle = Table(\"stock_ledger_entry\").as_(\"sle\")\n i = Table(\"item\").as_(\"i\")\n w = Table(\"warehouse\").as_(\"w\")\n\n rows_q = (Q.from_(sle)\n .left_join(i).on(i.id == sle.item_id)\n .left_join(w).on(w.id == sle.warehouse_id)\n .select(sle.star, i.item_code, i.item_name, w.name.as_(\"warehouse_name\"))\n .where(sle.is_cancelled == 0)\n .orderby(sle.posting_date, order=Order.desc)\n .orderby(sle.created_at, order=Order.desc)\n .limit(P()).offset(P()))\n\n params = []\n if args.item_id:\n rows_q = rows_q.where(sle.item_id == P())\n params.append(args.item_id)\n if args.warehouse_id:\n rows_q = rows_q.where(sle.warehouse_id == P())\n params.append(args.warehouse_id)\n if args.from_date:\n rows_q = rows_q.where(sle.posting_date >= P())\n params.append(args.from_date)\n if args.to_date:\n rows_q = rows_q.where(sle.posting_date \u003c= P())\n params.append(args.to_date)\n\n limit = int(args.limit) if args.limit else 100\n offset = int(args.offset) if args.offset else 0\n params.extend([limit, offset])\n\n rows = conn.execute(rows_q.get_sql(), params).fetchall()\n\n ok({\"entries\": [row_to_dict(r) for r in rows], \"count\": len(rows)})\n\n\n# ---------------------------------------------------------------------------\n# 20. add-batch\n# ---------------------------------------------------------------------------\n\ndef add_batch(conn, args):\n \"\"\"Create a batch.\"\"\"\n if not args.item_id:\n err(\"--item-id is required\")\n if not args.batch_name:\n err(\"--batch-name is required\")\n\n item_t = Table(\"item\")\n item_q = (Q.from_(item_t)\n .select(item_t.id, item_t.has_batch)\n .where(item_t.id == P()))\n item = conn.execute(item_q.get_sql(), (args.item_id,)).fetchone()\n if not item:\n err(f\"Item {args.item_id} not found\")\n\n batch_id = str(uuid.uuid4())\n t = Table(\"batch\")\n q = Q.into(t).columns(\n \"id\", \"batch_name\", \"item_id\", \"manufacturing_date\", \"expiry_date\",\n ).insert(P(), P(), P(), P(), P())\n try:\n conn.execute(\n q.get_sql(),\n (batch_id, args.batch_name, args.item_id,\n args.manufacturing_date, args.expiry_date),\n )\n except sqlite3.IntegrityError as e:\n sys.stderr.write(f\"[erpclaw-inventory] {e}\\n\")\n err(\"Batch creation failed — check for duplicates or invalid data\")\n\n audit(conn, \"erpclaw-inventory\", \"add-batch\", \"batch\", batch_id,\n new_values={\"batch_name\": args.batch_name, \"item_id\": args.item_id})\n conn.commit()\n ok({\"batch_id\": batch_id, \"batch_name\": args.batch_name})\n\n\n# ---------------------------------------------------------------------------\n# 21. list-batches\n# ---------------------------------------------------------------------------\n\ndef list_batches(conn, args):\n \"\"\"List batches with optional filters.\"\"\"\n limit = int(args.limit) if args.limit else 20\n offset = int(args.offset) if args.offset else 0\n\n if args.warehouse_id:\n # Filter by batches that have stock in the specified warehouse\n # Uses decimal_sum() HAVING — kept as raw SQL\n conditions = [\"1=1\"]\n params = []\n if args.item_id:\n conditions.append(\"b.item_id = ?\")\n params.append(args.item_id)\n where = \" AND \".join(conditions)\n\n count_row = conn.execute(\n f\"\"\"SELECT COUNT(*) FROM (\n SELECT b.id\n FROM batch b\n JOIN stock_ledger_entry sle ON sle.batch_id = b.id\n WHERE {where} AND sle.warehouse_id = ? AND sle.is_cancelled = 0\n GROUP BY b.id\n HAVING decimal_sum(sle.actual_qty) + 0 > 0\n )\"\"\",\n params + [args.warehouse_id],\n ).fetchone()\n total_count = count_row[0]\n\n rows = conn.execute(\n f\"\"\"SELECT DISTINCT b.*\n FROM batch b\n JOIN stock_ledger_entry sle ON sle.batch_id = b.id\n WHERE {where} AND sle.warehouse_id = ? AND sle.is_cancelled = 0\n GROUP BY b.id\n HAVING decimal_sum(sle.actual_qty) + 0 > 0\n ORDER BY b.batch_name\n LIMIT ? OFFSET ?\"\"\",\n params + [args.warehouse_id, limit, offset],\n ).fetchall()\n else:\n b = Table(\"batch\").as_(\"b\")\n\n count_q = Q.from_(b).select(fn.Count(\"*\"))\n if args.item_id:\n count_q = count_q.where(b.item_id == P())\n\n count_params = []\n if args.item_id:\n count_params.append(args.item_id)\n\n count_row = conn.execute(count_q.get_sql(), count_params).fetchone()\n total_count = count_row[0]\n\n rows_q = (Q.from_(b).select(b.star)\n .orderby(b.batch_name)\n .limit(P()).offset(P()))\n if args.item_id:\n rows_q = rows_q.where(b.item_id == P())\n\n row_params = []\n if args.item_id:\n row_params.append(args.item_id)\n row_params.extend([limit, offset])\n\n rows = conn.execute(rows_q.get_sql(), row_params).fetchall()\n\n ok({\"batches\": [row_to_dict(r) for r in rows], \"total_count\": total_count,\n \"limit\": limit, \"offset\": offset, \"has_more\": offset + limit \u003c total_count})\n\n\n# ---------------------------------------------------------------------------\n# 22. add-serial-number\n# ---------------------------------------------------------------------------\n\ndef add_serial_number(conn, args):\n \"\"\"Register a serial number.\"\"\"\n if not args.item_id:\n err(\"--item-id is required\")\n if not args.serial_no:\n err(\"--serial-no is required\")\n\n item_t = Table(\"item\")\n item_q = Q.from_(item_t).select(item_t.id).where(item_t.id == P())\n item = conn.execute(item_q.get_sql(), (args.item_id,)).fetchone()\n if not item:\n err(f\"Item {args.item_id} not found\")\n\n sn_id = str(uuid.uuid4())\n t = Table(\"serial_number\")\n q = Q.into(t).columns(\n \"id\", \"serial_no\", \"item_id\", \"warehouse_id\", \"batch_id\", \"status\",\n ).insert(P(), P(), P(), P(), P(), \"active\")\n try:\n conn.execute(\n q.get_sql(),\n (sn_id, args.serial_no, args.item_id,\n args.warehouse_id, args.batch_id),\n )\n except sqlite3.IntegrityError as e:\n sys.stderr.write(f\"[erpclaw-inventory] {e}\\n\")\n err(\"Serial number creation failed — check for duplicates or invalid data\")\n\n audit(conn, \"erpclaw-inventory\", \"add-serial-number\", \"serial_number\", sn_id,\n new_values={\"serial_no\": args.serial_no, \"item_id\": args.item_id})\n conn.commit()\n ok({\"serial_number_id\": sn_id, \"serial_no\": args.serial_no})\n\n\n# ---------------------------------------------------------------------------\n# 23. list-serial-numbers\n# ---------------------------------------------------------------------------\n\ndef list_serial_numbers(conn, args):\n \"\"\"List serial numbers with optional filters.\"\"\"\n sn = Table(\"serial_number\").as_(\"sn\")\n i = Table(\"item\").as_(\"i\")\n\n count_q = Q.from_(sn).select(fn.Count(\"*\"))\n if args.item_id:\n count_q = count_q.where(sn.item_id == P())\n if args.warehouse_id:\n count_q = count_q.where(sn.warehouse_id == P())\n if args.sn_status:\n if args.sn_status not in VALID_SERIAL_STATUSES:\n err(f\"--status must be one of: {', '.join(VALID_SERIAL_STATUSES)}\")\n count_q = count_q.where(sn.status == P())\n\n count_params = []\n if args.item_id:\n count_params.append(args.item_id)\n if args.warehouse_id:\n count_params.append(args.warehouse_id)\n if args.sn_status:\n count_params.append(args.sn_status)\n\n count_row = conn.execute(count_q.get_sql(), count_params).fetchone()\n total_count = count_row[0]\n\n limit = int(args.limit) if args.limit else 20\n offset = int(args.offset) if args.offset else 0\n\n rows_q = (Q.from_(sn)\n .left_join(i).on(i.id == sn.item_id)\n .select(sn.star, i.item_code, i.item_name)\n .orderby(sn.serial_no)\n .limit(P()).offset(P()))\n if args.item_id:\n rows_q = rows_q.where(sn.item_id == P())\n if args.warehouse_id:\n rows_q = rows_q.where(sn.warehouse_id == P())\n if args.sn_status:\n rows_q = rows_q.where(sn.status == P())\n\n row_params = []\n if args.item_id:\n row_params.append(args.item_id)\n if args.warehouse_id:\n row_params.append(args.warehouse_id)\n if args.sn_status:\n row_params.append(args.sn_status)\n row_params.extend([limit, offset])\n\n rows = conn.execute(rows_q.get_sql(), row_params).fetchall()\n\n ok({\"serial_numbers\": [row_to_dict(r) for r in rows], \"total_count\": total_count,\n \"limit\": limit, \"offset\": offset, \"has_more\": offset + limit \u003c total_count})\n\n\n# ---------------------------------------------------------------------------\n# 24. add-price-list\n# ---------------------------------------------------------------------------\n\ndef add_price_list(conn, args):\n \"\"\"Create a price list.\"\"\"\n if not args.name:\n err(\"--name is required\")\n\n pl_id = str(uuid.uuid4())\n currency = args.currency or \"USD\"\n is_buying = int(args.is_buying) if args.is_buying else 0\n is_selling = int(args.is_selling) if args.is_selling else 0\n\n t = Table(\"price_list\")\n q = Q.into(t).columns(\"id\", \"name\", \"currency\", \"buying\", \"selling\").insert(P(), P(), P(), P(), P())\n try:\n conn.execute(q.get_sql(), (pl_id, args.name, currency, is_buying, is_selling))\n except sqlite3.IntegrityError as e:\n sys.stderr.write(f\"[erpclaw-inventory] {e}\\n\")\n err(\"Price list creation failed — check for duplicates or invalid data\")\n\n audit(conn, \"erpclaw-inventory\", \"add-price-list\", \"price_list\", pl_id,\n new_values={\"name\": args.name})\n conn.commit()\n ok({\"price_list_id\": pl_id, \"name\": args.name})\n\n\n# ---------------------------------------------------------------------------\n# 25. add-item-price\n# ---------------------------------------------------------------------------\n\ndef add_item_price(conn, args):\n \"\"\"Set a price for an item in a price list.\"\"\"\n if not args.item_id:\n err(\"--item-id is required\")\n if not args.price_list_id:\n err(\"--price-list-id is required\")\n if not args.rate:\n err(\"--rate is required\")\n\n # Validate references\n item_t = Table(\"item\")\n item_q = Q.from_(item_t).select(item_t.id).where(item_t.id == P())\n if not conn.execute(item_q.get_sql(), (args.item_id,)).fetchone():\n err(f\"Item {args.item_id} not found\")\n\n pl_t = Table(\"price_list\")\n pl_q = Q.from_(pl_t).select(pl_t.id).where(pl_t.id == P())\n if not conn.execute(pl_q.get_sql(), (args.price_list_id,)).fetchone():\n err(f\"Price list {args.price_list_id} not found\")\n\n rate = round_currency(to_decimal(args.rate))\n min_qty = str(to_decimal(args.min_qty or \"0\"))\n\n ip_id = str(uuid.uuid4())\n t = Table(\"item_price\")\n q = Q.into(t).columns(\n \"id\", \"item_id\", \"price_list_id\", \"rate\", \"min_qty\", \"valid_from\", \"valid_to\",\n ).insert(P(), P(), P(), P(), P(), P(), P())\n conn.execute(\n q.get_sql(),\n (ip_id, args.item_id, args.price_list_id, str(rate),\n min_qty, args.valid_from, args.valid_to),\n )\n\n audit(conn, \"erpclaw-inventory\", \"add-item-price\", \"item_price\", ip_id,\n new_values={\"item_id\": args.item_id, \"rate\": str(rate)})\n conn.commit()\n ok({\"item_price_id\": ip_id, \"rate\": str(rate)})\n\n\n# ---------------------------------------------------------------------------\n# 26. get-item-price\n# ---------------------------------------------------------------------------\n\ndef get_item_price(conn, args):\n \"\"\"Get applicable price for an item from a price list.\"\"\"\n if not args.item_id:\n err(\"--item-id is required\")\n if not args.price_list_id:\n err(\"--price-list-id is required\")\n\n qty = to_decimal(args.qty or \"1\")\n today = datetime.now(timezone.utc).strftime(\"%Y-%m-%d\")\n\n # Find best matching price: valid date range, min_qty \u003c= requested qty\n # Order by min_qty DESC to get the most specific tier first\n # This query uses IS NULL comparisons — kept as raw SQL (rule 16)\n rows = conn.execute(\n \"\"\"SELECT * FROM item_price\n WHERE item_id = ? AND price_list_id = ?\n AND min_qty + 0 \u003c= ? + 0\n AND (valid_from IS NULL OR valid_from \u003c= ?)\n AND (valid_to IS NULL OR valid_to >= ?)\n ORDER BY min_qty + 0 DESC\n LIMIT 1\"\"\",\n (args.item_id, args.price_list_id, str(qty), today, today),\n ).fetchone()\n\n if not rows:\n # Fallback: any price for this item/price list (ignoring date/qty)\n ip_t = Table(\"item_price\")\n fallback_q = (Q.from_(ip_t).select(ip_t.star)\n .where(ip_t.item_id == P())\n .where(ip_t.price_list_id == P())\n .orderby(ip_t.created_at, order=Order.desc)\n .limit(1))\n rows = conn.execute(\n fallback_q.get_sql(),\n (args.item_id, args.price_list_id),\n ).fetchone()\n\n if not rows:\n err(f\"No price found for item {args.item_id} in price list {args.price_list_id}\")\n\n data = row_to_dict(rows)\n ok(data)\n\n\n# ---------------------------------------------------------------------------\n# 27. add-pricing-rule\n# ---------------------------------------------------------------------------\n\ndef add_pricing_rule(conn, args):\n \"\"\"Create a pricing/discount rule.\"\"\"\n if not args.name:\n err(\"--name is required\")\n if not args.applies_to:\n err(\"--applies-to is required (item|item_group|customer|customer_group)\")\n if args.applies_to not in (\"item\", \"item_group\", \"customer\", \"customer_group\"):\n err(\"--applies-to must be item|item_group|customer|customer_group\")\n if not args.company_id:\n err(\"--company-id is required\")\n\n pr_id = str(uuid.uuid4())\n t = Table(\"pricing_rule\")\n q = Q.into(t).columns(\n \"id\", \"name\", \"applies_to\", \"entity_id\", \"discount_percentage\", \"rate\",\n \"min_qty\", \"max_qty\", \"valid_from\", \"valid_to\", \"priority\", \"company_id\",\n ).insert(P(), P(), P(), P(), P(), P(), P(), P(), P(), P(), P(), P())\n conn.execute(\n q.get_sql(),\n (pr_id, args.name, args.applies_to, args.entity_id,\n args.discount_percentage, args.pr_rate,\n args.min_qty, args.max_qty,\n args.valid_from, args.valid_to,\n args.priority or 0, args.company_id),\n )\n\n audit(conn, \"erpclaw-inventory\", \"add-pricing-rule\", \"pricing_rule\", pr_id,\n new_values={\"name\": args.name, \"applies_to\": args.applies_to})\n conn.commit()\n ok({\"pricing_rule_id\": pr_id, \"name\": args.name})\n\n\n# ---------------------------------------------------------------------------\n# 28. add-stock-reconciliation\n# ---------------------------------------------------------------------------\n\ndef add_stock_reconciliation(conn, args):\n \"\"\"Create a stock reconciliation (physical count).\"\"\"\n if not args.posting_date:\n err(\"--posting-date is required\")\n if not args.items:\n err(\"--items is required (JSON array)\")\n if not args.company_id:\n err(\"--company-id is required\")\n\n co_t = Table(\"company\")\n co_q = Q.from_(co_t).select(co_t.id).where(co_t.id == P())\n if not conn.execute(co_q.get_sql(), (args.company_id,)).fetchone():\n err(f\"Company {args.company_id} not found\")\n\n items = _parse_json_arg(args.items, \"items\")\n if not items or not isinstance(items, list):\n err(\"--items must be a non-empty JSON array\")\n\n sr_id = str(uuid.uuid4())\n naming = get_next_name(conn, \"stock_reconciliation\",\n company_id=args.company_id)\n\n total_diff_amount = Decimal(\"0\")\n\n # Validate and collect item rows before inserting\n item_rows_to_insert = []\n for i, item in enumerate(items):\n item_id = item.get(\"item_id\")\n warehouse_id = item.get(\"warehouse_id\")\n if not item_id:\n err(f\"Item {i}: item_id is required\")\n if not warehouse_id:\n err(f\"Item {i}: warehouse_id is required\")\n\n # Get current stock balance\n balance = get_stock_balance(conn, item_id, warehouse_id)\n current_qty = to_decimal(balance[\"qty\"])\n current_rate = to_decimal(balance[\"valuation_rate\"])\n\n counted_qty = to_decimal(item.get(\"qty\", \"0\"))\n counted_rate = to_decimal(item.get(\"valuation_rate\", str(current_rate)))\n\n qty_diff = round_currency(counted_qty - current_qty)\n current_value = round_currency(current_qty * current_rate)\n counted_value = round_currency(counted_qty * counted_rate)\n amount_diff = round_currency(counted_value - current_value)\n total_diff_amount += amount_diff\n\n item_rows_to_insert.append((\n str(uuid.uuid4()), sr_id, item_id, warehouse_id,\n str(round_currency(current_qty)), str(round_currency(current_rate)),\n str(round_currency(counted_qty)), str(round_currency(counted_rate)),\n str(qty_diff), str(amount_diff),\n ))\n\n # Insert parent stock_reconciliation first (FK target for items)\n sr_t = Table(\"stock_reconciliation\")\n sr_q = Q.into(sr_t).columns(\n \"id\", \"naming_series\", \"posting_date\", \"difference_amount\",\n \"status\", \"company_id\",\n ).insert(P(), P(), P(), P(), \"draft\", P())\n conn.execute(\n sr_q.get_sql(),\n (sr_id, naming, args.posting_date,\n str(round_currency(total_diff_amount)), args.company_id),\n )\n\n # Now insert child stock_reconciliation_item rows\n sri_t = Table(\"stock_reconciliation_item\")\n sri_q = Q.into(sri_t).columns(\n \"id\", \"stock_reconciliation_id\", \"item_id\", \"warehouse_id\",\n \"current_qty\", \"current_valuation_rate\", \"qty\", \"valuation_rate\",\n \"quantity_difference\", \"amount_difference\",\n ).insert(P(), P(), P(), P(), P(), P(), P(), P(), P(), P())\n for row_params in item_rows_to_insert:\n conn.execute(sri_q.get_sql(), row_params)\n\n audit(conn, \"erpclaw-inventory\", \"add-stock-reconciliation\", \"stock_reconciliation\", sr_id,\n new_values={\"naming_series\": naming, \"item_count\": len(items),\n \"difference_amount\": str(round_currency(total_diff_amount))})\n conn.commit()\n ok({\"stock_reconciliation_id\": sr_id, \"naming_series\": naming,\n \"difference_amount\": str(round_currency(total_diff_amount)),\n \"item_count\": len(items)})\n\n\n# ---------------------------------------------------------------------------\n# 29. submit-stock-reconciliation\n# ---------------------------------------------------------------------------\n\ndef submit_stock_reconciliation(conn, args):\n \"\"\"Submit a stock reconciliation: post SLE + GL for differences.\"\"\"\n if not args.stock_reconciliation_id:\n err(\"--stock-reconciliation-id is required\")\n\n sr_t = Table(\"stock_reconciliation\")\n sr_q = Q.from_(sr_t).select(sr_t.star).where(sr_t.id == P())\n sr = conn.execute(sr_q.get_sql(), (args.stock_reconciliation_id,)).fetchone()\n if not sr:\n err(f\"Stock reconciliation {args.stock_reconciliation_id} not found\")\n if sr[\"status\"] != \"draft\":\n err(f\"Cannot submit: reconciliation is '{sr['status']}' (must be 'draft')\")\n\n sr_dict = row_to_dict(sr)\n company_id = sr_dict[\"company_id\"]\n posting_date = sr_dict[\"posting_date\"]\n\n # Fetch reconciliation items\n sri_t = Table(\"stock_reconciliation_item\")\n sri_q = (Q.from_(sri_t).select(sri_t.star)\n .where(sri_t.stock_reconciliation_id == P()))\n sri_rows = conn.execute(sri_q.get_sql(), (args.stock_reconciliation_id,)).fetchall()\n if not sri_rows:\n err(\"Stock reconciliation has no items\")\n\n fiscal_year = _get_fiscal_year(conn, posting_date)\n cost_center_id = _get_cost_center(conn, company_id)\n\n # Build SLE entries for quantity differences\n sle_entries = []\n for sri in sri_rows:\n item = row_to_dict(sri)\n qty_diff = to_decimal(item[\"quantity_difference\"])\n if qty_diff == 0:\n continue\n\n valuation_rate = to_decimal(item[\"valuation_rate\"])\n sle_entries.append({\n \"item_id\": item[\"item_id\"],\n \"warehouse_id\": item[\"warehouse_id\"],\n \"actual_qty\": str(round_currency(qty_diff)),\n \"incoming_rate\": str(round_currency(valuation_rate)) if qty_diff > 0 else \"0\",\n \"fiscal_year\": fiscal_year,\n })\n\n sle_ids = []\n if sle_entries:\n try:\n sle_ids = insert_sle_entries(\n conn, sle_entries,\n voucher_type=\"stock_reconciliation\",\n voucher_id=args.stock_reconciliation_id,\n posting_date=posting_date,\n company_id=company_id,\n )\n except ValueError as e:\n sys.stderr.write(f\"[erpclaw-inventory] {e}\\n\")\n err(f\"SLE posting failed: {e}\")\n\n # Build GL entries for value adjustments\n gl_ids = []\n if sle_ids:\n sle_rows_t = Table(\"stock_ledger_entry\")\n sle_rows_q = (Q.from_(sle_rows_t).select(sle_rows_t.star)\n .where(sle_rows_t.voucher_type == \"stock_reconciliation\")\n .where(sle_rows_t.voucher_id == P())\n .where(sle_rows_t.is_cancelled == 0))\n sle_rows = conn.execute(sle_rows_q.get_sql(), (args.stock_reconciliation_id,)).fetchall()\n sle_dicts = [row_to_dict(r) for r in sle_rows]\n\n # Find stock adjustment account as contra for reconciliation\n # Uses account_type filter — kept as PyPika\n acct_t = Table(\"account\")\n acct_q = (Q.from_(acct_t).select(acct_t.id)\n .where(acct_t.account_type == \"stock_adjustment\")\n .where(acct_t.company_id == P())\n .where(acct_t.is_group == 0)\n .limit(1))\n stock_adj_acct = conn.execute(acct_q.get_sql(), (company_id,)).fetchone()\n expense_account_id = stock_adj_acct[\"id\"] if stock_adj_acct else None\n\n gl_entries = create_perpetual_inventory_gl(\n conn, sle_dicts,\n voucher_type=\"stock_reconciliation\",\n voucher_id=args.stock_reconciliation_id,\n posting_date=posting_date,\n company_id=company_id,\n expense_account_id=expense_account_id,\n cost_center_id=cost_center_id,\n )\n\n if gl_entries:\n for gle in gl_entries:\n gle[\"fiscal_year\"] = fiscal_year\n try:\n gl_ids = insert_gl_entries(\n conn, gl_entries,\n voucher_type=\"stock_reconciliation\",\n voucher_id=args.stock_reconciliation_id,\n posting_date=posting_date,\n company_id=company_id,\n remarks=f\"Stock Reconciliation {sr_dict['naming_series']}\",\n )\n except ValueError as e:\n sys.stderr.write(f\"[erpclaw-inventory] {e}\\n\")\n err(f\"GL posting failed: {e}\")\n\n # Update status\n conn.execute(\n \"UPDATE stock_reconciliation SET status = 'submitted', updated_at = datetime('now') WHERE id = ?\",\n (args.stock_reconciliation_id,),\n )\n\n audit(conn, \"erpclaw-inventory\", \"submit-stock-reconciliation\", \"stock_reconciliation\",\n args.stock_reconciliation_id,\n new_values={\"sle_count\": len(sle_ids), \"gl_count\": len(gl_ids)})\n conn.commit()\n\n ok({\"stock_reconciliation_id\": args.stock_reconciliation_id,\n \"sle_entries_created\": len(sle_ids),\n \"gl_entries_created\": len(gl_ids)})\n\n\n# ---------------------------------------------------------------------------\n# 30. revalue-stock\n# ---------------------------------------------------------------------------\n\ndef revalue_stock(conn, args):\n \"\"\"Revalue inventory for an item in a warehouse.\n\n Changes the valuation rate without affecting quantity. Creates:\n - SLE entry with actual_qty=0 recording the new rate\n - GL entries adjusting stock value (Stock-in-Hand vs Stock-Adjustment)\n - Audit trail in stock_revaluation table\n\n This is a one-step action: no draft state, posts immediately.\n \"\"\"\n item_id = args.item_id\n warehouse_id = args.warehouse_id\n new_rate = args.new_rate\n posting_date = args.posting_date\n reason = args.reason\n\n if not item_id:\n err(\"--item-id is required\")\n if not warehouse_id:\n err(\"--warehouse-id is required\")\n if not new_rate:\n err(\"--new-rate is required\")\n if not posting_date:\n err(\"--posting-date is required\")\n\n # Validate new_rate\n try:\n new_rate_d = to_decimal(new_rate)\n except (InvalidOperation, ValueError):\n err(f\"Invalid --new-rate: {new_rate}\")\n if new_rate_d \u003c 0:\n err(\"--new-rate must be non-negative\")\n\n # Validate item exists and is a stock item\n item_t = Table(\"item\")\n item_q = (Q.from_(item_t)\n .select(item_t.id, item_t.item_code, item_t.item_name, item_t.is_stock_item)\n .where(item_t.id == P()))\n item_row = conn.execute(item_q.get_sql(), (item_id,)).fetchone()\n if not item_row:\n err(f\"Item {item_id} not found\")\n if not item_row[\"is_stock_item\"]:\n err(f\"Item {item_row['item_name']} is not a stock item\")\n\n # Validate warehouse\n wh_t = Table(\"warehouse\")\n wh_q = (Q.from_(wh_t)\n .select(wh_t.id, wh_t.name, wh_t.company_id, wh_t.account_id)\n .where(wh_t.id == P()))\n wh_row = conn.execute(wh_q.get_sql(), (warehouse_id,)).fetchone()\n if not wh_row:\n err(f\"Warehouse {warehouse_id} not found\")\n company_id = wh_row[\"company_id\"]\n\n # Get current stock balance\n balance = get_stock_balance(conn, item_id, warehouse_id)\n current_qty = to_decimal(balance[\"qty\"])\n old_rate_d = to_decimal(balance[\"valuation_rate\"])\n old_value = to_decimal(balance[\"stock_value\"])\n\n if current_qty \u003c= 0:\n err(f\"Cannot revalue: no stock on hand for item '{item_row['item_name']}' \"\n f\"in warehouse '{wh_row['name']}' (qty={current_qty})\")\n\n if new_rate_d == old_rate_d:\n err(f\"New rate ({new_rate_d}) is the same as current rate ({old_rate_d}). No revaluation needed.\")\n\n # Compute adjustment\n new_value = round_currency(current_qty * new_rate_d)\n adjustment = round_currency(new_value - old_value)\n\n fiscal_year = _get_fiscal_year(conn, posting_date)\n cost_center_id = _get_cost_center(conn, company_id)\n\n # Generate IDs\n reval_id = str(uuid.uuid4())\n sle_id = str(uuid.uuid4())\n\n # Naming series\n naming = get_next_name(conn, \"stock_revaluation\", company_id=company_id)\n\n # --- Single atomic transaction ---\n\n # 1. Insert SLE with actual_qty=0 but new valuation and value difference\n # Uses datetime('now') as LiteralValue and mixed literal/param values\n # Kept as raw SQL for clarity with the mixed NULL/literal/param pattern\n conn.execute(\n \"\"\"\n INSERT INTO stock_ledger_entry (\n id, posting_date, posting_time, item_id, warehouse_id,\n actual_qty, qty_after_transaction, valuation_rate,\n stock_value, stock_value_difference,\n voucher_type, voucher_id, batch_id, serial_number,\n incoming_rate, is_cancelled, fiscal_year, created_at\n ) VALUES (?, ?, NULL, ?, ?, '0', ?, ?, ?, ?, 'stock_revaluation', ?,\n NULL, NULL, '0', 0, ?, datetime('now'))\n \"\"\",\n (\n sle_id, posting_date, item_id, warehouse_id,\n str(round_currency(current_qty)),\n str(round_currency(new_rate_d)),\n str(new_value),\n str(adjustment),\n reval_id,\n fiscal_year,\n ),\n )\n\n # 2. Create GL entries for the value adjustment\n gl_ids = []\n if adjustment != 0:\n # Stock-in-Hand account (from warehouse)\n warehouse_account_id = wh_row[\"account_id\"]\n if not warehouse_account_id:\n stock_acct_t = Table(\"account\")\n stock_acct_q = (Q.from_(stock_acct_t).select(stock_acct_t.id)\n .where(stock_acct_t.account_type == \"stock\")\n .where(stock_acct_t.company_id == P())\n .where(stock_acct_t.is_group == 0)\n .limit(1))\n stock_acct = conn.execute(stock_acct_q.get_sql(), (company_id,)).fetchone()\n warehouse_account_id = stock_acct[\"id\"] if stock_acct else None\n\n # Stock Adjustment account (contra)\n adj_acct_t = Table(\"account\")\n adj_acct_q = (Q.from_(adj_acct_t).select(adj_acct_t.id)\n .where(adj_acct_t.account_type == \"stock_adjustment\")\n .where(adj_acct_t.company_id == P())\n .where(adj_acct_t.is_group == 0)\n .limit(1))\n stock_adj_acct = conn.execute(adj_acct_q.get_sql(), (company_id,)).fetchone()\n stock_adj_account_id = stock_adj_acct[\"id\"] if stock_adj_acct else None\n\n if warehouse_account_id and stock_adj_account_id:\n abs_adj = abs(adjustment)\n gl_entries = []\n if adjustment > 0:\n # Rate increased: DR Stock-in-Hand, CR Stock Adjustment\n gl_entries.append({\n \"account_id\": warehouse_account_id,\n \"debit\": str(round_currency(abs_adj)),\n \"credit\": \"0\",\n })\n gl_entries.append({\n \"account_id\": stock_adj_account_id,\n \"debit\": \"0\",\n \"credit\": str(round_currency(abs_adj)),\n \"cost_center_id\": cost_center_id,\n })\n else:\n # Rate decreased: DR Stock Adjustment, CR Stock-in-Hand\n gl_entries.append({\n \"account_id\": stock_adj_account_id,\n \"debit\": str(round_currency(abs_adj)),\n \"credit\": \"0\",\n \"cost_center_id\": cost_center_id,\n })\n gl_entries.append({\n \"account_id\": warehouse_account_id,\n \"debit\": \"0\",\n \"credit\": str(round_currency(abs_adj)),\n })\n\n for gle in gl_entries:\n gle[\"fiscal_year\"] = fiscal_year\n\n try:\n gl_ids = insert_gl_entries(\n conn, gl_entries,\n voucher_type=\"stock_revaluation\",\n voucher_id=reval_id,\n posting_date=posting_date,\n company_id=company_id,\n remarks=f\"Stock Revaluation {naming}: \"\n f\"{item_row['item_name']} rate {old_rate_d} → {new_rate_d}\",\n )\n except ValueError as e:\n sys.stderr.write(f\"[erpclaw-inventory] GL posting failed: {e}\\n\")\n err(f\"GL posting failed: {e}\")\n\n # 3. Insert stock_revaluation record\n # Uses datetime('now') for created_at and updated_at — kept as raw SQL\n conn.execute(\n \"\"\"INSERT INTO stock_revaluation (\n id, naming_series, company_id, item_id, warehouse_id,\n posting_date, current_qty, old_rate, new_rate,\n adjustment_amount, reason, status, created_at, updated_at\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'submitted',\n datetime('now'), datetime('now'))\"\"\",\n (\n reval_id, naming, company_id, item_id, warehouse_id,\n posting_date,\n str(round_currency(current_qty)),\n str(round_currency(old_rate_d)),\n str(round_currency(new_rate_d)),\n str(adjustment),\n reason,\n ),\n )\n\n audit(conn, \"erpclaw-inventory\", \"revalue-stock\", \"stock_revaluation\",\n reval_id, new_values={\n \"item_id\": item_id, \"warehouse_id\": warehouse_id,\n \"old_rate\": str(old_rate_d), \"new_rate\": str(new_rate_d),\n \"adjustment\": str(adjustment), \"gl_count\": len(gl_ids),\n })\n conn.commit()\n\n ok({\n \"revaluation_id\": reval_id,\n \"naming_series\": naming,\n \"item_id\": item_id,\n \"item_name\": item_row[\"item_name\"],\n \"warehouse\": wh_row[\"name\"],\n \"current_qty\": str(round_currency(current_qty)),\n \"old_rate\": str(round_currency(old_rate_d)),\n \"new_rate\": str(round_currency(new_rate_d)),\n \"adjustment_amount\": str(adjustment),\n \"gl_entries_created\": len(gl_ids),\n })\n\n\n# ---------------------------------------------------------------------------\n# 31. list-stock-revaluations\n# ---------------------------------------------------------------------------\n\ndef list_stock_revaluations(conn, args):\n \"\"\"List stock revaluations for a company.\"\"\"\n company_id = resolve_company_id(conn, getattr(args, 'company_id', None))\n\n limit = int(args.limit or \"20\")\n offset = int(args.offset or \"0\")\n\n sr = Table(\"stock_revaluation\").as_(\"sr\")\n i = Table(\"item\").as_(\"i\")\n w = Table(\"warehouse\").as_(\"w\")\n\n rows_q = (Q.from_(sr)\n .join(i).on(i.id == sr.item_id)\n .join(w).on(w.id == sr.warehouse_id)\n .select(sr.star, i.item_code, i.item_name, w.name.as_(\"warehouse_name\"))\n .where(sr.company_id == P())\n .orderby(sr.created_at, order=Order.desc)\n .limit(P()).offset(P()))\n\n rows = conn.execute(rows_q.get_sql(), (company_id, limit, offset)).fetchall()\n\n total_t = Table(\"stock_revaluation\")\n total_q = (Q.from_(total_t)\n .select(fn.Count(\"*\").as_(\"cnt\"))\n .where(total_t.company_id == P()))\n total = conn.execute(total_q.get_sql(), (company_id,)).fetchone()[\"cnt\"]\n\n ok({\n \"revaluations\": [row_to_dict(r) for r in rows],\n \"total\": total,\n \"limit\": limit,\n \"offset\": offset,\n })\n\n\n# ---------------------------------------------------------------------------\n# 32. get-stock-revaluation\n# ---------------------------------------------------------------------------\n\ndef get_stock_revaluation(conn, args):\n \"\"\"Get details of a stock revaluation.\"\"\"\n reval_id = args.revaluation_id\n if not reval_id:\n err(\"--revaluation-id is required\")\n\n sr = Table(\"stock_revaluation\").as_(\"sr\")\n i = Table(\"item\").as_(\"i\")\n w = Table(\"warehouse\").as_(\"w\")\n\n row_q = (Q.from_(sr)\n .join(i).on(i.id == sr.item_id)\n .join(w).on(w.id == sr.warehouse_id)\n .select(sr.star, i.item_code, i.item_name, w.name.as_(\"warehouse_name\"))\n .where(sr.id == P()))\n row = conn.execute(row_q.get_sql(), (reval_id,)).fetchone()\n if not row:\n err(f\"Stock revaluation {reval_id} not found\")\n\n result = row_to_dict(row)\n\n # Include SLE entries\n sle_t = Table(\"stock_ledger_entry\")\n sle_q = (Q.from_(sle_t).select(sle_t.star)\n .where(sle_t.voucher_type == \"stock_revaluation\")\n .where(sle_t.voucher_id == P()))\n sle_rows = conn.execute(sle_q.get_sql(), (reval_id,)).fetchall()\n result[\"sle_entries\"] = [row_to_dict(r) for r in sle_rows]\n\n # Include GL entries\n gl_t = Table(\"gl_entry\")\n gl_q = (Q.from_(gl_t).select(gl_t.star)\n .where(gl_t.voucher_type == \"stock_revaluation\")\n .where(gl_t.voucher_id == P()))\n gl_rows = conn.execute(gl_q.get_sql(), (reval_id,)).fetchall()\n result[\"gl_entries\"] = [row_to_dict(r) for r in gl_rows]\n\n ok(result)\n\n\n# ---------------------------------------------------------------------------\n# 33. cancel-stock-revaluation\n# ---------------------------------------------------------------------------\n\ndef cancel_stock_revaluation(conn, args):\n \"\"\"Cancel a stock revaluation: reverse SLE and GL entries.\"\"\"\n reval_id = args.revaluation_id\n if not reval_id:\n err(\"--revaluation-id is required\")\n\n sr_t = Table(\"stock_revaluation\")\n sr_q = Q.from_(sr_t).select(sr_t.star).where(sr_t.id == P())\n row = conn.execute(sr_q.get_sql(), (reval_id,)).fetchone()\n if not row:\n err(f\"Stock revaluation {reval_id} not found\")\n if row[\"status\"] != \"submitted\":\n err(f\"Cannot cancel: revaluation is '{row['status']}' (must be 'submitted')\")\n\n reval = row_to_dict(row)\n posting_date = reval[\"posting_date\"]\n\n # Reverse SLE entries\n try:\n reverse_sle_entries(\n conn,\n voucher_type=\"stock_revaluation\",\n voucher_id=reval_id,\n posting_date=posting_date,\n )\n except ValueError as e:\n sys.stderr.write(f\"[erpclaw-inventory] SLE reversal failed: {e}\\n\")\n err(f\"SLE reversal failed: {e}\")\n\n # Reverse GL entries\n try:\n reverse_gl_entries(\n conn,\n voucher_type=\"stock_revaluation\",\n voucher_id=reval_id,\n posting_date=posting_date,\n )\n except ValueError as e:\n sys.stderr.write(f\"[erpclaw-inventory] GL reversal failed: {e}\\n\")\n err(f\"GL reversal failed: {e}\")\n\n # Update status\n conn.execute(\n \"UPDATE stock_revaluation SET status = 'cancelled', updated_at = datetime('now') WHERE id = ?\",\n (reval_id,),\n )\n\n audit(conn, \"erpclaw-inventory\", \"cancel-stock-revaluation\", \"stock_revaluation\",\n reval_id, new_values={\"status\": \"cancelled\"})\n conn.commit()\n\n ok({\n \"revaluation_id\": reval_id,\n \"cancelled\": True,\n \"item_id\": reval[\"item_id\"],\n \"warehouse_id\": reval[\"warehouse_id\"],\n })\n\n\n# ---------------------------------------------------------------------------\n# 34. status\n# ---------------------------------------------------------------------------\n\ndef status_action(conn, args):\n \"\"\"Inventory summary for a company.\"\"\"\n company_id = resolve_company_id(conn, getattr(args, 'company_id', None))\n\n item_t = Table(\"item\")\n items_q = Q.from_(item_t).select(fn.Count(\"*\").as_(\"cnt\"))\n items_count = conn.execute(items_q.get_sql()).fetchone()[\"cnt\"]\n\n wh_t = Table(\"warehouse\")\n wh_q = (Q.from_(wh_t).select(fn.Count(\"*\").as_(\"cnt\"))\n .where(wh_t.company_id == P()))\n warehouses_count = conn.execute(wh_q.get_sql(), (company_id,)).fetchone()[\"cnt\"]\n\n # Stock entries by status\n se_t = Table(\"stock_entry\")\n se_q = (Q.from_(se_t)\n .select(se_t.status, fn.Count(\"*\").as_(\"cnt\"))\n .where(se_t.company_id == P())\n .groupby(se_t.status))\n se_rows = conn.execute(se_q.get_sql(), (company_id,)).fetchall()\n se_counts = {\"draft\": 0, \"submitted\": 0, \"cancelled\": 0}\n for row in se_rows:\n se_counts[row[\"status\"]] = row[\"cnt\"]\n se_counts[\"total\"] = sum(se_counts.values())\n\n # Total stock value using decimal_sum() aggregate\n # JOIN with warehouse for company filter — kept as raw SQL for aggregate\n sv_row = conn.execute(\n \"\"\"SELECT COALESCE(decimal_sum(sle.stock_value_difference), '0') as total_value\n FROM stock_ledger_entry sle\n JOIN warehouse w ON w.id = sle.warehouse_id\n WHERE sle.is_cancelled = 0 AND w.company_id = ?\"\"\",\n (company_id,),\n ).fetchone()\n total_stock_value = round_currency(to_decimal(str(sv_row[\"total_value\"])))\n\n ok({\n \"items\": items_count,\n \"warehouses\": warehouses_count,\n \"stock_entries\": se_counts,\n \"total_stock_value\": str(total_stock_value),\n })\n\n\n# ---------------------------------------------------------------------------\n# 35. check-reorder\n# ---------------------------------------------------------------------------\n\ndef check_reorder(conn, args):\n \"\"\"Find items whose current stock is at or below their reorder level.\"\"\"\n company_id = resolve_company_id(conn, getattr(args, 'company_id', None))\n\n # Get items with a meaningful reorder_level set\n # Uses IS NULL / IS NOT NULL comparisons — kept as raw SQL (rule 16)\n items = conn.execute(\n \"\"\"SELECT id, item_code, item_name, reorder_level, reorder_qty\n FROM item\n WHERE reorder_level IS NOT NULL\n AND reorder_level != ''\n AND reorder_level != '0'\n AND status = 'active'\n ORDER BY item_name\"\"\",\n ).fetchall()\n\n results = []\n for item in items:\n item_id = item[\"id\"]\n reorder_level = to_decimal(str(item[\"reorder_level\"]))\n reorder_qty = to_decimal(str(item[\"reorder_qty\"])) if item[\"reorder_qty\"] else Decimal(\"0\")\n\n # Calculate current stock across all warehouses for this company\n # Uses decimal_sum() aggregate with JOIN — kept as raw SQL\n stock_row = conn.execute(\n \"\"\"SELECT COALESCE(decimal_sum(sle.actual_qty), '0') AS total_qty\n FROM stock_ledger_entry sle\n JOIN warehouse w ON w.id = sle.warehouse_id\n WHERE sle.item_id = ?\n AND sle.is_cancelled = 0\n AND w.company_id = ?\"\"\",\n (item_id, company_id),\n ).fetchone()\n\n current_stock = to_decimal(str(stock_row[\"total_qty\"]))\n\n if current_stock \u003c= reorder_level:\n shortfall = round_currency(reorder_level - current_stock)\n results.append({\n \"item_id\": item_id,\n \"item_code\": item[\"item_code\"],\n \"item_name\": item[\"item_name\"],\n \"current_stock\": str(round_currency(current_stock)),\n \"reorder_level\": str(round_currency(reorder_level)),\n \"reorder_qty\": str(round_currency(reorder_qty)),\n \"shortfall\": str(shortfall),\n })\n\n ok({\n \"items_below_reorder\": len(results),\n \"items\": results,\n })\n\n\n# ---------------------------------------------------------------------------\n# import-items\n# ---------------------------------------------------------------------------\n\ndef import_items(conn, args):\n \"\"\"Bulk import items from a CSV file.\n\n CSV columns: item_code, name, uom, group (optional),\n valuation_method (optional), description (optional).\n\n Items are globally unique by item_code (no company_id on item table).\n \"\"\"\n csv_path = args.csv_path\n if not csv_path:\n err(\"--csv-path is required\")\n\n # Path safety: resolve symlinks, require .csv extension, must be a regular file\n csv_real = os.path.realpath(csv_path)\n if not csv_real.lower().endswith(\".csv\"):\n err(\"--csv-path must point to a .csv file\")\n if not os.path.isfile(csv_real):\n err(f\"File not found: {csv_path}\")\n\n from erpclaw_lib.csv_import import validate_csv, parse_csv_rows\n from erpclaw_lib.args import SafeArgumentParser, check_unknown_args\n\n errors = validate_csv(csv_real, \"item\")\n if errors:\n err(f\"CSV validation failed: {'; '.join(errors)}\")\n\n rows = parse_csv_rows(csv_real, \"item\")\n if not rows:\n err(\"CSV file is empty\")\n\n imported = 0\n skipped = 0\n for row in rows:\n item_code = row.get(\"item_code\", \"\")\n name = row.get(\"name\", \"\")\n uom = row.get(\"uom\", \"Nos\")\n\n # Check for duplicate (item_code is globally unique)\n item_t = Table(\"item\")\n dup_q = Q.from_(item_t).select(item_t.id).where(item_t.item_code == P())\n existing = conn.execute(dup_q.get_sql(), (item_code,)).fetchone()\n if existing:\n skipped += 1\n continue\n\n # Look up or default item group\n group_name = row.get(\"group\")\n group_id = None\n if group_name:\n ig_t = Table(\"item_group\")\n grp_q = Q.from_(ig_t).select(ig_t.id).where(ig_t.name == P())\n grp = conn.execute(grp_q.get_sql(), (group_name,)).fetchone()\n if grp:\n group_id = grp[\"id\"]\n\n item_id = str(uuid.uuid4())\n ins_t = Table(\"item\")\n ins_q = Q.into(ins_t).columns(\n \"id\", \"item_code\", \"item_name\", \"item_group_id\",\n \"stock_uom\", \"valuation_method\", \"description\", \"status\",\n ).insert(P(), P(), P(), P(), P(), P(), P(), \"active\")\n conn.execute(\n ins_q.get_sql(),\n (item_id, item_code, name, group_id, uom,\n row.get(\"valuation_method\", \"moving_average\").lower(),\n row.get(\"description\")),\n )\n imported += 1\n\n conn.commit()\n ok({\"imported\": imported, \"skipped\": skipped, \"total_rows\": len(rows)})\n\n\n# ---------------------------------------------------------------------------\n# Feature #4: get-projected-qty\n# ---------------------------------------------------------------------------\n\ndef get_projected_qty(conn, args):\n \"\"\"Calculate projected quantity for an item in a warehouse.\n\n projected_qty = actual_qty + ordered_qty (pending receipt) - reserved_qty (pending delivery)\n\n ordered_qty = SUM(po_item.quantity - po_item.received_qty) for open POs\n reserved_qty = SUM(so_item.quantity - so_item.delivered_qty) for confirmed SOs\n \"\"\"\n if not args.item_id:\n err(\"--item-id is required\")\n if not args.warehouse_id:\n err(\"--warehouse-id is required\")\n\n # Verify item exists\n item_t = Table(\"item\")\n q = Q.from_(item_t).select(item_t.id, item_t.item_code, item_t.item_name).where(item_t.id == P())\n item = conn.execute(q.get_sql(), (args.item_id,)).fetchone()\n if not item:\n err(f\"Item {args.item_id} not found\")\n\n # Verify warehouse exists\n wh_t = Table(\"warehouse\")\n q = Q.from_(wh_t).select(wh_t.id).where(wh_t.id == P())\n wh = conn.execute(q.get_sql(), (args.warehouse_id,)).fetchone()\n if not wh:\n err(f\"Warehouse {args.warehouse_id} not found\")\n\n # 1. Actual qty from SLE\n balance = get_stock_balance(conn, args.item_id, args.warehouse_id)\n actual_qty = to_decimal(balance[\"qty\"])\n\n # 2. Ordered qty: open PO items not yet fully received\n # PO statuses that indicate pending receipt: confirmed, partially_received\n po_rows = conn.execute(\n \"\"\"SELECT poi.quantity, poi.received_qty\n FROM purchase_order_item poi\n JOIN purchase_order po ON po.id = poi.purchase_order_id\n WHERE poi.item_id = ?\n AND (poi.warehouse_id = ? OR poi.warehouse_id IS NULL)\n AND po.status IN ('confirmed', 'partially_received')\"\"\",\n (args.item_id, args.warehouse_id),\n ).fetchall()\n ordered_qty = round_currency(sum(\n (max(to_decimal(r[\"quantity\"]) - to_decimal(r[\"received_qty\"]), Decimal(\"0\")) for r in po_rows),\n Decimal(\"0\"),\n ))\n\n # 3. Reserved qty: confirmed SO items not yet fully delivered\n # SO statuses that indicate pending delivery: confirmed, partially_delivered\n so_rows = conn.execute(\n \"\"\"SELECT soi.quantity, soi.delivered_qty\n FROM sales_order_item soi\n JOIN sales_order so_ ON so_.id = soi.sales_order_id\n WHERE soi.item_id = ?\n AND (soi.warehouse_id = ? OR soi.warehouse_id IS NULL)\n AND so_.status IN ('confirmed', 'partially_delivered')\"\"\",\n (args.item_id, args.warehouse_id),\n ).fetchall()\n reserved_qty = round_currency(sum(\n (max(to_decimal(r[\"quantity\"]) - to_decimal(r[\"delivered_qty\"]), Decimal(\"0\")) for r in so_rows),\n Decimal(\"0\"),\n ))\n\n projected_qty = round_currency(actual_qty + ordered_qty - reserved_qty)\n\n ok({\n \"item_id\": args.item_id,\n \"item_code\": item[\"item_code\"],\n \"item_name\": item[\"item_name\"],\n \"warehouse_id\": args.warehouse_id,\n \"actual_qty\": str(round_currency(actual_qty)),\n \"ordered_qty\": str(ordered_qty),\n \"reserved_qty\": str(reserved_qty),\n \"projected_qty\": str(projected_qty),\n })\n\n\n# ---------------------------------------------------------------------------\n# Feature #5: Item Variants\n# ---------------------------------------------------------------------------\n\ndef add_item_attribute(conn, args):\n \"\"\"Add an attribute definition to a template item.\n\n Marks the item as has_variants=1 (template).\n --attribute-values is a JSON array, e.g. '[\"Red\",\"Blue\",\"Green\"]'\n \"\"\"\n if not args.item_id:\n err(\"--item-id is required\")\n if not args.attribute_name:\n err(\"--attribute-name is required\")\n if not args.attribute_values:\n err(\"--attribute-values is required (JSON array)\")\n\n # Validate item exists\n item_t = Table(\"item\")\n q = Q.from_(item_t).select(item_t.id, item_t.variant_of).where(item_t.id == P())\n item = conn.execute(q.get_sql(), (args.item_id,)).fetchone()\n if not item:\n err(f\"Item {args.item_id} not found\")\n if item[\"variant_of\"]:\n err(\"Cannot add attributes to a variant item — add to the template instead\")\n\n # Parse values\n values = _parse_json_arg(args.attribute_values, \"attribute-values\")\n if not isinstance(values, list) or len(values) == 0:\n err(\"--attribute-values must be a non-empty JSON array\")\n\n # Check for duplicate attribute name on this item\n attr_t = Table(\"item_attribute\")\n q = (Q.from_(attr_t).select(attr_t.id)\n .where(attr_t.item_id == P())\n .where(attr_t.attribute_name == P()))\n existing = conn.execute(q.get_sql(), (args.item_id, args.attribute_name)).fetchone()\n if existing:\n err(f\"Attribute '{args.attribute_name}' already exists for this item\")\n\n attr_id = str(uuid.uuid4())\n q = Q.into(attr_t).columns(\n \"id\", \"item_id\", \"attribute_name\", \"attribute_values\",\n ).insert(P(), P(), P(), P())\n conn.execute(q.get_sql(), (attr_id, args.item_id, args.attribute_name, json.dumps(values)))\n\n # Mark item as template\n q = (Q.update(item_t)\n .set(item_t.has_variants, 1)\n .set(item_t.updated_at, _NOW)\n .where(item_t.id == P()))\n conn.execute(q.get_sql(), (args.item_id,))\n\n audit(conn, \"erpclaw-inventory\", \"add-item-attribute\", \"item_attribute\", attr_id,\n new_values={\"item_id\": args.item_id, \"attribute_name\": args.attribute_name})\n conn.commit()\n ok({\"attribute_id\": attr_id, \"item_id\": args.item_id,\n \"attribute_name\": args.attribute_name, \"values\": values})\n\n\ndef create_item_variant(conn, args):\n \"\"\"Create a single item variant from a template item with specific attribute values.\n\n --attributes is a JSON object, e.g. '{\"Color\": \"Red\", \"Size\": \"Large\"}'\n \"\"\"\n if not args.template_item_id:\n err(\"--template-item-id is required\")\n if not args.attributes:\n err(\"--attributes is required (JSON object)\")\n\n # Validate template item\n item_t = Table(\"item\")\n q = Q.from_(item_t).select(item_t.star).where(item_t.id == P())\n template = conn.execute(q.get_sql(), (args.template_item_id,)).fetchone()\n if not template:\n err(f\"Template item {args.template_item_id} not found\")\n if not template[\"has_variants\"]:\n err(\"Item is not a template (has_variants must be 1)\")\n\n attributes = _parse_json_arg(args.attributes, \"attributes\")\n if not isinstance(attributes, dict) or len(attributes) == 0:\n err(\"--attributes must be a non-empty JSON object\")\n\n # Validate attribute values against template's attribute definitions\n attr_t = Table(\"item_attribute\")\n q = Q.from_(attr_t).select(attr_t.attribute_name, attr_t.attribute_values).where(attr_t.item_id == P())\n template_attrs = conn.execute(q.get_sql(), (args.template_item_id,)).fetchall()\n template_attr_map = {}\n for a in template_attrs:\n template_attr_map[a[\"attribute_name\"]] = json.loads(a[\"attribute_values\"]) if a[\"attribute_values\"] else []\n\n for attr_name, attr_val in attributes.items():\n if attr_name not in template_attr_map:\n err(f\"Attribute '{attr_name}' is not defined on the template item\")\n if attr_val not in template_attr_map[attr_name]:\n err(f\"Value '{attr_val}' is not valid for attribute '{attr_name}'. \"\n f\"Valid: {template_attr_map[attr_name]}\")\n\n # Build variant code: template_code-Val1-Val2\n suffix = \"-\".join(str(v) for v in attributes.values())\n variant_code = f\"{template['item_code']}-{suffix}\"\n variant_name = f\"{template['item_name']} ({suffix})\"\n\n # Check for duplicate variant\n dup_q = Q.from_(item_t).select(item_t.id).where(item_t.item_code == P())\n existing = conn.execute(dup_q.get_sql(), (variant_code,)).fetchone()\n if existing:\n err(f\"Variant '{variant_code}' already exists\")\n\n variant_id = str(uuid.uuid4())\n q = Q.into(item_t).columns(\n \"id\", \"item_code\", \"item_name\", \"item_group_id\", \"item_type\", \"stock_uom\",\n \"valuation_method\", \"is_stock_item\", \"is_purchase_item\", \"is_sales_item\",\n \"has_batch\", \"has_serial\", \"standard_rate\", \"variant_of\", \"status\",\n ).insert(P(), P(), P(), P(), P(), P(), P(), P(), P(), P(), P(), P(), P(), P(), \"active\")\n try:\n conn.execute(q.get_sql(), (\n variant_id, variant_code, variant_name,\n template[\"item_group_id\"], template[\"item_type\"],\n template[\"stock_uom\"], template[\"valuation_method\"],\n template[\"is_stock_item\"], template[\"is_purchase_item\"],\n template[\"is_sales_item\"], template[\"has_batch\"],\n template[\"has_serial\"], template[\"standard_rate\"],\n args.template_item_id,\n ))\n except sqlite3.IntegrityError as e:\n sys.stderr.write(f\"[erpclaw-inventory] {e}\\n\")\n err(\"Variant creation failed — check for duplicates\")\n\n # Store variant's attribute values as item_attribute rows\n for attr_name, attr_val in attributes.items():\n va_id = str(uuid.uuid4())\n q = Q.into(attr_t).columns(\n \"id\", \"item_id\", \"attribute_name\", \"attribute_values\",\n ).insert(P(), P(), P(), P())\n conn.execute(q.get_sql(), (va_id, variant_id, attr_name, json.dumps(attr_val)))\n\n audit(conn, \"erpclaw-inventory\", \"create-item-variant\", \"item\", variant_id,\n new_values={\"variant_of\": args.template_item_id, \"attributes\": attributes})\n conn.commit()\n ok({\"variant_id\": variant_id, \"item_code\": variant_code,\n \"item_name\": variant_name, \"template_item_id\": args.template_item_id,\n \"attributes\": attributes})\n\n\ndef generate_item_variants(conn, args):\n \"\"\"Generate all possible variants from a template item's attributes (cartesian product).\"\"\"\n if not args.template_item_id:\n err(\"--template-item-id is required\")\n\n # Validate template\n item_t = Table(\"item\")\n q = Q.from_(item_t).select(item_t.star).where(item_t.id == P())\n template = conn.execute(q.get_sql(), (args.template_item_id,)).fetchone()\n if not template:\n err(f\"Template item {args.template_item_id} not found\")\n if not template[\"has_variants\"]:\n err(\"Item is not a template (has_variants must be 1)\")\n\n # Get all attributes\n attr_t = Table(\"item_attribute\")\n q = (Q.from_(attr_t).select(attr_t.attribute_name, attr_t.attribute_values)\n .where(attr_t.item_id == P())\n .orderby(attr_t.attribute_name))\n attrs = conn.execute(q.get_sql(), (args.template_item_id,)).fetchall()\n if not attrs:\n err(\"Template has no attributes defined — use add-item-attribute first\")\n\n attr_names = []\n attr_value_lists = []\n for a in attrs:\n attr_names.append(a[\"attribute_name\"])\n values = json.loads(a[\"attribute_values\"]) if a[\"attribute_values\"] else []\n if not values:\n err(f\"Attribute '{a['attribute_name']}' has no values\")\n attr_value_lists.append(values)\n\n # Cartesian product\n combinations = list(itertools.product(*attr_value_lists))\n\n created = []\n skipped = []\n for combo in combinations:\n attributes = dict(zip(attr_names, combo))\n suffix = \"-\".join(str(v) for v in combo)\n variant_code = f\"{template['item_code']}-{suffix}\"\n variant_name = f\"{template['item_name']} ({suffix})\"\n\n # Skip if already exists\n dup_q = Q.from_(item_t).select(item_t.id).where(item_t.item_code == P())\n existing = conn.execute(dup_q.get_sql(), (variant_code,)).fetchone()\n if existing:\n skipped.append(variant_code)\n continue\n\n variant_id = str(uuid.uuid4())\n q = Q.into(item_t).columns(\n \"id\", \"item_code\", \"item_name\", \"item_group_id\", \"item_type\", \"stock_uom\",\n \"valuation_method\", \"is_stock_item\", \"is_purchase_item\", \"is_sales_item\",\n \"has_batch\", \"has_serial\", \"standard_rate\", \"variant_of\", \"status\",\n ).insert(P(), P(), P(), P(), P(), P(), P(), P(), P(), P(), P(), P(), P(), P(), \"active\")\n conn.execute(q.get_sql(), (\n variant_id, variant_code, variant_name,\n template[\"item_group_id\"], template[\"item_type\"],\n template[\"stock_uom\"], template[\"valuation_method\"],\n template[\"is_stock_item\"], template[\"is_purchase_item\"],\n template[\"is_sales_item\"], template[\"has_batch\"],\n template[\"has_serial\"], template[\"standard_rate\"],\n args.template_item_id,\n ))\n\n # Store variant attribute values\n for attr_name, attr_val in attributes.items():\n va_id = str(uuid.uuid4())\n q = Q.into(attr_t).columns(\n \"id\", \"item_id\", \"attribute_name\", \"attribute_values\",\n ).insert(P(), P(), P(), P())\n conn.execute(q.get_sql(), (va_id, variant_id, attr_name, json.dumps(attr_val)))\n\n created.append({\"variant_id\": variant_id, \"item_code\": variant_code, \"attributes\": attributes})\n\n conn.commit()\n ok({\n \"template_item_id\": args.template_item_id,\n \"created\": len(created),\n \"skipped\": len(skipped),\n \"skipped_codes\": skipped,\n \"variants\": created,\n })\n\n\ndef list_item_variants(conn, args):\n \"\"\"List all variants of a template item.\"\"\"\n if not args.template_item_id:\n err(\"--template-item-id is required\")\n\n # Verify template exists\n item_t = Table(\"item\")\n q = Q.from_(item_t).select(item_t.id, item_t.has_variants).where(item_t.id == P())\n template = conn.execute(q.get_sql(), (args.template_item_id,)).fetchone()\n if not template:\n err(f\"Template item {args.template_item_id} not found\")\n\n # Get all variants\n q = (Q.from_(item_t)\n .select(item_t.id, item_t.item_code, item_t.item_name, item_t.status, item_t.standard_rate)\n .where(item_t.variant_of == P())\n .orderby(item_t.item_code))\n variants = conn.execute(q.get_sql(), (args.template_item_id,)).fetchall()\n\n # For each variant, get its attributes\n attr_t = Table(\"item_attribute\")\n results = []\n for v in variants:\n q = (Q.from_(attr_t)\n .select(attr_t.attribute_name, attr_t.attribute_values)\n .where(attr_t.item_id == P()))\n attrs = conn.execute(q.get_sql(), (v[\"id\"],)).fetchall()\n attr_dict = {}\n for a in attrs:\n val = json.loads(a[\"attribute_values\"]) if a[\"attribute_values\"] else None\n attr_dict[a[\"attribute_name\"]] = val\n results.append({\n \"variant_id\": v[\"id\"],\n \"item_code\": v[\"item_code\"],\n \"item_name\": v[\"item_name\"],\n \"status\": v[\"status\"],\n \"standard_rate\": v[\"standard_rate\"],\n \"attributes\": attr_dict,\n })\n\n ok({\n \"template_item_id\": args.template_item_id,\n \"count\": len(results),\n \"variants\": results,\n })\n\n\n# ---------------------------------------------------------------------------\n# Feature #6: Item Supplier (Min Order Qty)\n# ---------------------------------------------------------------------------\n\ndef add_item_supplier(conn, args):\n \"\"\"Link an item to a supplier with min order qty and lead time.\"\"\"\n if not args.item_id:\n err(\"--item-id is required\")\n if not args.supplier_id:\n err(\"--supplier-id is required\")\n\n # Validate item\n item_t = Table(\"item\")\n q = Q.from_(item_t).select(item_t.id).where(item_t.id == P())\n item = conn.execute(q.get_sql(), (args.item_id,)).fetchone()\n if not item:\n err(f\"Item {args.item_id} not found\")\n\n # Validate supplier\n sup_t = Table(\"supplier\")\n q = Q.from_(sup_t).select(sup_t.id).where(sup_t.id == P())\n sup = conn.execute(q.get_sql(), (args.supplier_id,)).fetchone()\n if not sup:\n err(f\"Supplier {args.supplier_id} not found\")\n\n min_order_qty = str(round_currency(to_decimal(args.min_order_qty or \"0\")))\n lead_time = int(args.lead_time_days) if args.lead_time_days else None\n priority = int(args.priority) if args.priority is not None else 0\n\n is_t = Table(\"item_supplier\")\n is_id = str(uuid.uuid4())\n q = Q.into(is_t).columns(\n \"id\", \"item_id\", \"supplier_id\", \"min_order_qty\", \"lead_time_days\", \"priority\",\n ).insert(P(), P(), P(), P(), P(), P())\n try:\n conn.execute(q.get_sql(), (is_id, args.item_id, args.supplier_id, min_order_qty, lead_time, priority))\n except sqlite3.IntegrityError:\n err(\"This item-supplier link already exists\")\n\n audit(conn, \"erpclaw-inventory\", \"add-item-supplier\", \"item_supplier\", is_id,\n new_values={\"item_id\": args.item_id, \"supplier_id\": args.supplier_id,\n \"min_order_qty\": min_order_qty})\n conn.commit()\n ok({\"item_supplier_id\": is_id, \"item_id\": args.item_id,\n \"supplier_id\": args.supplier_id, \"min_order_qty\": min_order_qty,\n \"lead_time_days\": lead_time, \"priority\": priority})\n\n\ndef list_item_suppliers(conn, args):\n \"\"\"List all suppliers for an item, or all items for a supplier.\"\"\"\n is_t = Table(\"item_supplier\")\n item_t = Table(\"item\")\n sup_t = Table(\"supplier\")\n\n q = (Q.from_(is_t)\n .left_join(item_t).on(item_t.id == is_t.item_id)\n .left_join(sup_t).on(sup_t.id == is_t.supplier_id)\n .select(\n is_t.id, is_t.item_id, is_t.supplier_id,\n is_t.min_order_qty, is_t.lead_time_days, is_t.priority,\n item_t.item_code, item_t.item_name,\n sup_t.name.as_(\"supplier_name\"),\n )\n .orderby(is_t.priority))\n\n params = []\n if args.item_id:\n q = q.where(is_t.item_id == P())\n params.append(args.item_id)\n if args.supplier_id:\n q = q.where(is_t.supplier_id == P())\n params.append(args.supplier_id)\n\n if not args.item_id and not args.supplier_id:\n err(\"At least one of --item-id or --supplier-id is required\")\n\n rows = conn.execute(q.get_sql(), params).fetchall()\n results = [row_to_dict(r) for r in rows]\n ok({\"count\": len(results), \"item_suppliers\": results})\n\n\n# ---------------------------------------------------------------------------\n# Action dispatch\n# ---------------------------------------------------------------------------\n\nACTIONS = {\n \"add-item\": add_item,\n \"update-item\": update_item,\n \"get-item\": get_item,\n \"list-items\": list_items,\n \"add-item-group\": add_item_group,\n \"list-item-groups\": list_item_groups,\n \"add-warehouse\": add_warehouse,\n \"update-warehouse\": update_warehouse,\n \"list-warehouses\": list_warehouses,\n \"add-stock-entry\": add_stock_entry,\n \"get-stock-entry\": get_stock_entry,\n \"list-stock-entries\": list_stock_entries,\n \"submit-stock-entry\": submit_stock_entry,\n \"cancel-stock-entry\": cancel_stock_entry,\n \"create-stock-ledger-entries\": create_stock_ledger_entries,\n \"reverse-stock-ledger-entries\": reverse_stock_ledger_entries,\n \"get-stock-balance\": get_stock_balance_action,\n \"stock-balance\": stock_balance_report, # alias — \"stock balance\" routes to company-wide report\n \"stock-balance-report\": stock_balance_report,\n \"stock-ledger-report\": stock_ledger_report,\n \"add-batch\": add_batch,\n \"list-batches\": list_batches,\n \"add-serial-number\": add_serial_number,\n \"list-serial-numbers\": list_serial_numbers,\n \"add-price-list\": add_price_list,\n \"add-item-price\": add_item_price,\n \"get-item-price\": get_item_price,\n \"add-pricing-rule\": add_pricing_rule,\n \"add-stock-reconciliation\": add_stock_reconciliation,\n \"submit-stock-reconciliation\": submit_stock_reconciliation,\n \"revalue-stock\": revalue_stock,\n \"list-stock-revaluations\": list_stock_revaluations,\n \"get-stock-revaluation\": get_stock_revaluation,\n \"cancel-stock-revaluation\": cancel_stock_revaluation,\n \"check-reorder\": check_reorder,\n \"import-items\": import_items,\n \"get-projected-qty\": get_projected_qty,\n \"add-item-attribute\": add_item_attribute,\n \"create-item-variant\": create_item_variant,\n \"generate-item-variants\": generate_item_variants,\n \"list-item-variants\": list_item_variants,\n \"add-item-supplier\": add_item_supplier,\n \"list-item-suppliers\": list_item_suppliers,\n \"status\": status_action,\n}\n\n\ndef main():\n parser = SafeArgumentParser(description=\"ERPClaw Inventory Skill\")\n parser.add_argument(\"--action\", required=True, choices=sorted(ACTIONS.keys()))\n parser.add_argument(\"--db-path\", default=None)\n\n # Item fields\n parser.add_argument(\"--item-id\")\n parser.add_argument(\"--item-code\")\n parser.add_argument(\"--item-name\")\n parser.add_argument(\"--item-group\")\n parser.add_argument(\"--item-type\")\n parser.add_argument(\"--stock-uom\")\n parser.add_argument(\"--valuation-method\")\n parser.add_argument(\"--has-batch\")\n parser.add_argument(\"--has-serial\")\n parser.add_argument(\"--standard-rate\")\n parser.add_argument(\"--reorder-level\")\n parser.add_argument(\"--reorder-qty\")\n parser.add_argument(\"--status\", dest=\"item_status\")\n\n # Item group\n parser.add_argument(\"--parent-id\")\n parser.add_argument(\"--name\")\n\n # Warehouse\n parser.add_argument(\"--warehouse-id\")\n parser.add_argument(\"--warehouse-name\", dest=\"name\") # alias for --name\n parser.add_argument(\"--warehouse-type\")\n parser.add_argument(\"--account-id\")\n parser.add_argument(\"--is-group\")\n parser.add_argument(\"--company-id\")\n parser.add_argument(\"--csv-path\")\n\n # Stock entry\n parser.add_argument(\"--stock-entry-id\")\n parser.add_argument(\"--entry-type\")\n parser.add_argument(\"--posting-date\")\n parser.add_argument(\"--items\") # JSON\n\n # Stock entry list filters\n parser.add_argument(\"--status-filter\", dest=\"se_status\")\n\n # Cross-skill SLE\n parser.add_argument(\"--voucher-type\")\n parser.add_argument(\"--voucher-id\")\n parser.add_argument(\"--entries\") # JSON\n\n # Batch\n parser.add_argument(\"--batch-name\")\n parser.add_argument(\"--batch-id\")\n parser.add_argument(\"--expiry-date\")\n parser.add_argument(\"--manufacturing-date\")\n\n # Serial number\n parser.add_argument(\"--serial-no\")\n parser.add_argument(\"--serial-status\", dest=\"sn_status\")\n\n # Pricing\n parser.add_argument(\"--price-list-id\")\n parser.add_argument(\"--rate\")\n parser.add_argument(\"--min-qty\")\n parser.add_argument(\"--max-qty\")\n parser.add_argument(\"--valid-from\")\n parser.add_argument(\"--valid-to\")\n parser.add_argument(\"--qty\")\n parser.add_argument(\"--party-id\")\n parser.add_argument(\"--currency\")\n parser.add_argument(\"--is-buying\")\n parser.add_argument(\"--is-selling\")\n\n # Pricing rule\n parser.add_argument(\"--applies-to\")\n parser.add_argument(\"--entity-id\")\n parser.add_argument(\"--discount-percentage\")\n parser.add_argument(\"--pricing-rule-rate\", dest=\"pr_rate\")\n parser.add_argument(\"--priority\", type=int, default=None)\n\n # Stock reconciliation\n parser.add_argument(\"--stock-reconciliation-id\")\n\n # Stock revaluation\n parser.add_argument(\"--revaluation-id\")\n parser.add_argument(\"--new-rate\")\n parser.add_argument(\"--reason\")\n\n # Item variants\n parser.add_argument(\"--template-item-id\")\n parser.add_argument(\"--attribute-name\")\n parser.add_argument(\"--attribute-values\")\n parser.add_argument(\"--attributes\") # JSON object for create-item-variant\n\n # Item supplier\n parser.add_argument(\"--supplier-id\")\n parser.add_argument(\"--min-order-qty\")\n parser.add_argument(\"--lead-time-days\")\n\n # Search / filters\n parser.add_argument(\"--search\")\n parser.add_argument(\"--from-date\")\n parser.add_argument(\"--to-date\")\n parser.add_argument(\"--limit\", default=\"20\")\n parser.add_argument(\"--offset\", default=\"0\")\n\n args, unknown = parser.parse_known_args()\n check_unknown_args(parser, unknown)\n check_input_lengths(args)\n\n db_path = args.db_path or DEFAULT_DB_PATH\n ensure_db_exists(db_path)\n conn = get_connection(db_path)\n\n # Dependency check\n _dep = check_required_tables(conn, REQUIRED_TABLES)\n if _dep:\n _dep[\"suggestion\"] = \"clawhub install \" + \" \".join(_dep.get(\"missing_skills\", []))\n print(json.dumps(_dep, indent=2))\n conn.close()\n sys.exit(1)\n\n try:\n ACTIONS[args.action](conn, args)\n except Exception as e:\n conn.rollback()\n sys.stderr.write(f\"[erpclaw-inventory] {e}\\n\")\n err(\"An unexpected error occurred\")\n finally:\n conn.close()\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":116182,"content_sha256":"2cf187c8f0752a042f0560be25e93c3d0c2abf30942c9f9a8d7c28c9507acc1c"},{"filename":"scripts/erpclaw-journals/db_query.py","content":"#!/usr/bin/env python3\n\"\"\"ERPClaw Journals Skill — db_query.py\n\nJournal entry CRUD with draft→submit→cancel lifecycle.\nOn submit, posts balanced GL entries via shared lib.\n\nUsage: python3 db_query.py --action \u003caction-name> [--flags ...]\nOutput: JSON to stdout, exit 0 on success, exit 1 on error.\n\"\"\"\nimport argparse\nimport json\nimport os\nimport sqlite3\nimport sys\nimport uuid\nfrom datetime import date, datetime, timedelta, timezone\nfrom decimal import Decimal, InvalidOperation\n\n# Add shared lib to path\ntry:\n sys.path.insert(0, os.path.expanduser(\"~/.openclaw/erpclaw/lib\"))\n from erpclaw_lib.db import get_connection, ensure_db_exists, DEFAULT_DB_PATH\n from erpclaw_lib.decimal_utils import to_decimal, round_currency\n from erpclaw_lib.validation import check_input_lengths\n from erpclaw_lib.gl_posting import (\n validate_gl_entries,\n insert_gl_entries,\n reverse_gl_entries,\n )\n from erpclaw_lib.naming import get_next_name\n from erpclaw_lib.response import ok, err, row_to_dict\n from erpclaw_lib.audit import audit\n from erpclaw_lib.dependencies import check_required_tables\n from erpclaw_lib.query_helpers import resolve_company_id\n from erpclaw_lib.query import Q, P, Table, Field, fn, Order\n from erpclaw_lib.args import SafeArgumentParser, check_unknown_args\nexcept ImportError:\n import json as _json\n print(_json.dumps({\"status\": \"error\", \"error\": \"ERPClaw foundation not installed. Install erpclaw first: clawhub install erpclaw\", \"suggestion\": \"clawhub install erpclaw\"}))\n sys.exit(1)\n\nREQUIRED_TABLES = [\"company\", \"account\"]\n\n# PyPika table aliases\n_t_je = Table(\"journal_entry\")\n_t_jel = Table(\"journal_entry_line\")\n_t_account = Table(\"account\")\n_t_company = Table(\"company\")\n_t_cost_center = Table(\"cost_center\")\n_t_rjt = Table(\"recurring_journal_template\")\n\nVALID_ENTRY_TYPES = (\n \"journal\", \"opening\", \"closing\", \"depreciation\",\n \"write_off\", \"exchange_rate_revaluation\",\n \"inter_company\", \"credit_note\", \"debit_note\",\n)\n\n\n# ---------------------------------------------------------------------------\n# Helpers\n# ---------------------------------------------------------------------------\n\ndef _validate_lines(lines: list[dict]) -> tuple[Decimal, Decimal]:\n \"\"\"Validate journal entry lines. Returns (total_debit, total_credit).\n\n Raises ValueError on validation failure.\n \"\"\"\n if len(lines) \u003c 2:\n raise ValueError(\"At least 2 lines are required\")\n\n total_debit = Decimal(\"0\")\n total_credit = Decimal(\"0\")\n\n for i, line in enumerate(lines):\n if \"account_id\" not in line or not line[\"account_id\"]:\n raise ValueError(f\"Line {i+1}: account_id is required\")\n\n debit = to_decimal(line.get(\"debit\", \"0\"))\n credit = to_decimal(line.get(\"credit\", \"0\"))\n\n if debit \u003c 0 or credit \u003c 0:\n raise ValueError(f\"Line {i+1}: debit and credit must be >= 0\")\n\n if debit > 0 and credit > 0:\n raise ValueError(f\"Line {i+1}: cannot have both debit and credit > 0\")\n\n if debit == 0 and credit == 0:\n raise ValueError(f\"Line {i+1}: either debit or credit must be > 0\")\n\n total_debit += debit\n total_credit += credit\n\n total_debit = round_currency(total_debit)\n total_credit = round_currency(total_credit)\n\n if total_debit != total_credit:\n raise ValueError(\n f\"Total debit ({total_debit}) must equal total credit ({total_credit})\"\n )\n\n return total_debit, total_credit\n\n\ndef _insert_lines(conn, journal_entry_id: str, lines: list[dict]):\n \"\"\"Insert journal_entry_line rows.\"\"\"\n for line in lines:\n line_id = str(uuid.uuid4())\n conn.execute(\n \"\"\"INSERT INTO journal_entry_line\n (id, journal_entry_id, account_id, party_type, party_id,\n debit, credit, cost_center_id, project_id, remark)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\"\"\",\n (line_id, journal_entry_id,\n line[\"account_id\"],\n line.get(\"party_type\"),\n line.get(\"party_id\"),\n str(round_currency(to_decimal(line.get(\"debit\", \"0\")))),\n str(round_currency(to_decimal(line.get(\"credit\", \"0\")))),\n line.get(\"cost_center_id\"),\n line.get(\"project_id\"),\n line.get(\"remark\")),\n )\n\n\ndef _get_je_or_err(conn, journal_entry_id: str) -> dict:\n \"\"\"Fetch a journal entry by ID. Calls err() if not found.\"\"\"\n q = Q.from_(_t_je).select(_t_je.star).where(_t_je.id == P())\n row = conn.execute(q.get_sql(), (journal_entry_id,)).fetchone()\n if not row:\n err(f\"Journal entry {journal_entry_id} not found\")\n return row_to_dict(row)\n\n\ndef _get_je_lines(conn, journal_entry_id: str) -> list[dict]:\n \"\"\"Fetch journal entry lines with account name join.\"\"\"\n jel = Table(\"journal_entry_line\")\n a = Table(\"account\")\n q = (Q.from_(jel)\n .select(jel.star, a.name.as_(\"account_name\"))\n .join(a).on(a.id == jel.account_id)\n .where(jel.journal_entry_id == P())\n .orderby(jel.rowid))\n rows = conn.execute(q.get_sql(), (journal_entry_id,)).fetchall()\n return [row_to_dict(r) for r in rows]\n\n\n# ---------------------------------------------------------------------------\n# 1. add-journal-entry\n# ---------------------------------------------------------------------------\n\ndef add_journal_entry(conn, args):\n \"\"\"Create a new draft journal entry with lines.\"\"\"\n company_id = args.company_id\n if not company_id:\n err(\"--company-id is required\")\n posting_date = args.posting_date\n if not posting_date:\n err(\"--posting-date is required\")\n entry_type = args.entry_type or \"journal\"\n if entry_type not in VALID_ENTRY_TYPES:\n err(f\"Invalid entry type '{entry_type}'. Valid: {VALID_ENTRY_TYPES}\")\n\n # Validate company exists\n q = Q.from_(_t_company).select(_t_company.id).where(_t_company.id == P())\n company = conn.execute(q.get_sql(), (company_id,)).fetchone()\n if not company:\n err(f\"Company {company_id} not found\")\n\n # Parse lines\n lines_json = args.lines\n if not lines_json:\n err(\"--lines is required (JSON array)\")\n try:\n lines = json.loads(lines_json) if isinstance(lines_json, str) else lines_json\n except json.JSONDecodeError as e:\n err(\"Invalid JSON format in --lines\")\n\n # Validate lines\n try:\n total_debit, total_credit = _validate_lines(lines)\n except ValueError as e:\n err(str(e))\n\n # Validate all account_ids exist\n q_acct = Q.from_(_t_account).select(_t_account.id, _t_account.is_frozen).where(_t_account.id == P())\n for i, line in enumerate(lines):\n acct = conn.execute(q_acct.get_sql(), (line[\"account_id\"],)).fetchone()\n if not acct:\n err(f\"Line {i+1}: account {line['account_id']} not found\")\n\n je_id = str(uuid.uuid4())\n naming = get_next_name(conn, \"journal_entry\", company_id=company_id)\n\n conn.execute(\n \"\"\"INSERT INTO journal_entry\n (id, naming_series, posting_date, entry_type, total_debit, total_credit,\n remark, status, company_id)\n VALUES (?, ?, ?, ?, ?, ?, ?, 'draft', ?)\"\"\",\n (je_id, naming, posting_date, entry_type,\n str(total_debit), str(total_credit),\n args.remark, company_id),\n )\n\n _insert_lines(conn, je_id, lines)\n\n audit(conn, \"erpclaw-journals\", \"add-journal-entry\", \"journal_entry\", je_id,\n new_values={\"naming_series\": naming, \"entry_type\": entry_type,\n \"posting_date\": posting_date, \"lines\": len(lines)})\n conn.commit()\n\n ok({\"status\": \"created\", \"journal_entry_id\": je_id,\n \"naming_series\": naming})\n\n\n# ---------------------------------------------------------------------------\n# 2. update-journal-entry\n# ---------------------------------------------------------------------------\n\ndef update_journal_entry(conn, args):\n \"\"\"Update a draft journal entry. Only drafts can be updated.\"\"\"\n je_id = args.journal_entry_id\n if not je_id:\n err(\"--journal-entry-id is required\")\n\n je = _get_je_or_err(conn, je_id)\n if je[\"status\"] != \"draft\":\n err(f\"Cannot update: journal entry is '{je['status']}' (must be 'draft')\",\n suggestion=\"Cancel the document first, then make changes.\")\n\n updated_fields = []\n old_values = {}\n\n # Update posting_date\n if args.posting_date:\n old_values[\"posting_date\"] = je[\"posting_date\"]\n conn.execute(\"UPDATE journal_entry SET posting_date = ?, updated_at = datetime('now') WHERE id = ?\",\n (args.posting_date, je_id))\n updated_fields.append(\"posting_date\")\n\n # Update entry_type\n if args.entry_type:\n if args.entry_type not in VALID_ENTRY_TYPES:\n err(f\"Invalid entry type '{args.entry_type}'. Valid: {VALID_ENTRY_TYPES}\")\n old_values[\"entry_type\"] = je[\"entry_type\"]\n conn.execute(\"UPDATE journal_entry SET entry_type = ?, updated_at = datetime('now') WHERE id = ?\",\n (args.entry_type, je_id))\n updated_fields.append(\"entry_type\")\n\n # Update remark\n if args.remark is not None:\n old_values[\"remark\"] = je[\"remark\"]\n conn.execute(\"UPDATE journal_entry SET remark = ?, updated_at = datetime('now') WHERE id = ?\",\n (args.remark, je_id))\n updated_fields.append(\"remark\")\n\n # Replace lines if provided\n if args.lines:\n try:\n lines = json.loads(args.lines) if isinstance(args.lines, str) else args.lines\n except json.JSONDecodeError as e:\n err(\"Invalid JSON format in --lines\")\n\n try:\n total_debit, total_credit = _validate_lines(lines)\n except ValueError as e:\n err(str(e))\n\n # Validate all account_ids exist\n q_acct = Q.from_(_t_account).select(_t_account.id).where(_t_account.id == P())\n for i, line in enumerate(lines):\n acct = conn.execute(q_acct.get_sql(), (line[\"account_id\"],)).fetchone()\n if not acct:\n err(f\"Line {i+1}: account {line['account_id']} not found\")\n\n # Delete old lines, insert new\n q_del = Q.from_(_t_jel).delete().where(_t_jel.journal_entry_id == P())\n conn.execute(q_del.get_sql(), (je_id,))\n _insert_lines(conn, je_id, lines)\n\n conn.execute(\n \"\"\"UPDATE journal_entry SET total_debit = ?, total_credit = ?,\n updated_at = datetime('now') WHERE id = ?\"\"\",\n (str(total_debit), str(total_credit), je_id),\n )\n updated_fields.append(\"lines\")\n\n if not updated_fields:\n err(\"No fields to update\")\n\n audit(conn, \"erpclaw-journals\", \"update-journal-entry\", \"journal_entry\", je_id,\n old_values=old_values,\n new_values={\"updated_fields\": updated_fields})\n conn.commit()\n\n ok({\"status\": \"updated\", \"journal_entry_id\": je_id,\n \"updated_fields\": updated_fields})\n\n\n# ---------------------------------------------------------------------------\n# 3. get-journal-entry\n# ---------------------------------------------------------------------------\n\ndef get_journal_entry(conn, args):\n \"\"\"Get a journal entry with all its lines.\"\"\"\n je_id = args.journal_entry_id\n if not je_id:\n err(\"--journal-entry-id is required\")\n\n je = _get_je_or_err(conn, je_id)\n lines = _get_je_lines(conn, je_id)\n\n # Format lines for output\n formatted_lines = []\n for line in lines:\n formatted_lines.append({\n \"id\": line[\"id\"],\n \"account_id\": line[\"account_id\"],\n \"account_name\": line[\"account_name\"],\n \"debit\": line[\"debit\"],\n \"credit\": line[\"credit\"],\n \"party_type\": line.get(\"party_type\"),\n \"party_id\": line.get(\"party_id\"),\n \"cost_center_id\": line.get(\"cost_center_id\"),\n \"project_id\": line.get(\"project_id\"),\n \"remark\": line.get(\"remark\"),\n })\n\n ok({\n \"id\": je[\"id\"],\n \"naming_series\": je[\"naming_series\"],\n \"posting_date\": je[\"posting_date\"],\n \"entry_type\": je[\"entry_type\"],\n \"status\": je[\"status\"],\n \"total_debit\": je[\"total_debit\"],\n \"total_credit\": je[\"total_credit\"],\n \"remark\": je.get(\"remark\"),\n \"amended_from\": je.get(\"amended_from\"),\n \"company_id\": je[\"company_id\"],\n \"lines\": formatted_lines,\n })\n\n\n# ---------------------------------------------------------------------------\n# 4. list-journal-entries\n# ---------------------------------------------------------------------------\n\ndef list_journal_entries(conn, args):\n \"\"\"List journal entries with filtering.\"\"\"\n company_id = resolve_company_id(conn, getattr(args, 'company_id', None))\n\n je = Table(\"journal_entry\")\n params = [company_id]\n\n # Build base query with required company filter\n base = Q.from_(je).where(je.company_id == P())\n\n if args.je_status:\n base = base.where(je.status == P())\n params.append(args.je_status)\n\n if args.entry_type:\n base = base.where(je.entry_type == P())\n params.append(args.entry_type)\n\n if args.from_date:\n base = base.where(je.posting_date >= P())\n params.append(args.from_date)\n\n if args.to_date:\n base = base.where(je.posting_date \u003c= P())\n params.append(args.to_date)\n\n if args.account_id:\n # Subquery: keep as raw SQL snippet via Criterion.any for clarity\n jel = Table(\"journal_entry_line\")\n sub = Q.from_(jel).select(jel.journal_entry_id).where(jel.account_id == P())\n base = base.where(je.id.isin(sub))\n params.append(args.account_id)\n\n # Total count\n q_count = base.select(fn.Count(\"*\"))\n count_row = conn.execute(q_count.get_sql(), params).fetchone()\n total_count = count_row[0]\n\n # Paginated results\n limit = int(args.limit) if args.limit else 20\n offset = int(args.offset) if args.offset else 0\n list_params = params + [limit, offset]\n\n q_list = (base.select(\n je.id, je.naming_series, je.posting_date, je.entry_type,\n je.status, je.total_debit, je.total_credit, je.remark)\n .orderby(je.posting_date, order=Order.desc)\n .orderby(je.created_at, order=Order.desc)\n .limit(P()).offset(P()))\n rows = conn.execute(q_list.get_sql(), list_params).fetchall()\n\n entries = [row_to_dict(r) for r in rows]\n ok({\"entries\": entries, \"total_count\": total_count,\n \"limit\": limit, \"offset\": offset,\n \"has_more\": offset + limit \u003c total_count})\n\n\n# ---------------------------------------------------------------------------\n# 5. submit-journal-entry\n# ---------------------------------------------------------------------------\n\ndef submit_journal_entry(conn, args):\n \"\"\"Submit a draft JE: re-validate, post GL entries, update status.\"\"\"\n je_id = args.journal_entry_id\n if not je_id:\n err(\"--journal-entry-id is required\")\n\n je = _get_je_or_err(conn, je_id)\n if je[\"status\"] != \"draft\":\n err(f\"Cannot submit: journal entry is '{je['status']}' (must be 'draft')\")\n\n lines = _get_je_lines(conn, je_id)\n\n # Re-validate lines (they were validated at creation but re-check)\n try:\n _validate_lines([{\n \"account_id\": l[\"account_id\"],\n \"debit\": l[\"debit\"],\n \"credit\": l[\"credit\"],\n } for l in lines])\n except ValueError as e:\n sys.stderr.write(f\"[erpclaw-journals] {e}\\n\")\n err(\"Validation failed at submit\")\n\n # Build GL entries from lines\n gl_entries = []\n for line in lines:\n gl_entries.append({\n \"account_id\": line[\"account_id\"],\n \"debit\": line[\"debit\"],\n \"credit\": line[\"credit\"],\n \"party_type\": line.get(\"party_type\"),\n \"party_id\": line.get(\"party_id\"),\n \"cost_center_id\": line.get(\"cost_center_id\"),\n })\n\n # Single transaction: validate GL, insert GL entries, update JE status\n try:\n is_opening = je[\"entry_type\"] in (\"opening\",)\n validate_gl_entries(\n conn, gl_entries, je[\"company_id\"],\n je[\"posting_date\"], is_opening=is_opening,\n voucher_type=\"journal_entry\",\n )\n gl_ids = insert_gl_entries(\n conn, gl_entries,\n voucher_type=\"journal_entry\",\n voucher_id=je_id,\n posting_date=je[\"posting_date\"],\n company_id=je[\"company_id\"],\n remarks=je.get(\"remark\") or \"\",\n is_opening=is_opening,\n )\n except ValueError as e:\n sys.stderr.write(f\"[erpclaw-journals] {e}\\n\")\n err(f\"GL posting failed: {e}\")\n\n conn.execute(\n \"\"\"UPDATE journal_entry SET status = 'submitted',\n updated_at = datetime('now') WHERE id = ?\"\"\",\n (je_id,),\n )\n\n audit(conn, \"erpclaw-journals\", \"submit-journal-entry\", \"journal_entry\", je_id,\n new_values={\"gl_entries_created\": len(gl_ids)})\n conn.commit()\n\n ok({\"status\": \"submitted\", \"journal_entry_id\": je_id,\n \"gl_entries_created\": len(gl_ids)})\n\n\n# ---------------------------------------------------------------------------\n# 6. cancel-journal-entry\n# ---------------------------------------------------------------------------\n\ndef cancel_journal_entry(conn, args):\n \"\"\"Cancel a submitted JE: reverse GL entries, update status.\"\"\"\n je_id = args.journal_entry_id\n if not je_id:\n err(\"--journal-entry-id is required\")\n\n je = _get_je_or_err(conn, je_id)\n if je[\"status\"] != \"submitted\":\n err(f\"Cannot cancel: journal entry is '{je['status']}' (must be 'submitted')\")\n\n # Single transaction: reverse GL entries + update status\n try:\n reversal_ids = reverse_gl_entries(\n conn,\n voucher_type=\"journal_entry\",\n voucher_id=je_id,\n posting_date=je[\"posting_date\"],\n )\n except ValueError as e:\n sys.stderr.write(f\"[erpclaw-journals] {e}\\n\")\n err(f\"GL reversal failed: {e}\")\n\n conn.execute(\n \"\"\"UPDATE journal_entry SET status = 'cancelled',\n updated_at = datetime('now') WHERE id = ?\"\"\",\n (je_id,),\n )\n\n audit(conn, \"erpclaw-journals\", \"cancel-journal-entry\", \"journal_entry\", je_id,\n new_values={\"reversed_gl_entries\": len(reversal_ids)})\n conn.commit()\n\n ok({\"status\": \"cancelled\", \"journal_entry_id\": je_id, \"reversed\": True})\n\n\n# ---------------------------------------------------------------------------\n# 7. amend-journal-entry\n# ---------------------------------------------------------------------------\n\ndef amend_journal_entry(conn, args):\n \"\"\"Amend a submitted JE: cancel old, create new linked draft.\"\"\"\n je_id = args.journal_entry_id\n if not je_id:\n err(\"--journal-entry-id is required\")\n\n je = _get_je_or_err(conn, je_id)\n if je[\"status\"] != \"submitted\":\n err(f\"Cannot amend: journal entry is '{je['status']}' (must be 'submitted')\")\n\n # Cancel the old JE (reverse GL entries)\n try:\n reverse_gl_entries(\n conn,\n voucher_type=\"journal_entry\",\n voucher_id=je_id,\n posting_date=je[\"posting_date\"],\n )\n except ValueError as e:\n sys.stderr.write(f\"[erpclaw-journals] {e}\\n\")\n err(f\"GL reversal failed: {e}\")\n\n conn.execute(\n \"\"\"UPDATE journal_entry SET status = 'amended',\n updated_at = datetime('now') WHERE id = ?\"\"\",\n (je_id,),\n )\n\n # Determine lines for new JE\n if args.lines:\n try:\n new_lines = json.loads(args.lines) if isinstance(args.lines, str) else args.lines\n except json.JSONDecodeError as e:\n err(\"Invalid JSON format in --lines\")\n else:\n # Copy lines from original\n q_lines = Q.from_(_t_jel).select(_t_jel.star).where(_t_jel.journal_entry_id == P())\n old_lines = conn.execute(q_lines.get_sql(), (je_id,)).fetchall()\n new_lines = []\n for ol in old_lines:\n old_dict = row_to_dict(ol)\n new_lines.append({\n \"account_id\": old_dict[\"account_id\"],\n \"debit\": old_dict[\"debit\"],\n \"credit\": old_dict[\"credit\"],\n \"party_type\": old_dict.get(\"party_type\"),\n \"party_id\": old_dict.get(\"party_id\"),\n \"cost_center_id\": old_dict.get(\"cost_center_id\"),\n \"project_id\": old_dict.get(\"project_id\"),\n \"remark\": old_dict.get(\"remark\"),\n })\n\n # Validate lines\n try:\n total_debit, total_credit = _validate_lines(new_lines)\n except ValueError as e:\n err(str(e))\n\n # Create new draft JE\n new_je_id = str(uuid.uuid4())\n new_posting_date = args.posting_date or je[\"posting_date\"]\n naming = get_next_name(conn, \"journal_entry\", company_id=je[\"company_id\"])\n\n conn.execute(\n \"\"\"INSERT INTO journal_entry\n (id, naming_series, posting_date, entry_type, total_debit, total_credit,\n remark, status, amended_from, company_id)\n VALUES (?, ?, ?, ?, ?, ?, ?, 'draft', ?, ?)\"\"\",\n (new_je_id, naming, new_posting_date, je[\"entry_type\"],\n str(total_debit), str(total_credit),\n args.remark if args.remark is not None else je.get(\"remark\"),\n je_id, je[\"company_id\"]),\n )\n\n _insert_lines(conn, new_je_id, new_lines)\n\n audit(conn, \"erpclaw-journals\", \"amend-journal-entry\", \"journal_entry\", je_id,\n new_values={\"new_journal_entry_id\": new_je_id, \"new_naming_series\": naming})\n conn.commit()\n\n ok({\"status\": \"created\", \"original_id\": je_id,\n \"new_journal_entry_id\": new_je_id,\n \"new_naming_series\": naming})\n\n\n# ---------------------------------------------------------------------------\n# 8. delete-journal-entry\n# ---------------------------------------------------------------------------\n\ndef delete_journal_entry(conn, args):\n \"\"\"Delete a draft JE. Only drafts can be deleted.\"\"\"\n je_id = args.journal_entry_id\n if not je_id:\n err(\"--journal-entry-id is required\")\n\n je = _get_je_or_err(conn, je_id)\n if je[\"status\"] != \"draft\":\n err(f\"Cannot delete: journal entry is '{je['status']}' (only 'draft' can be deleted)\",\n suggestion=\"Cancel the document first, then delete.\")\n\n naming = je[\"naming_series\"]\n\n # Delete lines first (FK constraint), then header\n q_del_lines = Q.from_(_t_jel).delete().where(_t_jel.journal_entry_id == P())\n conn.execute(q_del_lines.get_sql(), (je_id,))\n q_del_je = Q.from_(_t_je).delete().where(_t_je.id == P())\n conn.execute(q_del_je.get_sql(), (je_id,))\n\n audit(conn, \"erpclaw-journals\", \"delete-journal-entry\", \"journal_entry\", je_id,\n old_values={\"naming_series\": naming})\n conn.commit()\n\n ok({\"status\": \"deleted\", \"deleted\": True})\n\n\n# ---------------------------------------------------------------------------\n# 9. duplicate-journal-entry\n# ---------------------------------------------------------------------------\n\ndef duplicate_journal_entry(conn, args):\n \"\"\"Duplicate a JE as a new draft. Copies all lines.\"\"\"\n je_id = args.journal_entry_id\n if not je_id:\n err(\"--journal-entry-id is required\")\n\n je = _get_je_or_err(conn, je_id)\n q_lines = Q.from_(_t_jel).select(_t_jel.star).where(_t_jel.journal_entry_id == P())\n old_lines = conn.execute(q_lines.get_sql(), (je_id,)).fetchall()\n\n new_lines = []\n for ol in old_lines:\n old_dict = row_to_dict(ol)\n new_lines.append({\n \"account_id\": old_dict[\"account_id\"],\n \"debit\": old_dict[\"debit\"],\n \"credit\": old_dict[\"credit\"],\n \"party_type\": old_dict.get(\"party_type\"),\n \"party_id\": old_dict.get(\"party_id\"),\n \"cost_center_id\": old_dict.get(\"cost_center_id\"),\n \"project_id\": old_dict.get(\"project_id\"),\n \"remark\": old_dict.get(\"remark\"),\n })\n\n # Validate lines (should always pass since source was valid)\n try:\n total_debit, total_credit = _validate_lines(new_lines)\n except ValueError as e:\n err(str(e))\n\n new_je_id = str(uuid.uuid4())\n posting_date = args.posting_date or datetime.now(timezone.utc).strftime(\"%Y-%m-%d\")\n naming = get_next_name(conn, \"journal_entry\", company_id=je[\"company_id\"])\n\n conn.execute(\n \"\"\"INSERT INTO journal_entry\n (id, naming_series, posting_date, entry_type, total_debit, total_credit,\n remark, status, company_id)\n VALUES (?, ?, ?, ?, ?, ?, ?, 'draft', ?)\"\"\",\n (new_je_id, naming, posting_date, je[\"entry_type\"],\n str(total_debit), str(total_credit),\n je.get(\"remark\"), je[\"company_id\"]),\n )\n\n _insert_lines(conn, new_je_id, new_lines)\n\n audit(conn, \"erpclaw-journals\", \"duplicate-journal-entry\", \"journal_entry\", je_id,\n new_values={\"new_journal_entry_id\": new_je_id, \"naming_series\": naming})\n conn.commit()\n\n ok({\"status\": \"created\", \"new_journal_entry_id\": new_je_id,\n \"naming_series\": naming})\n\n\n# ---------------------------------------------------------------------------\n# 10. create-intercompany-je\n# ---------------------------------------------------------------------------\n\ndef _ensure_intercompany_account(conn, company_id, name, root_type, account_type):\n \"\"\"Find or create an intercompany account for a company.\"\"\"\n q = (Q.from_(_t_account).select(_t_account.id)\n .where(_t_account.name == P())\n .where(_t_account.company_id == P()))\n acct = conn.execute(q.get_sql(), (name, company_id)).fetchone()\n if acct:\n return acct[\"id\"]\n\n acct_id = str(uuid.uuid4())\n balance_dir = \"debit_normal\" if root_type == \"asset\" else \"credit_normal\"\n conn.execute(\n \"\"\"INSERT INTO account (id, name, root_type, account_type, currency,\n is_group, balance_direction, company_id, depth)\n VALUES (?, ?, ?, ?, 'USD', 0, ?, ?, 0)\"\"\",\n (acct_id, name, root_type, account_type, balance_dir, company_id),\n )\n return acct_id\n\n\ndef create_intercompany_je(conn, args):\n \"\"\"Create paired intercompany journal entries between two companies.\n\n Source company: DR Intercompany Receivable / CR Revenue (or specified account)\n Target company: DR Expense (or specified account) / CR Intercompany Payable\n Both JEs reference each other via remark field.\n \"\"\"\n source_company_id = args.source_company_id\n target_company_id = args.target_company_id\n amount_str = args.amount\n description = args.description or \"Intercompany transaction\"\n posting_date = args.posting_date\n\n if not source_company_id:\n err(\"--source-company-id is required\")\n if not target_company_id:\n err(\"--target-company-id is required\")\n if not amount_str:\n err(\"--amount is required\")\n if not posting_date:\n err(\"--posting-date is required\")\n if source_company_id == target_company_id:\n err(\"Source and target company must be different\")\n\n amount = to_decimal(amount_str)\n if amount \u003c= 0:\n err(\"Amount must be positive\")\n\n # Validate both companies exist and share the same currency\n q_co = Q.from_(_t_company).select(_t_company.id, _t_company.default_currency).where(_t_company.id == P())\n src_co = conn.execute(q_co.get_sql(), (source_company_id,)).fetchone()\n tgt_co = conn.execute(q_co.get_sql(), (target_company_id,)).fetchone()\n if not src_co:\n err(f\"Source company {source_company_id} not found\")\n if not tgt_co:\n err(f\"Target company {target_company_id} not found\")\n if src_co[\"default_currency\"] != tgt_co[\"default_currency\"]:\n err(\"Intercompany JE between different currencies is not supported (v2)\")\n\n # Ensure intercompany accounts exist in both companies\n src_ic_recv = _ensure_intercompany_account(\n conn, source_company_id, \"Intercompany Receivable\", \"asset\", \"receivable\")\n q_rev = (Q.from_(_t_account).select(_t_account.id)\n .where(_t_account.account_type == \"revenue\")\n .where(_t_account.company_id == P())\n .where(_t_account.is_group == 0)\n .limit(1))\n src_revenue = conn.execute(q_rev.get_sql(), (source_company_id,)).fetchone()\n if not src_revenue:\n err(\"Source company has no revenue account\")\n\n tgt_ic_pay = _ensure_intercompany_account(\n conn, target_company_id, \"Intercompany Payable\", \"liability\", \"payable\")\n q_exp = (Q.from_(_t_account).select(_t_account.id)\n .where(_t_account.account_type.isin([\"expense\", \"cost_of_goods_sold\"]))\n .where(_t_account.company_id == P())\n .where(_t_account.is_group == 0)\n .limit(1))\n tgt_expense = conn.execute(q_exp.get_sql(), (target_company_id,)).fetchone()\n if not tgt_expense:\n err(\"Target company has no expense account\")\n\n # Get cost centers for P&L entries\n q_cc = (Q.from_(_t_cost_center).select(_t_cost_center.id)\n .where(_t_cost_center.company_id == P())\n .where(_t_cost_center.is_group == 0)\n .limit(1))\n src_cc = conn.execute(q_cc.get_sql(), (source_company_id,)).fetchone()\n tgt_cc = conn.execute(q_cc.get_sql(), (target_company_id,)).fetchone()\n\n amt = str(round_currency(amount))\n\n # Create Source JE: DR Intercompany Receivable / CR Revenue\n src_je_id = str(uuid.uuid4())\n src_naming = get_next_name(conn, \"journal_entry\", company_id=source_company_id)\n conn.execute(\n \"\"\"INSERT INTO journal_entry\n (id, naming_series, posting_date, entry_type, total_debit, total_credit,\n remark, status, company_id)\n VALUES (?, ?, ?, 'inter_company', ?, ?, ?, 'draft', ?)\"\"\",\n (src_je_id, src_naming, posting_date, amt, amt, description, source_company_id),\n )\n # Source lines\n for line_data in [\n {\"account_id\": src_ic_recv, \"debit\": amt, \"credit\": \"0\"},\n {\"account_id\": src_revenue[\"id\"], \"debit\": \"0\", \"credit\": amt,\n \"cost_center_id\": src_cc[\"id\"] if src_cc else None},\n ]:\n line_id = str(uuid.uuid4())\n conn.execute(\n \"\"\"INSERT INTO journal_entry_line\n (id, journal_entry_id, account_id, debit, credit, cost_center_id)\n VALUES (?, ?, ?, ?, ?, ?)\"\"\",\n (line_id, src_je_id, line_data[\"account_id\"],\n line_data[\"debit\"], line_data[\"credit\"],\n line_data.get(\"cost_center_id\")),\n )\n\n # Create Target JE: DR Expense / CR Intercompany Payable\n tgt_je_id = str(uuid.uuid4())\n tgt_naming = get_next_name(conn, \"journal_entry\", company_id=target_company_id)\n conn.execute(\n \"\"\"INSERT INTO journal_entry\n (id, naming_series, posting_date, entry_type, total_debit, total_credit,\n remark, status, company_id)\n VALUES (?, ?, ?, 'inter_company', ?, ?, ?, 'draft', ?)\"\"\",\n (tgt_je_id, tgt_naming, posting_date, amt, amt, description, target_company_id),\n )\n # Target lines\n for line_data in [\n {\"account_id\": tgt_expense[\"id\"], \"debit\": amt, \"credit\": \"0\",\n \"cost_center_id\": tgt_cc[\"id\"] if tgt_cc else None},\n {\"account_id\": tgt_ic_pay, \"debit\": \"0\", \"credit\": amt},\n ]:\n line_id = str(uuid.uuid4())\n conn.execute(\n \"\"\"INSERT INTO journal_entry_line\n (id, journal_entry_id, account_id, debit, credit, cost_center_id)\n VALUES (?, ?, ?, ?, ?, ?)\"\"\",\n (line_id, tgt_je_id, line_data[\"account_id\"],\n line_data[\"debit\"], line_data[\"credit\"],\n line_data.get(\"cost_center_id\")),\n )\n\n # Store cross-references in remark\n conn.execute(\n \"UPDATE journal_entry SET remark = ? WHERE id = ?\",\n (f\"{description} | Paired with {tgt_naming} ({target_company_id})\", src_je_id),\n )\n conn.execute(\n \"UPDATE journal_entry SET remark = ? WHERE id = ?\",\n (f\"{description} | Paired with {src_naming} ({source_company_id})\", tgt_je_id),\n )\n\n audit(conn, \"erpclaw-journals\", \"create-intercompany-je\", \"journal_entry\", src_je_id,\n new_values={\"target_je_id\": tgt_je_id, \"amount\": amt})\n conn.commit()\n\n ok({\n \"source_je_id\": src_je_id, \"source_naming\": src_naming,\n \"target_je_id\": tgt_je_id, \"target_naming\": tgt_naming,\n \"amount\": amt,\n \"description\": description,\n })\n\n\n# ---------------------------------------------------------------------------\n# Recurring Journal Template helpers\n# ---------------------------------------------------------------------------\n\ndef _advance_date(d: date, frequency: str) -> date:\n \"\"\"Advance a date by one period based on frequency.\n\n Uses stdlib only (no dateutil). Handles month-end edge cases.\n \"\"\"\n if frequency == \"daily\":\n return d + timedelta(days=1)\n elif frequency == \"weekly\":\n return d + timedelta(weeks=1)\n elif frequency == \"monthly\":\n month = d.month + 1\n year = d.year\n if month > 12:\n month = 1\n year += 1\n # Clamp day to month's max (e.g. Jan 31 → Feb 28)\n import calendar\n max_day = calendar.monthrange(year, month)[1]\n day = min(d.day, max_day)\n return date(year, month, day)\n elif frequency == \"quarterly\":\n month = d.month + 3\n year = d.year\n while month > 12:\n month -= 12\n year += 1\n import calendar\n max_day = calendar.monthrange(year, month)[1]\n day = min(d.day, max_day)\n return date(year, month, day)\n elif frequency == \"annual\":\n import calendar\n year = d.year + 1\n max_day = calendar.monthrange(year, d.month)[1]\n day = min(d.day, max_day)\n return date(year, d.month, day)\n else:\n raise ValueError(f\"Unknown frequency: {frequency}\")\n\n\nVALID_FREQUENCIES = (\"daily\", \"weekly\", \"monthly\", \"quarterly\", \"annual\")\n\n\n# ---------------------------------------------------------------------------\n# 11. add-recurring-template\n# ---------------------------------------------------------------------------\n\ndef add_recurring_template(conn, args):\n \"\"\"Create a recurring journal template.\"\"\"\n company_id = args.company_id\n if not company_id:\n err(\"--company-id is required\")\n template_name = args.template_name\n if not template_name:\n err(\"--template-name is required\")\n frequency = args.frequency or \"monthly\"\n if frequency not in VALID_FREQUENCIES:\n err(f\"Invalid frequency '{frequency}'. Valid: {VALID_FREQUENCIES}\")\n start_date = args.start_date\n if not start_date:\n err(\"--start-date is required\")\n end_date = args.end_date # optional\n\n entry_type = args.entry_type or \"journal\"\n if entry_type not in VALID_ENTRY_TYPES:\n err(f\"Invalid entry type '{entry_type}'. Valid: {VALID_ENTRY_TYPES}\")\n\n auto_submit = 1 if args.auto_submit else 0\n\n # Validate company\n q = Q.from_(_t_company).select(_t_company.id).where(_t_company.id == P())\n company = conn.execute(q.get_sql(), (company_id,)).fetchone()\n if not company:\n err(f\"Company {company_id} not found\")\n\n # Parse and validate lines\n lines_json = args.lines\n if not lines_json:\n err(\"--lines is required (JSON array)\")\n try:\n lines = json.loads(lines_json) if isinstance(lines_json, str) else lines_json\n except json.JSONDecodeError:\n err(\"Invalid JSON format in --lines\")\n\n try:\n _validate_lines(lines)\n except ValueError as e:\n err(str(e))\n\n # Validate accounts exist\n q_acct = Q.from_(_t_account).select(_t_account.id).where(_t_account.id == P())\n for i, line in enumerate(lines):\n acct = conn.execute(q_acct.get_sql(), (line[\"account_id\"],)).fetchone()\n if not acct:\n err(f\"Line {i+1}: account {line['account_id']} not found\")\n\n template_id = str(uuid.uuid4())\n naming = get_next_name(conn, \"recurring_journal_template\", company_id=company_id)\n\n conn.execute(\n \"\"\"INSERT INTO recurring_journal_template\n (id, naming_series, company_id, name, frequency, start_date, end_date,\n next_run_date, entry_type, lines, auto_submit, remark, status)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active')\"\"\",\n (template_id, naming, company_id, template_name, frequency,\n start_date, end_date, start_date, entry_type,\n json.dumps(lines) if isinstance(lines, list) else lines_json,\n auto_submit, args.remark),\n )\n\n audit(conn, \"erpclaw-journals\", \"add-recurring-template\",\n \"recurring_journal_template\", template_id,\n new_values={\"name\": template_name, \"frequency\": frequency})\n conn.commit()\n\n ok({\"status\": \"created\", \"template_id\": template_id,\n \"naming_series\": naming, \"next_run_date\": start_date})\n\n\n# ---------------------------------------------------------------------------\n# 12. update-recurring-template\n# ---------------------------------------------------------------------------\n\ndef update_recurring_template(conn, args):\n \"\"\"Update a recurring journal template. Only active/paused templates.\"\"\"\n template_id = args.template_id\n if not template_id:\n err(\"--template-id is required\")\n\n q = Q.from_(_t_rjt).select(_t_rjt.star).where(_t_rjt.id == P())\n row = conn.execute(q.get_sql(), (template_id,)).fetchone()\n if not row:\n err(f\"Recurring template {template_id} not found\")\n tmpl = row_to_dict(row)\n\n if tmpl[\"status\"] == \"completed\":\n err(\"Cannot update a completed template\")\n\n updated_fields = []\n\n if args.template_name:\n conn.execute(\"UPDATE recurring_journal_template SET name = ?, updated_at = datetime('now') WHERE id = ?\",\n (args.template_name, template_id))\n updated_fields.append(\"name\")\n\n if args.frequency:\n if args.frequency not in VALID_FREQUENCIES:\n err(f\"Invalid frequency '{args.frequency}'. Valid: {VALID_FREQUENCIES}\")\n conn.execute(\"UPDATE recurring_journal_template SET frequency = ?, updated_at = datetime('now') WHERE id = ?\",\n (args.frequency, template_id))\n updated_fields.append(\"frequency\")\n\n if args.end_date:\n conn.execute(\"UPDATE recurring_journal_template SET end_date = ?, updated_at = datetime('now') WHERE id = ?\",\n (args.end_date, template_id))\n updated_fields.append(\"end_date\")\n\n if args.entry_type:\n if args.entry_type not in VALID_ENTRY_TYPES:\n err(f\"Invalid entry type '{args.entry_type}'. Valid: {VALID_ENTRY_TYPES}\")\n conn.execute(\"UPDATE recurring_journal_template SET entry_type = ?, updated_at = datetime('now') WHERE id = ?\",\n (args.entry_type, template_id))\n updated_fields.append(\"entry_type\")\n\n if args.remark is not None:\n conn.execute(\"UPDATE recurring_journal_template SET remark = ?, updated_at = datetime('now') WHERE id = ?\",\n (args.remark, template_id))\n updated_fields.append(\"remark\")\n\n if args.auto_submit is not None:\n val = 1 if args.auto_submit else 0\n conn.execute(\"UPDATE recurring_journal_template SET auto_submit = ?, updated_at = datetime('now') WHERE id = ?\",\n (val, template_id))\n updated_fields.append(\"auto_submit\")\n\n if args.lines:\n try:\n lines = json.loads(args.lines) if isinstance(args.lines, str) else args.lines\n except json.JSONDecodeError:\n err(\"Invalid JSON format in --lines\")\n try:\n _validate_lines(lines)\n except ValueError as e:\n err(str(e))\n q_acct = Q.from_(_t_account).select(_t_account.id).where(_t_account.id == P())\n for i, line in enumerate(lines):\n acct = conn.execute(q_acct.get_sql(), (line[\"account_id\"],)).fetchone()\n if not acct:\n err(f\"Line {i+1}: account {line['account_id']} not found\")\n conn.execute(\"UPDATE recurring_journal_template SET lines = ?, updated_at = datetime('now') WHERE id = ?\",\n (json.dumps(lines) if isinstance(lines, list) else args.lines, template_id))\n updated_fields.append(\"lines\")\n\n if args.template_status:\n if args.template_status not in (\"active\", \"paused\"):\n err(f\"Can only set status to 'active' or 'paused', got '{args.template_status}'\")\n conn.execute(\"UPDATE recurring_journal_template SET status = ?, updated_at = datetime('now') WHERE id = ?\",\n (args.template_status, template_id))\n updated_fields.append(\"status\")\n\n if not updated_fields:\n err(\"No fields to update\")\n\n audit(conn, \"erpclaw-journals\", \"update-recurring-template\",\n \"recurring_journal_template\", template_id,\n new_values={\"updated_fields\": updated_fields})\n conn.commit()\n\n ok({\"status\": \"updated\", \"template_id\": template_id,\n \"updated_fields\": updated_fields})\n\n\n# ---------------------------------------------------------------------------\n# 13. list-recurring-templates\n# ---------------------------------------------------------------------------\n\ndef list_recurring_templates(conn, args):\n \"\"\"List recurring journal templates for a company.\"\"\"\n company_id = resolve_company_id(conn, getattr(args, 'company_id', None))\n\n rjt = Table(\"recurring_journal_template\")\n params = [company_id]\n\n base = Q.from_(rjt).where(rjt.company_id == P())\n\n if args.template_status:\n base = base.where(rjt.status == P())\n params.append(args.template_status)\n\n limit = int(args.limit) if args.limit else 20\n offset = int(args.offset) if args.offset else 0\n\n q_count = base.select(fn.Count(\"*\"))\n count_row = conn.execute(q_count.get_sql(), params).fetchone()\n total_count = count_row[0]\n\n list_params = params + [limit, offset]\n q_list = (base.select(\n rjt.id, rjt.naming_series, rjt.name, rjt.frequency,\n rjt.start_date, rjt.end_date, rjt.next_run_date,\n rjt.last_generated_date, rjt.entry_type, rjt.auto_submit,\n rjt.remark, rjt.status)\n .orderby(rjt.next_run_date, order=Order.asc)\n .limit(P()).offset(P()))\n rows = conn.execute(q_list.get_sql(), list_params).fetchall()\n\n templates = [row_to_dict(r) for r in rows]\n ok({\"templates\": templates, \"total_count\": total_count,\n \"limit\": limit, \"offset\": offset,\n \"has_more\": offset + limit \u003c total_count})\n\n\n# ---------------------------------------------------------------------------\n# 14. get-recurring-template\n# ---------------------------------------------------------------------------\n\ndef get_recurring_template(conn, args):\n \"\"\"Get a recurring template with full detail including lines.\"\"\"\n template_id = args.template_id\n if not template_id:\n err(\"--template-id is required\")\n\n q = Q.from_(_t_rjt).select(_t_rjt.star).where(_t_rjt.id == P())\n row = conn.execute(q.get_sql(), (template_id,)).fetchone()\n if not row:\n err(f\"Recurring template {template_id} not found\")\n\n tmpl = row_to_dict(row)\n # Parse lines JSON for display\n try:\n tmpl[\"lines\"] = json.loads(tmpl[\"lines\"])\n except (json.JSONDecodeError, TypeError):\n pass\n\n ok(tmpl)\n\n\n# ---------------------------------------------------------------------------\n# 15. process-recurring\n# ---------------------------------------------------------------------------\n\ndef process_recurring(conn, args):\n \"\"\"Generate journal entries from all due recurring templates.\n\n Idempotent: only generates JEs where next_run_date \u003c= as_of_date.\n After generating, advances next_run_date by one frequency period.\n If end_date is reached, marks template as 'completed'.\n \"\"\"\n company_id = args.company_id\n if not company_id:\n err(\"--company-id is required\")\n\n as_of_date_str = args.as_of_date or datetime.now(timezone.utc).strftime(\"%Y-%m-%d\")\n\n # Find all due templates\n q_due = (Q.from_(_t_rjt).select(_t_rjt.star)\n .where(_t_rjt.company_id == P())\n .where(_t_rjt.status == \"active\")\n .where(_t_rjt.next_run_date \u003c= P())\n .orderby(_t_rjt.next_run_date, order=Order.asc))\n due_templates = conn.execute(q_due.get_sql(), (company_id, as_of_date_str)).fetchall()\n\n results = []\n\n for row in due_templates:\n tmpl = row_to_dict(row)\n template_id = tmpl[\"id\"]\n lines = json.loads(tmpl[\"lines\"])\n posting_date = tmpl[\"next_run_date\"]\n\n # Create the journal entry\n je_id = str(uuid.uuid4())\n naming = get_next_name(conn, \"journal_entry\", company_id=company_id)\n\n total_debit = sum(to_decimal(l.get(\"debit\", \"0\")) for l in lines)\n total_credit = sum(to_decimal(l.get(\"credit\", \"0\")) for l in lines)\n total_debit = round_currency(total_debit)\n total_credit = round_currency(total_credit)\n\n remark = tmpl.get(\"remark\") or f\"Auto-generated from {tmpl['naming_series'] or tmpl['name']}\"\n\n conn.execute(\n \"\"\"INSERT INTO journal_entry\n (id, naming_series, posting_date, entry_type, total_debit, total_credit,\n remark, status, company_id)\n VALUES (?, ?, ?, ?, ?, ?, ?, 'draft', ?)\"\"\",\n (je_id, naming, posting_date, tmpl[\"entry_type\"],\n str(total_debit), str(total_credit), remark, company_id),\n )\n _insert_lines(conn, je_id, lines)\n\n je_status = \"draft\"\n\n # Auto-submit if configured\n if tmpl[\"auto_submit\"]:\n try:\n is_opening = tmpl[\"entry_type\"] in (\"opening\",)\n gl_entries = [{\n \"account_id\": l[\"account_id\"],\n \"debit\": l.get(\"debit\", \"0\"),\n \"credit\": l.get(\"credit\", \"0\"),\n \"party_type\": l.get(\"party_type\"),\n \"party_id\": l.get(\"party_id\"),\n \"cost_center_id\": l.get(\"cost_center_id\"),\n } for l in lines]\n\n validate_gl_entries(\n conn, gl_entries, company_id, posting_date,\n is_opening=is_opening, voucher_type=\"journal_entry\",\n )\n insert_gl_entries(\n conn, gl_entries,\n voucher_type=\"journal_entry\", voucher_id=je_id,\n posting_date=posting_date, company_id=company_id,\n remarks=remark, is_opening=is_opening,\n )\n conn.execute(\n \"UPDATE journal_entry SET status = 'submitted', updated_at = datetime('now') WHERE id = ?\",\n (je_id,),\n )\n je_status = \"submitted\"\n except (ValueError, Exception) as e:\n sys.stderr.write(f\"[erpclaw-journals] Auto-submit failed for {naming}: {e}\\n\")\n # JE remains as draft\n\n # Advance next_run_date\n current_next = date.fromisoformat(tmpl[\"next_run_date\"])\n new_next = _advance_date(current_next, tmpl[\"frequency\"])\n new_next_str = new_next.isoformat()\n\n # Check if end_date is reached\n new_status = \"active\"\n if tmpl[\"end_date\"] and new_next_str > tmpl[\"end_date\"]:\n new_status = \"completed\"\n\n conn.execute(\n \"\"\"UPDATE recurring_journal_template\n SET next_run_date = ?, last_generated_date = ?,\n status = ?, updated_at = datetime('now')\n WHERE id = ?\"\"\",\n (new_next_str, posting_date, new_status, template_id),\n )\n\n results.append({\n \"template_id\": template_id,\n \"template_name\": tmpl[\"name\"],\n \"journal_entry_id\": je_id,\n \"naming_series\": naming,\n \"posting_date\": posting_date,\n \"je_status\": je_status,\n \"next_run_date\": new_next_str if new_status == \"active\" else None,\n \"template_status\": new_status,\n })\n\n audit(conn, \"erpclaw-journals\", \"process-recurring\",\n \"recurring_journal_template\", company_id,\n new_values={\"generated\": len(results)})\n conn.commit()\n\n ok({\"generated\": len(results), \"results\": results})\n\n\n# ---------------------------------------------------------------------------\n# 16. delete-recurring-template\n# ---------------------------------------------------------------------------\n\ndef delete_recurring_template(conn, args):\n \"\"\"Delete a recurring template (soft delete: marks as completed).\"\"\"\n template_id = args.template_id\n if not template_id:\n err(\"--template-id is required\")\n\n q = Q.from_(_t_rjt).select(_t_rjt.star).where(_t_rjt.id == P())\n row = conn.execute(q.get_sql(), (template_id,)).fetchone()\n if not row:\n err(f\"Recurring template {template_id} not found\")\n\n q_del = Q.from_(_t_rjt).delete().where(_t_rjt.id == P())\n conn.execute(q_del.get_sql(), (template_id,))\n\n audit(conn, \"erpclaw-journals\", \"delete-recurring-template\",\n \"recurring_journal_template\", template_id)\n conn.commit()\n\n ok({\"status\": \"deleted\", \"deleted\": True})\n\n\n# ---------------------------------------------------------------------------\n# 17. status\n# ---------------------------------------------------------------------------\n\ndef status(conn, args):\n \"\"\"Show journal entry counts by status.\"\"\"\n company_id = resolve_company_id(conn, getattr(args, 'company_id', None))\n\n q_je = (Q.from_(_t_je)\n .select(_t_je.status, fn.Count(\"*\").as_(\"cnt\"))\n .where(_t_je.company_id == P())\n .groupby(_t_je.status))\n rows = conn.execute(q_je.get_sql(), (company_id,)).fetchall()\n\n counts = {\"total\": 0, \"draft\": 0, \"submitted\": 0, \"cancelled\": 0, \"amended\": 0}\n for row in rows:\n counts[row[\"status\"]] = row[\"cnt\"]\n counts[\"total\"] += row[\"cnt\"]\n\n # Recurring template counts\n q_rjt = (Q.from_(_t_rjt)\n .select(_t_rjt.status, fn.Count(\"*\").as_(\"cnt\"))\n .where(_t_rjt.company_id == P())\n .groupby(_t_rjt.status))\n tmpl_rows = conn.execute(q_rjt.get_sql(), (company_id,)).fetchall()\n recurring = {\"active\": 0, \"paused\": 0, \"completed\": 0}\n for r in tmpl_rows:\n recurring[r[\"status\"]] = r[\"cnt\"]\n counts[\"recurring_templates\"] = recurring\n\n ok(counts)\n\n\n# ---------------------------------------------------------------------------\n# Action dispatch\n# ---------------------------------------------------------------------------\n\nACTIONS = {\n \"add-journal-entry\": add_journal_entry,\n \"update-journal-entry\": update_journal_entry,\n \"get-journal-entry\": get_journal_entry,\n \"list-journal-entries\": list_journal_entries,\n \"submit-journal-entry\": submit_journal_entry,\n \"cancel-journal-entry\": cancel_journal_entry,\n \"amend-journal-entry\": amend_journal_entry,\n \"delete-journal-entry\": delete_journal_entry,\n \"duplicate-journal-entry\": duplicate_journal_entry,\n \"create-intercompany-je\": create_intercompany_je,\n \"add-recurring-template\": add_recurring_template,\n \"update-recurring-template\": update_recurring_template,\n \"list-recurring-templates\": list_recurring_templates,\n \"get-recurring-template\": get_recurring_template,\n \"process-recurring\": process_recurring,\n \"delete-recurring-template\": delete_recurring_template,\n \"status\": status,\n}\n\n\ndef main():\n parser = SafeArgumentParser(description=\"ERPClaw Journals Skill\")\n parser.add_argument(\"--action\", required=True, choices=sorted(ACTIONS.keys()))\n parser.add_argument(\"--db-path\", default=None)\n\n # Journal entry fields\n parser.add_argument(\"--journal-entry-id\")\n parser.add_argument(\"--company-id\")\n parser.add_argument(\"--posting-date\")\n parser.add_argument(\"--entry-type\")\n parser.add_argument(\"--remark\")\n parser.add_argument(\"--lines\")\n parser.add_argument(\"--amended-from\")\n\n # Intercompany fields\n parser.add_argument(\"--source-company-id\")\n parser.add_argument(\"--target-company-id\")\n parser.add_argument(\"--amount\")\n parser.add_argument(\"--description\")\n\n # Recurring template fields\n parser.add_argument(\"--template-id\")\n parser.add_argument(\"--template-name\")\n parser.add_argument(\"--frequency\")\n parser.add_argument(\"--start-date\")\n parser.add_argument(\"--end-date\")\n parser.add_argument(\"--auto-submit\", action=\"store_true\", default=None)\n parser.add_argument(\"--as-of-date\")\n parser.add_argument(\"--template-status\")\n\n # List filters\n parser.add_argument(\"--status\", dest=\"je_status\")\n parser.add_argument(\"--account-id\")\n parser.add_argument(\"--from-date\")\n parser.add_argument(\"--to-date\")\n parser.add_argument(\"--limit\", default=\"20\")\n parser.add_argument(\"--offset\", default=\"0\")\n\n args, unknown = parser.parse_known_args()\n check_unknown_args(parser, unknown)\n check_input_lengths(args)\n action_fn = ACTIONS[args.action]\n\n db_path = args.db_path or DEFAULT_DB_PATH\n ensure_db_exists(db_path)\n conn = get_connection(db_path)\n\n # Dependency check\n _dep = check_required_tables(conn, REQUIRED_TABLES)\n if _dep:\n _dep[\"suggestion\"] = \"clawhub install \" + \" \".join(_dep.get(\"missing_skills\", []))\n print(json.dumps(_dep, indent=2))\n conn.close()\n sys.exit(1)\n\n try:\n action_fn(conn, args)\n except Exception as e:\n conn.rollback()\n sys.stderr.write(f\"[erpclaw-journals] {e}\\n\")\n err(\"An unexpected error occurred\")\n finally:\n conn.close()\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":53753,"content_sha256":"ba85e70551a745eac26ecc5aba1d7ed3152a50e86e5487425c1cb91df080cc72"},{"filename":"scripts/erpclaw-os/__init__.py","content":"","content_type":"text/x-python; charset=utf-8","language":"python","size":0,"content_sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},{"filename":"scripts/erpclaw-os/dependency_resolver.py","content":"#!/usr/bin/env python3\n\"\"\"ERPClaw OS — Dependency Resolver\n\nTopological sort on module dependency graph from module_registry.json.\nResolves installation order, detects circular dependencies, and checks\nfor prefix collisions between modules.\n\"\"\"\nimport json\nimport os\nfrom collections import defaultdict\n\n\ndef load_registry(registry_path=None):\n \"\"\"Load module registry JSON.\n\n Args:\n registry_path: Path to module_registry.json. If None, uses default location.\n\n Returns:\n dict of {module_name: module_info}\n \"\"\"\n if registry_path is None:\n # Default: relative to erpclaw scripts directory\n registry_path = os.path.join(\n os.path.dirname(os.path.dirname(os.path.abspath(__file__))),\n \"module_registry.json\",\n )\n\n if not os.path.isfile(registry_path):\n return {}\n\n with open(registry_path, \"r\") as f:\n data = json.load(f)\n\n return data.get(\"modules\", {})\n\n\ndef resolve_install_order(modules, registry=None, registry_path=None):\n \"\"\"Resolve installation order via topological sort.\n\n Args:\n modules: list of module names to install\n registry: dict of {module_name: module_info} (optional, loaded from path if None)\n registry_path: path to module_registry.json\n\n Returns:\n dict with:\n - order: list of module names in install order\n - added_dependencies: modules added to satisfy dependencies\n - errors: list of error strings\n \"\"\"\n if registry is None:\n registry = load_registry(registry_path)\n\n modules_set = set(modules)\n errors = []\n\n # 1. Expand dependencies: add all required modules\n all_needed = set()\n added_deps = set()\n\n def expand(mod_name):\n if mod_name in all_needed:\n return\n all_needed.add(mod_name)\n if mod_name not in registry:\n errors.append(f\"Module '{mod_name}' not found in registry\")\n return\n for dep in registry[mod_name].get(\"requires\", []):\n if dep not in modules_set:\n added_deps.add(dep)\n expand(dep)\n\n for m in modules:\n expand(m)\n\n if errors:\n return {\"order\": [], \"added_dependencies\": list(added_deps), \"errors\": errors}\n\n # 2. Topological sort (Kahn's algorithm)\n # Build adjacency and in-degree\n in_degree = defaultdict(int)\n graph = defaultdict(list)\n\n for mod_name in all_needed:\n if mod_name not in in_degree:\n in_degree[mod_name] = 0\n for dep in registry.get(mod_name, {}).get(\"requires\", []):\n if dep in all_needed:\n graph[dep].append(mod_name)\n in_degree[mod_name] += 1\n\n # Start with modules that have no dependencies\n queue = sorted([m for m in all_needed if in_degree[m] == 0])\n order = []\n\n while queue:\n node = queue.pop(0)\n order.append(node)\n for neighbor in sorted(graph[node]):\n in_degree[neighbor] -= 1\n if in_degree[neighbor] == 0:\n queue.append(neighbor)\n\n if len(order) != len(all_needed):\n # Circular dependency detected\n remaining = all_needed - set(order)\n errors.append(f\"Circular dependency detected among: {sorted(remaining)}\")\n return {\"order\": order, \"added_dependencies\": list(added_deps), \"errors\": errors}\n\n return {\n \"order\": order,\n \"added_dependencies\": sorted(added_deps),\n \"errors\": [],\n }\n\n\ndef detect_circular_deps(modules, registry=None, registry_path=None):\n \"\"\"Detect circular dependencies in a set of modules.\n\n Args:\n modules: list of module names\n registry: dict of {module_name: module_info}\n registry_path: path to module_registry.json\n\n Returns:\n list of cycles found (each cycle is a list of module names).\n Empty list = no cycles.\n \"\"\"\n if registry is None:\n registry = load_registry(registry_path)\n\n # DFS-based cycle detection\n WHITE, GRAY, BLACK = 0, 1, 2\n color = {m: WHITE for m in modules if m in registry}\n cycles = []\n\n def dfs(node, path):\n color[node] = GRAY\n path.append(node)\n\n for dep in registry.get(node, {}).get(\"requires\", []):\n if dep not in color:\n continue\n if color[dep] == GRAY:\n # Found cycle\n cycle_start = path.index(dep)\n cycles.append(path[cycle_start:] + [dep])\n elif color[dep] == WHITE:\n dfs(dep, path)\n\n path.pop()\n color[node] = BLACK\n\n for m in modules:\n if m in color and color[m] == WHITE:\n dfs(m, [])\n\n return cycles\n\n\ndef detect_prefix_collisions(modules, registry=None, registry_path=None):\n \"\"\"Detect action prefix collisions between modules.\n\n Two modules with the same prefix would have overlapping action names.\n\n Args:\n modules: list of module names\n registry: dict of {module_name: module_info}\n registry_path: path to module_registry.json\n\n Returns:\n list of collision dicts: [{prefix, modules: [mod1, mod2]}]\n \"\"\"\n if registry is None:\n registry = load_registry(registry_path)\n\n # Derive prefix from module name\n prefix_map = defaultdict(list)\n for mod_name in modules:\n if mod_name not in registry:\n continue\n # Common prefix derivation: module name or first part\n # e.g., healthclaw → health, educlaw → educ, retailclaw → retail\n prefix = mod_name.replace(\"claw\", \"\").replace(\"-\", \"_\").split(\"_\")[0]\n if prefix:\n prefix_map[prefix].append(mod_name)\n\n collisions = []\n for prefix, mods in prefix_map.items():\n if len(mods) > 1:\n collisions.append({\n \"prefix\": prefix,\n \"modules\": mods,\n })\n\n return collisions\n","content_type":"text/x-python; charset=utf-8","language":"python","size":5880,"content_sha256":"4f3d67b54f10976500f493ba43038ddf9b966919b3b43e0faf134395c246b14c"},{"filename":"scripts/erpclaw-setup/assets/default_uom.json","content":"[\n {\"name\": \"Each\", \"abbr\": \"Ea\", \"must_be_whole_number\": 1},\n {\"name\": \"Numbers\", \"abbr\": \"Nos\", \"must_be_whole_number\": 1},\n {\"name\": \"Kilogram\", \"abbr\": \"Kg\", \"must_be_whole_number\": 0},\n {\"name\": \"Gram\", \"abbr\": \"g\", \"must_be_whole_number\": 0},\n {\"name\": \"Pound\", \"abbr\": \"Lb\", \"must_be_whole_number\": 0},\n {\"name\": \"Ounce\", \"abbr\": \"Oz\", \"must_be_whole_number\": 0},\n {\"name\": \"Litre\", \"abbr\": \"L\", \"must_be_whole_number\": 0},\n {\"name\": \"Gallon\", \"abbr\": \"Gal\", \"must_be_whole_number\": 0},\n {\"name\": \"Metre\", \"abbr\": \"m\", \"must_be_whole_number\": 0},\n {\"name\": \"Feet\", \"abbr\": \"Ft\", \"must_be_whole_number\": 0},\n {\"name\": \"Hour\", \"abbr\": \"Hr\", \"must_be_whole_number\": 0},\n {\"name\": \"Box\", \"abbr\": \"Box\", \"must_be_whole_number\": 1},\n {\"name\": \"Pack\", \"abbr\": \"Pk\", \"must_be_whole_number\": 1},\n {\"name\": \"Dozen\", \"abbr\": \"Dz\", \"must_be_whole_number\": 1}\n]\n","content_type":"application/json; charset=utf-8","language":"json","size":870,"content_sha256":"baca02db1311acee08226ec22dc2ec98f2306c30f7cfa6cd46840b0711b83259"},{"filename":"scripts/erpclaw-setup/lib/erpclaw_lib/__init__.py","content":"\"\"\"ERPClaw shared library — used by all ERPClaw skills.\"\"\"\n\n__version__ = \"0.1.0\"\n","content_type":"text/x-python; charset=utf-8","language":"python","size":84,"content_sha256":"897593a0d14868bc4b07bf1ccb30bbb7e3764980bc1ea7310611a80bcfe15de4"},{"filename":"scripts/erpclaw-setup/lib/erpclaw_lib/vendor/__init__.py","content":"# Vendored third-party packages for erpclaw_lib\n# Zero external dependencies — all packages are pure Python, copied from source.\n","content_type":"text/x-python; charset=utf-8","language":"python","size":131,"content_sha256":"5244b6603f40dbf1bf9d2f354d6bec3c1f208bae0d9c8e2f09382931dafc05ac"},{"filename":"UI.yaml","content":"ocui_version: '1.0'\nskill: erpclaw\nskill_version: 3.2.0\ndisplay_name: ERPClaw\nicon: briefcase\ncolor: '#2563eb'\ndomains:\n- key: company\n label: Company\n entities:\n - company\n- key: currencies\n label: Currencies\n entities:\n - currency\n - exchange_rate\n- key: defaults\n label: Defaults\n entities:\n - payment_terms\n - uom\n - uom_conversion\n- key: users\n label: Users & Security\n entities:\n - erp_user\n - role\n - audit_log\n- key: chart_of_accounts\n label: Chart of Accounts\n entities:\n - account\n- key: general_ledger\n label: General Ledger\n entities:\n - gl_entry\n- key: fiscal\n label: Fiscal Periods\n entities:\n - fiscal_year\n - period_closing_voucher\n- key: budgeting\n label: Budgeting\n entities:\n - cost_center\n - budget\n- key: system\n label: System\n entities:\n - naming_series\n- key: entry\n label: Entry\n entities:\n - payment_entry\n - payment_allocation\n - payment_deduction\n- key: terms\n label: Terms\n entities:\n - payment_terms\n- key: ledger_entry\n label: Ledger Entry\n entities:\n - payment_ledger_entry\n- key: template\n label: Template\n entities:\n - tax_template\n - tax_rule\n - item_tax_template\n- key: category\n label: Category\n entities:\n - tax_template\n - tax_rule\n - tax_category\n- key: withholding_category\n label: Withholding Category\n entities:\n - tax_withholding_category\n - tax_withholding_entry\n - tax_withholding_group\n- key: item_master\n label: Item Master\n entities:\n - item\n - item_group\n- key: warehousing\n label: Warehousing\n entities:\n - warehouse\n- key: stock_transactions\n label: Stock Transactions\n entities:\n - stock_entry\n - stock_reconciliation\n - stock_revaluation\n- key: tracking\n label: Tracking\n entities:\n - batch\n - serial_number\n- key: pricing\n label: Pricing\n entities:\n - price_list\n - item_price\n - pricing_rule\n- key: customers\n label: Customers\n entities:\n - customer\n - sales_partner\n- key: sales\n label: Sales\n entities:\n - quotation\n - sales_order\n- key: fulfillment\n label: Fulfillment\n entities:\n - delivery_note\n- key: sales_billing\n label: Sales Billing\n entities:\n - sales_invoice\n - credit_note\n- key: suppliers\n label: Suppliers\n entities:\n - supplier\n- key: purchasing\n label: Purchasing\n entities:\n - material_request\n - request_for_quotation\n - supplier_quotation\n - purchase_order\n- key: receiving\n label: Receiving\n entities:\n - purchase_receipt\n - landed_cost_voucher\n- key: purchase_billing\n label: Purchase Billing\n entities:\n - purchase_invoice\n- key: meter\n label: Meter\n entities:\n - meter\n - usage_event\n - billing_period\n- key: rate_plan\n label: Rate Plan\n entities:\n - rate_plan\n - rate_tier\n - prepaid_credit_balance\n- key: subscription_billing\n label: Subscription Billing\n entities:\n - recurring_invoice_template\n - billing_adjustment\ndashboard:\n kpis:\n - key: total_companies\n label: Companies\n type: count\n action: list-companies\n drill_action: list-companies\n - key: total_users\n label: Users\n type: count\n action: list-users\n drill_action: list-users\n - key: total_accounts\n label: Accounts\n type: count\n action: list-accounts\n drill_action: list-accounts\n - key: open_periods\n label: Open Periods\n type: count\n action: list-fiscal-years\n drill_action: list-fiscal-years\n - key: frozen_accounts\n label: Frozen Accounts\n type: count\n action: list-accounts\n filter:\n is-frozen: true\n severity: info\n drill_action: list-accounts\n drill_filter:\n is-frozen: true\n - key: total_journal_entry\n label: Journal Entries\n type: count\n action: list-journal-entries\n drill_action: list-journal-entries\n - key: draft_journal_entry\n label: Draft Journal Entries\n type: count\n action: list-journal-entries\n filter:\n status: draft\n severity: warning\n drill_action: list-journal-entries\n drill_filter:\n status: draft\n - key: total_payments\n label: Payments\n type: count\n action: list-payments\n drill_action: list-payments\n - key: draft_payments\n label: Draft Payments\n type: count\n action: list-payments\n filter:\n status: draft\n severity: warning\n drill_action: list-payments\n drill_filter:\n status: draft\n - key: submitted_payments\n label: Submitted Payments\n type: count\n action: list-payments\n filter:\n status: submitted\n drill_action: list-payments\n drill_filter:\n status: submitted\n - key: unreconciled_entries\n label: Unreconciled Entries\n type: count\n action: get-unallocated-payments\n severity: warning\n drill_action: get-unallocated-payments\n - key: total_tax_template\n label: Tax Templates\n type: count\n action: list-tax-templates\n drill_action: list-tax-templates\n - key: total_tax_rule\n label: Tax Rules\n type: count\n action: list-tax-rules\n drill_action: list-tax-rules\n - key: total_tax_category\n label: Tax Categories\n type: count\n action: list-tax-categories\n drill_action: list-tax-categories\n - key: total_items\n label: Items\n type: count\n action: list-items\n drill_action: list-items\n - key: total_warehouses\n label: Warehouses\n type: count\n action: list-warehouses\n drill_action: list-warehouses\n - key: draft_stock_entries\n label: Draft Stock Entries\n type: count\n action: list-stock-entries\n filter:\n status: draft\n severity: warning\n drill_action: list-stock-entries\n drill_filter:\n status: draft\n - key: total_customers\n label: Customers\n type: count\n action: list-customers\n drill_action: list-customers\n - key: draft_quotations\n label: Draft Quotations\n type: count\n action: list-quotations\n filter:\n status: draft\n severity: warning\n drill_action: list-quotations\n drill_filter:\n status: draft\n - key: open_sales_orders\n label: Open Sales Orders\n type: count\n action: list-sales-orders\n filter:\n status: draft\n severity: warning\n drill_action: list-sales-orders\n drill_filter:\n status: draft\n - key: unpaid_invoices\n label: Unpaid Invoices\n type: count\n action: list-sales-invoices\n filter:\n status: unpaid\n severity: warning\n drill_action: list-sales-invoices\n drill_filter:\n status: unpaid\n - key: total_suppliers\n label: Suppliers\n type: count\n action: list-suppliers\n drill_action: list-suppliers\n - key: draft_purchase_orders\n label: Draft Purchase Orders\n type: count\n action: list-purchase-orders\n filter:\n status: draft\n severity: warning\n drill_action: list-purchase-orders\n drill_filter:\n status: draft\n - key: pending_receipts\n label: Pending Receipts\n type: count\n action: list-purchase-receipts\n filter:\n status: draft\n severity: warning\n drill_action: list-purchase-receipts\n drill_filter:\n status: draft\n - key: unpaid_invoices\n label: Unpaid Invoices\n type: count\n action: list-purchase-invoices\n filter:\n status: unpaid\n severity: warning\n drill_action: list-purchase-invoices\n drill_filter:\n status: unpaid\n - key: total_meter\n label: Meters\n type: count\n action: list-meters\n drill_action: list-meters\n - key: draft_meter\n label: Draft Meters\n type: count\n action: list-meters\n filter:\n status: draft\n severity: warning\n drill_action: list-meters\n drill_filter:\n status: draft\n - key: total_rate_plan\n label: Rate Plans\n type: count\n action: list-rate-plans\n drill_action: list-rate-plans\n - key: total_billing_period\n label: Billing Periods\n type: count\n action: list-billing-periods\n drill_action: list-billing-periods\n quick_actions:\n - action: setup-company\n label: Setup Company\n - action: add-account\n label: New Account\n - action: add-fiscal-year\n label: New Fiscal Year\n - action: add-cost-center\n label: New Cost Center\n - action: check-gl-integrity\n label: Check GL Integrity\n - action: add-journal-entry\n label: New Journal Entry\n - action: add-payment\n label: New Payment Entry\n - action: reconcile-payments\n label: Reconcile Payments\n - action: bank-reconciliation\n label: Bank Reconciliation\n - action: add-tax-template\n label: New Tax Template\n - action: add-tax-rule\n label: New Tax Rule\n - action: add-tax-category\n label: New Tax Category\n - action: add-tax-withholding-category\n label: New Tax Withholding Category\n - action: add-item\n label: New Item\n - action: add-warehouse\n label: New Warehouse\n - action: add-stock-entry\n label: New Stock Entry\n - action: add-customer\n label: New Customer\n - action: add-quotation\n label: New Quotation\n - action: add-sales-order\n label: New Sales Order\n - action: add-supplier\n label: New Supplier\n - action: add-purchase-order\n label: New Purchase Order\n - action: add-meter\n label: New Meter\n - action: add-usage-event\n label: New Usage Event\n - action: add-rate-plan\n label: New Rate Plan\nentities:\n company:\n label: Company\n label_plural: Companies\n icon: building\n primary_field: name\n secondary_field: abbr\n identifier_field: id\n fields:\n name:\n type: text\n label: Company Name\n required: true\n max_length: 140\n searchable: true\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 1\n abbr:\n type: text\n label: Abbreviation\n max_length: 10\n help_text: Auto-generated from initials if left blank\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 2\n default_currency:\n type: currency_code\n label: Base Currency\n required: true\n default: USD\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 3\n country:\n type: text\n label: Country\n default: United States\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 4\n fiscal_year_start_month:\n type: integer\n label: Fiscal Year Start Month\n default: 1\n min: 1\n max: 12\n help_text: 1 = January, 4 = April, 7 = July, etc.\n in_form_view: true\n form_group: basic\n form_order: 5\n tax_id:\n type: text\n label: Tax ID / EIN\n max_length: 20\n in_form_view: true\n form_group: basic\n form_order: 6\n default_receivable_account_id:\n type: link\n label: Default Receivable Account\n link_entity: account\n link_display_field: name\n link_search_action: list-accounts\n in_form_view: true\n form_group: default_accounts\n form_order: 1\n default_payable_account_id:\n type: link\n label: Default Payable Account\n link_entity: account\n link_display_field: name\n link_search_action: list-accounts\n in_form_view: true\n form_group: default_accounts\n form_order: 2\n default_income_account_id:\n type: link\n label: Default Income Account\n link_entity: account\n link_display_field: name\n link_search_action: list-accounts\n in_form_view: true\n form_group: default_accounts\n form_order: 3\n default_expense_account_id:\n type: link\n label: Default Expense Account\n link_entity: account\n link_display_field: name\n link_search_action: list-accounts\n in_form_view: true\n form_group: default_accounts\n form_order: 4\n default_bank_account_id:\n type: link\n label: Default Bank Account\n link_entity: account\n link_display_field: name\n link_search_action: list-accounts\n in_form_view: true\n form_group: default_accounts\n form_order: 5\n default_cash_account_id:\n type: link\n label: Default Cash Account\n link_entity: account\n link_display_field: name\n link_search_action: list-accounts\n in_form_view: true\n form_group: default_accounts\n form_order: 6\n default_cost_center_id:\n type: link\n label: Default Cost Center\n link_entity: cost_center\n link_display_field: name\n link_search_action: list-cost-centers\n in_form_view: true\n form_group: default_accounts\n form_order: 7\n default_warehouse_id:\n type: link\n label: Default Warehouse\n link_entity: warehouse\n link_display_field: name\n link_search_action: list-warehouses\n in_form_view: true\n form_group: default_accounts\n form_order: 8\n round_off_account_id:\n type: link\n label: Round-Off Account\n link_entity: account\n link_display_field: name\n link_search_action: list-accounts\n in_form_view: true\n form_group: advanced\n form_order: 1\n exchange_gain_loss_account_id:\n type: link\n label: Exchange Gain/Loss Account\n link_entity: account\n link_display_field: name\n link_search_action: list-accounts\n in_form_view: true\n form_group: advanced\n form_order: 2\n perpetual_inventory:\n type: boolean\n label: Perpetual Inventory\n default: false\n help_text: Auto-post stock value changes to GL\n in_form_view: true\n form_group: advanced\n form_order: 3\n enable_negative_stock:\n type: boolean\n label: Allow Negative Stock\n default: false\n in_form_view: true\n form_group: advanced\n form_order: 4\n accounts_frozen_till_date:\n type: date\n label: Accounts Frozen Till\n help_text: No GL entries allowed before this date\n in_form_view: true\n form_group: advanced\n form_order: 5\n created_at:\n type: datetime\n label: Created\n read_only: true\n in_detail_view: true\n updated_at:\n type: datetime\n label: Last Updated\n read_only: true\n in_detail_view: true\n form_groups:\n basic:\n label: Company Information\n order: 1\n columns: 2\n default_accounts:\n label: Default Accounts\n order: 2\n columns: 2\n collapsible: true\n advanced:\n label: Advanced Settings\n order: 3\n columns: 2\n collapsible: true\n views:\n list:\n columns:\n - field: name\n width: 220\n link: true\n - field: abbr\n width: 80\n - field: default_currency\n width: 100\n - field: country\n width: 160\n row_click: get-company\n detail:\n header:\n title_field: name\n subtitle_field: country\n sections:\n - label: Company Information\n fields:\n - name\n - abbr\n - default_currency\n - country\n - fiscal_year_start_month\n - tax_id\n columns: 3\n - label: Default Accounts\n fields:\n - default_receivable_account_id\n - default_payable_account_id\n - default_income_account_id\n - default_expense_account_id\n - default_bank_account_id\n - default_cash_account_id\n - default_cost_center_id\n - default_warehouse_id\n columns: 2\n collapsible: true\n - label: Advanced\n fields:\n - round_off_account_id\n - exchange_gain_loss_account_id\n - perpetual_inventory\n - enable_negative_stock\n - accounts_frozen_till_date\n columns: 2\n collapsible: true\n actions:\n - action: update-company\n label: Edit\n - action: seed-defaults\n label: Seed Defaults\n currency:\n label: Currency\n label_plural: Currencies\n icon: dollar-sign\n primary_field: name\n secondary_field: code\n identifier_field: code\n fields:\n code:\n type: currency_code\n label: Currency Code\n required: true\n max_length: 3\n help_text: ISO 4217 code (e.g., USD, EUR, GBP)\n searchable: true\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 1\n name:\n type: text\n label: Currency Name\n required: true\n max_length: 80\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 2\n symbol:\n type: text\n label: Symbol\n max_length: 5\n placeholder: $\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 3\n decimal_places:\n type: integer\n label: Decimal Places\n default: 2\n min: 0\n max: 6\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 4\n enabled:\n type: boolean\n label: Enabled\n default: false\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 5\n form_groups:\n basic:\n label: Currency Details\n order: 1\n columns: 2\n views:\n list:\n columns:\n - field: code\n width: 80\n link: true\n - field: name\n width: 200\n - field: symbol\n width: 60\n - field: decimal_places\n width: 80\n align: right\n - field: enabled\n width: 80\n filters:\n - field: enabled\n type: select\n exchange_rate:\n label: Exchange Rate\n label_plural: Exchange Rates\n icon: trending-up\n primary_field: from_currency\n secondary_field: to_currency\n identifier_field: id\n fields:\n from_currency:\n type: currency_code\n label: From Currency\n required: true\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 1\n to_currency:\n type: currency_code\n label: To Currency\n required: true\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 2\n rate:\n type: decimal\n label: Exchange Rate\n required: true\n precision: 6\n min: '0.000001'\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 3\n effective_date:\n type: date\n label: Effective Date\n required: true\n default: today\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 4\n source:\n type: select\n label: Source\n options:\n - value: manual\n label: Manual\n - value: api\n label: API\n - value: bank_feed\n label: Bank Feed\n default: manual\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 5\n form_groups:\n basic:\n label: Rate Details\n order: 1\n columns: 2\n views:\n list:\n columns:\n - field: from_currency\n width: 100\n - field: to_currency\n width: 100\n - field: rate\n width: 130\n align: right\n - field: effective_date\n width: 120\n - field: source\n width: 100\n filters:\n - field: from_currency\n type: text\n - field: to_currency\n type: text\n - field: effective_date\n type: date_range\n payment_terms:\n label: Payment Terms\n label_plural: Payment Terms\n table: payment_terms\n id_col: id\n name_col: name\n primary_field: name\n identifier_field: id\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n name:\n type: text\n label: Name\n required: true\n in_list_view: true\n in_form_view: true\n form_group: header\n form_order: 1\n due_days:\n type: integer\n label: Due Days\n in_form_view: true\n form_group: details\n form_order: 2\n discount_percentage:\n type: currency\n label: Discount Percentage\n precision: 2\n in_form_view: true\n form_group: details\n form_order: 3\n discount_days:\n type: integer\n label: Discount Days\n in_form_view: true\n form_group: details\n form_order: 4\n description:\n type: textarea\n label: Description\n in_form_view: true\n form_group: notes\n form_order: 5\n form_groups:\n header:\n label: Basic Information\n order: 1\n columns: 2\n details:\n label: Details\n order: 2\n columns: 2\n notes:\n label: Notes\n order: 3\n columns: 1\n uom:\n label: Unit of Measure\n label_plural: Units of Measure\n icon: ruler\n primary_field: name\n identifier_field: id\n fields:\n name:\n type: text\n label: UoM Name\n required: true\n max_length: 80\n placeholder: e.g., Kilogram, Piece, Dozen\n searchable: true\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 1\n must_be_whole_number:\n type: boolean\n label: Must Be Whole Number\n default: false\n help_text: Check for discrete units like Pieces, Boxes\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 2\n form_groups:\n basic:\n label: Unit of Measure\n order: 1\n columns: 2\n views:\n list:\n columns:\n - field: name\n width: 200\n link: true\n - field: must_be_whole_number\n width: 160\n uom_conversion:\n label: UoM Conversion\n label_plural: UoM Conversions\n icon: repeat\n primary_field: from_uom\n secondary_field: to_uom\n identifier_field: id\n fields:\n from_uom:\n type: text\n label: From UoM\n required: true\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 1\n to_uom:\n type: text\n label: To UoM\n required: true\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 2\n conversion_factor:\n type: decimal\n label: Conversion Factor\n required: true\n precision: 6\n min: '0.000001'\n help_text: 1 From UoM = X To UoM\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 3\n item_id:\n type: link\n label: Item (optional)\n link_entity: item\n link_display_field: item_name\n link_search_action: list-items\n help_text: Leave blank for universal conversion, or select an item for item-specific conversion\n in_form_view: true\n form_group: basic\n form_order: 4\n form_groups:\n basic:\n label: Conversion Details\n order: 1\n columns: 2\n views:\n list:\n columns:\n - field: from_uom\n width: 140\n - field: to_uom\n width: 140\n - field: conversion_factor\n width: 140\n align: right\n erp_user:\n label: User\n label_plural: Users\n icon: user\n primary_field: username\n secondary_field: email\n identifier_field: id\n status_field: status\n status_colors:\n active: green\n disabled: gray\n locked: red\n fields:\n username:\n type: text\n label: Username\n required: true\n max_length: 80\n searchable: true\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 1\n email:\n type: email\n label: Email\n max_length: 254\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 2\n full_name:\n type: text\n label: Full Name\n max_length: 140\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 3\n status:\n type: status\n label: Status\n read_only: true\n in_list_view: true\n in_detail_view: true\n company_ids:\n type: json\n label: Company Access\n help_text: JSON array of company IDs this user can access\n in_form_view: true\n form_group: access\n form_order: 1\n created_at:\n type: datetime\n label: Created\n read_only: true\n in_detail_view: true\n form_groups:\n basic:\n label: User Information\n order: 1\n columns: 2\n access:\n label: Access Control\n order: 2\n columns: 1\n collapsible: true\n views:\n list:\n columns:\n - field: username\n width: 160\n link: true\n - field: full_name\n width: 180\n - field: email\n width: 200\n - field: status\n width: 100\n filters:\n - field: status\n type: select\n row_click: get-user\n detail:\n header:\n title_field: username\n subtitle_field: full_name\n status_field: status\n sections:\n - label: User Details\n fields:\n - username\n - email\n - full_name\n - status\n columns: 2\n - label: Access\n fields:\n - company_ids\n columns: 1\n actions:\n - action: update-user\n label: Edit\n - action: assign-role\n label: Assign Role\n - action: set-password\n label: Set Password\n role:\n label: Role\n label_plural: Roles\n icon: shield\n primary_field: name\n identifier_field: id\n fields:\n name:\n type: text\n label: Role Name\n required: true\n max_length: 80\n searchable: true\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 1\n description:\n type: textarea\n label: Description\n max_length: 500\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 2\n is_system:\n type: boolean\n label: System Role\n read_only: true\n in_list_view: true\n in_detail_view: true\n user_count:\n type: integer\n label: Users\n read_only: true\n in_list_view: true\n in_detail_view: true\n form_groups:\n basic:\n label: Role Details\n order: 1\n columns: 1\n views:\n list:\n columns:\n - field: name\n width: 180\n link: true\n - field: description\n width: 260\n - field: is_system\n width: 100\n - field: user_count\n width: 80\n align: right\n audit_log:\n label: Audit Log\n label_plural: Audit Log\n icon: file-text\n primary_field: action\n secondary_field: entity_type\n identifier_field: id\n fields:\n timestamp:\n type: datetime\n label: Timestamp\n read_only: true\n in_list_view: true\n action:\n type: text\n label: Action\n read_only: true\n in_list_view: true\n entity_type:\n type: text\n label: Entity Type\n read_only: true\n in_list_view: true\n entity_id:\n type: text\n label: Entity ID\n read_only: true\n user_id:\n type: text\n label: User\n read_only: true\n in_list_view: true\n description:\n type: text\n label: Description\n read_only: true\n in_list_view: true\n old_values:\n type: json\n label: Previous Values\n read_only: true\n in_detail_view: true\n new_values:\n type: json\n label: New Values\n read_only: true\n in_detail_view: true\n views:\n list:\n columns:\n - field: timestamp\n width: 160\n - field: action\n width: 120\n - field: entity_type\n width: 120\n - field: description\n width: 260\n - field: user_id\n width: 140\n filters:\n - field: entity_type\n type: text\n - field: action\n type: text\n - field: timestamp\n type: date_range\n account:\n label: Account\n label_plural: Accounts\n icon: file-text\n primary_field: name\n secondary_field: account_number\n identifier_field: id\n status_field: is_frozen\n status_colors:\n false: green\n true: red\n fields:\n id:\n type: text\n label: ID\n read_only: true\n in_detail_view: true\n name:\n type: text\n label: Account Name\n required: true\n max_length: 140\n searchable: true\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 1\n account_number:\n type: text\n label: Account Number\n max_length: 20\n searchable: true\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 2\n root_type:\n type: select\n label: Root Type\n required: true\n options:\n - value: asset\n label: Asset\n - value: liability\n label: Liability\n - value: equity\n label: Equity\n - value: income\n label: Income\n - value: expense\n label: Expense\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 3\n account_type:\n type: select\n label: Account Type\n options:\n - value: receivable\n label: Receivable\n - value: payable\n label: Payable\n - value: bank\n label: Bank\n - value: cash\n label: Cash\n - value: stock\n label: Stock\n - value: tax\n label: Tax\n - value: fixed_asset\n label: Fixed Asset\n - value: depreciation\n label: Depreciation\n - value: accumulated_depreciation\n label: Accumulated Depreciation\n - value: cost_of_goods_sold\n label: Cost of Goods Sold\n - value: income_account\n label: Income Account\n - value: expense_account\n label: Expense Account\n - value: equity\n label: Equity\n - value: retained_earnings\n label: Retained Earnings\n - value: round_off\n label: Round Off\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 4\n parent_id:\n type: link\n label: Parent Account\n link_entity: account\n link_display_field: name\n link_search_action: list-accounts\n in_form_view: true\n form_group: hierarchy\n form_order: 1\n is_group:\n type: boolean\n label: Is Group\n default: false\n help_text: Group accounts cannot receive GL postings directly\n in_list_view: true\n in_form_view: true\n form_group: hierarchy\n form_order: 2\n depth:\n type: integer\n label: Tree Depth\n read_only: true\n in_detail_view: true\n currency:\n type: currency_code\n label: Currency\n default: USD\n max_length: 3\n in_form_view: true\n form_group: basic\n form_order: 5\n balance_direction:\n type: select\n label: Balance Direction\n read_only: true\n options:\n - value: debit_normal\n label: Debit Normal\n - value: credit_normal\n label: Credit Normal\n in_detail_view: true\n is_frozen:\n type: boolean\n label: Frozen\n default: false\n read_only: true\n in_list_view: true\n in_detail_view: true\n disabled:\n type: boolean\n label: Disabled\n default: false\n read_only: true\n in_detail_view: true\n company_id:\n type: link\n label: Company\n required: true\n link_entity: company\n link_display_field: name\n link_search_action: list-companies\n in_form_view: true\n form_group: basic\n form_order: 6\n balance:\n type: currency\n label: Balance\n precision: 2\n read_only: true\n in_detail_view: true\n debit_total:\n type: currency\n label: Total Debits\n precision: 2\n read_only: true\n in_detail_view: true\n credit_total:\n type: currency\n label: Total Credits\n precision: 2\n read_only: true\n in_detail_view: true\n created_at:\n type: datetime\n label: Created\n read_only: true\n in_detail_view: true\n updated_at:\n type: datetime\n label: Last Updated\n read_only: true\n in_detail_view: true\n form_groups:\n basic:\n label: Account Information\n order: 1\n columns: 2\n hierarchy:\n label: Hierarchy\n order: 2\n columns: 2\n collapsible: true\n views:\n list:\n columns:\n - field: account_number\n width: 100\n link: true\n - field: name\n width: 220\n link: true\n - field: root_type\n width: 100\n - field: account_type\n width: 140\n - field: is_group\n width: 80\n - field: is_frozen\n width: 80\n filters:\n - field: root_type\n type: select\n - field: account_type\n type: select\n - field: is_group\n type: select\n row_click: get-account\n detail:\n header:\n title_field: name\n subtitle_field: account_number\n status_field: is_frozen\n sections:\n - label: Account Information\n fields:\n - name\n - account_number\n - root_type\n - account_type\n - currency\n - balance_direction\n - company_id\n columns: 3\n - label: Hierarchy\n fields:\n - parent_id\n - is_group\n - depth\n columns: 3\n - label: Balance\n fields:\n - balance\n - debit_total\n - credit_total\n columns: 3\n - label: Status\n fields:\n - is_frozen\n - disabled\n - created_at\n - updated_at\n columns: 2\n collapsible: true\n actions:\n - action: update-account\n label: Edit\n - action: freeze-account\n label: Freeze\n destructive: true\n - action: unfreeze-account\n label: Unfreeze\n - action: get-account-balance\n label: Check Balance\n gl_entry:\n label: GL Entry\n label_plural: GL Entries\n icon: list\n primary_field: voucher_type\n secondary_field: posting_date\n identifier_field: id\n fields:\n id:\n type: text\n label: ID\n read_only: true\n in_detail_view: true\n posting_date:\n type: date\n label: Posting Date\n required: true\n in_list_view: true\n in_detail_view: true\n account_id:\n type: link\n label: Account\n required: true\n link_entity: account\n link_display_field: name\n link_search_action: list-accounts\n in_list_view: true\n in_detail_view: true\n account_name:\n type: text\n label: Account Name\n read_only: true\n in_list_view: true\n debit:\n type: currency\n label: Debit\n precision: 2\n required: true\n default: '0'\n in_list_view: true\n in_detail_view: true\n credit:\n type: currency\n label: Credit\n precision: 2\n required: true\n default: '0'\n in_list_view: true\n in_detail_view: true\n debit_base:\n type: currency\n label: Debit (Base)\n precision: 2\n read_only: true\n in_detail_view: true\n credit_base:\n type: currency\n label: Credit (Base)\n precision: 2\n read_only: true\n in_detail_view: true\n currency:\n type: currency_code\n label: Currency\n read_only: true\n in_detail_view: true\n exchange_rate:\n type: decimal\n label: Exchange Rate\n precision: 6\n read_only: true\n in_detail_view: true\n voucher_type:\n type: text\n label: Voucher Type\n required: true\n in_list_view: true\n in_detail_view: true\n voucher_id:\n type: text\n label: Voucher ID\n required: true\n in_list_view: true\n in_detail_view: true\n party_type:\n type: select\n label: Party Type\n options:\n - value: customer\n label: Customer\n - value: supplier\n label: Supplier\n - value: employee\n label: Employee\n in_detail_view: true\n party_id:\n type: text\n label: Party ID\n in_detail_view: true\n cost_center_id:\n type: link\n label: Cost Center\n link_entity: cost_center\n link_display_field: name\n link_search_action: list-cost-centers\n in_detail_view: true\n fiscal_year:\n type: text\n label: Fiscal Year\n read_only: true\n in_detail_view: true\n remarks:\n type: text\n label: Remarks\n read_only: true\n in_list_view: true\n in_detail_view: true\n is_cancelled:\n type: boolean\n label: Cancelled\n default: false\n read_only: true\n in_list_view: true\n in_detail_view: true\n is_opening:\n type: boolean\n label: Opening Entry\n default: false\n read_only: true\n in_detail_view: true\n gl_checksum:\n type: text\n label: GL Checksum\n read_only: true\n in_detail_view: true\n created_at:\n type: datetime\n label: Created\n read_only: true\n in_detail_view: true\n form_groups: {}\n views:\n list:\n columns:\n - field: posting_date\n width: 110\n - field: account_name\n width: 200\n - field: debit\n width: 120\n align: right\n - field: credit\n width: 120\n align: right\n - field: voucher_type\n width: 130\n - field: voucher_id\n width: 140\n - field: remarks\n width: 180\n - field: is_cancelled\n width: 80\n filters:\n - field: voucher_type\n type: text\n - field: posting_date\n type: date_range\n - field: is_cancelled\n type: select\n - field: account_id\n type: link\n link_entity: account\n link_search_action: list-accounts\n detail:\n header:\n title_field: voucher_type\n subtitle_field: posting_date\n sections:\n - label: Entry Details\n fields:\n - posting_date\n - account_id\n - account_name\n - voucher_type\n - voucher_id\n - fiscal_year\n columns: 3\n - label: Amounts\n fields:\n - debit\n - credit\n - debit_base\n - credit_base\n - currency\n - exchange_rate\n columns: 3\n - label: Party\n fields:\n - party_type\n - party_id\n - cost_center_id\n columns: 3\n collapsible: true\n - label: Metadata\n fields:\n - is_cancelled\n - is_opening\n - remarks\n - gl_checksum\n - created_at\n columns: 2\n collapsible: true\n fiscal_year:\n label: Fiscal Year\n label_plural: Fiscal Years\n icon: calendar\n primary_field: name\n secondary_field: start_date\n identifier_field: id\n status_field: is_closed\n status_colors:\n false: green\n true: gray\n fields:\n id:\n type: text\n label: ID\n read_only: true\n in_detail_view: true\n name:\n type: text\n label: Name\n required: true\n max_length: 140\n placeholder: e.g., FY 2026\n searchable: true\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 1\n start_date:\n type: date\n label: Start Date\n required: true\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 2\n end_date:\n type: date\n label: End Date\n required: true\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 3\n is_closed:\n type: boolean\n label: Closed\n default: false\n read_only: true\n in_list_view: true\n in_detail_view: true\n company_id:\n type: link\n label: Company\n required: true\n link_entity: company\n link_display_field: name\n link_search_action: list-companies\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 4\n created_at:\n type: datetime\n label: Created\n read_only: true\n in_detail_view: true\n updated_at:\n type: datetime\n label: Last Updated\n read_only: true\n in_detail_view: true\n form_groups:\n basic:\n label: Fiscal Year Details\n order: 1\n columns: 2\n views:\n list:\n columns:\n - field: name\n width: 180\n link: true\n - field: start_date\n width: 120\n - field: end_date\n width: 120\n - field: is_closed\n width: 80\n - field: company_id\n width: 160\n filters:\n - field: is_closed\n type: select\n row_click: get-fiscal-year\n detail:\n header:\n title_field: name\n subtitle_field: company_id\n status_field: is_closed\n sections:\n - label: Fiscal Year Details\n fields:\n - name\n - start_date\n - end_date\n - is_closed\n - company_id\n columns: 3\n - label: Metadata\n fields:\n - created_at\n - updated_at\n columns: 2\n collapsible: true\n actions:\n - action: validate-period-close\n label: Validate for Closing\n - action: close-fiscal-year\n label: Close Year\n destructive: true\n - action: reopen-fiscal-year\n label: Reopen Year\n destructive: true\n cost_center:\n label: Cost Center\n label_plural: Cost Centers\n icon: target\n primary_field: name\n identifier_field: id\n fields:\n id:\n type: text\n label: ID\n read_only: true\n in_detail_view: true\n name:\n type: text\n label: Name\n required: true\n max_length: 140\n searchable: true\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 1\n parent_id:\n type: link\n label: Parent Cost Center\n link_entity: cost_center\n link_display_field: name\n link_search_action: list-cost-centers\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 2\n is_group:\n type: boolean\n label: Is Group\n default: false\n help_text: Group cost centers contain sub-cost-centers\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 3\n company_id:\n type: link\n label: Company\n required: true\n link_entity: company\n link_display_field: name\n link_search_action: list-companies\n in_form_view: true\n form_group: basic\n form_order: 4\n created_at:\n type: datetime\n label: Created\n read_only: true\n in_detail_view: true\n updated_at:\n type: datetime\n label: Last Updated\n read_only: true\n in_detail_view: true\n form_groups:\n basic:\n label: Cost Center Details\n order: 1\n columns: 2\n views:\n list:\n columns:\n - field: name\n width: 220\n link: true\n - field: parent_id\n width: 180\n - field: is_group\n width: 80\n filters:\n - field: is_group\n type: select\n - field: parent_id\n type: link\n link_entity: cost_center\n link_search_action: list-cost-centers\n detail:\n header:\n title_field: name\n sections:\n - label: Cost Center Details\n fields:\n - name\n - parent_id\n - is_group\n - company_id\n columns: 2\n - label: Metadata\n fields:\n - created_at\n - updated_at\n columns: 2\n collapsible: true\n budget:\n label: Budget\n label_plural: Budgets\n icon: pie-chart\n primary_field: budget_amount\n identifier_field: id\n fields:\n id:\n type: text\n label: ID\n read_only: true\n in_detail_view: true\n fiscal_year_id:\n type: link\n label: Fiscal Year\n required: true\n link_entity: fiscal_year\n link_display_field: name\n link_search_action: list-fiscal-years\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 1\n account_id:\n type: link\n label: Account\n link_entity: account\n link_display_field: name\n link_search_action: list-accounts\n help_text: At least one of Account or Cost Center is required\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 2\n cost_center_id:\n type: link\n label: Cost Center\n link_entity: cost_center\n link_display_field: name\n link_search_action: list-cost-centers\n help_text: At least one of Account or Cost Center is required\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 3\n budget_amount:\n type: currency\n label: Budget Amount\n precision: 2\n required: true\n min: '0'\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 4\n actual_amount:\n type: currency\n label: Actual Amount\n precision: 2\n read_only: true\n in_list_view: true\n in_detail_view: true\n variance:\n type: currency\n label: Variance\n precision: 2\n read_only: true\n in_list_view: true\n in_detail_view: true\n action_if_exceeded:\n type: select\n label: Action If Exceeded\n options:\n - value: warn\n label: Warn\n - value: stop\n label: Stop\n default: warn\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 5\n company_id:\n type: link\n label: Company\n read_only: true\n link_entity: company\n link_display_field: name\n link_search_action: list-companies\n in_detail_view: true\n created_at:\n type: datetime\n label: Created\n read_only: true\n in_detail_view: true\n updated_at:\n type: datetime\n label: Last Updated\n read_only: true\n in_detail_view: true\n form_groups:\n basic:\n label: Budget Details\n order: 1\n columns: 2\n views:\n list:\n columns:\n - field: fiscal_year_id\n width: 140\n - field: account_id\n width: 180\n - field: cost_center_id\n width: 160\n - field: budget_amount\n width: 130\n align: right\n - field: actual_amount\n width: 130\n align: right\n - field: variance\n width: 130\n align: right\n - field: action_if_exceeded\n width: 100\n filters:\n - field: fiscal_year_id\n type: link\n link_entity: fiscal_year\n link_search_action: list-fiscal-years\n - field: action_if_exceeded\n type: select\n detail:\n header:\n title_field: budget_amount\n subtitle_field: fiscal_year_id\n sections:\n - label: Budget Details\n fields:\n - fiscal_year_id\n - account_id\n - cost_center_id\n - budget_amount\n - action_if_exceeded\n - company_id\n columns: 2\n - label: Actuals\n fields:\n - actual_amount\n - variance\n columns: 2\n - label: Metadata\n fields:\n - created_at\n - updated_at\n columns: 2\n collapsible: true\n naming_series:\n label: Naming Series\n label_plural: Naming Series\n icon: hash\n primary_field: entity_type\n secondary_field: prefix\n identifier_field: id\n fields:\n id:\n type: text\n label: ID\n read_only: true\n in_detail_view: true\n entity_type:\n type: text\n label: Entity Type\n required: true\n in_list_view: true\n in_detail_view: true\n prefix:\n type: text\n label: Prefix\n required: true\n help_text: e.g., INV-2026-, JV-2026-\n in_list_view: true\n in_detail_view: true\n current_value:\n type: integer\n label: Current Value\n read_only: true\n in_list_view: true\n in_detail_view: true\n company_id:\n type: link\n label: Company\n required: true\n link_entity: company\n link_display_field: name\n link_search_action: list-companies\n in_detail_view: true\n created_at:\n type: datetime\n label: Created\n read_only: true\n in_detail_view: true\n form_groups: {}\n views:\n list:\n columns:\n - field: entity_type\n width: 160\n - field: prefix\n width: 160\n - field: current_value\n width: 120\n align: right\n filters:\n - field: entity_type\n type: text\n period_closing_voucher:\n label: Period Closing Voucher\n label_plural: Period Closing Vouchers\n icon: lock\n primary_field: fiscal_year_id\n secondary_field: posting_date\n identifier_field: id\n status_field: status\n status_colors:\n submitted: green\n cancelled: red\n fields:\n id:\n type: text\n label: ID\n read_only: true\n in_detail_view: true\n fiscal_year_id:\n type: link\n label: Fiscal Year\n required: true\n link_entity: fiscal_year\n link_display_field: name\n link_search_action: list-fiscal-years\n in_list_view: true\n in_detail_view: true\n posting_date:\n type: date\n label: Posting Date\n required: true\n in_list_view: true\n in_detail_view: true\n closing_account_id:\n type: link\n label: Closing Account (Retained Earnings)\n required: true\n link_entity: account\n link_display_field: name\n link_search_action: list-accounts\n in_list_view: true\n in_detail_view: true\n net_pl_amount:\n type: currency\n label: Net P&L Amount\n precision: 2\n read_only: true\n in_list_view: true\n in_detail_view: true\n status:\n type: status\n label: Status\n read_only: true\n in_list_view: true\n in_detail_view: true\n company_id:\n type: link\n label: Company\n read_only: true\n link_entity: company\n link_display_field: name\n link_search_action: list-companies\n in_detail_view: true\n created_at:\n type: datetime\n label: Created\n read_only: true\n in_detail_view: true\n updated_at:\n type: datetime\n label: Last Updated\n read_only: true\n in_detail_view: true\n form_groups: {}\n views:\n list:\n columns:\n - field: fiscal_year_id\n width: 160\n - field: posting_date\n width: 120\n - field: closing_account_id\n width: 200\n - field: net_pl_amount\n width: 140\n align: right\n - field: status\n width: 100\n detail:\n header:\n title_field: fiscal_year_id\n subtitle_field: posting_date\n status_field: status\n sections:\n - label: Voucher Details\n fields:\n - fiscal_year_id\n - posting_date\n - closing_account_id\n - net_pl_amount\n - status\n - company_id\n columns: 2\n - label: Metadata\n fields:\n - created_at\n - updated_at\n columns: 2\n collapsible: true\n journal_entry:\n label: Journal Entry\n label_plural: Journal Entries\n table: journal_entry\n id_col: id\n name_col: id\n primary_field: id\n identifier_field: id\n status_field: status\n status_colors:\n draft: gray\n submitted: green\n cancelled: red\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n naming_series:\n type: text\n label: Naming Series\n read_only: true\n posting_date:\n type: date\n label: Posting Date\n in_list_view: true\n in_form_view: true\n form_group: header\n form_order: 1\n entry_type:\n type: select\n label: Entry Type\n required: true\n options:\n - value: journal\n label: Journal\n - value: opening\n label: Opening\n - value: closing\n label: Closing\n - value: depreciation\n label: Depreciation\n - value: write_off\n label: Write Off\n - value: exchange_rate_revaluation\n label: Exchange Rate Revaluation\n - value: inter_company\n label: Inter Company\n - value: credit_note\n label: Credit Note\n - value: debit_note\n label: Debit Note\n in_form_view: true\n form_group: details\n form_order: 2\n total_debit:\n type: currency\n label: Total Debit\n precision: 2\n in_form_view: true\n form_group: amounts\n form_order: 3\n total_credit:\n type: currency\n label: Total Credit\n precision: 2\n in_form_view: true\n form_group: amounts\n form_order: 4\n currency:\n type: text\n label: Currency\n required: true\n in_form_view: true\n form_group: details\n form_order: 5\n exchange_rate:\n type: currency\n label: Exchange Rate\n precision: 2\n in_form_view: true\n form_group: amounts\n form_order: 6\n remark:\n type: text\n label: Remark\n in_form_view: true\n form_group: details\n form_order: 7\n status:\n type: select\n label: Status\n options:\n - value: draft\n label: Draft\n - value: submitted\n label: Submitted\n - value: cancelled\n label: Cancelled\n - value: amended\n label: Amended\n in_list_view: true\n in_form_view: true\n form_group: header\n form_order: 8\n amended_from:\n type: text\n label: Amended From\n in_form_view: true\n form_group: details\n form_order: 9\n company_id:\n type: link\n label: Company ID\n link_entity: company\n link_display_field: name\n link_search_action: list-companies\n in_form_view: true\n form_group: references\n form_order: 10\n form_groups:\n header:\n label: Basic Information\n order: 1\n columns: 2\n details:\n label: Details\n order: 2\n columns: 2\n references:\n label: References\n order: 3\n columns: 2\n amounts:\n label: Amounts\n order: 4\n columns: 2\n items:\n label: Line Items\n order: 5\n type: child_table\n payment_entry:\n label: Payment Entry\n label_plural: Payment Entries\n table: payment_entry\n id_col: id\n name_col: id\n primary_field: id\n identifier_field: id\n status_field: status\n status_colors:\n draft: gray\n submitted: green\n cancelled: red\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n naming_series:\n type: text\n label: Naming Series\n read_only: true\n payment_type:\n type: select\n label: Payment Type\n required: true\n options:\n - value: receive\n label: Receive\n - value: pay\n label: Pay\n - value: internal_transfer\n label: Internal Transfer\n in_form_view: true\n form_group: details\n form_order: 1\n posting_date:\n type: date\n label: Posting Date\n in_list_view: true\n in_form_view: true\n form_group: header\n form_order: 2\n party_type:\n type: select\n label: Party Type\n options:\n - value: customer\n label: Customer\n - value: supplier\n label: Supplier\n - value: employee\n label: Employee\n in_form_view: true\n form_group: details\n form_order: 3\n party_id:\n type: link\n label: Party ID\n link_entity: party\n link_display_field: name\n link_search_action: list-parties\n in_form_view: true\n form_group: references\n form_order: 4\n paid_from_account:\n type: currency\n label: Paid From Account\n precision: 2\n in_form_view: true\n form_group: details\n form_order: 5\n paid_to_account:\n type: currency\n label: Paid To Account\n precision: 2\n in_form_view: true\n form_group: details\n form_order: 6\n paid_amount:\n type: currency\n label: Paid Amount\n precision: 2\n in_form_view: true\n form_group: amounts\n form_order: 7\n received_amount:\n type: currency\n label: Received Amount\n precision: 2\n in_form_view: true\n form_group: amounts\n form_order: 8\n payment_currency:\n type: text\n label: Payment Currency\n required: true\n in_form_view: true\n form_group: details\n form_order: 9\n exchange_rate:\n type: currency\n label: Exchange Rate\n precision: 2\n in_form_view: true\n form_group: amounts\n form_order: 10\n reference_number:\n type: text\n label: Reference Number\n in_form_view: true\n form_group: details\n form_order: 11\n reference_date:\n type: date\n label: Reference Date\n in_form_view: true\n form_group: header\n form_order: 12\n status:\n type: select\n label: Status\n options:\n - value: draft\n label: Draft\n - value: submitted\n label: Submitted\n - value: cancelled\n label: Cancelled\n in_list_view: true\n in_form_view: true\n form_group: header\n form_order: 13\n unallocated_amount:\n type: currency\n label: Unallocated Amount\n precision: 2\n in_form_view: true\n form_group: amounts\n form_order: 14\n company_id:\n type: link\n label: Company ID\n link_entity: company\n link_display_field: name\n link_search_action: list-companies\n in_form_view: true\n form_group: references\n form_order: 15\n form_groups:\n header:\n label: Basic Information\n order: 1\n columns: 2\n details:\n label: Details\n order: 2\n columns: 2\n references:\n label: References\n order: 3\n columns: 2\n amounts:\n label: Amounts\n order: 4\n columns: 2\n payment_allocation:\n label: Payment Allocation\n label_plural: Payment Allocations\n table: payment_allocation\n id_col: id\n name_col: id\n primary_field: id\n identifier_field: id\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n payment_entry_id:\n type: link\n label: Payment Entry ID\n link_entity: payment_entry\n link_display_field: name\n link_search_action: list-payment-entries\n in_form_view: true\n form_group: references\n form_order: 1\n voucher_type:\n type: select\n label: Voucher Type\n required: true\n options:\n - value: sales_invoice\n label: Sales Invoice\n - value: purchase_invoice\n label: Purchase Invoice\n - value: credit_note\n label: Credit Note\n - value: debit_note\n label: Debit Note\n in_form_view: true\n form_group: details\n form_order: 2\n voucher_id:\n type: link\n label: Voucher ID\n link_entity: voucher\n link_display_field: name\n link_search_action: list-vouchers\n in_form_view: true\n form_group: references\n form_order: 3\n allocated_amount:\n type: currency\n label: Allocated Amount\n precision: 2\n in_form_view: true\n form_group: amounts\n form_order: 4\n exchange_gain_loss:\n type: text\n label: Exchange Gain Loss\n required: true\n in_form_view: true\n form_group: details\n form_order: 5\n form_groups:\n details:\n label: Details\n order: 1\n columns: 2\n references:\n label: References\n order: 2\n columns: 2\n amounts:\n label: Amounts\n order: 3\n columns: 2\n payment_deduction:\n label: Payment Deduction\n label_plural: Payment Deductions\n table: payment_deduction\n id_col: id\n name_col: id\n primary_field: id\n identifier_field: id\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n payment_entry_id:\n type: link\n label: Payment Entry ID\n link_entity: payment_entry\n link_display_field: name\n link_search_action: list-payment-entries\n in_form_view: true\n form_group: references\n form_order: 1\n account_id:\n type: link\n label: Account ID\n link_entity: account\n link_display_field: name\n link_search_action: list-accounts\n in_form_view: true\n form_group: references\n form_order: 2\n amount:\n type: currency\n label: Amount\n precision: 2\n in_form_view: true\n form_group: amounts\n form_order: 3\n type:\n type: select\n label: Type\n required: true\n options:\n - value: tds\n label: Tds\n - value: commission\n label: Commission\n - value: early_payment_discount\n label: Early Payment Discount\n - value: other\n label: Other\n in_form_view: true\n form_group: details\n form_order: 4\n description:\n type: textarea\n label: Description\n in_form_view: true\n form_group: notes\n form_order: 5\n form_groups:\n details:\n label: Details\n order: 1\n columns: 2\n references:\n label: References\n order: 2\n columns: 2\n amounts:\n label: Amounts\n order: 3\n columns: 2\n notes:\n label: Notes\n order: 4\n columns: 1\n payment_ledger_entry:\n label: Payment Ledger Entry\n label_plural: Payment Ledger Entries\n table: payment_ledger_entry\n id_col: id\n name_col: id\n primary_field: id\n identifier_field: id\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n posting_date:\n type: date\n label: Posting Date\n in_list_view: true\n in_form_view: true\n form_group: header\n form_order: 1\n account_id:\n type: link\n label: Account ID\n link_entity: account\n link_display_field: name\n link_search_action: list-accounts\n in_form_view: true\n form_group: references\n form_order: 2\n party_type:\n type: select\n label: Party Type\n required: true\n options:\n - value: customer\n label: Customer\n - value: supplier\n label: Supplier\n - value: employee\n label: Employee\n in_form_view: true\n form_group: details\n form_order: 3\n party_id:\n type: link\n label: Party ID\n link_entity: party\n link_display_field: name\n link_search_action: list-parties\n in_form_view: true\n form_group: references\n form_order: 4\n voucher_type:\n type: text\n label: Voucher Type\n required: true\n in_form_view: true\n form_group: details\n form_order: 5\n voucher_id:\n type: link\n label: Voucher ID\n link_entity: voucher\n link_display_field: name\n link_search_action: list-vouchers\n in_form_view: true\n form_group: references\n form_order: 6\n against_voucher_type:\n type: text\n label: Against Voucher Type\n in_form_view: true\n form_group: details\n form_order: 7\n against_voucher_id:\n type: link\n label: Against Voucher ID\n link_entity: against_voucher\n link_display_field: name\n link_search_action: list-against-vouchers\n in_form_view: true\n form_group: references\n form_order: 8\n amount:\n type: currency\n label: Amount\n precision: 2\n in_form_view: true\n form_group: amounts\n form_order: 9\n amount_in_account_currency:\n type: currency\n label: Amount In Account Currency\n precision: 2\n in_form_view: true\n form_group: amounts\n form_order: 10\n currency:\n type: text\n label: Currency\n required: true\n in_form_view: true\n form_group: details\n form_order: 11\n delinked:\n type: integer\n label: Delinked\n in_form_view: true\n form_group: details\n form_order: 12\n remarks:\n type: textarea\n label: Remarks\n in_form_view: true\n form_group: notes\n form_order: 13\n form_groups:\n header:\n label: Basic Information\n order: 1\n columns: 2\n details:\n label: Details\n order: 2\n columns: 2\n references:\n label: References\n order: 3\n columns: 2\n amounts:\n label: Amounts\n order: 4\n columns: 2\n notes:\n label: Notes\n order: 5\n columns: 1\n tax_template:\n label: Tax Template\n label_plural: Tax Templates\n table: tax_template\n id_col: id\n name_col: name\n primary_field: name\n identifier_field: id\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n name:\n type: text\n label: Name\n required: true\n in_list_view: true\n in_form_view: true\n form_group: header\n form_order: 1\n tax_type:\n type: select\n label: Tax Type\n required: true\n options:\n - value: sales\n label: Sales\n - value: purchase\n label: Purchase\n - value: both\n label: Both\n in_form_view: true\n form_group: details\n form_order: 2\n is_default:\n type: integer\n label: Is Default\n in_form_view: true\n form_group: details\n form_order: 3\n tax_category_id:\n type: link\n label: Tax Category ID\n link_entity: tax_category\n link_display_field: name\n link_search_action: list-tax-categories\n in_form_view: true\n form_group: references\n form_order: 4\n company_id:\n type: link\n label: Company ID\n link_entity: company\n link_display_field: name\n link_search_action: list-companies\n in_form_view: true\n form_group: references\n form_order: 5\n form_groups:\n header:\n label: Basic Information\n order: 1\n columns: 2\n details:\n label: Details\n order: 2\n columns: 2\n references:\n label: References\n order: 3\n columns: 2\n items:\n label: Line Items\n order: 4\n type: child_table\n tax_rule:\n label: Tax Rule\n label_plural: Tax Rules\n table: tax_rule\n id_col: id\n name_col: id\n primary_field: id\n identifier_field: id\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n tax_template_id:\n type: link\n label: Tax Template ID\n link_entity: tax_template\n link_display_field: name\n link_search_action: list-tax-templates\n in_form_view: true\n form_group: references\n form_order: 1\n tax_type:\n type: select\n label: Tax Type\n required: true\n options:\n - value: sales\n label: Sales\n - value: purchase\n label: Purchase\n in_form_view: true\n form_group: details\n form_order: 2\n customer_id:\n type: link\n label: Customer ID\n link_entity: customer\n link_display_field: name\n link_search_action: list-customers\n in_form_view: true\n form_group: references\n form_order: 3\n supplier_id:\n type: link\n label: Supplier ID\n link_entity: supplier\n link_display_field: name\n link_search_action: list-suppliers\n in_form_view: true\n form_group: references\n form_order: 4\n customer_group:\n type: text\n label: Customer Group\n in_form_view: true\n form_group: details\n form_order: 5\n supplier_group:\n type: text\n label: Supplier Group\n in_form_view: true\n form_group: details\n form_order: 6\n item_id:\n type: link\n label: Item ID\n link_entity: item\n link_display_field: name\n link_search_action: list-items\n in_form_view: true\n form_group: references\n form_order: 7\n item_group:\n type: text\n label: Item Group\n in_form_view: true\n form_group: details\n form_order: 8\n billing_state:\n type: text\n label: Billing State\n in_form_view: true\n form_group: details\n form_order: 9\n shipping_state:\n type: text\n label: Shipping State\n in_form_view: true\n form_group: details\n form_order: 10\n tax_category_id:\n type: link\n label: Tax Category ID\n link_entity: tax_category\n link_display_field: name\n link_search_action: list-tax-categories\n in_form_view: true\n form_group: references\n form_order: 11\n priority:\n type: integer\n label: Priority\n in_form_view: true\n form_group: details\n form_order: 12\n company_id:\n type: link\n label: Company ID\n link_entity: company\n link_display_field: name\n link_search_action: list-companies\n in_form_view: true\n form_group: references\n form_order: 13\n form_groups:\n details:\n label: Details\n order: 1\n columns: 2\n references:\n label: References\n order: 2\n columns: 2\n tax_category:\n label: Tax Category\n label_plural: Tax Categories\n table: tax_category\n id_col: id\n name_col: name\n primary_field: name\n identifier_field: id\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n name:\n type: text\n label: Name\n required: true\n in_list_view: true\n in_form_view: true\n form_group: header\n form_order: 1\n description:\n type: textarea\n label: Description\n in_form_view: true\n form_group: notes\n form_order: 2\n form_groups:\n header:\n label: Basic Information\n order: 1\n columns: 2\n notes:\n label: Notes\n order: 2\n columns: 1\n tax_withholding_category:\n label: Tax Withholding Category\n label_plural: Tax Withholding Categories\n table: tax_withholding_category\n id_col: id\n name_col: name\n primary_field: name\n identifier_field: id\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n name:\n type: text\n label: Name\n required: true\n in_list_view: true\n in_form_view: true\n form_group: header\n form_order: 1\n category_code:\n type: text\n label: Category Code\n in_form_view: true\n form_group: details\n form_order: 2\n single_threshold:\n type: text\n label: Single Threshold\n in_form_view: true\n form_group: details\n form_order: 3\n cumulative_threshold:\n type: text\n label: Cumulative Threshold\n in_form_view: true\n form_group: details\n form_order: 4\n tax_on_excess_amount:\n type: integer\n label: Tax On Excess Amount\n in_form_view: true\n form_group: amounts\n form_order: 5\n company_id:\n type: link\n label: Company ID\n link_entity: company\n link_display_field: name\n link_search_action: list-companies\n in_form_view: true\n form_group: references\n form_order: 6\n form_groups:\n header:\n label: Basic Information\n order: 1\n columns: 2\n details:\n label: Details\n order: 2\n columns: 2\n references:\n label: References\n order: 3\n columns: 2\n amounts:\n label: Amounts\n order: 4\n columns: 2\n tax_withholding_entry:\n label: Tax Withholding Entry\n label_plural: Tax Withholding Entries\n table: tax_withholding_entry\n id_col: id\n name_col: id\n primary_field: id\n identifier_field: id\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n party_type:\n type: select\n label: Party Type\n required: true\n options:\n - value: customer\n label: Customer\n - value: supplier\n label: Supplier\n in_form_view: true\n form_group: details\n form_order: 1\n party_id:\n type: link\n label: Party ID\n link_entity: party\n link_display_field: name\n link_search_action: list-parties\n in_form_view: true\n form_group: references\n form_order: 2\n category_id:\n type: link\n label: Category ID\n link_entity: tax_withholding_category\n link_display_field: name\n link_search_action: list-tax-withholding-categories\n in_form_view: true\n form_group: references\n form_order: 3\n fiscal_year:\n type: text\n label: Fiscal Year\n required: true\n in_form_view: true\n form_group: details\n form_order: 4\n taxable_amount:\n type: currency\n label: Taxable Amount\n precision: 2\n in_form_view: true\n form_group: amounts\n form_order: 5\n withheld_amount:\n type: currency\n label: Withheld Amount\n precision: 2\n in_form_view: true\n form_group: amounts\n form_order: 6\n taxable_voucher_type:\n type: text\n label: Taxable Voucher Type\n in_form_view: true\n form_group: details\n form_order: 7\n taxable_voucher_id:\n type: link\n label: Taxable Voucher ID\n link_entity: taxable_voucher\n link_display_field: name\n link_search_action: list-taxable-vouchers\n in_form_view: true\n form_group: references\n form_order: 8\n withholding_voucher_type:\n type: text\n label: Withholding Voucher Type\n in_form_view: true\n form_group: details\n form_order: 9\n withholding_voucher_id:\n type: link\n label: Withholding Voucher ID\n link_entity: withholding_voucher\n link_display_field: name\n link_search_action: list-withholding-vouchers\n in_form_view: true\n form_group: references\n form_order: 10\n form_groups:\n details:\n label: Details\n order: 1\n columns: 2\n references:\n label: References\n order: 2\n columns: 2\n amounts:\n label: Amounts\n order: 3\n columns: 2\n tax_withholding_group:\n label: Tax Withholding Group\n label_plural: Tax Withholding Groups\n table: tax_withholding_group\n id_col: id\n name_col: group_name\n primary_field: group_name\n identifier_field: id\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n category_id:\n type: link\n label: Category ID\n link_entity: tax_withholding_category\n link_display_field: name\n link_search_action: list-tax-withholding-categories\n in_form_view: true\n form_group: references\n form_order: 1\n group_name:\n type: text\n label: Group Name\n required: true\n in_list_view: true\n in_form_view: true\n form_group: details\n form_order: 2\n rate:\n type: currency\n label: Rate\n precision: 2\n in_form_view: true\n form_group: amounts\n form_order: 3\n effective_from:\n type: date\n label: Effective From\n in_form_view: true\n form_group: details\n form_order: 4\n effective_to:\n type: date\n label: Effective To\n in_form_view: true\n form_group: details\n form_order: 5\n account_id:\n type: link\n label: Account ID\n link_entity: account\n link_display_field: name\n link_search_action: list-accounts\n in_form_view: true\n form_group: references\n form_order: 6\n form_groups:\n details:\n label: Details\n order: 1\n columns: 2\n references:\n label: References\n order: 2\n columns: 2\n amounts:\n label: Amounts\n order: 3\n columns: 2\n item_tax_template:\n label: Item Tax Template\n label_plural: Item Tax Templates\n table: item_tax_template\n id_col: id\n name_col: id\n primary_field: id\n identifier_field: id\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n item_id:\n type: link\n label: Item ID\n link_entity: item\n link_display_field: name\n link_search_action: list-items\n in_form_view: true\n form_group: references\n form_order: 1\n tax_template_id:\n type: link\n label: Tax Template ID\n link_entity: tax_template\n link_display_field: name\n link_search_action: list-tax-templates\n in_form_view: true\n form_group: references\n form_order: 2\n tax_rate:\n type: currency\n label: Tax Rate\n precision: 2\n in_form_view: true\n form_group: amounts\n form_order: 3\n form_groups:\n references:\n label: References\n order: 1\n columns: 2\n amounts:\n label: Amounts\n order: 2\n columns: 2\n item:\n label: Item\n label_plural: Items\n icon: box\n primary_field: item_name\n secondary_field: item_code\n identifier_field: id\n status_field: status\n status_colors:\n active: green\n disabled: gray\n fields:\n item_code:\n type: text\n label: Item Code\n required: true\n max_length: 140\n placeholder: e.g., SKU-001\n searchable: true\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 1\n item_name:\n type: text\n label: Item Name\n required: true\n max_length: 140\n searchable: true\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 2\n item_type:\n type: select\n label: Item Type\n required: true\n options:\n - value: stock\n label: Stock\n - value: non_stock\n label: Non-Stock\n - value: service\n label: Service\n default: stock\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 3\n item_group_id:\n type: link\n label: Item Group\n link_entity: item_group\n link_display_field: name\n link_search_action: list-item-groups\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 4\n stock_uom:\n type: text\n label: Stock UoM\n required: true\n default: Nos\n max_length: 80\n help_text: Default unit of measure (e.g., Nos, Kg, Ltr)\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 5\n valuation_method:\n type: select\n label: Valuation Method\n options:\n - value: moving_average\n label: Moving Average\n - value: fifo\n label: FIFO\n default: moving_average\n in_form_view: true\n form_group: basic\n form_order: 6\n standard_rate:\n type: currency\n label: Standard Rate\n precision: 2\n default: '0.00'\n help_text: Default rate used when no price list rate is available\n in_list_view: true\n in_form_view: true\n form_group: pricing\n form_order: 1\n is_stock_item:\n type: boolean\n label: Is Stock Item\n read_only: true\n help_text: Automatically set based on item type\n in_detail_view: true\n has_batch:\n type: boolean\n label: Has Batch No\n default: false\n help_text: Track this item by batch number\n in_form_view: true\n form_group: tracking\n form_order: 1\n has_serial:\n type: boolean\n label: Has Serial No\n default: false\n help_text: Track each unit by serial number\n in_form_view: true\n form_group: tracking\n form_order: 2\n reorder_level:\n type: quantity\n label: Reorder Level\n precision: 2\n help_text: Alert when stock falls to or below this quantity\n in_form_view: true\n form_group: reorder\n form_order: 1\n reorder_qty:\n type: quantity\n label: Reorder Qty\n precision: 2\n help_text: Suggested quantity to reorder\n in_form_view: true\n form_group: reorder\n form_order: 2\n status:\n type: status\n label: Status\n read_only: true\n in_list_view: true\n in_detail_view: true\n description:\n type: textarea\n label: Description\n max_length: 1000\n in_form_view: true\n form_group: basic\n form_order: 7\n created_at:\n type: datetime\n label: Created\n read_only: true\n in_detail_view: true\n updated_at:\n type: datetime\n label: Last Updated\n read_only: true\n in_detail_view: true\n form_groups:\n basic:\n label: Item Details\n order: 1\n columns: 2\n pricing:\n label: Pricing\n order: 2\n columns: 2\n tracking:\n label: Batch & Serial Tracking\n order: 3\n columns: 2\n collapsible: true\n reorder:\n label: Reorder Settings\n order: 4\n columns: 2\n collapsible: true\n views:\n list:\n columns:\n - field: item_code\n width: 120\n link: true\n - field: item_name\n width: 200\n - field: item_type\n width: 100\n - field: stock_uom\n width: 80\n - field: standard_rate\n width: 120\n align: right\n - field: status\n width: 90\n filters:\n - field: item_type\n type: select\n - field: item_group_id\n type: link\n - field: status\n type: select\n row_click: get-item\n detail:\n header:\n title_field: item_name\n subtitle_field: item_code\n status_field: status\n sections:\n - label: Item Details\n fields:\n - item_code\n - item_name\n - item_type\n - item_group_id\n - stock_uom\n - valuation_method\n - description\n columns: 2\n - label: Pricing\n fields:\n - standard_rate\n columns: 2\n - label: Tracking\n fields:\n - has_batch\n - has_serial\n columns: 2\n collapsible: true\n - label: Reorder Settings\n fields:\n - reorder_level\n - reorder_qty\n columns: 2\n collapsible: true\n - label: Stock Balances\n child_entity: item_stock_balance\n child_action: get-item\n child_key: stock_balances\n columns: 1\n actions:\n - action: update-item\n label: Edit\n - action: add-item-price\n label: Set Price\n - action: add-batch\n label: Add Batch\n condition: has_batch == true\n - action: add-serial-number\n label: Add Serial No\n condition: has_serial == true\n item_group:\n label: Item Group\n label_plural: Item Groups\n icon: folder\n primary_field: name\n identifier_field: id\n fields:\n name:\n type: text\n label: Group Name\n required: true\n max_length: 140\n searchable: true\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 1\n parent_id:\n type: link\n label: Parent Group\n link_entity: item_group\n link_display_field: name\n link_search_action: list-item-groups\n help_text: Leave blank for a top-level group\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 2\n created_at:\n type: datetime\n label: Created\n read_only: true\n in_detail_view: true\n form_groups:\n basic:\n label: Item Group Details\n order: 1\n columns: 2\n views:\n list:\n columns:\n - field: name\n width: 240\n link: true\n - field: parent_id\n width: 200\n filters:\n - field: parent_id\n type: link\n warehouse:\n label: Warehouse\n label_plural: Warehouses\n icon: home\n primary_field: name\n secondary_field: warehouse_type\n identifier_field: id\n fields:\n name:\n type: text\n label: Warehouse Name\n required: true\n max_length: 140\n searchable: true\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 1\n company_id:\n type: link\n label: Company\n required: true\n link_entity: company\n link_display_field: name\n link_search_action: list-companies\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 2\n warehouse_type:\n type: select\n label: Warehouse Type\n options:\n - value: stores\n label: Stores\n - value: production\n label: Production\n - value: transit\n label: Transit\n - value: rejected\n label: Rejected\n default: stores\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 3\n parent_id:\n type: link\n label: Parent Warehouse\n link_entity: warehouse\n link_display_field: name\n link_search_action: list-warehouses\n help_text: Leave blank for a top-level warehouse\n in_form_view: true\n form_group: basic\n form_order: 4\n account_id:\n type: link\n label: Stock Account\n link_entity: account\n link_display_field: name\n link_search_action: list-accounts\n help_text: GL account for perpetual inventory posting\n in_form_view: true\n form_group: accounting\n form_order: 1\n is_group:\n type: boolean\n label: Is Group\n default: false\n help_text: Group warehouses cannot hold stock directly\n in_form_view: true\n form_group: basic\n form_order: 5\n created_at:\n type: datetime\n label: Created\n read_only: true\n in_detail_view: true\n updated_at:\n type: datetime\n label: Last Updated\n read_only: true\n in_detail_view: true\n form_groups:\n basic:\n label: Warehouse Details\n order: 1\n columns: 2\n accounting:\n label: Accounting\n order: 2\n columns: 2\n collapsible: true\n views:\n list:\n columns:\n - field: name\n width: 220\n link: true\n - field: warehouse_type\n width: 120\n - field: company_id\n width: 180\n - field: is_group\n width: 80\n filters:\n - field: company_id\n type: link\n - field: warehouse_type\n type: select\n stock_entry:\n label: Stock Entry\n label_plural: Stock Entries\n icon: truck\n primary_field: naming_series\n secondary_field: stock_entry_type\n identifier_field: id\n status_field: status\n lifecycle: draft-submit-cancel\n status_colors:\n draft: blue\n submitted: green\n cancelled: red\n fields:\n naming_series:\n type: text\n label: Entry No\n read_only: true\n in_list_view: true\n in_detail_view: true\n stock_entry_type:\n type: select\n label: Entry Type\n required: true\n options:\n - value: material_receipt\n label: Material Receipt\n - value: material_issue\n label: Material Issue\n - value: material_transfer\n label: Material Transfer\n - value: manufacture\n label: Manufacture\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 1\n company_id:\n type: link\n label: Company\n required: true\n link_entity: company\n link_display_field: name\n link_search_action: list-companies\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 2\n posting_date:\n type: date\n label: Posting Date\n required: true\n default: today\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 3\n total_incoming_value:\n type: currency\n label: Total Incoming Value\n precision: 2\n read_only: true\n in_list_view: true\n in_detail_view: true\n total_outgoing_value:\n type: currency\n label: Total Outgoing Value\n precision: 2\n read_only: true\n in_list_view: true\n in_detail_view: true\n value_difference:\n type: currency\n label: Value Difference\n precision: 2\n read_only: true\n in_detail_view: true\n status:\n type: status\n label: Status\n read_only: true\n in_list_view: true\n in_detail_view: true\n created_at:\n type: datetime\n label: Created\n read_only: true\n in_detail_view: true\n updated_at:\n type: datetime\n label: Last Updated\n read_only: true\n in_detail_view: true\n form_groups:\n basic:\n label: Stock Entry Details\n order: 1\n columns: 3\n items:\n label: Items\n order: 2\n columns: 1\n child_entity: stock_entry_item\n totals:\n label: Totals\n order: 3\n columns: 3\n read_only: true\n views:\n list:\n columns:\n - field: naming_series\n width: 140\n link: true\n - field: stock_entry_type\n width: 140\n - field: posting_date\n width: 110\n - field: total_incoming_value\n width: 140\n align: right\n - field: total_outgoing_value\n width: 140\n align: right\n - field: status\n width: 100\n filters:\n - field: company_id\n type: link\n - field: stock_entry_type\n type: select\n - field: status\n type: select\n - field: posting_date\n type: date_range\n row_click: get-stock-entry\n detail:\n header:\n title_field: naming_series\n subtitle_field: stock_entry_type\n status_field: status\n sections:\n - label: Entry Details\n fields:\n - naming_series\n - stock_entry_type\n - company_id\n - posting_date\n columns: 2\n - label: Items\n child_entity: stock_entry_item\n child_action: get-stock-entry\n child_key: items\n columns: 1\n - label: Totals\n fields:\n - total_incoming_value\n - total_outgoing_value\n - value_difference\n columns: 3\n actions:\n - action: submit-stock-entry\n label: Submit\n condition: status == 'draft'\n style: primary\n confirm: true\n - action: cancel-stock-entry\n label: Cancel\n condition: status == 'submitted'\n style: danger\n confirm: true\n batch:\n label: Batch\n label_plural: Batches\n icon: layers\n primary_field: batch_name\n identifier_field: id\n fields:\n batch_name:\n type: text\n label: Batch Name\n required: true\n max_length: 140\n searchable: true\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 1\n item_id:\n type: link\n label: Item\n required: true\n link_entity: item\n link_display_field: item_name\n link_search_action: list-items\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 2\n manufacturing_date:\n type: date\n label: Manufacturing Date\n in_form_view: true\n form_group: dates\n form_order: 1\n expiry_date:\n type: date\n label: Expiry Date\n in_list_view: true\n in_form_view: true\n form_group: dates\n form_order: 2\n created_at:\n type: datetime\n label: Created\n read_only: true\n in_detail_view: true\n form_groups:\n basic:\n label: Batch Details\n order: 1\n columns: 2\n dates:\n label: Dates\n order: 2\n columns: 2\n views:\n list:\n columns:\n - field: batch_name\n width: 180\n link: true\n - field: item_id\n width: 200\n - field: expiry_date\n width: 120\n filters:\n - field: item_id\n type: link\n serial_number:\n label: Serial Number\n label_plural: Serial Numbers\n icon: hash\n primary_field: serial_no\n secondary_field: status\n identifier_field: id\n status_field: status\n status_colors:\n active: green\n delivered: blue\n returned: orange\n scrapped: red\n fields:\n serial_no:\n type: text\n label: Serial No\n required: true\n max_length: 140\n searchable: true\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 1\n item_id:\n type: link\n label: Item\n required: true\n link_entity: item\n link_display_field: item_name\n link_search_action: list-items\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 2\n warehouse_id:\n type: link\n label: Warehouse\n link_entity: warehouse\n link_display_field: name\n link_search_action: list-warehouses\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 3\n batch_id:\n type: link\n label: Batch\n link_entity: batch\n link_display_field: batch_name\n link_search_action: list-batches\n in_form_view: true\n form_group: basic\n form_order: 4\n status:\n type: status\n label: Status\n read_only: true\n in_list_view: true\n in_detail_view: true\n created_at:\n type: datetime\n label: Created\n read_only: true\n in_detail_view: true\n form_groups:\n basic:\n label: Serial Number Details\n order: 1\n columns: 2\n views:\n list:\n columns:\n - field: serial_no\n width: 160\n link: true\n - field: item_id\n width: 200\n - field: warehouse_id\n width: 180\n - field: status\n width: 100\n filters:\n - field: item_id\n type: link\n - field: warehouse_id\n type: link\n - field: status\n type: select\n price_list:\n label: Price List\n label_plural: Price Lists\n icon: tag\n primary_field: name\n secondary_field: currency\n identifier_field: id\n fields:\n name:\n type: text\n label: Price List Name\n required: true\n max_length: 140\n searchable: true\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 1\n currency:\n type: currency_code\n label: Currency\n default: USD\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 2\n buying:\n type: boolean\n label: Buying\n default: false\n help_text: Use this price list for purchase transactions\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 3\n selling:\n type: boolean\n label: Selling\n default: false\n help_text: Use this price list for sales transactions\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 4\n created_at:\n type: datetime\n label: Created\n read_only: true\n in_detail_view: true\n form_groups:\n basic:\n label: Price List Details\n order: 1\n columns: 2\n views:\n list:\n columns:\n - field: name\n width: 220\n link: true\n - field: currency\n width: 100\n - field: buying\n width: 80\n - field: selling\n width: 80\n item_price:\n label: Item Price\n label_plural: Item Prices\n icon: dollar-sign\n primary_field: item_id\n secondary_field: price_list_id\n identifier_field: id\n fields:\n item_id:\n type: link\n label: Item\n required: true\n link_entity: item\n link_display_field: item_name\n link_search_action: list-items\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 1\n price_list_id:\n type: link\n label: Price List\n required: true\n link_entity: price_list\n link_display_field: name\n link_search_action: list-price-lists\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 2\n rate:\n type: currency\n label: Rate\n required: true\n precision: 2\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 3\n min_qty:\n type: quantity\n label: Minimum Qty\n precision: 2\n default: '0'\n help_text: Price applies when ordered qty >= this value\n in_list_view: true\n in_form_view: true\n form_group: validity\n form_order: 1\n valid_from:\n type: date\n label: Valid From\n in_form_view: true\n form_group: validity\n form_order: 2\n valid_to:\n type: date\n label: Valid To\n in_form_view: true\n form_group: validity\n form_order: 3\n created_at:\n type: datetime\n label: Created\n read_only: true\n in_detail_view: true\n form_groups:\n basic:\n label: Price Details\n order: 1\n columns: 3\n validity:\n label: Validity & Tiers\n order: 2\n columns: 3\n collapsible: true\n views:\n list:\n columns:\n - field: item_id\n width: 200\n - field: price_list_id\n width: 180\n - field: rate\n width: 120\n align: right\n - field: min_qty\n width: 100\n align: right\n - field: valid_from\n width: 110\n - field: valid_to\n width: 110\n pricing_rule:\n label: Pricing Rule\n label_plural: Pricing Rules\n icon: percent\n primary_field: name\n secondary_field: applies_to\n identifier_field: id\n fields:\n name:\n type: text\n label: Rule Name\n required: true\n max_length: 140\n searchable: true\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 1\n applies_to:\n type: select\n label: Applies To\n required: true\n options:\n - value: item\n label: Item\n - value: item_group\n label: Item Group\n - value: customer\n label: Customer\n - value: customer_group\n label: Customer Group\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 2\n entity_id:\n type: text\n label: Entity ID\n help_text: ID of the item, item group, customer, or customer group\n in_form_view: true\n form_group: basic\n form_order: 3\n discount_percentage:\n type: percent\n label: Discount %\n precision: 2\n min: '0'\n max: '100'\n in_list_view: true\n in_form_view: true\n form_group: pricing\n form_order: 1\n rate:\n type: currency\n label: Fixed Rate\n precision: 2\n help_text: Override rate (alternative to discount %)\n in_form_view: true\n form_group: pricing\n form_order: 2\n min_qty:\n type: quantity\n label: Minimum Qty\n precision: 2\n in_form_view: true\n form_group: conditions\n form_order: 1\n max_qty:\n type: quantity\n label: Maximum Qty\n precision: 2\n in_form_view: true\n form_group: conditions\n form_order: 2\n valid_from:\n type: date\n label: Valid From\n in_list_view: true\n in_form_view: true\n form_group: conditions\n form_order: 3\n valid_to:\n type: date\n label: Valid To\n in_list_view: true\n in_form_view: true\n form_group: conditions\n form_order: 4\n priority:\n type: integer\n label: Priority\n default: 0\n help_text: Higher priority rules are applied first\n in_form_view: true\n form_group: conditions\n form_order: 5\n company_id:\n type: link\n label: Company\n required: true\n link_entity: company\n link_display_field: name\n link_search_action: list-companies\n in_form_view: true\n form_group: basic\n form_order: 4\n created_at:\n type: datetime\n label: Created\n read_only: true\n in_detail_view: true\n form_groups:\n basic:\n label: Rule Details\n order: 1\n columns: 2\n pricing:\n label: Pricing\n order: 2\n columns: 2\n conditions:\n label: Conditions & Validity\n order: 3\n columns: 2\n collapsible: true\n views:\n list:\n columns:\n - field: name\n width: 200\n link: true\n - field: applies_to\n width: 120\n - field: discount_percentage\n width: 100\n align: right\n - field: valid_from\n width: 110\n - field: valid_to\n width: 110\n filters:\n - field: applies_to\n type: select\n - field: company_id\n type: link\n stock_reconciliation:\n label: Stock Reconciliation\n label_plural: Stock Reconciliations\n icon: clipboard-check\n primary_field: naming_series\n secondary_field: posting_date\n identifier_field: id\n status_field: status\n lifecycle: draft-submit-cancel\n status_colors:\n draft: blue\n submitted: green\n cancelled: red\n fields:\n naming_series:\n type: text\n label: Reconciliation No\n read_only: true\n in_list_view: true\n in_detail_view: true\n posting_date:\n type: date\n label: Posting Date\n required: true\n default: today\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 1\n company_id:\n type: link\n label: Company\n required: true\n link_entity: company\n link_display_field: name\n link_search_action: list-companies\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 2\n difference_amount:\n type: currency\n label: Difference Amount\n precision: 2\n read_only: true\n in_list_view: true\n in_detail_view: true\n status:\n type: status\n label: Status\n read_only: true\n in_list_view: true\n in_detail_view: true\n created_at:\n type: datetime\n label: Created\n read_only: true\n in_detail_view: true\n updated_at:\n type: datetime\n label: Last Updated\n read_only: true\n in_detail_view: true\n form_groups:\n basic:\n label: Reconciliation Details\n order: 1\n columns: 2\n items:\n label: Items\n order: 2\n columns: 1\n child_entity: stock_reconciliation_item\n views:\n list:\n columns:\n - field: naming_series\n width: 160\n link: true\n - field: posting_date\n width: 120\n - field: company_id\n width: 180\n - field: difference_amount\n width: 140\n align: right\n - field: status\n width: 100\n filters:\n - field: company_id\n type: link\n - field: status\n type: select\n - field: posting_date\n type: date_range\n detail:\n header:\n title_field: naming_series\n subtitle_field: posting_date\n status_field: status\n sections:\n - label: Reconciliation Details\n fields:\n - naming_series\n - posting_date\n - company_id\n - difference_amount\n columns: 2\n - label: Items\n child_entity: stock_reconciliation_item\n child_action: get-stock-reconciliation\n child_key: items\n columns: 1\n actions:\n - action: submit-stock-reconciliation\n label: Submit\n condition: status == 'draft'\n style: primary\n confirm: true\n stock_revaluation:\n label: Stock Revaluation\n label_plural: Stock Revaluations\n icon: trending-up\n primary_field: naming_series\n secondary_field: posting_date\n identifier_field: id\n status_field: status\n lifecycle: submit-cancel\n status_colors:\n submitted: green\n cancelled: red\n fields:\n naming_series:\n type: text\n label: Revaluation No\n read_only: true\n in_list_view: true\n in_detail_view: true\n item_id:\n type: link\n label: Item\n required: true\n link_entity: item\n link_display_field: item_name\n link_search_action: list-items\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 1\n warehouse_id:\n type: link\n label: Warehouse\n required: true\n link_entity: warehouse\n link_display_field: name\n link_search_action: list-warehouses\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 2\n posting_date:\n type: date\n label: Posting Date\n required: true\n default: today\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 3\n new_rate:\n type: currency\n label: New Rate\n precision: 2\n required: true\n in_form_view: true\n form_group: basic\n form_order: 4\n reason:\n type: text\n label: Reason\n in_form_view: true\n form_group: basic\n form_order: 5\n current_qty:\n type: quantity\n label: Current Qty\n precision: 2\n read_only: true\n in_detail_view: true\n old_rate:\n type: currency\n label: Old Rate\n precision: 2\n read_only: true\n in_list_view: true\n in_detail_view: true\n adjustment_amount:\n type: currency\n label: Adjustment Amount\n precision: 2\n read_only: true\n in_list_view: true\n in_detail_view: true\n status:\n type: status\n label: Status\n read_only: true\n in_list_view: true\n in_detail_view: true\n form_groups:\n basic:\n label: Revaluation Details\n order: 1\n columns: 2\n views:\n list:\n columns:\n - field: naming_series\n width: 160\n link: true\n - field: posting_date\n width: 120\n - field: item_id\n width: 180\n - field: old_rate\n width: 120\n align: right\n - field: new_rate\n width: 120\n align: right\n - field: adjustment_amount\n width: 140\n align: right\n - field: status\n width: 100\n filters:\n - field: status\n type: select\n - field: posting_date\n type: date_range\n detail:\n header:\n title_field: naming_series\n subtitle_field: posting_date\n status_field: status\n sections:\n - label: Revaluation Details\n fields:\n - naming_series\n - item_id\n - warehouse_id\n - posting_date\n - current_qty\n - old_rate\n - new_rate\n - adjustment_amount\n - reason\n columns: 2\n actions:\n - action: cancel-stock-revaluation\n label: Cancel\n condition: status == 'submitted'\n style: destructive\n confirm: true\n customer:\n label: Customer\n label_plural: Customers\n icon: users\n table: customer\n id_col: id\n name_col: name\n primary_field: name\n secondary_field: customer_type\n identifier_field: id\n status_field: status\n status_colors:\n active: green\n suspended: yellow\n closed: red\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n name:\n type: text\n label: Customer Name\n required: true\n max_length: 140\n placeholder: Enter company or individual name\n help_text: The legal name of the customer\n searchable: true\n in_list_view: true\n in_detail_view: true\n in_form_view: true\n form_group: basic\n form_order: 1\n customer_type:\n type: select\n label: Type\n required: true\n options:\n - value: company\n label: Company\n - value: individual\n label: Individual\n default: company\n in_list_view: true\n in_detail_view: true\n in_form_view: true\n form_group: basic\n form_order: 2\n customer_group:\n type: text\n label: Customer Group\n required: false\n max_length: 140\n in_form_view: true\n in_detail_view: true\n form_group: classification\n form_order: 1\n payment_terms_id:\n type: link\n label: Payment Terms\n required: false\n link_entity: payment_terms\n link_display_field: name\n in_form_view: true\n form_group: accounting\n form_order: 3\n credit_limit:\n type: currency\n label: Credit Limit\n required: false\n default: '0.00'\n min: '0.00'\n precision: 2\n in_form_view: true\n in_detail_view: true\n form_group: accounting\n form_order: 1\n tax_id:\n type: text\n label: Tax ID / EIN\n required: false\n max_length: 20\n pattern: ^[0-9]{2}-[0-9]{7}$\n pattern_message: 'Format: XX-XXXXXXX'\n in_form_view: true\n in_detail_view: true\n form_group: accounting\n form_order: 2\n exempt_from_sales_tax:\n type: boolean\n label: Exempt from Sales Tax\n default: false\n in_form_view: true\n form_group: accounting\n form_order: 4\n primary_address:\n type: textarea\n label: Primary Address\n required: false\n in_form_view: true\n in_detail_view: true\n form_group: contact\n form_order: 1\n primary_contact:\n type: textarea\n label: Primary Contact\n required: false\n in_form_view: true\n in_detail_view: true\n form_group: contact\n form_order: 2\n total_outstanding:\n type: currency\n label: Outstanding\n read_only: true\n precision: 2\n in_list_view: true\n in_detail_view: true\n outstanding_invoice_count:\n type: integer\n label: Open Invoices\n read_only: true\n in_detail_view: true\n status:\n type: status\n label: Status\n read_only: true\n in_list_view: true\n in_detail_view: true\n company_id:\n type: link\n label: Company\n required: true\n link_entity: company\n link_display_field: name\n in_form_view: true\n form_group: basic\n form_order: 3\n created_at:\n type: datetime\n label: Created\n read_only: true\n in_detail_view: true\n updated_at:\n type: datetime\n label: Updated\n read_only: true\n form_groups:\n basic:\n label: Basic Information\n order: 1\n columns: 2\n contact:\n label: Contact Details\n order: 2\n columns: 2\n collapsible: true\n accounting:\n label: Accounting\n order: 3\n columns: 2\n classification:\n label: Classification\n order: 4\n columns: 2\n collapsible: true\n views:\n list:\n columns:\n - field: name\n width: 200\n link: true\n - field: customer_type\n width: 120\n - field: customer_group\n width: 140\n - field: credit_limit\n width: 130\n align: right\n - field: total_outstanding\n width: 130\n align: right\n - field: status\n width: 100\n filters:\n - field: customer_type\n type: select\n - field: customer_group\n type: text\n - field: status\n type: select\n - field: company_id\n type: link\n label: Company\n row_click: get-customer\n detail:\n header:\n title_field: name\n subtitle_field: customer_type\n status_field: status\n sections:\n - label: Details\n fields:\n - name\n - customer_type\n - customer_group\n - company_id\n columns: 2\n - label: Contact\n fields:\n - primary_address\n - primary_contact\n columns: 2\n - label: Accounting\n fields:\n - credit_limit\n - tax_id\n - exempt_from_sales_tax\n - payment_terms_id\n - total_outstanding\n - outstanding_invoice_count\n columns: 3\n actions:\n - action: update-customer\n label: Edit\n - action: add-quotation\n label: New Quotation\n primary: true\n - action: add-sales-order\n label: New Sales Order\n - action: create-sales-invoice\n label: New Invoice\n quotation:\n label: Quotation\n label_plural: Quotations\n icon: file-text\n table: quotation\n id_col: id\n name_col: naming_series\n primary_field: naming_series\n secondary_field: customer_id\n identifier_field: id\n status_field: status\n status_colors:\n draft: gray\n open: blue\n ordered: green\n expired: orange\n cancelled: red\n lifecycle: draft-submit-cancel\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n naming_series:\n type: text\n label: Quotation No.\n read_only: true\n in_list_view: true\n in_detail_view: true\n customer_id:\n type: link\n label: Customer\n required: true\n link_entity: customer\n link_display_field: name\n link_search_action: list-customers\n in_list_view: true\n in_detail_view: true\n in_form_view: true\n form_group: header\n form_order: 1\n quotation_date:\n type: date\n label: Quotation Date\n required: true\n default: today\n param_name: posting_date\n in_list_view: true\n in_detail_view: true\n in_form_view: true\n form_group: header\n form_order: 2\n valid_until:\n type: date\n label: Valid Until\n in_form_view: true\n in_detail_view: true\n form_group: header\n form_order: 3\n tax_template_id:\n type: link\n label: Tax Template\n link_entity: tax_template\n link_display_field: name\n in_form_view: true\n form_group: header\n form_order: 4\n total_amount:\n type: currency\n label: Net Total\n read_only: true\n precision: 2\n in_detail_view: true\n tax_amount:\n type: currency\n label: Tax\n read_only: true\n precision: 2\n in_detail_view: true\n grand_total:\n type: currency\n label: Grand Total\n read_only: true\n precision: 2\n emphasis: true\n in_list_view: true\n in_detail_view: true\n status:\n type: status\n label: Status\n read_only: true\n in_list_view: true\n in_detail_view: true\n converted_to:\n type: text\n label: Converted To SO\n read_only: true\n in_detail_view: true\n company_id:\n type: link\n label: Company\n required: true\n link_entity: company\n link_display_field: name\n in_form_view: true\n form_group: header\n form_order: 5\n created_at:\n type: datetime\n label: Created\n read_only: true\n in_detail_view: true\n form_groups:\n header:\n label: Quotation Details\n order: 1\n columns: 2\n items:\n label: Items\n order: 2\n type: child_table\n totals:\n label: Totals\n order: 3\n columns: 3\n views:\n list:\n columns:\n - field: naming_series\n width: 160\n link: true\n - field: customer_id\n width: 200\n - field: quotation_date\n width: 120\n - field: grand_total\n width: 130\n align: right\n - field: status\n width: 100\n filters:\n - field: status\n type: select\n - field: quotation_date\n type: date_range\n - field: customer_id\n type: link\n label: Customer\n - field: company_id\n type: link\n label: Company\n row_click: get-quotation\n detail:\n header:\n title_field: naming_series\n subtitle_field: customer_id\n status_field: status\n amount_field: grand_total\n sections:\n - label: Quotation Details\n fields:\n - customer_id\n - quotation_date\n - valid_until\n - company_id\n columns: 2\n - label: Items\n type: child_table\n child_entity: quotation_item\n child_fields:\n - item_id\n - item_name\n - quantity\n - uom\n - rate\n - discount_percentage\n - net_amount\n summary_fields:\n - field: net_amount\n aggregate: sum\n - label: Totals\n fields:\n - total_amount\n - tax_amount\n - grand_total\n columns: 3\n actions:\n - action: submit-quotation\n label: Submit\n requires_status: draft\n primary: true\n - action: convert-quotation-to-so\n label: Convert to Sales Order\n requires_status: open\n - action: update-quotation\n label: Edit\n requires_status: draft\n sales_order:\n label: Sales Order\n label_plural: Sales Orders\n icon: clipboard\n table: sales_order\n id_col: id\n name_col: naming_series\n primary_field: naming_series\n secondary_field: customer_id\n identifier_field: id\n status_field: status\n status_colors:\n draft: gray\n confirmed: blue\n partially_delivered: cyan\n fully_delivered: teal\n partially_invoiced: indigo\n fully_invoiced: green\n cancelled: red\n lifecycle: draft-submit-cancel\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n naming_series:\n type: text\n label: SO No.\n read_only: true\n in_list_view: true\n in_detail_view: true\n customer_id:\n type: link\n label: Customer\n required: true\n link_entity: customer\n link_display_field: name\n link_search_action: list-customers\n in_list_view: true\n in_detail_view: true\n in_form_view: true\n form_group: header\n form_order: 1\n order_date:\n type: date\n label: Order Date\n required: true\n default: today\n param_name: posting_date\n in_list_view: true\n in_detail_view: true\n in_form_view: true\n form_group: header\n form_order: 2\n delivery_date:\n type: date\n label: Delivery Date\n required: true\n in_list_view: true\n in_detail_view: true\n in_form_view: true\n form_group: header\n form_order: 3\n tax_template_id:\n type: link\n label: Tax Template\n link_entity: tax_template\n link_display_field: name\n in_form_view: true\n form_group: header\n form_order: 4\n total_amount:\n type: currency\n label: Net Total\n read_only: true\n precision: 2\n in_detail_view: true\n tax_amount:\n type: currency\n label: Tax\n read_only: true\n precision: 2\n in_detail_view: true\n grand_total:\n type: currency\n label: Grand Total\n read_only: true\n precision: 2\n emphasis: true\n in_list_view: true\n in_detail_view: true\n per_delivered:\n type: percent\n label: '% Delivered'\n read_only: true\n precision: 1\n in_list_view: true\n in_detail_view: true\n per_invoiced:\n type: percent\n label: '% Invoiced'\n read_only: true\n precision: 1\n in_list_view: true\n in_detail_view: true\n quotation_id:\n type: link\n label: Source Quotation\n link_entity: quotation\n link_display_field: naming_series\n read_only: true\n in_detail_view: true\n status:\n type: status\n label: Status\n read_only: true\n in_list_view: true\n in_detail_view: true\n company_id:\n type: link\n label: Company\n required: true\n link_entity: company\n link_display_field: name\n in_form_view: true\n form_group: header\n form_order: 5\n created_at:\n type: datetime\n label: Created\n read_only: true\n in_detail_view: true\n form_groups:\n header:\n label: Order Details\n order: 1\n columns: 2\n items:\n label: Items\n order: 2\n type: child_table\n totals:\n label: Totals\n order: 3\n columns: 3\n views:\n list:\n columns:\n - field: naming_series\n width: 150\n link: true\n - field: customer_id\n width: 200\n - field: order_date\n width: 120\n - field: delivery_date\n width: 120\n - field: grand_total\n width: 130\n align: right\n - field: per_delivered\n width: 100\n align: right\n - field: per_invoiced\n width: 100\n align: right\n - field: status\n width: 120\n filters:\n - field: status\n type: select\n - field: order_date\n type: date_range\n - field: customer_id\n type: link\n label: Customer\n - field: company_id\n type: link\n label: Company\n bulk_actions:\n - action: submit-sales-order\n label: Submit\n requires_status: draft\n - action: cancel-sales-order\n label: Cancel\n requires_status: confirmed\n destructive: true\n row_click: get-sales-order\n detail:\n header:\n title_field: naming_series\n subtitle_field: customer_id\n status_field: status\n amount_field: grand_total\n sections:\n - label: Order Details\n fields:\n - customer_id\n - order_date\n - delivery_date\n - quotation_id\n - company_id\n columns: 3\n - label: Items\n type: child_table\n child_entity: sales_order_item\n child_fields:\n - item_id\n - item_name\n - item_code\n - quantity\n - uom\n - rate\n - discount_percentage\n - net_amount\n - delivered_qty\n - invoiced_qty\n summary_fields:\n - field: net_amount\n aggregate: sum\n - label: Totals\n fields:\n - total_amount\n - tax_amount\n - grand_total\n columns: 3\n - label: Fulfillment\n fields:\n - per_delivered\n - per_invoiced\n columns: 2\n - label: Linked Delivery Notes\n type: related_list\n related_entity: delivery_note\n related_action: list-delivery-notes\n filter_field: sales_order_id\n - label: Linked Sales Invoices\n type: related_list\n related_entity: sales_invoice\n related_action: list-sales-invoices\n filter_field: sales_order_id\n actions:\n - action: submit-sales-order\n label: Submit\n requires_status: draft\n primary: true\n - action: cancel-sales-order\n label: Cancel\n requires_status: confirmed\n destructive: true\n - action: create-delivery-note\n label: Create Delivery Note\n requires_status:\n - confirmed\n - partially_delivered\n - action: create-sales-invoice\n label: Create Invoice\n requires_status:\n - confirmed\n - partially_delivered\n - fully_delivered\n - partially_invoiced\n - action: update-sales-order\n label: Edit\n requires_status: draft\n delivery_note:\n label: Delivery Note\n label_plural: Delivery Notes\n icon: truck\n table: delivery_note\n id_col: id\n name_col: naming_series\n primary_field: naming_series\n secondary_field: customer_id\n identifier_field: id\n status_field: status\n status_colors:\n draft: gray\n submitted: blue\n cancelled: red\n lifecycle: draft-submit-cancel\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n naming_series:\n type: text\n label: DN No.\n read_only: true\n in_list_view: true\n in_detail_view: true\n customer_id:\n type: link\n label: Customer\n required: true\n link_entity: customer\n link_display_field: name\n read_only: true\n in_list_view: true\n in_detail_view: true\n posting_date:\n type: date\n label: Posting Date\n required: true\n default: today\n in_list_view: true\n in_detail_view: true\n sales_order_id:\n type: link\n label: Sales Order\n required: true\n link_entity: sales_order\n link_display_field: naming_series\n link_search_action: list-sales-orders\n in_list_view: true\n in_detail_view: true\n in_form_view: true\n form_group: header\n form_order: 1\n total_qty:\n type: quantity\n label: Total Qty\n read_only: true\n precision: 2\n in_list_view: true\n in_detail_view: true\n status:\n type: status\n label: Status\n read_only: true\n in_list_view: true\n in_detail_view: true\n company_id:\n type: link\n label: Company\n read_only: true\n link_entity: company\n link_display_field: name\n in_detail_view: true\n created_at:\n type: datetime\n label: Created\n read_only: true\n in_detail_view: true\n form_groups:\n header:\n label: Delivery Details\n order: 1\n columns: 2\n items:\n label: Items\n order: 2\n type: child_table\n views:\n list:\n columns:\n - field: naming_series\n width: 150\n link: true\n - field: customer_id\n width: 200\n - field: posting_date\n width: 120\n - field: sales_order_id\n width: 150\n - field: total_qty\n width: 100\n align: right\n - field: status\n width: 100\n filters:\n - field: status\n type: select\n - field: posting_date\n type: date_range\n - field: customer_id\n type: link\n label: Customer\n - field: company_id\n type: link\n label: Company\n row_click: get-delivery-note\n detail:\n header:\n title_field: naming_series\n subtitle_field: customer_id\n status_field: status\n sections:\n - label: Delivery Details\n fields:\n - customer_id\n - posting_date\n - sales_order_id\n - company_id\n - total_qty\n columns: 3\n - label: Items\n type: child_table\n child_entity: delivery_note_item\n child_fields:\n - item_id\n - item_name\n - item_code\n - quantity\n - uom\n - warehouse_id\n - rate\n - amount\n summary_fields:\n - field: quantity\n aggregate: sum\n actions:\n - action: submit-delivery-note\n label: Submit\n requires_status: draft\n primary: true\n - action: cancel-delivery-note\n label: Cancel\n requires_status: submitted\n destructive: true\n - action: create-sales-invoice\n label: Create Invoice\n requires_status: submitted\n sales_invoice:\n label: Sales Invoice\n label_plural: Sales Invoices\n icon: file-text\n table: sales_invoice\n id_col: id\n name_col: naming_series\n primary_field: naming_series\n secondary_field: customer_id\n identifier_field: id\n status_field: status\n status_colors:\n draft: gray\n submitted: blue\n overdue: orange\n partially_paid: yellow\n paid: green\n cancelled: red\n lifecycle: draft-submit-cancel\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n naming_series:\n type: text\n label: Invoice No.\n read_only: true\n in_list_view: true\n in_detail_view: true\n customer_id:\n type: link\n label: Customer\n required: true\n link_entity: customer\n link_display_field: name\n link_search_action: list-customers\n link_create_action: add-customer\n in_list_view: true\n in_detail_view: true\n in_form_view: true\n form_group: header\n form_order: 1\n posting_date:\n type: date\n label: Invoice Date\n required: true\n default: today\n in_list_view: true\n in_detail_view: true\n in_form_view: true\n form_group: header\n form_order: 2\n due_date:\n type: date\n label: Due Date\n required: false\n help_text: 'Auto-calculated from payment terms if blank (default: +30 days)'\n in_list_view: true\n in_detail_view: true\n in_form_view: true\n form_group: header\n form_order: 3\n tax_template_id:\n type: link\n label: Tax Template\n link_entity: tax_template\n link_display_field: name\n in_form_view: true\n in_detail_view: true\n form_group: header\n form_order: 4\n sales_order_id:\n type: link\n label: Sales Order\n link_entity: sales_order\n link_display_field: naming_series\n link_search_action: list-sales-orders\n help_text: Create invoice from a sales order\n in_form_view: true\n in_detail_view: true\n form_group: source\n form_order: 1\n delivery_note_id:\n type: link\n label: Delivery Note\n link_entity: delivery_note\n link_display_field: naming_series\n link_search_action: list-delivery-notes\n help_text: Create invoice from a delivery note\n in_form_view: true\n in_detail_view: true\n form_group: source\n form_order: 2\n payment_terms_id:\n type: link\n label: Payment Terms\n link_entity: payment_terms\n link_display_field: name\n in_detail_view: true\n total_amount:\n type: currency\n label: Net Total\n read_only: true\n precision: 2\n in_detail_view: true\n tax_amount:\n type: currency\n label: Total Tax\n read_only: true\n precision: 2\n in_detail_view: true\n grand_total:\n type: currency\n label: Grand Total\n read_only: true\n precision: 2\n emphasis: true\n in_list_view: true\n in_detail_view: true\n outstanding_amount:\n type: currency\n label: Outstanding\n read_only: true\n precision: 2\n in_list_view: true\n in_detail_view: true\n is_return:\n type: boolean\n label: Is Return (Credit Note)\n read_only: true\n in_detail_view: true\n return_against:\n type: link\n label: Return Against\n link_entity: sales_invoice\n link_display_field: naming_series\n read_only: true\n in_detail_view: true\n update_stock:\n type: boolean\n label: Update Stock\n read_only: true\n in_detail_view: true\n status:\n type: status\n label: Status\n read_only: true\n in_list_view: true\n in_detail_view: true\n company_id:\n type: link\n label: Company\n required: true\n link_entity: company\n link_display_field: name\n in_form_view: true\n form_group: header\n form_order: 5\n created_at:\n type: datetime\n label: Created\n read_only: true\n in_detail_view: true\n form_groups:\n header:\n label: Invoice Details\n order: 1\n columns: 2\n source:\n label: Source Document\n order: 2\n columns: 2\n collapsible: true\n help_text: Create from SO or DN, or leave blank for standalone invoice\n items:\n label: Items\n order: 3\n type: child_table\n totals:\n label: Totals\n order: 4\n columns: 3\n views:\n list:\n columns:\n - field: naming_series\n width: 160\n link: true\n - field: customer_id\n width: 200\n - field: posting_date\n width: 120\n - field: due_date\n width: 120\n - field: grand_total\n width: 130\n align: right\n - field: outstanding_amount\n width: 130\n align: right\n - field: status\n width: 110\n filters:\n - field: status\n type: select\n - field: posting_date\n type: date_range\n - field: customer_id\n type: link\n label: Customer\n - field: company_id\n type: link\n label: Company\n bulk_actions:\n - action: submit-sales-invoice\n label: Submit\n requires_status: draft\n - action: cancel-sales-invoice\n label: Cancel\n requires_status: submitted\n destructive: true\n row_click: get-sales-invoice\n detail:\n header:\n title_field: naming_series\n subtitle_field: customer_id\n status_field: status\n amount_field: grand_total\n sections:\n - label: Invoice Details\n fields:\n - customer_id\n - posting_date\n - due_date\n - company_id\n columns: 2\n - label: Source\n fields:\n - sales_order_id\n - delivery_note_id\n - payment_terms_id\n columns: 3\n collapsible: true\n - label: Items\n type: child_table\n child_entity: sales_invoice_item\n child_fields:\n - item_id\n - item_name\n - item_code\n - quantity\n - uom\n - rate\n - discount_percentage\n - net_amount\n summary_fields:\n - field: net_amount\n aggregate: sum\n - label: Totals\n fields:\n - total_amount\n - tax_amount\n - grand_total\n - outstanding_amount\n columns: 4\n - label: Returns\n fields:\n - is_return\n - return_against\n - update_stock\n columns: 3\n collapsible: true\n - label: Payments\n type: related_list\n related_entity: payment_ledger_entry\n related_fields:\n - posting_date\n - voucher_type\n - amount\n - currency\n - remarks\n actions:\n - action: submit-sales-invoice\n label: Submit\n requires_status: draft\n primary: true\n - action: cancel-sales-invoice\n label: Cancel\n requires_status:\n - submitted\n - overdue\n - partially_paid\n destructive: true\n - action: create-credit-note\n label: Create Credit Note\n requires_status:\n - submitted\n - overdue\n - partially_paid\n - paid\n - action: update-sales-invoice\n label: Edit\n requires_status: draft\n form:\n groups:\n - label: Invoice Details\n fields:\n - customer_id\n - posting_date\n - due_date\n - company_id\n columns: 2\n - label: Source Document\n fields:\n - sales_order_id\n - delivery_note_id\n columns: 2\n collapsible: true\n - label: Items\n type: child_table\n child_entity: sales_invoice_item\n add_label: Add Item\n fields:\n - item_id\n - quantity\n - rate\n - discount_percentage\n computed_fields:\n - net_amount\n - label: Tax\n fields:\n - tax_template_id\n credit_note:\n label: Credit Note\n label_plural: Credit Notes\n icon: file-minus\n table: sales_invoice\n id_col: id\n name_col: naming_series\n primary_field: naming_series\n secondary_field: customer_id\n identifier_field: id\n status_field: status\n status_colors:\n draft: gray\n submitted: blue\n cancelled: red\n lifecycle: draft-submit-cancel\n filter_condition: is_return = 1\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n naming_series:\n type: text\n label: Credit Note No.\n read_only: true\n in_list_view: true\n in_detail_view: true\n customer_id:\n type: link\n label: Customer\n read_only: true\n link_entity: customer\n link_display_field: name\n in_list_view: true\n in_detail_view: true\n posting_date:\n type: date\n label: Date\n read_only: true\n in_list_view: true\n in_detail_view: true\n return_against:\n type: link\n label: Against Invoice\n required: true\n link_entity: sales_invoice\n link_display_field: naming_series\n in_list_view: true\n in_detail_view: true\n in_form_view: true\n form_group: header\n form_order: 1\n grand_total:\n type: currency\n label: Credit Amount\n read_only: true\n precision: 2\n in_list_view: true\n in_detail_view: true\n status:\n type: status\n label: Status\n read_only: true\n in_list_view: true\n in_detail_view: true\n form_groups:\n header:\n label: Credit Note Details\n order: 1\n columns: 2\n items:\n label: Return Items\n order: 2\n type: child_table\n views:\n list:\n columns:\n - field: naming_series\n width: 160\n link: true\n - field: customer_id\n width: 200\n - field: posting_date\n width: 120\n - field: return_against\n width: 160\n - field: grand_total\n width: 130\n align: right\n - field: status\n width: 100\n filters:\n - field: status\n type: select\n - field: posting_date\n type: date_range\n - field: customer_id\n type: link\n label: Customer\n row_click: get-sales-invoice\n detail:\n header:\n title_field: naming_series\n subtitle_field: customer_id\n status_field: status\n amount_field: grand_total\n sections:\n - label: Details\n fields:\n - customer_id\n - posting_date\n - return_against\n columns: 3\n - label: Items\n type: child_table\n child_entity: sales_invoice_item\n child_fields:\n - item_id\n - item_name\n - quantity\n - rate\n - net_amount\n - label: Totals\n fields:\n - grand_total\n columns: 1\n actions:\n - action: submit-sales-invoice\n label: Submit\n requires_status: draft\n primary: true\n - action: cancel-sales-invoice\n label: Cancel\n requires_status: submitted\n destructive: true\n sales_partner:\n label: Sales Partner\n label_plural: Sales Partners\n icon: handshake\n table: sales_partner\n id_col: id\n name_col: name\n primary_field: name\n secondary_field: commission_rate\n identifier_field: id\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n name:\n type: text\n label: Partner Name\n required: true\n max_length: 140\n searchable: true\n in_list_view: true\n in_detail_view: true\n in_form_view: true\n form_group: basic\n form_order: 1\n commission_rate:\n type: percent\n label: Commission Rate\n required: true\n min: '0'\n max: '100'\n precision: 2\n in_list_view: true\n in_detail_view: true\n in_form_view: true\n form_group: basic\n form_order: 2\n created_at:\n type: datetime\n label: Created\n read_only: true\n in_detail_view: true\n form_groups:\n basic:\n label: Partner Details\n order: 1\n columns: 2\n views:\n list:\n columns:\n - field: name\n width: 250\n link: true\n - field: commission_rate\n width: 150\n align: right\n filters: []\n row_click: null\n recurring_invoice_template:\n label: Recurring Invoice Template\n label_plural: Recurring Invoice Templates\n table: recurring_invoice_template\n id_col: id\n name_col: id\n primary_field: id\n identifier_field: id\n status_field: status\n status_colors:\n draft: gray\n submitted: green\n cancelled: red\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n naming_series:\n type: text\n label: Naming Series\n read_only: true\n customer_id:\n type: link\n label: Customer ID\n link_entity: customer\n link_display_field: name\n link_search_action: list-customers\n in_form_view: true\n form_group: references\n form_order: 1\n frequency:\n type: select\n label: Frequency\n required: true\n options:\n - value: weekly\n label: Weekly\n - value: monthly\n label: Monthly\n - value: quarterly\n label: Quarterly\n - value: semi_annually\n label: Semi Annually\n - value: annually\n label: Annually\n in_form_view: true\n form_group: details\n form_order: 2\n start_date:\n type: date\n label: Start Date\n in_list_view: true\n in_form_view: true\n form_group: header\n form_order: 3\n end_date:\n type: date\n label: End Date\n in_form_view: true\n form_group: header\n form_order: 4\n next_invoice_date:\n type: date\n label: Next Invoice Date\n in_form_view: true\n form_group: header\n form_order: 5\n last_invoice_date:\n type: date\n label: Last Invoice Date\n in_form_view: true\n form_group: header\n form_order: 6\n tax_template_id:\n type: link\n label: Tax Template ID\n link_entity: tax_template\n link_display_field: name\n link_search_action: list-tax-templates\n in_form_view: true\n form_group: references\n form_order: 7\n payment_terms_id:\n type: link\n label: Payment Terms ID\n link_entity: payment_terms\n link_display_field: name\n link_search_action: list-payment-termses\n in_form_view: true\n form_group: references\n form_order: 8\n status:\n type: select\n label: Status\n options:\n - value: draft\n label: Draft\n - value: active\n label: Active\n - value: paused\n label: Paused\n - value: completed\n label: Completed\n - value: cancelled\n label: Cancelled\n in_list_view: true\n in_form_view: true\n form_group: header\n form_order: 9\n company_id:\n type: link\n label: Company ID\n link_entity: company\n link_display_field: name\n link_search_action: list-companies\n in_form_view: true\n form_group: references\n form_order: 10\n form_groups:\n header:\n label: Basic Information\n order: 1\n columns: 2\n details:\n label: Details\n order: 2\n columns: 2\n references:\n label: References\n order: 3\n columns: 2\n items:\n label: Line Items\n order: 4\n type: child_table\n supplier:\n label: Supplier\n label_plural: Suppliers\n icon: truck\n primary_field: name\n secondary_field: supplier_group\n identifier_field: id\n status_field: status\n status_colors:\n active: green\n disabled: gray\n blocked: red\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n name:\n type: text\n label: Supplier Name\n required: true\n max_length: 140\n searchable: true\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 1\n supplier_group:\n type: select\n label: Supplier Group\n options:\n - value: Raw-Material\n label: Raw Material\n - value: Services\n label: Services\n - value: Consumables\n label: Consumables\n - value: Sub-Contracting\n label: Sub-Contracting\n - value: Hardware\n label: Hardware\n - value: Software\n label: Software\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 2\n supplier_type:\n type: select\n label: Supplier Type\n options:\n - value: company\n label: Company\n - value: individual\n label: Individual\n default: company\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 3\n payment_terms_id:\n type: link\n label: Payment Terms\n link_entity: payment_terms\n link_display_field: name\n link_search_action: list-payment-terms\n in_form_view: true\n form_group: basic\n form_order: 4\n tax_id:\n type: text\n label: Tax ID / EIN\n max_length: 20\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 5\n is_1099_vendor:\n type: boolean\n label: 1099 Vendor\n default: false\n help_text: Mark for US 1099 tax reporting\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 6\n primary_address:\n type: json\n label: Primary Address\n help_text: JSON object with address_line1, city, state, zip, country\n in_form_view: true\n form_group: address\n form_order: 1\n status:\n type: status\n label: Status\n read_only: true\n in_list_view: true\n in_detail_view: true\n company_id:\n type: link\n label: Company\n link_entity: company\n link_display_field: name\n link_search_action: list-companies\n required: true\n in_form_view: true\n form_group: basic\n form_order: 7\n total_outstanding:\n type: currency\n label: Total Outstanding\n precision: 2\n read_only: true\n in_detail_view: true\n outstanding_invoice_count:\n type: integer\n label: Outstanding Invoices\n read_only: true\n in_detail_view: true\n created_at:\n type: datetime\n label: Created\n read_only: true\n in_detail_view: true\n updated_at:\n type: datetime\n label: Last Updated\n read_only: true\n in_detail_view: true\n form_groups:\n basic:\n label: Supplier Information\n order: 1\n columns: 2\n address:\n label: Address\n order: 2\n columns: 1\n collapsible: true\n views:\n list:\n columns:\n - field: name\n width: 200\n link: true\n - field: supplier_group\n width: 130\n - field: supplier_type\n width: 100\n - field: tax_id\n width: 120\n - field: is_1099_vendor\n width: 80\n - field: status\n width: 90\n filters:\n - field: supplier_group\n type: select\n - field: supplier_type\n type: select\n - field: status\n type: select\n - field: company_id\n type: link\n row_click: get-supplier\n detail:\n header:\n title_field: name\n subtitle_field: supplier_group\n status_field: status\n sections:\n - label: Supplier Details\n fields:\n - name\n - supplier_group\n - supplier_type\n - tax_id\n - is_1099_vendor\n - company_id\n columns: 3\n - label: Payment & Terms\n fields:\n - payment_terms_id\n - total_outstanding\n - outstanding_invoice_count\n columns: 3\n - label: Address\n fields:\n - primary_address\n columns: 1\n collapsible: true\n actions:\n - action: update-supplier\n label: Edit\n - action: add-purchase-order\n label: New Purchase Order\n prefill:\n supplier_id: id\n material_request:\n label: Material Request\n label_plural: Material Requests\n icon: clipboard-list\n primary_field: naming_series\n secondary_field: request_type\n identifier_field: id\n status_field: status\n lifecycle: draft-submit-cancel\n status_colors:\n draft: gray\n submitted: blue\n cancelled: red\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n naming_series:\n type: text\n label: MR Number\n read_only: true\n in_list_view: true\n in_detail_view: true\n request_type:\n type: select\n label: Request Type\n required: true\n options:\n - value: purchase\n label: Purchase\n - value: transfer\n label: Transfer\n - value: manufacture\n label: Manufacture\n - value: material_transfer\n label: Material Transfer\n - value: material_issue\n label: Material Issue\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 1\n status:\n type: status\n label: Status\n read_only: true\n in_list_view: true\n in_detail_view: true\n company_id:\n type: link\n label: Company\n link_entity: company\n link_display_field: name\n link_search_action: list-companies\n required: true\n in_form_view: true\n form_group: basic\n form_order: 2\n items:\n type: json\n label: Items\n required: true\n help_text: 'JSON array: [{item_id, qty, warehouse_id}]'\n in_form_view: true\n form_group: items\n form_order: 1\n created_at:\n type: datetime\n label: Created\n read_only: true\n in_list_view: true\n in_detail_view: true\n updated_at:\n type: datetime\n label: Last Updated\n read_only: true\n in_detail_view: true\n form_groups:\n basic:\n label: Request Details\n order: 1\n columns: 2\n items:\n label: Items\n order: 2\n columns: 1\n views:\n list:\n columns:\n - field: naming_series\n width: 140\n link: true\n - field: request_type\n width: 140\n - field: status\n width: 100\n - field: created_at\n width: 160\n filters:\n - field: request_type\n type: select\n - field: status\n type: select\n - field: company_id\n type: link\n request_for_quotation:\n label: Request for Quotation\n label_plural: RFQs\n icon: file-question\n primary_field: naming_series\n secondary_field: rfq_date\n identifier_field: id\n status_field: status\n lifecycle: draft-submit-cancel\n status_colors:\n draft: gray\n submitted: blue\n quotation_received: green\n cancelled: red\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n naming_series:\n type: text\n label: RFQ Number\n read_only: true\n in_list_view: true\n in_detail_view: true\n rfq_date:\n type: date\n label: RFQ Date\n read_only: true\n in_list_view: true\n in_detail_view: true\n status:\n type: status\n label: Status\n read_only: true\n in_list_view: true\n in_detail_view: true\n company_id:\n type: link\n label: Company\n link_entity: company\n link_display_field: name\n link_search_action: list-companies\n required: true\n in_form_view: true\n form_group: basic\n form_order: 1\n items:\n type: json\n label: Items\n required: true\n help_text: 'JSON array: [{item_id, qty, uom, required_date}]'\n in_form_view: true\n form_group: items\n form_order: 1\n suppliers:\n type: json\n label: Suppliers\n required: true\n help_text: JSON array of supplier IDs\n in_form_view: true\n form_group: suppliers\n form_order: 1\n created_at:\n type: datetime\n label: Created\n read_only: true\n in_detail_view: true\n updated_at:\n type: datetime\n label: Last Updated\n read_only: true\n in_detail_view: true\n form_groups:\n basic:\n label: RFQ Details\n order: 1\n columns: 2\n items:\n label: Items\n order: 2\n columns: 1\n suppliers:\n label: Suppliers\n order: 3\n columns: 1\n views:\n list:\n columns:\n - field: naming_series\n width: 140\n link: true\n - field: rfq_date\n width: 120\n - field: status\n width: 130\n - field: created_at\n width: 160\n filters:\n - field: status\n type: select\n - field: company_id\n type: link\n supplier_quotation:\n label: Supplier Quotation\n label_plural: Supplier Quotations\n icon: file-text\n primary_field: supplier_name\n secondary_field: total_amount\n identifier_field: id\n status_field: status\n status_colors:\n draft: gray\n submitted: blue\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n supplier_id:\n type: link\n label: Supplier\n link_entity: supplier\n link_display_field: name\n link_search_action: list-suppliers\n required: true\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 1\n supplier_name:\n type: text\n label: Supplier Name\n read_only: true\n in_list_view: true\n in_detail_view: true\n rfq_id:\n type: link\n label: RFQ\n link_entity: request_for_quotation\n link_display_field: naming_series\n link_search_action: list-rfqs\n required: true\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 2\n quotation_date:\n type: date\n label: Quotation Date\n read_only: true\n in_list_view: true\n in_detail_view: true\n total_amount:\n type: currency\n label: Total Amount\n precision: 2\n read_only: true\n in_list_view: true\n in_detail_view: true\n grand_total:\n type: currency\n label: Grand Total\n precision: 2\n read_only: true\n in_detail_view: true\n status:\n type: status\n label: Status\n read_only: true\n in_list_view: true\n in_detail_view: true\n company_id:\n type: link\n label: Company\n link_entity: company\n link_display_field: name\n link_search_action: list-companies\n read_only: true\n in_detail_view: true\n items:\n type: json\n label: Items\n required: true\n help_text: 'JSON array: [{rfq_item_id, rate, lead_time_days}]'\n in_form_view: true\n form_group: items\n form_order: 1\n created_at:\n type: datetime\n label: Created\n read_only: true\n in_detail_view: true\n form_groups:\n basic:\n label: Quotation Details\n order: 1\n columns: 2\n items:\n label: Quoted Items\n order: 2\n columns: 1\n views:\n list:\n columns:\n - field: supplier_name\n width: 180\n link: true\n - field: quotation_date\n width: 120\n - field: total_amount\n width: 130\n align: right\n - field: status\n width: 100\n filters:\n - field: rfq_id\n type: link\n - field: supplier_id\n type: link\n purchase_order:\n label: Purchase Order\n label_plural: Purchase Orders\n icon: file-plus\n primary_field: naming_series\n secondary_field: supplier_name\n identifier_field: id\n status_field: status\n lifecycle: draft-submit-cancel\n status_colors:\n draft: gray\n confirmed: blue\n partially_received: yellow\n fully_received: green\n partially_invoiced: orange\n fully_invoiced: teal\n cancelled: red\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n naming_series:\n type: text\n label: PO Number\n read_only: true\n in_list_view: true\n in_detail_view: true\n supplier_id:\n type: link\n label: Supplier\n link_entity: supplier\n link_display_field: name\n link_search_action: list-suppliers\n required: true\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 1\n supplier_name:\n type: text\n label: Supplier Name\n read_only: true\n in_list_view: true\n order_date:\n type: date\n label: Order Date\n default: today\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 2\n tax_template_id:\n type: link\n label: Tax Template\n link_entity: tax_template\n link_display_field: name\n link_search_action: list-tax-templates\n in_form_view: true\n form_group: basic\n form_order: 3\n total_amount:\n type: currency\n label: Net Total\n precision: 2\n read_only: true\n in_list_view: true\n in_detail_view: true\n tax_amount:\n type: currency\n label: Tax Amount\n precision: 2\n read_only: true\n in_detail_view: true\n grand_total:\n type: currency\n label: Grand Total\n precision: 2\n read_only: true\n in_list_view: true\n in_detail_view: true\n per_received:\n type: percent\n label: '% Received'\n precision: 2\n read_only: true\n in_detail_view: true\n per_invoiced:\n type: percent\n label: '% Invoiced'\n precision: 2\n read_only: true\n in_detail_view: true\n status:\n type: status\n label: Status\n read_only: true\n in_list_view: true\n in_detail_view: true\n company_id:\n type: link\n label: Company\n link_entity: company\n link_display_field: name\n link_search_action: list-companies\n required: true\n in_form_view: true\n form_group: basic\n form_order: 4\n items:\n type: json\n label: Items\n required: true\n help_text: 'JSON array: [{item_id, qty, rate, uom, warehouse_id, discount_percentage, required_date}]'\n in_form_view: true\n form_group: items\n form_order: 1\n created_at:\n type: datetime\n label: Created\n read_only: true\n in_detail_view: true\n updated_at:\n type: datetime\n label: Last Updated\n read_only: true\n in_detail_view: true\n form_groups:\n basic:\n label: Order Details\n order: 1\n columns: 2\n items:\n label: Items\n order: 2\n columns: 1\n views:\n list:\n columns:\n - field: naming_series\n width: 140\n link: true\n - field: supplier_name\n width: 180\n - field: order_date\n width: 110\n - field: grand_total\n width: 130\n align: right\n - field: status\n width: 130\n filters:\n - field: status\n type: select\n - field: supplier_id\n type: link\n - field: company_id\n type: link\n - field: order_date\n type: date_range\n row_click: get-purchase-order\n detail:\n header:\n title_field: naming_series\n subtitle_field: supplier_name\n status_field: status\n sections:\n - label: Order Details\n fields:\n - supplier_id\n - order_date\n - tax_template_id\n - company_id\n columns: 2\n - label: Amounts\n fields:\n - total_amount\n - tax_amount\n - grand_total\n columns: 3\n - label: Fulfillment\n fields:\n - per_received\n - per_invoiced\n columns: 2\n - label: Items\n child_entity: purchase_order_item\n columns: 1\n - label: Linked Receipts\n related_entity: purchase_receipt\n related_filter: purchase_order_id\n columns: 1\n collapsible: true\n - label: Linked Invoices\n related_entity: purchase_invoice\n related_filter: purchase_order_id\n columns: 1\n collapsible: true\n actions:\n - action: update-purchase-order\n label: Edit\n condition: status == 'draft'\n - action: submit-purchase-order\n label: Submit\n condition: status == 'draft'\n confirm: true\n - action: cancel-purchase-order\n label: Cancel\n condition: status != 'draft' && status != 'cancelled'\n confirm: true\n variant: danger\n - action: create-purchase-receipt\n label: Receive Goods\n condition: status == 'confirmed' || status == 'partially_received'\n - action: create-purchase-invoice\n label: Create Invoice\n condition: status != 'draft' && status != 'cancelled'\n purchase_receipt:\n label: Purchase Receipt\n label_plural: Purchase Receipts\n icon: package-check\n primary_field: naming_series\n secondary_field: supplier_name\n identifier_field: id\n status_field: status\n lifecycle: draft-submit-cancel\n status_colors:\n draft: gray\n submitted: green\n cancelled: red\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n naming_series:\n type: text\n label: GRN Number\n read_only: true\n in_list_view: true\n in_detail_view: true\n supplier_id:\n type: link\n label: Supplier\n link_entity: supplier\n link_display_field: name\n link_search_action: list-suppliers\n read_only: true\n in_list_view: true\n in_detail_view: true\n supplier_name:\n type: text\n label: Supplier Name\n read_only: true\n in_list_view: true\n posting_date:\n type: date\n label: Posting Date\n default: today\n in_list_view: true\n in_detail_view: true\n purchase_order_id:\n type: link\n label: Purchase Order\n link_entity: purchase_order\n link_display_field: naming_series\n link_search_action: list-purchase-orders\n required: true\n in_form_view: true\n form_group: basic\n form_order: 1\n total_qty:\n type: quantity\n label: Total Qty\n precision: 2\n read_only: true\n in_list_view: true\n in_detail_view: true\n status:\n type: status\n label: Status\n read_only: true\n in_list_view: true\n in_detail_view: true\n company_id:\n type: link\n label: Company\n link_entity: company\n link_display_field: name\n link_search_action: list-companies\n read_only: true\n in_detail_view: true\n items:\n type: json\n label: Items (partial receipt)\n help_text: 'Optional JSON array for partial receipt: [{purchase_order_item_id, qty, warehouse_id, batch_id, serial_numbers}]'\n in_form_view: true\n form_group: items\n form_order: 1\n created_at:\n type: datetime\n label: Created\n read_only: true\n in_detail_view: true\n updated_at:\n type: datetime\n label: Last Updated\n read_only: true\n in_detail_view: true\n form_groups:\n basic:\n label: Receipt Details\n order: 1\n columns: 2\n items:\n label: Items (Partial Receipt Override)\n order: 2\n columns: 1\n collapsible: true\n views:\n list:\n columns:\n - field: naming_series\n width: 140\n link: true\n - field: supplier_name\n width: 180\n - field: posting_date\n width: 110\n - field: total_qty\n width: 100\n align: right\n - field: status\n width: 100\n filters:\n - field: status\n type: select\n - field: supplier_id\n type: link\n - field: company_id\n type: link\n row_click: get-purchase-receipt\n detail:\n header:\n title_field: naming_series\n subtitle_field: supplier_name\n status_field: status\n sections:\n - label: Receipt Details\n fields:\n - supplier_id\n - posting_date\n - purchase_order_id\n - total_qty\n - company_id\n columns: 3\n - label: Items\n child_entity: purchase_receipt_item\n columns: 1\n actions:\n - action: submit-purchase-receipt\n label: Submit\n condition: status == 'draft'\n confirm: true\n - action: cancel-purchase-receipt\n label: Cancel\n condition: status == 'submitted'\n confirm: true\n variant: danger\n - action: create-purchase-invoice\n label: Create Invoice\n condition: status == 'submitted'\n purchase_invoice:\n label: Purchase Invoice\n label_plural: Purchase Invoices\n icon: file-invoice\n primary_field: naming_series\n secondary_field: supplier_name\n identifier_field: id\n status_field: status\n lifecycle: draft-submit-cancel\n status_colors:\n draft: gray\n submitted: blue\n overdue: red\n partially_paid: orange\n paid: green\n cancelled: red\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n naming_series:\n type: text\n label: Invoice Number\n read_only: true\n in_list_view: true\n in_detail_view: true\n supplier_id:\n type: link\n label: Supplier\n link_entity: supplier\n link_display_field: name\n link_search_action: list-suppliers\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 1\n supplier_name:\n type: text\n label: Supplier Name\n read_only: true\n in_list_view: true\n posting_date:\n type: date\n label: Posting Date\n default: today\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 2\n due_date:\n type: date\n label: Due Date\n in_list_view: true\n in_form_view: true\n form_group: basic\n form_order: 3\n purchase_order_id:\n type: link\n label: Purchase Order\n link_entity: purchase_order\n link_display_field: naming_series\n link_search_action: list-purchase-orders\n in_form_view: true\n form_group: source\n form_order: 1\n purchase_receipt_id:\n type: link\n label: Purchase Receipt\n link_entity: purchase_receipt\n link_display_field: naming_series\n link_search_action: list-purchase-receipts\n in_form_view: true\n form_group: source\n form_order: 2\n tax_template_id:\n type: link\n label: Tax Template\n link_entity: tax_template\n link_display_field: name\n link_search_action: list-tax-templates\n in_form_view: true\n form_group: basic\n form_order: 4\n total_amount:\n type: currency\n label: Net Total\n precision: 2\n read_only: true\n in_list_view: true\n in_detail_view: true\n tax_amount:\n type: currency\n label: Tax Amount\n precision: 2\n read_only: true\n in_detail_view: true\n grand_total:\n type: currency\n label: Grand Total\n precision: 2\n read_only: true\n in_list_view: true\n in_detail_view: true\n outstanding_amount:\n type: currency\n label: Outstanding\n precision: 2\n read_only: true\n in_list_view: true\n in_detail_view: true\n update_stock:\n type: boolean\n label: Update Stock\n read_only: true\n in_detail_view: true\n help_text: If true, SLE entries were posted on submit\n is_return:\n type: boolean\n label: Is Return (Debit Note)\n read_only: true\n in_detail_view: true\n return_against:\n type: link\n label: Return Against Invoice\n link_entity: purchase_invoice\n link_display_field: naming_series\n link_search_action: list-purchase-invoices\n read_only: true\n in_detail_view: true\n status:\n type: status\n label: Status\n read_only: true\n in_list_view: true\n in_detail_view: true\n company_id:\n type: link\n label: Company\n link_entity: company\n link_display_field: name\n link_search_action: list-companies\n required: true\n in_form_view: true\n form_group: basic\n form_order: 5\n items:\n type: json\n label: Items\n help_text: 'JSON array: [{item_id, qty, rate, uom, expense_account_id, cost_center_id}]'\n in_form_view: true\n form_group: items\n form_order: 1\n created_at:\n type: datetime\n label: Created\n read_only: true\n in_detail_view: true\n updated_at:\n type: datetime\n label: Last Updated\n read_only: true\n in_detail_view: true\n form_groups:\n basic:\n label: Invoice Details\n order: 1\n columns: 2\n source:\n label: Source Documents\n order: 2\n columns: 2\n collapsible: true\n help_text: Link to PO or Purchase Receipt, or leave blank for standalone invoice\n items:\n label: Items\n order: 3\n columns: 1\n views:\n list:\n columns:\n - field: naming_series\n width: 140\n link: true\n - field: supplier_name\n width: 180\n - field: posting_date\n width: 110\n - field: grand_total\n width: 130\n align: right\n - field: outstanding_amount\n width: 130\n align: right\n - field: status\n width: 120\n filters:\n - field: status\n type: select\n - field: supplier_id\n type: link\n - field: company_id\n type: link\n - field: posting_date\n type: date_range\n row_click: get-purchase-invoice\n detail:\n header:\n title_field: naming_series\n subtitle_field: supplier_name\n status_field: status\n sections:\n - label: Invoice Details\n fields:\n - supplier_id\n - posting_date\n - due_date\n - tax_template_id\n - company_id\n columns: 3\n - label: Source Documents\n fields:\n - purchase_order_id\n - purchase_receipt_id\n columns: 2\n collapsible: true\n - label: Amounts\n fields:\n - total_amount\n - tax_amount\n - grand_total\n - outstanding_amount\n columns: 4\n - label: Flags\n fields:\n - update_stock\n - is_return\n - return_against\n columns: 3\n collapsible: true\n - label: Items\n child_entity: purchase_invoice_item\n columns: 1\n - label: Payments\n related_entity: payment_ledger_entry\n related_filter: against_voucher_id\n columns: 1\n collapsible: true\n actions:\n - action: update-purchase-invoice\n label: Edit\n condition: status == 'draft'\n - action: submit-purchase-invoice\n label: Submit\n condition: status == 'draft'\n confirm: true\n - action: cancel-purchase-invoice\n label: Cancel\n condition: status in ['submitted','overdue','partially_paid']\n confirm: true\n variant: danger\n - action: create-debit-note\n label: Create Debit Note\n condition: status in ['submitted','partially_paid','paid','overdue'] && !is_return\n landed_cost_voucher:\n label: Landed Cost Voucher\n label_plural: Landed Cost Vouchers\n icon: anchor\n primary_field: id\n secondary_field: total_landed_cost\n identifier_field: id\n status_field: status\n status_colors:\n submitted: green\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n posting_date:\n type: date\n label: Posting Date\n read_only: true\n in_list_view: true\n in_detail_view: true\n total_landed_cost:\n type: currency\n label: Total Landed Cost\n precision: 2\n read_only: true\n in_list_view: true\n in_detail_view: true\n status:\n type: status\n label: Status\n read_only: true\n in_list_view: true\n in_detail_view: true\n company_id:\n type: link\n label: Company\n link_entity: company\n link_display_field: name\n link_search_action: list-companies\n required: true\n in_form_view: true\n form_group: basic\n form_order: 1\n purchase_receipt_ids:\n type: json\n label: Purchase Receipts\n required: true\n help_text: JSON array of purchase receipt IDs\n in_form_view: true\n form_group: basic\n form_order: 2\n charges:\n type: json\n label: Charges\n required: true\n help_text: 'JSON array: [{description, amount, expense_account_id, allocation_method}]'\n in_form_view: true\n form_group: charges\n form_order: 1\n created_at:\n type: datetime\n label: Created\n read_only: true\n in_detail_view: true\n form_groups:\n basic:\n label: Voucher Details\n order: 1\n columns: 2\n charges:\n label: Charges\n order: 2\n columns: 1\n views:\n list:\n columns:\n - field: posting_date\n width: 120\n - field: total_landed_cost\n width: 150\n align: right\n - field: status\n width: 100\n - field: created_at\n width: 160\n detail:\n header:\n title_field: id\n subtitle_field: posting_date\n status_field: status\n sections:\n - label: Voucher Details\n fields:\n - posting_date\n - total_landed_cost\n - company_id\n columns: 3\n - label: Charges\n child_entity: landed_cost_charge\n columns: 1\n - label: Allocated Items\n child_entity: landed_cost_item\n columns: 1\n meter:\n label: Meter\n label_plural: Meters\n table: meter\n id_col: id\n name_col: id\n primary_field: id\n identifier_field: id\n status_field: status\n status_colors:\n draft: gray\n submitted: green\n cancelled: red\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n meter_number:\n type: text\n label: Meter Number\n required: true\n in_form_view: true\n form_group: details\n form_order: 1\n customer_id:\n type: link\n label: Customer ID\n link_entity: customer\n link_display_field: name\n link_search_action: list-customers\n in_form_view: true\n form_group: references\n form_order: 2\n service_type:\n type: select\n label: Service Type\n required: true\n options:\n - value: electricity\n label: Electricity\n - value: water\n label: Water\n - value: gas\n label: Gas\n - value: telecom\n label: Telecom\n - value: saas\n label: Saas\n - value: parking\n label: Parking\n - value: rental\n label: Rental\n - value: waste\n label: Waste\n - value: custom\n label: Custom\n in_form_view: true\n form_group: details\n form_order: 3\n service_point_id:\n type: link\n label: Service Point ID\n link_entity: service_point\n link_display_field: name\n link_search_action: list-service-points\n in_form_view: true\n form_group: references\n form_order: 4\n service_point_address:\n type: text\n label: Service Point Address\n in_form_view: true\n form_group: details\n form_order: 5\n rate_plan_id:\n type: link\n label: Rate Plan ID\n link_entity: rate_plan\n link_display_field: name\n link_search_action: list-rate-plans\n in_form_view: true\n form_group: references\n form_order: 6\n install_date:\n type: date\n label: Install Date\n in_list_view: true\n in_form_view: true\n form_group: header\n form_order: 7\n last_reading_date:\n type: date\n label: Last Reading Date\n in_form_view: true\n form_group: header\n form_order: 8\n last_reading_value:\n type: text\n label: Last Reading Value\n in_form_view: true\n form_group: details\n form_order: 9\n status:\n type: select\n label: Status\n options:\n - value: active\n label: Active\n - value: disconnected\n label: Disconnected\n - value: removed\n label: Removed\n - value: suspended\n label: Suspended\n in_list_view: true\n in_form_view: true\n form_group: header\n form_order: 10\n metadata:\n type: text\n label: Metadata\n in_form_view: true\n form_group: details\n form_order: 11\n form_groups:\n header:\n label: Basic Information\n order: 1\n columns: 2\n details:\n label: Details\n order: 2\n columns: 2\n references:\n label: References\n order: 3\n columns: 2\n items:\n label: Line Items\n order: 4\n type: child_table\n usage_event:\n label: Usage Event\n label_plural: Usage Events\n table: usage_event\n id_col: id\n name_col: id\n primary_field: id\n identifier_field: id\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n customer_id:\n type: link\n label: Customer ID\n link_entity: customer\n link_display_field: name\n link_search_action: list-customers\n in_form_view: true\n form_group: references\n form_order: 1\n meter_id:\n type: link\n label: Meter ID\n link_entity: meter\n link_display_field: name\n link_search_action: list-meters\n in_form_view: true\n form_group: references\n form_order: 2\n event_type:\n type: text\n label: Event Type\n required: true\n in_form_view: true\n form_group: details\n form_order: 3\n quantity:\n type: quantity\n label: Quantity\n in_form_view: true\n form_group: details\n form_order: 4\n timestamp:\n type: text\n label: Timestamp\n required: true\n in_form_view: true\n form_group: details\n form_order: 5\n metadata:\n type: text\n label: Metadata\n in_form_view: true\n form_group: details\n form_order: 6\n idempotency_key:\n type: text\n label: IDempotency Key\n in_form_view: true\n form_group: details\n form_order: 7\n billing_period_id:\n type: link\n label: Billing Period ID\n link_entity: billing_period\n link_display_field: name\n link_search_action: list-billing-periods\n in_form_view: true\n form_group: references\n form_order: 8\n processed:\n type: integer\n label: Processed\n in_form_view: true\n form_group: details\n form_order: 9\n form_groups:\n details:\n label: Details\n order: 1\n columns: 2\n references:\n label: References\n order: 2\n columns: 2\n rate_plan:\n label: Rate Plan\n label_plural: Rate Plans\n table: rate_plan\n id_col: id\n name_col: name\n primary_field: name\n identifier_field: id\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n name:\n type: text\n label: Name\n required: true\n in_list_view: true\n in_form_view: true\n form_group: header\n form_order: 1\n service_type:\n type: text\n label: Service Type\n in_form_view: true\n form_group: details\n form_order: 2\n plan_type:\n type: select\n label: Plan Type\n required: true\n options:\n - value: flat\n label: Flat\n - value: tiered\n label: Tiered\n - value: time_of_use\n label: Time Of Use\n - value: demand\n label: Demand\n - value: volume_discount\n label: Volume Discount\n - value: prepaid_credit\n label: Prepaid Credit\n - value: hybrid\n label: Hybrid\n in_form_view: true\n form_group: details\n form_order: 3\n base_charge:\n type: text\n label: Base Charge\n in_form_view: true\n form_group: details\n form_order: 4\n base_charge_period:\n type: select\n label: Base Charge Period\n options:\n - value: monthly\n label: Monthly\n - value: quarterly\n label: Quarterly\n - value: annually\n label: Annually\n in_form_view: true\n form_group: details\n form_order: 5\n currency:\n type: text\n label: Currency\n required: true\n in_form_view: true\n form_group: details\n form_order: 6\n effective_from:\n type: date\n label: Effective From\n in_form_view: true\n form_group: details\n form_order: 7\n effective_to:\n type: date\n label: Effective To\n in_form_view: true\n form_group: details\n form_order: 8\n minimum_charge:\n type: text\n label: Minimum Charge\n in_form_view: true\n form_group: details\n form_order: 9\n minimum_commitment:\n type: text\n label: Minimum Commitment\n in_form_view: true\n form_group: details\n form_order: 10\n overage_rate:\n type: currency\n label: Overage Rate\n precision: 2\n in_form_view: true\n form_group: amounts\n form_order: 11\n metadata:\n type: text\n label: Metadata\n in_form_view: true\n form_group: details\n form_order: 12\n form_groups:\n header:\n label: Basic Information\n order: 1\n columns: 2\n details:\n label: Details\n order: 2\n columns: 2\n amounts:\n label: Amounts\n order: 3\n columns: 2\n rate_tier:\n label: Rate Tier\n label_plural: Rate Tiers\n table: rate_tier\n id_col: id\n name_col: id\n primary_field: id\n identifier_field: id\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n rate_plan_id:\n type: link\n label: Rate Plan ID\n link_entity: rate_plan\n link_display_field: name\n link_search_action: list-rate-plans\n in_form_view: true\n form_group: references\n form_order: 1\n tier_start:\n type: text\n label: Tier Start\n required: true\n in_form_view: true\n form_group: details\n form_order: 2\n tier_end:\n type: text\n label: Tier End\n in_form_view: true\n form_group: details\n form_order: 3\n rate:\n type: currency\n label: Rate\n precision: 2\n in_form_view: true\n form_group: amounts\n form_order: 4\n fixed_charge:\n type: text\n label: Fixed Charge\n in_form_view: true\n form_group: details\n form_order: 5\n time_of_use_period:\n type: select\n label: Time Of Use Period\n options:\n - value: peak\n label: Peak\n - value: off_peak\n label: Off Peak\n - value: shoulder\n label: Shoulder\n in_form_view: true\n form_group: details\n form_order: 6\n time_of_use_hours:\n type: text\n label: Time Of Use Hours\n in_form_view: true\n form_group: details\n form_order: 7\n demand_type:\n type: select\n label: Demand Type\n options:\n - value: energy\n label: Energy\n - value: demand\n label: Demand\n - value: reactive_power\n label: Reactive Power\n in_form_view: true\n form_group: details\n form_order: 8\n sort_order:\n type: integer\n label: Sort Order\n in_form_view: true\n form_group: details\n form_order: 9\n form_groups:\n details:\n label: Details\n order: 1\n columns: 2\n references:\n label: References\n order: 2\n columns: 2\n amounts:\n label: Amounts\n order: 3\n columns: 2\n billing_period:\n label: Billing Period\n label_plural: Billing Periods\n table: billing_period\n id_col: id\n name_col: id\n primary_field: id\n identifier_field: id\n status_field: status\n status_colors:\n draft: gray\n submitted: green\n cancelled: red\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n customer_id:\n type: link\n label: Customer ID\n link_entity: customer\n link_display_field: name\n link_search_action: list-customers\n in_form_view: true\n form_group: references\n form_order: 1\n meter_id:\n type: link\n label: Meter ID\n link_entity: meter\n link_display_field: name\n link_search_action: list-meters\n in_form_view: true\n form_group: references\n form_order: 2\n rate_plan_id:\n type: link\n label: Rate Plan ID\n link_entity: rate_plan\n link_display_field: name\n link_search_action: list-rate-plans\n in_form_view: true\n form_group: references\n form_order: 3\n period_start:\n type: text\n label: Period Start\n required: true\n in_form_view: true\n form_group: details\n form_order: 4\n period_end:\n type: text\n label: Period End\n required: true\n in_form_view: true\n form_group: details\n form_order: 5\n total_consumption:\n type: currency\n label: Total Consumption\n precision: 2\n in_form_view: true\n form_group: amounts\n form_order: 6\n consumption_uom:\n type: text\n label: Consumption UOM\n in_form_view: true\n form_group: details\n form_order: 7\n peak_demand:\n type: text\n label: Peak Demand\n in_form_view: true\n form_group: details\n form_order: 8\n base_charge:\n type: text\n label: Base Charge\n required: true\n in_form_view: true\n form_group: details\n form_order: 9\n usage_charge:\n type: text\n label: Usage Charge\n required: true\n in_form_view: true\n form_group: details\n form_order: 10\n demand_charge:\n type: text\n label: Demand Charge\n in_form_view: true\n form_group: details\n form_order: 11\n adjustments_total:\n type: currency\n label: Adjustments Total\n precision: 2\n in_form_view: true\n form_group: amounts\n form_order: 12\n subtotal:\n type: currency\n label: Subtotal\n precision: 2\n in_form_view: true\n form_group: amounts\n form_order: 13\n tax_amount:\n type: currency\n label: Tax Amount\n precision: 2\n in_form_view: true\n form_group: amounts\n form_order: 14\n grand_total:\n type: currency\n label: Grand Total\n precision: 2\n in_form_view: true\n form_group: amounts\n form_order: 15\n invoice_id:\n type: link\n label: Invoice ID\n link_entity: invoice\n link_display_field: name\n link_search_action: list-invoices\n in_form_view: true\n form_group: references\n form_order: 16\n status:\n type: select\n label: Status\n options:\n - value: open\n label: Open\n - value: rated\n label: Rated\n - value: invoiced\n label: Invoiced\n - value: paid\n label: Paid\n - value: disputed\n label: Disputed\n - value: void\n label: Void\n in_list_view: true\n in_form_view: true\n form_group: header\n form_order: 17\n rated_at:\n type: currency\n label: Rated At\n precision: 2\n in_form_view: true\n form_group: amounts\n form_order: 18\n invoiced_at:\n type: text\n label: Invoiced At\n in_form_view: true\n form_group: details\n form_order: 19\n form_groups:\n header:\n label: Basic Information\n order: 1\n columns: 2\n details:\n label: Details\n order: 2\n columns: 2\n references:\n label: References\n order: 3\n columns: 2\n amounts:\n label: Amounts\n order: 4\n columns: 2\n billing_adjustment:\n label: Billing Adjustment\n label_plural: Billing Adjustments\n table: billing_adjustment\n id_col: id\n name_col: id\n primary_field: id\n identifier_field: id\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n billing_period_id:\n type: link\n label: Billing Period ID\n link_entity: billing_period\n link_display_field: name\n link_search_action: list-billing-periods\n in_form_view: true\n form_group: references\n form_order: 1\n adjustment_type:\n type: select\n label: Adjustment Type\n required: true\n options:\n - value: credit\n label: Credit\n - value: late_fee\n label: Late Fee\n - value: deposit\n label: Deposit\n - value: refund\n label: Refund\n - value: proration\n label: Proration\n - value: discount\n label: Discount\n - value: penalty\n label: Penalty\n - value: write_off\n label: Write Off\n in_form_view: true\n form_group: details\n form_order: 2\n amount:\n type: currency\n label: Amount\n precision: 2\n in_form_view: true\n form_group: amounts\n form_order: 3\n reason:\n type: textarea\n label: Reason\n in_form_view: true\n form_group: notes\n form_order: 4\n approved_by:\n type: text\n label: Approved By\n in_form_view: true\n form_group: details\n form_order: 5\n form_groups:\n details:\n label: Details\n order: 1\n columns: 2\n references:\n label: References\n order: 2\n columns: 2\n amounts:\n label: Amounts\n order: 3\n columns: 2\n notes:\n label: Notes\n order: 4\n columns: 1\n prepaid_credit_balance:\n label: Prepaid Credit Balance\n label_plural: Prepaid Credit Balances\n table: prepaid_credit_balance\n id_col: id\n name_col: id\n primary_field: id\n identifier_field: id\n status_field: status\n status_colors:\n draft: gray\n submitted: green\n cancelled: red\n fields:\n id:\n type: text\n label: ID\n read_only: true\n hidden: true\n customer_id:\n type: link\n label: Customer ID\n link_entity: customer\n link_display_field: name\n link_search_action: list-customers\n in_form_view: true\n form_group: references\n form_order: 1\n rate_plan_id:\n type: link\n label: Rate Plan ID\n link_entity: rate_plan\n link_display_field: name\n link_search_action: list-rate-plans\n in_form_view: true\n form_group: references\n form_order: 2\n original_amount:\n type: currency\n label: Original Amount\n precision: 2\n in_form_view: true\n form_group: amounts\n form_order: 3\n remaining_amount:\n type: currency\n label: Remaining Amount\n precision: 2\n in_form_view: true\n form_group: amounts\n form_order: 4\n period_start:\n type: text\n label: Period Start\n required: true\n in_form_view: true\n form_group: details\n form_order: 5\n period_end:\n type: text\n label: Period End\n required: true\n in_form_view: true\n form_group: details\n form_order: 6\n overage_amount:\n type: currency\n label: Overage Amount\n precision: 2\n in_form_view: true\n form_group: amounts\n form_order: 7\n status:\n type: select\n label: Status\n options:\n - value: active\n label: Active\n - value: exhausted\n label: Exhausted\n - value: expired\n label: Expired\n in_list_view: true\n in_form_view: true\n form_group: header\n form_order: 8\n form_groups:\n header:\n label: Basic Information\n order: 1\n columns: 2\n details:\n label: Details\n order: 2\n columns: 2\n references:\n label: References\n order: 3\n columns: 2\n amounts:\n label: Amounts\n order: 4\n columns: 2\naction_map:\n setup-company:\n component: FormView\n entity: company\n mode: create\n success_redirect: get-company\n success_toast: Company '{name}' created successfully\n update-company:\n component: FormView\n entity: company\n mode: edit\n success_redirect: get-company\n get-company:\n component: DetailView\n entity: company\n list-companies:\n component: DataTable\n entity: company\n default_sort: name\n searchable: true\n add_action: setup-company\n add-currency:\n component: FormView\n entity: currency\n mode: create\n success_toast: Currency {code} added\n list-currencies:\n component: DataTable\n entity: currency\n default_sort: code\n searchable: true\n add_action: add-currency\n add-exchange-rate:\n component: FormView\n entity: exchange_rate\n mode: create\n success_toast: Exchange rate added for {effective_date}\n get-exchange-rate:\n component: DetailView\n entity: exchange_rate\n list-exchange-rates:\n component: DataTable\n entity: exchange_rate\n default_sort: effective_date\n default_sort_dir: desc\n add_action: add-exchange-rate\n add-payment-terms:\n component: FormView\n entity: payment_terms\n mode: create\n success_toast: Payment terms '{name}' created\n list-payment-terms:\n component: DataTable\n entity: payment_terms\n default_sort: due_days\n searchable: true\n add_action: add-payment-terms\n add-uom:\n component: FormView\n entity: uom\n mode: create\n success_toast: Unit of measure '{name}' added\n list-uoms:\n component: DataTable\n entity: uom\n default_sort: name\n searchable: true\n add_action: add-uom\n add-uom-conversion:\n component: FormView\n entity: uom_conversion\n mode: create\n success_toast: UoM conversion added\n add-user:\n component: FormView\n entity: erp_user\n mode: create\n success_redirect: get-user\n success_toast: User '{username}' created\n update-user:\n component: FormView\n entity: erp_user\n mode: edit\n success_redirect: get-user\n get-user:\n component: DetailView\n entity: erp_user\n list-users:\n component: DataTable\n entity: erp_user\n default_sort: username\n searchable: true\n add_action: add-user\n add-role:\n component: FormView\n entity: role\n mode: create\n success_toast: Role '{name}' created\n list-roles:\n component: DataTable\n entity: role\n default_sort: name\n add_action: add-role\n get-audit-log:\n component: DataTable\n entity: audit_log\n default_sort: timestamp\n default_sort_dir: desc\n searchable: false\n setup-chart-of-accounts:\n component: WizardFlow\n entity: account\n steps:\n - label: Select Template\n fields:\n - name: template\n type: select\n label: Chart of Accounts Template\n required: true\n default: us_gaap\n options:\n - value: us_gaap\n label: US GAAP (Full ~90 accounts)\n - value: us_gaap_simplified\n label: US GAAP Simplified (~40 accounts)\n - name: company_id\n type: link\n label: Company\n required: true\n link_entity: company\n link_display_field: name\n link_search_action: list-companies\n - label: Confirm\n confirmation: true\n message: This will load the selected chart of accounts template. Existing accounts will not be duplicated.\n success_redirect: list-accounts\n success_toast: 'Chart of accounts loaded: {accounts_created} accounts created'\n add-account:\n component: FormView\n entity: account\n mode: create\n success_redirect: get-account\n success_toast: Account '{name}' created\n update-account:\n component: FormView\n entity: account\n mode: edit\n success_redirect: get-account\n success_toast: Account updated\n list-accounts:\n component: DataTable\n entity: account\n default_sort: account_number\n searchable: true\n add_action: add-account\n extra_actions:\n - action: setup-chart-of-accounts\n label: Load Template\n icon: upload\n get-account:\n component: DetailView\n entity: account\n freeze-account:\n component: WizardFlow\n entity: account\n steps:\n - label: Confirm Freeze\n confirmation: true\n message: Freezing this account will prevent any new GL postings. Are you sure?\n fields:\n - name: account_id\n type: link\n label: Account\n required: true\n link_entity: account\n link_display_field: name\n link_search_action: list-accounts\n success_redirect: get-account\n success_toast: Account frozen\n unfreeze-account:\n component: FormView\n entity: account\n mode: action\n fields:\n - name: account_id\n type: link\n label: Account\n required: true\n link_entity: account\n link_display_field: name\n link_search_action: list-accounts\n success_redirect: get-account\n success_toast: Account unfrozen\n post-gl-entries:\n component: WizardFlow\n entity: gl_entry\n steps:\n - label: Voucher Details\n fields:\n - name: voucher_type\n type: text\n label: Voucher Type\n required: true\n - name: voucher_id\n type: text\n label: Voucher ID\n required: true\n - name: posting_date\n type: date\n label: Posting Date\n required: true\n default: today\n - name: company_id\n type: link\n label: Company\n required: true\n link_entity: company\n link_display_field: name\n link_search_action: list-companies\n - label: GL Entries\n fields:\n - name: entries\n type: json\n label: Entries (JSON array)\n required: true\n help_text: 'Each entry: {\"account_id\": \"...\", \"debit\": \"0\", \"credit\": \"100.00\"}'\n - label: Review\n confirmation: true\n message: 'GL entries must balance: SUM(debits) = SUM(credits). Confirm posting?'\n success_toast: 'GL entries posted: {entries_created} entries created'\n reverse-gl-entries:\n component: WizardFlow\n entity: gl_entry\n steps:\n - label: Select Voucher\n fields:\n - name: voucher_type\n type: text\n label: Voucher Type\n required: true\n - name: voucher_id\n type: text\n label: Voucher ID\n required: true\n - name: posting_date\n type: date\n label: Reversal Date\n default: today\n - label: Confirm Reversal\n confirmation: true\n message: This will create reversing GL entries for the selected voucher. This action cannot be undone.\n success_toast: 'GL entries reversed: {reversed_count} reversal entries created'\n list-gl-entries:\n component: DataTable\n entity: gl_entry\n default_sort: posting_date\n default_sort_dir: desc\n searchable: false\n paginated: true\n page_size: 50\n check-gl-integrity:\n component: DashboardView\n entity: gl_entry\n fields:\n - name: company_id\n type: link\n label: Company\n required: true\n link_entity: company\n link_display_field: name\n link_search_action: list-companies\n display:\n - key: balanced\n label: GL Balanced\n type: boolean_badge\n true_label: Balanced\n false_label: OUT OF BALANCE\n - key: total_debit\n label: Total Debits\n type: currency\n - key: total_credit\n label: Total Credits\n type: currency\n - key: difference\n label: Difference\n type: currency\n - key: chain_intact\n label: Hash Chain Intact\n type: boolean_badge\n true_label: Intact\n false_label: BROKEN\n - key: broken_links\n label: Broken Links\n type: integer\n - key: total_entries\n label: Total Entries\n type: integer\n add-fiscal-year:\n component: FormView\n entity: fiscal_year\n mode: create\n success_toast: Fiscal year '{name}' created\n list-fiscal-years:\n component: DataTable\n entity: fiscal_year\n default_sort: start_date\n default_sort_dir: desc\n searchable: true\n add_action: add-fiscal-year\n validate-period-close:\n component: DashboardView\n entity: fiscal_year\n fields:\n - name: fiscal_year_id\n type: link\n label: Fiscal Year\n required: true\n link_entity: fiscal_year\n link_display_field: name\n link_search_action: list-fiscal-years\n display:\n - key: fiscal_year\n label: Fiscal Year\n type: text\n - key: income_total\n label: Income Total\n type: currency\n - key: expense_total\n label: Expense Total\n type: currency\n - key: net_income\n label: Net Income\n type: currency\n - key: trial_balance_balanced\n label: Trial Balance Balanced\n type: boolean_badge\n true_label: Balanced\n false_label: NOT BALANCED\n close-fiscal-year:\n component: WizardFlow\n entity: fiscal_year\n steps:\n - label: Closing Parameters\n fields:\n - name: fiscal_year_id\n type: link\n label: Fiscal Year\n required: true\n link_entity: fiscal_year\n link_display_field: name\n link_search_action: list-fiscal-years\n - name: closing_account_id\n type: link\n label: Closing Account (Retained Earnings)\n required: true\n link_entity: account\n link_display_field: name\n link_search_action: list-accounts\n help_text: Must be an equity account (e.g., Retained Earnings)\n - name: posting_date\n type: date\n label: Closing Date\n required: true\n - label: Confirm Close\n confirmation: true\n message: Closing a fiscal year will transfer net P&L to Retained Earnings and mark the year as closed. This is a significant\n action.\n success_toast: Fiscal year closed. Net P&L of {net_pl_transferred} transferred.\n reopen-fiscal-year:\n component: WizardFlow\n entity: fiscal_year\n steps:\n - label: Select Fiscal Year\n fields:\n - name: fiscal_year_id\n type: link\n label: Fiscal Year\n required: true\n link_entity: fiscal_year\n link_display_field: name\n link_search_action: list-fiscal-years\n - label: Confirm Reopen\n confirmation: true\n message: Reopening a fiscal year will cancel the period closing voucher and reverse all closing GL entries. Are you\n sure?\n success_toast: Fiscal year reopened\n add-cost-center:\n component: FormView\n entity: cost_center\n mode: create\n success_toast: Cost center '{name}' created\n list-cost-centers:\n component: DataTable\n entity: cost_center\n default_sort: name\n searchable: true\n add_action: add-cost-center\n add-budget:\n component: FormView\n entity: budget\n mode: create\n success_toast: Budget created\n list-budgets:\n component: DataTable\n entity: budget\n default_sort: budget_amount\n default_sort_dir: desc\n paginated: true\n seed-naming-series:\n component: WizardFlow\n entity: naming_series\n steps:\n - label: Select Company\n fields:\n - name: company_id\n type: link\n label: Company\n required: true\n link_entity: company\n link_display_field: name\n link_search_action: list-companies\n - label: Confirm\n confirmation: true\n message: This will seed naming series for all entity types for the current year. Existing series will not be duplicated.\n success_toast: 'Naming series seeded: {series_created} created'\n next-series:\n component: FormView\n entity: naming_series\n mode: action\n fields:\n - name: entity_type\n type: text\n label: Entity Type\n required: true\n help_text: e.g., journal_entry, payment_entry, sales_invoice\n - name: company_id\n type: link\n label: Company\n required: true\n link_entity: company\n link_display_field: name\n link_search_action: list-companies\n success_toast: 'Next series: {series}'\n get-account-balance:\n component: DashboardView\n entity: account\n fields:\n - name: account_id\n type: link\n label: Account\n required: true\n link_entity: account\n link_display_field: name\n link_search_action: list-accounts\n - name: as_of_date\n type: date\n label: As-of Date\n required: true\n default: today\n - name: party_type\n type: select\n label: Party Type\n options:\n - value: customer\n label: Customer\n - value: supplier\n label: Supplier\n - value: employee\n label: Employee\n - name: party_id\n type: text\n label: Party ID\n display:\n - key: balance\n label: Balance\n type: currency\n - key: debit_total\n label: Total Debits\n type: currency\n - key: credit_total\n label: Total Credits\n type: currency\n - key: currency\n label: Currency\n type: text\n status:\n component: DashboardView\n revalue-foreign-balances:\n component: WizardFlow\n entity: gl_entry\n steps:\n - label: Revaluation Parameters\n fields:\n - name: company_id\n type: link\n label: Company\n required: true\n link_entity: company\n link_display_field: name\n link_search_action: list-companies\n - name: as_of_date\n type: date\n label: As-of Date\n required: true\n default: today\n - label: Confirm Revaluation\n confirmation: true\n message: This will post unrealized FX gain/loss entries for all foreign currency accounts. Review results carefully.\n success_toast: 'FX revaluation complete: {accounts_processed} accounts processed, total gain/loss {total_gain_loss}'\n import-chart-of-accounts:\n component: WizardFlow\n entity: account\n steps:\n - label: Import Settings\n fields:\n - name: csv_path\n type: text\n label: CSV File Path\n required: true\n help_text: Absolute path to the CSV file on the server\n - name: company_id\n type: link\n label: Company\n required: true\n link_entity: company\n link_display_field: name\n link_search_action: list-companies\n - label: Confirm Import\n confirmation: true\n message: This will import accounts from the CSV file. Duplicate account names will be skipped.\n success_redirect: list-accounts\n success_toast: 'Import complete: {imported} accounts imported, {skipped} skipped'\n import-opening-balances:\n component: WizardFlow\n entity: gl_entry\n steps:\n - label: Import Settings\n fields:\n - name: csv_path\n type: text\n label: CSV File Path\n required: true\n help_text: Absolute path to the CSV file on the server\n - name: company_id\n type: link\n label: Company\n required: true\n link_entity: company\n link_display_field: name\n link_search_action: list-companies\n - name: posting_date\n type: date\n label: Opening Balance Date\n required: true\n help_text: Typically the day before the first day of the new fiscal year\n - label: Confirm Import\n confirmation: true\n message: This will post opening balance GL entries from the CSV file. Ensure debits equal credits.\n success_toast: 'Opening balances imported: {gl_entries_created} GL entries created'\n add-journal-entry:\n component: FormView\n entity: journal_entry\n mode: create\n child_tables:\n - entity: journal_entry_line\n form_group: items\n add_label: Add Row\n amend-journal-entry:\n component: FormView\n mode: create\n cancel-journal-entry:\n component: WizardFlow\n entity: journal_entry\n steps:\n - label: Review\n type: detail\n - label: Confirm\n type: confirmation\n message: Cancel this journal entry? This cannot be undone.\n destructive: true\n delete-journal-entry:\n component: null\n entity: journal_entry\n hidden: true\n duplicate-journal-entry:\n component: FormView\n mode: create\n get-journal-entry:\n component: DetailView\n entity: journal_entry\n list-journal-entries:\n component: DataTable\n entity: journal_entry\n default_sort: created_at\n default_sort_dir: desc\n searchable: true\n add_action: add-journal-entry\n submit-journal-entry:\n component: WizardFlow\n entity: journal_entry\n steps:\n - label: Review\n type: detail\n - label: Confirm\n type: confirmation\n message: Submit this journal entry?\n update-journal-entry:\n component: FormView\n entity: journal_entry\n mode: edit\n add-payment:\n component: FormView\n mode: create\n allocate-payment:\n component: FormView\n mode: create\n bank-reconciliation:\n component: FormView\n mode: create\n cancel-payment:\n component: FormView\n mode: create\n create-payment-ledger-entry:\n component: FormView\n entity: payment_ledger_entry\n mode: create\n delete-payment:\n component: FormView\n mode: create\n get-outstanding:\n component: FormView\n mode: create\n get-payment:\n component: FormView\n mode: create\n get-unallocated-payments:\n component: FormView\n mode: create\n list-payments:\n component: FormView\n mode: create\n reconcile-payments:\n component: FormView\n mode: create\n submit-payment:\n component: FormView\n mode: create\n update-payment:\n component: FormView\n mode: create\n add-item-tax-template:\n component: FormView\n entity: item_tax_template\n mode: create\n add-tax-category:\n component: FormView\n entity: tax_category\n mode: create\n add-tax-rule:\n component: FormView\n entity: tax_rule\n mode: create\n add-tax-template:\n component: FormView\n entity: tax_template\n mode: create\n child_tables:\n - entity: tax_template_line\n form_group: items\n add_label: Add Row\n add-tax-withholding-category:\n component: FormView\n entity: tax_withholding_category\n mode: create\n calculate-tax:\n component: FormView\n mode: create\n delete-tax-template:\n component: null\n entity: tax_template\n hidden: true\n generate-1099-data:\n component: FormView\n mode: create\n get-tax-template:\n component: DetailView\n entity: tax_template\n get-withholding-details:\n component: FormView\n mode: create\n list-tax-categories:\n component: DataTable\n entity: tax_category\n default_sort: created_at\n default_sort_dir: desc\n searchable: true\n add_action: add-tax-category\n list-tax-rules:\n component: DataTable\n entity: tax_rule\n default_sort: created_at\n default_sort_dir: desc\n searchable: true\n add_action: add-tax-rule\n list-tax-templates:\n component: DataTable\n entity: tax_template\n default_sort: created_at\n default_sort_dir: desc\n searchable: true\n add_action: add-tax-template\n record-1099-payment:\n component: FormView\n mode: create\n record-withholding-entry:\n component: FormView\n mode: create\n resolve-tax-template:\n component: FormView\n mode: create\n update-tax-template:\n component: FormView\n entity: tax_template\n mode: edit\n ap-aging:\n component: FormView\n mode: create\n ar-aging:\n component: FormView\n mode: create\n balance-sheet:\n component: FormView\n mode: create\n budget-vs-actual:\n component: FormView\n mode: create\n cash-flow:\n component: FormView\n mode: create\n comparative-pl:\n component: FormView\n mode: create\n general-ledger:\n component: FormView\n mode: create\n gl-summary:\n component: FormView\n mode: create\n party-ledger:\n component: FormView\n mode: create\n payment-summary:\n component: FormView\n mode: create\n profit-and-loss:\n component: FormView\n mode: create\n tax-summary:\n component: FormView\n mode: create\n trial-balance:\n component: FormView\n mode: create\n add-item:\n component: FormView\n entity: item\n mode: create\n success_redirect: get-item\n success_toast: Item '{item_name}' ({item_code}) created successfully\n update-item:\n component: FormView\n entity: item\n mode: edit\n success_redirect: get-item\n success_toast: Item updated\n get-item:\n component: DetailView\n entity: item\n list-items:\n component: DataTable\n entity: item\n default_sort: item_name\n searchable: true\n search_fields:\n - item_name\n - item_code\n add_action: add-item\n import-items:\n component: FileUpload\n entity: item\n mode: bulk_import\n accept: .csv\n success_toast: Imported {imported} items ({skipped} skipped)\n add-item-group:\n component: FormView\n entity: item_group\n mode: create\n success_toast: Item group '{name}' created\n list-item-groups:\n component: DataTable\n entity: item_group\n default_sort: name\n searchable: true\n add_action: add-item-group\n add-warehouse:\n component: FormView\n entity: warehouse\n mode: create\n success_toast: Warehouse '{name}' created\n update-warehouse:\n component: FormView\n entity: warehouse\n mode: edit\n success_toast: Warehouse updated\n list-warehouses:\n component: DataTable\n entity: warehouse\n default_sort: name\n searchable: true\n add_action: add-warehouse\n add-stock-entry:\n component: FormView\n entity: stock_entry\n mode: create\n child_tables:\n - entity: stock_entry_item\n min_rows: 1\n success_redirect: get-stock-entry\n success_toast: Stock entry {naming_series} created as draft\n get-stock-entry:\n component: DetailView\n entity: stock_entry\n list-stock-entries:\n component: DataTable\n entity: stock_entry\n default_sort: posting_date\n default_sort_dir: desc\n searchable: false\n add_action: add-stock-entry\n submit-stock-entry:\n component: WizardFlow\n entity: stock_entry\n steps:\n - type: confirmation\n title: Submit Stock Entry\n message: This will post Stock Ledger Entries (SLE) and perpetual inventory GL entries. Stock balances and valuations\n will be updated. This action cannot be undone (only cancelled with reversal entries).\n confirm_label: Submit\n cancel_label: Cancel\n - type: action\n action: submit-stock-entry\n success_redirect: get-stock-entry\n success_toast: 'Stock entry submitted: {sle_entries_created} SLE and {gl_entries_created} GL entries posted'\n cancel-stock-entry:\n component: WizardFlow\n entity: stock_entry\n steps:\n - type: confirmation\n title: Cancel Stock Entry\n message: This will mark existing SLE entries as cancelled and post reversal SLE + GL entries. Stock balances and valuations\n will be adjusted. The stock entry will become immutable.\n confirm_label: Cancel Entry\n cancel_label: Go Back\n style: danger\n - type: action\n action: cancel-stock-entry\n success_redirect: get-stock-entry\n success_toast: 'Stock entry cancelled: {sle_reversals} SLE and {gl_reversals} GL reversal entries posted'\n create-stock-ledger-entries:\n component: ApiAction\n hidden: true\n description: 'Cross-skill: create SLE entries (called by selling/buying skills)'\n reverse-stock-ledger-entries:\n component: ApiAction\n hidden: true\n description: 'Cross-skill: reverse SLE entries (called by selling/buying skills)'\n get-stock-balance:\n component: DetailView\n entity: item\n custom_layout:\n title: Stock Balance\n description: Current stock for a specific item in a specific warehouse\n fields:\n - key: item_id\n label: Item\n type: link\n link_entity: item\n - key: warehouse_id\n label: Warehouse\n type: link\n link_entity: warehouse\n - key: qty\n label: Balance Qty\n type: quantity\n precision: 2\n - key: valuation_rate\n label: Valuation Rate\n type: currency\n precision: 2\n - key: stock_value\n label: Stock Value\n type: currency\n precision: 2\n stock-balance-report:\n component: ReportView\n custom_layout:\n title: Stock Balance Report\n description: Stock balance summary across all items and warehouses for a company\n filters:\n - key: company_id\n label: Company\n type: link\n link_entity: company\n link_search_action: list-companies\n required: true\n - key: warehouse_id\n label: Warehouse\n type: link\n link_entity: warehouse\n link_search_action: list-warehouses\n columns:\n - key: item_code\n label: Item Code\n width: 120\n - key: item_name\n label: Item Name\n width: 200\n - key: warehouse_name\n label: Warehouse\n width: 180\n - key: qty\n label: Balance Qty\n width: 100\n align: right\n type: quantity\n precision: 2\n - key: valuation_rate\n label: Valuation Rate\n width: 120\n align: right\n type: currency\n precision: 2\n - key: stock_value\n label: Stock Value\n width: 140\n align: right\n type: currency\n precision: 2\n summary:\n - key: total_stock_value\n label: Total Stock Value\n type: currency\n precision: 2\n - key: row_count\n label: Total Rows\n type: integer\n data_key: report\n stock-ledger-report:\n component: ReportView\n custom_layout:\n title: Stock Ledger Report\n description: Detailed stock ledger entry log with all movements\n filters:\n - key: item_id\n label: Item\n type: link\n link_entity: item\n link_search_action: list-items\n - key: warehouse_id\n label: Warehouse\n type: link\n link_entity: warehouse\n link_search_action: list-warehouses\n - key: from_date\n label: From Date\n type: date\n - key: to_date\n label: To Date\n type: date\n columns:\n - key: posting_date\n label: Date\n width: 100\n type: date\n - key: item_code\n label: Item Code\n width: 110\n - key: item_name\n label: Item Name\n width: 160\n - key: warehouse_name\n label: Warehouse\n width: 160\n - key: voucher_type\n label: Voucher Type\n width: 130\n - key: actual_qty\n label: Qty Change\n width: 100\n align: right\n type: quantity\n precision: 2\n - key: incoming_rate\n label: Incoming Rate\n width: 110\n align: right\n type: currency\n precision: 2\n - key: valuation_rate\n label: Valuation Rate\n width: 120\n align: right\n type: currency\n precision: 2\n - key: stock_value\n label: Stock Value\n width: 130\n align: right\n type: currency\n precision: 2\n - key: stock_value_difference\n label: Value Diff\n width: 120\n align: right\n type: currency\n precision: 2\n data_key: entries\n check-reorder:\n component: ReportView\n custom_layout:\n title: Reorder Check\n description: Items whose current stock is at or below their reorder level\n filters:\n - key: company_id\n label: Company\n type: link\n link_entity: company\n link_search_action: list-companies\n columns:\n - key: item_code\n label: Item Code\n width: 120\n - key: item_name\n label: Item Name\n width: 200\n - key: current_stock\n label: Current Stock\n width: 120\n align: right\n type: quantity\n precision: 2\n - key: reorder_level\n label: Reorder Level\n width: 120\n align: right\n type: quantity\n precision: 2\n - key: reorder_qty\n label: Reorder Qty\n width: 120\n align: right\n type: quantity\n precision: 2\n - key: shortfall\n label: Shortfall\n width: 120\n align: right\n type: quantity\n precision: 2\n summary:\n - key: items_below_reorder\n label: Items Below Reorder\n type: integer\n data_key: items\n empty_state:\n icon: check-circle\n message: All items are above reorder level\n add-batch:\n component: FormView\n entity: batch\n mode: create\n success_toast: Batch '{batch_name}' created\n list-batches:\n component: DataTable\n entity: batch\n default_sort: batch_name\n searchable: true\n add_action: add-batch\n add-serial-number:\n component: FormView\n entity: serial_number\n mode: create\n success_toast: Serial number '{serial_no}' registered\n list-serial-numbers:\n component: DataTable\n entity: serial_number\n default_sort: serial_no\n searchable: true\n add_action: add-serial-number\n add-price-list:\n component: FormView\n entity: price_list\n mode: create\n success_toast: Price list '{name}' created\n add-item-price:\n component: FormView\n entity: item_price\n mode: create\n success_toast: Item price set at {rate}\n get-item-price:\n component: DetailView\n entity: item_price\n custom_layout:\n title: Item Price Lookup\n description: Finds the applicable price from a price list for the given item and quantity\n fields:\n - key: item_id\n label: Item\n type: link\n link_entity: item\n - key: price_list_id\n label: Price List\n type: link\n link_entity: price_list\n - key: rate\n label: Rate\n type: currency\n precision: 2\n - key: min_qty\n label: Min Qty\n type: quantity\n precision: 2\n - key: valid_from\n label: Valid From\n type: date\n - key: valid_to\n label: Valid To\n type: date\n add-pricing-rule:\n component: FormView\n entity: pricing_rule\n mode: create\n success_toast: Pricing rule '{name}' created\n add-stock-reconciliation:\n component: FormView\n entity: stock_reconciliation\n mode: create\n child_tables:\n - entity: stock_reconciliation_item\n min_rows: 1\n success_redirect: get-stock-reconciliation\n success_toast: 'Stock reconciliation {naming_series} created as draft (difference: {difference_amount})'\n submit-stock-reconciliation:\n component: WizardFlow\n entity: stock_reconciliation\n steps:\n - type: confirmation\n title: Submit Stock Reconciliation\n message: This will post Stock Ledger Entries (SLE) for quantity differences and perpetual inventory GL entries for value\n adjustments. Stock balances and valuations will be updated to match the physical count.\n confirm_label: Submit Reconciliation\n cancel_label: Cancel\n - type: action\n action: submit-stock-reconciliation\n success_redirect: get-stock-reconciliation\n success_toast: 'Reconciliation submitted: {sle_entries_created} SLE and {gl_entries_created} GL entries posted'\n revalue-stock:\n component: FormView\n entity: stock_revaluation\n mode: create\n success_redirect: get-stock-revaluation\n success_toast: 'Stock revalued: {item_name} rate {old_rate} → {new_rate} (adjustment: {adjustment_amount})'\n list-stock-revaluations:\n component: DataTable\n entity: stock_revaluation\n default_sort: created_at\n sort_direction: desc\n add_action: revalue-stock\n get-stock-revaluation:\n component: DetailView\n entity: stock_revaluation\n cancel-stock-revaluation:\n component: WizardFlow\n entity: stock_revaluation\n steps:\n - type: confirmation\n title: Cancel Stock Revaluation\n message: This will reverse the SLE and GL entries created by this revaluation, restoring the previous valuation rate.\n confirm_label: Cancel Revaluation\n cancel_label: Keep\n - type: action\n action: cancel-stock-revaluation\n success_redirect: get-stock-revaluation\n success_toast: Revaluation cancelled and reversed\n add-customer:\n component: FormView\n entity: customer\n mode: create\n success_redirect: get-customer\n update-customer:\n component: FormView\n entity: customer\n mode: edit\n success_redirect: get-customer\n get-customer:\n component: DetailView\n entity: customer\n related:\n - entity: sales_invoice\n action: list-sales-invoices\n filter_field: customer_id\n label: Invoices\n - entity: sales_order\n action: list-sales-orders\n filter_field: customer_id\n label: Orders\n - entity: quotation\n action: list-quotations\n filter_field: customer_id\n label: Quotations\n list-customers:\n component: DataTable\n entity: customer\n default_sort: name\n default_sort_dir: asc\n searchable: true\n add_action: add-customer\n add-quotation:\n component: FormView\n entity: quotation\n mode: create\n child_tables:\n - entity: quotation_item\n form_group: items\n add_label: Add Item\n success_redirect: get-quotation\n update-quotation:\n component: FormView\n entity: quotation\n mode: edit\n child_tables:\n - entity: quotation_item\n form_group: items\n add_label: Add Item\n success_redirect: get-quotation\n get-quotation:\n component: DetailView\n entity: quotation\n list-quotations:\n component: DataTable\n entity: quotation\n default_sort: quotation_date\n default_sort_dir: desc\n searchable: true\n add_action: add-quotation\n submit-quotation:\n component: WizardFlow\n entity: quotation\n steps:\n - label: Review\n type: detail\n - label: Confirm\n type: confirmation\n message: Submit this quotation? It will be locked and can be converted to a sales order.\n success_toast: Quotation {naming_series} submitted.\n convert-quotation-to-so:\n component: WizardFlow\n entity: quotation\n steps:\n - label: Review\n type: detail\n - label: Confirm\n type: confirmation\n message: Convert this quotation to a sales order?\n success_toast: Sales order created from quotation.\n success_redirect: get-sales-order\n add-sales-order:\n component: FormView\n entity: sales_order\n mode: create\n child_tables:\n - entity: sales_order_item\n form_group: items\n add_label: Add Item\n success_redirect: get-sales-order\n update-sales-order:\n component: FormView\n entity: sales_order\n mode: edit\n child_tables:\n - entity: sales_order_item\n form_group: items\n add_label: Add Item\n success_redirect: get-sales-order\n get-sales-order:\n component: DetailView\n entity: sales_order\n related:\n - entity: delivery_note\n action: list-delivery-notes\n filter_field: sales_order_id\n label: Delivery Notes\n - entity: sales_invoice\n action: list-sales-invoices\n filter_field: sales_order_id\n label: Invoices\n list-sales-orders:\n component: DataTable\n entity: sales_order\n default_sort: order_date\n default_sort_dir: desc\n searchable: true\n add_action: add-sales-order\n submit-sales-order:\n component: WizardFlow\n entity: sales_order\n steps:\n - label: Review\n type: detail\n - label: Confirm\n type: confirmation\n message: Submit this sales order? A credit limit check will be performed.\n success_toast: Sales order {naming_series} confirmed.\n cancel-sales-order:\n component: WizardFlow\n entity: sales_order\n steps:\n - label: Review\n type: detail\n - label: Confirm\n type: confirmation\n message: Cancel this sales order? This cannot be undone.\n destructive: true\n success_toast: Sales order cancelled.\n create-delivery-note:\n component: FormView\n entity: delivery_note\n mode: create\n child_tables:\n - entity: delivery_note_item\n form_group: items\n add_label: Add Item\n success_redirect: get-delivery-note\n get-delivery-note:\n component: DetailView\n entity: delivery_note\n list-delivery-notes:\n component: DataTable\n entity: delivery_note\n default_sort: posting_date\n default_sort_dir: desc\n searchable: true\n submit-delivery-note:\n component: WizardFlow\n entity: delivery_note\n steps:\n - label: Review\n type: detail\n - label: Confirm\n type: confirmation\n message: Submit this delivery note? Stock ledger entries and COGS GL entries will be posted.\n success_toast: Delivery note {naming_series} submitted. {sle_entries_created} SLE + {gl_entries_created} GL entries posted.\n cancel-delivery-note:\n component: WizardFlow\n entity: delivery_note\n steps:\n - label: Review\n type: detail\n - label: Confirm\n type: confirmation\n message: Cancel this delivery note? SLE and GL reversal entries will be posted.\n destructive: true\n success_toast: Delivery note cancelled. Reversal entries posted.\n create-sales-invoice:\n component: FormView\n entity: sales_invoice\n mode: create\n child_tables:\n - entity: sales_invoice_item\n form_group: items\n add_label: Add Item\n success_redirect: get-sales-invoice\n update-sales-invoice:\n component: FormView\n entity: sales_invoice\n mode: edit\n child_tables:\n - entity: sales_invoice_item\n form_group: items\n add_label: Add Item\n success_redirect: get-sales-invoice\n get-sales-invoice:\n component: DetailView\n entity: sales_invoice\n list-sales-invoices:\n component: DataTable\n entity: sales_invoice\n default_sort: posting_date\n default_sort_dir: desc\n searchable: true\n add_action: create-sales-invoice\n submit-sales-invoice:\n component: WizardFlow\n entity: sales_invoice\n steps:\n - label: Review\n type: detail\n - label: Confirm\n type: confirmation\n message: Submit this invoice? GL entries will be posted and the invoice cannot be edited.\n success_toast: Invoice {naming_series} submitted. {gl_entries_created} GL entries posted.\n cancel-sales-invoice:\n component: WizardFlow\n entity: sales_invoice\n steps:\n - label: Review\n type: detail\n - label: Confirm\n type: confirmation\n message: Cancel this invoice? Reversal GL entries will be posted.\n destructive: true\n success_toast: Invoice {naming_series} cancelled. Reversal entries posted.\n create-credit-note:\n component: FormView\n entity: credit_note\n mode: create\n child_tables:\n - entity: sales_invoice_item\n form_group: items\n add_label: Add Return Item\n success_redirect: get-sales-invoice\n add-sales-partner:\n component: FormView\n entity: sales_partner\n mode: create\n list-sales-partners:\n component: DataTable\n entity: sales_partner\n default_sort: name\n default_sort_dir: asc\n searchable: true\n add_action: add-sales-partner\n add-recurring-template:\n component: FormView\n entity: recurring_invoice_template\n mode: create\n child_tables:\n - entity: recurring_invoice_template_item\n form_group: items\n add_label: Add Item\n update-recurring-template:\n component: FormView\n entity: recurring_invoice_template\n mode: edit\n child_tables:\n - entity: recurring_invoice_template_item\n form_group: items\n add_label: Add Item\n list-recurring-templates:\n component: DataTable\n entity: recurring_invoice_template\n default_sort: next_invoice_date\n default_sort_dir: asc\n searchable: true\n add_action: add-recurring-template\n generate-recurring-invoices:\n component: WizardFlow\n entity: recurring_invoice_template\n steps:\n - label: Confirm\n type: confirmation\n message: Generate all due recurring invoices? Draft invoices will be auto-submitted with GL entries.\n success_toast: '{invoices_generated} recurring invoices generated.'\n update-invoice-outstanding:\n component: InternalAction\n entity: purchase_invoice\n description: Called by erpclaw-payments to reduce outstanding amount after payment allocation\n params:\n - name: purchase_invoice_id\n type: text\n required: true\n - name: amount\n type: currency\n required: true\n precision: 2\n import-customers:\n component: null\n hidden: true\n add-intercompany-account-map:\n component: FormView\n entity: intercompany_account_map\n mode: create\n fields:\n - key: company_id\n label: Source Company\n type: entity_lookup\n entity: company\n required: true\n - key: target_company_id\n label: Target Company\n type: entity_lookup\n entity: company\n required: true\n - key: source_account_id\n label: Source Account\n type: entity_lookup\n entity: account\n required: true\n - key: target_account_id\n label: Target Account\n type: entity_lookup\n entity: account\n required: true\n list-intercompany-account-maps:\n component: DataTable\n entity: intercompany_account_map\n columns:\n - key: source_account_name\n label: Source Account\n - key: target_account_name\n label: Target Account\n filters:\n - key: company_id\n label: Source Company\n type: entity_lookup\n entity: company\n required: true\n - key: target_company_id\n label: Target Company\n type: entity_lookup\n entity: company\n create-intercompany-invoice:\n component: FormView\n entity: intercompany_invoice\n mode: create\n fields:\n - key: sales_invoice_id\n label: Sales Invoice\n type: entity_lookup\n entity: sales_invoice\n required: true\n - key: target_company_id\n label: Target Company\n type: entity_lookup\n entity: company\n required: true\n - key: supplier_id\n label: Supplier (in target)\n type: entity_lookup\n entity: supplier\n required: true\n list-intercompany-invoices:\n component: DataTable\n entity: intercompany_invoice\n columns:\n - key: naming_series\n label: 'Invoice #'\n - key: posting_date\n label: Date\n type: date\n - key: grand_total\n label: Amount\n type: currency\n - key: status\n label: Status\n type: status_badge\n - key: direction\n label: Direction\n filters:\n - key: company_id\n label: Company\n type: entity_lookup\n entity: company\n required: true\n cancel-intercompany-invoice:\n component: WizardFlow\n steps:\n - type: confirmation\n message: Cancel this intercompany invoice? Both the sales invoice and mirror purchase invoice will be cancelled.\n fields:\n - key: sales_invoice_id\n label: Sales Invoice\n type: entity_lookup\n entity: sales_invoice\n required: true\n add-supplier:\n component: FormView\n entity: supplier\n mode: create\n success_redirect: get-supplier\n success_toast: Supplier '{name}' created successfully\n update-supplier:\n component: FormView\n entity: supplier\n mode: edit\n success_redirect: get-supplier\n success_toast: Supplier updated\n get-supplier:\n component: DetailView\n entity: supplier\n list-suppliers:\n component: DataTable\n entity: supplier\n default_sort: name\n searchable: true\n add_action: add-supplier\n import-suppliers:\n component: WizardFlow\n entity: supplier\n steps:\n - label: Upload CSV\n fields:\n - csv_path\n - company_id\n success_toast: 'Suppliers imported: {imported} new, {skipped} skipped'\n add-material-request:\n component: FormView\n entity: material_request\n mode: create\n success_toast: Material request created ({item_count} items)\n submit-material-request:\n component: WizardFlow\n entity: material_request\n steps:\n - label: Confirm Submission\n confirmation_message: Submit this material request? It will be assigned a naming series and can no longer be edited.\n fields:\n - material_request_id\n success_toast: Material request {naming_series} submitted\n list-material-requests:\n component: DataTable\n entity: material_request\n default_sort: created_at\n default_sort_dir: desc\n add_action: add-material-request\n add-rfq:\n component: FormView\n entity: request_for_quotation\n mode: create\n success_toast: RFQ created with {item_count} items for {supplier_count} suppliers\n submit-rfq:\n component: WizardFlow\n entity: request_for_quotation\n steps:\n - label: Confirm Submission\n confirmation_message: Submit this RFQ? It will be sent to the selected suppliers.\n fields:\n - rfq_id\n success_toast: RFQ {naming_series} submitted to suppliers\n list-rfqs:\n component: DataTable\n entity: request_for_quotation\n default_sort: created_at\n default_sort_dir: desc\n add_action: add-rfq\n add-supplier-quotation:\n component: FormView\n entity: supplier_quotation\n mode: create\n success_toast: 'Supplier quotation recorded (total: ${total_amount})'\n list-supplier-quotations:\n component: DataTable\n entity: supplier_quotation\n default_sort: created_at\n default_sort_dir: desc\n compare-supplier-quotations:\n component: ComparisonView\n entity: supplier_quotation\n params:\n - name: rfq_id\n type: link\n link_entity: request_for_quotation\n required: true\n display:\n group_by: item\n columns_per_supplier:\n - rate\n - amount\n - lead_time_days\n highlight_lowest: true\n lowest_field: rate\n add-purchase-order:\n component: FormView\n entity: purchase_order\n mode: create\n success_redirect: get-purchase-order\n success_toast: 'Purchase order created (grand total: ${grand_total})'\n update-purchase-order:\n component: FormView\n entity: purchase_order\n mode: edit\n success_redirect: get-purchase-order\n success_toast: 'Purchase order updated (grand total: ${grand_total})'\n get-purchase-order:\n component: DetailView\n entity: purchase_order\n list-purchase-orders:\n component: DataTable\n entity: purchase_order\n default_sort: order_date\n default_sort_dir: desc\n searchable: true\n add_action: add-purchase-order\n submit-purchase-order:\n component: WizardFlow\n entity: purchase_order\n steps:\n - label: Confirm Submission\n confirmation_message: Submit this purchase order? It will be confirmed and locked for editing. A naming series will\n be assigned.\n fields:\n - purchase_order_id\n success_toast: Purchase order {naming_series} confirmed\n cancel-purchase-order:\n component: WizardFlow\n entity: purchase_order\n steps:\n - label: Confirm Cancellation\n confirmation_message: Cancel this purchase order? This cannot be undone. Ensure there are no linked receipts or invoices.\n fields:\n - purchase_order_id\n variant: danger\n success_toast: Purchase order cancelled\n create-purchase-receipt:\n component: FormView\n entity: purchase_receipt\n mode: create\n success_redirect: get-purchase-receipt\n success_toast: 'Purchase receipt created ({item_count} items, total qty: {total_qty})'\n get-purchase-receipt:\n component: DetailView\n entity: purchase_receipt\n list-purchase-receipts:\n component: DataTable\n entity: purchase_receipt\n default_sort: posting_date\n default_sort_dir: desc\n add_action: create-purchase-receipt\n submit-purchase-receipt:\n component: WizardFlow\n entity: purchase_receipt\n steps:\n - label: Confirm Submission\n confirmation_message: Submit this purchase receipt? This will post Stock Ledger Entries (SLE) to add inventory and GL\n entries (DR Stock In Hand / CR Stock Received Not Billed) for perpetual inventory. These ledger entries are immutable.\n fields:\n - purchase_receipt_id\n success_toast: Purchase receipt {naming_series} submitted ({sle_entries_created} SLE, {gl_entries_created} GL entries\n posted)\n cancel-purchase-receipt:\n component: WizardFlow\n entity: purchase_receipt\n steps:\n - label: Confirm Cancellation\n confirmation_message: Cancel this purchase receipt? This will reverse all SLE and GL entries. Inventory quantities and\n valuations will be rolled back. This cannot be undone.\n fields:\n - purchase_receipt_id\n variant: danger\n success_toast: Purchase receipt cancelled ({sle_reversals} SLE, {gl_reversals} GL reversals posted)\n create-purchase-invoice:\n component: FormView\n entity: purchase_invoice\n mode: create\n success_redirect: get-purchase-invoice\n success_toast: 'Purchase invoice created (grand total: ${grand_total})'\n update-purchase-invoice:\n component: FormView\n entity: purchase_invoice\n mode: edit\n success_redirect: get-purchase-invoice\n success_toast: Purchase invoice updated\n get-purchase-invoice:\n component: DetailView\n entity: purchase_invoice\n list-purchase-invoices:\n component: DataTable\n entity: purchase_invoice\n default_sort: posting_date\n default_sort_dir: desc\n searchable: true\n add_action: create-purchase-invoice\n submit-purchase-invoice:\n component: WizardFlow\n entity: purchase_invoice\n steps:\n - label: Confirm Submission\n confirmation_message: Submit this purchase invoice? This will post GL entries (DR Expense/SRNB / CR Accounts Payable),\n tax GL entries, and a Payment Ledger Entry (PLE). If update_stock is enabled, SLE entries will also be posted. These\n ledger entries are immutable.\n fields:\n - purchase_invoice_id\n success_toast: Purchase invoice {naming_series} submitted ({gl_entries_created} GL entries posted)\n cancel-purchase-invoice:\n component: WizardFlow\n entity: purchase_invoice\n steps:\n - label: Confirm Cancellation\n confirmation_message: Cancel this purchase invoice? This will reverse all GL entries, delink PLE entries, and (if update_stock\n was set) reverse SLE entries. This cannot be undone.\n fields:\n - purchase_invoice_id\n variant: danger\n success_toast: Purchase invoice cancelled ({gl_reversals} GL reversals posted)\n create-debit-note:\n component: WizardFlow\n entity: purchase_invoice\n steps:\n - label: Select Invoice & Items\n fields:\n - against_invoice_id\n - items\n - reason\n confirmation_message: Create a debit note (return) against this purchase invoice? The debit note will be created in\n draft status and must be submitted separately to post GL reversals.\n success_toast: 'Debit note created (total: ${total_amount})'\n add-landed-cost-voucher:\n component: WizardFlow\n entity: landed_cost_voucher\n steps:\n - label: Select Receipts\n fields:\n - purchase_receipt_ids\n - company_id\n - label: Define Charges\n fields:\n - charges\n confirmation_message: Submit this landed cost voucher? Charges will be allocated across receipt items proportionally,\n adjusting item valuation rates. GL entries (DR Stock In Hand / CR Expense) will be posted.\n success_toast: Landed cost voucher created (${total_landed_cost} allocated, {gl_entries_created} GL entries)\n add-billing-adjustment:\n component: FormView\n entity: billing_adjustment\n mode: create\n add-meter:\n component: FormView\n entity: meter\n mode: create\n child_tables:\n - entity: meter_reading\n form_group: items\n add_label: Add Row\n add-meter-reading:\n component: FormView\n mode: create\n add-prepaid-credit:\n component: FormView\n mode: create\n add-rate-plan:\n component: FormView\n entity: rate_plan\n mode: create\n add-usage-event:\n component: FormView\n entity: usage_event\n mode: create\n add-usage-events-batch:\n component: FormView\n mode: create\n create-billing-period:\n component: FormView\n entity: billing_period\n mode: create\n generate-invoices:\n component: FormView\n mode: create\n get-billing-period:\n component: DetailView\n entity: billing_period\n get-meter:\n component: DetailView\n entity: meter\n get-prepaid-balance:\n component: FormView\n mode: create\n get-rate-plan:\n component: DetailView\n entity: rate_plan\n list-billing-periods:\n component: DataTable\n entity: billing_period\n default_sort: created_at\n default_sort_dir: desc\n searchable: true\n add_action: create-billing-period\n list-meter-readings:\n component: FormView\n mode: create\n list-meters:\n component: DataTable\n entity: meter\n default_sort: created_at\n default_sort_dir: desc\n searchable: true\n add_action: add-meter\n list-rate-plans:\n component: DataTable\n entity: rate_plan\n default_sort: created_at\n default_sort_dir: desc\n searchable: true\n add_action: add-rate-plan\n rate-consumption:\n component: FormView\n mode: create\n run-billing:\n component: FormView\n mode: create\n update-meter:\n component: FormView\n entity: meter\n mode: edit\n update-rate-plan:\n component: FormView\n entity: rate_plan\n mode: edit\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":255270,"content_sha256":"7c7062cdbc7877a3daa5b22dc6b4e296ce0b1dad7cdf615fd0ddcf0f278b581c"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"erpclaw","type":"text"}]},{"type":"paragraph","content":[{"text":"You are a ","type":"text"},{"text":"Full-Stack ERP Controller","type":"text","marks":[{"type":"strong"}]},{"text":" for ERPClaw, an AI-native ERP system. You handle all core business operations: company setup, chart of accounts, journal entries, payments, tax, financial reports, customers, sales orders, invoices, suppliers, purchase orders, inventory, usage-based billing, HR (employees, leave, attendance, expenses), and US payroll (salary structures, FICA, income tax withholding, W-2 generation, garnishments). All data lives in a single local SQLite database with full double-entry accounting and immutable audit trail.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Security Model","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Local-first","type":"text","marks":[{"type":"strong"}]},{"text":": All data in ","type":"text"},{"text":"~/.openclaw/erpclaw/data.sqlite","type":"text","marks":[{"type":"code_inline"}]},{"text":". Parameterized queries, RBAC (PBKDF2-HMAC-SHA256 600K), immutable GL (cancel = reverse). PII stored locally only.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Network","type":"text","marks":[{"type":"strong"}]},{"text":" (user-initiated, requires confirmation): ","type":"text"},{"text":"fetch-exchange-rates","type":"text","marks":[{"type":"code_inline"}]},{"text":" (public API), ","type":"text"},{"text":"install-module","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"update-modules","type":"text","marks":[{"type":"code_inline"}]},{"text":" (GitHub ","type":"text"},{"text":"avansaber/*","type":"text","marks":[{"type":"code_inline"}]},{"text":" only).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Routing","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"scripts/db_query.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" → domain scripts or installed modules in ","type":"text"},{"text":"~/.openclaw/erpclaw/modules/","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Skill Activation Triggers","type":"text"}]},{"type":"paragraph","content":[{"text":"Activate this skill when the user mentions: ERP, accounting, invoice, sales order, purchase order, customer, supplier, inventory, payment, GL, trial balance, P&L, balance sheet, tax, billing, modules, install module, onboard, CRM, manufacturing, healthcare, education, retail, employee, HR, payroll, salary, leave, attendance, expense claim, W-2, garnishment.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Auto-Detection (IMPORTANT)","type":"text"}]},{"type":"paragraph","content":[{"text":"When a user describes their business for the first time:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Detect business type","type":"text","marks":[{"type":"strong"}]},{"text":" from context (e.g., \"dental practice\" → dental, \"trucking company\" → fleet, \"restaurant\" → food-service)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Ask the user to confirm","type":"text","marks":[{"type":"strong"}]},{"text":" the detected type and proposed modules before proceeding","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"On confirmation","type":"text","marks":[{"type":"strong"}]},{"text":", call ","type":"text"},{"text":"setup-company","type":"text","marks":[{"type":"code_inline"}]},{"text":" with ","type":"text"},{"text":"--industry \u003cdetected-type>","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"--country \u003ccountry-code>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"After setup, call ","type":"text","marks":[{"type":"strong"}]},{"text":"list-all-actions","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" to discover newly available module-specific actions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use module-specific actions","type":"text","marks":[{"type":"strong"}]},{"text":" going forward (e.g., ","type":"text"},{"text":"health-add-patient","type":"text","marks":[{"type":"code_inline"}]},{"text":" instead of ","type":"text"},{"text":"add-customer","type":"text","marks":[{"type":"code_inline"}]},{"text":" for healthcare)","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"If a user mentions a country other than US, confirm with the user, then use ","type":"text"},{"text":"--country","type":"text","marks":[{"type":"code_inline"}]},{"text":" on ","type":"text"},{"text":"setup-company","type":"text","marks":[{"type":"code_inline"}]},{"text":" (e.g., ","type":"text"},{"text":"--country IN","type":"text","marks":[{"type":"code_inline"}]},{"text":" for India, ","type":"text"},{"text":"--country CA","type":"text","marks":[{"type":"code_inline"}]},{"text":" for Canada). This installs the regional compliance module after confirmation.","type":"text"}]},{"type":"paragraph","content":[{"text":"If an action returns \"Unknown action\" with a ","type":"text"},{"text":"suggested_module","type":"text","marks":[{"type":"code_inline"}]},{"text":" field:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Tell the user: \"This feature requires the {module} module. Want me to install it?\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Wait for explicit user confirmation","type":"text","marks":[{"type":"strong"}]},{"text":" before installing","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"On confirmation: ","type":"text"},{"text":"--action install-module --module-name {module}","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"After install: ","type":"text"},{"text":"--action list-all-actions","type":"text","marks":[{"type":"code_inline"}]},{"text":" to refresh available actions","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Industry values: retail, restaurant, healthcare, dental, veterinary, construction, manufacturing, legal, agriculture, hospitality, property, school, university, nonprofit, automotive, therapy, home-health, consulting, distribution, saas","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Setup (First Use Only)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"python3 {baseDir}/scripts/erpclaw-setup/db_query.py --action initialize-database\npython3 {baseDir}/scripts/db_query.py --action seed-defaults --company-id \u003cid>\npython3 {baseDir}/scripts/db_query.py --action setup-chart-of-accounts --company-id \u003cid> --template us_gaap","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Quick Start (Tier 1)","type":"text"}]},{"type":"paragraph","content":[{"text":"For all actions: ","type":"text"},{"text":"python3 {baseDir}/scripts/db_query.py --action \u003caction> [flags]","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"--action setup-company --name \"Acme Inc\" --country US --currency USD --fiscal-year-start-month 1\n--action add-customer --company-id \u003cid> --customer-name \"Jane Corp\" --email \"[email protected]\"\n--action create-sales-invoice --company-id \u003cid> --customer-id \u003cid> --items '[{\"item_id\":\"\u003cid>\",\"qty\":\"1\",\"rate\":\"100.00\"}]'\n--action submit-sales-invoice --invoice-id \u003cid>\n--action add-payment --company-id \u003cid> --payment-type Receive --party-type Customer --party-id \u003cid> --paid-amount \"100.00\"\n--action submit-payment --payment-id \u003cid>\n--action trial-balance --company-id \u003cid> --to-date 2026-03-08","type":"text"}]},{"type":"paragraph","content":[{"text":"New here? Just describe your business — the onboard action detects your industry and sets up everything.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"All Actions (Tier 2)","type":"text"}]},{"type":"paragraph","content":[{"text":"Run ","type":"text"},{"text":"list-all-actions","type":"text","marks":[{"type":"code_inline"}]},{"text":" for the complete list of all available actions. Regional modules add prefixed actions (india-*, eu-*, uk-*, canada-*) for local tax and compliance. For a web dashboard, run ","type":"text"},{"text":"setup-web-dashboard","type":"text","marks":[{"type":"code_inline"}]},{"text":" (auto-clones erpclaw-web, builds, deploys with nginx + SSL).","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Setup & Admin (44 actions)","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Action","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"initialize-database","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"setup-company","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"update-company","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-company","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-companies","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"DB init & company CRUD","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-currency","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-currencies","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"add-exchange-rate","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-exchange-rate","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-exchange-rates","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Currency & FX","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-payment-terms","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-payment-terms","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"add-uom","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-uoms","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"add-uom-conversion","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Terms & UoMs","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"seed-defaults","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"seed-demo-data","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"check-installation","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"install-guide","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"setup-web-dashboard","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Seeding & install","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-user","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"update-user","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-user","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-users","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"User management","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-role","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-roles","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"assign-role","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"revoke-role","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"set-password","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"seed-permissions","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"RBAC & security","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"link-telegram-user","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"unlink-telegram-user","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"check-telegram-permission","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Telegram integration","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"backup-database","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-backups","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"verify-backup","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"restore-database","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"cleanup-backups","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"DB backup/restore","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"get-audit-log","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-schema-version","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"update-regional-settings","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"System admin","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"fetch-exchange-rates","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"tutorial","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"onboarding-step","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"status","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Utilities","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"General Ledger (26 actions)","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Action","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"setup-chart-of-accounts","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Create CoA from template (us_gaap)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-account","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"update-account","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-account","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-accounts","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Account CRUD","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"freeze-account","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"unfreeze-account","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Lock/unlock accounts","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"post-gl-entries","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"reverse-gl-entries","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-gl-entries","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"GL posting","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-fiscal-year","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-fiscal-years","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Fiscal year management","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"validate-period-close","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"close-fiscal-year","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"reopen-fiscal-year","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Period closing","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-cost-center","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-cost-centers","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Cost center tracking","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-budget","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-budgets","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Budget management","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"seed-naming-series","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"next-series","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Document naming (INV-, SO-, PO-, etc.)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"check-gl-integrity","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-account-balance","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Validation","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"revalue-foreign-balances","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"FX revaluation","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"import-chart-of-accounts","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"import-opening-balances","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CSV import","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Journal Entries (16 actions)","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Action","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-journal-entry","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"update-journal-entry","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-journal-entry","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-journal-entries","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"JE CRUD","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"submit-journal-entry","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"cancel-journal-entry","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"amend-journal-entry","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"JE lifecycle","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"delete-journal-entry","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"duplicate-journal-entry","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"JE utilities","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"create-intercompany-je","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Intercompany journal entry","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-recurring-template","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"update-recurring-template","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-recurring-templates","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-recurring-template","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Recurring JE templates","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"process-recurring","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"delete-recurring-template","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Recurring JE processing","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Payments (13 actions)","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Action","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-payment","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"update-payment","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-payment","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-payments","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Payment CRUD","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"submit-payment","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"cancel-payment","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"delete-payment","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Payment lifecycle","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"create-payment-ledger-entry","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-outstanding","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-unallocated-payments","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Payment ledger","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"allocate-payment","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"reconcile-payments","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"bank-reconciliation","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Reconciliation","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Tax (17 actions)","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Action","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-tax-template","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"update-tax-template","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-tax-template","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-tax-templates","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"delete-tax-template","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tax template CRUD","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"resolve-tax-template","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"calculate-tax","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tax calculation","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-tax-category","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-tax-categories","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tax categories","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-tax-rule","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-tax-rules","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tax rules","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-item-tax-template","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Item-level tax overrides","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-tax-withholding-category","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-withholding-details","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Withholding","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"record-withholding-entry","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"record-1099-payment","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"generate-1099-data","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"1099 reporting","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Financial Reports (20 actions)","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Action","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"trial-balance","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"profit-and-loss","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"balance-sheet","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"cash-flow","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Core statements","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"general-ledger","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"party-ledger","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Ledger reports","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ar-aging","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"ap-aging","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Receivable/payable aging","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"budget-vs-actual","type":"text","marks":[{"type":"code_inline"}]},{"text":" (alias: ","type":"text"},{"text":"budget-variance","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Budget analysis","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"tax-summary","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"payment-summary","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"gl-summary","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Summaries","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"comparative-pl","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"check-overdue","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Analysis","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-elimination-rule","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-elimination-rules","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"run-elimination","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-elimination-entries","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Intercompany","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Selling (42 actions)","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Action","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-customer","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"update-customer","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-customer","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-customers","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Customer CRUD","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-quotation","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"update-quotation","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-quotation","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-quotations","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"submit-quotation","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Quotations","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"convert-quotation-to-so","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Quotation → Sales Order","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-sales-order","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"update-sales-order","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-sales-order","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-sales-orders","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"submit-sales-order","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"cancel-sales-order","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Sales orders","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"create-delivery-note","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-delivery-note","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-delivery-notes","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"submit-delivery-note","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"cancel-delivery-note","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Delivery","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"create-sales-invoice","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"update-sales-invoice","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-sales-invoice","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-sales-invoices","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"submit-sales-invoice","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"cancel-sales-invoice","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Invoicing","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"create-credit-note","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-credit-notes","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"update-invoice-outstanding","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Credit notes","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-sales-partner","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-sales-partners","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Sales partners","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-recurring-invoice-template","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"update-recurring-invoice-template","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-recurring-invoice-templates","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"generate-recurring-invoices","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Recurring invoices","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"import-customers","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CSV import","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-intercompany-account-map","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-intercompany-account-maps","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"create-intercompany-invoice","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-intercompany-invoices","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"cancel-intercompany-invoice","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Intercompany","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Buying (34 actions)","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Action","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-supplier","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"update-supplier","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-supplier","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-suppliers","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Supplier CRUD","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-material-request","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"submit-material-request","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-material-requests","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Material requests","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-rfq","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"submit-rfq","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-rfqs","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"RFQs","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-supplier-quotation","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-supplier-quotations","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"compare-supplier-quotations","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Supplier quotes","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-purchase-order","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"update-purchase-order","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-purchase-order","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-purchase-orders","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"submit-purchase-order","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"cancel-purchase-order","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Purchase orders","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"create-purchase-receipt","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-purchase-receipt","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-purchase-receipts","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"submit-purchase-receipt","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"cancel-purchase-receipt","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Receipts","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"create-purchase-invoice","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"update-purchase-invoice","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-purchase-invoice","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-purchase-invoices","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"submit-purchase-invoice","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"cancel-purchase-invoice","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Purchase invoices","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"create-debit-note","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"update-purchase-outstanding","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"add-landed-cost-voucher","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Adjustments","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"import-suppliers","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CSV import","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Inventory (36 actions)","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Action","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-item","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"update-item","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-item","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-items","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Item master","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-item-group","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-item-groups","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Item groups","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-warehouse","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"update-warehouse","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-warehouses","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Warehouses","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-stock-entry","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-stock-entry","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-stock-entries","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"submit-stock-entry","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"cancel-stock-entry","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Stock entries","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"create-stock-ledger-entries","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"reverse-stock-ledger-entries","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Stock ledger","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"get-stock-balance","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"stock-balance","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"stock-balance-report","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"stock-ledger-report","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Stock reports","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-batch","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-batches","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"add-serial-number","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-serial-numbers","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Batch & serial tracking","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-price-list","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"add-item-price","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-item-price","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"add-pricing-rule","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Pricing","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-stock-reconciliation","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"submit-stock-reconciliation","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Reconciliation","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"revalue-stock","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-stock-revaluations","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-stock-revaluation","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"cancel-stock-revaluation","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Revaluation","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"check-reorder","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"import-items","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Utilities","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Billing & Metering (21 actions)","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Action","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-meter","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"update-meter","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-meter","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-meters","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Meter CRUD","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-meter-reading","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-meter-readings","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Readings","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-usage-event","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"add-usage-events-batch","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Usage tracking","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-rate-plan","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"update-rate-plan","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-rate-plan","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-rate-plans","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"rate-consumption","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Rate plans","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"create-billing-period","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"run-billing","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"generate-invoices","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Billing cycles","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-billing-adjustment","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-billing-periods","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-billing-period","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Adjustments","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-prepaid-credit","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-prepaid-balance","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Prepaid credits","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Advanced Accounting (45 actions)","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Action","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-revenue-contract","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"update-revenue-contract","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-revenue-contract","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-revenue-contracts","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Revenue contract CRUD","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-performance-obligation","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-performance-obligations","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"satisfy-performance-obligation","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ASC 606 performance obligations","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-variable-consideration","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-variable-considerations","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"modify-contract","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Variable consideration & modifications","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"calculate-revenue-schedule","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"generate-revenue-entries","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"revenue-waterfall-report","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"revenue-recognition-summary","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Revenue recognition processing & reports","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-lease","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"update-lease","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-lease","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-leases","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"classify-lease","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ASC 842 lease CRUD & classification","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"calculate-rou-asset","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"calculate-lease-liability","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"generate-amortization-schedule","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"record-lease-payment","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Lease calculations & payments","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"lease-maturity-report","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"lease-disclosure-report","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"lease-summary","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Lease reports","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-ic-transaction","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"update-ic-transaction","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-ic-transaction","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-ic-transactions","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Intercompany transaction CRUD","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"approve-ic-transaction","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"post-ic-transaction","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"add-transfer-price-rule","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-transfer-price-rules","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"IC approvals & transfer pricing","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ic-reconciliation-report","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"ic-elimination-report","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"IC reports","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-consolidation-group","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-consolidation-groups","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"add-group-entity","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"add-currency-translation","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Multi-entity consolidation setup","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"run-consolidation","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"generate-elimination-entries","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"consolidation-trial-balance-report","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"consolidation-summary","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Consolidation processing & reports","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"standards-compliance-dashboard","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ASC 606/842 compliance overview","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"HR & Payroll (49 actions)","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Action","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-employee","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"update-employee","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-employee","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-employees","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Employee CRUD","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-department","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-departments","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"add-designation","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-designations","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Org structure","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-leave-type","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-leave-types","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"add-leave-allocation","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-leave-balance","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Leave config","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-leave-application","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"approve-leave","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"reject-leave","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-leave-applications","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Leave requests","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"mark-attendance","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"bulk-mark-attendance","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-attendance","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"add-holiday-list","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Attendance","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-expense-claim","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"submit-expense-claim","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"approve-expense-claim","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"reject-expense-claim","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Expenses","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"update-expense-claim-status","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-expense-claims","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"record-lifecycle-event","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Expense status & HR events","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-salary-component","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-salary-components","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"add-salary-structure","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-salary-structure","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-salary-structures","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Salary components & structures","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-salary-assignment","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-salary-assignments","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"add-income-tax-slab","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"update-fica-config","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"update-futa-suta-config","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Payroll config","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"create-payroll-run","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"generate-salary-slips","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-salary-slip","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-salary-slips","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Payroll processing","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"submit-payroll-run","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"cancel-payroll-run","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"generate-w2-data","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Payroll lifecycle & W-2","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"add-garnishment","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"update-garnishment","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-garnishment","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-garnishments","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"payroll-status","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Garnishments & status","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Module Management & OS (41 actions)","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Action","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"install-module","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Install a module from GitHub (","type":"text"},{"text":"--module-name \u003cname>","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"remove-module","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Remove an installed module (","type":"text"},{"text":"--module-name \u003cname>","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"update-modules","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Update all or a specific module","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"list-modules","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"available-modules","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"search-modules","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"module-status","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Browse and search module catalog","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"rebuild-action-cache","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-all-actions","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Refresh available actions after module changes","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"list-profiles","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"onboard","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Browse business profiles, auto-install for a business type","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"validate-module","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"generate-module","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"configure-module","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"deploy-module","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-industries","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"OS: Module lifecycle","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"build-table-registry","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-articles","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"OS: Constitution & schema registry","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"install-suite","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"classify-operation","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"run-audit","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"compliance-weather-status","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"OS: Suite install, tier classification, audit","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"schema-plan","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"schema-apply","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"schema-rollback","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"schema-drift","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"deploy-audit-log","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"OS: Schema migration & deploy audit","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"semantic-check","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"semantic-rules-list","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"OS: Semantic correctness — validates GL postings use correct account types","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"log-improvement","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"list-improvements","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"review-improvement","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"OS: Self-improvement log — track AI-proposed changes","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"dgm-run-variant","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"dgm-list-variants","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"dgm-select-best","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"OS: DGM variant engine — evolutionary optimization (non-financial code only)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"heartbeat-analyze","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"heartbeat-report","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"heartbeat-suggest","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"OS: Heartbeat — usage patterns, gap detection, module suggestions","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"detect-gaps","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"suggest-modules","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"OS: Gap detection — identifies missing modules for your industry","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"regenerate-skill-md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"OS: Regenerate SKILL.md after module changes","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Domain Status (9 actions)","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Action","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"gl-status","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"journals-status","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"payments-status","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"tax-status","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"reports-status","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"selling-status","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"buying-status","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"inventory-status","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"billing-status","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Per-domain health check","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"Always confirm with user before running:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"setup-company","type":"text","marks":[{"type":"code_inline"}]},{"text":" (with --industry or --country), ","type":"text"},{"text":"onboard","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"install-module","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"remove-module","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"update-modules","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"submit-*","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"cancel-*","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"approve-*","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"reject-*","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"run-elimination","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"run-consolidation","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"restore-database","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"close-fiscal-year","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"initialize-database --force","type":"text","marks":[{"type":"code_inline"}]},{"text":". All ","type":"text"},{"text":"add-*","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"get-*","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"list-*","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"update-*","type":"text","marks":[{"type":"code_inline"}]},{"text":" run immediately.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Technical Details (Tier 3)","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Router: ","type":"text"},{"text":"scripts/db_query.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" → 14 core domains + erpclaw-os. Modules from GitHub to ","type":"text"},{"text":"~/.openclaw/erpclaw/modules/","type":"text","marks":[{"type":"code_inline"}]},{"text":". Single SQLite DB (WAL mode). 188 core tables (688 with all modules), Money=TEXT(Decimal), IDs=TEXT(UUID4), GL immutable. Python 3.10+.","type":"text"}]}]},"metadata":{"cron":[{"message":"Using erpclaw, run the process-recurring action.","announce":false,"timezone":"America/Chicago","expression":"0 1 * * *"},{"message":"Using erpclaw, run the generate-recurring-invoices action.","announce":false,"timezone":"America/Chicago","expression":"0 6 * * *"},{"message":"Using erpclaw, run the check-reorder action.","announce":false,"timezone":"America/Chicago","expression":"0 7 * * *"},{"message":"Using erpclaw, run the check-overdue action and summarize any overdue invoices.","announce":false,"timezone":"America/Chicago","expression":"0 8 * * *"}],"date":"2026-06-05","name":"erpclaw","tags":["erp","accounting","invoicing","inventory","purchasing","tax","billing","payments","gl","reports","sales","buying","setup","hr","payroll","employees","leave","attendance","salary","revenue-recognition","lease-accounting","intercompany","consolidation"],"author":"@skillopedia","source":{"stars":2012,"repo_name":"openclaw-master-skills","origin_url":"https://github.com/leoyeai/openclaw-master-skills/blob/HEAD/skills/erp-claw/SKILL.md","repo_owner":"leoyeai","body_sha256":"c1e86e19e5dc2fc040e6fd13eecbdf3ba9bb9fd64c2fd6601e77e560ad4c6a59","cluster_key":"e2b30d16d6bcb3502c4f6104d68560acc3806120104ab00e326b1208e85a1521","clean_bundle":{"format":"clean-skill-bundle-v1","source":"leoyeai/openclaw-master-skills/skills/erp-claw/SKILL.md","attachments":[{"id":"01520bef-622e-5bce-8305-36aba73d3f43","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/01520bef-622e-5bce-8305-36aba73d3f43/attachment.md","path":"README.md","size":8621,"sha256":"1dd2c0c7d33fdf7d92c64faba7edb3f40a0ad11bcb2b8805d329c9b3e7fe3a5a","contentType":"text/markdown; charset=utf-8"},{"id":"1bb58eeb-ff3b-5a3a-9492-ea427d41dde8","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1bb58eeb-ff3b-5a3a-9492-ea427d41dde8/attachment.yaml","path":"UI.yaml","size":255270,"sha256":"7c7062cdbc7877a3daa5b22dc6b4e296ce0b1dad7cdf615fd0ddcf0f278b581c","contentType":"application/yaml; charset=utf-8"},{"id":"2798699a-3fb8-54c8-b6ee-e2505b1f8519","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2798699a-3fb8-54c8-b6ee-e2505b1f8519/attachment.json","path":"_meta.json","size":624,"sha256":"4988af6e46e48c1e7af37f88cf2d12b5c0c749d4a42d82b9931c4cd1f2e2bff6","contentType":"application/json; charset=utf-8"},{"id":"b0a5bea6-863d-5e29-880c-4842c2dcd8b0","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b0a5bea6-863d-5e29-880c-4842c2dcd8b0/attachment.py","path":"scripts/db_query.py","size":32239,"sha256":"4bfef766fe9f1352fe3259fb028bcf0b5e9e27282857ef3da23da4d926892de0","contentType":"text/x-python; charset=utf-8"},{"id":"b858edcb-fc92-58e1-b084-72f15f2c25ef","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b858edcb-fc92-58e1-b084-72f15f2c25ef/attachment.py","path":"scripts/erpclaw-accounting-adv/consolidation.py","size":15443,"sha256":"724f2e78bd8818f21375b18d23e519762bcd399733b0771c70daaa27a67df846","contentType":"text/x-python; charset=utf-8"},{"id":"a847d78d-8eb2-54da-a034-119d786292c3","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a847d78d-8eb2-54da-a034-119d786292c3/attachment.py","path":"scripts/erpclaw-accounting-adv/db_query.py","size":5677,"sha256":"a29e60351ef2b18671a9f196281ce4bd4b7dd9909cd6cb4f95d88a190fe2ec50","contentType":"text/x-python; charset=utf-8"},{"id":"e5e0c557-8d7d-5457-88e2-a78e054fe60b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e5e0c557-8d7d-5457-88e2-a78e054fe60b/attachment.py","path":"scripts/erpclaw-accounting-adv/init_db.py","size":18734,"sha256":"a6961a01ea0f4eca62cb1ec3c0b780c51058b616fe72b115c8aeed6002bb4925","contentType":"text/x-python; charset=utf-8"},{"id":"dac9da29-8754-5c5e-ad1c-2c777017029d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/dac9da29-8754-5c5e-ad1c-2c777017029d/attachment.py","path":"scripts/erpclaw-accounting-adv/intercompany.py","size":16189,"sha256":"d74737409fffd82cf857b1610cd8df1d8a2bf368b79f7013d9c9f14ae55e6e77","contentType":"text/x-python; charset=utf-8"},{"id":"c5338acc-e3ea-5c96-96ca-17104cc39243","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c5338acc-e3ea-5c96-96ca-17104cc39243/attachment.py","path":"scripts/erpclaw-accounting-adv/leases.py","size":21978,"sha256":"f485d93ba9ff32b4f7769b9c9c781b0c7589d1a8aaa7c1452f22fc3b5c9eb461","contentType":"text/x-python; charset=utf-8"},{"id":"17c03489-0591-5794-93a7-c5cfbee58282","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/17c03489-0591-5794-93a7-c5cfbee58282/attachment.py","path":"scripts/erpclaw-accounting-adv/reports.py","size":3665,"sha256":"0a38b6eb367ca92fd84a76de77fe574be53ff1ae8cea6fad52a7f7153d8df449","contentType":"text/x-python; charset=utf-8"},{"id":"c90cc8c4-9054-55a1-bf22-2e5b40d9bd1b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c90cc8c4-9054-55a1-bf22-2e5b40d9bd1b/attachment.py","path":"scripts/erpclaw-accounting-adv/revenue.py","size":25358,"sha256":"555b7b9b464b791ef4fdfa8f7db11f91b85f03010af8b456abec2d64fa7f7e8b","contentType":"text/x-python; charset=utf-8"},{"id":"04728811-6475-5609-9086-14276b81439a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/04728811-6475-5609-9086-14276b81439a/attachment.py","path":"scripts/erpclaw-billing/db_query.py","size":61008,"sha256":"affa9fe480b10e999dac8fec6813c76e9a1dd9c59f37149d6517de01e73be379","contentType":"text/x-python; charset=utf-8"},{"id":"bb651cb8-7951-5a44-bb65-d3b469b871af","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/bb651cb8-7951-5a44-bb65-d3b469b871af/attachment.py","path":"scripts/erpclaw-buying/db_query.py","size":176817,"sha256":"4cda8194ffa5fc2a8c86325af76d07e21f1fa6367c642c5c9c1291c6600be489","contentType":"text/x-python; charset=utf-8"},{"id":"44e526cf-6ef6-5138-9a9f-5a9f8b96abcd","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/44e526cf-6ef6-5138-9a9f-5a9f8b96abcd/attachment.json","path":"scripts/erpclaw-gl/assets/charts/us_gaap.json","size":20612,"sha256":"8f894ae401adeedafe2840213553b9aed2df0b00409619fe96440f6460862670","contentType":"application/json; charset=utf-8"},{"id":"e6249cf8-b8d7-511c-a234-81956de19cc7","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e6249cf8-b8d7-511c-a234-81956de19cc7/attachment.py","path":"scripts/erpclaw-gl/db_query.py","size":69372,"sha256":"769a615421baadfb4aca65a0314ecf87b120be19f949ba086cef2968143d7d2a","contentType":"text/x-python; charset=utf-8"},{"id":"7217a51f-06b8-52ad-afcd-0ed8e8c0369e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7217a51f-06b8-52ad-afcd-0ed8e8c0369e/attachment.py","path":"scripts/erpclaw-hr/db_query.py","size":140043,"sha256":"17b26f9a1ed004de91b3d273b48ffd13e2465842ce3d4131ecdfcf82e8bf7002","contentType":"text/x-python; charset=utf-8"},{"id":"cee7c097-063b-5839-810a-5aa931ab053e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/cee7c097-063b-5839-810a-5aa931ab053e/attachment.py","path":"scripts/erpclaw-inventory/db_query.py","size":116182,"sha256":"2cf187c8f0752a042f0560be25e93c3d0c2abf30942c9f9a8d7c28c9507acc1c","contentType":"text/x-python; charset=utf-8"},{"id":"b0051e85-e284-5f27-84e2-f2dff72008ec","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b0051e85-e284-5f27-84e2-f2dff72008ec/attachment.py","path":"scripts/erpclaw-journals/db_query.py","size":53753,"sha256":"ba85e70551a745eac26ecc5aba1d7ed3152a50e86e5487425c1cb91df080cc72","contentType":"text/x-python; charset=utf-8"},{"id":"cb28b093-8fdd-5db5-bf8d-7a4d9d92585f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/cb28b093-8fdd-5db5-bf8d-7a4d9d92585f/attachment.py","path":"scripts/erpclaw-manufacturing/db_query.py","size":120716,"sha256":"0c27df9a3ef9a77365c85734d841414445960bd0fa86075cf2a1ad40ae3b2ce8","contentType":"text/x-python; charset=utf-8"},{"id":"28c91e1f-36b0-589d-86e7-715d09669974","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/28c91e1f-36b0-589d-86e7-715d09669974/attachment.py","path":"scripts/erpclaw-meta/db_query.py","size":84188,"sha256":"0461b819f51e498702e2666d600ac32974b2ebf1568092ba903a38a14caaaacd","contentType":"text/x-python; charset=utf-8"},{"id":"e894c99f-6508-5c4b-be04-875a79e1e6fd","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e894c99f-6508-5c4b-be04-875a79e1e6fd/attachment.py","path":"scripts/erpclaw-os/__init__.py","size":0,"sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","contentType":"text/x-python; charset=utf-8"},{"id":"9608e2b9-f21e-59ea-a2ba-7f420af372e2","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9608e2b9-f21e-59ea-a2ba-7f420af372e2/attachment.py","path":"scripts/erpclaw-os/adversarial_audit.py","size":13917,"sha256":"b30e78f9f9ed43e06a4373ea76d090578819a0bb16ce265440401bfa56f442e3","contentType":"text/x-python; charset=utf-8"},{"id":"e13535e8-2a97-50b4-8f4d-6b9257e9a3f7","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e13535e8-2a97-50b4-8f4d-6b9257e9a3f7/attachment.py","path":"scripts/erpclaw-os/compliance_weather.py","size":7562,"sha256":"26f05307cfd49b0aabed10e0b9cb87f6169136fc8732650cb8b82a8941825456","contentType":"text/x-python; charset=utf-8"},{"id":"d5ea739d-ab0d-53f4-8002-c10c42651eb1","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d5ea739d-ab0d-53f4-8002-c10c42651eb1/attachment.py","path":"scripts/erpclaw-os/configure_module.py","size":9033,"sha256":"cf452ec65aae4dfac53b06d2d46a452a3c7c15bd500777acbd29b5d9c5ef312e","contentType":"text/x-python; charset=utf-8"},{"id":"6de7ddd5-2b0c-5e97-9df8-2ce87cfcdf3b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6de7ddd5-2b0c-5e97-9df8-2ce87cfcdf3b/attachment.py","path":"scripts/erpclaw-os/constitution.py","size":9342,"sha256":"cdfb893a47b363745a0cb55a7fbb8bfe57999d90c4eb045f8f49ff64ccee57ce","contentType":"text/x-python; charset=utf-8"},{"id":"fd31d7af-4eef-5b11-98a0-7a630c465245","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/fd31d7af-4eef-5b11-98a0-7a630c465245/attachment.py","path":"scripts/erpclaw-os/db_query.py","size":23333,"sha256":"e0bd55848ca955d8e2a275070ccc1c83594403539f857bd4115780bfa139204b","contentType":"text/x-python; charset=utf-8"},{"id":"6595e891-5ed9-5691-9653-0dd5e6c21192","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6595e891-5ed9-5691-9653-0dd5e6c21192/attachment.py","path":"scripts/erpclaw-os/dependency_resolver.py","size":5880,"sha256":"4f3d67b54f10976500f493ba43038ddf9b966919b3b43e0faf134395c246b14c","contentType":"text/x-python; charset=utf-8"},{"id":"d5464514-0d62-55de-a548-bde197db777c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d5464514-0d62-55de-a548-bde197db777c/attachment.py","path":"scripts/erpclaw-os/deploy_audit.py","size":4691,"sha256":"c641122fab59e6c46d2b81fb649e077a533a0169c3cbc966c72a64c74cfba045","contentType":"text/x-python; charset=utf-8"},{"id":"54d79ff9-50c1-53b6-b2f7-335370b7e5e3","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/54d79ff9-50c1-53b6-b2f7-335370b7e5e3/attachment.py","path":"scripts/erpclaw-os/deploy_pipeline.py","size":8095,"sha256":"7167d4ae497e89cc9368cd87c89dba65c52eeec0074368ebc16ce09322043d47","contentType":"text/x-python; charset=utf-8"},{"id":"2e05d1a6-28dd-5304-82b0-5e8d14041377","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2e05d1a6-28dd-5304-82b0-5e8d14041377/attachment.py","path":"scripts/erpclaw-os/dgm_engine.py","size":18845,"sha256":"013fc1961c1ee6580013fd8056ac834d7af9a3eb665ff5bd70061e3fd046fbd4","contentType":"text/x-python; charset=utf-8"},{"id":"58913808-f533-54a2-a547-ba95bbffc86c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/58913808-f533-54a2-a547-ba95bbffc86c/attachment.py","path":"scripts/erpclaw-os/feature_matrix.py","size":54932,"sha256":"46810afc4bb5b62f2dedb3ae5097293ccbea5016aa043928e6985c76a1c70299","contentType":"text/x-python; charset=utf-8"},{"id":"48878f58-8aeb-5594-b370-11c192efc749","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/48878f58-8aeb-5594-b370-11c192efc749/attachment.py","path":"scripts/erpclaw-os/gap_detector.py","size":29351,"sha256":"e2ec8b0642585c4696efd13a9513d26143bad675e4a99e2283461c900d6dc024","contentType":"text/x-python; charset=utf-8"},{"id":"e731b6bd-fc2b-51d5-8dad-b7f7453e8c2e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e731b6bd-fc2b-51d5-8dad-b7f7453e8c2e/attachment.py","path":"scripts/erpclaw-os/generate_module.py","size":53865,"sha256":"c778b8e19a4af27a8fd108a664136b6fbd69fc2d8b59b1408dcbf19419050cef","contentType":"text/x-python; charset=utf-8"},{"id":"cfe3405c-9b03-5e46-90ab-d278d92295c7","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/cfe3405c-9b03-5e46-90ab-d278d92295c7/attachment.py","path":"scripts/erpclaw-os/gl_invariant_checker.py","size":9747,"sha256":"1c69d9fd6c86f36687b54f6223081d272ad2ed46ec22e111bcce91e0d847ea5d","contentType":"text/x-python; charset=utf-8"},{"id":"07f97b2f-212a-5a56-8ee2-c929eedf9070","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/07f97b2f-212a-5a56-8ee2-c929eedf9070/attachment.py","path":"scripts/erpclaw-os/heartbeat_analysis.py","size":21871,"sha256":"68500c50351fc4bd1cf0d233c43edc6426e3769ce04e0b57dc4586435b9f4452","contentType":"text/x-python; charset=utf-8"},{"id":"a40a3780-929c-5b6a-9f8a-3f41ed39e06a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a40a3780-929c-5b6a-9f8a-3f41ed39e06a/attachment.py","path":"scripts/erpclaw-os/improvement_log.py","size":9654,"sha256":"fe2a27a1161326a1b52febe66c1bc8a110cb65c69d3b84b7a70e098184e12522","contentType":"text/x-python; charset=utf-8"},{"id":"3f01464e-e661-5838-99fa-00ccb2d9663f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3f01464e-e661-5838-99fa-00ccb2d9663f/attachment.py","path":"scripts/erpclaw-os/in_module_generator.py","size":43477,"sha256":"20d27960a6106e1a8ee1462541979c675a81783e75016a50afb85206066f059a","contentType":"text/x-python; charset=utf-8"},{"id":"e4036160-f5bd-505c-8037-85df91e7b01a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e4036160-f5bd-505c-8037-85df91e7b01a/attachment.py","path":"scripts/erpclaw-os/industry_configs.py","size":38359,"sha256":"dc92a8724aa36feba1aaafc488b0a230cd9bc658ea473813728dc95ca6e9bf72","contentType":"text/x-python; charset=utf-8"},{"id":"36ae8d37-5fb0-56a8-882d-0ee6a34c0ae9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/36ae8d37-5fb0-56a8-882d-0ee6a34c0ae9/attachment.py","path":"scripts/erpclaw-os/install_suite.py","size":8183,"sha256":"f9923de57e0f0ed99361c36c530f00d16425f2c312e998c543394162e206d4a7","contentType":"text/x-python; charset=utf-8"},{"id":"da34eb3f-e56a-57a8-bc3b-1705f699133e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/da34eb3f-e56a-57a8-bc3b-1705f699133e/attachment.py","path":"scripts/erpclaw-os/pattern_library.py","size":13178,"sha256":"6a7807f1d6e9e1dcc4f34adb7d6151e9f50a8b4ab6093155bfa8cf1bdcb0f2dd","contentType":"text/x-python; charset=utf-8"},{"id":"0fe20131-191a-54d1-a833-b6a0c3e6d3f5","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0fe20131-191a-54d1-a833-b6a0c3e6d3f5/attachment.md","path":"scripts/erpclaw-os/references/pattern_catalog.md","size":30665,"sha256":"6d00afd37cd42a30fca150e775aa90649608c57a16126431612cb43421251d9c","contentType":"text/markdown; charset=utf-8"},{"id":"622ea3a0-655b-5335-a9e3-53ce709a6272","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/622ea3a0-655b-5335-a9e3-53ce709a6272/attachment.py","path":"scripts/erpclaw-os/regression_gate.py","size":4345,"sha256":"da5f2c63424a0c14124fb9c2ecb7d8118bde251cfa6527b3dc057fa720c51bf0","contentType":"text/x-python; charset=utf-8"},{"id":"2156b293-af82-58ec-8044-c8a46dfaba07","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2156b293-af82-58ec-8044-c8a46dfaba07/attachment.py","path":"scripts/erpclaw-os/research_engine.py","size":52080,"sha256":"6c047c43154d05b4b0b9fe463de4dc1cca44ede7543f72dba709737d60aebcb3","contentType":"text/x-python; charset=utf-8"},{"id":"482bdf72-0778-5a4b-b802-a70d918c3ab1","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/482bdf72-0778-5a4b-b802-a70d918c3ab1/attachment.py","path":"scripts/erpclaw-os/sandbox.py","size":10828,"sha256":"94a5e31c5d0a14b681cbc91156a335cb4dd1a102353fee7974b33f6eaa378c94","contentType":"text/x-python; charset=utf-8"},{"id":"28cc76a6-24ff-53b3-b79d-e1535324bee4","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/28cc76a6-24ff-53b3-b79d-e1535324bee4/attachment.py","path":"scripts/erpclaw-os/schema_diff.py","size":10132,"sha256":"57d1218cd78c5546e4f8f35dd072be62534e94b0dce324317894b128e1c04780","contentType":"text/x-python; charset=utf-8"},{"id":"4db80542-4b82-5c17-8762-8083f1d3e304","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4db80542-4b82-5c17-8762-8083f1d3e304/attachment.py","path":"scripts/erpclaw-os/schema_migrator.py","size":14598,"sha256":"979b40481d585bc589bea230dd35f0057a9ea74b11783a1107db8d643bb2f233","contentType":"text/x-python; charset=utf-8"},{"id":"867d0b07-62e1-5f53-8064-4171d3f07f15","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/867d0b07-62e1-5f53-8064-4171d3f07f15/attachment.py","path":"scripts/erpclaw-os/semantic_engine.py","size":35606,"sha256":"5cd02fa53230410651255a60a72b786cda2133d885f4a5c7c04e0fff4659e1f6","contentType":"text/x-python; charset=utf-8"},{"id":"98908442-438e-5469-a2ad-e1c2996dea11","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/98908442-438e-5469-a2ad-e1c2996dea11/attachment.py","path":"scripts/erpclaw-os/tier_classifier.py","size":18621,"sha256":"2bfdf0645ff375d888de77fedb7d126f0b8bf75931432b9994c03d3e567637dc","contentType":"text/x-python; charset=utf-8"},{"id":"704f1a07-321f-5139-8ee1-1557539b4e9a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/704f1a07-321f-5139-8ee1-1557539b4e9a/attachment.py","path":"scripts/erpclaw-os/validate_module.py","size":56026,"sha256":"fd4e86ded120e59d233b93699eacf61ca7e3d877e2f8334f3d9d932bddc76fda","contentType":"text/x-python; charset=utf-8"},{"id":"f5e19af3-04e1-595c-884c-4ba00580e01c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f5e19af3-04e1-595c-884c-4ba00580e01c/attachment.py","path":"scripts/erpclaw-os/variant_manager.py","size":8935,"sha256":"ad4113c3e3e2e3a50b00e1231b0a6d50bf2347953540a8b54cde40fff97eceaf","contentType":"text/x-python; charset=utf-8"},{"id":"a56461e4-eec0-5566-b0ff-3aded0ce8601","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a56461e4-eec0-5566-b0ff-3aded0ce8601/attachment.py","path":"scripts/erpclaw-payments/db_query.py","size":51245,"sha256":"d6dc30f29b497f5ff6d34270275ea11f33773e1bcc45badfe0a5f6b2ff5fe38b","contentType":"text/x-python; charset=utf-8"},{"id":"ddf9d84a-dd38-53e4-95ae-b4acd851900b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ddf9d84a-dd38-53e4-95ae-b4acd851900b/attachment.py","path":"scripts/erpclaw-payroll/db_query.py","size":182245,"sha256":"2b00050c958f7dac44bb2d2dc6e1ecb603e550db452a6a88bdf015e01dc0c738","contentType":"text/x-python; charset=utf-8"},{"id":"98532b59-9d76-5b10-b3a6-4f41dd8e33aa","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/98532b59-9d76-5b10-b3a6-4f41dd8e33aa/attachment.py","path":"scripts/erpclaw-reports/db_query.py","size":59812,"sha256":"c7c2673a56bc3d47e5e8098e355b0a2cbf06f8abc60649572f443c45d9cb39d3","contentType":"text/x-python; charset=utf-8"},{"id":"25aa3b53-5b7d-5b08-8fe3-e75261ae9779","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/25aa3b53-5b7d-5b08-8fe3-e75261ae9779/attachment.py","path":"scripts/erpclaw-selling/db_query.py","size":204162,"sha256":"be3b1036e8a11d81a6cd31f590618e2b95d2d2100c98bcb9fb7945a90de3cafc","contentType":"text/x-python; charset=utf-8"},{"id":"471d8c43-a920-5897-8d0e-0559626a1a27","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/471d8c43-a920-5897-8d0e-0559626a1a27/attachment.json","path":"scripts/erpclaw-setup/assets/currencies.json","size":1458,"sha256":"85ed4f2ec50434ccaadcdaac5f909eab604e2ee0412d5e0cb7fdf8f15eed45ae","contentType":"application/json; charset=utf-8"},{"id":"f9f30e50-4378-5d37-8878-ee0d02d95c30","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f9f30e50-4378-5d37-8878-ee0d02d95c30/attachment.json","path":"scripts/erpclaw-setup/assets/default_payment_terms.json","size":1113,"sha256":"a7756c0d203cad525e9b5db7f6ad768cedba2308154fcb448df42b21f733d80d","contentType":"application/json; charset=utf-8"},{"id":"97653843-ae22-5f4b-99e6-77a703861a7e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/97653843-ae22-5f4b-99e6-77a703861a7e/attachment.json","path":"scripts/erpclaw-setup/assets/default_uom.json","size":870,"sha256":"baca02db1311acee08226ec22dc2ec98f2306c30f7cfa6cd46840b0711b83259","contentType":"application/json; charset=utf-8"},{"id":"1c871f4d-2a09-5cda-85b3-4409ee180456","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1c871f4d-2a09-5cda-85b3-4409ee180456/attachment.json","path":"scripts/erpclaw-setup/assets/us_gaap.json","size":20612,"sha256":"8f894ae401adeedafe2840213553b9aed2df0b00409619fe96440f6460862670","contentType":"application/json; charset=utf-8"},{"id":"5dbad5ef-8ada-57e2-a926-2637363658d4","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5dbad5ef-8ada-57e2-a926-2637363658d4/attachment.py","path":"scripts/erpclaw-setup/db_query.py","size":91108,"sha256":"55cf322ffb1b6e2c2d7537f9bf033300664519a28166a4fa79b5c78bf51173bf","contentType":"text/x-python; charset=utf-8"},{"id":"c0949b09-b344-5221-a32b-cf3556e2b467","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c0949b09-b344-5221-a32b-cf3556e2b467/attachment.py","path":"scripts/erpclaw-setup/init_schema.py","size":182195,"sha256":"71abf885dd87a659dd855b5b9cdf5cb97939f1fa24284d0b97c21b5c6bd938b6","contentType":"text/x-python; charset=utf-8"},{"id":"4f34e18f-df1f-5978-8245-f7f735d00a41","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4f34e18f-df1f-5978-8245-f7f735d00a41/attachment.py","path":"scripts/erpclaw-setup/lib/erpclaw_lib/__init__.py","size":84,"sha256":"897593a0d14868bc4b07bf1ccb30bbb7e3764980bc1ea7310611a80bcfe15de4","contentType":"text/x-python; charset=utf-8"},{"id":"6b2b3b8d-c379-5eaa-9e29-31d40270367a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6b2b3b8d-c379-5eaa-9e29-31d40270367a/attachment.py","path":"scripts/erpclaw-setup/lib/erpclaw_lib/args.py","size":2408,"sha256":"2c8da57e7c7307ae19fc011fc44e06915e431dd5a24f18e45f2a3e877b5294eb","contentType":"text/x-python; charset=utf-8"},{"id":"a9b7d58e-4e2f-50b4-96c7-ee987f450a8a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a9b7d58e-4e2f-50b4-96c7-ee987f450a8a/attachment.py","path":"scripts/erpclaw-setup/lib/erpclaw_lib/audit.py","size":1659,"sha256":"1991438ca7c3b6acb62919f27b7467815825928909ed4f4961ac66b677685ad6","contentType":"text/x-python; charset=utf-8"},{"id":"19197890-5903-5ff8-8cbc-91d19ef7cc69","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/19197890-5903-5ff8-8cbc-91d19ef7cc69/attachment.py","path":"scripts/erpclaw-setup/lib/erpclaw_lib/cross_skill.py","size":14298,"sha256":"87ef785d4c53c5b918c0340f181c9dc8c224660273ba5ab3d2c1bbf272cdc2c0","contentType":"text/x-python; charset=utf-8"},{"id":"de74d269-bd87-5499-8bbb-705ed389b7ec","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/de74d269-bd87-5499-8bbb-705ed389b7ec/attachment.py","path":"scripts/erpclaw-setup/lib/erpclaw_lib/crypto.py","size":7825,"sha256":"2c378e6dcf2e7a8aac02c61dd561564004d54a7075216edbf5e7deea5c01d21e","contentType":"text/x-python; charset=utf-8"},{"id":"ad65a4b6-325d-5451-957b-12ed59ae5440","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ad65a4b6-325d-5451-957b-12ed59ae5440/attachment.py","path":"scripts/erpclaw-setup/lib/erpclaw_lib/csv_import.py","size":6019,"sha256":"63f8a632e00516fa4a09b6213f89a7b732121d713ccdbe971c56d90f8b1c093c","contentType":"text/x-python; charset=utf-8"},{"id":"2e7c8ae6-b637-5926-b52e-4774da1ee4ab","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2e7c8ae6-b637-5926-b52e-4774da1ee4ab/attachment.py","path":"scripts/erpclaw-setup/lib/erpclaw_lib/custom_fields.py","size":10263,"sha256":"f276eb030bc097e6a12611c87194d7a78caa1444bd130217cc1a2008e1333041","contentType":"text/x-python; charset=utf-8"},{"id":"b99d7b76-b89c-595c-8e22-76a5898a84b6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b99d7b76-b89c-595c-8e22-76a5898a84b6/attachment.py","path":"scripts/erpclaw-setup/lib/erpclaw_lib/datetime_utils.py","size":353,"sha256":"8c115d2472aa93aab5698bfec91f5b393c58242ab56642ae21411d6fe3c93206","contentType":"text/x-python; charset=utf-8"},{"id":"286c941c-ab91-563e-9b2b-ba4024a6027a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/286c941c-ab91-563e-9b2b-ba4024a6027a/attachment.py","path":"scripts/erpclaw-setup/lib/erpclaw_lib/db.py","size":4472,"sha256":"86cc637f217b05e184fdb0f3a77ed2c6f7114b88fbcea3fd0ea08cf64c61c923","contentType":"text/x-python; charset=utf-8"},{"id":"b9dc8a34-94e4-5c87-8b95-81df31ae367c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b9dc8a34-94e4-5c87-8b95-81df31ae367c/attachment.py","path":"scripts/erpclaw-setup/lib/erpclaw_lib/decimal_utils.py","size":3406,"sha256":"410461611c9d8932888ce61bfcf17bd5cbd6849c4bafb90fd03b0e3990c466ee","contentType":"text/x-python; charset=utf-8"},{"id":"3b40cc2a-d431-57df-b896-ff8f28780331","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3b40cc2a-d431-57df-b896-ff8f28780331/attachment.py","path":"scripts/erpclaw-setup/lib/erpclaw_lib/dependencies.py","size":8617,"sha256":"384f45ce3681a409031d126b66719345320cf1f1a448498188fc8929611798e9","contentType":"text/x-python; charset=utf-8"},{"id":"4743a8d0-563b-586c-a0c3-490a73e5c439","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4743a8d0-563b-586c-a0c3-490a73e5c439/attachment.py","path":"scripts/erpclaw-setup/lib/erpclaw_lib/fx_posting.py","size":7228,"sha256":"f83c498a2bf33fa9c621172333aaa632f671c1ad41f9bf2c30dc30687b2ddd02","contentType":"text/x-python; charset=utf-8"},{"id":"4d08d0dc-350a-5c86-b67d-6983e74ed38f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4d08d0dc-350a-5c86-b67d-6983e74ed38f/attachment.py","path":"scripts/erpclaw-setup/lib/erpclaw_lib/gl_posting.py","size":24181,"sha256":"430999f4c69fb58aaf09f70ac487c03a76ae4ab083851c2f1e9b98d51372594c","contentType":"text/x-python; charset=utf-8"},{"id":"f1ba899e-fdc7-5b8c-a242-077675a63e2a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f1ba899e-fdc7-5b8c-a242-077675a63e2a/attachment.py","path":"scripts/erpclaw-setup/lib/erpclaw_lib/naming.py","size":11660,"sha256":"a9c5313f40f1294834b702b89459f5459c21ab3b8716ee7e9b40a76ed87cf0a9","contentType":"text/x-python; charset=utf-8"},{"id":"f73c0b30-c8f4-5a15-aa85-2a427adba79d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f73c0b30-c8f4-5a15-aa85-2a427adba79d/attachment.py","path":"scripts/erpclaw-setup/lib/erpclaw_lib/pagination.py","size":2187,"sha256":"ffdbe600d38dda98bd6d54d785b0da2ee337cf8271751960bbd3290d3900f735","contentType":"text/x-python; charset=utf-8"},{"id":"c128e18d-57cf-5488-933f-b0621ce404e2","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c128e18d-57cf-5488-933f-b0621ce404e2/attachment.py","path":"scripts/erpclaw-setup/lib/erpclaw_lib/passwords.py","size":1201,"sha256":"f49f2959f8c559f2896091f8765a588edb5469619541de6a28a377b77dd3d2f9","contentType":"text/x-python; charset=utf-8"},{"id":"bd268c2d-d6a0-59ba-952f-b13ba1cfc9b9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/bd268c2d-d6a0-59ba-952f-b13ba1cfc9b9/attachment.py","path":"scripts/erpclaw-setup/lib/erpclaw_lib/query.py","size":11811,"sha256":"26c509bec058088202ec36e711f2589bb140472223c4ca0264fc36a184167c38","contentType":"text/x-python; charset=utf-8"},{"id":"36331c92-455f-59d6-8374-7d5ba7218909","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/36331c92-455f-59d6-8374-7d5ba7218909/attachment.py","path":"scripts/erpclaw-setup/lib/erpclaw_lib/query_helpers.py","size":3008,"sha256":"1b06505784251c26516b4bcf08f458dd1d4725bf65bda596b04336c6b675855e","contentType":"text/x-python; charset=utf-8"},{"id":"af232473-eff3-5db7-9fc7-321d15d3aac1","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/af232473-eff3-5db7-9fc7-321d15d3aac1/attachment.py","path":"scripts/erpclaw-setup/lib/erpclaw_lib/rbac.py","size":11492,"sha256":"b7aa7ad7dc1569f4b7a41845ebf45bd4f885b50bb42a7a353de2161d80c6e9b6","contentType":"text/x-python; charset=utf-8"},{"id":"da87d38d-a8a4-5bc6-8322-bdba04b292fb","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/da87d38d-a8a4-5bc6-8322-bdba04b292fb/attachment.py","path":"scripts/erpclaw-setup/lib/erpclaw_lib/response.py","size":887,"sha256":"cc1606c7be12f23bdc38f39685590829733b5c5376f8660e443409f67866433e","contentType":"text/x-python; charset=utf-8"},{"id":"31220dd7-5d97-5009-a361-af53c12e936e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/31220dd7-5d97-5009-a361-af53c12e936e/attachment.py","path":"scripts/erpclaw-setup/lib/erpclaw_lib/stock_posting.py","size":34231,"sha256":"ab4b96c786eb0b5ddb780ec68918e6855b91cdf47fa67f246b7fd445ad14e0a4","contentType":"text/x-python; charset=utf-8"},{"id":"1c515fcf-1a01-560b-bea3-7cb8399702dd","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1c515fcf-1a01-560b-bea3-7cb8399702dd/attachment.py","path":"scripts/erpclaw-setup/lib/erpclaw_lib/tax_calculation.py","size":15296,"sha256":"375b5ef34fb23bad636e6b8ce96578bbac3e6dff464f260e0759989a65a288f0","contentType":"text/x-python; charset=utf-8"},{"id":"ee3cb496-a62f-5b4a-a3e2-323b4ba2257d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ee3cb496-a62f-5b4a-a3e2-323b4ba2257d/attachment.py","path":"scripts/erpclaw-setup/lib/erpclaw_lib/validation.py","size":4808,"sha256":"5b4c9f92820bdb8b3acdf5e604f067da5717223f7be9541f129e8a0337a02660","contentType":"text/x-python; charset=utf-8"},{"id":"9e53bbf0-5389-5a5e-b108-4d4c0cf0746f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9e53bbf0-5389-5a5e-b108-4d4c0cf0746f/attachment.py","path":"scripts/erpclaw-setup/lib/erpclaw_lib/vendor/__init__.py","size":131,"sha256":"5244b6603f40dbf1bf9d2f354d6bec3c1f208bae0d9c8e2f09382931dafc05ac","contentType":"text/x-python; charset=utf-8"},{"id":"0cba79e7-8a53-5d52-a9fd-ce05f695b5ee","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0cba79e7-8a53-5d52-a9fd-ce05f695b5ee/attachment.py","path":"scripts/erpclaw-setup/lib/erpclaw_lib/vendor/pypika/__init__.py","size":1603,"sha256":"4ecb37c9f8898c390f119149364b193cc5b364a115b015bd5a4d771b5ecd6658","contentType":"text/x-python; charset=utf-8"},{"id":"8a7d2364-1360-5cbe-91d2-da8957e32512","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/8a7d2364-1360-5cbe-91d2-da8957e32512/attachment.py","path":"scripts/erpclaw-setup/lib/erpclaw_lib/vendor/pypika/analytics.py","size":3163,"sha256":"dbb578acce5fd64571c8432532e757a0d41c7aadbca90ee610f22a4665b88efb","contentType":"text/x-python; charset=utf-8"},{"id":"d79d8e4e-53df-5563-8481-fa69bfbb3f4e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d79d8e4e-53df-5563-8481-fa69bfbb3f4e/attachment.py","path":"scripts/erpclaw-setup/lib/erpclaw_lib/vendor/pypika/dialects.py","size":36053,"sha256":"782cd89e6875579c80d037006d175a58dbfb0929617d1b1b0b679edaf8279fcf","contentType":"text/x-python; charset=utf-8"},{"id":"eb707273-437d-5b05-83be-e039fd711038","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/eb707273-437d-5b05-83be-e039fd711038/attachment.py","path":"scripts/erpclaw-setup/lib/erpclaw_lib/vendor/pypika/enums.py","size":3162,"sha256":"1c8f94ce6fbab08675e0c88197a3b18a508ca16da6cced3f052e345f225daaff","contentType":"text/x-python; charset=utf-8"},{"id":"271bf5a6-aaaa-5d74-a8c6-23756bf3d1ec","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/271bf5a6-aaaa-5d74-a8c6-23756bf3d1ec/attachment.py","path":"scripts/erpclaw-setup/lib/erpclaw_lib/vendor/pypika/functions.py","size":9710,"sha256":"cdcc0960017f09c6128ef4d33a47f809fdae674450bfb676e993a943398860d8","contentType":"text/x-python; charset=utf-8"},{"id":"90918f07-6dc6-5ced-8159-69796cc60f54","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/90918f07-6dc6-5ced-8159-69796cc60f54/attachment.py","path":"scripts/erpclaw-setup/lib/erpclaw_lib/vendor/pypika/pseudocolumns.py","size":252,"sha256":"91b39555fea73d5ef09e0ff299ac90271b8d90717c31b6039034b68511d2a25d","contentType":"text/x-python; charset=utf-8"},{"id":"89b32dce-5ca2-58bf-b329-f4a66f01e027","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/89b32dce-5ca2-58bf-b329-f4a66f01e027/attachment.py","path":"scripts/erpclaw-setup/lib/erpclaw_lib/vendor/pypika/queries.py","size":76531,"sha256":"fd85eed872cf041e0965ca6e2141bf31d00d4da61bea5a9a18a5e4895b7c65e9","contentType":"text/x-python; charset=utf-8"},{"id":"2efbe48f-825a-53ec-96c9-f489807488c5","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2efbe48f-825a-53ec-96c9-f489807488c5/attachment.py","path":"scripts/erpclaw-setup/lib/erpclaw_lib/vendor/pypika/terms.py","size":61376,"sha256":"94f80937d983aeb8d2b5155ea3e711d0b4a5183c9b6a9155c4a3358f58961123","contentType":"text/x-python; charset=utf-8"},{"id":"89ab3882-6aa7-598c-9638-802296efe54d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/89ab3882-6aa7-598c-9638-802296efe54d/attachment.py","path":"scripts/erpclaw-setup/lib/erpclaw_lib/vendor/pypika/utils.py","size":4487,"sha256":"17351955c5cb611b4685adb1b650eff455ab111fdda14ea1f229149a9851cec8","contentType":"text/x-python; charset=utf-8"},{"id":"360529d1-cb7c-58d5-8833-ce10b2c7de0d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/360529d1-cb7c-58d5-8833-ce10b2c7de0d/attachment.py","path":"scripts/erpclaw-setup/migrations/001_registry_tables.py","size":10183,"sha256":"3290b37faa1daac03990979a3b316a8cf201cab4f14a947a7cb2e3c617ca0602","contentType":"text/x-python; charset=utf-8"},{"id":"7d9bfe0f-d935-5aab-a41c-b879cc7eb3a1","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7d9bfe0f-d935-5aab-a41c-b879cc7eb3a1/attachment.py","path":"scripts/erpclaw-tax/db_query.py","size":41180,"sha256":"e0d8fc062dcb1b842db97ed93e5f1f133db62bb8c7b8436ae038edca56e915b6","contentType":"text/x-python; charset=utf-8"},{"id":"d536fb9e-e3a8-594e-a560-e0fd5cf01b9f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d536fb9e-e3a8-594e-a560-e0fd5cf01b9f/attachment.py","path":"scripts/module_manager.py","size":45765,"sha256":"47c52676947900db59a81cc325a0ee1c69228f9fe93450780e063312f26166a9","contentType":"text/x-python; charset=utf-8"},{"id":"56975516-09d7-559b-a97f-da946f2a1909","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/56975516-09d7-559b-a97f-da946f2a1909/attachment.json","path":"scripts/module_registry.json","size":29831,"sha256":"64962b702abd7f872f8b6be672eb2e23231470b33362745adf7c91201b77a72d","contentType":"application/json; charset=utf-8"},{"id":"656b1433-0050-51e5-a98d-75e861d0fa85","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/656b1433-0050-51e5-a98d-75e861d0fa85/attachment.py","path":"scripts/onboarding.py","size":14877,"sha256":"cb4f9ff4c44f74865b5f18cffa62a37304f38a75ce68b133703045353e8a724f","contentType":"text/x-python; charset=utf-8"}],"bundle_sha256":"b9aa2149d5ab454b8070d26e2d1ca47b763fbd156deaf3a66663cc8035f2013d","attachment_count":98,"text_attachments":98,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"skills/erp-claw/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"security","category_label":"Security"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"security","homepage":"https://github.com/avansaber/erpclaw","metadata":{"openclaw":{"os":["darwin","linux"],"type":"executable","install":{"post":"python3 scripts/erpclaw-setup/db_query.py --action initialize-database"},"requires":{"env":[],"bins":["python3","git"],"optionalEnv":["ERPCLAW_DB_PATH"]}}},"import_tag":"clean-skills-v1","description":"AI-native ERP system with self-extending OS. Full accounting, invoicing, inventory, purchasing, tax, billing, HR, payroll, advanced accounting (ASC 606/842, intercompany, consolidation), and financial reporting. 413 actions across 14 domains, 43 expansion modules. Constitutional guardrails, adversarial audit, schema migration. Double-entry GL, immutable audit trail, US GAAP.\n","user-invocable":true}},"renderedAt":1782980493130}

erpclaw You are a Full-Stack ERP Controller for ERPClaw, an AI-native ERP system. You handle all core business operations: company setup, chart of accounts, journal entries, payments, tax, financial reports, customers, sales orders, invoices, suppliers, purchase orders, inventory, usage-based billing, HR (employees, leave, attendance, expenses), and US payroll (salary structures, FICA, income tax withholding, W-2 generation, garnishments). All data lives in a single local SQLite database with full double-entry accounting and immutable audit trail. Security Model - Local-first : All data in .…