mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-04 04:39:11 +00:00
Merge pull request #55547 from frappe/version-16-hotfix
This commit is contained in:
52
.github/helper/merge_po_files.py
vendored
Normal file
52
.github/helper/merge_po_files.py
vendored
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Overlay develop's .po translations onto hotfix's .po files.
|
||||||
|
|
||||||
|
Called by sync_hotfix_translations.sh before `bench update-po-files`.
|
||||||
|
Merge rules:
|
||||||
|
a. msgid absent from develop → keep hotfix's existing msgstr
|
||||||
|
b. language not yet in hotfix → copy file as-is (bench will filter to main.pot)
|
||||||
|
c. msgid present in both → use develop's msgstr
|
||||||
|
"""
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from babel.messages.pofile import read_po, write_po
|
||||||
|
|
||||||
|
DEVELOP = Path("/tmp/develop-po/erpnext/locale/")
|
||||||
|
LOCALE = Path("./apps/erpnext/erpnext/locale/")
|
||||||
|
|
||||||
|
added = updated = 0
|
||||||
|
|
||||||
|
for src in sorted(DEVELOP.glob("*.po")):
|
||||||
|
dst = LOCALE / src.name
|
||||||
|
|
||||||
|
with src.open("rb") as f:
|
||||||
|
dev = read_po(f)
|
||||||
|
|
||||||
|
if not dst.exists():
|
||||||
|
dev.revision_date = datetime.now(timezone.utc)
|
||||||
|
with dst.open("wb") as f:
|
||||||
|
write_po(f, dev)
|
||||||
|
added += 1
|
||||||
|
print(f" [new] {src.name}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
with dst.open("rb") as f:
|
||||||
|
hf = read_po(f)
|
||||||
|
|
||||||
|
changes = 0
|
||||||
|
for msg in hf:
|
||||||
|
if msg.id and msg.id in dev and dev[msg.id].string and dev[msg.id].string != msg.string:
|
||||||
|
msg.string = dev[msg.id].string
|
||||||
|
changes += 1
|
||||||
|
|
||||||
|
if changes:
|
||||||
|
hf.revision_date = datetime.now(timezone.utc)
|
||||||
|
with dst.open("wb") as f:
|
||||||
|
write_po(f, hf)
|
||||||
|
updated += 1
|
||||||
|
print(f" [updated] {src.name} ({changes} msgstr(s) from develop)")
|
||||||
|
else:
|
||||||
|
print(f" [no-op] {src.name}")
|
||||||
|
|
||||||
|
print(f"\n{added} new language(s), {updated} updated.")
|
||||||
121
.github/helper/sync_hotfix_translations.sh
vendored
Normal file
121
.github/helper/sync_hotfix_translations.sh
vendored
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Syncs Crowdin translations from develop to a hotfix branch.
|
||||||
|
# Merge logic: see merge_po_files.py.
|
||||||
|
# Env: GH_TOKEN, PR_REVIEWER, GITHUB_WORKSPACE, APP_NAME, GITHUB_REPOSITORY
|
||||||
|
# (all set by Actions).
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
HOTFIX_BRANCH="${HOTFIX_BRANCH:?HOTFIX_BRANCH env var is required}"
|
||||||
|
APP_NAME="${APP_NAME:?APP_NAME env var is required}"
|
||||||
|
|
||||||
|
cd ~ || exit
|
||||||
|
|
||||||
|
echo "=== Setting up bench ==="
|
||||||
|
pip install frappe-bench
|
||||||
|
bench -v init frappe-bench --skip-assets --skip-redis-config-generation --python "$(which python)"
|
||||||
|
cd ./frappe-bench || exit
|
||||||
|
bench get-app --skip-assets "${APP_NAME}" "${GITHUB_WORKSPACE}"
|
||||||
|
|
||||||
|
echo "=== Setting up sync_translations_${HOTFIX_BRANCH} branch ==="
|
||||||
|
cd "./apps/${APP_NAME}" || exit
|
||||||
|
git config user.email "developers@erpnext.com"
|
||||||
|
git config user.name "frappe-pr-bot"
|
||||||
|
git remote set-url upstream "https://github.com/${GITHUB_REPOSITORY}.git"
|
||||||
|
git config remote.upstream.fetch "+refs/heads/*:refs/remotes/upstream/*"
|
||||||
|
gh auth setup-git
|
||||||
|
git fetch upstream "${HOTFIX_BRANCH}"
|
||||||
|
|
||||||
|
if git ls-remote --exit-code --heads upstream "sync_translations_${HOTFIX_BRANCH}" >/dev/null 2>&1; then
|
||||||
|
git fetch upstream "sync_translations_${HOTFIX_BRANCH}"
|
||||||
|
git checkout -b "sync_translations_${HOTFIX_BRANCH}" "upstream/sync_translations_${HOTFIX_BRANCH}"
|
||||||
|
git merge -X theirs "upstream/${HOTFIX_BRANCH}" --no-edit
|
||||||
|
else
|
||||||
|
git checkout -b "sync_translations_${HOTFIX_BRANCH}" "upstream/${HOTFIX_BRANCH}"
|
||||||
|
fi
|
||||||
|
cd ../.. || exit
|
||||||
|
|
||||||
|
echo "=== Fetching develop's .po files ==="
|
||||||
|
mkdir -p /tmp/develop-po
|
||||||
|
git -C "${GITHUB_WORKSPACE}" fetch origin develop
|
||||||
|
git -C "${GITHUB_WORKSPACE}" archive origin/develop "${APP_NAME}/locale/" \
|
||||||
|
| tar -xf - -C /tmp/develop-po/
|
||||||
|
|
||||||
|
po_count=$(find "/tmp/develop-po/${APP_NAME}/locale" -name "*.po" | wc -l)
|
||||||
|
if [ "${po_count}" -eq 0 ]; then
|
||||||
|
echo "ERROR: No .po files found in develop's archive. Aborting." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Extracted ${po_count} .po file(s) from develop."
|
||||||
|
|
||||||
|
echo "=== Merging and reconciling ==="
|
||||||
|
env/bin/python "${GITHUB_WORKSPACE}/.github/helper/merge_po_files.py"
|
||||||
|
bench update-po-files --app "${APP_NAME}"
|
||||||
|
|
||||||
|
cd "./apps/${APP_NAME}" || exit
|
||||||
|
|
||||||
|
if git diff --quiet "${APP_NAME}/locale/" && [ -z "$(git ls-files --others --exclude-standard "${APP_NAME}/locale/")" ]; then
|
||||||
|
echo "Translations are already up to date. No PR needed."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Changed files:"
|
||||||
|
git diff --name-only "${APP_NAME}/locale/"
|
||||||
|
git ls-files --others --exclude-standard "${APP_NAME}/locale/"
|
||||||
|
|
||||||
|
echo "=== Committing ==="
|
||||||
|
while IFS= read -r file; do
|
||||||
|
git add "${file}"
|
||||||
|
lang=$(basename "${file}" .po)
|
||||||
|
git commit -m "chore: add ${lang} translation to ${HOTFIX_BRANCH}"
|
||||||
|
done < <(git ls-files --others --exclude-standard "${APP_NAME}/locale/" | grep '\.po$' | sort)
|
||||||
|
|
||||||
|
while IFS= read -r file; do
|
||||||
|
git add "${file}"
|
||||||
|
if ! git diff --staged --quiet -- "${file}"; then
|
||||||
|
lang=$(basename "${file}" .po)
|
||||||
|
git commit -m "chore: sync ${lang} translation to ${HOTFIX_BRANCH}"
|
||||||
|
else
|
||||||
|
git restore --staged -- "${file}"
|
||||||
|
fi
|
||||||
|
done < <(git diff --name-only "${APP_NAME}/locale/" | grep '\.po$' | sort)
|
||||||
|
|
||||||
|
if git ls-remote --exit-code --heads upstream "sync_translations_${HOTFIX_BRANCH}" >/dev/null 2>&1; then
|
||||||
|
git fetch upstream "sync_translations_${HOTFIX_BRANCH}"
|
||||||
|
git merge -X ours "upstream/sync_translations_${HOTFIX_BRANCH}" --no-edit
|
||||||
|
fi
|
||||||
|
git push -u upstream sync_translations_${HOTFIX_BRANCH}
|
||||||
|
|
||||||
|
echo "=== Opening PR (if not already open) ==="
|
||||||
|
existing_pr=$(gh pr list \
|
||||||
|
--base "${HOTFIX_BRANCH}" \
|
||||||
|
--head "sync_translations_${HOTFIX_BRANCH}" \
|
||||||
|
--state open \
|
||||||
|
--json number \
|
||||||
|
--jq 'length' \
|
||||||
|
-R "${GITHUB_REPOSITORY}")
|
||||||
|
|
||||||
|
if [ "${existing_pr}" -gt 0 ]; then
|
||||||
|
echo "PR already open — branch updated in place. No new PR needed."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
gh pr create \
|
||||||
|
--base "${HOTFIX_BRANCH}" \
|
||||||
|
--head "sync_translations_${HOTFIX_BRANCH}" \
|
||||||
|
--title "chore: sync translations to ${HOTFIX_BRANCH}" \
|
||||||
|
--body "Automated sync of Crowdin translations from \`develop\` to \`${HOTFIX_BRANCH}\`.
|
||||||
|
|
||||||
|
A 3-way merge is performed per language, then \`bench update-po-files\` reconciles each \`.po\` against hotfix's \`main.pot\`:
|
||||||
|
|
||||||
|
| Case | Condition | Result |
|
||||||
|
|------|-----------|--------|
|
||||||
|
| **a** | \`msgid\` in hotfix's \`main.pot\`, **not** in develop's \`.po\` | Hotfix's existing \`msgstr\` is **preserved** (string removed from develop but still needed in hotfix) |
|
||||||
|
| **b** | \`msgid\` **not** in hotfix's \`main.pot\` | **Dropped** from hotfix's \`.po\` |
|
||||||
|
| **c** | \`msgid\` in both hotfix's \`main.pot\` and develop's \`.po\` | Develop's \`msgstr\` is used (Crowdin translation wins) |
|
||||||
|
|
||||||
|
Generated by the \`sync-hotfix-translations\` workflow." \
|
||||||
|
--label "translation" \
|
||||||
|
--label "skip-release-notes" \
|
||||||
|
--reviewer "${PR_REVIEWER}" \
|
||||||
|
-R "${GITHUB_REPOSITORY}"
|
||||||
70
.github/workflows/build-and-commit-assets.yml
vendored
Normal file
70
.github/workflows/build-and-commit-assets.yml
vendored
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
name: Build and Upload Assets
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- 'version-*'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: build-assets-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-assets:
|
||||||
|
name: Build JS/CSS and upload to release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: frappe/frappe
|
||||||
|
path: apps/frappe
|
||||||
|
ref: ${{ github.ref_name }}
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
path: apps/erpnext
|
||||||
|
|
||||||
|
- name: Create bench structure
|
||||||
|
run: |
|
||||||
|
mkdir -p sites
|
||||||
|
printf "frappe\nerpnext\n" > sites/apps.txt
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
cache: yarn
|
||||||
|
cache-dependency-path: apps/frappe/yarn.lock
|
||||||
|
|
||||||
|
- name: Install frappe JS dependencies
|
||||||
|
working-directory: apps/frappe
|
||||||
|
run: yarn install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Install erpnext JS dependencies
|
||||||
|
working-directory: apps/erpnext
|
||||||
|
run: yarn install --frozen-lockfile --ignore-scripts
|
||||||
|
|
||||||
|
- name: Link node_modules into public/
|
||||||
|
working-directory: apps/frappe
|
||||||
|
run: ln -s "$PWD/node_modules" frappe/public/node_modules
|
||||||
|
|
||||||
|
- name: Build assets (production)
|
||||||
|
working-directory: apps/frappe
|
||||||
|
run: yarn run production
|
||||||
|
|
||||||
|
- name: Package assets
|
||||||
|
working-directory: apps/erpnext
|
||||||
|
run: tar czf erpnext-assets.tar.gz -C ../../sites/assets/erpnext dist
|
||||||
|
|
||||||
|
- name: Upload to rolling release
|
||||||
|
working-directory: apps/erpnext
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
TAG="assets-${GITHUB_REF_NAME//\//-}"
|
||||||
|
gh release create "$TAG" --prerelease --title "Assets: $GITHUB_REF_NAME" --notes "" 2>/dev/null || true
|
||||||
|
gh release upload "$TAG" erpnext-assets.tar.gz --clobber
|
||||||
52
.github/workflows/run-hotfix-translation-sync.yml
vendored
Normal file
52
.github/workflows/run-hotfix-translation-sync.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Runner — maintain this file on each hotfix branch, not on develop.
|
||||||
|
#
|
||||||
|
# Fires when main.pot changes on this branch (i.e. after a POT update PR
|
||||||
|
# merges), or when dispatched by the orchestrator on develop (weekly schedule).
|
||||||
|
#
|
||||||
|
# Uses github.ref_name so the file is identical across all hotfix branches
|
||||||
|
# with no branch-specific edits required.
|
||||||
|
|
||||||
|
name: Run hotfix translation sync
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# One run at a time per branch. cancel-in-progress: false to avoid leaving
|
||||||
|
# an orphaned remote branch from a mid-flight git push + gh pr create.
|
||||||
|
concurrency:
|
||||||
|
group: sync-hotfix-translations-${{ github.ref_name }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync-translations:
|
||||||
|
name: Sync translations from develop into ${{ github.ref_name }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
env:
|
||||||
|
HOTFIX_BRANCH: ${{ github.ref_name }}
|
||||||
|
APP_NAME: ${{ github.event.repository.name }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout ${{ env.HOTFIX_BRANCH }}
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
ref: ${{ env.HOTFIX_BRANCH }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: "3.14"
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
|
||||||
|
- name: Run sync script
|
||||||
|
run: |
|
||||||
|
bash "${GITHUB_WORKSPACE}/.github/helper/sync_hotfix_translations.sh"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
PR_REVIEWER: diptanilsaha
|
||||||
39
.github/workflows/sync-hotfix-translations.yml
vendored
Normal file
39
.github/workflows/sync-hotfix-translations.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Orchestrator — lives on develop only.
|
||||||
|
#
|
||||||
|
# Triggers on the weekly schedule and dispatches the runner workflow on each
|
||||||
|
# hotfix branch listed in the matrix. To add or remove a branch, edit the
|
||||||
|
# matrix below.
|
||||||
|
#
|
||||||
|
# POT-change triggers are handled by the runner on each hotfix branch
|
||||||
|
# (run-hotfix-translation-sync.yml), since GitHub only evaluates a workflow
|
||||||
|
# from the branch that receives the push.
|
||||||
|
|
||||||
|
name: Sync translations to hotfix branches
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# 10:00 UTC Monday
|
||||||
|
- cron: "0 10 * * 1"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
trigger-runners:
|
||||||
|
name: Trigger sync → ${{ matrix.hotfix_branch }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
hotfix_branch:
|
||||||
|
- version-16-hotfix
|
||||||
|
fail-fast: false
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Dispatch runner on ${{ matrix.hotfix_branch }}
|
||||||
|
run: |
|
||||||
|
gh workflow run run-hotfix-translation-sync.yml \
|
||||||
|
--repo "${{ github.repository }}" \
|
||||||
|
--ref "${{ matrix.hotfix_branch }}"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
@@ -518,6 +518,7 @@ def get_account_autoname(account_number, account_name, company):
|
|||||||
def update_account_number(name, account_name, account_number=None, from_descendant=False):
|
def update_account_number(name, account_name, account_number=None, from_descendant=False):
|
||||||
_ensure_idle_system()
|
_ensure_idle_system()
|
||||||
account = frappe.get_cached_doc("Account", name)
|
account = frappe.get_cached_doc("Account", name)
|
||||||
|
account.check_permission("write")
|
||||||
if not account:
|
if not account:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"actions": [],
|
"actions": [],
|
||||||
"autoname": "format:Bank Statement Import on {creation}",
|
"autoname": "format:Bank Statement Import on {creation}",
|
||||||
"beta": 1,
|
|
||||||
"creation": "2019-08-04 14:16:08.318714",
|
"creation": "2019-08-04 14:16:08.318714",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
@@ -226,7 +225,7 @@
|
|||||||
],
|
],
|
||||||
"hide_toolbar": 1,
|
"hide_toolbar": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-06-11 02:23:22.159961",
|
"modified": "2026-05-30 20:51:10.353723",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Bank Statement Import",
|
"name": "Bank Statement Import",
|
||||||
@@ -251,4 +250,4 @@
|
|||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
"actions": [],
|
"actions": [],
|
||||||
"allow_events_in_timeline": 1,
|
"allow_events_in_timeline": 1,
|
||||||
"autoname": "naming_series:",
|
"autoname": "naming_series:",
|
||||||
"beta": 1,
|
|
||||||
"creation": "2019-07-05 16:34:31.013238",
|
"creation": "2019-07-05 16:34:31.013238",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
@@ -400,7 +399,7 @@
|
|||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-11-26 13:46:07.760867",
|
"modified": "2026-05-30 23:18:04.712528",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Dunning",
|
"name": "Dunning",
|
||||||
@@ -449,9 +448,10 @@
|
|||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "ASC",
|
"sort_order": "ASC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"title_field": "customer_name",
|
"title_field": "customer_name",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"actions": [],
|
"actions": [],
|
||||||
"allow_rename": 1,
|
"allow_rename": 1,
|
||||||
"beta": 1,
|
|
||||||
"creation": "2019-12-04 04:59:08.003664",
|
"creation": "2019-12-04 04:59:08.003664",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
@@ -107,7 +106,7 @@
|
|||||||
"link_fieldname": "dunning_type"
|
"link_fieldname": "dunning_type"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2024-03-27 13:08:19.584112",
|
"modified": "2026-05-30 23:18:20.740726",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Dunning Type",
|
"name": "Dunning Type",
|
||||||
@@ -151,8 +150,9 @@
|
|||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -565,18 +565,19 @@ class FinancialQueryBuilder:
|
|||||||
frappe.qb.from_(acb_table)
|
frappe.qb.from_(acb_table)
|
||||||
.select(
|
.select(
|
||||||
acb_table.account,
|
acb_table.account,
|
||||||
(acb_table.debit - acb_table.credit).as_("balance"),
|
Sum(acb_table.debit - acb_table.credit).as_("balance"),
|
||||||
)
|
)
|
||||||
.where(acb_table.company == self.company)
|
.where(acb_table.company == self.company)
|
||||||
.where(acb_table.account.isin(account_names))
|
.where(acb_table.account.isin(account_names))
|
||||||
.where(acb_table.period_closing_voucher == closing_voucher)
|
.where(acb_table.period_closing_voucher == closing_voucher)
|
||||||
|
.groupby(acb_table.account)
|
||||||
)
|
)
|
||||||
|
|
||||||
query = self._apply_standard_filters(query, acb_table, "Account Closing Balance")
|
query = self._apply_standard_filters(query, acb_table, "Account Closing Balance")
|
||||||
results = self._execute_with_permissions(query, "Account Closing Balance")
|
results = self._execute_with_permissions(query, "Account Closing Balance")
|
||||||
|
|
||||||
for row in results:
|
for row in results:
|
||||||
closing_balances[row["account"]] = row["balance"]
|
closing_balances[row["account"]] = row["balance"] or 0.0
|
||||||
|
|
||||||
return closing_balances
|
return closing_balances
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from erpnext.accounts.doctype.financial_report_template.test_financial_report_te
|
|||||||
)
|
)
|
||||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||||
from erpnext.accounts.utils import get_currency_precision, get_fiscal_year
|
from erpnext.accounts.utils import get_currency_precision, get_fiscal_year
|
||||||
|
from erpnext.tests.utils import change_settings
|
||||||
|
|
||||||
|
|
||||||
class TestDependencyResolver(FinancialReportTemplateTestCase):
|
class TestDependencyResolver(FinancialReportTemplateTestCase):
|
||||||
@@ -1953,6 +1954,104 @@ class TestFinancialQueryBuilder(FinancialReportTemplateTestCase):
|
|||||||
|
|
||||||
jv_2023.cancel()
|
jv_2023.cancel()
|
||||||
|
|
||||||
|
@change_settings("Accounts Settings", {"use_legacy_controller_for_pcv": 1})
|
||||||
|
def test_opening_balance_sums_acb_rows_across_dimensions(self):
|
||||||
|
"""
|
||||||
|
Account Closing Balance stores one row per (account, cost_center,
|
||||||
|
project, finance_book). The closing-balance fetch must sum all rows.
|
||||||
|
"""
|
||||||
|
company = "_Test Company"
|
||||||
|
cash_account = "_Test Cash - _TC"
|
||||||
|
sales_account = "Sales - _TC"
|
||||||
|
cc_1 = "_Test Cost Center - _TC"
|
||||||
|
cc_2 = "_Test Cost Center 2 - _TC"
|
||||||
|
docs = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
jv_2023_cc1 = make_journal_entry(
|
||||||
|
account1=cash_account,
|
||||||
|
account2=sales_account,
|
||||||
|
amount=3000,
|
||||||
|
posting_date="2023-06-15",
|
||||||
|
cost_center=cc_1,
|
||||||
|
company=company,
|
||||||
|
submit=True,
|
||||||
|
)
|
||||||
|
docs.append(jv_2023_cc1)
|
||||||
|
jv_2023_cc2 = make_journal_entry(
|
||||||
|
account1=cash_account,
|
||||||
|
account2=sales_account,
|
||||||
|
amount=2000,
|
||||||
|
posting_date="2023-06-15",
|
||||||
|
cost_center=cc_2,
|
||||||
|
company=company,
|
||||||
|
submit=True,
|
||||||
|
)
|
||||||
|
docs.append(jv_2023_cc2)
|
||||||
|
|
||||||
|
fy_2023 = get_fiscal_year("2023-06-15", company=company)
|
||||||
|
|
||||||
|
pcv = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Period Closing Voucher",
|
||||||
|
"transaction_date": "2023-12-31",
|
||||||
|
"period_start_date": fy_2023[1],
|
||||||
|
"period_end_date": fy_2023[2],
|
||||||
|
"company": company,
|
||||||
|
"fiscal_year": fy_2023[0],
|
||||||
|
"cost_center": cc_1,
|
||||||
|
"closing_account_head": "Deferred Revenue - _TC",
|
||||||
|
"remarks": "Test multi-dim PCV",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
pcv.insert()
|
||||||
|
pcv.submit()
|
||||||
|
docs.append(pcv)
|
||||||
|
|
||||||
|
jv_2024 = make_journal_entry(
|
||||||
|
account1=cash_account,
|
||||||
|
account2=sales_account,
|
||||||
|
amount=100,
|
||||||
|
posting_date="2024-01-15",
|
||||||
|
cost_center=cc_1,
|
||||||
|
company=company,
|
||||||
|
submit=True,
|
||||||
|
)
|
||||||
|
docs.append(jv_2024)
|
||||||
|
|
||||||
|
filters = {
|
||||||
|
"company": company,
|
||||||
|
"from_fiscal_year": "2024",
|
||||||
|
"to_fiscal_year": "2024",
|
||||||
|
"period_start_date": "2024-01-01",
|
||||||
|
"period_end_date": "2024-03-31",
|
||||||
|
"filter_based_on": "Date Range",
|
||||||
|
"periodicity": "Monthly",
|
||||||
|
"ignore_closing_entries": True,
|
||||||
|
}
|
||||||
|
periods = [
|
||||||
|
{"key": "2024_jan", "from_date": "2024-01-01", "to_date": "2024-01-31"},
|
||||||
|
{"key": "2024_feb", "from_date": "2024-02-01", "to_date": "2024-02-29"},
|
||||||
|
{"key": "2024_mar", "from_date": "2024-03-01", "to_date": "2024-03-31"},
|
||||||
|
]
|
||||||
|
|
||||||
|
query_builder = FinancialQueryBuilder(filters, periods)
|
||||||
|
accounts = [
|
||||||
|
frappe._dict({"name": cash_account, "account_name": "Cash", "account_number": "1001"}),
|
||||||
|
]
|
||||||
|
|
||||||
|
balances_data = query_builder.fetch_account_balances(accounts)
|
||||||
|
cash_data = balances_data.get(cash_account)
|
||||||
|
self.assertIsNotNone(cash_data, "Cash account must appear in results")
|
||||||
|
|
||||||
|
jan_cash = cash_data.get_period("2024_jan")
|
||||||
|
self.assertEqual(jan_cash.opening, 5000.0)
|
||||||
|
self.assertEqual(jan_cash.movement, 100.0)
|
||||||
|
self.assertEqual(jan_cash.closing, 5100.0)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self.cancel_docs(docs)
|
||||||
|
|
||||||
def test_opening_entries_roll_into_opening_after_period_closing(self):
|
def test_opening_entries_roll_into_opening_after_period_closing(self):
|
||||||
"""
|
"""
|
||||||
Sequence:
|
Sequence:
|
||||||
|
|||||||
@@ -9,6 +9,14 @@ from erpnext.tests.utils import ERPNextTestSuite
|
|||||||
class FinancialReportTemplateTestCase(ERPNextTestSuite):
|
class FinancialReportTemplateTestCase(ERPNextTestSuite):
|
||||||
"""Utility class with common setup and helper methods for all test classes"""
|
"""Utility class with common setup and helper methods for all test classes"""
|
||||||
|
|
||||||
|
def cancel_docs(self, docs):
|
||||||
|
"""Cancel submitted docs in reverse creation order to avoid dependency issues."""
|
||||||
|
for doc in reversed(docs):
|
||||||
|
if doc:
|
||||||
|
doc.reload()
|
||||||
|
if doc.docstatus == 1:
|
||||||
|
doc.cancel()
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Set up test data"""
|
"""Set up test data"""
|
||||||
self.create_test_template()
|
self.create_test_template()
|
||||||
|
|||||||
@@ -433,15 +433,17 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
|
|||||||
|
|
||||||
accounts_add(doc, cdt, cdn) {
|
accounts_add(doc, cdt, cdn) {
|
||||||
var row = frappe.get_doc(cdt, cdn);
|
var row = frappe.get_doc(cdt, cdn);
|
||||||
row.exchange_rate = 1;
|
if (!row.exchange_rate) row.exchange_rate = 1;
|
||||||
$.each(doc.accounts, function (i, d) {
|
if (!row.account) {
|
||||||
if (d.account && d.party && d.party_type) {
|
$.each(doc.accounts, function (i, d) {
|
||||||
row.account = d.account;
|
if (d.account && d.party && d.party_type) {
|
||||||
row.party = d.party;
|
row.account = d.account;
|
||||||
row.party_type = d.party_type;
|
row.party = d.party;
|
||||||
row.exchange_rate = d.exchange_rate;
|
row.party_type = d.party_type;
|
||||||
}
|
row.exchange_rate = d.exchange_rate;
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// set difference
|
// set difference
|
||||||
if (doc.difference) {
|
if (doc.difference) {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"actions": [],
|
"actions": [],
|
||||||
"allow_copy": 1,
|
"allow_copy": 1,
|
||||||
"beta": 1,
|
|
||||||
"creation": "2017-08-29 02:22:54.947711",
|
"creation": "2017-08-29 02:22:54.947711",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
@@ -90,7 +89,7 @@
|
|||||||
"hide_toolbar": 1,
|
"hide_toolbar": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-03-31 01:47:20.360352",
|
"modified": "2026-05-30 23:18:48.691227",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Opening Invoice Creation Tool",
|
"name": "Opening Invoice Creation Tool",
|
||||||
|
|||||||
@@ -1726,6 +1726,35 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
before_cancel: function (frm) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
frappe.call({
|
||||||
|
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_linked_bank_transactions",
|
||||||
|
args: { payment_entry: frm.doc.name },
|
||||||
|
callback: function (r) {
|
||||||
|
const linked = r.message || [];
|
||||||
|
if (!linked.length) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const bt_links = linked
|
||||||
|
.map((name) => frappe.utils.get_form_link("Bank Transaction", name, true))
|
||||||
|
.join(", ");
|
||||||
|
frappe.confirm(
|
||||||
|
__(
|
||||||
|
"This Payment Entry is reconciled with {0}. Cancelling will automatically unreconcile it. Do you want to proceed?",
|
||||||
|
[bt_links]
|
||||||
|
),
|
||||||
|
() => resolve(),
|
||||||
|
() => reject(),
|
||||||
|
__("Yes"),
|
||||||
|
__("No")
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
frappe.ui.form.on("Payment Entry Reference", {
|
frappe.ui.form.on("Payment Entry Reference", {
|
||||||
|
|||||||
@@ -3568,3 +3568,16 @@ def make_payment_order(source_name, target_doc=None):
|
|||||||
@erpnext.allow_regional
|
@erpnext.allow_regional
|
||||||
def add_regional_gl_entries(gl_entries, doc):
|
def add_regional_gl_entries(gl_entries, doc):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_linked_bank_transactions(payment_entry: str) -> list:
|
||||||
|
frappe.has_permission("Payment Entry", ptype="read", doc=payment_entry, throw=True)
|
||||||
|
return frappe.get_all(
|
||||||
|
"Bank Transaction Payments",
|
||||||
|
filters={
|
||||||
|
"payment_document": "Payment Entry",
|
||||||
|
"payment_entry": payment_entry,
|
||||||
|
},
|
||||||
|
pluck="parent",
|
||||||
|
)
|
||||||
|
|||||||
@@ -26,8 +26,6 @@
|
|||||||
"due_date",
|
"due_date",
|
||||||
"amended_from",
|
"amended_from",
|
||||||
"return_against",
|
"return_against",
|
||||||
"section_break_abck",
|
|
||||||
"title",
|
|
||||||
"accounting_dimensions_section",
|
"accounting_dimensions_section",
|
||||||
"project",
|
"project",
|
||||||
"dimension_col_break",
|
"dimension_col_break",
|
||||||
@@ -172,6 +170,7 @@
|
|||||||
"is_discounted",
|
"is_discounted",
|
||||||
"col_break23",
|
"col_break23",
|
||||||
"status",
|
"status",
|
||||||
|
"title",
|
||||||
"more_info",
|
"more_info",
|
||||||
"debit_to",
|
"debit_to",
|
||||||
"party_account_currency",
|
"party_account_currency",
|
||||||
@@ -1625,10 +1624,6 @@
|
|||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Auto Repeat"
|
"label": "Auto Repeat"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "section_break_abck",
|
|
||||||
"fieldtype": "Section Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"fieldname": "title",
|
"fieldname": "title",
|
||||||
@@ -1641,7 +1636,7 @@
|
|||||||
"icon": "fa fa-file-text",
|
"icon": "fa fa-file-text",
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-05-01 02:37:30.580568",
|
"modified": "2026-05-28 12:22:50.253090",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "POS Invoice",
|
"name": "POS Invoice",
|
||||||
|
|||||||
@@ -208,15 +208,14 @@ class POSProfile(Document):
|
|||||||
def set_defaults(self, include_current_pos=True):
|
def set_defaults(self, include_current_pos=True):
|
||||||
frappe.defaults.clear_default("is_pos")
|
frappe.defaults.clear_default("is_pos")
|
||||||
|
|
||||||
if not include_current_pos:
|
pfu = frappe.qb.DocType("POS Profile User")
|
||||||
condition = " where pfu.name != '%s' and pfu.default = 1 " % self.name.replace("'", "'")
|
|
||||||
else:
|
|
||||||
condition = " where pfu.default = 1 "
|
|
||||||
|
|
||||||
pos_view_users = frappe.db.sql_list(
|
query = frappe.qb.from_(pfu).select(pfu.user).where(pfu.default == 1)
|
||||||
f"""select pfu.user
|
|
||||||
from `tabPOS Profile User` as pfu {condition}"""
|
if not include_current_pos:
|
||||||
)
|
query = query.where(pfu.name != self.name)
|
||||||
|
|
||||||
|
pos_view_users = query.run(as_list=1, pluck=True)
|
||||||
|
|
||||||
for user in pos_view_users:
|
for user in pos_view_users:
|
||||||
if user:
|
if user:
|
||||||
|
|||||||
@@ -151,13 +151,13 @@
|
|||||||
"label": "Default Advance Account",
|
"label": "Default Advance Account",
|
||||||
"mandatory_depends_on": "doc.party_type",
|
"mandatory_depends_on": "doc.party_type",
|
||||||
"options": "Account",
|
"options": "Account",
|
||||||
"reqd": 1
|
"reqd": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-01-08 08:22:14.798085",
|
"modified": "2026-05-16 11:43:12.758685",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Process Payment Reconciliation",
|
"name": "Process Payment Reconciliation",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class ProcessPaymentReconciliation(Document):
|
|||||||
bank_cash_account: DF.Link | None
|
bank_cash_account: DF.Link | None
|
||||||
company: DF.Link
|
company: DF.Link
|
||||||
cost_center: DF.Link | None
|
cost_center: DF.Link | None
|
||||||
default_advance_account: DF.Link
|
default_advance_account: DF.Link | None
|
||||||
error_log: DF.LongText | None
|
error_log: DF.LongText | None
|
||||||
from_invoice_date: DF.Date | None
|
from_invoice_date: DF.Date | None
|
||||||
from_payment_date: DF.Date | None
|
from_payment_date: DF.Date | None
|
||||||
@@ -218,10 +218,7 @@ def trigger_reconciliation_for_queued_docs():
|
|||||||
fields = ["company", "party_type", "party", "receivable_payable_account", "default_advance_account"]
|
fields = ["company", "party_type", "party", "receivable_payable_account", "default_advance_account"]
|
||||||
|
|
||||||
def get_filters_as_tuple(fields, doc):
|
def get_filters_as_tuple(fields, doc):
|
||||||
filters = ()
|
return tuple(doc.get(x) or "" for x in fields)
|
||||||
for x in fields:
|
|
||||||
filters += tuple(doc.get(x))
|
|
||||||
return filters
|
|
||||||
|
|
||||||
for x in all_queued:
|
for x in all_queued:
|
||||||
doc = frappe.get_doc("Process Payment Reconciliation", x)
|
doc = frappe.get_doc("Process Payment Reconciliation", x)
|
||||||
|
|||||||
@@ -27,8 +27,6 @@
|
|||||||
"update_billed_amount_in_purchase_receipt",
|
"update_billed_amount_in_purchase_receipt",
|
||||||
"apply_tds",
|
"apply_tds",
|
||||||
"amended_from",
|
"amended_from",
|
||||||
"section_break_hzux",
|
|
||||||
"title",
|
|
||||||
"supplier_invoice_details",
|
"supplier_invoice_details",
|
||||||
"bill_no",
|
"bill_no",
|
||||||
"column_break_15",
|
"column_break_15",
|
||||||
@@ -201,6 +199,7 @@
|
|||||||
"hold_comment",
|
"hold_comment",
|
||||||
"additional_info_section",
|
"additional_info_section",
|
||||||
"is_internal_supplier",
|
"is_internal_supplier",
|
||||||
|
"title",
|
||||||
"represents_company",
|
"represents_company",
|
||||||
"supplier_group",
|
"supplier_group",
|
||||||
"sender",
|
"sender",
|
||||||
@@ -1685,10 +1684,6 @@
|
|||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Automation"
|
"label": "Automation"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "section_break_hzux",
|
|
||||||
"fieldtype": "Section Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"fieldname": "title",
|
"fieldname": "title",
|
||||||
@@ -1703,7 +1698,7 @@
|
|||||||
"idx": 204,
|
"idx": 204,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-04-28 07:15:31.062404",
|
"modified": "2026-05-28 12:36:55.215363",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Purchase Invoice",
|
"name": "Purchase Invoice",
|
||||||
|
|||||||
@@ -33,8 +33,6 @@
|
|||||||
"is_created_using_pos",
|
"is_created_using_pos",
|
||||||
"pos_closing_entry",
|
"pos_closing_entry",
|
||||||
"has_subcontracted",
|
"has_subcontracted",
|
||||||
"section_break_qllv",
|
|
||||||
"title",
|
|
||||||
"accounting_dimensions_section",
|
"accounting_dimensions_section",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
"dimension_col_break",
|
"dimension_col_break",
|
||||||
@@ -234,6 +232,7 @@
|
|||||||
"status",
|
"status",
|
||||||
"remarks",
|
"remarks",
|
||||||
"customer_group",
|
"customer_group",
|
||||||
|
"title",
|
||||||
"column_break_imbx",
|
"column_break_imbx",
|
||||||
"is_internal_customer",
|
"is_internal_customer",
|
||||||
"represents_company",
|
"represents_company",
|
||||||
@@ -2343,10 +2342,6 @@
|
|||||||
"fieldname": "column_break_iaso",
|
"fieldname": "column_break_iaso",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "section_break_qllv",
|
|
||||||
"fieldtype": "Section Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"fieldname": "title",
|
"fieldname": "title",
|
||||||
@@ -2367,7 +2362,7 @@
|
|||||||
"link_fieldname": "consolidated_invoice"
|
"link_fieldname": "consolidated_invoice"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2026-05-21 17:31:11.190958",
|
"modified": "2026-05-28 12:15:12.486443",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice",
|
"name": "Sales Invoice",
|
||||||
|
|||||||
@@ -3036,15 +3036,22 @@ def update_multi_mode_option(doc, pos_profile):
|
|||||||
|
|
||||||
|
|
||||||
def get_all_mode_of_payments(doc):
|
def get_all_mode_of_payments(doc):
|
||||||
return frappe.db.sql(
|
ModeOfPaymentAccount = frappe.qb.DocType("Mode of Payment Account")
|
||||||
"""
|
ModeOfPayment = frappe.qb.DocType("Mode of Payment")
|
||||||
select mpa.default_account, mpa.parent, mp.type as type
|
|
||||||
from `tabMode of Payment Account` mpa,`tabMode of Payment` mp
|
query = (
|
||||||
where mpa.parent = mp.name and mpa.company = %(company)s and mp.enabled = 1""",
|
frappe.qb.from_(ModeOfPaymentAccount)
|
||||||
{"company": doc.company},
|
.join(ModeOfPayment)
|
||||||
as_dict=1,
|
.on(ModeOfPaymentAccount.parent == ModeOfPayment.name)
|
||||||
|
.select(
|
||||||
|
ModeOfPaymentAccount.default_account, ModeOfPaymentAccount.parent, ModeOfPayment.type.as_("type")
|
||||||
|
)
|
||||||
|
.where(ModeOfPaymentAccount.company == doc.company)
|
||||||
|
.where(ModeOfPayment.enabled == 1)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return query.run(as_dict=1)
|
||||||
|
|
||||||
|
|
||||||
def get_mode_of_payments_info(mode_of_payments, company):
|
def get_mode_of_payments_info(mode_of_payments, company):
|
||||||
data = frappe.db.sql(
|
data = frappe.db.sql(
|
||||||
|
|||||||
@@ -947,7 +947,8 @@
|
|||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Distributed Discount Amount",
|
"label": "Distributed Discount Amount",
|
||||||
"options": "currency",
|
"options": "currency",
|
||||||
"print_hide": 1
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "available_quantity_section",
|
"fieldname": "available_quantity_section",
|
||||||
@@ -1016,7 +1017,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-02-24 14:37:16.853941",
|
"modified": "2026-05-29 12:23:28.259905",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice Item",
|
"name": "Sales Invoice Item",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.query_builder.functions import Sum
|
from frappe.query_builder.functions import Sum
|
||||||
from frappe.utils import getdate
|
from frappe.utils import cstr, getdate
|
||||||
|
|
||||||
from erpnext import allow_regional
|
from erpnext import allow_regional
|
||||||
from erpnext.controllers.accounts_controller import validate_account_head
|
from erpnext.controllers.accounts_controller import validate_account_head
|
||||||
@@ -48,7 +48,7 @@ class TaxWithholdingCategory(Document):
|
|||||||
for d in self.get("rates"):
|
for d in self.get("rates"):
|
||||||
if getdate(d.from_date) >= getdate(d.to_date):
|
if getdate(d.from_date) >= getdate(d.to_date):
|
||||||
frappe.throw(_("Row #{0}: From Date cannot be before To Date").format(d.idx))
|
frappe.throw(_("Row #{0}: From Date cannot be before To Date").format(d.idx))
|
||||||
group_rates[d.tax_withholding_group].append(d)
|
group_rates[cstr(d.tax_withholding_group)].append(d)
|
||||||
|
|
||||||
# Validate overlapping dates within each group
|
# Validate overlapping dates within each group
|
||||||
for group, rates in group_rates.items():
|
for group, rates in group_rates.items():
|
||||||
@@ -92,10 +92,9 @@ class TaxWithholdingCategory(Document):
|
|||||||
|
|
||||||
def get_applicable_tax_row(self, posting_date, tax_withholding_group):
|
def get_applicable_tax_row(self, posting_date, tax_withholding_group):
|
||||||
for row in self.rates:
|
for row in self.rates:
|
||||||
if (
|
if getdate(row.from_date) <= getdate(posting_date) <= getdate(row.to_date) and cstr(
|
||||||
getdate(row.from_date) <= getdate(posting_date) <= getdate(row.to_date)
|
row.tax_withholding_group
|
||||||
and row.tax_withholding_group == tax_withholding_group
|
) == cstr(tax_withholding_group):
|
||||||
):
|
|
||||||
return row
|
return row
|
||||||
|
|
||||||
frappe.throw(_("No Tax Withholding data found for the current posting date."))
|
frappe.throw(_("No Tax Withholding data found for the current posting date."))
|
||||||
@@ -116,7 +115,7 @@ class TaxWithholdingDetails:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
tax_withholding_categories: list[str],
|
tax_withholding_categories: list[str],
|
||||||
tax_withholding_group: str,
|
tax_withholding_group: str | None,
|
||||||
posting_date: str,
|
posting_date: str,
|
||||||
party_type: str,
|
party_type: str,
|
||||||
party: str,
|
party: str,
|
||||||
|
|||||||
@@ -999,6 +999,47 @@ class TestTaxWithholdingCategory(ERPNextTestSuite):
|
|||||||
|
|
||||||
self.cleanup_invoices(invoices)
|
self.cleanup_invoices(invoices)
|
||||||
|
|
||||||
|
def test_null_and_empty_tax_withholding_group_are_equivalent(self):
|
||||||
|
"""
|
||||||
|
NULL and empty-string `tax_withholding_group` must be treated as the
|
||||||
|
same value.
|
||||||
|
"""
|
||||||
|
category = frappe.get_doc("Tax Withholding Category", "Cumulative Threshold TDS")
|
||||||
|
original_row = category.rates[0]
|
||||||
|
original_row.tax_withholding_group = None
|
||||||
|
|
||||||
|
# Part 1: validate_dates must detect overlap between NULL-group and
|
||||||
|
# empty-string-group rows covering the same date range.
|
||||||
|
category.append(
|
||||||
|
"rates",
|
||||||
|
{
|
||||||
|
"from_date": original_row.from_date,
|
||||||
|
"to_date": original_row.to_date,
|
||||||
|
"tax_withholding_group": "",
|
||||||
|
"tax_withholding_rate": original_row.tax_withholding_rate,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
with self.assertRaises(frappe.ValidationError):
|
||||||
|
category.validate_dates()
|
||||||
|
category.rates.pop()
|
||||||
|
|
||||||
|
# Part 2: get_applicable_tax_row must match NULL <-> "" in either direction.
|
||||||
|
posting_date = original_row.from_date
|
||||||
|
|
||||||
|
row = category.get_applicable_tax_row(posting_date=posting_date, tax_withholding_group="")
|
||||||
|
self.assertEqual(row.name, original_row.name)
|
||||||
|
|
||||||
|
row = category.get_applicable_tax_row(posting_date=posting_date, tax_withholding_group=None)
|
||||||
|
self.assertEqual(row.name, original_row.name)
|
||||||
|
|
||||||
|
original_row.tax_withholding_group = ""
|
||||||
|
row = category.get_applicable_tax_row(posting_date=posting_date, tax_withholding_group=None)
|
||||||
|
self.assertEqual(row.name, original_row.name)
|
||||||
|
|
||||||
|
original_row.tax_withholding_group = None
|
||||||
|
with self.assertRaises(frappe.ValidationError):
|
||||||
|
category.get_applicable_tax_row(posting_date=posting_date, tax_withholding_group="194R")
|
||||||
|
|
||||||
def test_tds_calculation_on_net_total(self):
|
def test_tds_calculation_on_net_total(self):
|
||||||
self.setup_party_with_category("Supplier", "Test TDS Supplier4", "Cumulative Threshold TDS")
|
self.setup_party_with_category("Supplier", "Test TDS Supplier4", "Cumulative Threshold TDS")
|
||||||
invoices = []
|
invoices = []
|
||||||
|
|||||||
@@ -4,14 +4,14 @@
|
|||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Number Card",
|
"doctype": "Number Card",
|
||||||
"document_type": "Purchase Invoice",
|
"document_type": "Purchase Invoice",
|
||||||
"dynamic_filters_json": "[[\"Purchase Invoice\",\"company\",\"=\",\" frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
"dynamic_filters_json": "[[\"Purchase Invoice\", \"company\", \"=\", \"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Purchase Invoice\", \"posting_date\", \"Between\", \"(frappe.boot.current_fiscal_year || [null, `${frappe.datetime.get_today().slice(0,4)}-01-01`, `${frappe.datetime.get_today().slice(0,4)}-12-31`]).slice(1)\"]]",
|
||||||
"filters_json": "[[\"Purchase Invoice\",\"docstatus\",\"=\",\"1\"],[\"Purchase Invoice\",\"posting_date\",\"Timespan\",\"this year\"]]",
|
"filters_json": "[[\"Purchase Invoice\",\"docstatus\",\"=\",\"1\"]]",
|
||||||
"function": "Sum",
|
"function": "Sum",
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"is_public": 1,
|
"is_public": 1,
|
||||||
"is_standard": 1,
|
"is_standard": 1,
|
||||||
"label": "Total Incoming Bills",
|
"label": "Total Incoming Bills",
|
||||||
"modified": "2024-12-05 12:00:00.000000",
|
"modified": "2026-06-01 12:00:00.000000",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Total Incoming Bills",
|
"name": "Total Incoming Bills",
|
||||||
|
|||||||
@@ -4,14 +4,14 @@
|
|||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Number Card",
|
"doctype": "Number Card",
|
||||||
"document_type": "Payment Entry",
|
"document_type": "Payment Entry",
|
||||||
"dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
"dynamic_filters_json": "[[\"Payment Entry\", \"company\", \"=\", \"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Payment Entry\", \"posting_date\", \"Between\", \"(frappe.boot.current_fiscal_year || [null, `${frappe.datetime.get_today().slice(0,4)}-01-01`, `${frappe.datetime.get_today().slice(0,4)}-12-31`]).slice(1)\"]]",
|
||||||
"filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\"],[\"Payment Entry\",\"posting_date\",\"Timespan\",\"this year\"],[\"Payment Entry\",\"payment_type\",\"=\",\"Receive\"]]",
|
"filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\"],[\"Payment Entry\",\"payment_type\",\"=\",\"Receive\"]]",
|
||||||
"function": "Sum",
|
"function": "Sum",
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"is_public": 1,
|
"is_public": 1,
|
||||||
"is_standard": 1,
|
"is_standard": 1,
|
||||||
"label": "Total Incoming Payment",
|
"label": "Total Incoming Payment",
|
||||||
"modified": "2024-12-05 12:00:00.000000",
|
"modified": "2026-06-01 12:00:00.000000",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Total Incoming Payment",
|
"name": "Total Incoming Payment",
|
||||||
|
|||||||
@@ -4,14 +4,14 @@
|
|||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Number Card",
|
"doctype": "Number Card",
|
||||||
"document_type": "Sales Invoice",
|
"document_type": "Sales Invoice",
|
||||||
"dynamic_filters_json": "[[\"Sales Invoice\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
"dynamic_filters_json": "[[\"Sales Invoice\", \"company\", \"=\", \"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Sales Invoice\", \"posting_date\", \"Between\", \"(frappe.boot.current_fiscal_year || [null, `${frappe.datetime.get_today().slice(0,4)}-01-01`, `${frappe.datetime.get_today().slice(0,4)}-12-31`]).slice(1)\"]]",
|
||||||
"filters_json": "[[\"Sales Invoice\",\"docstatus\",\"=\",\"1\"],[\"Sales Invoice\",\"posting_date\",\"Timespan\",\"this year\"]]",
|
"filters_json": "[[\"Sales Invoice\",\"docstatus\",\"=\",\"1\"]]",
|
||||||
"function": "Sum",
|
"function": "Sum",
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"is_public": 1,
|
"is_public": 1,
|
||||||
"is_standard": 1,
|
"is_standard": 1,
|
||||||
"label": "Total Outgoing Bills",
|
"label": "Total Outgoing Bills",
|
||||||
"modified": "2024-12-05 12:00:00.000000",
|
"modified": "2026-06-01 12:00:00.000000",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Total Outgoing Bills",
|
"name": "Total Outgoing Bills",
|
||||||
|
|||||||
@@ -4,14 +4,14 @@
|
|||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Number Card",
|
"doctype": "Number Card",
|
||||||
"document_type": "Payment Entry",
|
"document_type": "Payment Entry",
|
||||||
"dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
"dynamic_filters_json": "[[\"Payment Entry\", \"company\", \"=\", \"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Payment Entry\", \"posting_date\", \"Between\", \"(frappe.boot.current_fiscal_year || [null, `${frappe.datetime.get_today().slice(0,4)}-01-01`, `${frappe.datetime.get_today().slice(0,4)}-12-31`]).slice(1)\"]]",
|
||||||
"filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\"],[\"Payment Entry\",\"posting_date\",\"Timespan\",\"this year\"],[\"Payment Entry\",\"payment_type\",\"=\",\"Pay\"]]",
|
"filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\"],[\"Payment Entry\",\"payment_type\",\"=\",\"Pay\"]]",
|
||||||
"function": "Sum",
|
"function": "Sum",
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"is_public": 1,
|
"is_public": 1,
|
||||||
"is_standard": 1,
|
"is_standard": 1,
|
||||||
"label": "Total Outgoing Payment",
|
"label": "Total Outgoing Payment",
|
||||||
"modified": "2024-12-05 12:00:00.000000",
|
"modified": "2026-06-01 12:00:00.000000",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Total Outgoing Payment",
|
"name": "Total Outgoing Payment",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from frappe import _
|
|||||||
from frappe.query_builder import Order
|
from frappe.query_builder import Order
|
||||||
from frappe.query_builder.functions import Max, Min
|
from frappe.query_builder.functions import Max, Min
|
||||||
from frappe.utils import (
|
from frappe.utils import (
|
||||||
|
DateTimeLikeObject,
|
||||||
add_months,
|
add_months,
|
||||||
cint,
|
cint,
|
||||||
flt,
|
flt,
|
||||||
@@ -359,7 +360,8 @@ def get_message_for_depr_entry_posting_error(asset_links, error_log_links):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def scrap_asset(asset_name, scrap_date=None):
|
def scrap_asset(asset_name: str, scrap_date: DateTimeLikeObject | None = None):
|
||||||
|
frappe.has_permission("Asset", "write", asset_name, throw=True)
|
||||||
asset = frappe.get_doc("Asset", asset_name)
|
asset = frappe.get_doc("Asset", asset_name)
|
||||||
scrap_date = getdate(scrap_date) or getdate(today())
|
scrap_date = getdate(scrap_date) or getdate(today())
|
||||||
asset.db_set("disposal_date", scrap_date)
|
asset.db_set("disposal_date", scrap_date)
|
||||||
@@ -448,7 +450,8 @@ def create_journal_entry_for_scrap(asset, scrap_date):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def restore_asset(asset_name):
|
def restore_asset(asset_name: str):
|
||||||
|
frappe.has_permission("Asset", "write", asset_name, throw=True)
|
||||||
asset = frappe.get_doc("Asset", asset_name)
|
asset = frappe.get_doc("Asset", asset_name)
|
||||||
reverse_depreciation_entry_made_on_disposal(asset)
|
reverse_depreciation_entry_made_on_disposal(asset)
|
||||||
reset_depreciation_schedule(asset, get_note_for_restore(asset))
|
reset_depreciation_schedule(asset, get_note_for_restore(asset))
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"section_break_vwgg",
|
"section_break_vwgg",
|
||||||
"maintain_same_rate",
|
"maintain_same_rate",
|
||||||
"column_break_lwxs",
|
"column_break_lwxs",
|
||||||
|
"set_landed_cost_based_on_purchase_invoice_rate",
|
||||||
"maintain_same_rate_action",
|
"maintain_same_rate_action",
|
||||||
"role_to_override_stop_action",
|
"role_to_override_stop_action",
|
||||||
"transaction_settings_section",
|
"transaction_settings_section",
|
||||||
@@ -24,7 +25,8 @@
|
|||||||
"po_required",
|
"po_required",
|
||||||
"pr_required",
|
"pr_required",
|
||||||
"project_update_frequency",
|
"project_update_frequency",
|
||||||
"column_break_12",
|
"over_order_allowance",
|
||||||
|
"column_break_kdcm",
|
||||||
"allow_multiple_items",
|
"allow_multiple_items",
|
||||||
"allow_negative_rates_for_items",
|
"allow_negative_rates_for_items",
|
||||||
"set_valuation_rate_for_rejected_materials",
|
"set_valuation_rate_for_rejected_materials",
|
||||||
@@ -33,7 +35,6 @@
|
|||||||
"purchase_invoice_settings_section",
|
"purchase_invoice_settings_section",
|
||||||
"bill_for_rejected_quantity_in_purchase_invoice",
|
"bill_for_rejected_quantity_in_purchase_invoice",
|
||||||
"use_transaction_date_exchange_rate",
|
"use_transaction_date_exchange_rate",
|
||||||
"set_landed_cost_based_on_purchase_invoice_rate",
|
|
||||||
"zero_quantity_line_items_section",
|
"zero_quantity_line_items_section",
|
||||||
"allow_zero_qty_in_supplier_quotation",
|
"allow_zero_qty_in_supplier_quotation",
|
||||||
"allow_zero_qty_in_request_for_quotation",
|
"allow_zero_qty_in_request_for_quotation",
|
||||||
@@ -156,10 +157,6 @@
|
|||||||
"fieldtype": "Tab Break",
|
"fieldtype": "Tab Break",
|
||||||
"label": "Transaction Settings"
|
"label": "Transaction Settings"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "column_break_12",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"description": "Prevents the system from automatically using the rate from the last purchase transaction when creating new purchase orders or transactions.",
|
"description": "Prevents the system from automatically using the rate from the last purchase transaction when creating new purchase orders or transactions.",
|
||||||
@@ -335,6 +332,16 @@
|
|||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"is_virtual": 1,
|
"is_virtual": 1,
|
||||||
"label": "Naming Series options"
|
"label": "Naming Series options"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "The percentage by which you are allowed to order more on a Purchase Order than the quantity requested on the originating Material Request. For example, if the Material Request has 100 units and the allowance is 10%, you can order up to 110 units",
|
||||||
|
"fieldname": "over_order_allowance",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Over Order Allowance (%)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_kdcm",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
@@ -343,7 +350,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-05-05 16:30:37.184607",
|
"modified": "2026-05-27 23:04:00.842393",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Buying Settings",
|
"name": "Buying Settings",
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class BuyingSettings(Document):
|
|||||||
fixed_email: DF.Link | None
|
fixed_email: DF.Link | None
|
||||||
maintain_same_rate: DF.Check
|
maintain_same_rate: DF.Check
|
||||||
maintain_same_rate_action: DF.Literal["Stop", "Warn"]
|
maintain_same_rate_action: DF.Literal["Stop", "Warn"]
|
||||||
|
over_order_allowance: DF.Float
|
||||||
over_transfer_allowance: DF.Float
|
over_transfer_allowance: DF.Float
|
||||||
po_required: DF.Literal["No", "Yes"]
|
po_required: DF.Literal["No", "Yes"]
|
||||||
pr_required: DF.Literal["No", "Yes"]
|
pr_required: DF.Literal["No", "Yes"]
|
||||||
|
|||||||
@@ -23,8 +23,6 @@
|
|||||||
"is_subcontracted",
|
"is_subcontracted",
|
||||||
"has_unit_price_items",
|
"has_unit_price_items",
|
||||||
"supplier_warehouse",
|
"supplier_warehouse",
|
||||||
"section_break_ahub",
|
|
||||||
"title",
|
|
||||||
"accounting_dimensions_section",
|
"accounting_dimensions_section",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
"dimension_col_break",
|
"dimension_col_break",
|
||||||
@@ -56,9 +54,7 @@
|
|||||||
"net_total",
|
"net_total",
|
||||||
"section_break_48",
|
"section_break_48",
|
||||||
"pricing_rules",
|
"pricing_rules",
|
||||||
"raw_material_details",
|
|
||||||
"set_reserve_warehouse",
|
"set_reserve_warehouse",
|
||||||
"supplied_items",
|
|
||||||
"taxes_section",
|
"taxes_section",
|
||||||
"tax_category",
|
"tax_category",
|
||||||
"taxes_and_charges",
|
"taxes_and_charges",
|
||||||
@@ -156,6 +152,7 @@
|
|||||||
"auto_repeat",
|
"auto_repeat",
|
||||||
"update_auto_repeat_reference",
|
"update_auto_repeat_reference",
|
||||||
"additional_info_section",
|
"additional_info_section",
|
||||||
|
"title",
|
||||||
"party_account_currency",
|
"party_account_currency",
|
||||||
"represents_company",
|
"represents_company",
|
||||||
"ref_sq",
|
"ref_sq",
|
||||||
@@ -1312,10 +1309,6 @@
|
|||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Auto Repeat"
|
"label": "Auto Repeat"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "section_break_ahub",
|
|
||||||
"fieldtype": "Section Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"fieldname": "title",
|
"fieldname": "title",
|
||||||
@@ -1330,7 +1323,7 @@
|
|||||||
"idx": 105,
|
"idx": 105,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-04-28 06:11:46.904768",
|
"modified": "2026-05-28 12:34:19.659621",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Purchase Order",
|
"name": "Purchase Order",
|
||||||
|
|||||||
@@ -182,6 +182,9 @@ class PurchaseOrder(BuyingController):
|
|||||||
"target_ref_field": "stock_qty",
|
"target_ref_field": "stock_qty",
|
||||||
"source_field": "stock_qty",
|
"source_field": "stock_qty",
|
||||||
"percent_join_field": "material_request",
|
"percent_join_field": "material_request",
|
||||||
|
"global_allowance_field": "over_order_allowance",
|
||||||
|
"global_allowance_doctype": "Buying Settings",
|
||||||
|
"item_allowance_field": "over_order_allowance",
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -128,6 +128,44 @@ class TestPurchaseOrder(ERPNextTestSuite):
|
|||||||
frappe.db.set_value("Item", "_Test Item", "over_billing_allowance", 0)
|
frappe.db.set_value("Item", "_Test Item", "over_billing_allowance", 0)
|
||||||
frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", 0)
|
frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", 0)
|
||||||
|
|
||||||
|
def test_over_order_allowance_against_material_request(self) -> None:
|
||||||
|
"""Over Order Allowance in Buying Settings must govern PO qty vs MR qty independently
|
||||||
|
from Over Delivery/Receipt Allowance which governs receipt/delivery against a PO."""
|
||||||
|
mr = make_material_request(qty=100)
|
||||||
|
po = make_purchase_order(mr.name)
|
||||||
|
po.supplier = "_Test Supplier"
|
||||||
|
po.items[0].qty = 110 # 10% over the MR qty
|
||||||
|
|
||||||
|
# Without any allowance, submitting should raise an OverAllowanceError
|
||||||
|
from erpnext.controllers.status_updater import OverAllowanceError
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Buying Settings", "over_order_allowance", 0)
|
||||||
|
frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 0)
|
||||||
|
self.assertRaises(OverAllowanceError, po.submit)
|
||||||
|
|
||||||
|
# Granting 10% in Over Order Allowance (Buying Settings) must allow the submit
|
||||||
|
frappe.db.set_single_value("Buying Settings", "over_order_allowance", 10)
|
||||||
|
po.reload()
|
||||||
|
po.items[0].qty = 110
|
||||||
|
po.submit()
|
||||||
|
self.assertEqual(po.docstatus, 1)
|
||||||
|
po.cancel()
|
||||||
|
|
||||||
|
# Over Delivery/Receipt Allowance must remain independent — changing it must not
|
||||||
|
# affect the MR → PO validation when Over Order Allowance is 0.
|
||||||
|
frappe.db.set_single_value("Buying Settings", "over_order_allowance", 0)
|
||||||
|
frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 50)
|
||||||
|
|
||||||
|
mr2 = make_material_request(qty=100)
|
||||||
|
po2 = make_purchase_order(mr2.name)
|
||||||
|
po2.supplier = "_Test Supplier"
|
||||||
|
po2.items[0].qty = 110
|
||||||
|
self.assertRaises(OverAllowanceError, po2.submit)
|
||||||
|
|
||||||
|
# cleanup
|
||||||
|
frappe.db.set_single_value("Buying Settings", "over_order_allowance", 0)
|
||||||
|
frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 0)
|
||||||
|
|
||||||
def test_update_remove_child_linked_to_mr(self):
|
def test_update_remove_child_linked_to_mr(self):
|
||||||
"""Test impact on linked PO and MR on deleting/updating row."""
|
"""Test impact on linked PO and MR on deleting/updating row."""
|
||||||
mr = make_material_request(qty=10)
|
mr = make_material_request(qty=10)
|
||||||
|
|||||||
@@ -16,8 +16,6 @@
|
|||||||
"status",
|
"status",
|
||||||
"has_unit_price_items",
|
"has_unit_price_items",
|
||||||
"amended_from",
|
"amended_from",
|
||||||
"section_break_mhyw",
|
|
||||||
"title",
|
|
||||||
"suppliers_section",
|
"suppliers_section",
|
||||||
"suppliers",
|
"suppliers",
|
||||||
"items_section",
|
"items_section",
|
||||||
@@ -44,6 +42,7 @@
|
|||||||
"letter_head",
|
"letter_head",
|
||||||
"more_info",
|
"more_info",
|
||||||
"opportunity",
|
"opportunity",
|
||||||
|
"title",
|
||||||
"address_and_contact_tab",
|
"address_and_contact_tab",
|
||||||
"billing_address",
|
"billing_address",
|
||||||
"billing_address_display",
|
"billing_address_display",
|
||||||
@@ -374,10 +373,6 @@
|
|||||||
"label": "Shipping Address Details",
|
"label": "Shipping Address Details",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "section_break_mhyw",
|
|
||||||
"fieldtype": "Section Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"fieldname": "title",
|
"fieldname": "title",
|
||||||
@@ -392,7 +387,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-04-28 06:18:05.661710",
|
"modified": "2026-05-28 12:28:46.606963",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Request for Quotation",
|
"name": "Request for Quotation",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import json
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from frappe.contacts.doctype.contact.contact import get_full_name
|
||||||
from frappe.core.doctype.communication.email import make
|
from frappe.core.doctype.communication.email import make
|
||||||
from frappe.desk.form.load import get_attachments
|
from frappe.desk.form.load import get_attachments
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
@@ -275,12 +276,20 @@ class RequestforQuotation(BuyingController):
|
|||||||
supplier_doc.save()
|
supplier_doc.save()
|
||||||
|
|
||||||
def create_user(self, rfq_supplier, link):
|
def create_user(self, rfq_supplier, link):
|
||||||
|
contact_name = None
|
||||||
|
if rfq_supplier.contact:
|
||||||
|
name_fields = frappe.get_value(
|
||||||
|
"Contact", rfq_supplier.contact, ["first_name", "middle_name", "last_name"]
|
||||||
|
)
|
||||||
|
if name_fields:
|
||||||
|
contact_name = get_full_name(*name_fields)
|
||||||
|
|
||||||
user = frappe.get_doc(
|
user = frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "User",
|
"doctype": "User",
|
||||||
"send_welcome_email": 0,
|
"send_welcome_email": 0,
|
||||||
"email": rfq_supplier.email_id,
|
"email": rfq_supplier.email_id,
|
||||||
"first_name": rfq_supplier.supplier_name or rfq_supplier.supplier,
|
"first_name": contact_name or rfq_supplier.supplier_name or rfq_supplier.supplier,
|
||||||
"user_type": "Website User",
|
"user_type": "Website User",
|
||||||
"redirect_url": link,
|
"redirect_url": link,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,6 @@
|
|||||||
"quotation_number",
|
"quotation_number",
|
||||||
"has_unit_price_items",
|
"has_unit_price_items",
|
||||||
"amended_from",
|
"amended_from",
|
||||||
"section_break_kumc",
|
|
||||||
"title",
|
|
||||||
"accounting_dimensions_section",
|
"accounting_dimensions_section",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
"dimension_col_break",
|
"dimension_col_break",
|
||||||
@@ -118,6 +116,7 @@
|
|||||||
"more_info",
|
"more_info",
|
||||||
"is_subcontracted",
|
"is_subcontracted",
|
||||||
"column_break_57",
|
"column_break_57",
|
||||||
|
"title",
|
||||||
"opportunity",
|
"opportunity",
|
||||||
"connections_tab"
|
"connections_tab"
|
||||||
],
|
],
|
||||||
@@ -940,10 +939,6 @@
|
|||||||
"fieldname": "auto_repeat_section",
|
"fieldname": "auto_repeat_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Auto Repeat"
|
"label": "Auto Repeat"
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "section_break_kumc",
|
|
||||||
"fieldtype": "Section Break"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
@@ -952,7 +947,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-04-28 06:23:52.813948",
|
"modified": "2026-05-28 12:29:37.509487",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Supplier Quotation",
|
"name": "Supplier Quotation",
|
||||||
|
|||||||
@@ -390,10 +390,15 @@ def get_delivery_notes_to_be_billed(
|
|||||||
.where((DeliveryNote.docstatus == 1) & (DeliveryNote.is_return == 0) & (DeliveryNote.per_billed > 0))
|
.where((DeliveryNote.docstatus == 1) & (DeliveryNote.is_return == 0) & (DeliveryNote.per_billed > 0))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
query = frappe.qb.get_query(
|
||||||
|
"Delivery Note",
|
||||||
|
fields=fields,
|
||||||
|
filters=filters,
|
||||||
|
ignore_permissions=False,
|
||||||
|
)
|
||||||
|
|
||||||
query = (
|
query = (
|
||||||
frappe.qb.from_(DeliveryNote)
|
query.where(
|
||||||
.select(*[DeliveryNote[f] for f in fields])
|
|
||||||
.where(
|
|
||||||
(DeliveryNote.docstatus == 1)
|
(DeliveryNote.docstatus == 1)
|
||||||
& (DeliveryNote.status.notin(["Stopped", "Closed"]))
|
& (DeliveryNote.status.notin(["Stopped", "Closed"]))
|
||||||
& (DeliveryNote[searchfield].like(f"%{txt}%"))
|
& (DeliveryNote[searchfield].like(f"%{txt}%"))
|
||||||
@@ -407,12 +412,11 @@ def get_delivery_notes_to_be_billed(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.orderby(DeliveryNote[searchfield], order=Order.asc)
|
||||||
|
.limit(page_len)
|
||||||
|
.offset(start)
|
||||||
)
|
)
|
||||||
if filters and isinstance(filters, dict):
|
|
||||||
for key, value in filters.items():
|
|
||||||
query = query.where(DeliveryNote[key] == value)
|
|
||||||
|
|
||||||
query = query.orderby(DeliveryNote[searchfield], order=Order.asc).limit(page_len).offset(start)
|
|
||||||
return query.run(as_dict=as_dict)
|
return query.run(as_dict=as_dict)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -262,15 +262,17 @@ class StatusUpdater(Document):
|
|||||||
|
|
||||||
def validate_qty(self):
|
def validate_qty(self):
|
||||||
"""Validates qty at row level"""
|
"""Validates qty at row level"""
|
||||||
self.item_allowance = {}
|
|
||||||
self.global_qty_allowance = None
|
|
||||||
self.global_amount_allowance = None
|
|
||||||
|
|
||||||
for args in self.status_updater:
|
for args in self.status_updater:
|
||||||
if "target_ref_field" not in args or args.get("validate_qty") is False:
|
if "target_ref_field" not in args or args.get("validate_qty") is False:
|
||||||
# if target_ref_field is not specified or validate_qty is explicitly set to False, skip validation
|
# if target_ref_field is not specified or validate_qty is explicitly set to False, skip validation
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Reset per-args so each config block uses its own allowance source without
|
||||||
|
# leaking cached values from a previous config block.
|
||||||
|
self.item_allowance = {}
|
||||||
|
self.global_qty_allowance = None
|
||||||
|
self.global_amount_allowance = None
|
||||||
|
|
||||||
items_to_validate = []
|
items_to_validate = []
|
||||||
selling_negative_rate_allowed = frappe.get_single_value(
|
selling_negative_rate_allowed = frappe.get_single_value(
|
||||||
"Selling Settings", "allow_negative_rates_for_items"
|
"Selling Settings", "allow_negative_rates_for_items"
|
||||||
@@ -402,9 +404,12 @@ class StatusUpdater(Document):
|
|||||||
|
|
||||||
def check_overflow_with_allowance(self, item, args):
|
def check_overflow_with_allowance(self, item, args):
|
||||||
"""
|
"""
|
||||||
Checks if there is overflow condering a relaxation allowance
|
Checks if there is overflow considering a relaxation allowance.
|
||||||
"""
|
"""
|
||||||
qty_or_amount = "qty" if "qty" in args["target_ref_field"] else "amount"
|
qty_or_amount = "qty" if "qty" in args["target_ref_field"] else "amount"
|
||||||
|
global_qty_allowance_field = args.get("global_allowance_field", "over_delivery_receipt_allowance")
|
||||||
|
global_qty_allowance_doctype = args.get("global_allowance_doctype", "Stock Settings")
|
||||||
|
item_qty_allowance_field = args.get("item_allowance_field", "over_delivery_receipt_allowance")
|
||||||
|
|
||||||
# check if overflow is within allowance
|
# check if overflow is within allowance
|
||||||
(
|
(
|
||||||
@@ -419,6 +424,9 @@ class StatusUpdater(Document):
|
|||||||
self.global_qty_allowance,
|
self.global_qty_allowance,
|
||||||
self.global_amount_allowance,
|
self.global_amount_allowance,
|
||||||
qty_or_amount,
|
qty_or_amount,
|
||||||
|
global_qty_allowance_field,
|
||||||
|
global_qty_allowance_doctype,
|
||||||
|
item_qty_allowance_field,
|
||||||
)
|
)
|
||||||
if args["source_dt"] != "Pick List Item"
|
if args["source_dt"] != "Pick List Item"
|
||||||
else (0, {}, None, None)
|
else (0, {}, None, None)
|
||||||
@@ -463,7 +471,9 @@ class StatusUpdater(Document):
|
|||||||
"Quotation Item",
|
"Quotation Item",
|
||||||
"Packed Item",
|
"Packed Item",
|
||||||
]:
|
]:
|
||||||
if qty_or_amount == "qty":
|
if args.get("target_dt") == "Material Request Item":
|
||||||
|
action_msg = _('To allow over ordering, update "Over Order Allowance" in Buying Settings.')
|
||||||
|
elif qty_or_amount == "qty":
|
||||||
action_msg = _(
|
action_msg = _(
|
||||||
'To allow over receipt / delivery, update "Over Receipt/Delivery Allowance" in Stock Settings or the Item.'
|
'To allow over receipt / delivery, update "Over Receipt/Delivery Allowance" in Stock Settings or the Item.'
|
||||||
)
|
)
|
||||||
@@ -724,16 +734,28 @@ class StatusUpdater(Document):
|
|||||||
ref_doc.set_status(update=True)
|
ref_doc.set_status(update=True)
|
||||||
|
|
||||||
|
|
||||||
@frappe.request_cache
|
|
||||||
def get_allowance_for(
|
def get_allowance_for(
|
||||||
item_code,
|
item_code,
|
||||||
item_allowance=None,
|
item_allowance=None,
|
||||||
global_qty_allowance=None,
|
global_qty_allowance=None,
|
||||||
global_amount_allowance=None,
|
global_amount_allowance=None,
|
||||||
qty_or_amount="qty",
|
qty_or_amount="qty",
|
||||||
|
global_qty_allowance_field="over_delivery_receipt_allowance",
|
||||||
|
global_qty_allowance_doctype="Stock Settings",
|
||||||
|
item_qty_allowance_field="over_delivery_receipt_allowance",
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Returns the allowance for the item, if not set, returns global allowance
|
Returns the allowance for the item, if not set, returns global allowance.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
item_code: The item to get allowance for.
|
||||||
|
item_allowance: Cached per-item allowances from a previous call.
|
||||||
|
global_qty_allowance: Cached global qty allowance from a previous call.
|
||||||
|
global_amount_allowance: Cached global amount allowance from a previous call.
|
||||||
|
qty_or_amount: Whether to return qty or amount allowance.
|
||||||
|
global_qty_allowance_field: The field name on the settings doctype to use for the global qty allowance.
|
||||||
|
global_qty_allowance_doctype: The settings doctype to read the global qty allowance from.
|
||||||
|
item_qty_allowance_field: The field name on the Item doctype to use for the item-level qty allowance override.
|
||||||
"""
|
"""
|
||||||
if item_allowance is None:
|
if item_allowance is None:
|
||||||
item_allowance = {}
|
item_allowance = {}
|
||||||
@@ -755,13 +777,13 @@ def get_allowance_for(
|
|||||||
)
|
)
|
||||||
|
|
||||||
qty_allowance, over_billing_allowance = frappe.get_cached_value(
|
qty_allowance, over_billing_allowance = frappe.get_cached_value(
|
||||||
"Item", item_code, ["over_delivery_receipt_allowance", "over_billing_allowance"]
|
"Item", item_code, [item_qty_allowance_field, "over_billing_allowance"]
|
||||||
)
|
)
|
||||||
|
|
||||||
if qty_or_amount == "qty" and not qty_allowance:
|
if qty_or_amount == "qty" and not qty_allowance:
|
||||||
if global_qty_allowance is None:
|
if global_qty_allowance is None:
|
||||||
global_qty_allowance = flt(
|
global_qty_allowance = flt(
|
||||||
frappe.get_cached_value("Stock Settings", None, "over_delivery_receipt_allowance")
|
frappe.get_single_value(global_qty_allowance_doctype, global_qty_allowance_field)
|
||||||
)
|
)
|
||||||
qty_allowance = global_qty_allowance
|
qty_allowance = global_qty_allowance
|
||||||
elif qty_or_amount == "amount" and not over_billing_allowance:
|
elif qty_or_amount == "amount" and not over_billing_allowance:
|
||||||
|
|||||||
@@ -2112,7 +2112,7 @@ def repost_required_for_queue(doc: StockController) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def check_item_quality_inspection(doctype, items):
|
def check_item_quality_inspection(doctype: str, docstatus: str | int, items: str | list[dict]):
|
||||||
if isinstance(items, str):
|
if isinstance(items, str):
|
||||||
items = json.loads(items)
|
items = json.loads(items)
|
||||||
|
|
||||||
@@ -2124,13 +2124,30 @@ def check_item_quality_inspection(doctype, items):
|
|||||||
"Delivery Note": "inspection_required_before_delivery",
|
"Delivery Note": "inspection_required_before_delivery",
|
||||||
}
|
}
|
||||||
|
|
||||||
items_to_remove = []
|
inspection_fieldname = inspection_fieldname_map.get(doctype)
|
||||||
for item in items:
|
if inspection_fieldname is None:
|
||||||
if not frappe.db.get_value("Item", item.get("item_code"), inspection_fieldname_map.get(doctype)):
|
return []
|
||||||
items_to_remove.append(item)
|
|
||||||
items = [item for item in items if item not in items_to_remove]
|
|
||||||
|
|
||||||
return items
|
allow_after_transaction = cint(docstatus) == 1 and frappe.get_single_value(
|
||||||
|
"Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery"
|
||||||
|
)
|
||||||
|
|
||||||
|
if allow_after_transaction:
|
||||||
|
return items
|
||||||
|
|
||||||
|
item_codes = list({item.get("item_code") for item in items})
|
||||||
|
|
||||||
|
Item = frappe.qb.DocType("Item")
|
||||||
|
results = (
|
||||||
|
frappe.qb.from_(Item)
|
||||||
|
.select(Item.name)
|
||||||
|
.where((Item.name.isin(item_codes)) & (Item[inspection_fieldname] == 1))
|
||||||
|
.run(as_dict=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
inspection_required_items = {row.name for row in results}
|
||||||
|
|
||||||
|
return [item for item in items if item.get("item_code") in inspection_required_items]
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from collections import defaultdict
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from frappe.model.document import Document
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
from frappe.utils import cint, flt, get_link_to_form
|
from frappe.utils import cint, flt, get_link_to_form
|
||||||
|
|
||||||
@@ -1568,7 +1569,13 @@ def make_return_stock_entry_for_subcontract(
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_materials_from_supplier(subcontract_order, rm_details, order_doctype="Subcontracting Order"):
|
def get_materials_from_supplier(source_name: str, target_doc: Document | str | None = None):
|
||||||
|
args = frappe.flags.args or {}
|
||||||
|
|
||||||
|
subcontract_order = args.get("subcontract_order") or source_name
|
||||||
|
rm_details = args.get("rm_details")
|
||||||
|
order_doctype = args.get("order_doctype") or "Subcontracting Order"
|
||||||
|
|
||||||
if isinstance(rm_details, str):
|
if isinstance(rm_details, str):
|
||||||
rm_details = json.loads(rm_details)
|
rm_details = json.loads(rm_details)
|
||||||
|
|
||||||
|
|||||||
@@ -347,7 +347,12 @@ class TestSubcontractingController(ERPNextTestSuite):
|
|||||||
|
|
||||||
sco.load_from_db()
|
sco.load_from_db()
|
||||||
self.assertEqual(sco.supplied_items[0].consumed_qty, 5)
|
self.assertEqual(sco.supplied_items[0].consumed_qty, 5)
|
||||||
doc = get_materials_from_supplier(sco.name, [d.name for d in sco.supplied_items])
|
frappe.flags.args = frappe._dict(
|
||||||
|
subcontract_order=sco.name,
|
||||||
|
rm_details=[d.name for d in sco.supplied_items],
|
||||||
|
order_doctype=sco.doctype,
|
||||||
|
)
|
||||||
|
doc = get_materials_from_supplier(sco.name)
|
||||||
doc.save()
|
doc.save()
|
||||||
self.assertEqual(doc.items[0].qty, 1)
|
self.assertEqual(doc.items[0].qty, 1)
|
||||||
self.assertEqual(doc.items[0].s_warehouse, "_Test Warehouse 1 - _TC")
|
self.assertEqual(doc.items[0].s_warehouse, "_Test Warehouse 1 - _TC")
|
||||||
@@ -404,7 +409,12 @@ class TestSubcontractingController(ERPNextTestSuite):
|
|||||||
|
|
||||||
sco.load_from_db()
|
sco.load_from_db()
|
||||||
self.assertEqual(sco.supplied_items[0].consumed_qty, 5)
|
self.assertEqual(sco.supplied_items[0].consumed_qty, 5)
|
||||||
doc = get_materials_from_supplier(sco.name, [d.name for d in sco.supplied_items])
|
frappe.flags.args = frappe._dict(
|
||||||
|
subcontract_order=sco.name,
|
||||||
|
rm_details=[d.name for d in sco.supplied_items],
|
||||||
|
order_doctype=sco.doctype,
|
||||||
|
)
|
||||||
|
doc = get_materials_from_supplier(sco.name)
|
||||||
self.assertEqual(doc.items[0].qty, 1)
|
self.assertEqual(doc.items[0].qty, 1)
|
||||||
self.assertEqual(doc.items[0].s_warehouse, "_Test Warehouse 1 - _TC")
|
self.assertEqual(doc.items[0].s_warehouse, "_Test Warehouse 1 - _TC")
|
||||||
self.assertEqual(doc.items[0].t_warehouse, "_Test Warehouse - _TC")
|
self.assertEqual(doc.items[0].t_warehouse, "_Test Warehouse - _TC")
|
||||||
@@ -1133,7 +1143,12 @@ class TestSubcontractingController(ERPNextTestSuite):
|
|||||||
|
|
||||||
sco.load_from_db()
|
sco.load_from_db()
|
||||||
self.assertEqual(sco.supplied_items[0].consumed_qty, 5)
|
self.assertEqual(sco.supplied_items[0].consumed_qty, 5)
|
||||||
doc = get_materials_from_supplier(sco.name, [d.name for d in sco.supplied_items])
|
frappe.flags.args = frappe._dict(
|
||||||
|
subcontract_order=sco.name,
|
||||||
|
rm_details=[d.name for d in sco.supplied_items],
|
||||||
|
order_doctype=sco.doctype,
|
||||||
|
)
|
||||||
|
doc = get_materials_from_supplier(sco.name)
|
||||||
self.assertEqual(doc.items[0].qty, 1)
|
self.assertEqual(doc.items[0].qty, 1)
|
||||||
self.assertEqual(doc.items[0].s_warehouse, "_Test Warehouse 1 - _TC")
|
self.assertEqual(doc.items[0].s_warehouse, "_Test Warehouse 1 - _TC")
|
||||||
self.assertEqual(doc.items[0].t_warehouse, "_Test Warehouse - _TC")
|
self.assertEqual(doc.items[0].t_warehouse, "_Test Warehouse - _TC")
|
||||||
|
|||||||
97360
erpnext/locale/af.po
97360
erpnext/locale/af.po
File diff suppressed because it is too large
Load Diff
21132
erpnext/locale/ar.po
21132
erpnext/locale/ar.po
File diff suppressed because it is too large
Load Diff
14823
erpnext/locale/bs.po
14823
erpnext/locale/bs.po
File diff suppressed because it is too large
Load Diff
14087
erpnext/locale/cs.po
14087
erpnext/locale/cs.po
File diff suppressed because it is too large
Load Diff
13791
erpnext/locale/da.po
13791
erpnext/locale/da.po
File diff suppressed because it is too large
Load Diff
14665
erpnext/locale/de.po
14665
erpnext/locale/de.po
File diff suppressed because it is too large
Load Diff
14004
erpnext/locale/eo.po
14004
erpnext/locale/eo.po
File diff suppressed because it is too large
Load Diff
17888
erpnext/locale/es.po
17888
erpnext/locale/es.po
File diff suppressed because it is too large
Load Diff
14182
erpnext/locale/fa.po
14182
erpnext/locale/fa.po
File diff suppressed because it is too large
Load Diff
97360
erpnext/locale/fi.po
97360
erpnext/locale/fi.po
File diff suppressed because it is too large
Load Diff
13974
erpnext/locale/fr.po
13974
erpnext/locale/fr.po
File diff suppressed because it is too large
Load Diff
14861
erpnext/locale/hr.po
14861
erpnext/locale/hr.po
File diff suppressed because it is too large
Load Diff
14555
erpnext/locale/hu.po
14555
erpnext/locale/hu.po
File diff suppressed because it is too large
Load Diff
14015
erpnext/locale/id.po
14015
erpnext/locale/id.po
File diff suppressed because it is too large
Load Diff
14267
erpnext/locale/it.po
14267
erpnext/locale/it.po
File diff suppressed because it is too large
Load Diff
59482
erpnext/locale/ko.po
Normal file
59482
erpnext/locale/ko.po
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
14240
erpnext/locale/my.po
14240
erpnext/locale/my.po
File diff suppressed because it is too large
Load Diff
13926
erpnext/locale/nb.po
13926
erpnext/locale/nb.po
File diff suppressed because it is too large
Load Diff
30812
erpnext/locale/nl.po
30812
erpnext/locale/nl.po
File diff suppressed because it is too large
Load Diff
14163
erpnext/locale/pl.po
14163
erpnext/locale/pl.po
File diff suppressed because it is too large
Load Diff
14129
erpnext/locale/pt.po
14129
erpnext/locale/pt.po
File diff suppressed because it is too large
Load Diff
14365
erpnext/locale/pt_BR.po
14365
erpnext/locale/pt_BR.po
File diff suppressed because it is too large
Load Diff
14168
erpnext/locale/ru.po
14168
erpnext/locale/ru.po
File diff suppressed because it is too large
Load Diff
13920
erpnext/locale/sl.po
13920
erpnext/locale/sl.po
File diff suppressed because it is too large
Load Diff
14172
erpnext/locale/sr.po
14172
erpnext/locale/sr.po
File diff suppressed because it is too large
Load Diff
14175
erpnext/locale/sr_CS.po
14175
erpnext/locale/sr_CS.po
File diff suppressed because it is too large
Load Diff
14746
erpnext/locale/sv.po
14746
erpnext/locale/sv.po
File diff suppressed because it is too large
Load Diff
13791
erpnext/locale/ta.po
13791
erpnext/locale/ta.po
File diff suppressed because it is too large
Load Diff
21418
erpnext/locale/th.po
21418
erpnext/locale/th.po
File diff suppressed because it is too large
Load Diff
14138
erpnext/locale/tr.po
14138
erpnext/locale/tr.po
File diff suppressed because it is too large
Load Diff
31116
erpnext/locale/vi.po
31116
erpnext/locale/vi.po
File diff suppressed because it is too large
Load Diff
14161
erpnext/locale/zh.po
14161
erpnext/locale/zh.po
File diff suppressed because it is too large
Load Diff
95412
erpnext/locale/zh_TW.po
95412
erpnext/locale/zh_TW.po
File diff suppressed because it is too large
Load Diff
@@ -583,7 +583,7 @@ frappe.ui.form.on("BOM", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
routing(frm) {
|
routing(frm) {
|
||||||
if (frm.doc.routing) {
|
if (frm.doc.routing && frm.doc.with_operations && !frm.doc.operations) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
doc: frm.doc,
|
doc: frm.doc,
|
||||||
method: "get_routing",
|
method: "get_routing",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"actions": [],
|
"actions": [],
|
||||||
|
"allow_bulk_edit": 1,
|
||||||
"creation": "2013-02-22 01:27:49",
|
"creation": "2013-02-22 01:27:49",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"document_type": "Setup",
|
"document_type": "Setup",
|
||||||
@@ -139,11 +140,13 @@
|
|||||||
"label": "Image"
|
"label": "Image"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"default": "1",
|
||||||
"fetch_from": "operation.batch_size",
|
"fetch_from": "operation.batch_size",
|
||||||
"fetch_if_empty": 1,
|
"fetch_if_empty": 1,
|
||||||
"fieldname": "batch_size",
|
"fieldname": "batch_size",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Float",
|
||||||
"label": "Batch Size"
|
"label": "Batch Size",
|
||||||
|
"non_negative": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.parenttype == \"Routing\" || !parent.routing",
|
"depends_on": "eval:doc.parenttype == \"Routing\" || !parent.routing",
|
||||||
@@ -304,7 +307,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-04-01 17:09:48.771834",
|
"modified": "2026-05-25 17:15:42.044630",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "BOM Operation",
|
"name": "BOM Operation",
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class BOMOperation(Document):
|
|||||||
base_cost_per_unit: DF.Float
|
base_cost_per_unit: DF.Float
|
||||||
base_hour_rate: DF.Currency
|
base_hour_rate: DF.Currency
|
||||||
base_operating_cost: DF.Currency
|
base_operating_cost: DF.Currency
|
||||||
batch_size: DF.Int
|
batch_size: DF.Float
|
||||||
bom_no: DF.Link | None
|
bom_no: DF.Link | None
|
||||||
cost_per_unit: DF.Float
|
cost_per_unit: DF.Float
|
||||||
description: DF.TextEditor | None
|
description: DF.TextEditor | None
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ class JobCard(Document):
|
|||||||
self.validate_semi_finished_goods()
|
self.validate_semi_finished_goods()
|
||||||
|
|
||||||
def validate_semi_finished_goods(self):
|
def validate_semi_finished_goods(self):
|
||||||
if not self.track_semi_finished_goods:
|
if not self.track_semi_finished_goods or self.is_subcontracted:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.items and not self.transferred_qty and not self.skip_material_transfer:
|
if self.items and not self.transferred_qty and not self.skip_material_transfer:
|
||||||
@@ -1578,9 +1578,7 @@ def make_subcontracting_po(source_name, target_doc=None):
|
|||||||
"Job Card",
|
"Job Card",
|
||||||
source_name,
|
source_name,
|
||||||
{
|
{
|
||||||
"Job Card": {
|
"Job Card": {"doctype": "Purchase Order", "field_no_map": ["naming_series"]},
|
||||||
"doctype": "Purchase Order",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
target_doc,
|
target_doc,
|
||||||
set_missing_values,
|
set_missing_values,
|
||||||
|
|||||||
@@ -4223,6 +4223,7 @@ class TestWorkOrder(ERPNextTestSuite):
|
|||||||
"operations",
|
"operations",
|
||||||
{
|
{
|
||||||
"operation": fg_operation.name,
|
"operation": fg_operation.name,
|
||||||
|
"batch_size": fg_operation.batch_size,
|
||||||
"time_in_mins": 60,
|
"time_in_mins": 60,
|
||||||
"workstation": workstation.name,
|
"workstation": workstation.name,
|
||||||
},
|
},
|
||||||
@@ -4231,6 +4232,7 @@ class TestWorkOrder(ERPNextTestSuite):
|
|||||||
fg_bom.items[0].bom_no = subassembly_bom.name
|
fg_bom.items[0].bom_no = subassembly_bom.name
|
||||||
fg_bom.save()
|
fg_bom.save()
|
||||||
fg_bom.submit()
|
fg_bom.submit()
|
||||||
|
self.assertEqual(fg_bom.operations[0].batch_size, 25)
|
||||||
|
|
||||||
wo_order = make_wo_order_test_record(
|
wo_order = make_wo_order_test_record(
|
||||||
item=fg_item.name,
|
item=fg_item.name,
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ class WorkOrder(Document):
|
|||||||
self.calculate_operating_cost()
|
self.calculate_operating_cost()
|
||||||
self.validate_qty()
|
self.validate_qty()
|
||||||
self.validate_transfer_against()
|
self.validate_transfer_against()
|
||||||
self.validate_operation_time()
|
self.validate_operations()
|
||||||
self.status = self.get_status()
|
self.status = self.get_status()
|
||||||
self.validate_workstation_type()
|
self.validate_workstation_type()
|
||||||
self.reset_use_multi_level_bom()
|
self.reset_use_multi_level_bom()
|
||||||
@@ -1499,8 +1499,11 @@ class WorkOrder(Document):
|
|||||||
title=_("Missing value"),
|
title=_("Missing value"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate_operation_time(self):
|
def validate_operations(self):
|
||||||
for d in self.operations:
|
for d in self.operations:
|
||||||
|
if not d.batch_size or d.batch_size <= 0:
|
||||||
|
d.batch_size = 1
|
||||||
|
|
||||||
if d.time_in_mins <= 0:
|
if d.time_in_mins <= 0:
|
||||||
frappe.throw(_("Operation Time must be greater than 0 for Operation {0}").format(d.operation))
|
frappe.throw(_("Operation Time must be greater than 0 for Operation {0}").format(d.operation))
|
||||||
|
|
||||||
|
|||||||
@@ -196,10 +196,11 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"default": "1",
|
||||||
"fieldname": "batch_size",
|
"fieldname": "batch_size",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Batch Size",
|
"label": "Batch Size",
|
||||||
"read_only": 1
|
"non_negative": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "sequence_id",
|
"fieldname": "sequence_id",
|
||||||
@@ -316,7 +317,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-05-20 13:01:21.827200",
|
"modified": "2026-05-25 17:15:12.038470",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Work Order Operation",
|
"name": "Work Order Operation",
|
||||||
|
|||||||
@@ -2929,10 +2929,28 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
method: "erpnext.controllers.stock_controller.check_item_quality_inspection",
|
method: "erpnext.controllers.stock_controller.check_item_quality_inspection",
|
||||||
args: {
|
args: {
|
||||||
doctype: this.frm.doc.doctype,
|
doctype: this.frm.doc.doctype,
|
||||||
|
docstatus: this.frm.doc.docstatus,
|
||||||
items: this.frm.doc.items,
|
items: this.frm.doc.items,
|
||||||
},
|
},
|
||||||
freeze: true,
|
freeze: true,
|
||||||
callback: function (r) {
|
callback: function (r) {
|
||||||
|
if (r.message.length == 0) {
|
||||||
|
let type = inspection_type === "Incoming" ? "Purchase" : "Delivery";
|
||||||
|
let fieldname =
|
||||||
|
inspection_type === "Incoming"
|
||||||
|
? "Inspection Required before Purchase"
|
||||||
|
: "Inspection Required before Delivery";
|
||||||
|
|
||||||
|
frappe.msgprint({
|
||||||
|
title: __("Quality Inspection Not Configured"),
|
||||||
|
message: __(`Enable <b>{0}</b> on the Item master to proceed with {1} inspection.`, [
|
||||||
|
fieldname,
|
||||||
|
type,
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
r.message.forEach((item) => {
|
r.message.forEach((item) => {
|
||||||
if (me.has_inspection_required(item)) {
|
if (me.has_inspection_required(item)) {
|
||||||
let dialog_items = dialog.fields_dict.items;
|
let dialog_items = dialog.fields_dict.items;
|
||||||
|
|||||||
@@ -22,8 +22,6 @@
|
|||||||
"company",
|
"company",
|
||||||
"has_unit_price_items",
|
"has_unit_price_items",
|
||||||
"amended_from",
|
"amended_from",
|
||||||
"section_break_jdzz",
|
|
||||||
"title",
|
|
||||||
"currency_and_price_list",
|
"currency_and_price_list",
|
||||||
"currency",
|
"currency",
|
||||||
"conversion_rate",
|
"conversion_rate",
|
||||||
@@ -135,6 +133,7 @@
|
|||||||
"status",
|
"status",
|
||||||
"customer_group",
|
"customer_group",
|
||||||
"territory",
|
"territory",
|
||||||
|
"title",
|
||||||
"column_break_108",
|
"column_break_108",
|
||||||
"opportunity",
|
"opportunity",
|
||||||
"enq_det",
|
"enq_det",
|
||||||
@@ -304,7 +303,6 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:(doc.quotation_to=='Customer' && doc.party_name)",
|
|
||||||
"fieldname": "col_break98",
|
"fieldname": "col_break98",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break",
|
||||||
"width": "50%"
|
"width": "50%"
|
||||||
@@ -1125,10 +1123,6 @@
|
|||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Auto Repeat"
|
"label": "Auto Repeat"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "section_break_jdzz",
|
|
||||||
"fieldtype": "Section Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"fieldname": "title",
|
"fieldname": "title",
|
||||||
@@ -1142,7 +1136,7 @@
|
|||||||
"idx": 82,
|
"idx": 82,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-04-28 06:28:13.103302",
|
"modified": "2026-05-30 17:40:02.667637",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Quotation",
|
"name": "Quotation",
|
||||||
|
|||||||
@@ -26,8 +26,6 @@
|
|||||||
"has_unit_price_items",
|
"has_unit_price_items",
|
||||||
"is_subcontracted",
|
"is_subcontracted",
|
||||||
"amended_from",
|
"amended_from",
|
||||||
"section_break_zstt",
|
|
||||||
"title",
|
|
||||||
"accounting_dimensions_section",
|
"accounting_dimensions_section",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
"dimension_col_break",
|
"dimension_col_break",
|
||||||
@@ -176,6 +174,7 @@
|
|||||||
"po_no",
|
"po_no",
|
||||||
"po_date",
|
"po_date",
|
||||||
"represents_company",
|
"represents_company",
|
||||||
|
"title",
|
||||||
"column_break_yvzv",
|
"column_break_yvzv",
|
||||||
"inter_company_order_reference",
|
"inter_company_order_reference",
|
||||||
"party_account_currency",
|
"party_account_currency",
|
||||||
@@ -1747,10 +1746,6 @@
|
|||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "section_break_zstt",
|
|
||||||
"fieldtype": "Section Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"fieldname": "title",
|
"fieldname": "title",
|
||||||
@@ -1765,7 +1760,7 @@
|
|||||||
"idx": 105,
|
"idx": 105,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-05-01 02:37:30.937916",
|
"modified": "2026-05-28 11:41:11.823034",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Sales Order",
|
"name": "Sales Order",
|
||||||
|
|||||||
@@ -420,42 +420,80 @@ def get_past_order_list(search_term, status, limit=20):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def set_customer_info(fieldname, customer, value=""):
|
def set_customer_info(fieldname, customer, value=""):
|
||||||
|
customer_doc = frappe.get_doc("Customer", customer)
|
||||||
|
customer_doc.check_permission("write")
|
||||||
|
|
||||||
if fieldname == "loyalty_program":
|
if fieldname == "loyalty_program":
|
||||||
frappe.db.set_value("Customer", customer, "loyalty_program", value)
|
customer_doc.loyalty_program = value
|
||||||
|
else:
|
||||||
|
contact = customer_doc.get("customer_primary_contact")
|
||||||
|
if not contact:
|
||||||
|
Contact = DocType("Contact")
|
||||||
|
DynamicLink = DocType("Dynamic Link")
|
||||||
|
|
||||||
contact = frappe.get_cached_value("Customer", customer, "customer_primary_contact")
|
# Inner join with Contact DocType, to priorities records that have is_primary_contact set.
|
||||||
if not contact:
|
query = (
|
||||||
contact = frappe.db.sql(
|
frappe.qb.from_(DynamicLink)
|
||||||
"""
|
.join(Contact)
|
||||||
SELECT parent FROM `tabDynamic Link`
|
.on(DynamicLink.parent == Contact.name)
|
||||||
WHERE
|
.select(DynamicLink.parent)
|
||||||
parenttype = 'Contact' AND
|
.where(
|
||||||
parentfield = 'links' AND
|
(DynamicLink.link_name == customer)
|
||||||
link_doctype = 'Customer' AND
|
& (DynamicLink.parentfield == "links")
|
||||||
link_name = %s
|
& (DynamicLink.parenttype == "Contact")
|
||||||
""",
|
& (DynamicLink.link_doctype == "Customer")
|
||||||
(customer),
|
)
|
||||||
as_dict=1,
|
.orderby(Contact.is_primary_contact, order=Order.desc)
|
||||||
)
|
)
|
||||||
contact = contact[0].get("parent") if contact else None
|
|
||||||
|
|
||||||
if not contact:
|
contacts = query.run(pluck=DynamicLink.parent)
|
||||||
new_contact = frappe.new_doc("Contact")
|
|
||||||
new_contact.is_primary_contact = 1
|
|
||||||
new_contact.first_name = customer
|
|
||||||
new_contact.set("links", [{"link_doctype": "Customer", "link_name": customer}])
|
|
||||||
new_contact.save()
|
|
||||||
contact = new_contact.name
|
|
||||||
frappe.db.set_value("Customer", customer, "customer_primary_contact", contact)
|
|
||||||
|
|
||||||
contact_doc = frappe.get_doc("Contact", contact)
|
contact = contacts[0] if contacts else None
|
||||||
if fieldname == "email_id":
|
|
||||||
contact_doc.set("email_ids", [{"email_id": value, "is_primary": 1}])
|
if not contact:
|
||||||
frappe.db.set_value("Customer", customer, "email_id", value)
|
new_contact = frappe.new_doc("Contact")
|
||||||
elif fieldname == "mobile_no":
|
new_contact.is_primary_contact = 1
|
||||||
contact_doc.set("phone_nos", [{"phone": value, "is_primary_mobile_no": 1}])
|
new_contact.first_name = customer
|
||||||
frappe.db.set_value("Customer", customer, "mobile_no", value)
|
new_contact.set("links", [{"link_doctype": "Customer", "link_name": customer}])
|
||||||
contact_doc.save()
|
new_contact.save()
|
||||||
|
contact = new_contact.name
|
||||||
|
|
||||||
|
def set_primary_phone_no_email(field, value):
|
||||||
|
# Create new record instead deleting existing email or phone_no and setting the new row as primary.
|
||||||
|
field_mapper = {
|
||||||
|
"email_ids": {"field": "email_id", "primary": "is_primary"},
|
||||||
|
"phone_nos": {"field": "phone", "primary": "is_primary_mobile_no"},
|
||||||
|
}
|
||||||
|
|
||||||
|
value_already_exists = False
|
||||||
|
for d in contact_doc.get(field):
|
||||||
|
if d.get(field_mapper[field].get("field")) == value and not value_already_exists:
|
||||||
|
d.set(field_mapper[field]["primary"], 1)
|
||||||
|
value_already_exists = True
|
||||||
|
continue
|
||||||
|
d.set(field_mapper[field]["primary"], 0)
|
||||||
|
|
||||||
|
if not value_already_exists:
|
||||||
|
contact_doc.append(
|
||||||
|
field, {field_mapper[field]["field"]: value, field_mapper[field]["primary"]: 1}
|
||||||
|
)
|
||||||
|
|
||||||
|
contact_doc = frappe.get_doc("Contact", contact)
|
||||||
|
# setting is_primary_contact = 1 on Contact to refetch the same contact incase it's removed from Customer records.
|
||||||
|
contact_doc.set("is_primary_contact", 1)
|
||||||
|
if fieldname == "email_id":
|
||||||
|
set_primary_phone_no_email("email_ids", value)
|
||||||
|
elif fieldname == "mobile_no":
|
||||||
|
set_primary_phone_no_email("phone_nos", value)
|
||||||
|
# Saving contact_doc to set mobile_no and email.
|
||||||
|
contact_doc.save()
|
||||||
|
|
||||||
|
# Auto-fetches from Contact DocType, no need to set values separately.
|
||||||
|
customer_doc.customer_primary_contact = contact
|
||||||
|
|
||||||
|
# using save method instead db.set_value which bypasses the validation for loyalty program
|
||||||
|
# and auto sets the mobile_no and email field on customer records.
|
||||||
|
customer_doc.save()
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
set_opening_entry_status() {
|
set_opening_entry_status() {
|
||||||
this.page.set_title_sub(
|
this.page.set_title_sub(
|
||||||
`<span class="indicator orange">
|
`<span class="indicator orange">
|
||||||
<a class="text-muted" href="#Form/POS%20Opening%20Entry/${this.pos_opening}">
|
<a class="text-muted" href="#Form/POS%20Opening%20Entry/${encodeURIComponent(this.pos_opening)}">
|
||||||
Opened at ${frappe.datetime.str_to_user(this.pos_opening_time)}
|
Opened at ${frappe.datetime.str_to_user(this.pos_opening_time)}
|
||||||
</a>
|
</a>
|
||||||
</span>`
|
</span>`
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ erpnext.PointOfSale.ItemCart = class {
|
|||||||
me.$totals_section.find(".edit-cart-btn").click();
|
me.$totals_section.find(".edit-cart-btn").click();
|
||||||
}
|
}
|
||||||
|
|
||||||
const item_row_name = unescape($cart_item.attr("data-row-name"));
|
const item_row_name = $cart_item.attr("data-row-name");
|
||||||
me.events.cart_item_clicked({ name: item_row_name });
|
me.events.cart_item_clicked({ name: item_row_name });
|
||||||
this.numpad_value = "";
|
this.numpad_value = "";
|
||||||
});
|
});
|
||||||
@@ -464,10 +464,10 @@ erpnext.PointOfSale.ItemCart = class {
|
|||||||
<div class="customer-display">
|
<div class="customer-display">
|
||||||
${this.get_customer_image()}
|
${this.get_customer_image()}
|
||||||
<div class="customer-name-desc">
|
<div class="customer-name-desc">
|
||||||
<div class="customer-name">${customer_name}</div>
|
<div class="customer-name">${frappe.utils.escape_html(customer_name)}</div>
|
||||||
${get_customer_description()}
|
${get_customer_description()}
|
||||||
</div>
|
</div>
|
||||||
<div class="reset-customer-btn" data-customer="${escape(customer)}">
|
<div class="reset-customer-btn" data-customer="${frappe.utils.escape_html(customer)}">
|
||||||
<svg width="32" height="32" viewBox="0 0 14 14" fill="none">
|
<svg width="32" height="32" viewBox="0 0 14 14" fill="none">
|
||||||
<path d="M4.93764 4.93759L7.00003 6.99998M9.06243 9.06238L7.00003 6.99998M7.00003 6.99998L4.93764 9.06238L9.06243 4.93759" stroke="#8D99A6"/>
|
<path d="M4.93764 4.93759L7.00003 6.99998M9.06243 9.06238L7.00003 6.99998M7.00003 6.99998L4.93764 9.06238L9.06243 4.93759" stroke="#8D99A6"/>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -484,11 +484,13 @@ erpnext.PointOfSale.ItemCart = class {
|
|||||||
if (!email_id && !mobile_no) {
|
if (!email_id && !mobile_no) {
|
||||||
return `<div class="customer-desc">${__("Click to add email / phone")}</div>`;
|
return `<div class="customer-desc">${__("Click to add email / phone")}</div>`;
|
||||||
} else if (email_id && !mobile_no) {
|
} else if (email_id && !mobile_no) {
|
||||||
return `<div class="customer-desc">${email_id}</div>`;
|
return `<div class="customer-desc">${frappe.utils.escape_html(email_id)}</div>`;
|
||||||
} else if (mobile_no && !email_id) {
|
} else if (mobile_no && !email_id) {
|
||||||
return `<div class="customer-desc">${mobile_no}</div>`;
|
return `<div class="customer-desc">${frappe.utils.escape_html(mobile_no)}</div>`;
|
||||||
} else {
|
} else {
|
||||||
return `<div class="customer-desc">${email_id} - ${mobile_no}</div>`;
|
return `<div class="customer-desc">${frappe.utils.escape_html(
|
||||||
|
email_id
|
||||||
|
)} - ${frappe.utils.escape_html(mobile_no)}</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -496,9 +498,13 @@ erpnext.PointOfSale.ItemCart = class {
|
|||||||
get_customer_image() {
|
get_customer_image() {
|
||||||
const { customer, image } = this.customer_info || {};
|
const { customer, image } = this.customer_info || {};
|
||||||
if (image) {
|
if (image) {
|
||||||
return `<div class="customer-image"><img src="${image}" alt="${image}""></div>`;
|
return `<div class="customer-image"><img src="${frappe.utils.escape_html(
|
||||||
|
image
|
||||||
|
)}" alt="${frappe.utils.escape_html(image)}"></div>`;
|
||||||
} else {
|
} else {
|
||||||
return `<div class="customer-image customer-abbr">${frappe.get_abbr(customer)}</div>`;
|
return `<div class="customer-image customer-abbr">${frappe.utils.escape_html(
|
||||||
|
frappe.get_abbr(customer)
|
||||||
|
)}</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -559,7 +565,7 @@ erpnext.PointOfSale.ItemCart = class {
|
|||||||
.map((t) => {
|
.map((t) => {
|
||||||
if (t.tax_amount_after_discount_amount == 0.0) return;
|
if (t.tax_amount_after_discount_amount == 0.0) return;
|
||||||
return `<div class="tax-row">
|
return `<div class="tax-row">
|
||||||
<div class="tax-label">${t.description}</div>
|
<div class="tax-label">${frappe.utils.escape_html(t.description)}</div>
|
||||||
<div class="tax-value">${format_currency(t.tax_amount_after_discount_amount, currency)}</div>
|
<div class="tax-value">${format_currency(t.tax_amount_after_discount_amount, currency)}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
})
|
})
|
||||||
@@ -571,8 +577,9 @@ erpnext.PointOfSale.ItemCart = class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get_cart_item({ name }) {
|
get_cart_item({ name }) {
|
||||||
const item_selector = `.cart-item-wrapper[data-row-name="${escape(name)}"]`;
|
return this.$cart_items_wrapper.find(".cart-item-wrapper").filter(function () {
|
||||||
return this.$cart_items_wrapper.find(item_selector);
|
return $(this).attr("data-row-name") === name;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get_item_from_frm(item) {
|
get_item_from_frm(item) {
|
||||||
@@ -602,7 +609,9 @@ erpnext.PointOfSale.ItemCart = class {
|
|||||||
|
|
||||||
if (!$item_to_update.length) {
|
if (!$item_to_update.length) {
|
||||||
this.$cart_items_wrapper.append(
|
this.$cart_items_wrapper.append(
|
||||||
`<div class="cart-item-wrapper" data-row-name="${escape(item_data.name)}"></div>
|
`<div class="cart-item-wrapper" data-row-name="${frappe.utils.escape_html(
|
||||||
|
item_data.name
|
||||||
|
)}"></div>
|
||||||
<div class="seperator"></div>`
|
<div class="seperator"></div>`
|
||||||
);
|
);
|
||||||
$item_to_update = this.get_cart_item(item_data);
|
$item_to_update = this.get_cart_item(item_data);
|
||||||
@@ -612,7 +621,7 @@ erpnext.PointOfSale.ItemCart = class {
|
|||||||
`${get_item_image_html()}
|
`${get_item_image_html()}
|
||||||
<div class="item-name-desc">
|
<div class="item-name-desc">
|
||||||
<div class="item-name">
|
<div class="item-name">
|
||||||
${item_data.item_name}
|
${frappe.utils.escape_html(item_data.item_name)}
|
||||||
</div>
|
</div>
|
||||||
${get_description_html()}
|
${get_description_html()}
|
||||||
</div>
|
</div>
|
||||||
@@ -641,7 +650,7 @@ erpnext.PointOfSale.ItemCart = class {
|
|||||||
if (item_data.rate && item_data.amount && item_data.rate !== item_data.amount) {
|
if (item_data.rate && item_data.amount && item_data.rate !== item_data.amount) {
|
||||||
return `
|
return `
|
||||||
<div class="item-qty-rate">
|
<div class="item-qty-rate">
|
||||||
<div class="item-qty"><span>${item_data.qty || 0} ${item_data.uom}</span></div>
|
<div class="item-qty"><span>${item_data.qty || 0} ${frappe.utils.escape_html(item_data.uom)}</span></div>
|
||||||
<div class="item-rate-amount">
|
<div class="item-rate-amount">
|
||||||
<div class="item-rate">${format_currency(item_data.amount, currency)}</div>
|
<div class="item-rate">${format_currency(item_data.amount, currency)}</div>
|
||||||
<div class="item-amount">${format_currency(item_data.rate, currency)}</div>
|
<div class="item-amount">${format_currency(item_data.rate, currency)}</div>
|
||||||
@@ -650,7 +659,7 @@ erpnext.PointOfSale.ItemCart = class {
|
|||||||
} else {
|
} else {
|
||||||
return `
|
return `
|
||||||
<div class="item-qty-rate">
|
<div class="item-qty-rate">
|
||||||
<div class="item-qty"><span>${item_data.qty || 0} ${item_data.uom}</span></div>
|
<div class="item-qty"><span>${item_data.qty || 0} ${frappe.utils.escape_html(item_data.uom)}</span></div>
|
||||||
<div class="item-rate-amount">
|
<div class="item-rate-amount">
|
||||||
<div class="item-rate">${format_currency(item_data.rate, currency)}</div>
|
<div class="item-rate">${format_currency(item_data.rate, currency)}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -671,7 +680,7 @@ erpnext.PointOfSale.ItemCart = class {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
item_data.description = frappe.ellipsis(item_data.description, 45);
|
item_data.description = frappe.ellipsis(item_data.description, 45);
|
||||||
return `<div class="item-desc">${item_data.description}</div>`;
|
return `<div class="item-desc">${frappe.utils.escape_html(item_data.description)}</div>`;
|
||||||
}
|
}
|
||||||
return ``;
|
return ``;
|
||||||
}
|
}
|
||||||
@@ -683,22 +692,24 @@ erpnext.PointOfSale.ItemCart = class {
|
|||||||
<div class="item-image">
|
<div class="item-image">
|
||||||
<img
|
<img
|
||||||
onerror="cur_pos.cart.handle_broken_image(this)"
|
onerror="cur_pos.cart.handle_broken_image(this)"
|
||||||
src="${image}" alt="${frappe.get_abbr(item_name)}"">
|
src="${frappe.utils.escape_html(image)}" alt="${frappe.utils.escape_html(frappe.get_abbr(item_name))}">
|
||||||
</div>`;
|
</div>`;
|
||||||
} else {
|
} else {
|
||||||
return `<div class="item-image item-abbr">${frappe.get_abbr(item_name)}</div>`;
|
return `<div class="item-image item-abbr">${frappe.utils.escape_html(
|
||||||
|
frappe.get_abbr(item_name)
|
||||||
|
)}</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_broken_image($img) {
|
handle_broken_image($img) {
|
||||||
const item_abbr = $($img).attr("alt");
|
const item_abbr = frappe.utils.escape_html($($img).attr("alt"));
|
||||||
$($img).parent().replaceWith(`<div class="item-image item-abbr">${item_abbr}</div>`);
|
$($img).parent().replaceWith(`<div class="item-image item-abbr">${item_abbr}</div>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
update_selector_value_in_cart_item(selector, value, item) {
|
update_selector_value_in_cart_item(selector, value, item) {
|
||||||
const $item_to_update = this.get_cart_item(item);
|
const $item_to_update = this.get_cart_item(item);
|
||||||
$item_to_update.attr(`data-${selector}`, escape(value));
|
$item_to_update.attr(`data-${selector}`, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggle_checkout_btn(show_checkout) {
|
toggle_checkout_btn(show_checkout) {
|
||||||
@@ -899,8 +910,8 @@ erpnext.PointOfSale.ItemCart = class {
|
|||||||
<div class="customer-display">
|
<div class="customer-display">
|
||||||
${this.get_customer_image()}
|
${this.get_customer_image()}
|
||||||
<div class="customer-name-desc">
|
<div class="customer-name-desc">
|
||||||
<div class="customer-name">${customer_name}</div>
|
<div class="customer-name">${frappe.utils.escape_html(customer_name)}</div>
|
||||||
<div class="customer-desc">${customer}</div>
|
<div class="customer-desc">${frappe.utils.escape_html(customer)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="customer-fields-container">
|
<div class="customer-fields-container">
|
||||||
@@ -987,6 +998,7 @@ erpnext.PointOfSale.ItemCart = class {
|
|||||||
customer: current_customer,
|
customer: current_customer,
|
||||||
value: this.value,
|
value: this.value,
|
||||||
},
|
},
|
||||||
|
freeze: true,
|
||||||
callback: (r) => {
|
callback: (r) => {
|
||||||
if (!r.exc) {
|
if (!r.exc) {
|
||||||
me.customer_info[this.df.fieldname] = this.value;
|
me.customer_info[this.df.fieldname] = this.value;
|
||||||
@@ -1040,9 +1052,11 @@ erpnext.PointOfSale.ItemCart = class {
|
|||||||
};
|
};
|
||||||
|
|
||||||
transaction_container.append(
|
transaction_container.append(
|
||||||
`<div class="invoice-wrapper" data-invoice-name="${escape(invoice.name)}">
|
`<div class="invoice-wrapper" data-invoice-name="${frappe.utils.escape_html(
|
||||||
|
invoice.name
|
||||||
|
)}">
|
||||||
<div class="invoice-name-date">
|
<div class="invoice-name-date">
|
||||||
<div class="invoice-name">${invoice.name}</div>
|
<div class="invoice-name">${frappe.utils.escape_html(invoice.name)}</div>
|
||||||
<div class="invoice-date">${posting_datetime}</div>
|
<div class="invoice-date">${posting_datetime}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="invoice-total-status">
|
<div class="invoice-total-status">
|
||||||
@@ -1050,7 +1064,7 @@ erpnext.PointOfSale.ItemCart = class {
|
|||||||
${format_currency(invoice.grand_total, invoice.currency, frappe.sys_defaults.currency_precision) || 0}
|
${format_currency(invoice.grand_total, invoice.currency, frappe.sys_defaults.currency_precision) || 0}
|
||||||
</div>
|
</div>
|
||||||
<div class="invoice-status">
|
<div class="invoice-status">
|
||||||
<span class="indicator-pill whitespace-nowrap ${indicator_color[invoice.status]}">
|
<span class="indicator-pill whitespace-nowrap ${indicator_color[invoice.status] || ""}">
|
||||||
<span>${__(invoice.status)}</span>
|
<span>${__(invoice.status)}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -129,24 +129,26 @@ erpnext.PointOfSale.ItemDetails = class {
|
|||||||
return ``;
|
return ``;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$item_name.html(item_name);
|
this.$item_name.html(frappe.utils.escape_html(item_name));
|
||||||
this.$item_description.html(get_description_html());
|
this.$item_description.html(get_description_html());
|
||||||
this.$item_price.html(format_currency(price_list_rate, this.currency));
|
this.$item_price.html(format_currency(price_list_rate, this.currency));
|
||||||
if (!this.hide_images && image) {
|
if (!this.hide_images && image) {
|
||||||
this.$item_image.html(
|
this.$item_image.html(
|
||||||
`<img
|
`<img
|
||||||
onerror="cur_pos.item_details.handle_broken_image(this)"
|
onerror="cur_pos.item_details.handle_broken_image(this)"
|
||||||
class="h-full" src="${image}"
|
class="h-full" src="${frappe.utils.escape_html(image)}"
|
||||||
alt="${frappe.get_abbr(item_name)}"
|
alt="${frappe.utils.escape_html(frappe.get_abbr(item_name))}"
|
||||||
style="object-fit: cover;">`
|
style="object-fit: cover;">`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.$item_image.html(`<div class="item-abbr">${frappe.get_abbr(item_name)}</div>`);
|
this.$item_image.html(
|
||||||
|
`<div class="item-abbr">${frappe.utils.escape_html(frappe.get_abbr(item_name))}</div>`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_broken_image($img) {
|
handle_broken_image($img) {
|
||||||
const item_abbr = $($img).attr("alt");
|
const item_abbr = frappe.utils.escape_html($($img).attr("alt"));
|
||||||
$($img).replaceWith(`<div class="item-abbr">${item_abbr}</div>`);
|
$($img).replaceWith(`<div class="item-abbr">${item_abbr}</div>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -112,17 +112,37 @@ erpnext.PointOfSale.ItemSelector = class {
|
|||||||
|
|
||||||
render_item_list_column_header() {
|
render_item_list_column_header() {
|
||||||
return `<div class="list-column">
|
return `<div class="list-column">
|
||||||
<div class="column-name">Name</div>
|
<div class="column-name">${__("Name")}</div>
|
||||||
<div class="column-price">Price</div>
|
<div class="column-price">${__("Price")}</div>
|
||||||
<div class="column-uom">UOM</div>
|
<div class="column-uom">${__("UOM")}</div>
|
||||||
<div class="column-qty-available">Quantity Available</div>
|
<div class="column-qty-available">${__("Quantity Available")}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
get_item_html(item) {
|
get_item_html(item) {
|
||||||
const me = this;
|
const me = this;
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
const { item_image, serial_no, batch_no, barcode, actual_qty, uom, price_list_rate } = item;
|
function sanitize_item_data(item) {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(item).map(([key, value]) => [
|
||||||
|
key,
|
||||||
|
typeof value === "string" ? frappe.utils.escape_html(value) : value,
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const sanitize_item = sanitize_item_data(item);
|
||||||
|
const {
|
||||||
|
item_code,
|
||||||
|
stock_uom,
|
||||||
|
item_name,
|
||||||
|
item_image,
|
||||||
|
serial_no,
|
||||||
|
batch_no,
|
||||||
|
barcode,
|
||||||
|
actual_qty,
|
||||||
|
uom,
|
||||||
|
price_list_rate,
|
||||||
|
} = sanitize_item;
|
||||||
const precision = flt(price_list_rate, 2) % 1 != 0 ? 2 : 0;
|
const precision = flt(price_list_rate, 2) % 1 != 0 ? 2 : 0;
|
||||||
let indicator_color;
|
let indicator_color;
|
||||||
let qty_to_display = actual_qty;
|
let qty_to_display = actual_qty;
|
||||||
@@ -149,37 +169,41 @@ erpnext.PointOfSale.ItemSelector = class {
|
|||||||
<img
|
<img
|
||||||
onerror="cur_pos.item_selector.handle_broken_image(this)"
|
onerror="cur_pos.item_selector.handle_broken_image(this)"
|
||||||
class="item-img" src="${item_image}"
|
class="item-img" src="${item_image}"
|
||||||
alt="${item.item_name}"
|
alt="${item_name}"
|
||||||
>
|
>
|
||||||
</div>`;
|
</div>`;
|
||||||
} else {
|
} else {
|
||||||
return `<div class="item-qty-pill">
|
return `<div class="item-qty-pill">
|
||||||
<span class="indicator-pill whitespace-nowrap ${indicator_color}">${qty_to_display}</span>
|
<span class="indicator-pill whitespace-nowrap ${indicator_color}">${qty_to_display}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="item-display abbr">${frappe.get_abbr(item.item_name)}</div>`;
|
<div class="item-display abbr">${frappe.get_abbr(item_name)}</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return `<div class="item-wrapper"
|
return `<div class="item-wrapper"
|
||||||
data-item-code="${escape(item.item_code)}" data-serial-no="${escape(serial_no)}"
|
data-item-code="${item_code}" data-serial-no="${serial_no}"
|
||||||
data-batch-no="${escape(batch_no)}" data-uom="${escape(uom)}"
|
data-batch-no="${batch_no}" data-uom="${uom}"
|
||||||
data-rate="${escape(price_list_rate || 0)}"
|
data-rate="${price_list_rate || 0}"
|
||||||
data-stock-uom="${escape(item.stock_uom)}"
|
data-stock-uom="${stock_uom}"
|
||||||
title="${item.item_name}">
|
title="${item_name}">
|
||||||
|
|
||||||
${get_item_image_html()}
|
${get_item_image_html()}
|
||||||
|
|
||||||
<div class="item-detail">
|
<div class="item-detail">
|
||||||
<div class="item-name">
|
<div class="item-name">
|
||||||
${!me.hide_images ? frappe.ellipsis(item.item_name, 18) : item.item_name}
|
${!me.hide_images ? frappe.ellipsis(item_name, 18) : item_name}
|
||||||
</div>
|
</div>
|
||||||
${
|
${
|
||||||
!me.hide_images
|
!me.hide_images
|
||||||
? `<div class="item-rate">
|
? `<div class="item-rate">
|
||||||
${format_currency(price_list_rate, item.currency, precision) || 0} / ${uom}
|
${frappe.utils.escape_html(format_currency(price_list_rate, item.currency, precision)) || 0} / ${uom}
|
||||||
</div>`
|
</div>`
|
||||||
: `
|
: `
|
||||||
<div class="item-price">${format_currency(price_list_rate, item.currency, precision) || 0}</div>
|
<div class="item-price">${
|
||||||
|
frappe.utils.escape_html(
|
||||||
|
format_currency(price_list_rate, item.currency, precision)
|
||||||
|
) || 0
|
||||||
|
}</div>
|
||||||
<div class="item-uom">${uom}</div>
|
<div class="item-uom">${uom}</div>
|
||||||
<div class="item-qty-available">${qty_to_display || "Non stock item"}</div>
|
<div class="item-qty-available">${qty_to_display || "Non stock item"}</div>
|
||||||
`
|
`
|
||||||
@@ -189,7 +213,7 @@ erpnext.PointOfSale.ItemSelector = class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handle_broken_image($img) {
|
handle_broken_image($img) {
|
||||||
const item_abbr = $($img).attr("alt");
|
const item_abbr = frappe.utils.escape_html($($img).attr("alt"));
|
||||||
$($img).parent().replaceWith(`<div class="item-display abbr">${item_abbr}</div>`);
|
$($img).parent().replaceWith(`<div class="item-display abbr">${item_abbr}</div>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,7 +268,7 @@ erpnext.PointOfSale.ItemSelector = class {
|
|||||||
set_item_selector_filter_label(value) {
|
set_item_selector_filter_label(value) {
|
||||||
const $filter_label = this.$component.find(".label");
|
const $filter_label = this.$component.find(".label");
|
||||||
|
|
||||||
$filter_label.html(value ? __(value) : __("All Items"));
|
$filter_label.html(value ? frappe.utils.escape_html(__(value)) : __("All Items"));
|
||||||
}
|
}
|
||||||
|
|
||||||
hide_open_link_btn() {
|
hide_open_link_btn() {
|
||||||
@@ -329,12 +353,12 @@ erpnext.PointOfSale.ItemSelector = class {
|
|||||||
|
|
||||||
this.$component.on("click", ".item-wrapper", function () {
|
this.$component.on("click", ".item-wrapper", function () {
|
||||||
const $item = $(this);
|
const $item = $(this);
|
||||||
const item_code = unescape($item.attr("data-item-code"));
|
const item_code = $item.attr("data-item-code");
|
||||||
let batch_no = unescape($item.attr("data-batch-no"));
|
let batch_no = $item.attr("data-batch-no");
|
||||||
let serial_no = unescape($item.attr("data-serial-no"));
|
let serial_no = $item.attr("data-serial-no");
|
||||||
let uom = unescape($item.attr("data-uom"));
|
let uom = $item.attr("data-uom");
|
||||||
let rate = unescape($item.attr("data-rate"));
|
let rate = $item.attr("data-rate");
|
||||||
let stock_uom = unescape($item.attr("data-stock-uom"));
|
let stock_uom = $item.attr("data-stock-uom");
|
||||||
|
|
||||||
// escape(undefined) returns "undefined" then unescape returns "undefined"
|
// escape(undefined) returns "undefined" then unescape returns "undefined"
|
||||||
batch_no = batch_no === "undefined" ? undefined : batch_no;
|
batch_no = batch_no === "undefined" ? undefined : batch_no;
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ erpnext.PointOfSale.PastOrderList = class {
|
|||||||
this.$invoices_container.on("click", ".invoice-wrapper", function () {
|
this.$invoices_container.on("click", ".invoice-wrapper", function () {
|
||||||
const invoice_clicked = $(this);
|
const invoice_clicked = $(this);
|
||||||
const invoice_doctype = invoice_clicked.attr("data-invoice-doctype");
|
const invoice_doctype = invoice_clicked.attr("data-invoice-doctype");
|
||||||
const invoice_name = unescape(invoice_clicked.attr("data-invoice-name"));
|
const invoice_name = invoice_clicked.attr("data-invoice-name");
|
||||||
|
|
||||||
$(".invoice-wrapper").removeClass("invoice-selected");
|
$(".invoice-wrapper").removeClass("invoice-selected");
|
||||||
invoice_clicked.addClass("invoice-selected");
|
invoice_clicked.addClass("invoice-selected");
|
||||||
@@ -108,15 +108,15 @@ erpnext.PointOfSale.PastOrderList = class {
|
|||||||
);
|
);
|
||||||
return `<div class="invoice-wrapper" data-invoice-doctype="${
|
return `<div class="invoice-wrapper" data-invoice-doctype="${
|
||||||
invoice.doctype
|
invoice.doctype
|
||||||
}" data-invoice-name="${escape(invoice.name)}">
|
}" data-invoice-name="${frappe.utils.escape_html(invoice.name)}">
|
||||||
<div class="invoice-name-customer">
|
<div class="invoice-name-customer">
|
||||||
<div class="invoice-customer">
|
<div class="invoice-customer">
|
||||||
<svg class="mr-2" width="12" height="12" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
|
<svg class="mr-2" width="12" height="12" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
|
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
|
||||||
</svg>
|
</svg>
|
||||||
${frappe.ellipsis(invoice.customer_name, 20)}
|
${frappe.utils.escape_html(frappe.ellipsis(invoice.customer_name, 20))}
|
||||||
</div>
|
</div>
|
||||||
<div class="invoice-name">${invoice.name}</div>
|
<div class="invoice-name">${frappe.utils.escape_html(invoice.name)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="invoice-total-date">
|
<div class="invoice-total-date">
|
||||||
<div class="invoice-total">${format_currency(invoice.grand_total, invoice.currency) || 0}</div>
|
<div class="invoice-total">${format_currency(invoice.grand_total, invoice.currency) || 0}</div>
|
||||||
|
|||||||
@@ -82,15 +82,19 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
|||||||
|
|
||||||
return `<div class="left-section">
|
return `<div class="left-section">
|
||||||
<div class="customer-section">
|
<div class="customer-section">
|
||||||
<div class="customer-name">${doc.customer_name}</div>
|
<div class="customer-name">${frappe.utils.escape_html(doc.customer_name)}</div>
|
||||||
${is_customer_naming_by_customer_name ? `<div class="customer-code">${doc.customer}</div>` : ""}
|
${
|
||||||
<div class="customer-email">${this.customer_email}</div>
|
is_customer_naming_by_customer_name
|
||||||
|
? `<div class="customer-code">${frappe.utils.escape_html(doc.customer)}</div>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
<div class="customer-email">${frappe.utils.escape_html(this.customer_email)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="cashier">${__("Sold by")}: ${doc.owner}</div>
|
<div class="cashier">${__("Sold by")}: ${frappe.utils.escape_html(doc.owner)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="right-section">
|
<div class="right-section">
|
||||||
<div class="paid-amount">${format_currency(doc.paid_amount, doc.currency)}</div>
|
<div class="paid-amount">${format_currency(doc.paid_amount, doc.currency)}</div>
|
||||||
<div class="invoice-name">${doc.name}</div>
|
<div class="invoice-name">${frappe.utils.escape_html(doc.name)}</div>
|
||||||
<span class="indicator-pill whitespace-nowrap ${indicator_color}"><span>${__(doc.status)}</span></span>
|
<span class="indicator-pill whitespace-nowrap ${indicator_color}"><span>${__(doc.status)}</span></span>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
@@ -100,8 +104,8 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
|||||||
|
|
||||||
return `<div class="item-row-wrapper">
|
return `<div class="item-row-wrapper">
|
||||||
<div class="item-row-data">
|
<div class="item-row-data">
|
||||||
<div class="item-name">${item_data.item_name}</div>
|
<div class="item-name">${frappe.utils.escape_html(item_data.item_name)}</div>
|
||||||
<div class="item-qty">${item_data.qty || 0} ${item_data.uom}</div>
|
<div class="item-qty">${item_data.qty || 0} ${frappe.utils.escape_html(item_data.uom)}</div>
|
||||||
<div class="item-rate-disc">${get_rate_discount_html()}</div>
|
<div class="item-rate-disc">${get_rate_discount_html()}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -166,7 +170,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
|||||||
.map((t) => {
|
.map((t) => {
|
||||||
return `
|
return `
|
||||||
<div class="tax-row">
|
<div class="tax-row">
|
||||||
<div class="tax-label">${t.description}</div>
|
<div class="tax-label">${frappe.utils.escape_html(t.description)}</div>
|
||||||
<div class="tax-value">${format_currency(t.tax_amount_after_discount_amount, doc.currency)}</div>
|
<div class="tax-value">${format_currency(t.tax_amount_after_discount_amount, doc.currency)}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -185,7 +189,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
|||||||
|
|
||||||
get_payment_html(doc, payment) {
|
get_payment_html(doc, payment) {
|
||||||
return `<div class="summary-row-wrapper payments">
|
return `<div class="summary-row-wrapper payments">
|
||||||
<div>${__(payment.mode_of_payment)}</div>
|
<div>${frappe.utils.escape_html(__(payment.mode_of_payment))}</div>
|
||||||
<div>${format_currency(payment.amount, doc.currency)}</div>
|
<div>${format_currency(payment.amount, doc.currency)}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -519,7 +519,7 @@ erpnext.PointOfSale.Payment = class {
|
|||||||
return `
|
return `
|
||||||
<div class="payment-mode-wrapper">
|
<div class="payment-mode-wrapper">
|
||||||
<div class="mode-of-payment" data-mode="${mode}" data-payment-type="${payment_type}">
|
<div class="mode-of-payment" data-mode="${mode}" data-payment-type="${payment_type}">
|
||||||
${p.mode_of_payment}
|
${frappe.utils.escape_html(p.mode_of_payment)}
|
||||||
<div class="${mode}-amount pay-amount">${amount}</div>
|
<div class="${mode}-amount pay-amount">${amount}</div>
|
||||||
<div class="${mode} mode-of-payment-control"></div>
|
<div class="${mode} mode-of-payment-control"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -603,7 +603,7 @@ erpnext.PointOfSale.Payment = class {
|
|||||||
<div class="mode-of-payment loyalty-card" data-mode="loyalty-amount" data-payment-type="loyalty-amount">
|
<div class="mode-of-payment loyalty-card" data-mode="loyalty-amount" data-payment-type="loyalty-amount">
|
||||||
Redeem Loyalty Points
|
Redeem Loyalty Points
|
||||||
<div class="loyalty-amount-amount pay-amount">${amount}</div>
|
<div class="loyalty-amount-amount pay-amount">${amount}</div>
|
||||||
<div class="loyalty-amount-name">${loyalty_program}</div>
|
<div class="loyalty-amount-name">${frappe.utils.escape_html(loyalty_program)}</div>
|
||||||
<div class="loyalty-amount mode-of-payment-control"></div>
|
<div class="loyalty-amount mode-of-payment-control"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>`
|
</div>`
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ def get_opp_by(by_field, from_date, to_date, company):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
for x in opportunities
|
for x in opportunities
|
||||||
|
if x.get(by_field)
|
||||||
]
|
]
|
||||||
|
|
||||||
summary = {}
|
summary = {}
|
||||||
|
|||||||
@@ -191,12 +191,30 @@ class Analytics:
|
|||||||
self.get_sales_transactions_based_on_project()
|
self.get_sales_transactions_based_on_project()
|
||||||
self.get_rows()
|
self.get_rows()
|
||||||
|
|
||||||
|
def _get_permitted_parent_names(self):
|
||||||
|
return frappe.qb.get_query(
|
||||||
|
table=self.filters.doc_type,
|
||||||
|
fields=["name"],
|
||||||
|
filters={
|
||||||
|
"docstatus": 1,
|
||||||
|
"company": ["in", self.filters.company],
|
||||||
|
self.date_field: ("between", [self.filters.from_date, self.filters.to_date]),
|
||||||
|
},
|
||||||
|
ignore_permissions=False,
|
||||||
|
).run(pluck="name")
|
||||||
|
|
||||||
def get_sales_transactions_based_on_order_type(self):
|
def get_sales_transactions_based_on_order_type(self):
|
||||||
if self.filters["value_quantity"] == "Value":
|
if self.filters["value_quantity"] == "Value":
|
||||||
value_field = "base_net_total"
|
value_field = "base_net_total"
|
||||||
else:
|
else:
|
||||||
value_field = "total_qty"
|
value_field = "total_qty"
|
||||||
|
|
||||||
|
permitted_names = self._get_permitted_parent_names()
|
||||||
|
if not permitted_names:
|
||||||
|
self.entries = []
|
||||||
|
self.get_teams()
|
||||||
|
return
|
||||||
|
|
||||||
doctype = DocType(self.filters.doc_type)
|
doctype = DocType(self.filters.doc_type)
|
||||||
|
|
||||||
self.entries = (
|
self.entries = (
|
||||||
@@ -206,12 +224,7 @@ class Analytics:
|
|||||||
doctype[self.date_field],
|
doctype[self.date_field],
|
||||||
doctype[value_field].as_("value_field"),
|
doctype[value_field].as_("value_field"),
|
||||||
)
|
)
|
||||||
.where(
|
.where((doctype.name.isin(permitted_names)) & (IfNull(doctype.order_type, "") != ""))
|
||||||
(doctype.docstatus == 1)
|
|
||||||
& (doctype.company.isin(self.filters.company))
|
|
||||||
& (doctype[self.date_field].between(self.filters.from_date, self.filters.to_date))
|
|
||||||
& (IfNull(doctype.order_type, "") != "")
|
|
||||||
)
|
|
||||||
.orderby(doctype.order_type)
|
.orderby(doctype.order_type)
|
||||||
).run(as_dict=True)
|
).run(as_dict=True)
|
||||||
|
|
||||||
@@ -250,9 +263,12 @@ class Analytics:
|
|||||||
if self.filters.doc_type in ["Sales Invoice", "Purchase Invoice", "Payment Entry"]:
|
if self.filters.doc_type in ["Sales Invoice", "Purchase Invoice", "Payment Entry"]:
|
||||||
filters.update({"is_opening": "No"})
|
filters.update({"is_opening": "No"})
|
||||||
|
|
||||||
self.entries = frappe.get_all(
|
self.entries = frappe.qb.get_query(
|
||||||
self.filters.doc_type, fields=[entity, entity_name, value_field, self.date_field], filters=filters
|
table=self.filters.doc_type,
|
||||||
)
|
fields=[entity, entity_name, value_field, self.date_field],
|
||||||
|
filters=filters,
|
||||||
|
ignore_permissions=False,
|
||||||
|
).run(as_dict=True)
|
||||||
|
|
||||||
self.entity_names = {}
|
self.entity_names = {}
|
||||||
for d in self.entries:
|
for d in self.entries:
|
||||||
@@ -264,6 +280,12 @@ class Analytics:
|
|||||||
else:
|
else:
|
||||||
value_field = "stock_qty"
|
value_field = "stock_qty"
|
||||||
|
|
||||||
|
permitted_names = self._get_permitted_parent_names()
|
||||||
|
if not permitted_names:
|
||||||
|
self.entries = []
|
||||||
|
self.entity_names = {}
|
||||||
|
return
|
||||||
|
|
||||||
doctype = DocType(self.filters.doc_type)
|
doctype = DocType(self.filters.doc_type)
|
||||||
doctype_item = DocType(f"{self.filters.doc_type} Item")
|
doctype_item = DocType(f"{self.filters.doc_type} Item")
|
||||||
|
|
||||||
@@ -278,11 +300,7 @@ class Analytics:
|
|||||||
doctype_item[value_field].as_("value_field"),
|
doctype_item[value_field].as_("value_field"),
|
||||||
doctype[self.date_field],
|
doctype[self.date_field],
|
||||||
)
|
)
|
||||||
.where(
|
.where((doctype_item.docstatus == 1) & (doctype.name.isin(permitted_names)))
|
||||||
(doctype_item.docstatus == 1)
|
|
||||||
& (doctype.company.isin(self.filters.company))
|
|
||||||
& (doctype[self.date_field].between(self.filters.from_date, self.filters.to_date))
|
|
||||||
)
|
|
||||||
).run(as_dict=True)
|
).run(as_dict=True)
|
||||||
|
|
||||||
self.entity_names = {}
|
self.entity_names = {}
|
||||||
@@ -312,11 +330,12 @@ class Analytics:
|
|||||||
if self.filters.doc_type in ["Sales Invoice", "Purchase Invoice", "Payment Entry"]:
|
if self.filters.doc_type in ["Sales Invoice", "Purchase Invoice", "Payment Entry"]:
|
||||||
filters.update({"is_opening": "No"})
|
filters.update({"is_opening": "No"})
|
||||||
|
|
||||||
self.entries = frappe.get_all(
|
self.entries = frappe.qb.get_query(
|
||||||
self.filters.doc_type,
|
table=self.filters.doc_type,
|
||||||
fields=[entity_field, value_field, self.date_field],
|
fields=[entity_field, value_field, self.date_field],
|
||||||
filters=filters,
|
filters=filters,
|
||||||
)
|
ignore_permissions=False,
|
||||||
|
).run(as_dict=True)
|
||||||
self.get_groups()
|
self.get_groups()
|
||||||
|
|
||||||
def get_sales_transactions_based_on_item_group(self):
|
def get_sales_transactions_based_on_item_group(self):
|
||||||
@@ -325,6 +344,12 @@ class Analytics:
|
|||||||
else:
|
else:
|
||||||
value_field = "qty"
|
value_field = "qty"
|
||||||
|
|
||||||
|
permitted_names = self._get_permitted_parent_names()
|
||||||
|
if not permitted_names:
|
||||||
|
self.entries = []
|
||||||
|
self.get_groups()
|
||||||
|
return
|
||||||
|
|
||||||
doctype = DocType(self.filters.doc_type)
|
doctype = DocType(self.filters.doc_type)
|
||||||
doctype_item = DocType(f"{self.filters.doc_type} Item")
|
doctype_item = DocType(f"{self.filters.doc_type} Item")
|
||||||
|
|
||||||
@@ -337,11 +362,7 @@ class Analytics:
|
|||||||
doctype_item[value_field].as_("value_field"),
|
doctype_item[value_field].as_("value_field"),
|
||||||
doctype[self.date_field],
|
doctype[self.date_field],
|
||||||
)
|
)
|
||||||
.where(
|
.where((doctype_item.docstatus == 1) & (doctype.name.isin(permitted_names)))
|
||||||
(doctype_item.docstatus == 1)
|
|
||||||
& (doctype.company.isin(self.filters.company))
|
|
||||||
& (doctype[self.date_field].between(self.filters.from_date, self.filters.to_date))
|
|
||||||
)
|
|
||||||
).run(as_dict=True)
|
).run(as_dict=True)
|
||||||
|
|
||||||
self.get_groups()
|
self.get_groups()
|
||||||
@@ -367,9 +388,12 @@ class Analytics:
|
|||||||
if self.filters.doc_type in ["Sales Invoice", "Purchase Invoice", "Payment Entry"]:
|
if self.filters.doc_type in ["Sales Invoice", "Purchase Invoice", "Payment Entry"]:
|
||||||
filters.update({"is_opening": "No"})
|
filters.update({"is_opening": "No"})
|
||||||
|
|
||||||
self.entries = frappe.get_all(
|
self.entries = frappe.qb.get_query(
|
||||||
self.filters.doc_type, fields=[entity, value_field, self.date_field], filters=filters
|
table=self.filters.doc_type,
|
||||||
)
|
fields=[entity, value_field, self.date_field],
|
||||||
|
filters=filters,
|
||||||
|
ignore_permissions=False,
|
||||||
|
).run(as_dict=True)
|
||||||
|
|
||||||
def get_rows(self):
|
def get_rows(self):
|
||||||
self.data = []
|
self.data = []
|
||||||
|
|||||||
@@ -4219,9 +4219,14 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"Japan": {
|
"Japan": {
|
||||||
"Japan Tax": {
|
"Japan Tax 10%": {
|
||||||
"account_name": "CT",
|
"account_name": "CT 10%",
|
||||||
"tax_rate": 5.00
|
"tax_rate": 10.00,
|
||||||
|
"default": 1
|
||||||
|
},
|
||||||
|
"Japan Tax 8%": {
|
||||||
|
"account_name": "CT 8%",
|
||||||
|
"tax_rate": 8.00
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -264,8 +264,9 @@ def update_qty(bin_name, args):
|
|||||||
# actual qty is already updated by processing current voucher
|
# actual qty is already updated by processing current voucher
|
||||||
actual_qty = bin_details.actual_qty or 0.0
|
actual_qty = bin_details.actual_qty or 0.0
|
||||||
|
|
||||||
# actual qty is not up to date in case of backdated transaction
|
# actual qty is not up to date in case of backdated transactions
|
||||||
if future_sle_exists(args):
|
# or when cancellations are the most recent SLE
|
||||||
|
if future_sle_exists(args) or args.get("is_cancelled"):
|
||||||
actual_qty = get_actual_qty(args.get("item_code"), args.get("warehouse"))
|
actual_qty = get_actual_qty(args.get("item_code"), args.get("warehouse"))
|
||||||
|
|
||||||
ordered_qty = flt(bin_details.ordered_qty) + flt(args.get("ordered_qty"))
|
ordered_qty = flt(bin_details.ordered_qty) + flt(args.get("ordered_qty"))
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user