Merge remote-tracking branch 'frappe/develop' into mariadb-ci-fanout

# Conflicts:
#	.github/helper/hydrate.sh
#	.github/helper/install.sh
#	.github/helper/start-db.sh
This commit is contained in:
Mihir Kandoi
2026-06-25 13:31:28 +05:30
97 changed files with 1208 additions and 363 deletions

View File

@@ -65,6 +65,19 @@ jobs:
- name: Add to Hosts
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
# The v14 baseline backup is a fixed published file — cache it instead of re-downloading
# ~100MB from frappe.io every run.
- name: Cache erpnext v14 backup
id: cache-v14
uses: actions/cache@v4
with:
path: ~/erpnext-v14.sql.gz
key: erpnext-v14-sql-gz
- name: Download erpnext v14 backup
if: steps.cache-v14.outputs.cache-hit != 'true'
run: wget -O ~/erpnext-v14.sql.gz https://frappe.io/files/erpnext-v14.sql.gz
- name: Cache pip
uses: actions/cache@v4
with:
@@ -113,8 +126,7 @@ jobs:
jq 'del(.install_apps)' ~/frappe-bench/sites/test_site/site_config.json > tmp.json
mv tmp.json ~/frappe-bench/sites/test_site/site_config.json
wget https://frappe.io/files/erpnext-v14.sql.gz
bench --site test_site --force restore ~/frappe-bench/erpnext-v14.sql.gz
bench --site test_site --force restore ~/erpnext-v14.sql.gz
git -C "apps/frappe" remote set-url upstream https://github.com/frappe/frappe.git
git -C "apps/erpnext" remote set-url upstream https://github.com/frappe/erpnext.git

View File

@@ -1,79 +1,45 @@
name: Server (Postgres)
on:
repository_dispatch:
types: [frappe-framework-change]
schedule:
# 03:00 AM IST daily (21:30 UTC the previous day)
- cron: "30 21 * * *"
pull_request:
# 'labeled' is required so adding the 'postgres' label to an open PR triggers this run
# (the job itself is gated on that label below)
# 'labeled' so adding the 'postgres' label to an already-open PR re-triggers the run.
types: [opened, reopened, synchronize, labeled]
paths-ignore:
- '**.js'
- '**.css'
- '**.svg'
- '**.md'
- '**.html'
- 'crowdin.yml'
- '.coderabbit.yml'
- '.mergify.yml'
schedule:
# Run everday at midnight UTC / 5:30 IST
- cron: "0 0 * * *"
workflow_dispatch:
inputs:
user:
description: 'Frappe Framework repository user (add your username for forks)'
required: true
default: 'frappe'
type: string
branch:
description: 'Frappe Framework branch'
default: 'develop'
required: false
type: string
permissions:
contents: read
concurrency:
group: server-postgres-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }}
cancel-in-progress: true
permissions:
contents: read
# Postgres CI stays on GitHub-hosted (free, full-speed VM per shard) but follows the same fan-out
# we built for MariaDB: build the bench + reinstall ONCE in the setup job, bake the PostgreSQL
# PGDATA into the artifact, and have 4 test shards start Postgres on that datadir — no per-shard
# clone/build/reinstall/restore. Python is pinned so the venv transplants between VMs.
env:
TZ: 'Asia/Kolkata'
NODE_ENV: "production"
PYTHON_VERSION: '3.14'
jobs:
test:
# Opt-in on PRs: only runs when the PR carries the 'postgres' label. Scheduled / manual /
# framework-dispatch runs always execute (no PR labels to gate on).
if: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'postgres') }}
setup:
name: Build & reinstall (setup)
runs-on: ubuntu-latest
timeout-minutes: 60
env:
TZ: 'Asia/Kolkata'
NODE_ENV: "production"
WITH_COVERAGE: ${{ github.event_name != 'pull_request' }}
strategy:
fail-fast: false
matrix:
container: [1, 2, 3, 4]
# Distinct from the MariaDB job's "Python Unit Tests" so its check contexts do NOT collide with
# the required "Python Unit Tests (1..4)" status checks -- this keeps Postgres non-required for now.
name: Postgres Unit Tests
services:
postgres:
image: postgres:13.3
env:
POSTGRES_PASSWORD: travis
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
# Runs on the daily schedule (and workflow_dispatch). On PRs it runs ONLY when the PR carries
# the 'postgres' label — the test job needs setup, so it's skipped too when this is.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'postgres')
timeout-minutes: 40
steps:
- name: Clone
uses: actions/checkout@v6
@@ -81,7 +47,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: '3.14'
python-version: ${{ env.PYTHON_VERSION }}
- name: Check for valid Python & Merge Conflicts
run: |
@@ -100,98 +66,124 @@ jobs:
- name: Add to Hosts
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
- name: Cache pip
- name: Cache deps (uv/pip/npm/yarn)
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-pip-
${{ runner.os }}-
path: |
~/.cache/uv
~/.cache/pip
~/.npm
~/.cache/yarn
key: ${{ runner.os }}-deps-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/yarn.lock') }}
restore-keys: ${{ runner.os }}-deps-
- name: Cache node modules
# Warm-bench cache (the big one): install.sh saves the built base bench — frappe + env +
# node_modules + assets — here as frappe-bench-base-*.tar.zst. Later runs restore it and only
# fast-forward to the live develop SHA + rebuild the delta, so the bench BUILD is near-free and
# only the test_site reinstall (per-run DB, uncacheable) stays slow — matching the self-hosted
# box. The first run after a deps change populates it; every run after that is fast.
- name: Cache warm bench (base build)
uses: actions/cache@v4
with:
path: ~/bench-cache
key: ${{ runner.os }}-warmbench-v2-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/yarn.lock') }}
restore-keys: ${{ runner.os }}-warmbench-v2-
# Postgres runs in-runner on a PGDATA OUTSIDE the bench (install.sh wipes ~/frappe-bench);
# after the reinstall it's moved into the bench so it ships in the artifact.
- name: Start DB
run: bash ${GITHUB_WORKSPACE}/.github/helper/start-db.sh
env:
cache-name: cache-node-modules
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Cache wkhtmltopdf
uses: actions/cache@v4
with:
path: /tmp/wkhtmltox.deb
key: wkhtmltox-0.12.6.1-2-jammy-amd64
DB: postgres
CI_DB_DATADIR: /home/runner/pgdata
- name: Install
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env:
DB: postgres
TYPE: server
FRAPPE_USER: ${{ github.event.inputs.user }}
FRAPPE_BRANCH: ${{ github.event.client_payload.sha || github.event.inputs.branch }}
FRAPPE_BRANCH: develop
BENCH_CACHE_DIR: /home/runner/bench-cache
- name: Stop DB and stage datadir
run: |
PG_BIN=$(ls -d /usr/lib/postgresql/*/bin | sort -V | tail -1)
"$PG_BIN/pg_ctl" -D /home/runner/pgdata -m fast -w stop || true
mv /home/runner/pgdata /home/runner/frappe-bench/pgdata
- name: Package bench for test shards
run: |
cp "${GITHUB_WORKSPACE}/.github/helper/hydrate.sh" /home/runner/frappe-bench/hydrate.sh
cp "${GITHUB_WORKSPACE}/.github/helper/start-db.sh" /home/runner/frappe-bench/start-db.sh
tar czpf "${GITHUB_WORKSPACE}/bench.tar.gz" -C /home/runner \
--exclude='.git' --exclude='node_modules' frappe-bench
ls -lh "${GITHUB_WORKSPACE}/bench.tar.gz"
- name: Upload bench artifact
uses: actions/upload-artifact@v4
with:
name: bench-pg
path: bench.tar.gz
retention-days: 1
compression-level: 0
test:
name: Python Unit Tests (PG)
needs: setup
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
container: [1, 2, 3, 4]
steps:
- name: Download bench artifact
uses: actions/download-artifact@v4
with:
name: bench-pg
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Add to Hosts
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
# The bench CLI (frappe-bench) and redis are global/system tools — not in the bench tarball.
# The setup runner got them via install.sh; the MariaDB shards get them from the arc5 image.
# GitHub-hosted PG shards install them here (cheap vs the build+reinstall that setup did once).
- name: Install shard runtime (bench CLI + redis + wkhtmltopdf)
run: |
pip install frappe-bench
command -v redis-server >/dev/null || { sudo apt-get update -qq && sudo apt-get install -y -qq redis-server; }
# wkhtmltopdf (patched-qt build) for print-format / PDF tests — same .deb install.sh uses.
if ! command -v wkhtmltopdf >/dev/null; then
wget -qO /tmp/wkhtmltox.deb https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb
sudo apt-get install -y -qq /tmp/wkhtmltox.deb
fi
- name: Untar bench
run: |
tar xzpf "${GITHUB_WORKSPACE}/bench.tar.gz" -C /home/runner
ls -ld /home/runner/frappe-bench
- name: Hydrate (start Postgres on the baked datadir)
run: bash /home/runner/frappe-bench/hydrate.sh
env:
DB: postgres
DB_HOST: 127.0.0.1
- name: Run Tests
run: |
cd ~/frappe-bench/
coverage_flag=""
if [ "$WITH_COVERAGE" = "true" ]; then coverage_flag="--with-coverage"; fi
# print-format / PDF tests are engine-independent (they exercise wkhtmltopdf rendering,
# not postgres SQL — the MariaDB CI already covers them). They only fetch the static asset
# bundles from http://test_site:8000/assets/..., so a plain static file server over sites/
# satisfies wkhtmltopdf without the frappe web server (which never bound on a bare runner).
( cd ~/frappe-bench/sites && nohup python3 -m http.server 8000 --bind 127.0.0.1 > ~/frappe-bench/web.log 2>&1 & )
for _ in $(seq 1 15); do (exec 3<>/dev/tcp/127.0.0.1/8000) 2>/dev/null && { exec 3>&- 3<&-; break; }; sleep 1; done
bench --site test_site run-parallel-tests --lightmode --app erpnext \
--total-builds ${{ strategy.job-total }} \
--build-number ${{ matrix.container }} \
$coverage_flag
--total-builds ${{ strategy.job-total }} --build-number ${{ matrix.container }}
env:
TYPE: server
- name: Show bench output
if: ${{ always() }}
run: cat ~/frappe-bench/bench_start.log || true
- name: Upload coverage data
if: ${{ env.WITH_COVERAGE == 'true' }}
uses: actions/upload-artifact@v4
with:
name: coverage-postgres-${{ matrix.container }}
path: /home/runner/frappe-bench/sites/coverage.xml
coverage:
name: Coverage Wrap Up
needs: test
if: ${{ github.event_name != 'pull_request' }}
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v6
- name: Download artifacts
uses: actions/download-artifact@v4
with:
pattern: coverage-postgres-*
- name: Upload coverage data
uses: codecov/codecov-action@v4
with:
name: Postgres
flags: postgres
# explicit glob: download-artifact extracts each shard into its own coverage-postgres-N/ dir
files: coverage-postgres-*/coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
verbose: true

View File

@@ -3,8 +3,6 @@ import inspect
from typing import TypeVar
import frappe
from frappe.model.document import Document
from frappe.utils.user import is_website_user
__version__ = "17.0.0-dev"
@@ -155,6 +153,8 @@ def allow_regional(fn):
def check_app_permission():
from frappe.utils.user import is_website_user
if frappe.session.user == "Administrator":
return True
@@ -175,6 +175,8 @@ def normalize_ctx_input(T: type) -> callable:
- Casting the result to the specified type T
"""
from frappe.model.document import Document
def decorator(func: callable):
# conserve annotations for frappe.utils.typing_validations
@functools.wraps(func, assigned=(a for a in functools.WRAPPER_ASSIGNMENTS if a != "__annotations__"))

View File

@@ -137,7 +137,7 @@ def get_charts_for_country(country: str, with_standard: bool = False):
def _get_chart_name(content):
if content:
content = json.loads(content)
content = frappe.parse_json(content)
if (
content and content.get("disabled", "No") == "No"
) or frappe.local.flags.allow_unverified_charts:

View File

@@ -224,7 +224,7 @@ def disable_dimension(doc: str):
def toggle_disabling(doc):
doc = json.loads(doc)
doc = frappe.parse_json(doc)
if doc.get("disabled"):
df = {"read_only": 1}

View File

@@ -87,6 +87,7 @@
"period_closing_settings_section",
"ignore_account_closing_balance",
"use_legacy_controller_for_pcv",
"pcv_job_timeout",
"column_break_25",
"reports_tab",
"remarks_section",
@@ -612,6 +613,14 @@
"fieldtype": "Check",
"label": "Use legacy controller for Period Closing Voucher"
},
{
"default": "3600",
"depends_on": "eval: !doc.use_legacy_controller_for_pcv",
"description": "Timeout (in seconds) for each background job enqueued by Process Period Closing Voucher",
"fieldname": "pcv_job_timeout",
"fieldtype": "Int",
"label": "PCV Job Timeout (seconds)"
},
{
"description": "Users with this role will be notified if the asset depreciation gets failed",
"fieldname": "role_to_notify_on_depreciation_failure",
@@ -756,7 +765,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-06-03 13:11:54.721495",
"modified": "2026-06-24 12:59:41.868865",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -90,6 +90,7 @@ class AccountsSettings(Document):
make_payment_via_journal_entry: DF.Check
merge_similar_account_heads: DF.Check
over_billing_allowance: DF.Currency
pcv_job_timeout: DF.Int
preview_mode: DF.Check
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor"]
receivable_payable_remarks_length: DF.Int

View File

@@ -1058,9 +1058,9 @@ def get_auto_reconcile_message(partially_reconciled, reconciled):
@frappe.whitelist()
def reconcile_vouchers(bank_transaction_name: str | int, vouchers: str, is_new_voucher: bool = False):
def reconcile_vouchers(bank_transaction_name: str | int, vouchers: str | list, is_new_voucher: bool = False):
# updated clear date of all the vouchers based on the bank transaction
vouchers = json.loads(vouchers)
vouchers = frappe.parse_json(vouchers)
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
transaction.add_payment_entries(vouchers, is_new_voucher)
transaction.validate_duplicate_references()

View File

@@ -290,7 +290,7 @@ def update_mapping_db(bank, template_options):
for d in bank.bank_transaction_mapping:
d.delete()
for d in json.loads(template_options)["column_to_field_map"].items():
for d in frappe.parse_json(template_options)["column_to_field_map"].items():
bank.append("bank_transaction_mapping", {"bank_transaction_field": d[1], "file_field": d[0]})
bank.save()

View File

@@ -1183,8 +1183,7 @@ def update_pdf_tables(statement_import_id: str, tables: list | str):
if doc.status == "Completed":
frappe.throw(_("This statement has already been imported."), title=_("Already Imported"))
if isinstance(tables, str):
tables = json.loads(tables)
tables = frappe.parse_json(tables)
doc.apply_pdf_tables(tables)
@@ -1204,8 +1203,7 @@ def reextract_pdf_table(statement_import_id: str, page: int, table_index: int, b
if doc.status == "Completed":
frappe.throw(_("This statement has already been imported."), title=_("Already Imported"))
if isinstance(bbox, str):
bbox = json.loads(bbox)
bbox = frappe.parse_json(bbox)
page = int(page)
table_index = int(table_index)
@@ -1290,8 +1288,7 @@ def update_column_mapping(statement_import_id: str, column_mapping: list | str):
if doc.status == "Completed":
frappe.throw(_("This statement has already been imported."), title=_("Already Imported"))
if isinstance(column_mapping, str):
column_mapping = json.loads(column_mapping)
column_mapping = frappe.parse_json(column_mapping)
doc.apply_column_mapping(column_mapping)
doc.save()

View File

@@ -35,12 +35,12 @@ def upload_bank_statement():
@frappe.whitelist()
def create_bank_entries(columns: str, data: str, bank_account: str):
def create_bank_entries(columns: str, data: str | list, bank_account: str):
header_map = get_header_mapping(columns, bank_account)
success = 0
errors = 0
for d in json.loads(data):
for d in frappe.parse_json(data):
if all(item is None for item in d) is True:
continue
fields = {}
@@ -66,7 +66,7 @@ def get_header_mapping(columns, bank_account):
mapping = get_bank_mapping(bank_account)
header_map = {}
for column in json.loads(columns):
for column in frappe.parse_json(columns):
if column["content"] in mapping:
header_map.update({mapping[column["content"]]: column["colIndex"]})

View File

@@ -248,8 +248,7 @@ def get_dunning_letter_text(dunning_type: str, doc: str | dict, language: str |
DOCTYPE = "Dunning Letter Text"
FIELDS = ["body_text", "closing_text", "language"]
if isinstance(doc, str):
doc = json.loads(doc)
doc = frappe.parse_json(doc)
if not language:
language = doc.get("language")

View File

@@ -1032,8 +1032,7 @@ class FormulaFieldUpdater:
def get_filtered_accounts(company: str, account_rows: str | list):
frappe.has_permission("Financial Report Template", ptype="read", throw=True)
if isinstance(account_rows, str):
account_rows = json.loads(account_rows, object_hook=frappe._dict)
account_rows = [frappe._dict(row) for row in frappe.parse_json(account_rows)]
return DataCollector.get_filtered_accounts(company, account_rows)

View File

@@ -317,8 +317,8 @@ class InvoiceDiscounting(AccountsController):
@frappe.whitelist()
def get_invoices(filters: str):
filters = frappe._dict(json.loads(filters))
def get_invoices(filters: str | dict):
filters = frappe._dict(frappe.parse_json(filters))
si = frappe.qb.DocType("Sales Invoice")
di = frappe.qb.DocType("Discounted Invoice")

View File

@@ -1805,8 +1805,7 @@ class PaymentEntry(AccountsController):
if not self.references or not matched_payment_requests:
return
if isinstance(matched_payment_requests, str):
matched_payment_requests = json.loads(matched_payment_requests)
matched_payment_requests = frappe.parse_json(matched_payment_requests)
# modify matched_payment_requests
# like (reference_doctype, reference_name, allocated_amount): payment_request
@@ -2011,8 +2010,7 @@ def validate_inclusive_tax(tax, doc):
@frappe.whitelist()
def get_outstanding_reference_documents(args: str | dict, validate: bool = False):
if isinstance(args, str):
args = json.loads(args)
args = frappe.parse_json(args)
if args.get("party_type") == "Member":
return

View File

@@ -740,7 +740,7 @@ def make_payment_request(**args):
# Schedule-based PRs are allowed only if no Payment Entry exists for this document.
# Any existing Payment Entry forces legacy (amount-based) flow.
selected_payment_schedules = json.loads(args.get("schedules")) if args.get("schedules") else []
selected_payment_schedules = frappe.parse_json(args.get("schedules")) if args.get("schedules") else []
# Backend guard:
# If any Payment Entry exists, schedule-based PRs are not allowed.
@@ -931,7 +931,7 @@ def apply_payment_references(pr, payment_reference):
def set_payment_references(payment_schedules):
payment_schedules = json.loads(payment_schedules) if payment_schedules else []
payment_schedules = frappe.parse_json(payment_schedules) if payment_schedules else []
payment_reference = []
for row in payment_schedules:

View File

@@ -1036,8 +1036,7 @@ def make_sales_return(source_name: str, target_doc: Document | str | None = None
def make_merge_log(invoices: str | list):
import json
if isinstance(invoices, str):
invoices = json.loads(invoices)
invoices = frappe.parse_json(invoices)
if len(invoices) == 0:
frappe.throw(_("At least one invoice has to be selected."))

View File

@@ -341,8 +341,7 @@ def apply_pricing_rule(args: str | dict, doc: str | dict | Document | None = Non
}
"""
if isinstance(args, str):
args = json.loads(args)
args = frappe.parse_json(args)
args = frappe._dict(args)
@@ -397,8 +396,7 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
get_product_discount_rule,
)
if isinstance(doc, str):
doc = json.loads(doc)
doc = frappe.parse_json(doc)
if doc:
doc = frappe.get_doc(doc)
@@ -628,9 +626,7 @@ def remove_pricing_rule_for_item(
get_pricing_rule_items,
)
if isinstance(item_details, str):
item_details = json.loads(item_details)
item_details = frappe._dict(item_details)
item_details = frappe._dict(frappe.parse_json(item_details))
for d in get_applied_pricing_rules(pricing_rules):
if not d or not frappe.db.exists("Pricing Rule", d):
@@ -671,8 +667,7 @@ def remove_pricing_rule_for_item(
@frappe.whitelist()
def remove_pricing_rules(item_list: str | list):
if isinstance(item_list, str):
item_list = json.loads(item_list)
item_list = frappe.parse_json(item_list)
out = []
for item in item_list:

View File

@@ -636,7 +636,7 @@ def remove_free_item(doc):
def get_applied_pricing_rules(pricing_rules):
if pricing_rules:
if pricing_rules.startswith("["):
return json.loads(pricing_rules)
return frappe.parse_json(pricing_rules)
else:
return pricing_rules.split(",")

View File

@@ -542,8 +542,7 @@ def check_multi_currency(pr_doc):
def is_any_doc_running(for_filter: str | dict | None = None) -> str | None:
running_doc = None
if for_filter:
if isinstance(for_filter, str):
for_filter = json.loads(for_filter)
for_filter = frappe.parse_json(for_filter)
running_doc = frappe.db.get_value(
"Process Payment Reconciliation",

View File

@@ -95,6 +95,8 @@ def start_pcv_processing(docname: str):
frappe.has_permission("Process Payment Reconciliation", "write", doc=docname, throw=True)
frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Running")
timeout = frappe.db.get_single_value("Accounts Settings", "pcv_job_timeout") or 3600
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
if normal_balances := (
qb.from_(ppcvd)
@@ -121,7 +123,7 @@ def start_pcv_processing(docname: str):
frappe.enqueue(
method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date",
queue="long",
timeout="3600",
timeout=timeout,
is_async=True,
enqueue_after_commit=True,
docname=docname,
@@ -247,6 +249,8 @@ def get_gle_for_closing_account(pcv, dimension_balance, dimensions):
@frappe.whitelist()
def schedule_next_date(docname: str):
timeout = frappe.db.get_single_value("Accounts Settings", "pcv_job_timeout") or 3600
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
if to_process := (
qb.from_(ppcvd)
@@ -272,7 +276,7 @@ def schedule_next_date(docname: str):
frappe.enqueue(
method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date",
queue="long",
timeout="3600",
timeout=timeout,
is_async=True,
enqueue_after_commit=True,
docname=docname,
@@ -302,7 +306,7 @@ def schedule_next_date(docname: str):
frappe.enqueue(
method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.summarize_and_post_ledger_entries",
queue="long",
timeout="3600",
timeout=timeout,
is_async=True,
job_name=job_name,
enqueue_after_commit=True,

View File

@@ -50,8 +50,7 @@ def make_purchase_receipt(
):
if args is None:
args = {}
if isinstance(args, str):
args = json.loads(args)
args = frappe.parse_json(args)
def post_parent_process(source_parent, target_parent):
remove_items_with_zero_qty(target_parent)

View File

@@ -201,9 +201,9 @@ def get_linked_advances(company, docname):
@frappe.whitelist()
def create_unreconcile_doc_for_selection(selections: str | None = None):
def create_unreconcile_doc_for_selection(selections: str | list | None = None):
if selections:
selections = json.loads(selections)
selections = frappe.parse_json(selections)
# assuming each row is a unique voucher
for row in selections:
unrecon = frappe.new_doc("Unreconcile Payment")

View File

@@ -1,6 +1,6 @@
{
"align": "Left",
"content": "<table class=\"invoice-header\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<td class=\"logo-cell\" style=\"vertical-align:middle ! important\">\n\t\t\t\t<div class=\"logo-container\">\n\t\t\t\t\t{% set company_logo = frappe.db.get_value(\"Company\", doc.company, \"company_logo\") %} {% if\n\t\t\t\t\tcompany_logo %}\n\t\t\t\t\t<img src=\"{{ frappe.utils.get_url(company_logo) }}\" alt=\"Company Logo\">\n\t\t\t\t\t{% endif %}\n\t\t\t\t</div>\n\t\t\t</td>\n\n\t\t\t<td class=\"company-details\">\n\t\t\t\t<div class=\"company-name\">{{ doc.company }}</div>\n\t\t\t\t{% if doc.company_address %} {% set company_address = frappe.db.get_value(\"Address\",\n\t\t\t\tdoc.company_address, [\"address_line1\", \"address_line2\", \"city\", \"state\", \"pincode\",\n\t\t\t\t\"country\"], as_dict=True) %} {% elif doc.billing_address %} {% set company_address =\n\t\t\t\tfrappe.db.get_value(\"Address\", doc.billing_address, [\"address_line1\", \"address_line2\", \"city\",\n\t\t\t\t\"state\", \"pincode\", \"country\"], as_dict=True) %} {% endif %} {% if company_address %} {{\n\t\t\t\tcompany_address.address_line1 or \"\" }}<br>\n\t\t\t\t{% if company_address.address_line2 %} {{ company_address.address_line2 }}<br>\n\t\t\t\t{% endif %} {{ company_address.city or \"\" }}, {{ company_address.state or \"\" }} {{\n\t\t\t\tcompany_address.pincode or \"\" }}, {{ company_address.country or \"\"}}<br>\n\t\t\t\t{% endif %}\n\t\t\t</td>\n\n\t\t\t<td class=\"invoice-info-cell\">\n\t\t\t\t{% set website = frappe.db.get_value(\"Company\", doc.company, \"website\") %} {% set email =\n\t\t\t\tfrappe.db.get_value(\"Company\", doc.company, \"email\") %} {% set phone_no =\n\t\t\t\tfrappe.db.get_value(\"Company\", doc.company, \"phone_no\") %}\n\n\t\t\t\t<div class=\"invoice-info\">\n\t\t\t\t\t<span class=\"invoice-label\">{{ doc.doctype }}</span>\n\t\t\t\t\t<span>{{ doc.name }}</span>\n\t\t\t\t</div>\n\t\t\t\t{% if website %}\n\t\t\t\t<div class=\"invoice-info\">\n\t\t\t\t\t<span class=\"invoice-label\">{{ _(\"Website:\") }}</span>\n\t\t\t\t\t<span>{{ website }}</span>\n\t\t\t\t</div>\n\t\t\t\t{% endif %} {% if email %}\n\t\t\t\t<div class=\"invoice-info\">\n\t\t\t\t\t<span class=\"invoice-label\">{{ _(\"Email:\") }}</span>\n\t\t\t\t\t<span>{{ email }}</span>\n\t\t\t\t</div>\n\t\t\t\t{% endif %} {% if phone_no %}\n\t\t\t\t<div class=\"invoice-info\">\n\t\t\t\t\t<span class=\"invoice-label\">{{ _(\"Contact:\") }}</span>\n\t\t\t\t\t<span>{{ phone_no }}</span>\n\t\t\t\t</div>\n\t\t\t\t{% endif %}\n\t\t\t</td>\n\t\t</tr>\n\t</tbody>\n</table>",
"content": "<table class=\"invoice-header\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<td class=\"logo-cell\" style=\"vertical-align:middle ! important\">\n\t\t\t\t<div class=\"logo-container\">\n\t\t\t\t\t{% set company_logo = frappe.db.get_value(\"Company\", doc.company, \"company_logo\") %} {% if\n\t\t\t\t\tcompany_logo %}\n\t\t\t\t\t<img src=\"{{ frappe.utils.get_url(company_logo) }}\" alt=\"Company Logo\">\n\t\t\t\t\t{% endif %}\n\t\t\t\t</div>\n\t\t\t</td>\n\n\t\t\t<td class=\"company-details\">\n\t\t\t\t{% if doc.company %}<div class=\"company-name\">{{ doc.company }}</div>{% endif %}\n\t\t\t\t{% if doc.company_address %} {% set company_address = frappe.db.get_value(\"Address\",\n\t\t\t\tdoc.company_address, [\"address_line1\", \"address_line2\", \"city\", \"state\", \"pincode\",\n\t\t\t\t\"country\"], as_dict=True) %} {% elif doc.billing_address %} {% set company_address =\n\t\t\t\tfrappe.db.get_value(\"Address\", doc.billing_address, [\"address_line1\", \"address_line2\", \"city\",\n\t\t\t\t\"state\", \"pincode\", \"country\"], as_dict=True) %} {% endif %} {% if company_address %} {{\n\t\t\t\tcompany_address.address_line1 or \"\" }}<br>\n\t\t\t\t{% if company_address.address_line2 %} {{ company_address.address_line2 }}<br>\n\t\t\t\t{% endif %} {{ company_address.city or \"\" }}, {{ company_address.state or \"\" }} {{\n\t\t\t\tcompany_address.pincode or \"\" }}, {{ company_address.country or \"\"}}<br>\n\t\t\t\t{% endif %}\n\t\t\t</td>\n\n\t\t\t<td class=\"invoice-info-cell\">\n\t\t\t\t{% set website = frappe.db.get_value(\"Company\", doc.company, \"website\") %} {% set email =\n\t\t\t\tfrappe.db.get_value(\"Company\", doc.company, \"email\") %} {% set phone_no =\n\t\t\t\tfrappe.db.get_value(\"Company\", doc.company, \"phone_no\") %}\n\n\t\t\t\t<div class=\"invoice-info\">\n\t\t\t\t\t<span class=\"invoice-label\">{{ doc.doctype }}</span>\n\t\t\t\t\t<span>{{ doc.name }}</span>\n\t\t\t\t</div>\n\t\t\t\t{% if website %}\n\t\t\t\t<div class=\"invoice-info\">\n\t\t\t\t\t<span class=\"invoice-label\">{{ _(\"Website:\") }}</span>\n\t\t\t\t\t<span>{{ website }}</span>\n\t\t\t\t</div>\n\t\t\t\t{% endif %} {% if email %}\n\t\t\t\t<div class=\"invoice-info\">\n\t\t\t\t\t<span class=\"invoice-label\">{{ _(\"Email:\") }}</span>\n\t\t\t\t\t<span>{{ email }}</span>\n\t\t\t\t</div>\n\t\t\t\t{% endif %} {% if phone_no %}\n\t\t\t\t<div class=\"invoice-info\">\n\t\t\t\t\t<span class=\"invoice-label\">{{ _(\"Contact:\") }}</span>\n\t\t\t\t\t<span>{{ phone_no }}</span>\n\t\t\t\t</div>\n\t\t\t\t{% endif %}\n\t\t\t</td>\n\t\t</tr>\n\t</tbody>\n</table>",
"creation": "2026-05-15 15:21:48.255627",
"custom_css": "\t.letter-head {\n\t\tborder-radius: 18px;\n\t\tpadding-right: 12px;\n\t\tmargin-left: 12px;\n\t\tmargin-right: 12px;\n\t}\n\n\t.letter-head td {\n\t\tpadding: 0px !important;\n\t}\n\t.invoice-header {\n\t\twidth: 100%;\n\t}\n\t.logo-cell {\n\t\twidth: 100px;\n\t\ttext-align: center;\n\t\tposition: relative;\n\t}\n\t.logo-container {\n\t\twidth: 90px;\n\t\tdisplay: block;\n\t}\n\t.logo-container img {\n\t\tmax-width: 90px;\n\t\tmax-height: 90px;\n\t\tdisplay: inline-block;\n\t\tborder-radius: 15px;\n\t}\n\t.company-details {\n\t\twidth: 40%;\n\t\talign-content: center;\n\t}\n\t.company-name {\n\t\tfont-size: 14px;\n\t\tfont-weight: bold;\n\t\tcolor: #171717;\n\t\tmargin-bottom: 4px;\n\t}\n\t.invoice-info-cell {\n\t\tfloat: right;\n\t\tvertical-align: top;\n\t}\n\t.invoice-info {\n\t\tmargin-bottom: 2px;\n\t}\n\t.invoice-label {\n\t\tcolor: #7c7c7c;\n\t\tdisplay: inline-block;\n\t\tmargin-right: 5px;\n\t}",
"disabled": 0,
@@ -16,7 +16,7 @@
"is_default": 0,
"letter_head_for": "DocType",
"letter_head_name": "Company Letterhead",
"modified": "2026-05-16 15:15:23.014622",
"modified": "2026-06-24 17:49:52.350750",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Company Letterhead",

View File

@@ -1,6 +1,6 @@
{
"align": "Left",
"content": "<table class=\"letterhead-container\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<td class=\"logo-address\">\n\t\t\t\t{% set company_logo = frappe.db.get_value(\"Company\", doc.company, \"company_logo\") %} {% if\n\t\t\t\tcompany_logo %}\n\t\t\t\t<div class=\"logo\">\n\t\t\t\t\t<img src=\"{{ frappe.utils.get_url(company_logo) }}\">\n\t\t\t\t</div>\n\t\t\t\t{% endif %}\n\t\t\t\t<div class=\"company-name\">{{ doc.company }}</div>\n\t\t\t\t<div class=\"company-address\">\n\t\t\t\t\t{% if doc.company_address %} {% set company_address = frappe.db.get_value(\"Address\",\n\t\t\t\t\tdoc.company_address, [\"address_line1\", \"address_line2\", \"city\", \"state\", \"pincode\",\n\t\t\t\t\t\"country\"], as_dict=True) %} {% elif doc.billing_address %} {% set company_address =\n\t\t\t\t\tfrappe.db.get_value(\"Address\", doc.billing_address, [\"address_line1\", \"address_line2\",\n\t\t\t\t\t\"city\", \"state\", \"pincode\", \"country\"], as_dict=True) %} {% endif %} {% if company_address\n\t\t\t\t\t%} {{ company_address.address_line1 or \"\" }}<br>\n\t\t\t\t\t{% if company_address.address_line2 %} {{ company_address.address_line2 }}<br>\n\t\t\t\t\t{% endif %} {{ company_address.city or \"\" }}, {{ company_address.state or \"\" }} {{\n\t\t\t\t\tcompany_address.pincode or \"\" }}, {{ company_address.country or \"\"}}<br>\n\t\t\t\t\t{% endif %}\n\t\t\t\t</div>\n\t\t\t</td>\n\n\t\t\t<td style=\"vertical-align:top\">\n\t\t\t\t<div style=\"height:90px;margin-bottom:10px;text-align:right\">\n\t\t\t\t\t<div class=\"invoice-title\">{{ doc.doctype }}</div>\n\t\t\t\t\t<div class=\"invoice-number\">{{ doc.name }}</div>\n\t\t\t\t\t<br>\n\t\t\t\t</div>\n\t\t\t\t<div style=\"text-align:left;float:right\" class=\"other-details\">\n\t\t\t\t\t{% set company_details = frappe.db.get_value(\"Company\", doc.company, [\"website\", \"email\",\n\t\t\t\t\t\"phone_no\"], as_dict=True) %} {% set website = company_details.website %} {% set email =\n\t\t\t\t\tcompany_details.email %} {% set phone_no = company_details.phone_no %} {% if website %}\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<span class=\"contact-title\">{{ _(\"Website:\") }}</span><span class=\"contact-value\">{{ website }}</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t{% endif %} {% if email %}\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<span class=\"contact-title\">{{ _(\"Email:\") }}</span><span class=\"contact-value\">{{ email }}</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t{% endif %} {% if phone_no %}\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<span class=\"contact-title\">{{ _(\"Contact:\") }}</span><span class=\"contact-value\">{{ phone_no }}</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t{% endif %}\n\t\t\t\t</div>\n\t\t\t</td>\n\t\t</tr>\n\t</tbody>\n</table>\n",
"content": "<table class=\"letterhead-container\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<td class=\"logo-address\">\n\t\t\t\t{% set company_logo = frappe.db.get_value(\"Company\", doc.company, \"company_logo\") %} {% if\n\t\t\t\tcompany_logo %}\n\t\t\t\t<div class=\"logo\">\n\t\t\t\t\t<img src=\"{{ frappe.utils.get_url(company_logo) }}\">\n\t\t\t\t</div>\n\t\t\t\t{% endif %}\n\t\t\t\t{% if doc.company %}<div class=\"company-name\">{{ doc.company }}</div>{% endif %}\n\t\t\t\t<div class=\"company-address\">\n\t\t\t\t\t{% if doc.company_address %} {% set company_address = frappe.db.get_value(\"Address\",\n\t\t\t\t\tdoc.company_address, [\"address_line1\", \"address_line2\", \"city\", \"state\", \"pincode\",\n\t\t\t\t\t\"country\"], as_dict=True) %} {% elif doc.billing_address %} {% set company_address =\n\t\t\t\t\tfrappe.db.get_value(\"Address\", doc.billing_address, [\"address_line1\", \"address_line2\",\n\t\t\t\t\t\"city\", \"state\", \"pincode\", \"country\"], as_dict=True) %} {% endif %} {% if company_address\n\t\t\t\t\t%} {{ company_address.address_line1 or \"\" }}<br>\n\t\t\t\t\t{% if company_address.address_line2 %} {{ company_address.address_line2 }}<br>\n\t\t\t\t\t{% endif %} {{ company_address.city or \"\" }}, {{ company_address.state or \"\" }} {{\n\t\t\t\t\tcompany_address.pincode or \"\" }}, {{ company_address.country or \"\"}}<br>\n\t\t\t\t\t{% endif %}\n\t\t\t\t</div>\n\t\t\t</td>\n\n\t\t\t<td style=\"vertical-align:top\">\n\t\t\t\t<div style=\"height:90px;margin-bottom:10px;text-align:right\">\n\t\t\t\t\t<div class=\"invoice-title\">{{ doc.doctype }}</div>\n\t\t\t\t\t<div class=\"invoice-number\">{{ doc.name }}</div>\n\t\t\t\t\t<br>\n\t\t\t\t</div>\n\t\t\t\t<div style=\"text-align:left;float:right\" class=\"other-details\">\n\t\t\t\t\t{% if doc.company %}{% set company_details = frappe.db.get_value(\"Company\", doc.company, [\"website\", \"email\",\n\t\t\t\t\t\"phone_no\"], as_dict=True) %}{% set website = company_details.website %}{% set email =\n\t\t\t\t\tcompany_details.email %}{% set phone_no = company_details.phone_no %}{% else %}{% set website = None %}{% set email = None %}{% set phone_no = None %}{% endif %} {% if website %}\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<span class=\"contact-title\">{{ _(\"Website:\") }}</span><span class=\"contact-value\">{{ website }}</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t{% endif %} {% if email %}\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<span class=\"contact-title\">{{ _(\"Email:\") }}</span><span class=\"contact-value\">{{ email }}</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t{% endif %} {% if phone_no %}\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<span class=\"contact-title\">{{ _(\"Contact:\") }}</span><span class=\"contact-value\">{{ phone_no }}</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t{% endif %}\n\t\t\t\t</div>\n\t\t\t</td>\n\t\t</tr>\n\t</tbody>\n</table>\n",
"creation": "2026-05-15 15:21:48.373815",
"custom_css": "\t.print-format-preview {\n\t\tmargin-top: 12px;\n\t}\n\t.letter-head {\n\t\tborder-radius: 18px;\n\t\tbackground: #f8f8f8;\n\t\tpadding: 12px;\n\t\tmargin-left: 12px;\n\t\tmargin-right: 12px;\n\t}\n\t.letterhead-container {\n\t\twidth: 100%;\n\t}\n\t.letterhead-container .other-details {\n\t\tposition: absolute;\n\t\tright: 0;\n\t\tbottom: 0;\n\t}\n\t.logo-address {\n\t\twidth: 65%;\n\t\tvertical-align: top;\n\t}\n\n\t.letter-head .logo {\n\t\twidth: 90px;\n\t\tdisplay: block;\n\t\tmargin-bottom: 10px;\n\t}\n\n\t.letter-head .logo img {\n\t\tborder-radius: 15px;\n\t}\n\n\t.company-name {\n\t\tcolor: #171717;\n\t\tfont-weight: bold;\n\t\tline-height: 23px;\n\t\tmargin-bottom: 5px;\n\t}\n\n\t.company-address {\n\t\tcolor: #171717;\n\t\twidth: 300px;\n\t}\n\n\t.invoice-title {\n\t\tfont-weight: bold;\n\t}\n\n\t.invoice-number {\n\t\tcolor: #7c7c7c;\n\t}\n\n\t.contact-title {\n\t\tcolor: #7c7c7c;\n\t\twidth: 60px;\n\t\tdisplay: inline-block;\n\t\tvertical-align: top;\n\t\tmargin-right: 10px;\n\t}\n\n\t.contact-value {\n\t\tcolor: #171717;\n\t\tdisplay: inline-block;\n\t}\n\t.letterhead-container td {\n\t\tpadding: 0px !important;\n\t\tposition: relative;\n\t}",
"disabled": 0,
@@ -16,7 +16,7 @@
"is_default": 0,
"letter_head_for": "DocType",
"letter_head_name": "Company Letterhead - Grey",
"modified": "2026-05-16 15:15:19.942207",
"modified": "2026-06-24 18:23:05.120521",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Company Letterhead - Grey",

View File

@@ -1,6 +1,6 @@
{
"align": "Left",
"content": "<table class=\"invoice-header\">\n\t<tbody>\n\t\t<tr>\n\n\t\t\t<td class=\"logo-cell\" style=\"vertical-align:top\">\n\t\t\t\t{% set company = frappe.get_doc(\"Company\", doc.company) %}\n\n\t\t\t\t<div class=\"logo-container\">\n\t\t\t\t\t{% if company.company_logo %}\n\t\t\t\t\t<img src=\"{{ frappe.utils.get_url(company.company_logo) }}\" alt=\"Company Logo\">\n\t\t\t\t\t{% endif %}\n\t\t\t\t</div>\n\t\t\t</td>\n\n\t\t\t<td class=\"company-details\" style=\"vertical-align:top\">\n\t\t\t\t<div class=\"company-name\">{{ company.name }}</div>\n\n\t\t\t\t{% set company_address_name = frappe.db.get_value(\n\t\t\t\t\t\"Dynamic Link\",\n\t\t\t\t\t{\n\t\t\t\t\t\t\"link_doctype\": \"Company\",\n\t\t\t\t\t\t\"link_name\": company.name,\n\t\t\t\t\t\t\"parenttype\": \"Address\"\n\t\t\t\t\t},\n\t\t\t\t\t\"parent\"\n\t\t\t\t) %}\n\n\t\t\t\t{% if company_address_name %}\n\t\t\t\t\t{% set company_address = frappe.db.get_value(\n\t\t\t\t\t\t\"Address\",\n\t\t\t\t\t\tcompany_address_name,\n\t\t\t\t\t\t[\"address_line1\", \"address_line2\", \"city\", \"state\", \"pincode\", \"country\"],\n\t\t\t\t\t\tas_dict=True\n\t\t\t\t\t) %}\n\t\t\t\t{% endif %}\n\n\t\t\t\t{% if company_address %}\n\t\t\t\t<div class=\"company-address\">\n\t\t\t\t\t{{ company_address.address_line1 or \"\" }}\n\n\t\t\t\t\t{% if company_address.address_line2 %}\n\t\t\t\t\t\t<br>{{ company_address.address_line2 }}\n\t\t\t\t\t{% endif %}\n\n\t\t\t\t\t<br>\n\n\t\t\t\t\t{{ company_address.city or \"\" }}\n\t\t\t\t\t{% if company_address.state %}, {{ company_address.state }}{% endif %}\n\t\t\t\t\t{{ company_address.pincode or \"\" }}\n\n\t\t\t\t\t{% if company_address.country %}\n\t\t\t\t\t\t, {{ company_address.country }}\n\t\t\t\t\t{% endif %}\n\t\t\t\t</div>\n\t\t\t\t{% endif %}\n\t\t\t</td>\n\n\t\t\t<td class=\"invoice-info-cell\" style=\"vertical-align:top;text-align:right\">\n\n\t\t\t\t{% set website = frappe.db.get_value(\"Company\", doc.company, \"website\") %}\n\t\t\t\t{% set email = frappe.db.get_value(\"Company\", doc.company, \"email\") %}\n\t\t\t\t{% set phone_no = frappe.db.get_value(\"Company\", doc.company, \"phone_no\") %}\n\n\t\t\t\t{% if website %}\n\t\t\t\t<div class=\"invoice-info\">\n\t\t\t\t\t<span class=\"invoice-label\">{{ _(\"Website:\") }}</span>\n\t\t\t\t\t<span>{{ website }}</span>\n\t\t\t\t</div>\n\t\t\t\t{% endif %}\n\n\t\t\t\t{% if email %}\n\t\t\t\t<div class=\"invoice-info\">\n\t\t\t\t\t<span class=\"invoice-label\">{{ _(\"Email:\") }}</span>\n\t\t\t\t\t<span>{{ email }}</span>\n\t\t\t\t</div>\n\t\t\t\t{% endif %}\n\n\t\t\t\t{% if phone_no %}\n\t\t\t\t<div class=\"invoice-info\">\n\t\t\t\t\t<span class=\"invoice-label\">{{ _(\"Contact:\") }}</span>\n\t\t\t\t\t<span>{{ phone_no }}</span>\n\t\t\t\t</div>\n\t\t\t\t{% endif %}\n\t\t\t</td>\n\n\t\t</tr>\n\t</tbody>\n</table>",
"content": "<table class=\"invoice-header\">\n\t<tbody>\n\t\t<tr>\n\n\t\t\t<td class=\"logo-cell\" style=\"vertical-align:top\">\n\t\t\t\t{% if doc.company %}{% set company = frappe.get_doc(\"Company\", doc.company) %}{% else %}{% set company = frappe._dict() %}{% endif %}\n\n\t\t\t\t<div class=\"logo-container\">\n\t\t\t\t\t{% if company.company_logo %}\n\t\t\t\t\t<img src=\"{{ frappe.utils.get_url(company.company_logo) }}\" alt=\"Company Logo\">\n\t\t\t\t\t{% endif %}\n\t\t\t\t</div>\n\t\t\t</td>\n\n\t\t\t<td class=\"company-details\" style=\"vertical-align:top\">\n\t\t\t\t{% if company.name %}<div class=\"company-name\">{{ company.name }}</div>{% endif %}\n\n\t\t\t\t{% set company_address_name = frappe.db.get_value(\n\t\t\t\t\t\"Dynamic Link\",\n\t\t\t\t\t{\n\t\t\t\t\t\t\"link_doctype\": \"Company\",\n\t\t\t\t\t\t\"link_name\": company.name,\n\t\t\t\t\t\t\"parenttype\": \"Address\"\n\t\t\t\t\t},\n\t\t\t\t\t\"parent\"\n\t\t\t\t) %}\n\n\t\t\t\t{% if company_address_name %}\n\t\t\t\t\t{% set company_address = frappe.db.get_value(\n\t\t\t\t\t\t\"Address\",\n\t\t\t\t\t\tcompany_address_name,\n\t\t\t\t\t\t[\"address_line1\", \"address_line2\", \"city\", \"state\", \"pincode\", \"country\"],\n\t\t\t\t\t\tas_dict=True\n\t\t\t\t\t) %}\n\t\t\t\t{% endif %}\n\n\t\t\t\t{% if company_address %}\n\t\t\t\t<div class=\"company-address\">\n\t\t\t\t\t{{ company_address.address_line1 or \"\" }}\n\n\t\t\t\t\t{% if company_address.address_line2 %}\n\t\t\t\t\t\t<br>{{ company_address.address_line2 }}\n\t\t\t\t\t{% endif %}\n\n\t\t\t\t\t<br>\n\n\t\t\t\t\t{{ company_address.city or \"\" }}\n\t\t\t\t\t{% if company_address.state %}, {{ company_address.state }}{% endif %}\n\t\t\t\t\t{{ company_address.pincode or \"\" }}\n\n\t\t\t\t\t{% if company_address.country %}\n\t\t\t\t\t\t, {{ company_address.country }}\n\t\t\t\t\t{% endif %}\n\t\t\t\t</div>\n\t\t\t\t{% endif %}\n\t\t\t</td>\n\n\t\t\t<td class=\"invoice-info-cell\" style=\"vertical-align:top;text-align:right\">\n\n\t\t\t\t{% set website = frappe.db.get_value(\"Company\", doc.company, \"website\") %}\n\t\t\t\t{% set email = frappe.db.get_value(\"Company\", doc.company, \"email\") %}\n\t\t\t\t{% set phone_no = frappe.db.get_value(\"Company\", doc.company, \"phone_no\") %}\n\n\t\t\t\t{% if website %}\n\t\t\t\t<div class=\"invoice-info\">\n\t\t\t\t\t<span class=\"invoice-label\">{{ _(\"Website:\") }}</span>\n\t\t\t\t\t<span>{{ website }}</span>\n\t\t\t\t</div>\n\t\t\t\t{% endif %}\n\n\t\t\t\t{% if email %}\n\t\t\t\t<div class=\"invoice-info\">\n\t\t\t\t\t<span class=\"invoice-label\">{{ _(\"Email:\") }}</span>\n\t\t\t\t\t<span>{{ email }}</span>\n\t\t\t\t</div>\n\t\t\t\t{% endif %}\n\n\t\t\t\t{% if phone_no %}\n\t\t\t\t<div class=\"invoice-info\">\n\t\t\t\t\t<span class=\"invoice-label\">{{ _(\"Contact:\") }}</span>\n\t\t\t\t\t<span>{{ phone_no }}</span>\n\t\t\t\t</div>\n\t\t\t\t{% endif %}\n\t\t\t</td>\n\n\t\t</tr>\n\t</tbody>\n</table>",
"creation": "2026-05-15 19:49:47.582252",
"custom_css": ".letter-head {\n\tborder-radius: 18px;\n\tpadding: 8px 10px;\n\tmargin: 10px 0 14px;\n\tfont-family: Inter, sans-serif;\n\tfont-size: 14px;\n\tcolor: #171717;\n}\n\n.letter-head td {\n\tpadding: 0 !important;\n\tvertical-align: middle;\n}\n\n.invoice-header {\n\twidth: 100%;\n\tborder-collapse: collapse;\n\ttable-layout: fixed;\n\tborder-bottom: 1px solid #ededed;\n\tpadding-bottom: 10px;\n}\n\n.logo-cell {\n\twidth: 100px;\n\ttext-align: center;\n\twhite-space: nowrap;\n}\n\n.logo-container {\n\tdisplay: inline-block;\n\tmargin: auto;\n}\n\n.logo-container img {\n\tmax-width: 95px;\n\tmax-height: 95px;\n\tdisplay: block;\n\tborder-radius: 12px;\n}\n\n.company-details {\n\twidth: 55%;\n\tpadding-left: 10px !important;\n\tline-height: 1.5;\n}\n\n.company-name {\n\tfont-size: 14px;\n\tfont-weight: 600;\n\tcolor: #171717;\n\tmargin-bottom: 4px;\n}\n\n.company-address {\n\tfont-size: 14px;\n\tline-height: 1.5;\n\tcolor: #171717;\n}\n\n.invoice-info-cell {\n\twidth: 240px;\n\ttext-align: right;\n\tvertical-align: top !important;\n\tline-height: 1.5;\n}\n\n.document-name {\n\tfont-size: 14px;\n\tfont-weight: 600;\n\tcolor: #171717;\n\tmargin-bottom: 6px;\n}\n\n.invoice-info {\n\tfont-size: 14px;\n\tcolor: #171717;\n\tmargin-bottom: 2px;\n\tfont-variant-numeric: tabular-nums;\n}\n\n.invoice-label {\n\tcolor: #7c7c7c;\n\tfont-weight: 500;\n\tmargin-right: 4px;\n\tdisplay: inline-block;\n}",
"disabled": 0,
@@ -16,7 +16,7 @@
"is_default": 0,
"letter_head_for": "Report",
"letter_head_name": "Company Letterhead Report",
"modified": "2026-05-16 15:15:26.155770",
"modified": "2026-06-24 18:06:39.820968",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Company Letterhead Report",

View File

@@ -4,13 +4,20 @@
"columns": [],
"creation": "2013-04-22 16:16:03",
"default_print_format": "Accounts Payable Standard",
"disable_prepared_report_automation": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"doctype_to_sync": [
{
"doc_type": "Payment Ledger Entry"
}
],
"filters": [],
"generate_csv": 0,
"idx": 3,
"is_standard": "Yes",
"modified": "2026-05-22 14:35:14.716933",
"modified": "2026-06-25 12:03:36.559152",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Payable",
@@ -33,5 +40,6 @@
"role": "Auditor"
}
],
"synced_report": 0,
"timeout": 0
}

View File

@@ -4,13 +4,20 @@
"columns": [],
"creation": "2013-04-16 11:31:13",
"default_print_format": "Accounts Receivable Standard",
"disable_prepared_report_automation": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"doctype_to_sync": [
{
"doc_type": "Payment Ledger Entry"
}
],
"filters": [],
"generate_csv": 0,
"idx": 5,
"is_standard": "Yes",
"modified": "2026-05-22 14:34:57.666402",
"modified": "2026-06-25 12:03:28.812092",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Receivable",
@@ -27,5 +34,6 @@
"role": "Accounts User"
}
],
"synced_report": 0,
"timeout": 0
}

View File

@@ -4,13 +4,20 @@
"columns": [],
"creation": "2014-07-14 05:24:20.385279",
"default_print_format": "Balance Sheet Standard",
"disable_prepared_report_automation": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"doctype_to_sync": [
{
"doc_type": "GL Entry"
}
],
"filters": [],
"generate_csv": 0,
"idx": 3,
"is_standard": "Yes",
"modified": "2026-05-22 14:35:28.187799",
"modified": "2026-06-22 13:38:25.236839",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Balance Sheet",
@@ -30,5 +37,6 @@
"role": "Auditor"
}
],
"synced_report": 0,
"timeout": 0
}

View File

@@ -4,18 +4,27 @@
import frappe
from frappe import _
from frappe.utils import cint, flt
from frappe.utils import add_days, cint, flt
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
FinancialReportEngine,
get_xlsx_styles, #! DO NOT REMOVE - hook for styling
)
from erpnext.accounts.report.financial_statements import (
accumulate_values_into_parents,
add_total_row,
calculate_values,
compute_growth_view_data,
filter_accounts,
filter_out_zero_value_rows,
get_accounting_entries,
get_accounts,
get_appropriate_currency,
get_columns,
get_data,
get_filtered_list_for_consolidated_report,
get_period_list,
prepare_data,
)
@@ -266,3 +275,196 @@ def get_chart_data(filters, chart_columns, asset, liability, equity, currency):
chart["currency"] = currency
return chart
def execute_synced_report(filters):
from frappe.database.duckdb.database import get_latest_sync
if not (conn := get_latest_sync("GL Entry")):
frappe.throw(_("Balance Sheet requires {0} to be synced to DuckDB").format(frappe.bold("GL Entry")))
period_list = get_period_list(
filters.from_fiscal_year,
filters.to_fiscal_year,
filters.period_start_date,
filters.period_end_date,
filters.filter_based_on,
filters.periodicity,
company=filters.company,
)
filters.period_start_date = period_list[0]["year_start_date"]
currency = filters.presentation_currency or frappe.get_cached_value(
"Company", filters.company, "default_currency"
)
asset = _get_data_duckdb(conn, filters, "Asset", "Debit", period_list)
liability = _get_data_duckdb(conn, filters, "Liability", "Credit", period_list)
equity = _get_data_duckdb(conn, filters, "Equity", "Credit", period_list)
provisional_profit_loss, total_credit = get_provisional_profit_loss(
asset, liability, equity, period_list, filters.company, currency
)
message, opening_balance = check_opening_balance(asset, liability, equity)
data = []
data.extend(asset or [])
data.extend(liability or [])
data.extend(equity or [])
if opening_balance and round(opening_balance, 2) != 0:
unclosed = {
"account_name": "'" + _("Unclosed Fiscal Years Profit / Loss (Credit)") + "'",
"account": "'" + _("Unclosed Fiscal Years Profit / Loss (Credit)") + "'",
"warn_if_negative": True,
"currency": currency,
}
for period in period_list:
unclosed[period.key] = opening_balance
if provisional_profit_loss:
provisional_profit_loss[period.key] = provisional_profit_loss[period.key] - opening_balance
unclosed["total"] = opening_balance
data.append(unclosed)
if provisional_profit_loss:
data.append(provisional_profit_loss)
if total_credit:
data.append(total_credit)
columns = get_columns(
filters.periodicity, period_list, filters.accumulated_values, company=filters.company
)
chart = get_chart_data(filters, period_list, asset, liability, equity, currency)
report_summary, primitive_summary = get_report_summary(
period_list, asset, liability, equity, provisional_profit_loss, currency, filters
)
if filters.get("selected_view") == "Growth":
compute_growth_view_data(data, period_list)
return columns, data, message, chart, report_summary, primitive_summary
def _get_data_duckdb(conn, filters, root_type, balance_must_be, period_list):
accounts = get_accounts(filters.company, root_type)
if not accounts:
return None
accounts, accounts_by_name, parent_children_map = filter_accounts(accounts)
company_currency = get_appropriate_currency(filters.company, filters)
gl_entries_by_account = {}
_load_gl_entries_duckdb(conn, filters, period_list, accounts, gl_entries_by_account, root_type)
calculate_values(
accounts_by_name,
gl_entries_by_account,
period_list,
filters.accumulated_values,
False,
)
accumulate_values_into_parents(accounts, accounts_by_name, period_list)
out = prepare_data(
accounts,
balance_must_be,
period_list,
company_currency,
accumulated_values=filters.accumulated_values,
)
out = filter_out_zero_value_rows(out, parent_children_map, filters.show_zero_values)
if out:
add_total_row(out, root_type, balance_must_be, period_list, company_currency)
return out
def _load_gl_entries_duckdb(conn, filters, period_list, accounts, gl_entries_by_account, root_type):
from erpnext.accounts.report.trial_balance.trial_balance import (
_extra_gl_conditions,
_fetch_gl_rows_duckdb,
)
from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency
company = filters.company
year_start_date = period_list[0]["year_start_date"]
last_to_date = period_list[-1]["to_date"]
ignore_is_opening = frappe.get_single_value("Accounts Settings", "ignore_is_opening_check_for_reporting")
leaf_accounts = [acc.name for acc in accounts if not acc.is_group]
if not leaf_accounts:
return
opening_from_date = None
ignore_opening_entries = False
ignore_closing_balances = frappe.get_single_value("Accounts Settings", "ignore_account_closing_balance")
if not ignore_closing_balances:
last_pcv_list = frappe.db.get_all(
"Period Closing Voucher",
filters={
"docstatus": 1,
"company": company,
"period_end_date": ("<", filters.get("period_start_date") or year_start_date),
},
fields=["period_end_date", "name"],
order_by="period_end_date desc",
limit=1,
)
if last_pcv_list:
last_pcv = last_pcv_list[0]
pcv_entries = get_accounting_entries(
"Account Closing Balance",
None,
last_to_date,
filters,
root_type=root_type,
ignore_closing_entries=False,
period_closing_voucher=last_pcv.name,
)
if filters.get("presentation_currency"):
convert_to_presentation_currency(pcv_entries, get_currency(filters))
for entry in pcv_entries:
gl_entries_by_account.setdefault(entry.account, []).append(entry)
opening_from_date = add_days(last_pcv.period_end_date, 1)
ignore_opening_entries = True
extra_cond, extra_params = _extra_gl_conditions(filters)
account_placeholders = ", ".join(["?"] * len(leaf_accounts))
base_conds = [
"company = ?",
"is_cancelled = 0",
f"account IN ({account_placeholders})",
]
base_params = [company, *leaf_accounts]
if ignore_opening_entries and not ignore_is_opening:
base_conds.append("is_opening = 'No'")
base_conds.extend(extra_cond)
base_params.extend(extra_params)
# Opening GL entries from DuckDB (entries before year_start_date)
open_conds = [*base_conds, "posting_date < ?"]
open_params = [*base_params, year_start_date]
if opening_from_date:
open_conds = [*open_conds, "posting_date >= ?"]
open_params = [*open_params, opening_from_date]
opening_entries = _fetch_gl_rows_duckdb(conn, open_conds, open_params)
if filters.get("presentation_currency"):
convert_to_presentation_currency(opening_entries, get_currency(filters))
synthetic_open_date = add_days(year_start_date, -1)
for entry in opening_entries:
entry.posting_date = synthetic_open_date
gl_entries_by_account.setdefault(entry.account, []).append(entry)
# Period GL entries from DuckDB (one aggregated query per period)
for period in period_list:
period_conds = [*base_conds, "posting_date >= ?", "posting_date <= ?"]
period_params = [*base_params, period.from_date, period.to_date]
period_entries = _fetch_gl_rows_duckdb(conn, period_conds, period_params)
if filters.get("presentation_currency"):
convert_to_presentation_currency(period_entries, get_currency(filters))
for entry in period_entries:
entry.posting_date = period.to_date
gl_entries_by_account.setdefault(entry.account, []).append(entry)

View File

@@ -4,13 +4,20 @@
"columns": [],
"creation": "2013-12-06 13:22:23",
"default_print_format": "General Ledger Standard",
"disable_prepared_report_automation": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"doctype_to_sync": [
{
"doc_type": "GL Entry"
}
],
"filters": [],
"generate_csv": 0,
"idx": 4,
"is_standard": "Yes",
"modified": "2026-05-22 14:34:35.246000",
"modified": "2026-06-22 13:38:35.057216",
"modified_by": "Administrator",
"module": "Accounts",
"name": "General Ledger",
@@ -30,5 +37,6 @@
"role": "Auditor"
}
],
"synced_report": 0,
"timeout": 0
}

View File

@@ -817,3 +817,288 @@ def get_columns(filters):
columns.extend([{"label": _("Remarks"), "fieldname": "remarks", "width": 400}])
return columns
def execute_synced_report(filters):
from frappe.database.duckdb.database import get_latest_sync
if conn := get_latest_sync("GL Entry"):
return _execute_with_duckdb_conn(filters, conn)
frappe.throw(_("General Ledger requires {0} to be synced to DuckDB").format(frappe.bold("GL Entry")))
def _execute_with_duckdb_conn(filters, conn):
if not filters:
return [], []
account_details = {}
if filters.get("print_in_account_currency") and not filters.get("account"):
frappe.throw(_("Select an account to print in account currency"))
for acc in frappe.get_all("Account", fields=["name", "is_group"]):
account_details.setdefault(acc.name, acc)
if filters.get("party"):
filters.party = frappe.parse_json(filters.get("party"))
validate_filters(filters, account_details)
validate_party(filters)
filters = set_account_currency(filters)
columns = get_columns(filters)
res = get_result_duckdb(filters, account_details, conn)
return columns, res
def get_result_duckdb(filters, account_details, conn):
accounting_dimensions = []
if filters.get("include_dimensions"):
accounting_dimensions = get_accounting_dimensions()
gl_entries = get_gl_entries_duckdb(filters, accounting_dimensions, conn)
data = get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries)
return get_result_as_list(data, filters)
def get_gl_entries_duckdb(filters, accounting_dimensions, conn):
currency_map = get_currency(filters)
col_names = [
"gl_entry",
"posting_date",
"account",
"party_type",
"party",
"voucher_type",
"voucher_subtype",
"voucher_no",
"cost_center",
"project",
"against_voucher_type",
"against_voucher",
"account_currency",
"against",
"is_opening",
"creation",
"debit",
"credit",
"debit_in_account_currency",
"credit_in_account_currency",
]
select_exprs = [
"name",
"posting_date",
"account",
"party_type",
"party",
"voucher_type",
"voucher_subtype",
"voucher_no",
"cost_center",
"project",
"against_voucher_type",
"against_voucher",
"account_currency",
"against",
"is_opening",
"creation",
"debit",
"credit",
"debit_in_account_currency",
"credit_in_account_currency",
]
if filters.get("show_remarks"):
remarks_length = frappe.get_single_value("Accounts Settings", "general_ledger_remarks_length")
if remarks_length:
select_exprs.append(f"substr(remarks, 1, {int(remarks_length)})")
else:
select_exprs.append("remarks")
col_names.append("remarks")
if filters.get("add_values_in_transaction_currency"):
select_exprs += [
"debit_in_transaction_currency",
"credit_in_transaction_currency",
"transaction_currency",
]
col_names += [
"debit_in_transaction_currency",
"credit_in_transaction_currency",
"transaction_currency",
]
if accounting_dimensions:
select_exprs += accounting_dimensions
col_names += accounting_dimensions
order_by = "posting_date, account, creation"
if filters.get("include_dimensions"):
order_by = "posting_date, creation"
if filters.get("categorize_by") == "Categorize by Voucher":
order_by = "posting_date, voucher_type, voucher_no"
if filters.get("categorize_by") == "Categorize by Account":
order_by = "account, posting_date, creation"
if filters.get("include_default_book_entries"):
filters["company_fb"] = frappe.get_cached_value(
"Company", filters.get("company"), "default_finance_book"
)
conditions, params = _build_gl_conditions_duckdb(filters)
select_clause = ", ".join(select_exprs)
sql = f'SELECT {select_clause} FROM "tabGL Entry" WHERE {" AND ".join(conditions)} ORDER BY {order_by}'
rows = conn.execute(sql, params).fetchall()
gl_entries = [frappe._dict(zip(col_names, row, strict=False)) for row in rows]
party_name_map = get_party_name_map()
for gl_entry in gl_entries:
if gl_entry.party_type and gl_entry.party:
gl_entry.party_name = party_name_map.get(gl_entry.party_type, {}).get(gl_entry.party)
if filters.get("presentation_currency"):
return convert_to_presentation_currency(gl_entries, currency_map, filters)
return gl_entries
def _build_gl_conditions_duckdb(filters):
ignore_is_opening = frappe.get_single_value("Accounts Settings", "ignore_is_opening_check_for_reporting")
conditions = ["company = ?"]
params = [filters.company]
if filters.get("account"):
filters.account = get_accounts_with_children(filters.account)
if filters.account:
conditions.append(f"account IN ({', '.join(['?'] * len(filters.account))})")
params.extend(filters.account)
if filters.get("cost_center"):
filters.cost_center = get_cost_centers_with_children(filters.cost_center)
conditions.append(f"cost_center IN ({', '.join(['?'] * len(filters.cost_center))})")
params.extend(filters.cost_center)
if filters.get("voucher_no"):
conditions.append("voucher_no = ?")
params.append(filters.voucher_no)
if filters.get("against_voucher_no"):
conditions.append("against_voucher = ?")
params.append(filters.against_voucher_no)
if filters.get("ignore_err"):
err_journals = frappe.db.get_all(
"Journal Entry",
filters={
"company": filters.get("company"),
"docstatus": 1,
"voucher_type": ("in", ["Exchange Rate Revaluation", "Exchange Gain Or Loss"]),
},
pluck="name",
)
if err_journals:
filters.update({"voucher_no_not_in": err_journals})
if filters.get("ignore_cr_dr_notes"):
system_generated = frappe.db.get_all(
"Journal Entry",
filters={
"company": filters.get("company"),
"docstatus": 1,
"voucher_type": ("in", ["Credit Note", "Debit Note"]),
"is_system_generated": 1,
},
pluck="name",
)
if system_generated:
vouchers_to_ignore = (filters.get("voucher_no_not_in") or []) + system_generated
filters.update({"voucher_no_not_in": vouchers_to_ignore})
if filters.get("voucher_no_not_in"):
vouchers = filters.voucher_no_not_in
conditions.append(f"voucher_no NOT IN ({', '.join(['?'] * len(vouchers))})")
params.extend(vouchers)
if filters.get("categorize_by") == "Categorize by Party" and not filters.get("party_type"):
conditions.append("party_type IN ('Customer', 'Supplier')")
if filters.get("party_type"):
conditions.append("party_type = ?")
params.append(filters.party_type)
if filters.get("party"):
conditions.append(f"party IN ({', '.join(['?'] * len(filters.party))})")
params.extend(filters.party)
# from_date: skip when filtering by account/party to allow opening balance calc in Python
if filters.get("disable_opening_balance_calculation"):
if not ignore_is_opening:
conditions.append("(posting_date >= ? OR is_opening = 'Yes')")
else:
conditions.append("posting_date >= ?")
params.append(filters.from_date)
elif not (
filters.get("account")
or filters.get("party")
or filters.get("categorize_by") in ["Categorize by Account", "Categorize by Party"]
):
if not ignore_is_opening:
conditions.append("(posting_date >= ? OR is_opening = 'Yes')")
else:
conditions.append("posting_date >= ?")
params.append(filters.from_date)
if not ignore_is_opening:
conditions.append("(posting_date <= ? OR is_opening = 'Yes')")
else:
conditions.append("posting_date <= ?")
params.append(filters.to_date)
if filters.get("project"):
conditions.append(f"project IN ({', '.join(['?'] * len(filters.project))})")
params.extend(filters.project)
company_fb = filters.get("company_fb") or frappe.get_cached_value(
"Company", filters.company, "default_finance_book"
)
if filters.get("include_default_book_entries"):
if filters.get("finance_book"):
if company_fb and cstr(filters.finance_book) != cstr(company_fb):
frappe.throw(
_("To use a different finance book, please uncheck 'Include Default FB Entries'")
)
fb_vals = [cstr(filters.finance_book), ""]
else:
fb_vals = [cstr(company_fb), ""]
conditions.append(f"(finance_book IN ({', '.join(['?'] * len(fb_vals))}) OR finance_book IS NULL)")
params.extend(fb_vals)
else:
if filters.get("finance_book"):
conditions.append("(finance_book IN (?, '') OR finance_book IS NULL)")
params.append(cstr(filters.finance_book))
else:
conditions.append("(finance_book IN ('') OR finance_book IS NULL)")
if not filters.get("show_cancelled_entries"):
conditions.append("is_cancelled = 0")
accounting_dimensions_list = get_accounting_dimensions(as_list=False)
if accounting_dimensions_list:
for dimension in accounting_dimensions_list:
if not dimension.disabled and dimension.document_type != "Finance Book":
if filters.get(dimension.fieldname):
if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"):
filters[dimension.fieldname] = get_dimension_with_children(
dimension.document_type, filters.get(dimension.fieldname)
)
vals = (
filters[dimension.fieldname]
if isinstance(filters[dimension.fieldname], list)
else [filters[dimension.fieldname]]
)
conditions.append(f"{dimension.fieldname} IN ({', '.join(['?'] * len(vals))})")
params.extend(vals)
return conditions, params

View File

@@ -4,13 +4,20 @@
"columns": [],
"creation": "2014-07-18 11:43:33.173207",
"default_print_format": "P&L Statement Standard",
"disable_prepared_report_automation": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"doctype_to_sync": [
{
"doc_type": "GL Entry"
}
],
"filters": [],
"generate_csv": 0,
"idx": 2,
"is_standard": "Yes",
"modified": "2026-05-22 14:36:04.544347",
"modified": "2026-06-22 13:38:15.898375",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Profit and Loss Statement",
@@ -30,5 +37,6 @@
"role": "Auditor"
}
],
"synced_report": 0,
"timeout": 0
}

View File

@@ -11,12 +11,20 @@ from erpnext.accounts.doctype.financial_report_template.financial_report_engine
get_xlsx_styles, #! DO NOT REMOVE - hook for styling
)
from erpnext.accounts.report.financial_statements import (
accumulate_values_into_parents,
add_total_row,
calculate_values,
compute_growth_view_data,
compute_margin_view_data,
filter_accounts,
filter_out_zero_value_rows,
get_accounts,
get_appropriate_currency,
get_columns,
get_data,
get_filtered_list_for_consolidated_report,
get_period_list,
prepare_data,
)
@@ -197,3 +205,125 @@ def get_chart_data(filters, chart_columns, income, expense, net_profit_loss, cur
chart["currency"] = currency
return chart
def execute_synced_report(filters):
from frappe.database.duckdb.database import get_latest_sync
if not (conn := get_latest_sync("GL Entry")):
frappe.throw(
_("Profit and Loss Statement requires {0} to be synced to DuckDB").format(frappe.bold("GL Entry"))
)
period_list = get_period_list(
filters.from_fiscal_year,
filters.to_fiscal_year,
filters.period_start_date,
filters.period_end_date,
filters.filter_based_on,
filters.periodicity,
company=filters.company,
)
income = _get_data_duckdb(conn, filters, "Income", "Credit", period_list)
expense = _get_data_duckdb(conn, filters, "Expense", "Debit", period_list)
net_profit_loss = get_net_profit_loss(
income, expense, period_list, filters.company, filters.presentation_currency
)
data = []
data.extend(income or [])
data.extend(expense or [])
if net_profit_loss:
data.append(net_profit_loss)
columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company)
currency = filters.presentation_currency or frappe.get_cached_value(
"Company", filters.company, "default_currency"
)
chart = get_chart_data(filters, period_list, income, expense, net_profit_loss, currency)
report_summary, primitive_summary = get_report_summary(
period_list, filters.periodicity, income, expense, net_profit_loss, currency, filters
)
if filters.get("selected_view") == "Growth":
compute_growth_view_data(data, period_list)
if filters.get("selected_view") == "Margin":
compute_margin_view_data(data, period_list, filters.accumulated_values)
return columns, data, None, chart, report_summary, primitive_summary
def _get_data_duckdb(conn, filters, root_type, balance_must_be, period_list):
accounts = get_accounts(filters.company, root_type)
if not accounts:
return None
accounts, accounts_by_name, parent_children_map = filter_accounts(accounts)
company_currency = get_appropriate_currency(filters.company, filters)
gl_entries_by_account = {}
_load_gl_entries_duckdb(conn, filters, period_list, accounts, gl_entries_by_account)
calculate_values(
accounts_by_name,
gl_entries_by_account,
period_list,
filters.accumulated_values,
False,
)
accumulate_values_into_parents(accounts, accounts_by_name, period_list)
out = prepare_data(
accounts,
balance_must_be,
period_list,
company_currency,
accumulated_values=filters.accumulated_values,
)
out = filter_out_zero_value_rows(out, parent_children_map, filters.show_zero_values)
if out:
add_total_row(out, root_type, balance_must_be, period_list, company_currency)
return out
def _load_gl_entries_duckdb(conn, filters, period_list, accounts, gl_entries_by_account):
from erpnext.accounts.report.trial_balance.trial_balance import (
_extra_gl_conditions,
_fetch_gl_rows_duckdb,
)
from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency
company = filters.company
leaf_accounts = [acc.name for acc in accounts if not acc.is_group]
if not leaf_accounts:
return
extra_cond, extra_params = _extra_gl_conditions(filters)
account_placeholders = ", ".join(["?"] * len(leaf_accounts))
base_conds = [
"company = ?",
"is_cancelled = 0",
f"account IN ({account_placeholders})",
"voucher_type != 'Period Closing Voucher'",
]
base_params = [company, *leaf_accounts]
base_conds.extend(extra_cond)
base_params.extend(extra_params)
for period in period_list:
period_conds = [*base_conds, "posting_date >= ?", "posting_date <= ?"]
period_params = [*base_params, period.from_date, period.to_date]
period_entries = _fetch_gl_rows_duckdb(conn, period_conds, period_params)
if filters.get("presentation_currency"):
convert_to_presentation_currency(period_entries, get_currency(filters))
for entry in period_entries:
entry.posting_date = period.to_date
gl_entries_by_account.setdefault(entry.account, []).append(entry)

View File

@@ -4,13 +4,20 @@
"columns": [],
"creation": "2014-07-22 11:41:23.743564",
"default_print_format": "Trial Balance Standard",
"disable_prepared_report_automation": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"doctype_to_sync": [
{
"doc_type": "GL Entry"
}
],
"filters": [],
"idx": 2,
"generate_csv": 0,
"idx": 4,
"is_standard": "Yes",
"modified": "2026-05-22 14:35:44.889062",
"modified": "2026-06-22 13:38:42.740436",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Trial Balance",
@@ -30,5 +37,6 @@
"role": "Auditor"
}
],
"synced_report": 0,
"timeout": 0
}

View File

@@ -581,3 +581,215 @@ def hide_group_accounts(data):
d.update(indent=0)
non_group_accounts_data.append(d)
return non_group_accounts_data
def execute_synced_report(filters):
from frappe.database.duckdb.database import get_latest_sync
if conn := get_latest_sync("GL Entry"):
validate_filters(filters)
columns = get_columns()
data = get_data_duckdb(filters, conn)
return columns, data
else:
frappe.throw(_("Trial Balance requires {0} to be synced to DuckDB").format(frappe.bold("GL Entry")))
def get_data_duckdb(filters, conn):
# accounts and all metadata via frappe.db — only GL Entry comes from DuckDB
accounts = frappe.db.sql(
"""select name, account_number, parent_account, account_name, root_type, report_type, is_group, lft, rgt
from `tabAccount` where company=%s order by lft""",
filters.company,
as_dict=True,
)
if not accounts:
return None
company_currency = filters.presentation_currency or erpnext.get_company_currency(filters.company)
ignore_is_opening = frappe.get_single_value("Accounts Settings", "ignore_is_opening_check_for_reporting")
accounts, accounts_by_name, parent_children_map = filter_accounts(accounts)
gl_entries_by_account = get_period_gl_entries_duckdb(conn, filters, ignore_is_opening)
opening_balances = get_opening_balances_duckdb(conn, filters, ignore_is_opening)
calculate_values(
accounts,
gl_entries_by_account,
opening_balances,
filters.get("show_net_values"),
ignore_is_opening=ignore_is_opening,
)
accumulate_values_into_parents(accounts, accounts_by_name)
data = prepare_data(accounts, filters, parent_children_map, company_currency)
return filter_out_zero_value_rows(
data, parent_children_map, show_zero_values=filters.get("show_zero_values")
)
def _extra_gl_conditions(filters):
"""Returns (conditions, params) for optional shared GL Entry filters."""
conditions, params = [], []
if filters.get("cost_center"):
cc = get_cost_centers_with_children(filters.get("cost_center"))
conditions.append(f"cost_center IN ({', '.join(['?'] * len(cc))})")
params.extend(cc)
if filters.get("project"):
proj = filters.project if isinstance(filters.project, list) else [filters.project]
conditions.append(f"project IN ({', '.join(['?'] * len(proj))})")
params.extend(proj)
if frappe.db.count("Finance Book"):
company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book")
if filters.get("include_default_book_entries"):
if filters.get("finance_book") and company_fb and cstr(filters.finance_book) != cstr(company_fb):
frappe.throw(
_("To use a different finance book, please uncheck 'Include Default FB Entries'")
)
fb_list = [cstr(filters.get("finance_book")), cstr(company_fb), ""]
else:
fb_list = [cstr(filters.get("finance_book")), ""]
conditions.append(f"(finance_book IN ({', '.join(['?'] * len(fb_list))}) OR finance_book IS NULL)")
params.extend(fb_list)
for dim in get_accounting_dimensions(as_list=False):
if filters.get(dim.fieldname):
if frappe.get_cached_value("DocType", dim.document_type, "is_tree"):
filters[dim.fieldname] = get_dimension_with_children(
dim.document_type, filters.get(dim.fieldname)
)
vals = (
filters[dim.fieldname]
if isinstance(filters[dim.fieldname], list)
else [filters[dim.fieldname]]
)
conditions.append(f"{dim.fieldname} IN ({', '.join(['?'] * len(vals))})")
params.extend(vals)
return conditions, params
def _fetch_gl_rows_duckdb(conn, conditions, params):
cols = [
"account",
"debit",
"credit",
"debit_in_account_currency",
"credit_in_account_currency",
"account_currency",
]
sql = f"""SELECT account, SUM(debit), SUM(credit),
SUM(debit_in_account_currency), SUM(credit_in_account_currency), account_currency
FROM "tabGL Entry" WHERE {" AND ".join(conditions)}
GROUP BY account, account_currency"""
return [frappe._dict(zip(cols, row, strict=False)) for row in conn.execute(sql, params).fetchall()]
def get_period_gl_entries_duckdb(conn, filters, ignore_is_opening):
conditions = ["company = ?", "is_cancelled = 0", "posting_date >= ?", "posting_date <= ?"]
params = [filters.company, filters.from_date, filters.to_date]
if not ignore_is_opening:
conditions.append("is_opening = 'No'")
if not flt(filters.get("with_period_closing_entry_for_current_period")):
conditions.append("voucher_type != 'Period Closing Voucher'")
extra_cond, extra_params = _extra_gl_conditions(filters)
conditions.extend(extra_cond)
params.extend(extra_params)
entries = _fetch_gl_rows_duckdb(conn, conditions, params)
if filters.get("presentation_currency"):
convert_to_presentation_currency(entries, get_currency(filters))
gl_entries_by_account = {}
for entry in entries:
gl_entries_by_account.setdefault(entry.account, []).append(entry)
return gl_entries_by_account
def get_opening_balances_duckdb(conn, filters, ignore_is_opening):
bs = _get_rootwise_opening_duckdb(conn, filters, "Balance Sheet", ignore_is_opening)
pl = _get_rootwise_opening_duckdb(conn, filters, "Profit and Loss", ignore_is_opening)
bs.update(pl)
return bs
def _get_rootwise_opening_duckdb(conn, filters, report_type, ignore_is_opening):
accounting_dimensions = get_accounting_dimensions(as_list=False)
ignore_closing_balances = frappe.get_single_value("Accounts Settings", "ignore_account_closing_balance")
last_pcv = ""
if not ignore_closing_balances:
last_pcv = frappe.db.get_all(
"Period Closing Voucher",
filters={"docstatus": 1, "company": filters.company, "period_end_date": ("<", filters.from_date)},
fields=["period_end_date", "name"],
order_by="period_end_date desc",
limit=1,
)
if last_pcv:
# Account Closing Balance fetched via frappe (not GL Entry)
gle = get_opening_balance(
"Account Closing Balance",
filters,
report_type,
accounting_dimensions,
period_closing_voucher=last_pcv[0].name,
ignore_is_opening=ignore_is_opening,
)
if getdate(last_pcv[0].period_end_date) < getdate(add_days(filters.from_date, -1)):
start_date = add_days(last_pcv[0].period_end_date, 1)
gle += _get_gl_entry_opening_duckdb(
conn, filters, report_type, ignore_is_opening, start_date=start_date
)
else:
gle = _get_gl_entry_opening_duckdb(conn, filters, report_type, ignore_is_opening)
opening = frappe._dict()
for d in gle:
opening.setdefault(d.account, {"account": d.account, "opening_debit": 0.0, "opening_credit": 0.0})
opening[d.account]["opening_debit"] += flt(d.debit)
opening[d.account]["opening_credit"] += flt(d.credit)
return opening
def _get_gl_entry_opening_duckdb(conn, filters, report_type, ignore_is_opening, start_date=None):
accounts = frappe.db.get_all("Account", filters={"report_type": report_type}, pluck="name")
if not accounts:
return []
conditions = ["company = ?", f"account IN ({', '.join(['?'] * len(accounts))})", "is_cancelled = 0"]
params = [filters.company, *accounts]
if start_date:
conditions.append("posting_date >= ? AND posting_date < ?")
params.extend([start_date, filters.from_date])
if not ignore_is_opening:
conditions.append("is_opening = 'No'")
elif not ignore_is_opening:
conditions.append("(posting_date < ? OR is_opening = 'Yes')")
params.append(filters.from_date)
else:
conditions.append("posting_date < ?")
params.append(filters.from_date)
if not filters.get("show_unclosed_fy_pl_balances") and report_type == "Profit and Loss":
conditions.append("posting_date >= ?")
params.append(filters.year_start_date)
if not flt(filters.get("with_period_closing_entry_for_opening")):
conditions.append("voucher_type != 'Period Closing Voucher'")
extra_cond, extra_params = _extra_gl_conditions(filters)
conditions.extend(extra_cond)
params.extend(extra_params)
gle = _fetch_gl_rows_duckdb(conn, conditions, params)
if filters.get("presentation_currency"):
convert_to_presentation_currency(gle, get_currency(filters))
return gle

View File

@@ -30,7 +30,7 @@ class ChildItemUpdater:
self._ordered_items: dict | None = None
self._purchased_items: dict | None = None
def update(self, trans_items: str) -> None:
def update(self, trans_items: str | list) -> None:
"""Process item additions, edits, and deletions from trans_items JSON."""
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import get_purchased_items
from erpnext.selling.doctype.quotation.mapper import get_ordered_items

View File

@@ -995,8 +995,7 @@ class Asset(AccountsController):
@frappe.whitelist()
def get_depreciation_rate(self, args: str | dict | Document, on_validate: bool = False):
if isinstance(args, str):
args = json.loads(args)
args = frappe.parse_json(args)
rate_field_precision = frappe.get_single_value("System Settings", "float_precision") or 2

View File

@@ -162,8 +162,7 @@ def make_asset_movement(
assets: list[dict] | str,
purpose: str = "Transfer",
):
if isinstance(assets, str):
assets = json.loads(assets)
assets = frappe.parse_json(assets)
if len(assets) == 0:
frappe.throw(_("At least one asset has to be selected."))

View File

@@ -669,8 +669,7 @@ def get_service_item_details(ctx: ItemDetailsCtx) -> frappe._dict:
@frappe.whitelist()
def get_items_tagged_to_wip_composite_asset(params: dict | str):
if isinstance(params, str):
params = json.loads(params)
params = frappe.parse_json(params)
fields = [
"item_code",

View File

@@ -27,8 +27,7 @@ def make_purchase_receipt(
):
if args is None:
args = {}
if isinstance(args, str):
args = json.loads(args)
args = frappe.parse_json(args)
has_unit_price_items = frappe.db.get_value("Purchase Order", source_name, "has_unit_price_items")
@@ -123,8 +122,7 @@ def make_purchase_invoice_from_portal(purchase_order_name: str):
def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions=False, args=None):
if args is None:
args = {}
if isinstance(args, str):
args = json.loads(args)
args = frappe.parse_json(args)
def postprocess(source, target):
target.flags.ignore_permissions = ignore_permissions
@@ -294,7 +292,7 @@ def get_mapped_subcontracting_order(source_name: str, target_doc: str | Document
) or frappe.get_value("Production Plan", target_doc.production_plan, "reserve_stock")
if target_doc and isinstance(target_doc, str):
target_doc = json.loads(target_doc)
target_doc = frappe.parse_json(target_doc)
for key in ["service_items", "items", "supplied_items"]:
if key in target_doc:
del target_doc[key]

View File

@@ -549,11 +549,11 @@ def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=
@frappe.whitelist()
def close_or_unclose_purchase_orders(names: str, status: str):
def close_or_unclose_purchase_orders(names: str | list, status: str):
if not frappe.has_permission("Purchase Order", "write"):
frappe.throw(_("Not permitted"), frappe.PermissionError)
names = json.loads(names)
names = frappe.parse_json(names)
for name in names:
po = frappe.get_lazy_doc("Purchase Order", name)
if po.docstatus == 1:

View File

@@ -57,8 +57,7 @@ def make_supplier_quotation_from_rfq(
# This method is used to make supplier quotation from supplier's portal.
@frappe.whitelist()
def create_supplier_quotation(doc: str | Document | dict):
if isinstance(doc, str):
doc = json.loads(doc)
doc = frappe.parse_json(doc)
if frappe.session.user not in frappe.get_all(
"Portal User", {"parent": doc.get("supplier")}, pluck="user"

View File

@@ -15,8 +15,7 @@ def make_purchase_order(
):
if args is None:
args = {}
if isinstance(args, str):
args = json.loads(args)
args = frappe.parse_json(args)
def set_missing_values(source, target):
target.run_method("set_missing_values")

View File

@@ -124,12 +124,12 @@ def check_on_hold_or_closed_status(doctype, docname) -> None:
@frappe.whitelist()
def get_linked_material_requests(items: str):
def get_linked_material_requests(items: str | list):
"""
Retrieve Material Requests linked to a list of items.
"""
items = json.loads(items)
items = frappe.parse_json(items)
mr_list = []
mr = frappe.qb.DocType("Material Request")

View File

@@ -45,8 +45,7 @@ def get_variant(
if item_template.variant_based_on == "Manufacturer" and manufacturer:
return make_variant_based_on_manufacturer(item_template, manufacturer, manufacturer_part_no)
if isinstance(args, str):
args = json.loads(args)
args = frappe.parse_json(args)
attribute_args = {k: v for k, v in args.items() if k != "use_template_image"}
if not attribute_args:
@@ -258,8 +257,7 @@ def find_variant(template, args, variant_item_code=None):
@frappe.whitelist()
def create_variant(item: str, args: dict | str, use_template_image: bool = False):
use_template_image = frappe.parse_json(use_template_image)
if isinstance(args, str):
args = json.loads(args)
args = frappe.parse_json(args)
template = frappe.get_doc("Item", item)
variant = frappe.new_doc("Item")
@@ -286,10 +284,7 @@ def create_variant(item: str, args: dict | str, use_template_image: bool = False
def enqueue_multiple_variant_creation(item: str, args: dict | str, use_template_image: bool = False):
use_template_image = frappe.parse_json(use_template_image)
# There can be innumerable attribute combinations, enqueue
if isinstance(args, str):
variants = json.loads(args)
else:
variants = args
variants = frappe.parse_json(args)
variants = {key: values for key, values in variants.items() if values}
if not variants:
frappe.throw(_("Please select at least one attribute value"))
@@ -315,8 +310,7 @@ def enqueue_multiple_variant_creation(item: str, args: dict | str, use_template_
def create_multiple_variants(item, args, use_template_image=False):
count = 0
if isinstance(args, str):
args = json.loads(args)
args = frappe.parse_json(args)
args = {key: values for key, values in args.items() if values}
template_item = frappe.get_doc("Item", item)
@@ -483,7 +477,7 @@ def make_variant_item_code(template_item_code, template_item_name, variant):
@frappe.whitelist()
def create_variant_doc_for_quick_entry(template: str, args: dict | str):
variant_based_on = frappe.db.get_value("Item", template, "variant_based_on")
args = json.loads(args)
args = frappe.parse_json(args)
if variant_based_on == "Manufacturer":
variant = get_variant(template, **args)
else:

View File

@@ -213,8 +213,7 @@ def item_query(
"""
doctype = "Item"
if isinstance(filters, str):
filters = json.loads(filters)
filters = frappe.parse_json(filters)
if filters and isinstance(filters, dict):
if filters.get("customer") or filters.get("supplier"):

View File

@@ -625,8 +625,7 @@ def repost_required_for_queue(doc: StockController) -> bool:
def check_item_quality_inspection(doctype: str, docstatus: str | int, items: str | list[dict]):
from erpnext.stock.services.quality_inspection_service import INSPECTION_FIELDNAME_MAP
if isinstance(items, str):
items = json.loads(items)
items = frappe.parse_json(items)
inspection_fieldname = INSPECTION_FIELDNAME_MAP.get(doctype)
if inspection_fieldname is None:
@@ -658,8 +657,7 @@ def check_item_quality_inspection(doctype: str, docstatus: str | int, items: str
def make_quality_inspections(
company: str, doctype: str, docname: str, items: str | list, inspection_type: str
):
if isinstance(items, str):
items = json.loads(items)
items = frappe.parse_json(items)
inspections = []
for item in items:

View File

@@ -169,7 +169,7 @@ class calculate_taxes_and_totals:
return
if not self.discount_amount_applied:
do_not_round_fields = ["valuation_rate", "incoming_rate"]
do_not_round_fields = ["valuation_rate", "incoming_rate", "sales_incoming_rate"]
for item in self.doc.items:
self.doc.round_floats_in(item, do_not_round_fields=do_not_round_fields)
@@ -342,7 +342,7 @@ class calculate_taxes_and_totals:
self._set_in_company_currency(item, ["net_rate", "net_amount"])
def _load_item_tax_rate(self, item_tax_rate):
return json.loads(item_tax_rate) if item_tax_rate else {}
return frappe.parse_json(item_tax_rate) if item_tax_rate else {}
def get_current_tax_fraction(self, tax, item_tax_map):
"""

View File

@@ -35,8 +35,7 @@ class ContractTemplate(Document):
@frappe.whitelist()
def get_contract_template(template_name: str, doc: str | dict | Document):
if isinstance(doc, str):
doc = json.loads(doc)
doc = frappe.parse_json(doc)
contract_template = frappe.get_doc("Contract Template", template_name)
contract_terms = None

View File

@@ -391,7 +391,7 @@ def get_item_details(item_code: str):
@frappe.whitelist()
def set_multiple_status(names: str | list[str], status: str):
names = json.loads(names)
names = frappe.parse_json(names)
for name in names:
opp = frappe.get_doc("Opportunity", name)
opp.status = status

View File

@@ -33,7 +33,7 @@ def create_prospect_against_crm_deal():
pass
if doc.contacts and len(doc.contacts):
create_contacts(json.loads(doc.contacts), prospect.company_name, "Prospect", prospect_name)
create_contacts(frappe.parse_json(doc.contacts), prospect.company_name, "Prospect", prospect_name)
create_address("Prospect", prospect_name, doc.address)
frappe.response["message"] = prospect_name
@@ -69,8 +69,7 @@ def create_contacts(contacts, organization=None, link_doctype=None, link_docname
def create_address(doctype, docname, address):
if not address:
return
if isinstance(address, str):
address = json.loads(address)
address = frappe.parse_json(address)
try:
_address = frappe.db.exists("Address", address.get("name"))
if not _address:
@@ -153,7 +152,7 @@ def create_customer(customer_data: dict | None = None):
customer.insert(ignore_permissions=True)
customer_name = customer.name
contacts = json.loads(customer_data.get("contacts"))
contacts = frappe.parse_json(customer_data.get("contacts"))
create_contacts(contacts, customer_name, "Customer", customer_name)
create_address("Customer", customer_name, customer_data.get("address"))
return customer_name

View File

@@ -156,13 +156,15 @@ def process_genericode_import(
code_column: str,
title_column: str | None = None,
description_column: str | None = None,
filters: str | None = None,
filters: str | dict | None = None,
):
from erpnext.edi.doctype.common_code.common_code import import_genericode
column_map = {"code": code_column, "title": title_column, "description": description_column}
return import_genericode(code_list_name, file_name, column_map, json.loads(filters) if filters else None)
return import_genericode(
code_list_name, file_name, column_map, frappe.parse_json(filters) if filters else None
)
def get_genericode_columns_and_examples(root):

View File

@@ -51,8 +51,8 @@ def get_plaid_configuration():
@frappe.whitelist()
def add_institution(token: str, response: str):
response = json.loads(response)
def add_institution(token: str, response: str | dict):
response = frappe.parse_json(response)
plaid = PlaidConnector()
access_token = plaid.get_access_token(token)
@@ -80,13 +80,8 @@ def add_institution(token: str, response: str):
@frappe.whitelist()
def add_bank_accounts(response: str | dict, bank: str | dict, company: str):
try:
response = json.loads(response)
except TypeError:
pass
if isinstance(bank, str):
bank = json.loads(bank)
response = frappe.parse_json(response)
bank = frappe.parse_json(bank)
result = []
parent_gl_account = frappe.db.get_all(
@@ -358,8 +353,8 @@ def get_company(bank_account_name):
@frappe.whitelist()
def update_bank_account_ids(response: str):
data = json.loads(response)
def update_bank_account_ids(response: str | dict):
data = frappe.parse_json(response)
institution_name = data["institution"]["name"]
bank = frappe.get_doc("Bank", institution_name).as_dict()
bank_account_name = f"{data['account']['name']} - {institution_name}"

View File

@@ -712,6 +712,10 @@ default_log_clearing_doctypes = {
export_python_type_annotations = True
# Send non-GET requests for ERPNext's endpoints as native `application/json`
# bodies instead of form-encoded, per-key JSON-stringified values.
use_json_request_body = True
fields_for_group_similar_items = ["qty", "amount"]
# Translation

View File

@@ -585,7 +585,7 @@ class BOM(WebsiteGenerator):
if isinstance(kwargs, str):
import json
kwargs = json.loads(kwargs)
kwargs = frappe.parse_json(kwargs)
return kwargs

View File

@@ -32,8 +32,7 @@ class BOMUpdateTool(Document):
def enqueue_replace_bom(boms: dict | str | None = None, args: dict | str | None = None) -> "BOMUpdateLog":
"""Returns a BOM Update Log (that queues a job) for BOM Replacement."""
boms = boms or args
if isinstance(boms, str):
boms = json.loads(boms)
boms = frappe.parse_json(boms)
update_log = create_bom_update_log(boms=boms)
return update_log

View File

@@ -1685,8 +1685,7 @@ class JobCard(Document):
@frappe.whitelist()
def make_time_log(kwargs: str | dict):
if isinstance(kwargs, str):
kwargs = json.loads(kwargs)
kwargs = frappe.parse_json(kwargs)
kwargs = frappe._dict(kwargs)
doc = frappe.get_doc("Job Card", kwargs.job_card_id)
@@ -1761,8 +1760,7 @@ def get_job_card_filter_conditions(jc, filters):
Replaces the previous raw SQL ``get_filters_cond`` based filtering so that all
user supplied values are passed as bound parameters via the query builder.
"""
if isinstance(filters, str):
filters = json.loads(filters)
filters = frappe.parse_json(filters)
if not filters:
return []

View File

@@ -159,8 +159,7 @@ def get_items_for_material_requests(
def _normalize_mr_doc(doc):
if isinstance(doc, str):
doc = frappe._dict(json.loads(doc))
doc = frappe._dict(frappe.parse_json(doc))
return doc

View File

@@ -24,8 +24,7 @@ def get_bin_details(
):
frappe.has_permission("Production Plan", "read", throw=True)
if isinstance(row, str):
row = frappe._dict(json.loads(row))
row = frappe._dict(frappe.parse_json(row))
bin = frappe.qb.DocType("Bin")
subquery = _bin_warehouse_subquery(bin, company, row, for_warehouse, all_warehouse)
@@ -65,8 +64,7 @@ def _bin_qty_columns(bin):
def get_warehouse_list(warehouses):
warehouse_list = []
if isinstance(warehouses, str):
warehouses = json.loads(warehouses)
warehouses = frappe.parse_json(warehouses)
for row in warehouses:
child_warehouses = frappe.db.get_descendants("Warehouse", row.get("warehouse"))

View File

@@ -148,8 +148,7 @@ def _new_work_order(item, bom_no, company, item_details, use_multi_level_bom):
def add_variant_item(variant_items, wo_doc, bom_no, table_name="items"):
if isinstance(variant_items, str):
variant_items = json.loads(variant_items)
variant_items = frappe.parse_json(variant_items)
for item in variant_items:
_add_variant_row(item, wo_doc, bom_no, table_name)
@@ -289,8 +288,7 @@ def _set_stock_entry_warehouses(stock_entry, work_order, purpose, target_warehou
def make_job_card(work_order: str, operations: str | list, parent_bom: str | None = None):
frappe.has_permission("Job Card", "create", throw=True)
if isinstance(operations, str):
operations = json.loads(operations)
operations = frappe.parse_json(operations)
work_order = frappe.get_doc("Work Order", work_order)
for row in operations:
@@ -469,10 +467,10 @@ def get_work_order_operation_data(work_order, operation, workstation):
@frappe.whitelist()
def create_pick_list(source_name: str, target_doc: str | None = None, for_qty: float | None = None):
def create_pick_list(source_name: str, target_doc: str | dict | None = None, for_qty: float | None = None):
frappe.has_permission("Pick List", "create", throw=True)
for_qty = for_qty or json.loads(target_doc).get("for_qty")
for_qty = for_qty or frappe.parse_json(target_doc).get("for_qty")
max_finished_goods_qty = frappe.db.get_value("Work Order", source_name, "qty")
postprocess = partial(
_set_pick_list_item_qty, for_qty=for_qty, max_finished_goods_qty=max_finished_goods_qty

View File

@@ -491,3 +491,4 @@ erpnext.patches.v16_0.migrate_subscription_generate_invoice_at
erpnext.patches.v16_0.rename_subscription_billing_period_fields
erpnext.patches.v16_0.drop_redundant_serial_no_index_from_sabb
erpnext.patches.v16_0.set_default_close_opportunity_after_days
execute:frappe.db.set_single_value("Accounts Settings", "pcv_job_timeout", 3600)

View File

@@ -629,11 +629,11 @@ def allow_to_make_project_update(project, time, frequency):
@frappe.whitelist()
def create_duplicate_project(prev_doc: str, project_name: str):
def create_duplicate_project(prev_doc: str | dict, project_name: str):
"""Create duplicate project based on the old project"""
import json
prev_doc = json.loads(prev_doc)
prev_doc = frappe.parse_json(prev_doc)
if project_name == prev_doc.get("name"):
frappe.throw(_("Use a name that is different from previous project name"))

View File

@@ -363,8 +363,8 @@ def get_project(doctype: str, txt: str, searchfield: str, start: int, page_len:
@frappe.whitelist()
def set_multiple_status(names: str, status: str):
names = json.loads(names)
def set_multiple_status(names: str | list, status: str):
names = frappe.parse_json(names)
for name in names:
task = frappe.get_doc("Task", name)
task.status = status
@@ -459,8 +459,8 @@ def add_node():
@frappe.whitelist()
def add_multiple_tasks(data: str, parent: str):
data = json.loads(data)
def add_multiple_tasks(data: str | list, parent: str):
data = frappe.parse_json(data)
new_doc = {"doctype": "Task", "parent_task": parent if parent != "All Tasks" else ""}
new_doc["project"] = frappe.db.get_value("Task", {"name": parent}, "project") or ""

View File

@@ -497,7 +497,7 @@ def get_activity_cost(
@frappe.whitelist()
def get_events(start: str, end: str, filters: str | None = None):
def get_events(start: str, end: str, filters: str | list | dict | None = None):
"""Returns events for Gantt / Calendar view rendering.
:param start: Start date-time.
:param end: End date-time.
@@ -505,7 +505,7 @@ def get_events(start: str, end: str, filters: str | None = None):
"""
from erpnext.utilities.query import get_event_conditions_qb
filters = json.loads(filters) if filters else {}
filters = frappe.parse_json(filters) if filters else {}
tsd = frappe.qb.DocType("Timesheet Detail")
ts = frappe.qb.DocType("Timesheet")

View File

@@ -104,7 +104,7 @@ def prepare_invoice(invoice, progressive_number):
def get_conditions(filters):
filters = json.loads(filters)
filters = frappe.parse_json(filters)
conditions = {"docstatus": 1, "company_tax_id": ("!=", "")}

View File

@@ -84,7 +84,7 @@ def get_columns():
@frappe.whitelist()
def irs_1099_print(filters: str):
def irs_1099_print(filters: str | dict):
if not filters:
frappe._dict(
{
@@ -93,7 +93,7 @@ def irs_1099_print(filters: str):
}
)
else:
filters = frappe._dict(json.loads(filters))
filters = frappe._dict(frappe.parse_json(filters))
fiscal_year_doc = get_fiscal_year(fiscal_year=filters.fiscal_year, as_dict=True)
fiscal_year = cstr(fiscal_year_doc.year_start_date.year)

View File

@@ -559,8 +559,7 @@ def check_credit_limit(customer, company, ignore_outstanding_sales_order=False,
def send_emails(
customer: str, customer_outstanding: float, credit_limit: float, credit_controller_users_list: str | list
):
if isinstance(credit_controller_users_list, str):
credit_controller_users_list = json.loads(credit_controller_users_list)
credit_controller_users_list = frappe.parse_json(credit_controller_users_list)
subject = _("Credit limit reached for customer {0}").format(customer)
message = _("Credit limit has been crossed for customer {0} ({1}/{2})").format(
customer, customer_outstanding, credit_limit

View File

@@ -31,8 +31,7 @@ def make_sales_order(
def _make_sales_order(source_name, target_doc=None, ignore_permissions=False, args=None):
if args is None:
args = {}
if isinstance(args, str):
args = json.loads(args)
args = frappe.parse_json(args)
customer = _make_customer(source_name, ignore_permissions)
ordered_items = get_ordered_items(source_name)
@@ -151,8 +150,7 @@ def make_sales_invoice(
def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, args=None):
if args is None:
args = {}
if isinstance(args, str):
args = json.loads(args)
args = frappe.parse_json(args)
customer = _make_customer(source_name, ignore_permissions)

View File

@@ -430,8 +430,7 @@ def make_sales_invoice(
):
if args is None:
args = {}
if isinstance(args, str):
args = json.loads(args)
args = frappe.parse_json(args)
# 0 qty is accepted, as the qty is uncertain for some items
has_unit_price_items = frappe.db.get_value("Sales Order", source_name, "has_unit_price_items")
@@ -675,8 +674,7 @@ def make_purchase_order(
if not selected_items:
return
if isinstance(selected_items, str):
selected_items = json.loads(selected_items)
selected_items = frappe.parse_json(selected_items)
def set_missing_values(source, target):
target.supplier = supplier
@@ -843,9 +841,9 @@ def set_delivery_date(items: list, sales_order: str) -> None:
@frappe.whitelist()
def make_work_orders(items: str, sales_order: str, company: str, project: str | None = None):
def make_work_orders(items: str | dict, sales_order: str, company: str, project: str | None = None):
"""Make Work Orders against the given Sales Order for the given `items`"""
items = json.loads(items).get("items")
items = frappe.parse_json(items).get("items")
out = []
for i in items:
@@ -912,8 +910,7 @@ def make_raw_material_request(
if not frappe.has_permission("Sales Order", "write"):
frappe.throw(_("Not permitted"), frappe.PermissionError)
if isinstance(items, str):
items = frappe._dict(json.loads(items))
items = frappe._dict(frappe.parse_json(items))
for item in items.get("items"):
item["include_exploded_items"] = items.get("include_exploded_items")
@@ -1089,7 +1086,7 @@ def get_mapped_subcontracting_inward_order(
target_doc.populate_items_table()
if target_doc and isinstance(target_doc, str):
target_doc = json.loads(target_doc)
target_doc = frappe.parse_json(target_doc)
for key in ["service_items", "items", "received_items"]:
if key in target_doc:
del target_doc[key]

View File

@@ -711,11 +711,11 @@ def is_enable_cutoff_date_on_bulk_delivery_note_creation():
@frappe.whitelist()
def close_or_unclose_sales_orders(names: str, status: str):
def close_or_unclose_sales_orders(names: str | list, status: str):
if not frappe.has_permission("Sales Order", "write"):
frappe.throw(_("Not permitted"), frappe.PermissionError)
names = json.loads(names)
names = frappe.parse_json(names)
for name in names:
so = frappe.get_lazy_doc("Sales Order", name)
if so.docstatus == 1:

View File

@@ -347,8 +347,8 @@ def check_opening_entry(user: str):
@frappe.whitelist()
def create_opening_voucher(pos_profile: str, company: str, balance_details: str):
balance_details = json.loads(balance_details)
def create_opening_voucher(pos_profile: str, company: str, balance_details: str | list):
balance_details = frappe.parse_json(balance_details)
new_pos_opening = frappe.get_doc(
{

View File

@@ -77,8 +77,7 @@ def get_children(
is_root: bool = False,
include_disabled: str | dict | None = None,
):
if isinstance(include_disabled, str):
include_disabled = json.loads(include_disabled)
include_disabled = frappe.parse_json(include_disabled)
fields = ["name as value", "is_group as expandable"]
filters = {}

View File

@@ -176,7 +176,7 @@ def get_events(start: DateTimeLikeObject, end: DateTimeLikeObject, filters: str
:param filters: Filters (JSON).
"""
if filters:
filters = json.loads(filters)
filters = frappe.parse_json(filters)
else:
filters = []

View File

@@ -37,8 +37,7 @@ class TermsandConditions(Document):
@frappe.whitelist()
def get_terms_and_conditions(template_name: str, doc: str | dict):
if isinstance(doc, str):
doc = json.loads(doc)
doc = frappe.parse_json(doc)
terms_and_conditions = frappe.get_doc("Terms and Conditions", template_name)

View File

@@ -318,12 +318,23 @@ class TransactionDeletionRecord(Document):
Returns:
list: List of child table DocType names (Table field options)
"""
return frappe.get_all(
child_tables = frappe.get_all(
"DocField",
filters={"parent": doctype_name, "fieldtype": ["in", ["Table", "Table MultiSelect"]]},
pluck="options",
)
if not child_tables:
return []
child_tables = frappe.get_all(
"DocType",
filters={"name": ["in", child_tables], "is_virtual": 0},
pluck="name",
)
return child_tables
def _get_to_delete_row_infos(self, doctype_name, company_field=None, company=None):
"""Get child tables and document count for a To Delete list row

View File

@@ -404,8 +404,7 @@ def make_batch(kwargs):
def get_pos_reserved_batch_qty(filters: dict | str):
import json
if isinstance(filters, str):
filters = json.loads(filters)
filters = frappe.parse_json(filters)
p = frappe.qb.DocType("POS Invoice").as_("p")
item = frappe.qb.DocType("POS Invoice Item").as_("item")

View File

@@ -66,8 +66,7 @@ def make_sales_invoice(
if args is None:
args = {}
if isinstance(args, str):
args = json.loads(args)
args = frappe.parse_json(args)
doc = frappe.get_doc("Delivery Note", source_name)

View File

@@ -53,8 +53,7 @@ def make_purchase_order(
):
if args is None:
args = {}
if isinstance(args, str):
args = json.loads(args)
args = frappe.parse_json(args)
is_subcontracted = (
frappe.db.get_value("Material Request", source_name, "material_request_type") == "Subcontracting"

View File

@@ -432,7 +432,7 @@ def on_doctype_update():
@frappe.whitelist()
def get_items_from_product_bundle(row: str):
def get_items_from_product_bundle(row: str | dict):
"""Item details for each component of a Product Bundle.
``row.product_bundle`` selects a specific version by document name (the buying
@@ -441,7 +441,7 @@ def get_items_from_product_bundle(row: str):
"""
from erpnext.selling.doctype.product_bundle.product_bundle import get_active_product_bundle
row, items = ItemDetailsCtx(json.loads(row)), []
row, items = ItemDetailsCtx(frappe.parse_json(row)), []
if bundle_name := row.get("product_bundle"):
frappe.has_permission("Product Bundle", "read", bundle_name, throw=True)

View File

@@ -113,8 +113,7 @@ def create_dn_for_pick_lists(
"""Get Items from Multiple Pick Lists and create a Delivery Note for filtered customer"""
if kwargs is None:
kwargs = {}
if isinstance(kwargs, str):
kwargs = json.loads(kwargs)
kwargs = frappe.parse_json(kwargs)
pick_list = frappe.get_doc("Pick List", source_name)
validate_item_locations(pick_list)
@@ -282,8 +281,8 @@ def add_product_bundles_to_target(pick_list, target_doc, item_mapper, sales_orde
@frappe.whitelist()
def create_stock_entry(pick_list: str):
pick_list = frappe.get_doc(json.loads(pick_list))
def create_stock_entry(pick_list: str | dict):
pick_list = frappe.get_doc(frappe.parse_json(pick_list))
validate_item_locations(pick_list)
if stock_entry_exists(pick_list.get("name")):

View File

@@ -60,8 +60,7 @@ def make_purchase_invoice(
):
if args is None:
args = {}
if isinstance(args, str):
args = json.loads(args)
args = frappe.parse_json(args)
from erpnext.accounts.party import get_payment_terms_template

View File

@@ -111,8 +111,7 @@ def apply_putaway_rule(
purpose: Purpose of Stock Entry
sync (optional): Sync with client side only for client side calls
"""
if isinstance(items, str):
items = json.loads(items)
items = frappe.parse_json(items)
items_not_accomodated, updated_table = [], []
item_wise_rules = defaultdict(list)
@@ -198,7 +197,7 @@ def apply_putaway_rule(
frappe.msgprint(_("Applied putaway rules."), alert=True)
return updated_table
if sync and json.loads(sync): # sync with client side
if sync and frappe.parse_json(sync): # sync with client side
return items

View File

@@ -345,6 +345,9 @@ class RepostItemValuation(Document):
def _recalculate_valuation_rate(self):
doc = frappe.get_doc(self.voucher_type, self.voucher_no)
if doc.get("is_internal_supplier"):
doc.set_sales_incoming_rate_for_internal_transfer()
doc.update_valuation_rate()
for item in doc.items:
item.db_set("valuation_rate", item.valuation_rate)
@@ -361,8 +364,8 @@ class RepostItemValuation(Document):
@frappe.whitelist()
def bulk_restart_reposting(names: str):
names = json.loads(names)
def bulk_restart_reposting(names: str | list):
names = frappe.parse_json(names)
for name in names:
doc = frappe.get_doc("Repost Item Valuation", name)
if doc.status != "Failed":

View File

@@ -222,8 +222,7 @@ def auto_fetch_serial_number(
@frappe.whitelist()
def get_pos_reserved_serial_nos(filters: str | dict):
if isinstance(filters, str):
filters = json.loads(filters)
filters = frappe.parse_json(filters)
POSInvoice = frappe.qb.DocType("POS Invoice")
POSInvoiceItem = frappe.qb.DocType("POS Invoice Item")

View File

@@ -1040,8 +1040,7 @@ def ceil_qty_if_uom_has_whole_number(qty, stock_uom):
@frappe.whitelist()
def move_sample_to_retention_warehouse(company: str, items: str | list):
if isinstance(items, str):
items = json.loads(items)
items = frappe.parse_json(items)
retention_warehouse = frappe.get_single_value("Stock Settings", "sample_retention_warehouse")
stock_entry = frappe.new_doc("Stock Entry")

View File

@@ -253,8 +253,7 @@ def get_supplied_items(
def get_items_from_subcontract_order(source_name: str, target_doc: str | Document | None = None):
from erpnext.controllers.subcontracting_controller import make_rm_stock_entry
if isinstance(target_doc, str):
target_doc = frappe.get_doc(json.loads(target_doc))
target_doc = frappe.get_doc(frappe.parse_json(target_doc))
order_doctype = "Purchase Order" if target_doc.purchase_order else "Subcontracting Order"
target_doc = make_rm_stock_entry(

View File

@@ -1683,8 +1683,7 @@ def get_uom_details(item_code: str, uom: str, qty: float | None):
@frappe.whitelist()
def get_warehouse_details(args: str | dict):
if isinstance(args, str):
args = json.loads(args)
args = frappe.parse_json(args)
args = frappe._dict(args)

View File

@@ -1239,8 +1239,7 @@ def get_stock_balance_for(
item_dict = frappe.get_cached_value("Item", item_code, ["has_serial_no", "has_batch_no"], as_dict=1)
if isinstance(row, str):
row = json.loads(row)
row = frappe.parse_json(row)
if isinstance(row, dict):
row = frappe._dict(row)

View File

@@ -176,8 +176,7 @@ def get_children(
if is_root:
parent = ""
if isinstance(include_disabled, str):
include_disabled = json.loads(include_disabled)
include_disabled = frappe.parse_json(include_disabled)
fields = ["name as value", "is_group as expandable"]

View File

@@ -90,8 +90,7 @@ def get_item_details(
item = frappe.get_cached_doc("Item", ctx.item_code)
validate_item_details(ctx, item)
if isinstance(doc, str):
doc = json.loads(doc)
doc = frappe.parse_json(doc)
if doc:
ctx.transaction_date = doc.get("transaction_date") or doc.get("posting_date")

View File

@@ -100,12 +100,12 @@ def get_data(filters=None):
@frappe.whitelist()
def update_batch_qty(selected_batches: str | None = None):
def update_batch_qty(selected_batches: str | list | None = None):
frappe.has_permission("Batch", "write", throw=True, ignore_share_permissions=True)
if not selected_batches:
return
selected_batches = json.loads(selected_batches)
selected_batches = frappe.parse_json(selected_batches)
for row in selected_batches:
batch_name = row.get("batch")

View File

@@ -245,8 +245,7 @@ def get_incoming_rate(args: dict | str, raise_error_if_no_rate: bool = True, fal
"""Get Incoming Rate based on valuation method"""
from erpnext.stock.stock_ledger import get_previous_sle, get_valuation_rate
if isinstance(args, str):
args = json.loads(args)
args = frappe.parse_json(args)
if not args.get("posting_datetime") and args.get("posting_date"):
args["posting_datetime"] = get_combine_datetime(args.get("posting_date"), args.get("posting_time"))

View File

@@ -217,8 +217,8 @@ def get_issue_list(doctype, txt, filters, limit_start, limit_page_length=20, ord
@frappe.whitelist()
def set_multiple_status(names: str, status: str):
for name in json.loads(names):
def set_multiple_status(names: str | list, status: str):
for name in frappe.parse_json(names):
set_status(name, status)

View File

@@ -13,13 +13,9 @@ def transaction_processing(
frappe.has_permission(from_doctype, "read", throw=True)
frappe.has_permission(to_doctype, "create", throw=True)
if isinstance(data, str):
deserialized_data = json.loads(data)
else:
deserialized_data = data
deserialized_data = frappe.parse_json(data)
if isinstance(args, str):
args = frappe._dict(json.loads(args))
args = frappe._dict(frappe.parse_json(args))
skipped_records = [d for d in deserialized_data if d.get("status") in ("On Hold", "Closed")]

View File

@@ -62,8 +62,7 @@ def get_filter_conditions_qb(doctype, filters, ignore_permissions=None):
if isinstance(filters, Criterion):
return [filters]
if isinstance(filters, str):
filters = json.loads(filters)
filters = frappe.parse_json(filters)
if isinstance(filters, dict):
# Mirror get_filters_cond's dict normalization: a string value prefixed with "!" means

View File

@@ -101,7 +101,7 @@ def get_available_slots_between(query_start_time, query_end_time, settings):
@frappe.whitelist(allow_guest=True)
def create_appointment(date: str, time: str, tz: str, contact: str):
def create_appointment(date: str, time: str, tz: str, contact: str | dict):
handle_appointment_booking_disabled()
format_string = "%Y-%m-%d %H:%M:%S"
scheduled_time = datetime.datetime.strptime(date + " " + time, format_string)
@@ -112,7 +112,7 @@ def create_appointment(date: str, time: str, tz: str, contact: str):
# Create a appointment document from form
appointment = frappe.new_doc("Appointment")
appointment.scheduled_time = scheduled_time
contact = json.loads(contact)
contact = frappe.parse_json(contact)
appointment.customer_name = contact.get("name", None)
appointment.customer_phone_number = contact.get("number", None)
appointment.customer_skype = contact.get("skype", None)