Verify the ERPNext integration live and fix the defects it found (Phase F4)
Ran the push flow against ERPNext 15.121.6 on a plain site and on a site with India Compliance 15.32.0 (podman, scripts/erpnext-e2e). 15 ignored live tests pass on both. Fixes, each covered by a mock-based test: - load_options no longer fails on a plain site: the India Compliance-only gstin field is dropped on a 417 and retried. - Code-less rows with fractional hours or minutes send stock_uom, since ERPNext defaults it to Nos. - A created document whose total differs from Voiced's (ERPNext's default Banker's Rounding on half-paise ties) is recorded as a conflict, kept as a draft, not submitted and not given a PDF. The message names Commercial Rounding as the fix. - An existing customer address is reused instead of creating a duplicate on every first push. A bare creation order-by is not used with a Dynamic Link filter. - A re-adopted document no longer gets a second copy of the PDF. - Reverse charge: India Compliance rejects is_reverse_charge unless tax rows are negative RCM amounts, so the flag is sent as 0 with a warning that the invoice lands as a normal taxed invoice. - The 16-character name rule is checked locally in mirror mode under India Compliance, and a TDS-only payment is refused locally with the reason. Payment Entry mapping verified: bank_account is the Account name (paid_to), paid and received amounts equal the cash received, allocated_amount is cash plus TDS, and TDS is one positive deductions row. The integration user needs the Accounts User and Sales User roles. Not verified: ERPNext v14 and v16, other India Compliance versions, the Tauri commands and UI against a live site, SEZ and overseas customers, UTGST supplier states, TLS sites, e-invoicing.
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
# frappe/erpnext v15 plus the india_compliance app (installed per site, not globally)
|
||||
FROM docker.io/frappe/erpnext:v15.121.6
|
||||
USER frappe
|
||||
RUN bench get-app --skip-assets --branch version-15 https://github.com/resilient-tech/india-compliance.git \
|
||||
&& ls -1 apps > sites/apps.txt || true
|
||||
@@ -0,0 +1,148 @@
|
||||
# ERPNext live end-to-end (podman)
|
||||
|
||||
Runs Voiced's ERPNext integration against real ERPNext v15 sites: one plain site and one with India Compliance.
|
||||
Everything is rootless podman (`podman` and `podman-compose`, never docker). All names are prefixed `voiced-erp`.
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `compose.yml` | Lean stack: MariaDB, one Redis, backend (gunicorn on `127.0.0.1:8088`), one worker. Project name `voiced-erp`. |
|
||||
| `Containerfile.ic` | `frappe/erpnext:v15.121.6` plus the `india_compliance` app files (`version-15` branch). |
|
||||
| `setup.sh` | Builds the image, starts the stack, creates both sites, prepares them, writes `.env.a` and `.env.b`. |
|
||||
| `setup_site.py` | Runs inside the container: company, accounts, address, items, integration user and keys. |
|
||||
| `../../src-tauri/src/integrations/erpnext/live_tests.rs` | The `#[ignore]` Rust tests. |
|
||||
|
||||
## Reproduce
|
||||
|
||||
```bash
|
||||
cd scripts/erpnext-e2e
|
||||
./setup.sh # first run pulls ~2 GB of images and takes about 10 minutes
|
||||
```
|
||||
|
||||
`setup.sh` creates two sites that one backend serves by Host header:
|
||||
|
||||
| Site | URL | Contents | Env file |
|
||||
|---|---|---|---|
|
||||
| `127.0.0.1` | `http://127.0.0.1:8088` | ERPNext only | `.env.a` |
|
||||
| `localhost` | `http://localhost:8088` | ERPNext + India Compliance | `.env.b` |
|
||||
|
||||
Both get the company "Voiced Test Co" (abbreviation `VTC`, INR, India, fiscal year 2026-27), the output tax accounts
|
||||
`Output Tax CGST|SGST|UTGST|IGST - VTC` (India Compliance creates its own with the same names), `TDS Receivable - VTC`,
|
||||
the bank account `HDFC - VTC`, a company address, the UOMs Hour/Minute/Second, the items `VOICED-SERVICE` (stock UOM Nos)
|
||||
and `VOICED-HOURLY` (stock UOM Hour), and an integration user with a fresh API key and secret.
|
||||
`.env.*` files hold those keys; they are git-ignored (`.env.*` in the root `.gitignore`) and must never be committed.
|
||||
|
||||
Run the Rust tests (they only touch the site named by the sourced env file):
|
||||
|
||||
```bash
|
||||
set -a; . scripts/erpnext-e2e/.env.a; set +a # or .env.b for the India Compliance site
|
||||
cargo test --manifest-path src-tauri/Cargo.toml live_ -- --ignored --test-threads=1
|
||||
```
|
||||
|
||||
Environment variables the tests read:
|
||||
|
||||
| Variable | Meaning |
|
||||
|---|---|
|
||||
| `ERPNEXT_URL`, `ERPNEXT_KEY`, `ERPNEXT_SECRET` | Site address and the integration user's API key and secret. |
|
||||
| `ERPNEXT_COMPANY` (`Voiced Test Co`), `ERPNEXT_ABBR` (`VTC`) | Company; account names derive from the abbreviation. |
|
||||
| `ERPNEXT_IC` | `1` on the India Compliance site; the `live_ic_*` tests skip themselves otherwise. |
|
||||
| `ERPNEXT_ROUNDING` | `banker` (ERPNext default) or `commercial`; decides what the half-paise test expects. |
|
||||
| `ERPNEXT_BANK_ACCOUNT` (`HDFC - VTC`) | Account that receives payments. |
|
||||
|
||||
Each test builds its own in-memory Voiced database and a fresh invoice series (`L<time><n>/001`), so reruns never collide
|
||||
with documents from earlier runs. Switch the rounding method of a site with
|
||||
`podman exec -i voiced-erp-backend bash -c "cd /home/frappe/frappe-bench/sites && /home/frappe/frappe-bench/env/bin/python - 127.0.0.1 rounding=commercial" < setup_site.py`
|
||||
(re-run `setup.sh` or copy the printed `ERPNEXT_*` lines if you need the new key; the key is regenerated on every run).
|
||||
|
||||
Tear down (touches only the `voiced-erp` project; the cached images stay):
|
||||
|
||||
```bash
|
||||
podman-compose -p voiced-erp -f compose.yml down -v
|
||||
podman volume rm voiced-erp_voiced-erp-db voiced-erp_voiced-erp-sites voiced-erp_voiced-erp-logs # only if `down -v` left them
|
||||
```
|
||||
|
||||
Notes on rootless podman: the compose file has no healthchecks (podman's healthcheck timers need systemd), and the
|
||||
backend and worker are started by `setup.sh` after the configurator has exited.
|
||||
|
||||
## Integration user (feeds the main README)
|
||||
|
||||
Create a dedicated user in ERPNext (User list, type System User) with **the roles "Accounts User" and "Sales User"**.
|
||||
Then open the user, "Settings" tab, "API Access", "Generate Keys", and copy the secret once.
|
||||
|
||||
Why both (verified live on plain ERPNext and with India Compliance; `setup_site.py` creates exactly this user):
|
||||
|
||||
- "Accounts User": create and submit Sales Invoices (draft, insert-and-submit, submit-later) and Payment Entries; read
|
||||
Accounts, Items, UOMs, Cost Centers, Price Lists, Item Groups, Company, the Sales Invoice naming series and (with
|
||||
India Compliance) GST Settings; attach a private PDF to the invoice.
|
||||
- "Sales User": look up and create Customers and Addresses, read Customer Groups, Territories and the tax templates.
|
||||
With "Accounts User" alone a fresh site answers HTTP 403 on Customer.
|
||||
|
||||
No tax template is used (Voiced always sends explicit tax rows), so the tax-template list is only a convenience.
|
||||
Do not use Administrator or System Manager keys. Roles on a user are cached by Frappe: after changing them run
|
||||
`bench clear-cache` or the next request may still use the old set.
|
||||
|
||||
## Results (what was verified live)
|
||||
|
||||
Versions: Frappe 15.121.3, ERPNext 15.121.6, India Compliance 15.32.0 (the `version-15` branch; commit `db73b3e` at the time of this run, so a later image build may differ), MariaDB 11.8,
|
||||
Redis 8.6, API v2 naming available (Frappe 15.73 or newer).
|
||||
|
||||
Verified on both sites unless marked IC (India Compliance site only):
|
||||
|
||||
- Connection test: user, frappe/erpnext/india_compliance versions, India Compliance detection, no warnings when the
|
||||
accounts match. Option lists load (companies, addresses, accounts, UOMs, cost centers, groups, series).
|
||||
- Sales Invoice push in mirror mode (API v2, `name` = Voiced number): CGST+SGST, IGST, discount (`apply_discount_on:
|
||||
Net Total`), fractional hours, repeated descriptions on code-less rows, item-code rows from a preset, remarks. The
|
||||
grand total, the tax amounts per head and the posting date equal Voiced's; `disable_rounded_total` works.
|
||||
- PDF attachment: uploaded through `upload_file`, `is_private` is 1.
|
||||
- Idempotency: a second push is a no-op; with the local sync row deleted, the 409 path adopts the existing document
|
||||
(no duplicate, no second PDF).
|
||||
- Submit: insert then submit in a second push (v2 `method/submit`), and series mode through the v1 `run_method` route.
|
||||
- Series mode: ERPNext names the document, the Voiced number is in `remarks`, and a lost sync row is recovered by the
|
||||
remarks lookup.
|
||||
- Payments: `get_payment_entry` plus overrides. `bank_account` is the Account name and lands in `paid_to`;
|
||||
`paid_amount = received_amount = cash`; the reference's `allocated_amount = cash + TDS`; TDS is one `deductions` row
|
||||
`{account, cost_center, amount: +TDS}`; the invoice's outstanding falls by cash + TDS; partial payments leave the rest.
|
||||
- Customer and Address shape (IC: `gstin`, `gst_category` on both; state exact name; link to the Customer).
|
||||
- IC: `place_of_supply` as `29-Karnataka`; Voiced's 38 state names equal India Compliance's `gst_state` list; HSN/SAC
|
||||
must be 6 or 8 digits (GST Settings `min_hsn_digits` was 6) and is enforced at submit; the 16-character name rule;
|
||||
GST-account warnings (mismatching CGST account, company without GST accounts, a too-long next number).
|
||||
|
||||
Defects found and fixed (each has a mock-based test in the normal suite):
|
||||
|
||||
1. `load_options`: the company-address list asked for the India Compliance-only `gstin` field and failed with HTTP 417
|
||||
on a plain site. It now retries without it.
|
||||
2. Code-less rows with a fractional quantity of hours/minutes failed ("Quantity cannot be a fraction ... UOM Nos"):
|
||||
ERPNext defaults the row's stock UOM to Nos without an item. Such rows now also send `stock_uom` = the row UOM.
|
||||
3. Rounding: ERPNext's default Rounding Method is Banker's Rounding; Voiced rounds half-paise up. On ties (for example
|
||||
9% of 10.50) the totals differ by 1 paise. A created document whose total differs is now a `conflict`: kept as a
|
||||
draft, never submitted, no PDF, with a message naming the fix (System Settings, Rounding Method, "Commercial
|
||||
Rounding"). The existing-document conflict message carries the same hint. With Commercial Rounding every case matched.
|
||||
4. A lost link to a client's address created a new address on every first push. The customer's matching address (same
|
||||
first line and PIN) is reused. (`creation` in the order-by is ambiguous with a Dynamic Link filter and gives HTTP 500.)
|
||||
5. A re-adopted document got a second copy of the PDF. A file of the same name is now looked up first.
|
||||
6. IC refuses `is_reverse_charge = 1` unless the tax rows are negative amounts on separate "RCM" accounts, which would
|
||||
make ERPNext's total differ from Voiced's. It is sent as 0 and the push warns.
|
||||
7. IC's 16-character rule is now checked before the POST in mirror mode (a readable, local refusal that points to series
|
||||
naming); ERPNext's own message is "Transaction Name must be 16 characters or fewer to meet GST requirements".
|
||||
8. A TDS-only payment (no cash) got ERPNext's bare "Paid Amount is mandatory"; it is refused locally with the reason.
|
||||
9. A fractional quantity on a whole-number UOM is refused by ERPNext with a readable message; Voiced appends what to
|
||||
change (unit mapping). No local pre-check: whether "Nos" is whole-number is a site setting.
|
||||
|
||||
Limits and notes:
|
||||
|
||||
- Rounding setting cannot be read by a restricted user, so it is detected by comparing the totals after creating the
|
||||
draft, not in the connection test.
|
||||
- With India Compliance, an invoice with a missing or short HSN/SAC is created as a draft and fails at submit; the
|
||||
readable ERPNext message ("HSN/SAC must exist and should be 6 or 8 digits long for the following row numbers") is
|
||||
shown.
|
||||
- A mirrored item row with an item code whose stock UOM differs from the row UOM works for whole quantities
|
||||
(`conversion_factor` 1); a fractional quantity needs the item's stock UOM to allow fractions.
|
||||
- One intermittent failure was seen once in the half-paise test on the India Compliance site (the push of a non-tie
|
||||
case failed; three later full runs and a single-test run passed). The cause was not captured.
|
||||
|
||||
Not verified:
|
||||
|
||||
- ERPNext v14 and v16 (the v16 images that exist on some machines were not used), other India Compliance versions.
|
||||
- Windows and WebView2; the Tauri commands and UI (the tests call the Rust push/payment functions directly).
|
||||
- Overseas/SEZ customers, UTGST supplier states, multi-currency, GST Settings with "Round Off GST Values" on,
|
||||
reverse charge booked on RCM accounts, e-invoice/e-waybill (India Compliance API features), a TLS or reverse-proxied
|
||||
site (only plain `http://` on a loopback address), Frappe Cloud or any site with `allow_cors` or rate limits.
|
||||
@@ -0,0 +1,66 @@
|
||||
# Lean ERPNext v15 stack for Voiced's live integration tests (project name: voiced-erp).
|
||||
# db + redis + backend (gunicorn, published on 127.0.0.1:8088) + one worker. No nginx/websocket/scheduler.
|
||||
# setup.sh starts it in order (db, redis, configurator, then backend and worker); do not `up` it blindly.
|
||||
# Image: voiced-erp-ic:v15 = frappe/erpnext v15 + the india_compliance app files (see Containerfile.ic).
|
||||
name: voiced-erp
|
||||
services:
|
||||
db:
|
||||
image: docker.io/library/mariadb:11.8
|
||||
container_name: voiced-erp-db
|
||||
command:
|
||||
- --character-set-server=utf8mb4
|
||||
- --collation-server=utf8mb4_unicode_ci
|
||||
- --skip-character-set-client-handshake
|
||||
- --innodb-buffer-pool-size=256M
|
||||
environment:
|
||||
MARIADB_ROOT_PASSWORD: admin
|
||||
volumes:
|
||||
- voiced-erp-db:/var/lib/mysql
|
||||
|
||||
redis:
|
||||
image: docker.io/library/redis:8.6-alpine
|
||||
container_name: voiced-erp-redis
|
||||
|
||||
configurator:
|
||||
image: localhost/voiced-erp-ic:v15
|
||||
container_name: voiced-erp-configurator
|
||||
restart: "no"
|
||||
entrypoint: ["bash", "-c"]
|
||||
command:
|
||||
- >
|
||||
wait-for-it -t 120 db:3306;
|
||||
ls -1 apps > sites/apps.txt;
|
||||
bench set-config -g db_host db;
|
||||
bench set-config -gp db_port 3306;
|
||||
bench set-config -g redis_cache redis://redis:6379;
|
||||
bench set-config -g redis_queue redis://redis:6379;
|
||||
bench set-config -g redis_socketio redis://redis:6379;
|
||||
bench set-config -gp socketio_port 9000;
|
||||
volumes:
|
||||
- voiced-erp-sites:/home/frappe/frappe-bench/sites
|
||||
- voiced-erp-logs:/home/frappe/frappe-bench/logs
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
|
||||
backend:
|
||||
image: localhost/voiced-erp-ic:v15
|
||||
container_name: voiced-erp-backend
|
||||
ports:
|
||||
- "127.0.0.1:8088:8000"
|
||||
volumes:
|
||||
- voiced-erp-sites:/home/frappe/frappe-bench/sites
|
||||
- voiced-erp-logs:/home/frappe/frappe-bench/logs
|
||||
|
||||
worker:
|
||||
image: localhost/voiced-erp-ic:v15
|
||||
container_name: voiced-erp-worker
|
||||
command: ["bench", "worker", "--queue", "short,default,long"]
|
||||
volumes:
|
||||
- voiced-erp-sites:/home/frappe/frappe-bench/sites
|
||||
- voiced-erp-logs:/home/frappe/frappe-bench/logs
|
||||
|
||||
volumes:
|
||||
voiced-erp-db:
|
||||
voiced-erp-sites:
|
||||
voiced-erp-logs:
|
||||
Executable
+42
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
# Brings up the voiced-erp podman project and prepares two ERPNext v15 sites for the live tests:
|
||||
# 127.0.0.1 plain ERPNext -> .env.a (URL http://127.0.0.1:8088)
|
||||
# localhost ERPNext + India Compliance -> .env.b (URL http://localhost:8088)
|
||||
# Both are served by the same backend; Frappe picks the site from the Host header.
|
||||
# Safe to re-run: existing sites are kept and the integration user's key is regenerated.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
COMPOSE=(podman-compose -p voiced-erp -f compose.yml)
|
||||
IMAGE=localhost/voiced-erp-ic:v15
|
||||
BENCH_SITES=/home/frappe/frappe-bench/sites
|
||||
|
||||
podman image exists "$IMAGE" || podman build -t "$IMAGE" -f Containerfile.ic .
|
||||
|
||||
"${COMPOSE[@]}" up -d db redis configurator
|
||||
until [ "$(podman inspect -f '{{.State.Status}}' voiced-erp-configurator)" = exited ]; do sleep 2; done
|
||||
[ "$(podman inspect -f '{{.State.ExitCode}}' voiced-erp-configurator)" = 0 ] || { echo "configurator failed: podman logs voiced-erp-configurator" >&2; exit 1; }
|
||||
"${COMPOSE[@]}" up -d backend worker
|
||||
|
||||
in_backend() { podman exec -i voiced-erp-backend bash -c "$1"; }
|
||||
|
||||
for site in 127.0.0.1 localhost; do
|
||||
default=""; [ "$site" = 127.0.0.1 ] && default="--set-default"
|
||||
in_backend "test -d $BENCH_SITES/$site || bench new-site $site --mariadb-user-host-login-scope='%' \
|
||||
--admin-password=admin --db-root-username=root --db-root-password=admin --install-app erpnext $default" >/dev/null
|
||||
done
|
||||
|
||||
until curl -fsS http://127.0.0.1:8088/api/method/ping >/dev/null 2>&1; do sleep 2; done
|
||||
|
||||
# setup_site.py prints `ENV KEY=VALUE` lines; turn them into a sourceable env file (untracked: .env.* is ignored).
|
||||
write_env() { # site, url, file, extra args...
|
||||
local site=$1 url=$2 file=$3; shift 3
|
||||
in_backend "cd $BENCH_SITES && /home/frappe/frappe-bench/env/bin/python - $site $*" < setup_site.py \
|
||||
| sed -n 's/^ENV \([A-Z_]*\)=\(.*\)$/export \1="\2"/p' > "$file"
|
||||
echo "export ERPNEXT_URL=\"$url\"" >> "$file"
|
||||
chmod 600 "$file"
|
||||
}
|
||||
write_env 127.0.0.1 http://127.0.0.1:8088 .env.a
|
||||
write_env localhost http://localhost:8088 .env.b ic
|
||||
|
||||
echo "Ready. Source .env.a (plain) or .env.b (India Compliance), then run the live tests (see README.md)."
|
||||
@@ -0,0 +1,197 @@
|
||||
"""Prepare one ERPNext site for Voiced's live integration tests.
|
||||
|
||||
Run inside the backend container (see README.md):
|
||||
podman exec -i voiced-erp-backend python - <site> [ic] [rounding=commercial] < setup_site.py
|
||||
|
||||
Creates (idempotently): the Indian company "Voiced Test Co" (abbr VTC, INR), the fiscal year, GST output tax
|
||||
accounts, a bank account, a company address, an integration user with API keys, and a few Items/UOMs.
|
||||
Prints one line `ENV KEY=VALUE` per value the Rust live tests need. Never run against a real site.
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
|
||||
import frappe
|
||||
|
||||
SITE = sys.argv[1]
|
||||
IC = "ic" in sys.argv[2:]
|
||||
# `rounding=commercial` switches System Settings > Rounding Method (the ERPNext default is Banker's Rounding).
|
||||
ROUNDING = {"commercial": "Commercial Rounding", "banker": "Banker's Rounding"}.get(
|
||||
next((a.split("=", 1)[1] for a in sys.argv[2:] if a.startswith("rounding=")), ""), None
|
||||
)
|
||||
ABBR = "VTC"
|
||||
COMPANY = "Voiced Test Co"
|
||||
|
||||
frappe.init(site=SITE, sites_path="/home/frappe/frappe-bench/sites")
|
||||
frappe.connect()
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
|
||||
def out(key, value):
|
||||
print(f"ENV {key}={value}")
|
||||
|
||||
|
||||
def exists(doctype, name):
|
||||
return bool(frappe.db.exists(doctype, name))
|
||||
|
||||
|
||||
def insert(doc):
|
||||
d = frappe.get_doc(doc)
|
||||
d.insert(ignore_permissions=True)
|
||||
frappe.db.commit()
|
||||
return d
|
||||
|
||||
|
||||
# ---- 1. site, optionally India Compliance (before the wizard so it can add its own accounts) ----
|
||||
if IC and "india_compliance" not in frappe.get_installed_apps():
|
||||
from frappe.installer import install_app
|
||||
|
||||
install_app("india_compliance", verbose=False)
|
||||
frappe.db.commit()
|
||||
|
||||
# ---- 2. setup wizard ----
|
||||
if not frappe.db.get_single_value("System Settings", "setup_complete"):
|
||||
from frappe.desk.page.setup_wizard.setup_wizard import setup_complete
|
||||
|
||||
setup_complete(
|
||||
{
|
||||
"language": "English",
|
||||
"country": "India",
|
||||
"timezone": "Asia/Kolkata",
|
||||
"currency": "INR",
|
||||
"full_name": "Voiced Admin",
|
||||
"email": "admin@voiced.test",
|
||||
"password": "admin",
|
||||
"company_name": COMPANY,
|
||||
"company_abbr": ABBR,
|
||||
"domains": ["Services"],
|
||||
"chart_of_accounts": "Standard",
|
||||
"fy_start_date": "2026-04-01",
|
||||
"fy_end_date": "2027-03-31",
|
||||
"bank_account": "HDFC",
|
||||
"setup_demo": 0,
|
||||
}
|
||||
)
|
||||
# setup_complete may run its stages on a worker; wait for it.
|
||||
for _ in range(120):
|
||||
frappe.db.rollback()
|
||||
if frappe.db.get_single_value("System Settings", "setup_complete") and exists("Company", COMPANY):
|
||||
break
|
||||
time.sleep(2)
|
||||
frappe.clear_cache()
|
||||
frappe.db.commit()
|
||||
assert exists("Company", COMPANY), "setup wizard did not create the company"
|
||||
|
||||
# ---- 3. accounts ----
|
||||
duties = frappe.db.get_value("Account", {"company": COMPANY, "account_name": "Duties and Taxes", "is_group": 1}, "name")
|
||||
assert duties, "Duties and Taxes group not found"
|
||||
for head in ("CGST", "SGST", "UTGST", "IGST"):
|
||||
name = f"Output Tax {head} - {ABBR}"
|
||||
if not exists("Account", name):
|
||||
insert(
|
||||
{
|
||||
"doctype": "Account",
|
||||
"account_name": f"Output Tax {head}",
|
||||
"parent_account": duties,
|
||||
"company": COMPANY,
|
||||
"account_type": "Tax",
|
||||
"root_type": "Liability",
|
||||
"is_group": 0,
|
||||
}
|
||||
)
|
||||
tds = f"TDS Receivable - {ABBR}"
|
||||
if not exists("Account", tds):
|
||||
parent = frappe.db.get_value("Account", {"company": COMPANY, "account_name": "Current Assets", "is_group": 1}, "name")
|
||||
insert(
|
||||
{
|
||||
"doctype": "Account",
|
||||
"account_name": "TDS Receivable",
|
||||
"parent_account": parent,
|
||||
"company": COMPANY,
|
||||
"account_type": "Tax",
|
||||
"root_type": "Asset",
|
||||
"is_group": 0,
|
||||
}
|
||||
)
|
||||
|
||||
# ---- 4. company address (India Compliance needs one with a GSTIN-ish state) ----
|
||||
addr_name = f"{COMPANY}-Billing"
|
||||
if not exists("Address", addr_name):
|
||||
d = {
|
||||
"doctype": "Address",
|
||||
"address_title": COMPANY,
|
||||
"address_type": "Billing",
|
||||
"address_line1": "1 Test Street",
|
||||
"city": "Mumbai",
|
||||
"state": "Maharashtra",
|
||||
"pincode": "400001",
|
||||
"country": "India",
|
||||
"links": [{"link_doctype": "Company", "link_name": COMPANY}],
|
||||
}
|
||||
if IC:
|
||||
d["gst_category"] = "Registered Regular"
|
||||
d["gstin"] = "27AAPFU0939F1ZV"
|
||||
insert(d)
|
||||
|
||||
# ---- 5. items / UOMs ----
|
||||
for uom in ("Hour", "Minute", "Second"):
|
||||
if not exists("UOM", uom):
|
||||
insert({"doctype": "UOM", "uom_name": uom})
|
||||
for code, uom in (("VOICED-SERVICE", "Nos"), ("VOICED-HOURLY", "Hour")):
|
||||
if not exists("Item", code):
|
||||
d = {
|
||||
"doctype": "Item",
|
||||
"item_code": code,
|
||||
"item_name": code,
|
||||
"item_group": "Services",
|
||||
"stock_uom": uom,
|
||||
"is_stock_item": 0,
|
||||
}
|
||||
if IC:
|
||||
d["gst_hsn_code"] = "998314"
|
||||
insert(d)
|
||||
|
||||
# ---- 5b. rounding method ----
|
||||
if ROUNDING:
|
||||
frappe.db.set_single_value("System Settings", "rounding_method", ROUNDING)
|
||||
frappe.db.commit()
|
||||
frappe.clear_cache()
|
||||
|
||||
# ---- 6. integration user ----
|
||||
USER = f"voiced-integration@{SITE.replace('.', '-')}.test"
|
||||
# The role set that was verified live (with and without India Compliance): "Accounts User" creates and submits Sales
|
||||
# Invoices and Payment Entries and reads the accounting masters; "Sales User" adds the Customer and Address rights
|
||||
# (look up, create) and the tax-template list. Accounts User alone gets HTTP 403 on Customer.
|
||||
ROLES = ["Accounts User", "Sales User"]
|
||||
if not exists("User", USER):
|
||||
insert(
|
||||
{
|
||||
"doctype": "User",
|
||||
"email": USER,
|
||||
"first_name": "Voiced",
|
||||
"last_name": "Integration",
|
||||
"send_welcome_email": 0,
|
||||
"user_type": "System User",
|
||||
"roles": [{"role": r} for r in ROLES],
|
||||
}
|
||||
)
|
||||
user = frappe.get_doc("User", USER)
|
||||
user.set("roles", [{"role": r} for r in ROLES])
|
||||
secret = frappe.generate_hash(length=32)
|
||||
if not user.api_key:
|
||||
user.api_key = frappe.generate_hash(length=15)
|
||||
user.api_secret = secret
|
||||
user.save(ignore_permissions=True)
|
||||
frappe.db.commit()
|
||||
frappe.clear_cache(user=USER)
|
||||
frappe.clear_cache()
|
||||
|
||||
out("ERPNEXT_KEY", user.api_key)
|
||||
out("ERPNEXT_SECRET", secret)
|
||||
out("ERPNEXT_COMPANY", COMPANY)
|
||||
out("ERPNEXT_ABBR", ABBR)
|
||||
out("ERPNEXT_IC", "1" if IC else "0")
|
||||
out("ERPNEXT_USER", USER)
|
||||
out("ERPNEXT_ROUNDING", "commercial" if frappe.db.get_single_value("System Settings", "rounding_method") == "Commercial Rounding" else "banker")
|
||||
print(json.dumps({"roles": ROLES}))
|
||||
frappe.destroy()
|
||||
Reference in New Issue
Block a user