Skill: gaelbuenobarthe/shopify-auto-invoicing-inventory (version 1.0.0)

--- name: shopify-auto-invoicing-inventory description: Lightweight Shopify order invoicing and inventory operations workflow for detecting new orders, preparing invoice-ready billing data, updating stock tables, and producing simple monthly summaries. Use when building a review-first Shopify back-office process, preparing invoice exports from new orders, reconciling stock after sales, or generating easy e-commerce operations reports for a free ClawHub edition. --- # Shopify Auto Invoicing & Inventory Run a simple, review-first Shopify operations workflow focused on order detection, invoice preparation, stock updates, and clear monthly reporting. Keep outputs professional, merchant-friendly, and easy to validate before any external action. ## Workflow Use this sequence for complete requests: 1. collect store context 2. detect new or relevant orders 3. prepare invoice-ready billing data 4. update stock tables 5. summarize exceptions 6. export a monthly operations report ## 1. Collect store context Capture the operating brief before producing outputs. Minimum inputs: - store name - currency - tax regime or VAT note - invoice numbering rule if provided - order date range or event trigger - product SKU source - current stock source - desired report month If details are missing, keep assumptions explicit and conservative. ## 2. Detect new orders Use Shopify order exports, webhook payloads, user-provided CSV files, or manually reviewed order lists. Capture these fields whenever possible: - order_id - order_name - created_at - financial_status - fulfillment_status - customer_name - customer_email - billing country - shipping country - currency - line items - SKU - quantity - subtotal - tax - shipping - total price Classify orders into these simple buckets: - ready_for_invoice - paid_but_review_needed - pending_payment - cancelled_or_ignore Rules: - invoice only orders that are paid or explicitly approved - never invent tax data - flag missing customer billing fields before invoice generation ## 3. Prepare invoice-ready billing data Generate structured invoice rows rather than pretending to issue official accounting documents automatically. For each invoice-ready order, prepare: - invoice_number or placeholder - invoice_date - merchant legal name placeholder - customer billing name - customer billing address - tax identifier if available - line description - quantity - unit price - tax amount - total amount - payment status - source order reference Use `references/templates.md` for invoice field structure and merchant-facing language. If the user asks for a PDF, produce a professional invoice content block or a PDF-ready dataset unless a real PDF renderer is already available. ## 4. Update stock tables After processing valid orders, create a stock movement view. Recommended columns: - sku - product_title - variant_title - opening_stock - quantity_sold - quantity_refunded - net_change - closing_stock - low_stock_flag - reorder_note Rules: - subtract sold quantities from stock - add refunded quantities back only when explicitly confirmed - flag negative resulting stock as `needs_review` - flag low stock using the user threshold, otherwise default to 5 Use `scripts/stock_sync.py` to compute a flat stock reconciliation file from inventory and order exports. ## 5. Summarize exceptions Always produce a short exception section. Include: - orders missing billing data - unpaid orders excluded from invoicing - orders with missing SKU data - products with negative or low stock - tax or currency mismatches Keep this section concise and actionable. ## 6. Export monthly report When the user asks for a monthly summary, include: - total orders processed - paid orders - pending-payment orders - cancelled orders - invoice-ready orders - invoiced revenue - tax total - units sold - top SKUs by quantity - low-stock SKU count Use `scripts/monthly_ops_report.py` to aggregate order and stock data into a compact CSV summary. ## Output order For a complete request, produce outputs in this order: 1. operating assumptions 2. order status summary 3. invoice-ready table 4. stock reconciliation table 5. exceptions list 6. monthly report summary ## Ease-of-use standard Write for non-technical Shopify merchants. Prefer: - plain labels - compact tables or CSV-ready rows - obvious status names - clean summaries with next actions Avoid: - jargon-heavy accounting language unless requested - pretending an official invoice was sent when only a draft was prepared - risky automation claims without source data ## Compliance standard Operate in a review-first and accounting-aware way. Use this skill to support: - order review - invoice preparation - stock reconciliation - monthly reporting Do not claim legal or tax compliance beyond the data provided. Recommend accountant review when tax treatment is uncertain. ## Community edition note This free edition focuses on invoice-ready data prep, stock reconciliation, and simple monthly reporting. It intentionally excludes automated payment reminders, advanced PDF workflows, and richer finance automation. ## Resources Use bundled resources when useful: - `references/templates.md` for invoice and report templates - `scripts/invoice_export.py` to convert Shopify-style order JSON into invoice-ready CSV rows - `scripts/stock_sync.py` to reconcile stock from orders and inventory CSV files - `scripts/monthly_ops_report.py` to build a simple monthly operations summary

path: scripts/invoice_export.py
content: #!/usr/bin/env python3
import csv
import json
import sys
from pathlib import Path

FIELDS = [
    "invoice_number",
    "invoice_date",
    "order_id",
    "order_name",
    "customer_name",
    "customer_email",
    "billing_name",
    "billing_address",
    "currency",
    "subtotal",
    "shipping",
    "tax",
    "total",
    "payment_status",
    "source_order_reference",
]


def main() -> int:
    if len(sys.argv) != 3:
        print("Usage: invoice_export.py input.json output.csv")
        return 1

    input_path = Path(sys.argv[1])
    output_path = Path(sys.argv[2])

    if not input_path.exists():
        print(f"Input file not found: {input_path}")
        return 1

    try:
        data = json.loads(input_path.read_text(encoding="utf-8-sig"))
    except json.JSONDecodeError as exc:
        print(f"Invalid JSON in {input_path}: {exc}")
        return 1

    if not isinstance(data, list):
        print("Input JSON must be a list of orders")
        return 1

    output_path.parent.mkdir(parents=True, exist_ok=True)
    with output_path.open("w", encoding="utf-8", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=FIELDS)
        writer.writeheader()
        for idx, row in enumerate(data, start=1):
            if not isinstance(row, dict):
                print("Each order entry must be a JSON object")
                return 1
            financial_status = str(row.get("financial_status", "")).strip().lower()
            if financial_status not in {"paid", "authorized", "partially_paid"}:
                continue
            billing_name = row.get("billing_name") or row.get("customer_name", "")
            out = {
                "invoice_number": row.get("invoice_number", f"DRAFT-{idx:04d}"),
                "invoice_date": row.get("invoice_date") or str(row.get("created_at", ""))[:10],
                "order_id": row.get("order_id", ""),
                "order_name": row.get("order_name", ""),
                "customer_name": row.get("customer_name", ""),
                "customer_email": row.get("customer_email", ""),
                "billing_name": billing_name,
                "billing_address": row.get("billing_address", ""),
                "currency": row.get("currency", ""),
                "subtotal": row.get("subtotal", 0),
                "shipping": row.get("shipping", 0),
                "tax": row.get("tax", 0),
                "total": row.get("total", 0),
                "payment_status": financial_status,
                "source_order_reference": row.get("order_name") or row.get("order_id", ""),
            }
            writer.writerow(out)

    print(f"Prepared invoice-ready CSV: {output_path}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())


path: scripts/monthly_ops_report.py
content: #!/usr/bin/env python3
import csv
import sys
from collections import Counter
from pathlib import Path


def to_float(value):
    try:
        return float(value or 0)
    except (TypeError, ValueError):
        return 0.0


def main() -> int:
    if len(sys.argv) != 4:
        print("Usage: monthly_ops_report.py orders.csv stock.csv output.csv")
        return 1

    orders_path = Path(sys.argv[1])
    stock_path = Path(sys.argv[2])
    output_path = Path(sys.argv[3])

    if not orders_path.exists() or not stock_path.exists():
        print("Input file not found")
        return 1

    with orders_path.open("r", encoding="utf-8-sig", newline="") as f:
        orders = list(csv.DictReader(f))
    with stock_path.open("r", encoding="utf-8-sig", newline="") as f:
        stock = list(csv.DictReader(f))

    paid = 0
    pending = 0
    cancelled = 0
    revenue = 0.0
    tax_total = 0.0
    units = 0
    sku_counter = Counter()

    for row in orders:
        status = str(row.get("financial_status", "")).strip().lower()
        if status in {"paid", "authorized", "partially_paid"}:
            paid += 1
        elif "pending" in status:
            pending += 1
        elif "cancel" in status or "void" in status:
            cancelled += 1
        revenue += to_float(row.get("total") or row.get("total_price"))
        tax_total += to_float(row.get("tax") or row.get("tax_total"))
        try:
            units += int(float(row.get("quantity") or 0))
        except (TypeError, ValueError):
            pass
        sku = (row.get("sku") or "").strip()
        if sku:
            sku_counter[sku] += int(float(row.get("quantity") or 0)) if str(row.get("quantity") or "").strip() else 0

    low_stock_count = sum(1 for row in stock if str(row.get("low_stock_flag", "")).strip().lower() == "yes")
    top_skus = "; ".join(f"{sku}:{qty}" for sku, qty in sku_counter.most_common(5))

    summary = [{
        "total_orders_processed": len(orders),
        "paid_orders": paid,
        "pending_payment_orders": pending,
        "cancelled_orders": cancelled,
        "invoice_ready_orders": paid,
        "invoiced_revenue": round(revenue, 2),
        "tax_total": round(tax_total, 2),
        "units_sold": units,
        "top_skus_by_quantity": top_skus,
        "low_stock_sku_count": low_stock_count,
    }]

    output_path.parent.mkdir(parents=True, exist_ok=True)
    with output_path.open("w", encoding="utf-8", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=list(summary[0].keys()))
        writer.writeheader()
        writer.writerows(summary)

    print(f"Wrote monthly ops report: {output_path}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())


path: scripts/stock_sync.py
content: #!/usr/bin/env python3
import csv
import sys
from pathlib import Path

OUT_FIELDS = [
    "sku",
    "product_title",
    "variant_title",
    "opening_stock",
    "quantity_sold",
    "quantity_refunded",
    "net_change",
    "closing_stock",
    "low_stock_flag",
    "reorder_note",
]


def to_int(value):
    try:
        return int(float(value or 0))
    except (TypeError, ValueError):
        return 0


def main() -> int:
    if len(sys.argv) not in {4, 5}:
        print("Usage: stock_sync.py inventory.csv orders.csv output.csv [low_stock_threshold]")
        return 1

    inventory_path = Path(sys.argv[1])
    orders_path = Path(sys.argv[2])
    output_path = Path(sys.argv[3])
    threshold = to_int(sys.argv[4]) if len(sys.argv) == 5 else 5

    if not inventory_path.exists() or not orders_path.exists():
        print("Input file not found")
        return 1

    with inventory_path.open("r", encoding="utf-8-sig", newline="") as f:
        inventory_rows = list(csv.DictReader(f))
    with orders_path.open("r", encoding="utf-8-sig", newline="") as f:
        order_rows = list(csv.DictReader(f))

    by_sku = {}
    for row in inventory_rows:
        sku = (row.get("sku") or row.get("SKU") or "").strip()
        if not sku:
            continue
        by_sku[sku] = {
            "sku": sku,
            "product_title": row.get("product_title") or row.get("Product") or "",
            "variant_title": row.get("variant_title") or row.get("Variant") or "",
            "opening_stock": to_int(row.get("opening_stock") or row.get("stock") or row.get("available")),
            "quantity_sold": 0,
            "quantity_refunded": 0,
        }

    for row in order_rows:
        sku = (row.get("sku") or row.get("SKU") or "").strip()
        if not sku:
            continue
        entry = by_sku.setdefault(sku, {
            "sku": sku,
            "product_title": row.get("product_title", ""),
            "variant_title": row.get("variant_title", ""),
            "opening_stock": 0,
            "quantity_sold": 0,
            "quantity_refunded": 0,
        })
        entry["quantity_sold"] += to_int(row.get("quantity") or row.get("quantity_sold"))
        entry["quantity_refunded"] += to_int(row.get("quantity_refunded"))

    output_path.parent.mkdir(parents=True, exist_ok=True)
    with output_path.open("w", encoding="utf-8", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=OUT_FIELDS)
        writer.writeheader()
        for sku in sorted(by_sku):
            row = by_sku[sku]
            net_change = row["quantity_refunded"] - row["quantity_sold"]
            closing_stock = row["opening_stock"] + net_change
            writer.writerow({
                "sku": sku,
                "product_title": row["product_title"],
                "variant_title": row["variant_title"],
                "opening_stock": row["opening_stock"],
                "quantity_sold": row["quantity_sold"],
                "quantity_refunded": row["quantity_refunded"],
                "net_change": net_change,
                "closing_stock": closing_stock,
                "low_stock_flag": "yes" if closing_stock <= threshold else "no",
                "reorder_note": "needs_review" if closing_stock < 0 else ("reorder_soon" if closing_stock <= threshold else "ok"),
            })

    print(f"Wrote stock reconciliation CSV: {output_path}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())


path: references/templates.md
content: # Templates

## Invoice-ready order object

```json
{
  "order_id": "",
  "order_name": "",
  "created_at": "",
  "financial_status": "paid",
  "customer_name": "",
  "customer_email": "",
  "billing_name": "",
  "billing_address": "",
  "currency": "EUR",
  "line_items": [
    {
      "sku": "",
      "title": "",
      "quantity": 1,
      "unit_price": 0,
      "tax_amount": 0,
      "line_total": 0
    }
  ],
  "subtotal": 0,
  "shipping": 0,
  "tax": 0,
  "total": 0
}
```

## Invoice section template

- Invoice number:
- Invoice date:
- Order reference:
- Merchant:
- Customer:
- Billing address:
- Currency:
- Line items:
- Subtotal:
- Tax:
- Shipping:
- Grand total:
- Payment status:

## Monthly report template

- Period:
- Orders processed:
- Paid orders:
- Pending-payment orders:
- Cancelled orders:
- Invoice-ready orders:
- Invoiced revenue:
- Tax collected:
- Units sold:
- Low-stock SKUs:
- Exceptions:

## Merchant-facing language

### Invoice note

Thank you for your order. This invoice summarizes the products billed for the referenced Shopify order.

### Stock alert note

This product is approaching the low-stock threshold and should be reviewed for replenishment.

### Exception note

This order requires review before invoicing because one or more billing or inventory fields are incomplete.


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)