Merge branch 'develop' of https://github.com/frappe/erpnext into dev_remove_regional_france
diff --git a/.github/helper/install.sh b/.github/helper/install.sh
index 48337ce..d1a97f8 100644
--- a/.github/helper/install.sh
+++ b/.github/helper/install.sh
@@ -68,6 +68,6 @@
wait $wkpid
-bench start &> bench_run_logs.txt &
+bench start &>> ~/frappe-bench/bench_start.log &
CI=Yes bench build --app frappe &
bench --site test_site reinstall --yes
diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml
index aae2928..07b8de7 100644
--- a/.github/workflows/patch.yml
+++ b/.github/workflows/patch.yml
@@ -23,7 +23,7 @@
services:
mysql:
- image: mariadb:10.3
+ image: mariadb:10.6
env:
MARIADB_ROOT_PASSWORD: 'root'
ports:
@@ -45,9 +45,7 @@
- name: Setup Python
uses: "actions/setup-python@v4"
with:
- python-version: |
- 3.7
- 3.10
+ python-version: '3.10'
- name: Setup Node
uses: actions/setup-node@v2
@@ -102,40 +100,60 @@
- name: Run Patch Tests
run: |
cd ~/frappe-bench/
- wget https://erpnext.com/files/v10-erpnext.sql.gz
- bench --site test_site --force restore ~/frappe-bench/v10-erpnext.sql.gz
+ bench remove-app payments --force
+ jq 'del(.install_apps)' ~/frappe-bench/sites/test_site/site_config.json > tmp.json
+ mv tmp.json ~/frappe-bench/sites/test_site/site_config.json
+
+ wget https://erpnext.com/files/v13-erpnext.sql.gz
+ bench --site test_site --force restore ~/frappe-bench/v13-erpnext.sql.gz
git -C "apps/frappe" remote set-url upstream https://github.com/frappe/frappe.git
git -C "apps/erpnext" remote set-url upstream https://github.com/frappe/erpnext.git
- for version in $(seq 12 13)
- do
- echo "Updating to v$version"
- branch_name="version-$version-hotfix"
- git -C "apps/frappe" fetch --depth 1 upstream $branch_name:$branch_name
- git -C "apps/erpnext" fetch --depth 1 upstream $branch_name:$branch_name
+ function update_to_version() {
+ version=$1
- git -C "apps/frappe" checkout -q -f $branch_name
- git -C "apps/erpnext" checkout -q -f $branch_name
+ branch_name="version-$version-hotfix"
+ echo "Updating to v$version"
- rm -rf ~/frappe-bench/env
- bench setup env --python python3.7
- bench pip install -e ./apps/payments
- bench pip install -e ./apps/erpnext
+ # Fetch and checkout branches
+ git -C "apps/frappe" fetch --depth 1 upstream $branch_name:$branch_name
+ git -C "apps/erpnext" fetch --depth 1 upstream $branch_name:$branch_name
+ git -C "apps/frappe" checkout -q -f $branch_name
+ git -C "apps/erpnext" checkout -q -f $branch_name
- bench --site test_site migrate
- done
+ # Resetup env and install apps
+ pgrep honcho | xargs kill
+ rm -rf ~/frappe-bench/env
+ bench -v setup env
+ bench pip install -e ./apps/erpnext
+ bench start &>> ~/frappe-bench/bench_start.log &
+ bench --site test_site migrate
+ }
+
+ update_to_version 14
echo "Updating to latest version"
git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}"
git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA"
+ pgrep honcho | xargs kill
rm -rf ~/frappe-bench/env
- bench -v setup env --python python3.10
- bench pip install -e ./apps/payments
+ bench -v setup env
bench pip install -e ./apps/erpnext
+ bench start &>> ~/frappe-bench/bench_start.log &
bench --site test_site migrate
- bench --site test_site install-app payments
+
+ - name: Show bench output
+ if: ${{ always() }}
+ run: |
+ cd ~/frappe-bench
+ cat bench_start.log || true
+ cd logs
+ for f in ./*.log*; do
+ echo "Printing log: $f";
+ cat $f
+ done
diff --git a/.github/workflows/server-tests-mariadb.yml b/.github/workflows/server-tests-mariadb.yml
index 2ce1125..559be06 100644
--- a/.github/workflows/server-tests-mariadb.yml
+++ b/.github/workflows/server-tests-mariadb.yml
@@ -123,6 +123,10 @@
CI_BUILD_ID: ${{ github.run_id }}
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
+ - name: Show bench output
+ if: ${{ always() }}
+ run: cat ~/frappe-bench/bench_start.log || true
+
- name: Upload coverage data
uses: actions/upload-artifact@v3
with:
diff --git a/erpnext/accounts/doctype/account/account.js b/erpnext/accounts/doctype/account/account.js
index 3c0eb85..bcf7efc 100644
--- a/erpnext/accounts/doctype/account/account.js
+++ b/erpnext/accounts/doctype/account/account.js
@@ -137,9 +137,6 @@
args: {
old: frm.doc.name,
new: data.name,
- is_group: frm.doc.is_group,
- root_type: frm.doc.root_type,
- company: frm.doc.company,
},
callback: function (r) {
if (!r.exc) {
diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py
index c1eca72..02e6c20 100644
--- a/erpnext/accounts/doctype/account/account.py
+++ b/erpnext/accounts/doctype/account/account.py
@@ -18,6 +18,10 @@
pass
+class InvalidAccountMergeError(frappe.ValidationError):
+ pass
+
+
class Account(NestedSet):
nsm_parent_field = "parent_account"
@@ -460,25 +464,34 @@
@frappe.whitelist()
-def merge_account(old, new, is_group, root_type, company):
+def merge_account(old, new):
# Validate properties before merging
new_account = frappe.get_cached_doc("Account", new)
+ old_account = frappe.get_cached_doc("Account", old)
if not new_account:
throw(_("Account {0} does not exist").format(new))
- if (new_account.is_group, new_account.root_type, new_account.company) != (
- cint(is_group),
- root_type,
- company,
+ if (
+ cint(new_account.is_group),
+ new_account.root_type,
+ new_account.company,
+ cstr(new_account.account_currency),
+ ) != (
+ cint(old_account.is_group),
+ old_account.root_type,
+ old_account.company,
+ cstr(old_account.account_currency),
):
throw(
- _(
- """Merging is only possible if following properties are same in both records. Is Group, Root Type, Company"""
- )
+ msg=_(
+ """Merging is only possible if following properties are same in both records. Is Group, Root Type, Company and Account Currency"""
+ ),
+ title=("Invalid Accounts"),
+ exc=InvalidAccountMergeError,
)
- if is_group and new_account.parent_account == old:
+ if old_account.is_group and new_account.parent_account == old:
new_account.db_set("parent_account", frappe.get_cached_value("Account", old, "parent_account"))
frappe.rename_doc("Account", old, new, merge=1, force=1)
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/import_from_openerp.py b/erpnext/accounts/doctype/account/chart_of_accounts/import_from_openerp.py
deleted file mode 100644
index 3f25ada..0000000
--- a/erpnext/accounts/doctype/account/chart_of_accounts/import_from_openerp.py
+++ /dev/null
@@ -1,289 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
-
-"""
-Import chart of accounts from OpenERP sources
-"""
-
-import ast
-import json
-import os
-from xml.etree import ElementTree as ET
-
-import frappe
-from frappe.utils.csvutils import read_csv_content
-
-path = "/Users/nabinhait/projects/odoo/addons"
-
-accounts = {}
-charts = {}
-all_account_types = []
-all_roots = {}
-
-
-def go():
- global accounts, charts
- default_account_types = get_default_account_types()
-
- country_dirs = []
- for basepath, folders, files in os.walk(path):
- basename = os.path.basename(basepath)
- if basename.startswith("l10n_"):
- country_dirs.append(basename)
-
- for country_dir in country_dirs:
- accounts, charts = {}, {}
- country_path = os.path.join(path, country_dir)
- manifest = ast.literal_eval(open(os.path.join(country_path, "__openerp__.py")).read())
- data_files = (
- manifest.get("data", []) + manifest.get("init_xml", []) + manifest.get("update_xml", [])
- )
- files_path = [os.path.join(country_path, d) for d in data_files]
- xml_roots = get_xml_roots(files_path)
- csv_content = get_csv_contents(files_path)
- prefix = country_dir if csv_content else None
- account_types = get_account_types(
- xml_roots.get("account.account.type", []), csv_content.get("account.account.type", []), prefix
- )
- account_types.update(default_account_types)
-
- if xml_roots:
- make_maps_for_xml(xml_roots, account_types, country_dir)
-
- if csv_content:
- make_maps_for_csv(csv_content, account_types, country_dir)
- make_account_trees()
- make_charts()
-
- create_all_roots_file()
-
-
-def get_default_account_types():
- default_types_root = []
- default_types_root.append(
- ET.parse(os.path.join(path, "account", "data", "data_account_type.xml")).getroot()
- )
- return get_account_types(default_types_root, None, prefix="account")
-
-
-def get_xml_roots(files_path):
- xml_roots = frappe._dict()
- for filepath in files_path:
- fname = os.path.basename(filepath)
- if fname.endswith(".xml"):
- tree = ET.parse(filepath)
- root = tree.getroot()
- for node in root[0].findall("record"):
- if node.get("model") in [
- "account.account.template",
- "account.chart.template",
- "account.account.type",
- ]:
- xml_roots.setdefault(node.get("model"), []).append(root)
- break
- return xml_roots
-
-
-def get_csv_contents(files_path):
- csv_content = {}
- for filepath in files_path:
- fname = os.path.basename(filepath)
- for file_type in ["account.account.template", "account.account.type", "account.chart.template"]:
- if fname.startswith(file_type) and fname.endswith(".csv"):
- with open(filepath, "r") as csvfile:
- try:
- csv_content.setdefault(file_type, []).append(read_csv_content(csvfile.read()))
- except Exception as e:
- continue
- return csv_content
-
-
-def get_account_types(root_list, csv_content, prefix=None):
- types = {}
- account_type_map = {
- "cash": "Cash",
- "bank": "Bank",
- "tr_cash": "Cash",
- "tr_bank": "Bank",
- "receivable": "Receivable",
- "tr_receivable": "Receivable",
- "account rec": "Receivable",
- "payable": "Payable",
- "tr_payable": "Payable",
- "equity": "Equity",
- "stocks": "Stock",
- "stock": "Stock",
- "tax": "Tax",
- "tr_tax": "Tax",
- "tax-out": "Tax",
- "tax-in": "Tax",
- "charges_personnel": "Chargeable",
- "fixed asset": "Fixed Asset",
- "cogs": "Cost of Goods Sold",
- }
- for root in root_list:
- for node in root[0].findall("record"):
- if node.get("model") == "account.account.type":
- data = {}
- for field in node.findall("field"):
- if (
- field.get("name") == "code"
- and field.text.lower() != "none"
- and account_type_map.get(field.text)
- ):
- data["account_type"] = account_type_map[field.text]
-
- node_id = prefix + "." + node.get("id") if prefix else node.get("id")
- types[node_id] = data
-
- if csv_content and csv_content[0][0] == "id":
- for row in csv_content[1:]:
- row_dict = dict(zip(csv_content[0], row))
- data = {}
- if row_dict.get("code") and account_type_map.get(row_dict["code"]):
- data["account_type"] = account_type_map[row_dict["code"]]
- if data and data.get("id"):
- node_id = prefix + "." + data.get("id") if prefix else data.get("id")
- types[node_id] = data
- return types
-
-
-def make_maps_for_xml(xml_roots, account_types, country_dir):
- """make maps for `charts` and `accounts`"""
- for model, root_list in xml_roots.items():
- for root in root_list:
- for node in root[0].findall("record"):
- if node.get("model") == "account.account.template":
- data = {}
- for field in node.findall("field"):
- if field.get("name") == "name":
- data["name"] = field.text
- if field.get("name") == "parent_id":
- parent_id = field.get("ref") or field.get("eval")
- data["parent_id"] = parent_id
-
- if field.get("name") == "user_type":
- value = field.get("ref")
- if account_types.get(value, {}).get("account_type"):
- data["account_type"] = account_types[value]["account_type"]
- if data["account_type"] not in all_account_types:
- all_account_types.append(data["account_type"])
-
- data["children"] = []
- accounts[node.get("id")] = data
-
- if node.get("model") == "account.chart.template":
- data = {}
- for field in node.findall("field"):
- if field.get("name") == "name":
- data["name"] = field.text
- if field.get("name") == "account_root_id":
- data["account_root_id"] = field.get("ref")
- data["id"] = country_dir
- charts.setdefault(node.get("id"), {}).update(data)
-
-
-def make_maps_for_csv(csv_content, account_types, country_dir):
- for content in csv_content.get("account.account.template", []):
- for row in content[1:]:
- data = dict(zip(content[0], row))
- account = {
- "name": data.get("name"),
- "parent_id": data.get("parent_id:id") or data.get("parent_id/id"),
- "children": [],
- }
- user_type = data.get("user_type/id") or data.get("user_type:id")
- if account_types.get(user_type, {}).get("account_type"):
- account["account_type"] = account_types[user_type]["account_type"]
- if account["account_type"] not in all_account_types:
- all_account_types.append(account["account_type"])
-
- accounts[data.get("id")] = account
- if not account.get("parent_id") and data.get("chart_template_id:id"):
- chart_id = data.get("chart_template_id:id")
- charts.setdefault(chart_id, {}).update({"account_root_id": data.get("id")})
-
- for content in csv_content.get("account.chart.template", []):
- for row in content[1:]:
- if row:
- data = dict(zip(content[0], row))
- charts.setdefault(data.get("id"), {}).update(
- {
- "account_root_id": data.get("account_root_id:id") or data.get("account_root_id/id"),
- "name": data.get("name"),
- "id": country_dir,
- }
- )
-
-
-def make_account_trees():
- """build tree hierarchy"""
- for id in accounts.keys():
- account = accounts[id]
-
- if account.get("parent_id"):
- if accounts.get(account["parent_id"]):
- # accounts[account["parent_id"]]["children"].append(account)
- accounts[account["parent_id"]][account["name"]] = account
- del account["parent_id"]
- del account["name"]
-
- # remove empty children
- for id in accounts.keys():
- if "children" in accounts[id] and not accounts[id].get("children"):
- del accounts[id]["children"]
-
-
-def make_charts():
- """write chart files in app/setup/doctype/company/charts"""
- for chart_id in charts:
- src = charts[chart_id]
- if not src.get("name") or not src.get("account_root_id"):
- continue
-
- if not src["account_root_id"] in accounts:
- continue
-
- filename = src["id"][5:] + "_" + chart_id
-
- print("building " + filename)
- chart = {}
- chart["name"] = src["name"]
- chart["country_code"] = src["id"][5:]
- chart["tree"] = accounts[src["account_root_id"]]
-
- for key, val in chart["tree"].items():
- if key in ["name", "parent_id"]:
- chart["tree"].pop(key)
- if type(val) == dict:
- val["root_type"] = ""
- if chart:
- fpath = os.path.join(
- "erpnext", "erpnext", "accounts", "doctype", "account", "chart_of_accounts", filename + ".json"
- )
-
- with open(fpath, "r") as chartfile:
- old_content = chartfile.read()
- if not old_content or (
- json.loads(old_content).get("is_active", "No") == "No"
- and json.loads(old_content).get("disabled", "No") == "No"
- ):
- with open(fpath, "w") as chartfile:
- chartfile.write(json.dumps(chart, indent=4, sort_keys=True))
-
- all_roots.setdefault(filename, chart["tree"].keys())
-
-
-def create_all_roots_file():
- with open("all_roots.txt", "w") as f:
- for filename, roots in sorted(all_roots.items()):
- f.write(filename)
- f.write("\n----------------------\n")
- for r in sorted(roots):
- f.write(r.encode("utf-8"))
- f.write("\n")
- f.write("\n\n\n")
-
-
-if __name__ == "__main__":
- go()
diff --git a/erpnext/accounts/doctype/account/test_account.py b/erpnext/accounts/doctype/account/test_account.py
index 62303bd..30eebef 100644
--- a/erpnext/accounts/doctype/account/test_account.py
+++ b/erpnext/accounts/doctype/account/test_account.py
@@ -7,7 +7,11 @@
import frappe
from frappe.test_runner import make_test_records
-from erpnext.accounts.doctype.account.account import merge_account, update_account_number
+from erpnext.accounts.doctype.account.account import (
+ InvalidAccountMergeError,
+ merge_account,
+ update_account_number,
+)
from erpnext.stock import get_company_default_inventory_account, get_warehouse_account
test_dependencies = ["Company"]
@@ -47,49 +51,53 @@
frappe.delete_doc("Account", "1211-11-4 - 6 - Debtors 1 - Test - - _TC")
def test_merge_account(self):
- if not frappe.db.exists("Account", "Current Assets - _TC"):
- acc = frappe.new_doc("Account")
- acc.account_name = "Current Assets"
- acc.is_group = 1
- acc.parent_account = "Application of Funds (Assets) - _TC"
- acc.company = "_Test Company"
- acc.insert()
- if not frappe.db.exists("Account", "Securities and Deposits - _TC"):
- acc = frappe.new_doc("Account")
- acc.account_name = "Securities and Deposits"
- acc.parent_account = "Current Assets - _TC"
- acc.is_group = 1
- acc.company = "_Test Company"
- acc.insert()
- if not frappe.db.exists("Account", "Earnest Money - _TC"):
- acc = frappe.new_doc("Account")
- acc.account_name = "Earnest Money"
- acc.parent_account = "Securities and Deposits - _TC"
- acc.company = "_Test Company"
- acc.insert()
- if not frappe.db.exists("Account", "Cash In Hand - _TC"):
- acc = frappe.new_doc("Account")
- acc.account_name = "Cash In Hand"
- acc.is_group = 1
- acc.parent_account = "Current Assets - _TC"
- acc.company = "_Test Company"
- acc.insert()
- if not frappe.db.exists("Account", "Accumulated Depreciation - _TC"):
- acc = frappe.new_doc("Account")
- acc.account_name = "Accumulated Depreciation"
- acc.parent_account = "Fixed Assets - _TC"
- acc.company = "_Test Company"
- acc.account_type = "Accumulated Depreciation"
- acc.insert()
+ create_account(
+ account_name="Current Assets",
+ is_group=1,
+ parent_account="Application of Funds (Assets) - _TC",
+ company="_Test Company",
+ )
- doc = frappe.get_doc("Account", "Securities and Deposits - _TC")
+ create_account(
+ account_name="Securities and Deposits",
+ is_group=1,
+ parent_account="Current Assets - _TC",
+ company="_Test Company",
+ )
+
+ create_account(
+ account_name="Earnest Money",
+ parent_account="Securities and Deposits - _TC",
+ company="_Test Company",
+ )
+
+ create_account(
+ account_name="Cash In Hand",
+ is_group=1,
+ parent_account="Current Assets - _TC",
+ company="_Test Company",
+ )
+
+ create_account(
+ account_name="Receivable INR",
+ parent_account="Current Assets - _TC",
+ company="_Test Company",
+ account_currency="INR",
+ )
+
+ create_account(
+ account_name="Receivable USD",
+ parent_account="Current Assets - _TC",
+ company="_Test Company",
+ account_currency="USD",
+ )
+
parent = frappe.db.get_value("Account", "Earnest Money - _TC", "parent_account")
self.assertEqual(parent, "Securities and Deposits - _TC")
- merge_account(
- "Securities and Deposits - _TC", "Cash In Hand - _TC", doc.is_group, doc.root_type, doc.company
- )
+ merge_account("Securities and Deposits - _TC", "Cash In Hand - _TC")
+
parent = frappe.db.get_value("Account", "Earnest Money - _TC", "parent_account")
# Parent account of the child account changes after merging
@@ -98,30 +106,28 @@
# Old account doesn't exist after merging
self.assertFalse(frappe.db.exists("Account", "Securities and Deposits - _TC"))
- doc = frappe.get_doc("Account", "Current Assets - _TC")
-
# Raise error as is_group property doesn't match
self.assertRaises(
- frappe.ValidationError,
+ InvalidAccountMergeError,
merge_account,
"Current Assets - _TC",
"Accumulated Depreciation - _TC",
- doc.is_group,
- doc.root_type,
- doc.company,
)
- doc = frappe.get_doc("Account", "Capital Stock - _TC")
-
# Raise error as root_type property doesn't match
self.assertRaises(
- frappe.ValidationError,
+ InvalidAccountMergeError,
merge_account,
"Capital Stock - _TC",
"Softwares - _TC",
- doc.is_group,
- doc.root_type,
- doc.company,
+ )
+
+ # Raise error as currency doesn't match
+ self.assertRaises(
+ InvalidAccountMergeError,
+ merge_account,
+ "Receivable INR - _TC",
+ "Receivable USD - _TC",
)
def test_account_sync(self):
@@ -400,11 +406,20 @@
"Account", filters={"account_name": kwargs.get("account_name"), "company": kwargs.get("company")}
)
if account:
- return account
+ account = frappe.get_doc("Account", account)
+ account.update(
+ dict(
+ is_group=kwargs.get("is_group", 0),
+ parent_account=kwargs.get("parent_account"),
+ )
+ )
+ account.save()
+ return account.name
else:
account = frappe.get_doc(
dict(
doctype="Account",
+ is_group=kwargs.get("is_group", 0),
account_name=kwargs.get("account_name"),
account_type=kwargs.get("account_type"),
parent_account=kwargs.get("parent_account"),
diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
index cfe5e6e..3a2c3cb 100644
--- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
+++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
@@ -265,20 +265,21 @@
@frappe.whitelist()
def get_dimensions(with_cost_center_and_project=False):
- dimension_filters = frappe.db.sql(
- """
- SELECT label, fieldname, document_type
- FROM `tabAccounting Dimension`
- WHERE disabled = 0
- """,
- as_dict=1,
- )
- default_dimensions = frappe.db.sql(
- """SELECT p.fieldname, c.company, c.default_dimension
- FROM `tabAccounting Dimension Detail` c, `tabAccounting Dimension` p
- WHERE c.parent = p.name""",
- as_dict=1,
+ c = frappe.qb.DocType("Accounting Dimension Detail")
+ p = frappe.qb.DocType("Accounting Dimension")
+ dimension_filters = (
+ frappe.qb.from_(p)
+ .select(p.label, p.fieldname, p.document_type)
+ .where(p.disabled == 0)
+ .run(as_dict=1)
+ )
+ default_dimensions = (
+ frappe.qb.from_(c)
+ .inner_join(p)
+ .on(c.parent == p.name)
+ .select(p.fieldname, c.company, c.default_dimension)
+ .run(as_dict=1)
)
if isinstance(with_cost_center_and_project, str):
diff --git a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py
index 25ef2ea..cb7f5f5 100644
--- a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py
+++ b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py
@@ -84,12 +84,22 @@
frappe.set_user("Administrator")
if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}):
- frappe.get_doc(
+ dimension = frappe.get_doc(
{
"doctype": "Accounting Dimension",
"document_type": "Department",
}
- ).insert()
+ )
+ dimension.append(
+ "dimension_defaults",
+ {
+ "company": "_Test Company",
+ "reference_document": "Department",
+ "default_dimension": "_Test Department - _TC",
+ },
+ )
+ dimension.insert()
+ dimension.save()
else:
dimension = frappe.get_doc("Accounting Dimension", "Department")
dimension.disabled = 0
diff --git a/erpnext/accounts/doctype/bank_account/bank_account.json b/erpnext/accounts/doctype/bank_account/bank_account.json
index 41d7947..32f1c67 100644
--- a/erpnext/accounts/doctype/bank_account/bank_account.json
+++ b/erpnext/accounts/doctype/bank_account/bank_account.json
@@ -13,6 +13,7 @@
"account_type",
"account_subtype",
"column_break_7",
+ "disabled",
"is_default",
"is_company_account",
"company",
@@ -199,10 +200,16 @@
"fieldtype": "Data",
"in_global_search": 1,
"label": "Branch Code"
+ },
+ {
+ "default": "0",
+ "fieldname": "disabled",
+ "fieldtype": "Check",
+ "label": "Disabled"
}
],
"links": [],
- "modified": "2022-05-04 15:49:42.620630",
+ "modified": "2023-09-22 21:31:34.763977",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Account",
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
index 3da5ac3..9a7a9a3 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
@@ -7,7 +7,9 @@
import frappe
from frappe import _
from frappe.model.document import Document
+from frappe.query_builder.custom import ConstantColumn
from frappe.utils import cint, flt
+from pypika.terms import Parameter
from erpnext import get_default_cost_center
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount
@@ -15,7 +17,7 @@
get_amounts_not_reflected_in_system,
get_entries,
)
-from erpnext.accounts.utils import get_balance_on
+from erpnext.accounts.utils import get_account_currency, get_balance_on
class BankReconciliationTool(Document):
@@ -283,68 +285,68 @@
to_reference_date=None,
):
frappe.flags.auto_reconcile_vouchers = True
- document_types = ["payment_entry", "journal_entry"]
+ reconciled, partially_reconciled = set(), set()
+
bank_transactions = get_bank_transactions(bank_account)
- matched_transaction = []
for transaction in bank_transactions:
linked_payments = get_linked_payments(
transaction.name,
- document_types,
+ ["payment_entry", "journal_entry"],
from_date,
to_date,
filter_by_reference_date,
from_reference_date,
to_reference_date,
)
- vouchers = []
- for r in linked_payments:
- vouchers.append(
- {
- "payment_doctype": r[1],
- "payment_name": r[2],
- "amount": r[4],
- }
- )
- transaction = frappe.get_doc("Bank Transaction", transaction.name)
- account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
- matched_trans = 0
- for voucher in vouchers:
- gl_entry = frappe.db.get_value(
- "GL Entry",
- dict(
- account=account, voucher_type=voucher["payment_doctype"], voucher_no=voucher["payment_name"]
- ),
- ["credit", "debit"],
- as_dict=1,
- )
- gl_amount, transaction_amount = (
- (gl_entry.credit, transaction.deposit)
- if gl_entry.credit > 0
- else (gl_entry.debit, transaction.withdrawal)
- )
- allocated_amount = gl_amount if gl_amount >= transaction_amount else transaction_amount
- transaction.append(
- "payment_entries",
- {
- "payment_document": voucher["payment_doctype"],
- "payment_entry": voucher["payment_name"],
- "allocated_amount": allocated_amount,
+
+ if not linked_payments:
+ continue
+
+ vouchers = list(
+ map(
+ lambda entry: {
+ "payment_doctype": entry.get("doctype"),
+ "payment_name": entry.get("name"),
+ "amount": entry.get("paid_amount"),
},
+ linked_payments,
)
- matched_transaction.append(str(transaction.name))
- transaction.save()
- transaction.update_allocations()
- matched_transaction_len = len(set(matched_transaction))
- if matched_transaction_len == 0:
- frappe.msgprint(_("No matching references found for auto reconciliation"))
- elif matched_transaction_len == 1:
- frappe.msgprint(_("{0} transaction is reconcilied").format(matched_transaction_len))
- else:
- frappe.msgprint(_("{0} transactions are reconcilied").format(matched_transaction_len))
+ )
+
+ updated_transaction = reconcile_vouchers(transaction.name, json.dumps(vouchers))
+
+ if updated_transaction.status == "Reconciled":
+ reconciled.add(updated_transaction.name)
+ elif flt(transaction.unallocated_amount) != flt(updated_transaction.unallocated_amount):
+ # Partially reconciled (status = Unreconciled & unallocated amount changed)
+ partially_reconciled.add(updated_transaction.name)
+
+ alert_message, indicator = get_auto_reconcile_message(partially_reconciled, reconciled)
+ frappe.msgprint(title=_("Auto Reconciliation"), msg=alert_message, indicator=indicator)
frappe.flags.auto_reconcile_vouchers = False
+ return reconciled, partially_reconciled
- return frappe.get_doc("Bank Transaction", transaction.name)
+
+def get_auto_reconcile_message(partially_reconciled, reconciled):
+ """Returns alert message and indicator for auto reconciliation depending on result state."""
+ alert_message, indicator = "", "blue"
+ if not partially_reconciled and not reconciled:
+ alert_message = _("No matches occurred via auto reconciliation")
+ return alert_message, indicator
+
+ indicator = "green"
+ if reconciled:
+ alert_message += _("{0} Transaction(s) Reconciled").format(len(reconciled))
+ alert_message += "<br>"
+
+ if partially_reconciled:
+ alert_message += _("{0} {1} Partially Reconciled").format(
+ len(partially_reconciled),
+ _("Transactions") if len(partially_reconciled) > 1 else _("Transaction"),
+ )
+
+ return alert_message, indicator
@frappe.whitelist()
@@ -390,19 +392,13 @@
"Look up & subtract any existing Bank Transaction allocations"
copied = []
for voucher in vouchers:
- rows = get_total_allocated_amount(voucher[1], voucher[2])
- amount = None
- for row in rows:
- if row["gl_account"] == gl_account:
- amount = row["total"]
- break
+ rows = get_total_allocated_amount(voucher.get("doctype"), voucher.get("name"))
+ filtered_row = list(filter(lambda row: row.get("gl_account") == gl_account, rows))
- if amount:
- l = list(voucher)
- l[3] -= amount
- copied.append(tuple(l))
- else:
- copied.append(voucher)
+ if amount := None if not filtered_row else filtered_row[0]["total"]:
+ voucher["paid_amount"] -= amount
+
+ copied.append(voucher)
return copied
@@ -418,6 +414,18 @@
to_reference_date,
):
exact_match = True if "exact_match" in document_types else False
+ queries = get_queries(
+ bank_account,
+ company,
+ transaction,
+ document_types,
+ from_date,
+ to_date,
+ filter_by_reference_date,
+ from_reference_date,
+ to_reference_date,
+ exact_match,
+ )
filters = {
"amount": transaction.unallocated_amount,
@@ -429,30 +437,15 @@
}
matching_vouchers = []
+ for query in queries:
+ matching_vouchers.extend(frappe.db.sql(query, filters, as_dict=True))
- # get matching vouchers from all the apps
- for method_name in frappe.get_hooks("get_matching_vouchers_for_bank_reconciliation"):
- matching_vouchers.extend(
- frappe.get_attr(method_name)(
- bank_account,
- company,
- transaction,
- document_types,
- from_date,
- to_date,
- filter_by_reference_date,
- from_reference_date,
- to_reference_date,
- exact_match,
- filters,
- )
- or []
- )
-
- return sorted(matching_vouchers, key=lambda x: x[0], reverse=True) if matching_vouchers else []
+ return (
+ sorted(matching_vouchers, key=lambda x: x["rank"], reverse=True) if matching_vouchers else []
+ )
-def get_matching_vouchers_for_bank_reconciliation(
+def get_queries(
bank_account,
company,
transaction,
@@ -463,7 +456,6 @@
from_reference_date,
to_reference_date,
exact_match,
- filters,
):
# get queries to get matching vouchers
account_from_to = "paid_to" if transaction.deposit > 0.0 else "paid_from"
@@ -488,17 +480,7 @@
or []
)
- vouchers = []
-
- for query in queries:
- vouchers.extend(
- frappe.db.sql(
- query,
- filters,
- )
- )
-
- return vouchers
+ return queries
def get_matching_queries(
@@ -515,6 +497,8 @@
to_reference_date,
):
queries = []
+ currency = get_account_currency(bank_account)
+
if "payment_entry" in document_types:
query = get_pe_matching_query(
exact_match,
@@ -541,12 +525,12 @@
queries.append(query)
if transaction.deposit > 0.0 and "sales_invoice" in document_types:
- query = get_si_matching_query(exact_match)
+ query = get_si_matching_query(exact_match, currency)
queries.append(query)
if transaction.withdrawal > 0.0:
if "purchase_invoice" in document_types:
- query = get_pi_matching_query(exact_match)
+ query = get_pi_matching_query(exact_match, currency)
queries.append(query)
if "bank_transaction" in document_types:
@@ -560,33 +544,48 @@
# get matching bank transaction query
# find bank transactions in the same bank account with opposite sign
# same bank account must have same company and currency
+ bt = frappe.qb.DocType("Bank Transaction")
+
field = "deposit" if transaction.withdrawal > 0.0 else "withdrawal"
+ amount_equality = getattr(bt, field) == transaction.unallocated_amount
+ amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
+ amount_condition = amount_equality if exact_match else getattr(bt, field) > 0.0
- return f"""
+ ref_rank = (
+ frappe.qb.terms.Case().when(bt.reference_number == transaction.reference_number, 1).else_(0)
+ )
+ unallocated_rank = (
+ frappe.qb.terms.Case().when(bt.unallocated_amount == transaction.unallocated_amount, 1).else_(0)
+ )
- SELECT
- (CASE WHEN reference_number = %(reference_no)s THEN 1 ELSE 0 END
- + CASE WHEN {field} = %(amount)s THEN 1 ELSE 0 END
- + CASE WHEN ( party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
- + CASE WHEN unallocated_amount = %(amount)s THEN 1 ELSE 0 END
- + 1) AS rank,
- 'Bank Transaction' AS doctype,
- name,
- unallocated_amount AS paid_amount,
- reference_number AS reference_no,
- date AS reference_date,
- party,
- party_type,
- date AS posting_date,
- currency
- FROM
- `tabBank Transaction`
- WHERE
- status != 'Reconciled'
- AND name != '{transaction.name}'
- AND bank_account = '{transaction.bank_account}'
- AND {field} {'= %(amount)s' if exact_match else '> 0.0'}
- """
+ party_condition = (
+ (bt.party_type == transaction.party_type)
+ & (bt.party == transaction.party)
+ & bt.party.isnotnull()
+ )
+ party_rank = frappe.qb.terms.Case().when(party_condition, 1).else_(0)
+
+ query = (
+ frappe.qb.from_(bt)
+ .select(
+ (ref_rank + amount_rank + party_rank + unallocated_rank + 1).as_("rank"),
+ ConstantColumn("Bank Transaction").as_("doctype"),
+ bt.name,
+ bt.unallocated_amount.as_("paid_amount"),
+ bt.reference_number.as_("reference_no"),
+ bt.date.as_("reference_date"),
+ bt.party,
+ bt.party_type,
+ bt.date.as_("posting_date"),
+ bt.currency,
+ )
+ .where(bt.status != "Reconciled")
+ .where(bt.name != transaction.name)
+ .where(bt.bank_account == transaction.bank_account)
+ .where(amount_condition)
+ .where(bt.docstatus == 1)
+ )
+ return str(query)
def get_pe_matching_query(
@@ -600,45 +599,56 @@
to_reference_date,
):
# get matching payment entries query
- if transaction.deposit > 0.0:
- currency_field = "paid_to_account_currency as currency"
- else:
- currency_field = "paid_from_account_currency as currency"
- filter_by_date = f"AND posting_date between '{from_date}' and '{to_date}'"
- order_by = " posting_date"
- filter_by_reference_no = ""
+ to_from = "to" if transaction.deposit > 0.0 else "from"
+ currency_field = f"paid_{to_from}_account_currency"
+ payment_type = "Receive" if transaction.deposit > 0.0 else "Pay"
+ pe = frappe.qb.DocType("Payment Entry")
+
+ ref_condition = pe.reference_no == transaction.reference_number
+ ref_rank = frappe.qb.terms.Case().when(ref_condition, 1).else_(0)
+
+ amount_equality = pe.paid_amount == transaction.unallocated_amount
+ amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
+ amount_condition = amount_equality if exact_match else pe.paid_amount > 0.0
+
+ party_condition = (
+ (pe.party_type == transaction.party_type)
+ & (pe.party == transaction.party)
+ & pe.party.isnotnull()
+ )
+ party_rank = frappe.qb.terms.Case().when(party_condition, 1).else_(0)
+
+ filter_by_date = pe.posting_date.between(from_date, to_date)
if cint(filter_by_reference_date):
- filter_by_date = f"AND reference_date between '{from_reference_date}' and '{to_reference_date}'"
- order_by = " reference_date"
+ filter_by_date = pe.reference_date.between(from_reference_date, to_reference_date)
+
+ query = (
+ frappe.qb.from_(pe)
+ .select(
+ (ref_rank + amount_rank + party_rank + 1).as_("rank"),
+ ConstantColumn("Payment Entry").as_("doctype"),
+ pe.name,
+ pe.paid_amount,
+ pe.reference_no,
+ pe.reference_date,
+ pe.party,
+ pe.party_type,
+ pe.posting_date,
+ getattr(pe, currency_field).as_("currency"),
+ )
+ .where(pe.docstatus == 1)
+ .where(pe.payment_type.isin([payment_type, "Internal Transfer"]))
+ .where(pe.clearance_date.isnull())
+ .where(getattr(pe, account_from_to) == Parameter("%(bank_account)s"))
+ .where(amount_condition)
+ .where(filter_by_date)
+ .orderby(pe.reference_date if cint(filter_by_reference_date) else pe.posting_date)
+ )
+
if frappe.flags.auto_reconcile_vouchers == True:
- filter_by_reference_no = f"AND reference_no = '{transaction.reference_number}'"
- return f"""
- SELECT
- (CASE WHEN reference_no=%(reference_no)s THEN 1 ELSE 0 END
- + CASE WHEN (party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
- + CASE WHEN paid_amount = %(amount)s THEN 1 ELSE 0 END
- + 1 ) AS rank,
- 'Payment Entry' as doctype,
- name,
- paid_amount,
- reference_no,
- reference_date,
- party,
- party_type,
- posting_date,
- {currency_field}
- FROM
- `tabPayment Entry`
- WHERE
- docstatus = 1
- AND payment_type IN (%(payment_type)s, 'Internal Transfer')
- AND ifnull(clearance_date, '') = ""
- AND {account_from_to} = %(bank_account)s
- AND paid_amount {'= %(amount)s' if exact_match else '> 0.0'}
- {filter_by_date}
- {filter_by_reference_no}
- order by{order_by}
- """
+ query = query.where(ref_condition)
+
+ return str(query)
def get_je_matching_query(
@@ -655,100 +665,121 @@
# So one bank could have both types of bank accounts like asset and liability
# So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type
cr_or_dr = "credit" if transaction.withdrawal > 0.0 else "debit"
- filter_by_date = f"AND je.posting_date between '{from_date}' and '{to_date}'"
- order_by = " je.posting_date"
- filter_by_reference_no = ""
+ je = frappe.qb.DocType("Journal Entry")
+ jea = frappe.qb.DocType("Journal Entry Account")
+
+ ref_condition = je.cheque_no == transaction.reference_number
+ ref_rank = frappe.qb.terms.Case().when(ref_condition, 1).else_(0)
+
+ amount_field = f"{cr_or_dr}_in_account_currency"
+ amount_equality = getattr(jea, amount_field) == transaction.unallocated_amount
+ amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
+
+ filter_by_date = je.posting_date.between(from_date, to_date)
if cint(filter_by_reference_date):
- filter_by_date = f"AND je.cheque_date between '{from_reference_date}' and '{to_reference_date}'"
- order_by = " je.cheque_date"
- if frappe.flags.auto_reconcile_vouchers == True:
- filter_by_reference_no = f"AND je.cheque_no = '{transaction.reference_number}'"
- return f"""
- SELECT
- (CASE WHEN je.cheque_no=%(reference_no)s THEN 1 ELSE 0 END
- + CASE WHEN jea.{cr_or_dr}_in_account_currency = %(amount)s THEN 1 ELSE 0 END
- + 1) AS rank ,
- 'Journal Entry' AS doctype,
+ filter_by_date = je.cheque_date.between(from_reference_date, to_reference_date)
+
+ query = (
+ frappe.qb.from_(jea)
+ .join(je)
+ .on(jea.parent == je.name)
+ .select(
+ (ref_rank + amount_rank + 1).as_("rank"),
+ ConstantColumn("Journal Entry").as_("doctype"),
je.name,
- jea.{cr_or_dr}_in_account_currency AS paid_amount,
- je.cheque_no AS reference_no,
- je.cheque_date AS reference_date,
- je.pay_to_recd_from AS party,
+ getattr(jea, amount_field).as_("paid_amount"),
+ je.cheque_no.as_("reference_no"),
+ je.cheque_date.as_("reference_date"),
+ je.pay_to_recd_from.as_("party"),
jea.party_type,
je.posting_date,
- jea.account_currency AS currency
- FROM
- `tabJournal Entry Account` AS jea
- JOIN
- `tabJournal Entry` AS je
- ON
- jea.parent = je.name
- WHERE
- je.docstatus = 1
- AND je.voucher_type NOT IN ('Opening Entry')
- AND (je.clearance_date IS NULL OR je.clearance_date='0000-00-00')
- AND jea.account = %(bank_account)s
- AND jea.{cr_or_dr}_in_account_currency {'= %(amount)s' if exact_match else '> 0.0'}
- AND je.docstatus = 1
- {filter_by_date}
- {filter_by_reference_no}
- order by {order_by}
- """
+ jea.account_currency.as_("currency"),
+ )
+ .where(je.docstatus == 1)
+ .where(je.voucher_type != "Opening Entry")
+ .where(je.clearance_date.isnull())
+ .where(jea.account == Parameter("%(bank_account)s"))
+ .where(amount_equality if exact_match else getattr(jea, amount_field) > 0.0)
+ .where(je.docstatus == 1)
+ .where(filter_by_date)
+ .orderby(je.cheque_date if cint(filter_by_reference_date) else je.posting_date)
+ )
+
+ if frappe.flags.auto_reconcile_vouchers == True:
+ query = query.where(ref_condition)
+
+ return str(query)
-def get_si_matching_query(exact_match):
+def get_si_matching_query(exact_match, currency):
# get matching sales invoice query
- return f"""
- SELECT
- ( CASE WHEN si.customer = %(party)s THEN 1 ELSE 0 END
- + CASE WHEN sip.amount = %(amount)s THEN 1 ELSE 0 END
- + 1 ) AS rank,
- 'Sales Invoice' as doctype,
+ si = frappe.qb.DocType("Sales Invoice")
+ sip = frappe.qb.DocType("Sales Invoice Payment")
+
+ amount_equality = sip.amount == Parameter("%(amount)s")
+ amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
+ amount_condition = amount_equality if exact_match else sip.amount > 0.0
+
+ party_condition = si.customer == Parameter("%(party)s")
+ party_rank = frappe.qb.terms.Case().when(party_condition, 1).else_(0)
+
+ query = (
+ frappe.qb.from_(sip)
+ .join(si)
+ .on(sip.parent == si.name)
+ .select(
+ (party_rank + amount_rank + 1).as_("rank"),
+ ConstantColumn("Sales Invoice").as_("doctype"),
si.name,
- sip.amount as paid_amount,
- '' as reference_no,
- '' as reference_date,
- si.customer as party,
- 'Customer' as party_type,
+ sip.amount.as_("paid_amount"),
+ ConstantColumn("").as_("reference_no"),
+ ConstantColumn("").as_("reference_date"),
+ si.customer.as_("party"),
+ ConstantColumn("Customer").as_("party_type"),
si.posting_date,
- si.currency
+ si.currency,
+ )
+ .where(si.docstatus == 1)
+ .where(sip.clearance_date.isnull())
+ .where(sip.account == Parameter("%(bank_account)s"))
+ .where(amount_condition)
+ .where(si.currency == currency)
+ )
- FROM
- `tabSales Invoice Payment` as sip
- JOIN
- `tabSales Invoice` as si
- ON
- sip.parent = si.name
- WHERE
- si.docstatus = 1
- AND (sip.clearance_date is null or sip.clearance_date='0000-00-00')
- AND sip.account = %(bank_account)s
- AND sip.amount {'= %(amount)s' if exact_match else '> 0.0'}
- """
+ return str(query)
-def get_pi_matching_query(exact_match):
+def get_pi_matching_query(exact_match, currency):
# get matching purchase invoice query when they are also used as payment entries (is_paid)
- return f"""
- SELECT
- ( CASE WHEN supplier = %(party)s THEN 1 ELSE 0 END
- + CASE WHEN paid_amount = %(amount)s THEN 1 ELSE 0 END
- + 1 ) AS rank,
- 'Purchase Invoice' as doctype,
- name,
- paid_amount,
- '' as reference_no,
- '' as reference_date,
- supplier as party,
- 'Supplier' as party_type,
- posting_date,
- currency
- FROM
- `tabPurchase Invoice`
- WHERE
- docstatus = 1
- AND is_paid = 1
- AND ifnull(clearance_date, '') = ""
- AND cash_bank_account = %(bank_account)s
- AND paid_amount {'= %(amount)s' if exact_match else '> 0.0'}
- """
+ purchase_invoice = frappe.qb.DocType("Purchase Invoice")
+
+ amount_equality = purchase_invoice.paid_amount == Parameter("%(amount)s")
+ amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
+ amount_condition = amount_equality if exact_match else purchase_invoice.paid_amount > 0.0
+
+ party_condition = purchase_invoice.supplier == Parameter("%(party)s")
+ party_rank = frappe.qb.terms.Case().when(party_condition, 1).else_(0)
+
+ query = (
+ frappe.qb.from_(purchase_invoice)
+ .select(
+ (party_rank + amount_rank + 1).as_("rank"),
+ ConstantColumn("Purchase Invoice").as_("doctype"),
+ purchase_invoice.name,
+ purchase_invoice.paid_amount,
+ ConstantColumn("").as_("reference_no"),
+ ConstantColumn("").as_("reference_date"),
+ purchase_invoice.supplier.as_("party"),
+ ConstantColumn("Supplier").as_("party_type"),
+ purchase_invoice.posting_date,
+ purchase_invoice.currency,
+ )
+ .where(purchase_invoice.docstatus == 1)
+ .where(purchase_invoice.is_paid == 1)
+ .where(purchase_invoice.clearance_date.isnull())
+ .where(purchase_invoice.cash_bank_account == Parameter("%(bank_account)s"))
+ .where(amount_condition)
+ .where(purchase_invoice.currency == currency)
+ )
+
+ return str(query)
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/test_bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/test_bank_reconciliation_tool.py
index 599ced5..5a6bb69 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/test_bank_reconciliation_tool.py
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/test_bank_reconciliation_tool.py
@@ -1,9 +1,100 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-# import frappe
import unittest
+import frappe
+from frappe import qb
+from frappe.tests.utils import FrappeTestCase, change_settings
+from frappe.utils import add_days, flt, getdate, today
-class TestBankReconciliationTool(unittest.TestCase):
- pass
+from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
+ auto_reconcile_vouchers,
+ get_bank_transactions,
+)
+from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
+from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
+
+
+class TestBankReconciliationTool(AccountsTestMixin, FrappeTestCase):
+ def setUp(self):
+ self.create_company()
+ self.create_customer()
+ self.clear_old_entries()
+ bank_dt = qb.DocType("Bank")
+ q = qb.from_(bank_dt).delete().where(bank_dt.name == "HDFC").run()
+ self.create_bank_account()
+
+ def tearDown(self):
+ frappe.db.rollback()
+
+ def create_bank_account(self):
+ bank = frappe.get_doc(
+ {
+ "doctype": "Bank",
+ "bank_name": "HDFC",
+ }
+ ).save()
+
+ self.bank_account = (
+ frappe.get_doc(
+ {
+ "doctype": "Bank Account",
+ "account_name": "HDFC _current_",
+ "bank": bank,
+ "is_company_account": True,
+ "account": self.bank, # account from Chart of Accounts
+ }
+ )
+ .insert()
+ .name
+ )
+
+ def test_auto_reconcile(self):
+ # make payment
+ from_date = add_days(today(), -1)
+ to_date = today()
+ payment = create_payment_entry(
+ company=self.company,
+ posting_date=from_date,
+ payment_type="Receive",
+ party_type="Customer",
+ party=self.customer,
+ paid_from=self.debit_to,
+ paid_to=self.bank,
+ paid_amount=100,
+ ).save()
+ payment.reference_no = "123"
+ payment = payment.save().submit()
+
+ # make bank transaction
+ bank_transaction = (
+ frappe.get_doc(
+ {
+ "doctype": "Bank Transaction",
+ "date": to_date,
+ "deposit": 100,
+ "bank_account": self.bank_account,
+ "reference_number": "123",
+ }
+ )
+ .save()
+ .submit()
+ )
+
+ # assert API output pre reconciliation
+ transactions = get_bank_transactions(self.bank_account, from_date, to_date)
+ self.assertEqual(len(transactions), 1)
+ self.assertEqual(transactions[0].name, bank_transaction.name)
+
+ # auto reconcile
+ auto_reconcile_vouchers(
+ bank_account=self.bank_account,
+ from_date=from_date,
+ to_date=to_date,
+ filter_by_reference_date=False,
+ )
+
+ # assert API output post reconciliation
+ transactions = get_bank_transactions(self.bank_account, from_date, to_date)
+ self.assertEqual(len(transactions), 0)
diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
index 59905da..0c328ff 100644
--- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
@@ -47,7 +47,7 @@
from_date=bank_transaction.date,
to_date=utils.today(),
)
- self.assertTrue(linked_payments[0][6] == "Conrad Electronic")
+ self.assertTrue(linked_payments[0]["party"] == "Conrad Electronic")
# This test validates a simple reconciliation leading to the clearance of the bank transaction and the payment
def test_reconcile(self):
@@ -93,7 +93,7 @@
from_date=bank_transaction.date,
to_date=utils.today(),
)
- self.assertTrue(linked_payments[0][3])
+ self.assertTrue(linked_payments[0]["paid_amount"])
# Check error if already reconciled
def test_already_reconciled(self):
@@ -188,7 +188,7 @@
repayment_entry = create_loan_and_repayment()
linked_payments = get_linked_payments(bank_transaction.name, ["loan_repayment", "exact_match"])
- self.assertEqual(linked_payments[0][2], repayment_entry.name)
+ self.assertEqual(linked_payments[0]["name"], repayment_entry.name)
@if_lending_app_installed
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js
index 35a3788..cdd1203 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.js
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js
@@ -50,6 +50,8 @@
frm.trigger("make_inter_company_journal_entry");
}, __('Make'));
}
+
+ erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm);
},
make_inter_company_journal_entry: function(frm) {
diff --git a/erpnext/accounts/doctype/ledger_merge/ledger_merge.py b/erpnext/accounts/doctype/ledger_merge/ledger_merge.py
index 381083b..362d273 100644
--- a/erpnext/accounts/doctype/ledger_merge/ledger_merge.py
+++ b/erpnext/accounts/doctype/ledger_merge/ledger_merge.py
@@ -48,9 +48,6 @@
merge_account(
row.account,
ledger_merge.account,
- ledger_merge.is_group,
- ledger_merge.root_type,
- ledger_merge.company,
)
row.db_set("merged", 1)
frappe.db.commit()
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 9a0adf5..0203c45 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -9,7 +9,7 @@
frappe.ui.form.on('Payment Entry', {
onload: function(frm) {
- frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger'];
+ frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger', 'Unreconcile Payments', 'Unreconcile Payment Entries'];
if(frm.doc.__islocal) {
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
@@ -154,6 +154,13 @@
frm.events.set_dynamic_labels(frm);
frm.events.show_general_ledger(frm);
erpnext.accounts.ledger_preview.show_accounting_ledger_preview(frm);
+ if(frm.doc.references.find((elem) => {return elem.exchange_gain_loss != 0})) {
+ frm.add_custom_button(__("View Exchange Gain/Loss Journals"), function() {
+ frappe.set_route("List", "Journal Entry", {"voucher_type": "Exchange Gain Or Loss", "reference_name": frm.doc.name});
+ }, __('Actions'));
+
+ }
+ erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm);
},
validate_company: (frm) => {
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 8a894e2..38a5209 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -148,6 +148,8 @@
"Repost Payment Ledger Items",
"Repost Accounting Ledger",
"Repost Accounting Ledger Items",
+ "Unreconcile Payments",
+ "Unreconcile Payment Entries",
)
super(PaymentEntry, self).on_cancel()
self.make_gl_entries(cancel=1)
@@ -1150,8 +1152,25 @@
)
make_reverse_gl_entries(gl_entries=gl_entries, partial_cancel=True)
- else:
- make_gl_entries(gl_entries)
+ return
+
+ # same reference added to payment entry
+ for gl_entry in gl_entries.copy():
+ if frappe.db.exists(
+ "GL Entry",
+ {
+ "account": gl_entry.account,
+ "voucher_type": gl_entry.voucher_type,
+ "voucher_no": gl_entry.voucher_no,
+ "voucher_detail_no": gl_entry.voucher_detail_no,
+ "debit": gl_entry.debit,
+ "credit": gl_entry.credit,
+ "is_cancelled": 0,
+ },
+ ):
+ gl_entries.remove(gl_entry)
+
+ make_gl_entries(gl_entries)
def make_invoice_liability_entry(self, gl_entries, invoice):
args_dict = {
@@ -1586,6 +1605,14 @@
fieldname, args.get(date_fields[0]), args.get(date_fields[1])
)
posting_and_due_date.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])])
+ elif args.get(date_fields[0]):
+ # if only from date is supplied
+ condition += " and {0} >= '{1}'".format(fieldname, args.get(date_fields[0]))
+ posting_and_due_date.append(ple[fieldname].gte(args.get(date_fields[0])))
+ elif args.get(date_fields[1]):
+ # if only to date is supplied
+ condition += " and {0} <= '{1}'".format(fieldname, args.get(date_fields[1]))
+ posting_and_due_date.append(ple[fieldname].lte(args.get(date_fields[1])))
if args.get("company"):
condition += " and company = {0}".format(frappe.db.escape(args.get("company")))
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index 96ae0c3..3285a52 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -19,7 +19,7 @@
get_outstanding_invoices,
reconcile_against_document,
)
-from erpnext.controllers.accounts_controller import get_advance_payment_entries
+from erpnext.controllers.accounts_controller import get_advance_payment_entries_for_regional
class PaymentReconciliation(Document):
@@ -78,7 +78,7 @@
if self.payment_name:
condition.update({"name": self.payment_name})
- payment_entries = get_advance_payment_entries(
+ payment_entries = get_advance_payment_entries_for_regional(
self.party_type,
self.party,
party_account,
@@ -363,6 +363,7 @@
)
def reconcile_allocations(self, skip_ref_details_update_for_pe=False):
+ adjust_allocations_for_taxes(self)
dr_or_cr = (
"credit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable"
@@ -663,3 +664,8 @@
None,
inv.cost_center,
)
+
+
+@erpnext.allow_regional
+def adjust_allocations_for_taxes(doc):
+ pass
diff --git a/erpnext/accounts/doctype/payment_request/payment_request.json b/erpnext/accounts/doctype/payment_request/payment_request.json
index 381f3fb..5ffd718 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.json
+++ b/erpnext/accounts/doctype/payment_request/payment_request.json
@@ -231,6 +231,28 @@
"label": "SWIFT Number"
},
{
+ "collapsible": 1,
+ "fieldname": "accounting_dimensions_section",
+ "fieldtype": "Section Break",
+ "label": "Accounting Dimensions"
+ },
+ {
+ "fieldname": "cost_center",
+ "fieldtype": "Link",
+ "label": "Cost Center",
+ "options": "Cost Center"
+ },
+ {
+ "fieldname": "dimension_col_break",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "project",
+ "fieldtype": "Link",
+ "label": "Project",
+ "options": "Project"
+ },
+ {
"depends_on": "eval: doc.payment_request_type == 'Inward'",
"fieldname": "recipient_and_message",
"fieldtype": "Section Break",
@@ -246,7 +268,8 @@
"fieldname": "email_to",
"fieldtype": "Data",
"in_global_search": 1,
- "label": "To"
+ "label": "To",
+ "options": "Email"
},
{
"depends_on": "eval: doc.payment_channel != \"Phone\"",
@@ -317,9 +340,10 @@
},
{
"fieldname": "payment_url",
- "fieldtype": "Small Text",
"hidden": 1,
- "label": "payment_url",
+ "fieldtype": "Data",
+ "length": 500,
+ "options": "URL",
"read_only": 1
},
{
@@ -344,6 +368,14 @@
"read_only": 1
},
{
+ "fetch_from": "payment_gateway_account.payment_channel",
+ "fieldname": "payment_channel",
+ "fieldtype": "Select",
+ "label": "Payment Channel",
+ "options": "\nEmail\nPhone",
+ "read_only": 1
+ },
+ {
"fieldname": "payment_order",
"fieldtype": "Link",
"label": "Payment Order",
@@ -358,43 +390,13 @@
"options": "Payment Request",
"print_hide": 1,
"read_only": 1
- },
- {
- "fetch_from": "payment_gateway_account.payment_channel",
- "fieldname": "payment_channel",
- "fieldtype": "Select",
- "label": "Payment Channel",
- "options": "\nEmail\nPhone",
- "read_only": 1
- },
- {
- "collapsible": 1,
- "fieldname": "accounting_dimensions_section",
- "fieldtype": "Section Break",
- "label": "Accounting Dimensions"
- },
- {
- "fieldname": "cost_center",
- "fieldtype": "Link",
- "label": "Cost Center",
- "options": "Cost Center"
- },
- {
- "fieldname": "dimension_col_break",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "project",
- "fieldtype": "Link",
- "label": "Project",
- "options": "Project"
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2022-12-21 16:56:40.115737",
+ "modified": "2023-09-16 14:15:02.510890",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Request",
diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
index af1c066..d984d86 100644
--- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
+++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
@@ -33,7 +33,7 @@
def on_cancel(self):
self.validate_future_closing_vouchers()
self.db_set("gle_processing_status", "In Progress")
- self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
+ self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
gle_count = frappe.db.count(
"GL Entry",
{"voucher_type": "Period Closing Voucher", "voucher_no": self.name, "is_cancelled": 0},
diff --git a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py
index 93ba90a..62b342a 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py
+++ b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py
@@ -5,6 +5,10 @@
import frappe
+from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
+ create_dimension,
+ disable_dimension,
+)
from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import (
make_closing_entry_from_opening,
)
@@ -140,6 +144,43 @@
pos_inv1.load_from_db()
self.assertEqual(pos_inv1.status, "Paid")
+ def test_pos_closing_for_required_accounting_dimension_in_pos_profile(self):
+ """
+ test case to check whether we can create POS Closing Entry without mandatory accounting dimension
+ """
+
+ create_dimension()
+ pos_profile = make_pos_profile(do_not_insert=1, do_not_set_accounting_dimension=1)
+
+ self.assertRaises(frappe.ValidationError, pos_profile.insert)
+
+ pos_profile.location = "Block 1"
+ pos_profile.insert()
+ self.assertTrue(frappe.db.exists("POS Profile", pos_profile.name))
+
+ test_user = init_user_and_profile(do_not_create_pos_profile=1)
+
+ opening_entry = create_opening_entry(pos_profile, test_user.name)
+ pos_inv1 = create_pos_invoice(rate=350, do_not_submit=1, pos_profile=pos_profile.name)
+ pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
+ pos_inv1.submit()
+
+ # if in between a mandatory accounting dimension is added to the POS Profile then
+ accounting_dimension_department = frappe.get_doc("Accounting Dimension", {"name": "Department"})
+ accounting_dimension_department.dimension_defaults[0].mandatory_for_bs = 1
+ accounting_dimension_department.save()
+
+ pcv_doc = make_closing_entry_from_opening(opening_entry)
+ # will assert coz the new mandatory accounting dimension bank is not set in POS Profile
+ self.assertRaises(frappe.ValidationError, pcv_doc.submit)
+
+ accounting_dimension_department = frappe.get_doc(
+ "Accounting Dimension Detail", {"parent": "Department"}
+ )
+ accounting_dimension_department.mandatory_for_bs = 0
+ accounting_dimension_department.save()
+ disable_dimension()
+
def init_user_and_profile(**args):
user = "test@example.com"
@@ -149,6 +190,9 @@
test_user.add_roles(*roles)
frappe.set_user(user)
+ if args.get("do_not_create_pos_profile"):
+ return test_user
+
pos_profile = make_pos_profile(**args)
pos_profile.append("applicable_for_users", {"default": 1, "user": user})
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index 842f159..e36e97b 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -414,7 +414,7 @@
selling_price_list = (
customer_price_list or customer_group_price_list or profile.get("selling_price_list")
)
- if customer_currency != profile.get("currency"):
+ if customer_currency and customer_currency != profile.get("currency"):
self.set("currency", customer_currency)
else:
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
index b587ce6..d42b1e4 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
@@ -12,6 +12,8 @@
from frappe.utils.background_jobs import enqueue, is_job_enqueued
from frappe.utils.scheduler import is_scheduler_inactive
+from erpnext.accounts.doctype.pos_profile.pos_profile import required_accounting_dimensions
+
class POSInvoiceMergeLog(Document):
def validate(self):
@@ -163,7 +165,8 @@
for i in items:
if (
i.item_code == item.item_code
- and not i.serial_and_batch_bundle
+ and not i.serial_no
+ and not i.batch_no
and i.uom == item.uom
and i.net_rate == item.net_rate
and i.warehouse == item.warehouse
@@ -238,6 +241,22 @@
invoice.disable_rounded_total = cint(
frappe.db.get_value("POS Profile", invoice.pos_profile, "disable_rounded_total")
)
+ accounting_dimensions = required_accounting_dimensions()
+ dimension_values = frappe.db.get_value(
+ "POS Profile", {"name": invoice.pos_profile}, accounting_dimensions, as_dict=1
+ )
+ for dimension in accounting_dimensions:
+ dimension_value = dimension_values.get(dimension)
+
+ if not dimension_value:
+ frappe.throw(
+ _("Please set Accounting Dimension {} in {}").format(
+ frappe.bold(frappe.unscrub(dimension)),
+ frappe.get_desk_link("POS Profile", invoice.pos_profile),
+ )
+ )
+
+ invoice.set(dimension, dimension_value)
if self.merge_invoices_based_on == "Customer Group":
invoice.flags.ignore_pos_profile = True
@@ -424,11 +443,9 @@
)
merge_log.customer = customer
merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None
-
merge_log.set("pos_invoices", _invoices)
merge_log.save(ignore_permissions=True)
merge_log.submit()
-
if closing_entry:
closing_entry.set_status(update=True, status="Submitted")
closing_entry.db_set("error_message", "")
diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.js b/erpnext/accounts/doctype/pos_profile/pos_profile.js
index 0a89aee..ceaafaa 100755
--- a/erpnext/accounts/doctype/pos_profile/pos_profile.js
+++ b/erpnext/accounts/doctype/pos_profile/pos_profile.js
@@ -1,6 +1,5 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
-
frappe.ui.form.on('POS Profile', {
setup: function(frm) {
frm.set_query("selling_price_list", function() {
@@ -140,6 +139,7 @@
company: function(frm) {
frm.trigger("toggle_display_account_head");
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
+
},
toggle_display_account_head: function(frm) {
diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.py b/erpnext/accounts/doctype/pos_profile/pos_profile.py
index e8aee73..58be2d3 100644
--- a/erpnext/accounts/doctype/pos_profile/pos_profile.py
+++ b/erpnext/accounts/doctype/pos_profile/pos_profile.py
@@ -3,7 +3,7 @@
import frappe
-from frappe import _, msgprint
+from frappe import _, msgprint, scrub, unscrub
from frappe.model.document import Document
from frappe.utils import get_link_to_form, now
@@ -14,6 +14,21 @@
self.validate_all_link_fields()
self.validate_duplicate_groups()
self.validate_payment_methods()
+ self.validate_accounting_dimensions()
+
+ def validate_accounting_dimensions(self):
+ acc_dim_names = required_accounting_dimensions()
+ for acc_dim in acc_dim_names:
+ if not self.get(acc_dim):
+ frappe.throw(
+ _(
+ "{0} is a mandatory Accounting Dimension. <br>"
+ "Please set a value for {0} in Accounting Dimensions section."
+ ).format(
+ unscrub(frappe.bold(acc_dim)),
+ ),
+ title=_("Mandatory Accounting Dimension"),
+ )
def validate_default_profile(self):
for row in self.applicable_for_users:
@@ -152,6 +167,24 @@
)
+def required_accounting_dimensions():
+
+ p = frappe.qb.DocType("Accounting Dimension")
+ c = frappe.qb.DocType("Accounting Dimension Detail")
+
+ acc_dim_doc = (
+ frappe.qb.from_(p)
+ .inner_join(c)
+ .on(p.name == c.parent)
+ .select(c.parent)
+ .where((c.mandatory_for_bs == 1) | (c.mandatory_for_pl == 1))
+ .where(p.disabled == 0)
+ ).run(as_dict=1)
+
+ acc_dim_names = [scrub(d.parent) for d in acc_dim_doc]
+ return acc_dim_names
+
+
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def pos_profile_query(doctype, txt, searchfield, start, page_len, filters):
diff --git a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py
index 788aa62..b468ad3 100644
--- a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py
+++ b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py
@@ -5,7 +5,10 @@
import frappe
-from erpnext.accounts.doctype.pos_profile.pos_profile import get_child_nodes
+from erpnext.accounts.doctype.pos_profile.pos_profile import (
+ get_child_nodes,
+ required_accounting_dimensions,
+)
from erpnext.stock.get_item_details import get_pos_profile
test_dependencies = ["Item"]
@@ -118,6 +121,7 @@
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"write_off_account": args.write_off_account or "_Test Write Off - _TC",
"write_off_cost_center": args.write_off_cost_center or "_Test Write Off Cost Center - _TC",
+ "location": "Block 1" if not args.do_not_set_accounting_dimension else None,
}
)
@@ -132,6 +136,7 @@
pos_profile.append("payments", {"mode_of_payment": "Cash", "default": 1})
if not frappe.db.exists("POS Profile", args.name or "_Test POS Profile"):
- pos_profile.insert()
+ if not args.get("do_not_insert"):
+ pos_profile.insert()
return pos_profile
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
index 7863103..9a5ad35 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
@@ -65,6 +65,7 @@
filters = get_common_filters(doc)
if doc.report == "General Ledger":
+ filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))
col, res = get_soa(filters)
for x in [0, -2, -1]:
res[x]["account"] = res[x]["account"].replace("'", "")
diff --git a/erpnext/crm/doctype/linkedin_settings/__init__.py b/erpnext/accounts/doctype/process_subscription/__init__.py
similarity index 100%
copy from erpnext/crm/doctype/linkedin_settings/__init__.py
copy to erpnext/accounts/doctype/process_subscription/__init__.py
diff --git a/erpnext/accounts/doctype/process_subscription/process_subscription.js b/erpnext/accounts/doctype/process_subscription/process_subscription.js
new file mode 100644
index 0000000..858c913
--- /dev/null
+++ b/erpnext/accounts/doctype/process_subscription/process_subscription.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+// frappe.ui.form.on("Process Subscription", {
+// refresh(frm) {
+
+// },
+// });
diff --git a/erpnext/accounts/doctype/process_subscription/process_subscription.json b/erpnext/accounts/doctype/process_subscription/process_subscription.json
new file mode 100644
index 0000000..502d002
--- /dev/null
+++ b/erpnext/accounts/doctype/process_subscription/process_subscription.json
@@ -0,0 +1,90 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2023-09-17 15:40:59.724177",
+ "default_view": "List",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "posting_date",
+ "subscription",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Process Subscription",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "posting_date",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Posting Date",
+ "reqd": 1
+ },
+ {
+ "fieldname": "subscription",
+ "fieldtype": "Link",
+ "label": "Subscription",
+ "options": "Subscription"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2023-09-17 17:33:37.974166",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Process Subscription",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/process_subscription/process_subscription.py b/erpnext/accounts/doctype/process_subscription/process_subscription.py
new file mode 100644
index 0000000..99269d6
--- /dev/null
+++ b/erpnext/accounts/doctype/process_subscription/process_subscription.py
@@ -0,0 +1,27 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from datetime import datetime
+from typing import Union
+
+import frappe
+from frappe.model.document import Document
+from frappe.utils import getdate
+
+from erpnext.accounts.doctype.subscription.subscription import process_all
+
+
+class ProcessSubscription(Document):
+ def on_submit(self):
+ process_all(subscription=self.subscription, posting_date=self.posting_date)
+
+
+def create_subscription_process(
+ subscription: str | None, posting_date: Union[str, datetime.date] | None
+):
+ """Create a new Process Subscription document"""
+ doc = frappe.new_doc("Process Subscription")
+ doc.subscription = subscription
+ doc.posting_date = getdate(posting_date)
+ doc.insert(ignore_permissions=True)
+ doc.submit()
diff --git a/erpnext/accounts/doctype/process_subscription/test_process_subscription.py b/erpnext/accounts/doctype/process_subscription/test_process_subscription.py
new file mode 100644
index 0000000..723695f
--- /dev/null
+++ b/erpnext/accounts/doctype/process_subscription/test_process_subscription.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestProcessSubscription(FrappeTestCase):
+ pass
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index efe9741..c8c9ad1 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -161,6 +161,7 @@
}
this.frm.set_df_property("tax_withholding_category", "hidden", doc.apply_tds ? 0 : 1);
+ erpnext.accounts.unreconcile_payments.add_unreconcile_btn(me.frm);
}
unblock_invoice() {
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index ce7ada3..b4dd75a 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -1164,7 +1164,7 @@
item = create_item("_Test Item for Deferred Accounting", is_purchase_item=True)
item.enable_deferred_expense = 1
- item.deferred_expense_account = deferred_account
+ item.item_defaults[0].deferred_expense_account = deferred_account
item.save()
pi = make_purchase_invoice(item=item.name, qty=1, rate=100, do_not_save=True)
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 642e99c..d4d9239 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -37,7 +37,7 @@
super.onload();
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log',
- 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger"];
+ 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payments", "Unreconcile Payment Entries"];
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
// show debit_to in print format
@@ -183,8 +183,11 @@
}, __('Create'));
}
}
+
+ erpnext.accounts.unreconcile_payments.add_unreconcile_btn(me.frm);
}
+
make_maintenance_schedule() {
frappe.model.open_mapped_doc({
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_maintenance_schedule",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index fba2fa7..7bdb2b4 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -388,6 +388,8 @@
"Repost Payment Ledger Items",
"Repost Accounting Ledger",
"Repost Accounting Ledger Items",
+ "Unreconcile Payments",
+ "Unreconcile Payment Entries",
"Payment Ledger Entry",
"Serial and Batch Bundle",
)
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 21b39d7..84b0149 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -1801,6 +1801,10 @@
)
def test_outstanding_amount_after_advance_payment_entry_cancellation(self):
+ """Test impact of advance PE submission/cancellation on SI and SO."""
+ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
+
+ sales_order = make_sales_order(item_code="138-CMS Shoe", qty=1, price_list_rate=500)
pe = frappe.get_doc(
{
"doctype": "Payment Entry",
@@ -1820,10 +1824,25 @@
"paid_to": "_Test Cash - _TC",
}
)
+ pe.append(
+ "references",
+ {
+ "reference_doctype": "Sales Order",
+ "reference_name": sales_order.name,
+ "total_amount": sales_order.grand_total,
+ "outstanding_amount": sales_order.grand_total,
+ "allocated_amount": 300,
+ },
+ )
pe.insert()
pe.submit()
+ sales_order.reload()
+ self.assertEqual(sales_order.advance_paid, 300)
+
si = frappe.copy_doc(test_records[0])
+ si.items[0].sales_order = sales_order.name
+ si.items[0].so_detail = sales_order.get("items")[0].name
si.is_pos = 0
si.append(
"advances",
@@ -1831,6 +1850,7 @@
"doctype": "Sales Invoice Advance",
"reference_type": "Payment Entry",
"reference_name": pe.name,
+ "reference_row": pe.references[0].name,
"advance_amount": 300,
"allocated_amount": 300,
"remarks": pe.remarks,
@@ -1839,7 +1859,13 @@
si.insert()
si.submit()
- si.load_from_db()
+ si.reload()
+ pe.reload()
+ sales_order.reload()
+
+ # Check if SO is unlinked/replaced by SI in PE & if SO advance paid is 0
+ self.assertEqual(pe.references[0].reference_name, si.name)
+ self.assertEqual(sales_order.advance_paid, 0.0)
# check outstanding after advance allocation
self.assertEqual(
@@ -1847,11 +1873,9 @@
flt(si.rounded_total - si.total_advance, si.precision("outstanding_amount")),
)
- # added to avoid Document has been modified exception
- pe = frappe.get_doc("Payment Entry", pe.name)
pe.cancel()
+ si.reload()
- si.load_from_db()
# check outstanding after advance cancellation
self.assertEqual(
flt(si.outstanding_amount),
@@ -2322,7 +2346,7 @@
item = create_item("_Test Item for Deferred Accounting")
item.enable_deferred_revenue = 1
- item.deferred_revenue_account = deferred_account
+ item.item_defaults[0].deferred_revenue_account = deferred_account
item.no_of_months = 12
item.save()
@@ -3102,7 +3126,7 @@
item = create_item("_Test Item for Deferred Accounting")
item.enable_deferred_expense = 1
- item.deferred_revenue_account = deferred_account
+ item.item_defaults[0].deferred_revenue_account = deferred_account
item.save()
si = create_sales_invoice(
diff --git a/erpnext/accounts/doctype/subscription/subscription.json b/erpnext/accounts/doctype/subscription/subscription.json
index c15aa1e..187b7ab 100644
--- a/erpnext/accounts/doctype/subscription/subscription.json
+++ b/erpnext/accounts/doctype/subscription/subscription.json
@@ -24,8 +24,9 @@
"current_invoice_start",
"current_invoice_end",
"days_until_due",
+ "generate_invoice_at",
+ "number_of_days",
"cancel_at_period_end",
- "generate_invoice_at_period_start",
"sb_4",
"plans",
"sb_1",
@@ -86,12 +87,14 @@
"fieldname": "current_invoice_start",
"fieldtype": "Date",
"label": "Current Invoice Start Date",
+ "no_copy": 1,
"read_only": 1
},
{
"fieldname": "current_invoice_end",
"fieldtype": "Date",
"label": "Current Invoice End Date",
+ "no_copy": 1,
"read_only": 1
},
{
@@ -108,12 +111,6 @@
"label": "Cancel At End Of Period"
},
{
- "default": "0",
- "fieldname": "generate_invoice_at_period_start",
- "fieldtype": "Check",
- "label": "Generate Invoice At Beginning Of Period"
- },
- {
"allow_on_submit": 1,
"fieldname": "sb_4",
"fieldtype": "Section Break",
@@ -240,6 +237,21 @@
"fieldname": "submit_invoice",
"fieldtype": "Check",
"label": "Submit Generated Invoices"
+ },
+ {
+ "default": "End of the current subscription period",
+ "fieldname": "generate_invoice_at",
+ "fieldtype": "Select",
+ "label": "Generate Invoice At",
+ "options": "End of the current subscription period\nBeginning of the current subscription period\nDays before the current subscription period",
+ "reqd": 1
+ },
+ {
+ "depends_on": "eval:doc.generate_invoice_at === \"Days before the current subscription period\"",
+ "fieldname": "number_of_days",
+ "fieldtype": "Int",
+ "label": "Number of Days",
+ "mandatory_depends_on": "eval:doc.generate_invoice_at === \"Days before the current subscription period\""
}
],
"index_web_pages_for_search": 1,
@@ -255,7 +267,7 @@
"link_fieldname": "subscription"
}
],
- "modified": "2022-02-18 23:24:57.185054",
+ "modified": "2023-09-18 17:48:21.900252",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Subscription",
diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py
index bbcade1..3cf7d28 100644
--- a/erpnext/accounts/doctype/subscription/subscription.py
+++ b/erpnext/accounts/doctype/subscription/subscription.py
@@ -36,12 +36,15 @@
pass
+DateTimeLikeObject = Union[str, datetime.date]
+
+
class Subscription(Document):
def before_insert(self):
# update start just before the subscription doc is created
self.update_subscription_period(self.start_date)
- def update_subscription_period(self, date: Optional[Union[datetime.date, str]] = None):
+ def update_subscription_period(self, date: Optional["DateTimeLikeObject"] = None):
"""
Subscription period is the period to be billed. This method updates the
beginning of the billing period and end of the billing period.
@@ -52,14 +55,14 @@
self.current_invoice_start = self.get_current_invoice_start(date)
self.current_invoice_end = self.get_current_invoice_end(self.current_invoice_start)
- def _get_subscription_period(self, date: Optional[Union[datetime.date, str]] = None):
+ def _get_subscription_period(self, date: Optional["DateTimeLikeObject"] = None):
_current_invoice_start = self.get_current_invoice_start(date)
_current_invoice_end = self.get_current_invoice_end(_current_invoice_start)
return _current_invoice_start, _current_invoice_end
def get_current_invoice_start(
- self, date: Optional[Union[datetime.date, str]] = None
+ self, date: Optional["DateTimeLikeObject"] = None
) -> Union[datetime.date, str]:
"""
This returns the date of the beginning of the current billing period.
@@ -84,7 +87,7 @@
return _current_invoice_start
def get_current_invoice_end(
- self, date: Optional[Union[datetime.date, str]] = None
+ self, date: Optional["DateTimeLikeObject"] = None
) -> Union[datetime.date, str]:
"""
This returns the date of the end of the current billing period.
@@ -179,30 +182,24 @@
return data
- def set_subscription_status(self) -> None:
+ def set_subscription_status(self, posting_date: Optional["DateTimeLikeObject"] = None) -> None:
"""
Sets the status of the `Subscription`
"""
if self.is_trialling():
self.status = "Trialling"
elif (
- self.status == "Active"
- and self.end_date
- and getdate(frappe.flags.current_date) > getdate(self.end_date)
+ self.status == "Active" and self.end_date and getdate(posting_date) > getdate(self.end_date)
):
self.status = "Completed"
elif self.is_past_grace_period():
self.status = self.get_status_for_past_grace_period()
- self.cancelation_date = (
- getdate(frappe.flags.current_date) if self.status == "Cancelled" else None
- )
+ self.cancelation_date = getdate(posting_date) if self.status == "Cancelled" else None
elif self.current_invoice_is_past_due() and not self.is_past_grace_period():
self.status = "Past Due Date"
elif not self.has_outstanding_invoice() or self.is_new_subscription():
self.status = "Active"
- self.save()
-
def is_trialling(self) -> bool:
"""
Returns `True` if the `Subscription` is in trial period.
@@ -210,7 +207,9 @@
return not self.period_has_passed(self.trial_period_end) and self.is_new_subscription()
@staticmethod
- def period_has_passed(end_date: Union[str, datetime.date]) -> bool:
+ def period_has_passed(
+ end_date: Union[str, datetime.date], posting_date: Optional["DateTimeLikeObject"] = None
+ ) -> bool:
"""
Returns true if the given `end_date` has passed
"""
@@ -218,7 +217,7 @@
if not end_date:
return True
- return getdate(frappe.flags.current_date) > getdate(end_date)
+ return getdate(posting_date) > getdate(end_date)
def get_status_for_past_grace_period(self) -> str:
cancel_after_grace = cint(frappe.get_value("Subscription Settings", None, "cancel_after_grace"))
@@ -229,7 +228,7 @@
return status
- def is_past_grace_period(self) -> bool:
+ def is_past_grace_period(self, posting_date: Optional["DateTimeLikeObject"] = None) -> bool:
"""
Returns `True` if the grace period for the `Subscription` has passed
"""
@@ -237,18 +236,18 @@
return
grace_period = cint(frappe.get_value("Subscription Settings", None, "grace_period"))
- return getdate(frappe.flags.current_date) >= getdate(
- add_days(self.current_invoice.due_date, grace_period)
- )
+ return getdate(posting_date) >= getdate(add_days(self.current_invoice.due_date, grace_period))
- def current_invoice_is_past_due(self) -> bool:
+ def current_invoice_is_past_due(
+ self, posting_date: Optional["DateTimeLikeObject"] = None
+ ) -> bool:
"""
Returns `True` if the current generated invoice is overdue
"""
if not self.current_invoice or self.is_paid(self.current_invoice):
return False
- return getdate(frappe.flags.current_date) >= getdate(self.current_invoice.due_date)
+ return getdate(posting_date) >= getdate(self.current_invoice.due_date)
@property
def invoice_document_type(self) -> str:
@@ -270,6 +269,9 @@
if not self.cost_center:
self.cost_center = get_default_cost_center(self.get("company"))
+ if self.is_new():
+ self.set_subscription_status()
+
def validate_trial_period(self) -> None:
"""
Runs sanity checks on trial period dates for the `Subscription`
@@ -305,10 +307,6 @@
if billing_info[0]["billing_interval"] != "Month":
frappe.throw(_("Billing Interval in Subscription Plan must be Month to follow calendar months"))
- def after_insert(self) -> None:
- # todo: deal with users who collect prepayments. Maybe a new Subscription Invoice doctype?
- self.set_subscription_status()
-
def generate_invoice(
self,
from_date: Optional[Union[str, datetime.date]] = None,
@@ -344,7 +342,7 @@
invoice.set_posting_time = 1
invoice.posting_date = (
self.current_invoice_start
- if self.generate_invoice_at_period_start
+ if self.generate_invoice_at == "Beginning of the current subscription period"
else self.current_invoice_end
)
@@ -438,7 +436,7 @@
prorate_factor = get_prorata_factor(
self.current_invoice_end,
self.current_invoice_start,
- cint(self.generate_invoice_at_period_start),
+ cint(self.generate_invoice_at == "Beginning of the current subscription period"),
)
items = []
@@ -503,42 +501,45 @@
return items
@frappe.whitelist()
- def process(self) -> bool:
+ def process(self, posting_date: Optional["DateTimeLikeObject"] = None) -> bool:
"""
To be called by task periodically. It checks the subscription and takes appropriate action
as need be. It calls either of these methods depending the `Subscription` status:
1. `process_for_active`
2. `process_for_past_due`
"""
- if (
- not self.is_current_invoice_generated(self.current_invoice_start, self.current_invoice_end)
- and self.can_generate_new_invoice()
- ):
+ if not self.is_current_invoice_generated(
+ self.current_invoice_start, self.current_invoice_end
+ ) and self.can_generate_new_invoice(posting_date):
self.generate_invoice()
self.update_subscription_period(add_days(self.current_invoice_end, 1))
if self.cancel_at_period_end and (
- getdate(frappe.flags.current_date) >= getdate(self.current_invoice_end)
- or getdate(frappe.flags.current_date) >= getdate(self.end_date)
+ getdate(posting_date) >= getdate(self.current_invoice_end)
+ or getdate(posting_date) >= getdate(self.end_date)
):
self.cancel_subscription()
- self.set_subscription_status()
+ self.set_subscription_status(posting_date=posting_date)
self.save()
- def can_generate_new_invoice(self) -> bool:
+ def can_generate_new_invoice(self, posting_date: Optional["DateTimeLikeObject"] = None) -> bool:
if self.cancelation_date:
return False
- elif self.generate_invoice_at_period_start and (
- getdate(frappe.flags.current_date) == getdate(self.current_invoice_start)
- or self.is_new_subscription()
+
+ if self.has_outstanding_invoice() and not self.generate_new_invoices_past_due_date:
+ return False
+
+ if self.generate_invoice_at == "Beginning of the current subscription period" and (
+ getdate(posting_date) == getdate(self.current_invoice_start) or self.is_new_subscription()
):
return True
- elif getdate(frappe.flags.current_date) == getdate(self.current_invoice_end):
- if self.has_outstanding_invoice() and not self.generate_new_invoices_past_due_date:
- return False
-
+ elif self.generate_invoice_at == "Days before the current subscription period" and (
+ getdate(posting_date) == getdate(add_days(self.current_invoice_start, -1 * self.number_of_days))
+ ):
+ return True
+ elif getdate(posting_date) == getdate(self.current_invoice_end):
return True
else:
return False
@@ -628,7 +629,10 @@
frappe.throw(_("subscription is already cancelled."), InvoiceCancelled)
to_generate_invoice = (
- True if self.status == "Active" and not self.generate_invoice_at_period_start else False
+ True
+ if self.status == "Active"
+ and not self.generate_invoice_at == "Beginning of the current subscription period"
+ else False
)
self.status = "Cancelled"
self.cancelation_date = nowdate()
@@ -639,7 +643,7 @@
self.save()
@frappe.whitelist()
- def restart_subscription(self) -> None:
+ def restart_subscription(self, posting_date: Optional["DateTimeLikeObject"] = None) -> None:
"""
This sets the subscription as active. The subscription will be made to be like a new
subscription and the `Subscription` will lose all the history of generated invoices
@@ -650,7 +654,7 @@
self.status = "Active"
self.cancelation_date = None
- self.update_subscription_period(frappe.flags.current_date or nowdate())
+ self.update_subscription_period(posting_date or nowdate())
self.save()
@@ -671,14 +675,21 @@
return diff / plan_days
-def process_all() -> None:
+def process_all(
+ subscription: str | None, posting_date: Optional["DateTimeLikeObject"] = None
+) -> None:
"""
Task to updates the status of all `Subscription` apart from those that are cancelled
"""
- for subscription in frappe.get_all("Subscription", {"status": ("!=", "Cancelled")}, pluck="name"):
+ filters = {"status": ("!=", "Cancelled")}
+
+ if subscription:
+ filters["name"] = subscription
+
+ for subscription in frappe.get_all("Subscription", filters, pluck="name"):
try:
subscription = frappe.get_doc("Subscription", subscription)
- subscription.process()
+ subscription.process(posting_date)
frappe.db.commit()
except frappe.ValidationError:
frappe.db.rollback()
diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py
index 0bb171f..803e879 100644
--- a/erpnext/accounts/doctype/subscription/test_subscription.py
+++ b/erpnext/accounts/doctype/subscription/test_subscription.py
@@ -8,6 +8,7 @@
add_days,
add_months,
add_to_date,
+ cint,
date_diff,
flt,
get_date_str,
@@ -20,99 +21,16 @@
test_dependencies = ("UOM", "Item Group", "Item")
-def create_plan():
- if not frappe.db.exists("Subscription Plan", "_Test Plan Name"):
- plan = frappe.new_doc("Subscription Plan")
- plan.plan_name = "_Test Plan Name"
- plan.item = "_Test Non Stock Item"
- plan.price_determination = "Fixed Rate"
- plan.cost = 900
- plan.billing_interval = "Month"
- plan.billing_interval_count = 1
- plan.insert()
-
- if not frappe.db.exists("Subscription Plan", "_Test Plan Name 2"):
- plan = frappe.new_doc("Subscription Plan")
- plan.plan_name = "_Test Plan Name 2"
- plan.item = "_Test Non Stock Item"
- plan.price_determination = "Fixed Rate"
- plan.cost = 1999
- plan.billing_interval = "Month"
- plan.billing_interval_count = 1
- plan.insert()
-
- if not frappe.db.exists("Subscription Plan", "_Test Plan Name 3"):
- plan = frappe.new_doc("Subscription Plan")
- plan.plan_name = "_Test Plan Name 3"
- plan.item = "_Test Non Stock Item"
- plan.price_determination = "Fixed Rate"
- plan.cost = 1999
- plan.billing_interval = "Day"
- plan.billing_interval_count = 14
- plan.insert()
-
- # Defined a quarterly Subscription Plan
- if not frappe.db.exists("Subscription Plan", "_Test Plan Name 4"):
- plan = frappe.new_doc("Subscription Plan")
- plan.plan_name = "_Test Plan Name 4"
- plan.item = "_Test Non Stock Item"
- plan.price_determination = "Monthly Rate"
- plan.cost = 20000
- plan.billing_interval = "Month"
- plan.billing_interval_count = 3
- plan.insert()
-
- if not frappe.db.exists("Subscription Plan", "_Test Plan Multicurrency"):
- plan = frappe.new_doc("Subscription Plan")
- plan.plan_name = "_Test Plan Multicurrency"
- plan.item = "_Test Non Stock Item"
- plan.price_determination = "Fixed Rate"
- plan.cost = 50
- plan.currency = "USD"
- plan.billing_interval = "Month"
- plan.billing_interval_count = 1
- plan.insert()
-
-
-def create_parties():
- if not frappe.db.exists("Supplier", "_Test Supplier"):
- supplier = frappe.new_doc("Supplier")
- supplier.supplier_name = "_Test Supplier"
- supplier.supplier_group = "All Supplier Groups"
- supplier.insert()
-
- if not frappe.db.exists("Customer", "_Test Subscription Customer"):
- customer = frappe.new_doc("Customer")
- customer.customer_name = "_Test Subscription Customer"
- customer.billing_currency = "USD"
- customer.append(
- "accounts", {"company": "_Test Company", "account": "_Test Receivable USD - _TC"}
- )
- customer.insert()
-
-
-def reset_settings():
- settings = frappe.get_single("Subscription Settings")
- settings.grace_period = 0
- settings.cancel_after_grace = 0
- settings.save()
-
-
class TestSubscription(unittest.TestCase):
def setUp(self):
- create_plan()
+ make_plans()
create_parties()
reset_settings()
def test_create_subscription_with_trial_with_correct_period(self):
- subscription = frappe.new_doc("Subscription")
- subscription.party_type = "Customer"
- subscription.party = "_Test Customer"
- subscription.trial_period_start = nowdate()
- subscription.trial_period_end = add_months(nowdate(), 1)
- subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
- subscription.save()
-
+ subscription = create_subscription(
+ trial_period_start=nowdate(), trial_period_end=add_months(nowdate(), 1)
+ )
self.assertEqual(subscription.trial_period_start, nowdate())
self.assertEqual(subscription.trial_period_end, add_months(nowdate(), 1))
self.assertEqual(
@@ -126,12 +44,7 @@
self.assertEqual(subscription.status, "Trialling")
def test_create_subscription_without_trial_with_correct_period(self):
- subscription = frappe.new_doc("Subscription")
- subscription.party_type = "Customer"
- subscription.party = "_Test Customer"
- subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
- subscription.save()
-
+ subscription = create_subscription()
self.assertEqual(subscription.trial_period_start, None)
self.assertEqual(subscription.trial_period_end, None)
self.assertEqual(subscription.current_invoice_start, nowdate())
@@ -141,55 +54,28 @@
self.assertEqual(subscription.status, "Active")
def test_create_subscription_trial_with_wrong_dates(self):
- subscription = frappe.new_doc("Subscription")
- subscription.party_type = "Customer"
- subscription.party = "_Test Customer"
- subscription.trial_period_end = nowdate()
- subscription.trial_period_start = add_days(nowdate(), 30)
- subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
-
- self.assertRaises(frappe.ValidationError, subscription.save)
-
- def test_create_subscription_multi_with_different_billing_fails(self):
- subscription = frappe.new_doc("Subscription")
- subscription.party_type = "Customer"
- subscription.party = "_Test Customer"
- subscription.trial_period_end = nowdate()
- subscription.trial_period_start = add_days(nowdate(), 30)
- subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
- subscription.append("plans", {"plan": "_Test Plan Name 3", "qty": 1})
-
+ subscription = create_subscription(
+ trial_period_start=add_days(nowdate(), 30), trial_period_end=nowdate(), do_not_save=True
+ )
self.assertRaises(frappe.ValidationError, subscription.save)
def test_invoice_is_generated_at_end_of_billing_period(self):
- subscription = frappe.new_doc("Subscription")
- subscription.party_type = "Customer"
- subscription.party = "_Test Customer"
- subscription.start_date = "2018-01-01"
- subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
- subscription.insert()
-
+ subscription = create_subscription(start_date="2018-01-01")
self.assertEqual(subscription.status, "Active")
self.assertEqual(subscription.current_invoice_start, "2018-01-01")
self.assertEqual(subscription.current_invoice_end, "2018-01-31")
- frappe.flags.current_date = "2018-01-31"
- subscription.process()
+ subscription.process(posting_date="2018-01-31")
self.assertEqual(len(subscription.invoices), 1)
self.assertEqual(subscription.current_invoice_start, "2018-02-01")
self.assertEqual(subscription.current_invoice_end, "2018-02-28")
self.assertEqual(subscription.status, "Unpaid")
def test_status_goes_back_to_active_after_invoice_is_paid(self):
- subscription = frappe.new_doc("Subscription")
- subscription.party_type = "Customer"
- subscription.party = "_Test Customer"
- subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
- subscription.start_date = "2018-01-01"
- subscription.generate_invoice_at_period_start = True
- subscription.insert()
- frappe.flags.current_date = "2018-01-01"
- subscription.process() # generate first invoice
+ subscription = create_subscription(
+ start_date="2018-01-01", generate_invoice_at="Beginning of the current subscription period"
+ )
+ subscription.process(posting_date="2018-01-01") # generate first invoice
self.assertEqual(len(subscription.invoices), 1)
# Status is unpaid as Days until Due is zero and grace period is Zero
@@ -213,18 +99,10 @@
settings.cancel_after_grace = 1
settings.save()
- subscription = frappe.new_doc("Subscription")
- subscription.party_type = "Customer"
- subscription.party = "_Test Customer"
- subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
- # subscription.generate_invoice_at_period_start = True
- subscription.start_date = "2018-01-01"
- subscription.insert()
-
+ subscription = create_subscription(start_date="2018-01-01")
self.assertEqual(subscription.status, "Active")
- frappe.flags.current_date = "2018-01-31"
- subscription.process() # generate first invoice
+ subscription.process(posting_date="2018-01-31") # generate first invoice
# This should change status to Cancelled since grace period is 0
# And is backdated subscription so subscription will be cancelled after processing
self.assertEqual(subscription.status, "Cancelled")
@@ -235,13 +113,8 @@
settings.cancel_after_grace = 0
settings.save()
- subscription = frappe.new_doc("Subscription")
- subscription.party_type = "Customer"
- subscription.party = "_Test Customer"
- subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
- subscription.start_date = "2018-01-01"
- subscription.insert()
- subscription.process() # generate first invoice
+ subscription = create_subscription(start_date="2018-01-01")
+ subscription.process(posting_date="2018-01-31") # generate first invoice
# Status is unpaid as Days until Due is zero and grace period is Zero
self.assertEqual(subscription.status, "Unpaid")
@@ -251,21 +124,9 @@
def test_subscription_invoice_days_until_due(self):
_date = add_months(nowdate(), -1)
- subscription = frappe.new_doc("Subscription")
- subscription.party_type = "Customer"
- subscription.party = "_Test Customer"
- subscription.days_until_due = 10
- subscription.start_date = _date
- subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
- subscription.insert()
+ subscription = create_subscription(start_date=_date, days_until_due=10)
- frappe.flags.current_date = subscription.current_invoice_end
-
- subscription.process() # generate first invoice
- self.assertEqual(len(subscription.invoices), 1)
- self.assertEqual(subscription.status, "Active")
-
- frappe.flags.current_date = add_days(subscription.current_invoice_end, 3)
+ subscription.process(posting_date=subscription.current_invoice_end) # generate first invoice
self.assertEqual(len(subscription.invoices), 1)
self.assertEqual(subscription.status, "Active")
@@ -275,16 +136,9 @@
settings.grace_period = 1000
settings.save()
- subscription = frappe.new_doc("Subscription")
- subscription.party_type = "Customer"
- subscription.party = "_Test Customer"
- subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
- subscription.start_date = add_days(nowdate(), -1000)
- subscription.insert()
+ subscription = create_subscription(start_date=add_days(nowdate(), -1000))
- frappe.flags.current_date = subscription.current_invoice_end
- subscription.process() # generate first invoice
-
+ subscription.process(posting_date=subscription.current_invoice_end) # generate first invoice
self.assertEqual(subscription.status, "Past Due Date")
subscription.process()
@@ -301,12 +155,7 @@
settings.save()
def test_subscription_remains_active_during_invoice_period(self):
- subscription = frappe.new_doc("Subscription")
- subscription.party_type = "Customer"
- subscription.party = "_Test Customer"
- subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
- subscription.save()
- subscription.process() # no changes expected
+ subscription = create_subscription() # no changes expected
self.assertEqual(subscription.status, "Active")
self.assertEqual(subscription.current_invoice_start, nowdate())
@@ -325,12 +174,8 @@
self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1))
self.assertEqual(len(subscription.invoices), 0)
- def test_subscription_cancelation(self):
- subscription = frappe.new_doc("Subscription")
- subscription.party_type = "Customer"
- subscription.party = "_Test Customer"
- subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
- subscription.save()
+ def test_subscription_cancellation(self):
+ subscription = create_subscription()
subscription.cancel_subscription()
self.assertEqual(subscription.status, "Cancelled")
@@ -341,11 +186,7 @@
settings.prorate = 1
settings.save()
- subscription = frappe.new_doc("Subscription")
- subscription.party_type = "Customer"
- subscription.party = "_Test Customer"
- subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
- subscription.save()
+ subscription = create_subscription()
self.assertEqual(subscription.status, "Active")
@@ -365,7 +206,7 @@
get_prorata_factor(
subscription.current_invoice_end,
subscription.current_invoice_start,
- subscription.generate_invoice_at_period_start,
+ cint(subscription.generate_invoice_at == "Beginning of the current subscription period"),
),
2,
),
@@ -383,11 +224,7 @@
settings.prorate = 0
settings.save()
- subscription = frappe.new_doc("Subscription")
- subscription.party_type = "Customer"
- subscription.party = "_Test Customer"
- subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
- subscription.save()
+ subscription = create_subscription()
subscription.cancel_subscription()
invoice = subscription.get_current_invoice()
@@ -402,11 +239,7 @@
settings.prorate = 1
settings.save()
- subscription = frappe.new_doc("Subscription")
- subscription.party_type = "Customer"
- subscription.party = "_Test Customer"
- subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
- subscription.save()
+ subscription = create_subscription()
subscription.cancel_subscription()
invoice = subscription.get_current_invoice()
@@ -421,18 +254,13 @@
settings.prorate = to_prorate
settings.save()
- def test_subcription_cancellation_and_process(self):
+ def test_subscription_cancellation_and_process(self):
settings = frappe.get_single("Subscription Settings")
default_grace_period_action = settings.cancel_after_grace
settings.cancel_after_grace = 1
settings.save()
- subscription = frappe.new_doc("Subscription")
- subscription.party_type = "Customer"
- subscription.party = "_Test Customer"
- subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
- subscription.start_date = "2018-01-01"
- subscription.insert()
+ subscription = create_subscription(start_date="2018-01-01")
subscription.process() # generate first invoice
# Generate an invoice for the cancelled period
@@ -458,14 +286,8 @@
settings.cancel_after_grace = 0
settings.save()
- subscription = frappe.new_doc("Subscription")
- subscription.party_type = "Customer"
- subscription.party = "_Test Customer"
- subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
- subscription.start_date = "2018-01-01"
- subscription.insert()
- frappe.flags.current_date = "2018-01-31"
- subscription.process() # generate first invoice
+ subscription = create_subscription(start_date="2018-01-01")
+ subscription.process(posting_date="2018-01-31") # generate first invoice
# Status is unpaid as Days until Due is zero and grace period is Zero
self.assertEqual(subscription.status, "Unpaid")
@@ -494,17 +316,10 @@
settings.cancel_after_grace = 0
settings.save()
- subscription = frappe.new_doc("Subscription")
- subscription.party_type = "Customer"
- subscription.party = "_Test Customer"
- subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
- subscription.start_date = "2018-01-01"
- subscription.generate_invoice_at_period_start = True
- subscription.insert()
-
- frappe.flags.current_date = subscription.current_invoice_start
-
- subscription.process() # generate first invoice
+ subscription = create_subscription(
+ start_date="2018-01-01", generate_invoice_at="Beginning of the current subscription period"
+ )
+ subscription.process(subscription.current_invoice_start) # generate first invoice
# This should change status to Unpaid since grace period is 0
self.assertEqual(subscription.status, "Unpaid")
@@ -516,29 +331,18 @@
self.assertEqual(subscription.status, "Active")
# A new invoice is generated
- frappe.flags.current_date = subscription.current_invoice_start
- subscription.process()
+ subscription.process(posting_date=subscription.current_invoice_start)
self.assertEqual(subscription.status, "Unpaid")
settings.cancel_after_grace = default_grace_period_action
settings.save()
def test_restart_active_subscription(self):
- subscription = frappe.new_doc("Subscription")
- subscription.party_type = "Customer"
- subscription.party = "_Test Customer"
- subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
- subscription.save()
-
+ subscription = create_subscription()
self.assertRaises(frappe.ValidationError, subscription.restart_subscription)
def test_subscription_invoice_discount_percentage(self):
- subscription = frappe.new_doc("Subscription")
- subscription.party_type = "Customer"
- subscription.party = "_Test Customer"
- subscription.additional_discount_percentage = 10
- subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
- subscription.save()
+ subscription = create_subscription(additional_discount_percentage=10)
subscription.cancel_subscription()
invoice = subscription.get_current_invoice()
@@ -547,12 +351,7 @@
self.assertEqual(invoice.apply_discount_on, "Grand Total")
def test_subscription_invoice_discount_amount(self):
- subscription = frappe.new_doc("Subscription")
- subscription.party_type = "Customer"
- subscription.party = "_Test Customer"
- subscription.additional_discount_amount = 11
- subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
- subscription.save()
+ subscription = create_subscription(additional_discount_amount=11)
subscription.cancel_subscription()
invoice = subscription.get_current_invoice()
@@ -563,18 +362,13 @@
def test_prepaid_subscriptions(self):
# Create a non pre-billed subscription, processing should not create
# invoices.
- subscription = frappe.new_doc("Subscription")
- subscription.party_type = "Customer"
- subscription.party = "_Test Customer"
- subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
- subscription.save()
+ subscription = create_subscription()
subscription.process()
-
self.assertEqual(len(subscription.invoices), 0)
# Change the subscription type to prebilled and process it.
# Prepaid invoice should be generated
- subscription.generate_invoice_at_period_start = True
+ subscription.generate_invoice_at = "Beginning of the current subscription period"
subscription.save()
subscription.process()
@@ -586,12 +380,9 @@
settings.prorate = 1
settings.save()
- subscription = frappe.new_doc("Subscription")
- subscription.party_type = "Customer"
- subscription.party = "_Test Customer"
- subscription.generate_invoice_at_period_start = True
- subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
- subscription.save()
+ subscription = create_subscription(
+ generate_invoice_at="Beginning of the current subscription period"
+ )
subscription.process()
subscription.cancel_subscription()
@@ -609,9 +400,10 @@
def test_subscription_with_follow_calendar_months(self):
subscription = frappe.new_doc("Subscription")
+ subscription.company = "_Test Company"
subscription.party_type = "Supplier"
subscription.party = "_Test Supplier"
- subscription.generate_invoice_at_period_start = 1
+ subscription.generate_invoice_at = "Beginning of the current subscription period"
subscription.follow_calendar_months = 1
# select subscription start date as "2018-01-15"
@@ -625,39 +417,33 @@
self.assertEqual(get_date_str(subscription.current_invoice_end), "2018-03-31")
def test_subscription_generate_invoice_past_due(self):
- subscription = frappe.new_doc("Subscription")
- subscription.party_type = "Supplier"
- subscription.party = "_Test Supplier"
- subscription.generate_invoice_at_period_start = 1
- subscription.generate_new_invoices_past_due_date = 1
- # select subscription start date as "2018-01-15"
- subscription.start_date = "2018-01-01"
- subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1})
- subscription.save()
+ subscription = create_subscription(
+ start_date="2018-01-01",
+ party_type="Supplier",
+ party="_Test Supplier",
+ generate_invoice_at="Beginning of the current subscription period",
+ generate_new_invoices_past_due_date=1,
+ plans=[{"plan": "_Test Plan Name 4", "qty": 1}],
+ )
- frappe.flags.current_date = "2018-01-01"
# Process subscription and create first invoice
# Subscription status will be unpaid since due date has already passed
- subscription.process()
+ subscription.process(posting_date="2018-01-01")
self.assertEqual(len(subscription.invoices), 1)
self.assertEqual(subscription.status, "Unpaid")
# Now the Subscription is unpaid
# Even then new invoice should be created as we have enabled `generate_new_invoices_past_due_date` in
# subscription and the interval between the subscriptions is 3 months
- frappe.flags.current_date = "2018-04-01"
- subscription.process()
+ subscription.process(posting_date="2018-04-01")
self.assertEqual(len(subscription.invoices), 2)
def test_subscription_without_generate_invoice_past_due(self):
- subscription = frappe.new_doc("Subscription")
- subscription.party_type = "Supplier"
- subscription.party = "_Test Supplier"
- subscription.generate_invoice_at_period_start = 1
- # select subscription start date as "2018-01-15"
- subscription.start_date = "2018-01-01"
- subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1})
- subscription.save()
+ subscription = create_subscription(
+ start_date="2018-01-01",
+ generate_invoice_at="Beginning of the current subscription period",
+ plans=[{"plan": "_Test Plan Name 4", "qty": 1}],
+ )
# Process subscription and create first invoice
# Subscription status will be unpaid since due date has already passed
@@ -668,16 +454,13 @@
subscription.process()
self.assertEqual(len(subscription.invoices), 1)
- def test_multicurrency_subscription(self):
- subscription = frappe.new_doc("Subscription")
- subscription.party_type = "Customer"
- subscription.party = "_Test Subscription Customer"
- subscription.generate_invoice_at_period_start = 1
- subscription.company = "_Test Company"
- # select subscription start date as "2018-01-15"
- subscription.start_date = "2018-01-01"
- subscription.append("plans", {"plan": "_Test Plan Multicurrency", "qty": 1})
- subscription.save()
+ def test_multi_currency_subscription(self):
+ subscription = create_subscription(
+ start_date="2018-01-01",
+ generate_invoice_at="Beginning of the current subscription period",
+ plans=[{"plan": "_Test Plan Multicurrency", "qty": 1}],
+ party="_Test Subscription Customer",
+ )
subscription.process()
self.assertEqual(len(subscription.invoices), 1)
@@ -689,42 +472,135 @@
def test_subscription_recovery(self):
"""Test if Subscription recovers when start/end date run out of sync with created invoices."""
- subscription = frappe.new_doc("Subscription")
- subscription.party_type = "Customer"
- subscription.party = "_Test Subscription Customer"
- subscription.company = "_Test Company"
- subscription.start_date = "2021-12-01"
- subscription.generate_new_invoices_past_due_date = 1
- subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
- subscription.submit_invoice = 0
- subscription.save()
+ subscription = create_subscription(
+ start_date="2021-01-01",
+ submit_invoice=0,
+ generate_new_invoices_past_due_date=1,
+ party="_Test Subscription Customer",
+ )
# create invoices for the first two moths
- frappe.flags.current_date = "2021-12-31"
- subscription.process()
+ subscription.process(posting_date="2021-01-31")
- frappe.flags.current_date = "2022-01-31"
- subscription.process()
+ subscription.process(posting_date="2021-02-28")
self.assertEqual(len(subscription.invoices), 2)
self.assertEqual(
getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "from_date")),
- getdate("2021-12-01"),
+ getdate("2021-01-01"),
)
self.assertEqual(
getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[1].name, "from_date")),
- getdate("2022-01-01"),
+ getdate("2021-02-01"),
)
# recreate most recent invoice
- subscription.process()
+ subscription.process(posting_date="2022-01-31")
self.assertEqual(len(subscription.invoices), 2)
self.assertEqual(
getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "from_date")),
- getdate("2021-12-01"),
+ getdate("2021-01-01"),
)
self.assertEqual(
getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[1].name, "from_date")),
- getdate("2022-01-01"),
+ getdate("2021-02-01"),
)
+
+ def test_subscription_invoice_generation_before_days(self):
+ subscription = create_subscription(
+ start_date="2023-01-01",
+ generate_invoice_at="Days before the current subscription period",
+ number_of_days=10,
+ generate_new_invoices_past_due_date=1,
+ )
+
+ subscription.process(posting_date="2022-12-22")
+ self.assertEqual(len(subscription.invoices), 1)
+
+ subscription.process(posting_date="2023-01-22")
+ self.assertEqual(len(subscription.invoices), 2)
+
+
+def make_plans():
+ create_plan(plan_name="_Test Plan Name", cost=900)
+ create_plan(plan_name="_Test Plan Name 2", cost=1999)
+ create_plan(
+ plan_name="_Test Plan Name 3", cost=1999, billing_interval="Day", billing_interval_count=14
+ )
+ create_plan(
+ plan_name="_Test Plan Name 4", cost=20000, billing_interval="Month", billing_interval_count=3
+ )
+ create_plan(
+ plan_name="_Test Plan Multicurrency", cost=50, billing_interval="Month", currency="USD"
+ )
+
+
+def create_plan(**kwargs):
+ if not frappe.db.exists("Subscription Plan", kwargs.get("plan_name")):
+ plan = frappe.new_doc("Subscription Plan")
+ plan.plan_name = kwargs.get("plan_name") or "_Test Plan Name"
+ plan.item = kwargs.get("item") or "_Test Non Stock Item"
+ plan.price_determination = kwargs.get("price_determination") or "Fixed Rate"
+ plan.cost = kwargs.get("cost") or 1000
+ plan.billing_interval = kwargs.get("billing_interval") or "Month"
+ plan.billing_interval_count = kwargs.get("billing_interval_count") or 1
+ plan.currency = kwargs.get("currency")
+ plan.insert()
+
+
+def create_parties():
+ if not frappe.db.exists("Supplier", "_Test Supplier"):
+ supplier = frappe.new_doc("Supplier")
+ supplier.supplier_name = "_Test Supplier"
+ supplier.supplier_group = "All Supplier Groups"
+ supplier.insert()
+
+ if not frappe.db.exists("Customer", "_Test Subscription Customer"):
+ customer = frappe.new_doc("Customer")
+ customer.customer_name = "_Test Subscription Customer"
+ customer.billing_currency = "USD"
+ customer.append(
+ "accounts", {"company": "_Test Company", "account": "_Test Receivable USD - _TC"}
+ )
+ customer.insert()
+
+
+def reset_settings():
+ settings = frappe.get_single("Subscription Settings")
+ settings.grace_period = 0
+ settings.cancel_after_grace = 0
+ settings.save()
+
+
+def create_subscription(**kwargs):
+ subscription = frappe.new_doc("Subscription")
+ subscription.party_type = (kwargs.get("party_type") or "Customer",)
+ subscription.company = kwargs.get("company") or "_Test Company"
+ subscription.party = kwargs.get("party") or "_Test Customer"
+ subscription.trial_period_start = kwargs.get("trial_period_start")
+ subscription.trial_period_end = kwargs.get("trial_period_end")
+ subscription.start_date = kwargs.get("start_date")
+ subscription.generate_invoice_at = kwargs.get("generate_invoice_at")
+ subscription.additional_discount_percentage = kwargs.get("additional_discount_percentage")
+ subscription.additional_discount_amount = kwargs.get("additional_discount_amount")
+ subscription.follow_calendar_months = kwargs.get("follow_calendar_months")
+ subscription.generate_new_invoices_past_due_date = kwargs.get(
+ "generate_new_invoices_past_due_date"
+ )
+ subscription.submit_invoice = kwargs.get("submit_invoice")
+ subscription.days_until_due = kwargs.get("days_until_due")
+ subscription.number_of_days = kwargs.get("number_of_days")
+
+ if not kwargs.get("plans"):
+ subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
+ else:
+ for plan in kwargs.get("plans"):
+ subscription.append("plans", plan)
+
+ if kwargs.get("do_not_save"):
+ return subscription
+
+ subscription.save()
+
+ return subscription
diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py
index f3acdc5..75223c2 100644
--- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py
+++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py
@@ -57,18 +57,17 @@
prorate = frappe.db.get_single_value("Subscription Settings", "prorate")
if prorate:
- prorate_factor = flt(
- date_diff(start_date, get_first_day(start_date))
- / date_diff(get_last_day(start_date), get_first_day(start_date)),
- 1,
- )
-
- prorate_factor += flt(
- date_diff(get_last_day(end_date), end_date)
- / date_diff(get_last_day(end_date), get_first_day(end_date)),
- 1,
- )
-
- cost -= plan.cost * prorate_factor
-
+ cost -= plan.cost * get_prorate_factor(start_date, end_date)
return cost
+
+
+def get_prorate_factor(start_date, end_date):
+ total_days_to_skip = date_diff(start_date, get_first_day(start_date))
+ total_days_in_month = int(get_last_day(start_date).strftime("%d"))
+ prorate_factor = flt(total_days_to_skip / total_days_in_month)
+
+ total_days_to_skip = date_diff(get_last_day(end_date), end_date)
+ total_days_in_month = int(get_last_day(end_date).strftime("%d"))
+ prorate_factor += flt(total_days_to_skip / total_days_in_month)
+
+ return prorate_factor
diff --git a/erpnext/crm/doctype/linkedin_settings/__init__.py b/erpnext/accounts/doctype/unreconcile_payment_entries/__init__.py
similarity index 100%
copy from erpnext/crm/doctype/linkedin_settings/__init__.py
copy to erpnext/accounts/doctype/unreconcile_payment_entries/__init__.py
diff --git a/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json
new file mode 100644
index 0000000..42da669
--- /dev/null
+++ b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json
@@ -0,0 +1,83 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2023-08-22 10:28:10.196712",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "account",
+ "party_type",
+ "party",
+ "reference_doctype",
+ "reference_name",
+ "allocated_amount",
+ "account_currency",
+ "unlinked"
+ ],
+ "fields": [
+ {
+ "fieldname": "reference_name",
+ "fieldtype": "Dynamic Link",
+ "in_list_view": 1,
+ "label": "Reference Name",
+ "options": "reference_doctype"
+ },
+ {
+ "fieldname": "allocated_amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Allocated Amount",
+ "options": "account_currency"
+ },
+ {
+ "default": "0",
+ "fieldname": "unlinked",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Unlinked",
+ "read_only": 1
+ },
+ {
+ "fieldname": "reference_doctype",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Reference Type",
+ "options": "DocType"
+ },
+ {
+ "fieldname": "account",
+ "fieldtype": "Data",
+ "label": "Account"
+ },
+ {
+ "fieldname": "party_type",
+ "fieldtype": "Data",
+ "label": "Party Type"
+ },
+ {
+ "fieldname": "party",
+ "fieldtype": "Data",
+ "label": "Party"
+ },
+ {
+ "fieldname": "account_currency",
+ "fieldtype": "Link",
+ "label": "Account Currency",
+ "options": "Currency",
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2023-09-05 09:33:28.620149",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Unreconcile Payment Entries",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.py b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.py
new file mode 100644
index 0000000..c41545c
--- /dev/null
+++ b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class UnreconcilePaymentEntries(Document):
+ pass
diff --git a/erpnext/crm/doctype/linkedin_settings/__init__.py b/erpnext/accounts/doctype/unreconcile_payments/__init__.py
similarity index 100%
rename from erpnext/crm/doctype/linkedin_settings/__init__.py
rename to erpnext/accounts/doctype/unreconcile_payments/__init__.py
diff --git a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py
new file mode 100644
index 0000000..78e04bf
--- /dev/null
+++ b/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py
@@ -0,0 +1,316 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+import frappe
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import today
+
+from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
+from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
+
+
+class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase):
+ def setUp(self):
+ self.create_company()
+ self.create_customer()
+ self.create_usd_receivable_account()
+ self.create_item()
+ self.clear_old_entries()
+
+ def tearDown(self):
+ frappe.db.rollback()
+
+ def create_sales_invoice(self, do_not_submit=False):
+ si = create_sales_invoice(
+ item=self.item,
+ company=self.company,
+ customer=self.customer,
+ debit_to=self.debit_to,
+ posting_date=today(),
+ parent_cost_center=self.cost_center,
+ cost_center=self.cost_center,
+ rate=100,
+ price_list_rate=100,
+ do_not_submit=do_not_submit,
+ )
+ return si
+
+ def create_payment_entry(self):
+ pe = create_payment_entry(
+ company=self.company,
+ payment_type="Receive",
+ party_type="Customer",
+ party=self.customer,
+ paid_from=self.debit_to,
+ paid_to=self.cash,
+ paid_amount=200,
+ save=True,
+ )
+ return pe
+
+ def test_01_unreconcile_invoice(self):
+ si1 = self.create_sales_invoice()
+ si2 = self.create_sales_invoice()
+
+ pe = self.create_payment_entry()
+ pe.append(
+ "references",
+ {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 100},
+ )
+ pe.append(
+ "references",
+ {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 100},
+ )
+ # Allocation payment against both invoices
+ pe.save().submit()
+
+ # Assert outstanding
+ [doc.reload() for doc in [si1, si2, pe]]
+ self.assertEqual(si1.outstanding_amount, 0)
+ self.assertEqual(si2.outstanding_amount, 0)
+ self.assertEqual(pe.unallocated_amount, 0)
+
+ unreconcile = frappe.get_doc(
+ {
+ "doctype": "Unreconcile Payments",
+ "company": self.company,
+ "voucher_type": pe.doctype,
+ "voucher_no": pe.name,
+ }
+ )
+ unreconcile.add_references()
+ self.assertEqual(len(unreconcile.allocations), 2)
+ allocations = [x.reference_name for x in unreconcile.allocations]
+ self.assertEquals([si1.name, si2.name], allocations)
+ # unreconcile si1
+ for x in unreconcile.allocations:
+ if x.reference_name != si1.name:
+ unreconcile.remove(x)
+ unreconcile.save().submit()
+
+ # Assert outstanding
+ [doc.reload() for doc in [si1, si2, pe]]
+ self.assertEqual(si1.outstanding_amount, 100)
+ self.assertEqual(si2.outstanding_amount, 0)
+ self.assertEqual(len(pe.references), 1)
+ self.assertEqual(pe.unallocated_amount, 100)
+
+ def test_02_unreconcile_one_payment_from_multi_payments(self):
+ """
+ Scenario: 2 payments, both split against 2 different invoices
+ Unreconcile only one payment from one invoice
+ """
+ si1 = self.create_sales_invoice()
+ si2 = self.create_sales_invoice()
+ pe1 = self.create_payment_entry()
+ pe1.paid_amount = 100
+ # Allocate payment against both invoices
+ pe1.append(
+ "references",
+ {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50},
+ )
+ pe1.append(
+ "references",
+ {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50},
+ )
+ pe1.save().submit()
+
+ pe2 = self.create_payment_entry()
+ pe2.paid_amount = 100
+ # Allocate payment against both invoices
+ pe2.append(
+ "references",
+ {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50},
+ )
+ pe2.append(
+ "references",
+ {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50},
+ )
+ pe2.save().submit()
+
+ # Assert outstanding and unallocated
+ [doc.reload() for doc in [si1, si2, pe1, pe2]]
+ self.assertEqual(si1.outstanding_amount, 0.0)
+ self.assertEqual(si2.outstanding_amount, 0.0)
+ self.assertEqual(pe1.unallocated_amount, 0.0)
+ self.assertEqual(pe2.unallocated_amount, 0.0)
+
+ unreconcile = frappe.get_doc(
+ {
+ "doctype": "Unreconcile Payments",
+ "company": self.company,
+ "voucher_type": pe2.doctype,
+ "voucher_no": pe2.name,
+ }
+ )
+ unreconcile.add_references()
+ self.assertEqual(len(unreconcile.allocations), 2)
+ allocations = [x.reference_name for x in unreconcile.allocations]
+ self.assertEquals([si1.name, si2.name], allocations)
+ # unreconcile si1 from pe2
+ for x in unreconcile.allocations:
+ if x.reference_name != si1.name:
+ unreconcile.remove(x)
+ unreconcile.save().submit()
+
+ # Assert outstanding and unallocated
+ [doc.reload() for doc in [si1, si2, pe1, pe2]]
+ self.assertEqual(si1.outstanding_amount, 50)
+ self.assertEqual(si2.outstanding_amount, 0)
+ self.assertEqual(len(pe1.references), 2)
+ self.assertEqual(len(pe2.references), 1)
+ self.assertEqual(pe1.unallocated_amount, 0)
+ self.assertEqual(pe2.unallocated_amount, 50)
+
+ def test_03_unreconciliation_on_multi_currency_invoice(self):
+ self.create_customer("_Test MC Customer USD", "USD")
+ si1 = self.create_sales_invoice(do_not_submit=True)
+ si1.currency = "USD"
+ si1.debit_to = self.debtors_usd
+ si1.conversion_rate = 80
+ si1.save().submit()
+
+ si2 = self.create_sales_invoice(do_not_submit=True)
+ si2.currency = "USD"
+ si2.debit_to = self.debtors_usd
+ si2.conversion_rate = 80
+ si2.save().submit()
+
+ pe = self.create_payment_entry()
+ pe.paid_from = self.debtors_usd
+ pe.paid_from_account_currency = "USD"
+ pe.source_exchange_rate = 75
+ pe.received_amount = 75 * 200
+ pe.save()
+ # Allocate payment against both invoices
+ pe.append(
+ "references",
+ {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 100},
+ )
+ pe.append(
+ "references",
+ {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 100},
+ )
+ pe.save().submit()
+
+ unreconcile = frappe.get_doc(
+ {
+ "doctype": "Unreconcile Payments",
+ "company": self.company,
+ "voucher_type": pe.doctype,
+ "voucher_no": pe.name,
+ }
+ )
+ unreconcile.add_references()
+ self.assertEqual(len(unreconcile.allocations), 2)
+ allocations = [x.reference_name for x in unreconcile.allocations]
+ self.assertEquals([si1.name, si2.name], allocations)
+ # unreconcile si1 from pe
+ for x in unreconcile.allocations:
+ if x.reference_name != si1.name:
+ unreconcile.remove(x)
+ unreconcile.save().submit()
+
+ # Assert outstanding and unallocated
+ [doc.reload() for doc in [si1, si2, pe]]
+ self.assertEqual(si1.outstanding_amount, 100)
+ self.assertEqual(si2.outstanding_amount, 0)
+ self.assertEqual(len(pe.references), 1)
+ self.assertEqual(pe.unallocated_amount, 100)
+
+ # Exc gain/loss JE should've been cancelled as well
+ self.assertEqual(
+ frappe.db.count(
+ "Journal Entry Account",
+ filters={"reference_type": si1.doctype, "reference_name": si1.name, "docstatus": 1},
+ ),
+ 0,
+ )
+
+ def test_04_unreconciliation_on_multi_currency_invoice(self):
+ """
+ 2 payments split against 2 foreign currency invoices
+ """
+ self.create_customer("_Test MC Customer USD", "USD")
+ si1 = self.create_sales_invoice(do_not_submit=True)
+ si1.currency = "USD"
+ si1.debit_to = self.debtors_usd
+ si1.conversion_rate = 80
+ si1.save().submit()
+
+ si2 = self.create_sales_invoice(do_not_submit=True)
+ si2.currency = "USD"
+ si2.debit_to = self.debtors_usd
+ si2.conversion_rate = 80
+ si2.save().submit()
+
+ pe1 = self.create_payment_entry()
+ pe1.paid_from = self.debtors_usd
+ pe1.paid_from_account_currency = "USD"
+ pe1.source_exchange_rate = 75
+ pe1.received_amount = 75 * 100
+ pe1.save()
+ # Allocate payment against both invoices
+ pe1.append(
+ "references",
+ {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50},
+ )
+ pe1.append(
+ "references",
+ {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50},
+ )
+ pe1.save().submit()
+
+ pe2 = self.create_payment_entry()
+ pe2.paid_from = self.debtors_usd
+ pe2.paid_from_account_currency = "USD"
+ pe2.source_exchange_rate = 75
+ pe2.received_amount = 75 * 100
+ pe2.save()
+ # Allocate payment against both invoices
+ pe2.append(
+ "references",
+ {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50},
+ )
+ pe2.append(
+ "references",
+ {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50},
+ )
+ pe2.save().submit()
+
+ unreconcile = frappe.get_doc(
+ {
+ "doctype": "Unreconcile Payments",
+ "company": self.company,
+ "voucher_type": pe2.doctype,
+ "voucher_no": pe2.name,
+ }
+ )
+ unreconcile.add_references()
+ self.assertEqual(len(unreconcile.allocations), 2)
+ allocations = [x.reference_name for x in unreconcile.allocations]
+ self.assertEquals([si1.name, si2.name], allocations)
+ # unreconcile si1 from pe2
+ for x in unreconcile.allocations:
+ if x.reference_name != si1.name:
+ unreconcile.remove(x)
+ unreconcile.save().submit()
+
+ # Assert outstanding and unallocated
+ [doc.reload() for doc in [si1, si2, pe1, pe2]]
+ self.assertEqual(si1.outstanding_amount, 50)
+ self.assertEqual(si2.outstanding_amount, 0)
+ self.assertEqual(len(pe1.references), 2)
+ self.assertEqual(len(pe2.references), 1)
+ self.assertEqual(pe1.unallocated_amount, 0)
+ self.assertEqual(pe2.unallocated_amount, 50)
+
+ # Exc gain/loss JE from PE1 should be available
+ self.assertEqual(
+ frappe.db.count(
+ "Journal Entry Account",
+ filters={"reference_type": si1.doctype, "reference_name": si1.name, "docstatus": 1},
+ ),
+ 1,
+ )
diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js
new file mode 100644
index 0000000..c522567
--- /dev/null
+++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js
@@ -0,0 +1,41 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on("Unreconcile Payments", {
+ refresh(frm) {
+ frm.set_query("voucher_type", function() {
+ return {
+ filters: {
+ name: ["in", ["Payment Entry", "Journal Entry"]]
+ }
+ }
+ });
+
+
+ frm.set_query("voucher_no", function(doc) {
+ return {
+ filters: {
+ company: doc.company,
+ docstatus: 1
+ }
+ }
+ });
+
+ },
+ get_allocations: function(frm) {
+ frm.clear_table("allocations");
+ frappe.call({
+ method: "get_allocations_from_payment",
+ doc: frm.doc,
+ callback: function(r) {
+ if (r.message) {
+ r.message.forEach(x => {
+ frm.add_child("allocations", x)
+ })
+ frm.refresh_fields();
+ }
+ }
+ })
+
+ }
+});
diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json
new file mode 100644
index 0000000..f29e61b
--- /dev/null
+++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json
@@ -0,0 +1,93 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "format:UNREC-{#####}",
+ "creation": "2023-08-22 10:26:34.421423",
+ "default_view": "List",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "company",
+ "voucher_type",
+ "voucher_no",
+ "get_allocations",
+ "allocations",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Unreconcile Payments",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company"
+ },
+ {
+ "fieldname": "voucher_type",
+ "fieldtype": "Link",
+ "label": "Voucher Type",
+ "options": "DocType"
+ },
+ {
+ "fieldname": "voucher_no",
+ "fieldtype": "Dynamic Link",
+ "label": "Voucher No",
+ "options": "voucher_type"
+ },
+ {
+ "fieldname": "get_allocations",
+ "fieldtype": "Button",
+ "label": "Get Allocations"
+ },
+ {
+ "fieldname": "allocations",
+ "fieldtype": "Table",
+ "label": "Allocations",
+ "options": "Unreconcile Payment Entries"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2023-08-28 17:42:50.261377",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Unreconcile Payments",
+ "naming_rule": "Expression",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "read": 1,
+ "role": "Accounts Manager",
+ "select": 1,
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "read": 1,
+ "role": "Accounts User",
+ "select": 1,
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py
new file mode 100644
index 0000000..4f9fb50
--- /dev/null
+++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py
@@ -0,0 +1,158 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _, qb
+from frappe.model.document import Document
+from frappe.query_builder import Criterion
+from frappe.query_builder.functions import Abs, Sum
+from frappe.utils.data import comma_and
+
+from erpnext.accounts.utils import (
+ cancel_exchange_gain_loss_journal,
+ unlink_ref_doc_from_payment_entries,
+ update_voucher_outstanding,
+)
+
+
+class UnreconcilePayments(Document):
+ def validate(self):
+ self.supported_types = ["Payment Entry", "Journal Entry"]
+ if not self.voucher_type in self.supported_types:
+ frappe.throw(_("Only {0} are supported").format(comma_and(self.supported_types)))
+
+ @frappe.whitelist()
+ def get_allocations_from_payment(self):
+ allocated_references = []
+ ple = qb.DocType("Payment Ledger Entry")
+ allocated_references = (
+ qb.from_(ple)
+ .select(
+ ple.account,
+ ple.party_type,
+ ple.party,
+ ple.against_voucher_type.as_("reference_doctype"),
+ ple.against_voucher_no.as_("reference_name"),
+ Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
+ ple.account_currency,
+ )
+ .where(
+ (ple.docstatus == 1)
+ & (ple.voucher_type == self.voucher_type)
+ & (ple.voucher_no == self.voucher_no)
+ & (ple.voucher_no != ple.against_voucher_no)
+ )
+ .groupby(ple.against_voucher_type, ple.against_voucher_no)
+ .run(as_dict=True)
+ )
+
+ return allocated_references
+
+ def add_references(self):
+ allocations = self.get_allocations_from_payment()
+
+ for alloc in allocations:
+ self.append("allocations", alloc)
+
+ def on_submit(self):
+ # todo: more granular unreconciliation
+ for alloc in self.allocations:
+ doc = frappe.get_doc(alloc.reference_doctype, alloc.reference_name)
+ unlink_ref_doc_from_payment_entries(doc, self.voucher_no)
+ cancel_exchange_gain_loss_journal(doc, self.voucher_type, self.voucher_no)
+ update_voucher_outstanding(
+ alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party
+ )
+ frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True)
+
+
+@frappe.whitelist()
+def doc_has_references(doctype: str = None, docname: str = None):
+ if doctype in ["Sales Invoice", "Purchase Invoice"]:
+ return frappe.db.count(
+ "Payment Ledger Entry",
+ filters={"delinked": 0, "against_voucher_no": docname, "amount": ["<", 0]},
+ )
+ else:
+ return frappe.db.count(
+ "Payment Ledger Entry",
+ filters={"delinked": 0, "voucher_no": docname, "against_voucher_no": ["!=", docname]},
+ )
+
+
+@frappe.whitelist()
+def get_linked_payments_for_doc(
+ company: str = None, doctype: str = None, docname: str = None
+) -> list:
+ if company and doctype and docname:
+ _dt = doctype
+ _dn = docname
+ ple = qb.DocType("Payment Ledger Entry")
+ if _dt in ["Sales Invoice", "Purchase Invoice"]:
+ criteria = [
+ (ple.company == company),
+ (ple.delinked == 0),
+ (ple.against_voucher_no == _dn),
+ (ple.amount < 0),
+ ]
+
+ res = (
+ qb.from_(ple)
+ .select(
+ ple.company,
+ ple.voucher_type,
+ ple.voucher_no,
+ Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
+ ple.account_currency,
+ )
+ .where(Criterion.all(criteria))
+ .groupby(ple.voucher_no, ple.against_voucher_no)
+ .having(qb.Field("allocated_amount") > 0)
+ .run(as_dict=True)
+ )
+ return res
+ else:
+ criteria = [
+ (ple.company == company),
+ (ple.delinked == 0),
+ (ple.voucher_no == _dn),
+ (ple.against_voucher_no != _dn),
+ ]
+
+ query = (
+ qb.from_(ple)
+ .select(
+ ple.company,
+ ple.against_voucher_type.as_("voucher_type"),
+ ple.against_voucher_no.as_("voucher_no"),
+ Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
+ ple.account_currency,
+ )
+ .where(Criterion.all(criteria))
+ .groupby(ple.against_voucher_no)
+ )
+ res = query.run(as_dict=True)
+ return res
+ return []
+
+
+@frappe.whitelist()
+def create_unreconcile_doc_for_selection(selections=None):
+ if selections:
+ selections = frappe.json.loads(selections)
+ # assuming each row is a unique voucher
+ for row in selections:
+ unrecon = frappe.new_doc("Unreconcile Payments")
+ unrecon.company = row.get("company")
+ unrecon.voucher_type = row.get("voucher_type")
+ unrecon.voucher_no = row.get("voucher_no")
+ unrecon.add_references()
+
+ # remove unselected references
+ unrecon.allocations = [
+ x
+ for x in unrecon.allocations
+ if x.reference_doctype == row.get("against_voucher_type")
+ and x.reference_name == row.get("against_voucher_no")
+ ]
+ unrecon.save().submit()
diff --git a/erpnext/accounts/report/accounts_payable/test_accounts_payable.py b/erpnext/accounts/report/accounts_payable/test_accounts_payable.py
index cb84cf4..3cf93cc 100644
--- a/erpnext/accounts/report/accounts_payable/test_accounts_payable.py
+++ b/erpnext/accounts/report/accounts_payable/test_accounts_payable.py
@@ -24,7 +24,7 @@
def tearDown(self):
frappe.db.rollback()
- def test_accounts_receivable_with_supplier(self):
+ def test_accounts_payable_for_foreign_currency_supplier(self):
pi = self.create_purchase_invoice(do_not_submit=True)
pi.currency = "USD"
pi.conversion_rate = 80
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js
index cb8ec87..bb00d61 100644
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js
@@ -38,33 +38,32 @@
}
},
{
- "fieldname": "customer",
- "label": __("Customer"),
+ "fieldname": "party_type",
+ "label": __("Party Type"),
"fieldtype": "Link",
- "options": "Customer",
+ "options": "Party Type",
+ "Default": "Customer",
+ get_query: () => {
+ return {
+ filters: {
+ 'account_type': 'Receivable'
+ }
+ };
+ },
on_change: () => {
- var customer = frappe.query_report.get_filter_value('customer');
- var company = frappe.query_report.get_filter_value('company');
- if (customer) {
- frappe.db.get_value('Customer', customer, ["customer_name", "payment_terms"], function(value) {
- frappe.query_report.set_filter_value('customer_name', value["customer_name"]);
- frappe.query_report.set_filter_value('payment_terms', value["payment_terms"]);
- });
+ frappe.query_report.set_filter_value('party', "");
+ let party_type = frappe.query_report.get_filter_value('party_type');
+ frappe.query_report.toggle_filter_display('customer_group', frappe.query_report.get_filter_value('party_type') !== "Customer");
- frappe.db.get_value('Customer Credit Limit', {'parent': customer, 'company': company},
- ["credit_limit"], function(value) {
- if (value) {
- frappe.query_report.set_filter_value('credit_limit', value["credit_limit"]);
- }
- }, "Customer");
- } else {
- frappe.query_report.set_filter_value('customer_name', "");
- frappe.query_report.set_filter_value('credit_limit', "");
- frappe.query_report.set_filter_value('payment_terms', "");
- }
}
},
{
+ "fieldname":"party",
+ "label": __("Party"),
+ "fieldtype": "Dynamic Link",
+ "options": "party_type",
+ },
+ {
"fieldname": "party_account",
"label": __("Receivable Account"),
"fieldtype": "Link",
@@ -174,24 +173,6 @@
"fieldname": "show_remarks",
"label": __("Show Remarks"),
"fieldtype": "Check",
- },
- {
- "fieldname": "customer_name",
- "label": __("Customer Name"),
- "fieldtype": "Data",
- "hidden": 1
- },
- {
- "fieldname": "payment_terms",
- "label": __("Payment Tems"),
- "fieldtype": "Data",
- "hidden": 1
- },
- {
- "fieldname": "credit_limit",
- "label": __("Credit Limit"),
- "fieldtype": "Currency",
- "hidden": 1
}
],
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index 14f8993..7942402 100755
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -769,15 +769,12 @@
self.or_filters = []
for party_type in self.party_type:
- party_type_field = scrub(party_type)
- self.or_filters.append(self.ple.party_type == party_type)
+ self.add_common_filters()
- self.add_common_filters(party_type_field=party_type_field)
-
- if party_type_field == "customer":
+ if self.account_type == "Receivable":
self.add_customer_filters()
- elif party_type_field == "supplier":
+ elif self.account_type == "Payable":
self.add_supplier_filters()
if self.filters.cost_center:
@@ -793,16 +790,13 @@
]
self.qb_selection_filter.append(self.ple.cost_center.isin(cost_center_list))
- def add_common_filters(self, party_type_field):
+ def add_common_filters(self):
if self.filters.company:
self.qb_selection_filter.append(self.ple.company == self.filters.company)
if self.filters.finance_book:
self.qb_selection_filter.append(self.ple.finance_book == self.filters.finance_book)
- if self.filters.get(party_type_field):
- self.qb_selection_filter.append(self.ple.party == self.filters.get(party_type_field))
-
if self.filters.get("party_type"):
self.qb_selection_filter.append(self.filters.party_type == self.ple.party_type)
@@ -969,6 +963,20 @@
fieldtype="Link",
options="Contact",
)
+ if self.filters.party_type == "Customer":
+ self.add_column(
+ _("Customer Name"),
+ fieldname="customer_name",
+ fieldtype="Link",
+ options="Customer",
+ )
+ elif self.filters.party_type == "Supplier":
+ self.add_column(
+ _("Supplier Name"),
+ fieldname="supplier_name",
+ fieldtype="Link",
+ options="Supplier",
+ )
self.add_column(label=_("Cost Center"), fieldname="cost_center", fieldtype="Data")
self.add_column(label=_("Voucher Type"), fieldname="voucher_type", fieldtype="Data")
diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
index 0c7d931..b98916e 100644
--- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
@@ -568,3 +568,40 @@
row.account_currency,
],
)
+
+ def test_usd_customer_filter(self):
+ filters = {
+ "company": self.company,
+ "party_type": "Customer",
+ "party": self.customer,
+ "report_date": today(),
+ "range1": 30,
+ "range2": 60,
+ "range3": 90,
+ "range4": 120,
+ }
+
+ si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
+ si.currency = "USD"
+ si.conversion_rate = 80
+ si.debit_to = self.debtors_usd
+ si.save().submit()
+ name = si.name
+
+ # check invoice grand total and invoiced column's value for 3 payment terms
+ report = execute(filters)
+
+ expected = {
+ "voucher_type": si.doctype,
+ "voucher_no": si.name,
+ "party_account": self.debtors_usd,
+ "customer_name": self.customer,
+ "invoiced": 100.0,
+ "outstanding": 100.0,
+ "account_currency": "USD",
+ }
+ self.assertEqual(len(report[1]), 1)
+ report_output = report[1][0]
+ for field in expected:
+ with self.subTest(field=field):
+ self.assertEqual(report_output.get(field), expected.get(field))
diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py
index 28d0c20..7b1a902 100644
--- a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py
+++ b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py
@@ -81,7 +81,7 @@
self.create_item("_Test Internet Subscription", 0, self.warehouse, self.company)
item = frappe.get_doc("Item", self.item)
item.enable_deferred_revenue = 1
- item.deferred_revenue_account = self.deferred_revenue_account
+ item.item_defaults[0].deferred_revenue_account = self.deferred_revenue_account
item.no_of_months = 3
item.save()
@@ -150,7 +150,7 @@
self.create_item("_Test Office Desk", 0, self.warehouse, self.company)
item = frappe.get_doc("Item", self.item)
item.enable_deferred_expense = 1
- item.deferred_expense_account = self.deferred_expense_account
+ item.item_defaults[0].deferred_expense_account = self.deferred_expense_account
item.no_of_months_exp = 3
item.save()
diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.js b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.js
index b709ab9..423cd6a 100644
--- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.js
+++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.js
@@ -24,6 +24,12 @@
"options": "Item",
},
{
+ "fieldname": "item_group",
+ "label": __("Item Group"),
+ "fieldtype": "Link",
+ "options": "Item Group",
+ },
+ {
"fieldname":"supplier",
"label": __("Supplier"),
"fieldtype": "Link",
diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
index 5d3d4d7..ad196a9 100644
--- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
+++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
@@ -293,6 +293,7 @@
("from_date", " and `tabPurchase Invoice`.posting_date>=%(from_date)s"),
("to_date", " and `tabPurchase Invoice`.posting_date<=%(to_date)s"),
("mode_of_payment", " and ifnull(mode_of_payment, '') = %(mode_of_payment)s"),
+ ("item_group", " and ifnull(`tabPurchase Invoice Item`.item_group, '') = %(item_group)s"),
):
if filters.get(opts[0]):
conditions += opts[1]
diff --git a/erpnext/accounts/report/trial_balance/trial_balance.js b/erpnext/accounts/report/trial_balance/trial_balance.js
index ee6b4fe..c12ab0f 100644
--- a/erpnext/accounts/report/trial_balance/trial_balance.js
+++ b/erpnext/accounts/report/trial_balance/trial_balance.js
@@ -99,6 +99,12 @@
"label": __("Include Default Book Entries"),
"fieldtype": "Check",
"default": 1
+ },
+ {
+ "fieldname": "show_net_values",
+ "label": __("Show net values in opening and closing columns"),
+ "fieldtype": "Check",
+ "default": 1
}
],
"formatter": erpnext.financial_statements.formatter,
diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py
index 376571f..2a8aa0c 100644
--- a/erpnext/accounts/report/trial_balance/trial_balance.py
+++ b/erpnext/accounts/report/trial_balance/trial_balance.py
@@ -120,7 +120,9 @@
ignore_opening_entries=True,
)
- calculate_values(accounts, gl_entries_by_account, opening_balances)
+ calculate_values(
+ accounts, gl_entries_by_account, opening_balances, filters.get("show_net_values")
+ )
accumulate_values_into_parents(accounts, accounts_by_name)
data = prepare_data(accounts, filters, parent_children_map, company_currency)
@@ -310,7 +312,7 @@
return gle
-def calculate_values(accounts, gl_entries_by_account, opening_balances):
+def calculate_values(accounts, gl_entries_by_account, opening_balances, show_net_values):
init = {
"opening_debit": 0.0,
"opening_credit": 0.0,
@@ -335,7 +337,8 @@
d["closing_debit"] = d["opening_debit"] + d["debit"]
d["closing_credit"] = d["opening_credit"] + d["credit"]
- prepare_opening_closing(d)
+ if show_net_values:
+ prepare_opening_closing(d)
def calculate_total_row(accounts, company_currency):
@@ -375,7 +378,7 @@
for d in accounts:
# Prepare opening closing for group account
- if parent_children_map.get(d.account):
+ if parent_children_map.get(d.account) and filters.get("show_net_values"):
prepare_opening_closing(d)
has_value = False
diff --git a/erpnext/accounts/test/accounts_mixin.py b/erpnext/accounts/test/accounts_mixin.py
index 0868860..d503f7b 100644
--- a/erpnext/accounts/test/accounts_mixin.py
+++ b/erpnext/accounts/test/accounts_mixin.py
@@ -158,6 +158,8 @@
"Journal Entry",
"Sales Order",
"Exchange Rate Revaluation",
+ "Bank Account",
+ "Bank Transaction",
]
for doctype in doctype_list:
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 6a80f20..555ed4f 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -581,6 +581,10 @@
"""
jv_detail = journal_entry.get("accounts", {"name": d["voucher_detail_no"]})[0]
+ # Update Advance Paid in SO/PO since they might be getting unlinked
+ if jv_detail.get("reference_type") in ("Sales Order", "Purchase Order"):
+ frappe.get_doc(jv_detail.reference_type, jv_detail.reference_name).set_total_advance_paid()
+
if flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) != 0:
# adjust the unreconciled balance
amount_in_account_currency = flt(d["unadjusted_amount"]) - flt(d["allocated_amount"])
@@ -647,6 +651,13 @@
if d.voucher_detail_no:
existing_row = payment_entry.get("references", {"name": d["voucher_detail_no"]})[0]
+
+ # Update Advance Paid in SO/PO since they are getting unlinked
+ if existing_row.get("reference_doctype") in ("Sales Order", "Purchase Order"):
+ frappe.get_doc(
+ existing_row.reference_doctype, existing_row.reference_name
+ ).set_total_advance_paid()
+
original_row = existing_row.as_dict().copy()
existing_row.update(reference_details)
@@ -674,7 +685,9 @@
payment_entry.save(ignore_permissions=True)
-def cancel_exchange_gain_loss_journal(parent_doc: dict | object) -> None:
+def cancel_exchange_gain_loss_journal(
+ parent_doc: dict | object, referenced_dt: str = None, referenced_dn: str = None
+) -> None:
"""
Cancel Exchange Gain/Loss for Sales/Purchase Invoice, if they have any.
"""
@@ -701,76 +714,147 @@
as_list=1,
)
for doc in gain_loss_journals:
- frappe.get_doc("Journal Entry", doc[0]).cancel()
+ gain_loss_je = frappe.get_doc("Journal Entry", doc[0])
+ if referenced_dt and referenced_dn:
+ references = [(x.reference_type, x.reference_name) for x in gain_loss_je.accounts]
+ if (
+ len(references) == 2
+ and (referenced_dt, referenced_dn) in references
+ and (parent_doc.doctype, parent_doc.name) in references
+ ):
+ # only cancel JE generated against parent_doc and referenced_dn
+ gain_loss_je.cancel()
+ else:
+ gain_loss_je.cancel()
-def unlink_ref_doc_from_payment_entries(ref_doc):
- remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name)
- remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name)
-
- frappe.db.sql(
- """update `tabGL Entry`
- set against_voucher_type=null, against_voucher=null,
- modified=%s, modified_by=%s
- where against_voucher_type=%s and against_voucher=%s
- and voucher_no != ifnull(against_voucher, '')""",
- (now(), frappe.session.user, ref_doc.doctype, ref_doc.name),
+def update_accounting_ledgers_after_reference_removal(
+ ref_type: str = None, ref_no: str = None, payment_name: str = None
+):
+ # General Ledger
+ gle = qb.DocType("GL Entry")
+ gle_update_query = (
+ qb.update(gle)
+ .set(gle.against_voucher_type, None)
+ .set(gle.against_voucher, None)
+ .set(gle.modified, now())
+ .set(gle.modified_by, frappe.session.user)
+ .where((gle.against_voucher_type == ref_type) & (gle.against_voucher == ref_no))
)
+ if payment_name:
+ gle_update_query = gle_update_query.where(gle.voucher_no == payment_name)
+ gle_update_query.run()
+
+ # Payment Ledger
ple = qb.DocType("Payment Ledger Entry")
+ ple_update_query = (
+ qb.update(ple)
+ .set(ple.against_voucher_type, ple.voucher_type)
+ .set(ple.against_voucher_no, ple.voucher_no)
+ .set(ple.modified, now())
+ .set(ple.modified_by, frappe.session.user)
+ .where(
+ (ple.against_voucher_type == ref_type)
+ & (ple.against_voucher_no == ref_no)
+ & (ple.delinked == 0)
+ )
+ )
- qb.update(ple).set(ple.against_voucher_type, ple.voucher_type).set(
- ple.against_voucher_no, ple.voucher_no
- ).set(ple.modified, now()).set(ple.modified_by, frappe.session.user).where(
- (ple.against_voucher_type == ref_doc.doctype)
- & (ple.against_voucher_no == ref_doc.name)
- & (ple.delinked == 0)
- ).run()
+ if payment_name:
+ ple_update_query = ple_update_query.where(ple.voucher_no == payment_name)
+ ple_update_query.run()
+
+def remove_ref_from_advance_section(ref_doc: object = None):
+ # TODO: this might need some testing
if ref_doc.doctype in ("Sales Invoice", "Purchase Invoice"):
ref_doc.set("advances", [])
-
- frappe.db.sql(
- """delete from `tab{0} Advance` where parent = %s""".format(ref_doc.doctype), ref_doc.name
- )
+ adv_type = qb.DocType(f"{ref_doc.doctype} Advance")
+ qb.from_(adv_type).delete().where(adv_type.parent == ref_doc.name).run()
-def remove_ref_doc_link_from_jv(ref_type, ref_no):
- linked_jv = frappe.db.sql_list(
- """select parent from `tabJournal Entry Account`
- where reference_type=%s and reference_name=%s and docstatus < 2""",
- (ref_type, ref_no),
+def unlink_ref_doc_from_payment_entries(ref_doc: object = None, payment_name: str = None):
+ remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name, payment_name)
+ remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name, payment_name)
+ update_accounting_ledgers_after_reference_removal(ref_doc.doctype, ref_doc.name, payment_name)
+ remove_ref_from_advance_section(ref_doc)
+
+
+def remove_ref_doc_link_from_jv(
+ ref_type: str = None, ref_no: str = None, payment_name: str = None
+):
+ jea = qb.DocType("Journal Entry Account")
+
+ linked_jv = (
+ qb.from_(jea)
+ .select(jea.parent)
+ .where((jea.reference_type == ref_type) & (jea.reference_name == ref_no) & (jea.docstatus.lt(2)))
+ .run(as_list=1)
)
+ linked_jv = convert_to_list(linked_jv)
+ # remove reference only from specified payment
+ linked_jv = [x for x in linked_jv if x == payment_name] if payment_name else linked_jv
if linked_jv:
- frappe.db.sql(
- """update `tabJournal Entry Account`
- set reference_type=null, reference_name = null,
- modified=%s, modified_by=%s
- where reference_type=%s and reference_name=%s
- and docstatus < 2""",
- (now(), frappe.session.user, ref_type, ref_no),
+ update_query = (
+ qb.update(jea)
+ .set(jea.reference_type, None)
+ .set(jea.reference_name, None)
+ .set(jea.modified, now())
+ .set(jea.modified_by, frappe.session.user)
+ .where((jea.reference_type == ref_type) & (jea.reference_name == ref_no))
)
+ if payment_name:
+ update_query = update_query.where(jea.parent == payment_name)
+
+ update_query.run()
+
frappe.msgprint(_("Journal Entries {0} are un-linked").format("\n".join(linked_jv)))
-def remove_ref_doc_link_from_pe(ref_type, ref_no):
- linked_pe = frappe.db.sql_list(
- """select parent from `tabPayment Entry Reference`
- where reference_doctype=%s and reference_name=%s and docstatus < 2""",
- (ref_type, ref_no),
+def convert_to_list(result):
+ """
+ Convert tuple to list
+ """
+ return [x[0] for x in result]
+
+
+def remove_ref_doc_link_from_pe(
+ ref_type: str = None, ref_no: str = None, payment_name: str = None
+):
+ per = qb.DocType("Payment Entry Reference")
+ pay = qb.DocType("Payment Entry")
+
+ linked_pe = (
+ qb.from_(per)
+ .select(per.parent)
+ .where(
+ (per.reference_doctype == ref_type) & (per.reference_name == ref_no) & (per.docstatus.lt(2))
+ )
+ .run(as_list=1)
)
+ linked_pe = convert_to_list(linked_pe)
+ # remove reference only from specified payment
+ linked_pe = [x for x in linked_pe if x == payment_name] if payment_name else linked_pe
if linked_pe:
- frappe.db.sql(
- """update `tabPayment Entry Reference`
- set allocated_amount=0, modified=%s, modified_by=%s
- where reference_doctype=%s and reference_name=%s
- and docstatus < 2""",
- (now(), frappe.session.user, ref_type, ref_no),
+ update_query = (
+ qb.update(per)
+ .set(per.allocated_amount, 0)
+ .set(per.modified, now())
+ .set(per.modified_by, frappe.session.user)
+ .where(
+ (per.docstatus.lt(2) & (per.reference_doctype == ref_type) & (per.reference_name == ref_no))
+ )
)
+ if payment_name:
+ update_query = update_query.where(per.parent == payment_name)
+
+ update_query.run()
+
for pe in linked_pe:
try:
pe_doc = frappe.get_doc("Payment Entry", pe)
@@ -784,19 +868,13 @@
msg += _("Please cancel payment entry manually first")
frappe.throw(msg, exc=PaymentEntryUnlinkError, title=_("Payment Unlink Error"))
- frappe.db.sql(
- """update `tabPayment Entry` set total_allocated_amount=%s,
- base_total_allocated_amount=%s, unallocated_amount=%s, modified=%s, modified_by=%s
- where name=%s""",
- (
- pe_doc.total_allocated_amount,
- pe_doc.base_total_allocated_amount,
- pe_doc.unallocated_amount,
- now(),
- frappe.session.user,
- pe,
- ),
- )
+ qb.update(pay).set(pay.total_allocated_amount, pe_doc.total_allocated_amount).set(
+ pay.base_total_allocated_amount, pe_doc.base_total_allocated_amount
+ ).set(pay.unallocated_amount, pe_doc.unallocated_amount).set(pay.modified, now()).set(
+ pay.modified_by, frappe.session.user
+ ).where(
+ pay.name == pe
+ ).run()
frappe.msgprint(_("Payment Entries {0} are un-linked").format("\n".join(linked_pe)))
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js
index dae993a..03afcb9 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.js
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.js
@@ -40,16 +40,6 @@
}
}
});
-
- let sbb_field = frm.get_docfield('stock_items', 'serial_and_batch_bundle');
- if (sbb_field) {
- sbb_field.get_route_options_for_new_doc = (row) => {
- return {
- 'item_code': row.doc.item_code,
- 'voucher_type': frm.doc.doctype,
- }
- };
- }
},
refresh: function(frm) {
@@ -61,6 +51,16 @@
frappe.set_route("query-report", "General Ledger");
});
}
+
+ let sbb_field = frm.get_docfield('stock_items', 'serial_and_batch_bundle');
+ if (sbb_field) {
+ sbb_field.get_route_options_for_new_doc = (row) => {
+ return {
+ 'item_code': row.doc.item_code,
+ 'voucher_type': frm.doc.doctype,
+ }
+ };
+ }
},
repair_status: (frm) => {
diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json
index a07af71..19972ca 100644
--- a/erpnext/buying/doctype/supplier/supplier.json
+++ b/erpnext/buying/doctype/supplier/supplier.json
@@ -45,12 +45,13 @@
"column_break1",
"contact_html",
"primary_address_and_contact_detail_section",
- "supplier_primary_contact",
- "mobile_no",
- "email_id",
"column_break_44",
"supplier_primary_address",
"primary_address",
+ "column_break_mglr",
+ "supplier_primary_contact",
+ "mobile_no",
+ "email_id",
"accounting_tab",
"payment_terms",
"default_accounts_section",
@@ -469,6 +470,10 @@
{
"fieldname": "column_break_1mqv",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_mglr",
+ "fieldtype": "Column Break"
}
],
"icon": "fa fa-user",
@@ -481,7 +486,7 @@
"link_fieldname": "party"
}
],
- "modified": "2023-06-26 14:20:00.961554",
+ "modified": "2023-09-21 12:24:20.398889",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",
diff --git a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py
index 21241e0..0718735 100644
--- a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py
+++ b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py
@@ -7,7 +7,7 @@
import frappe
from frappe import _
from frappe.query_builder.functions import Coalesce, Sum
-from frappe.utils import date_diff, flt, getdate
+from frappe.utils import cint, date_diff, flt, getdate
def execute(filters=None):
@@ -47,8 +47,10 @@
mr.transaction_date.as_("date"),
mr_item.schedule_date.as_("required_date"),
mr_item.item_code.as_("item_code"),
- Sum(Coalesce(mr_item.stock_qty, 0)).as_("qty"),
- Coalesce(mr_item.stock_uom, "").as_("uom"),
+ Sum(Coalesce(mr_item.qty, 0)).as_("qty"),
+ Sum(Coalesce(mr_item.stock_qty, 0)).as_("stock_qty"),
+ Coalesce(mr_item.uom, "").as_("uom"),
+ Coalesce(mr_item.stock_uom, "").as_("stock_uom"),
Sum(Coalesce(mr_item.ordered_qty, 0)).as_("ordered_qty"),
Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"),
(Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.received_qty, 0))).as_(
@@ -96,7 +98,7 @@
def update_qty_columns(row_to_update, data_row):
- fields = ["qty", "ordered_qty", "received_qty", "qty_to_receive", "qty_to_order"]
+ fields = ["qty", "stock_qty", "ordered_qty", "received_qty", "qty_to_receive", "qty_to_order"]
for field in fields:
row_to_update[field] += flt(data_row[field])
@@ -104,16 +106,20 @@
def prepare_data(data, filters):
"""Prepare consolidated Report data and Chart data"""
material_request_map, item_qty_map = {}, {}
+ precision = cint(frappe.db.get_default("float_precision")) or 2
for row in data:
# item wise map for charts
if not row["item_code"] in item_qty_map:
item_qty_map[row["item_code"]] = {
- "qty": row["qty"],
- "ordered_qty": row["ordered_qty"],
- "received_qty": row["received_qty"],
- "qty_to_receive": row["qty_to_receive"],
- "qty_to_order": row["qty_to_order"],
+ "qty": flt(row["stock_qty"], precision),
+ "stock_qty": flt(row["stock_qty"], precision),
+ "stock_uom": row["stock_uom"],
+ "uom": row["uom"],
+ "ordered_qty": flt(row["ordered_qty"], precision),
+ "received_qty": flt(row["received_qty"], precision),
+ "qty_to_receive": flt(row["qty_to_receive"], precision),
+ "qty_to_order": flt(row["qty_to_order"], precision),
}
else:
item_entry = item_qty_map[row["item_code"]]
@@ -200,21 +206,34 @@
{"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 100},
{"label": _("Description"), "fieldname": "description", "fieldtype": "Data", "width": 200},
{
- "label": _("Stock UOM"),
+ "label": _("UOM"),
"fieldname": "uom",
"fieldtype": "Data",
"width": 100,
},
+ {
+ "label": _("Stock UOM"),
+ "fieldname": "stock_uom",
+ "fieldtype": "Data",
+ "width": 100,
+ },
]
)
columns.extend(
[
{
- "label": _("Stock Qty"),
+ "label": _("Qty"),
"fieldname": "qty",
"fieldtype": "Float",
- "width": 120,
+ "width": 140,
+ "convertible": "qty",
+ },
+ {
+ "label": _("Qty in Stock UOM"),
+ "fieldname": "stock_qty",
+ "fieldtype": "Float",
+ "width": 140,
"convertible": "qty",
},
{
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 0ca1e94..e635aa7 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -169,7 +169,6 @@
self.validate_value("base_grand_total", ">=", 0)
validate_return(self)
- self.set_total_in_words()
self.validate_all_documents_schedule()
@@ -208,9 +207,42 @@
if self.doctype != "Material Request" and not self.ignore_pricing_rule:
apply_pricing_rule_on_transaction(self)
+ self.set_total_in_words()
+
def before_cancel(self):
validate_einvoice_fields(self)
+ def _remove_references_in_unreconcile(self):
+ upe = frappe.qb.DocType("Unreconcile Payment Entries")
+ rows = (
+ frappe.qb.from_(upe)
+ .select(upe.name, upe.parent)
+ .where((upe.reference_doctype == self.doctype) & (upe.reference_name == self.name))
+ .run(as_dict=True)
+ )
+
+ if rows:
+ references_map = frappe._dict()
+ for x in rows:
+ references_map.setdefault(x.parent, []).append(x.name)
+
+ for doc, rows in references_map.items():
+ unreconcile_doc = frappe.get_doc("Unreconcile Payments", doc)
+ for row in rows:
+ unreconcile_doc.remove(unreconcile_doc.get("allocations", {"name": row})[0])
+
+ unreconcile_doc.flags.ignore_validate_update_after_submit = True
+ unreconcile_doc.flags.ignore_links = True
+ unreconcile_doc.save(ignore_permissions=True)
+
+ # delete docs upon parent doc deletion
+ unreconcile_docs = frappe.db.get_all("Unreconcile Payments", filters={"voucher_no": self.name})
+ for x in unreconcile_docs:
+ _doc = frappe.get_doc("Unreconcile Payments", x.name)
+ if _doc.docstatus == 1:
+ _doc.cancel()
+ _doc.delete()
+
def on_trash(self):
# delete references in 'Repost Payment Ledger'
rpi = frappe.qb.DocType("Repost Payment Ledger Items")
@@ -218,6 +250,8 @@
(rpi.voucher_type == self.doctype) & (rpi.voucher_no == self.name)
).run()
+ self._remove_references_in_unreconcile()
+
# delete sl and gl entries on deletion of transaction
if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"):
ple = frappe.qb.DocType("Payment Ledger Entry")
@@ -935,7 +969,7 @@
party_type, party, party_account, amount_field, order_doctype, order_list, include_unallocated
)
- payment_entries = get_advance_payment_entries(
+ payment_entries = get_advance_payment_entries_for_regional(
party_type, party, party_account, order_doctype, order_list, include_unallocated
)
@@ -2376,6 +2410,11 @@
return list(journal_entries)
+@erpnext.allow_regional
+def get_advance_payment_entries_for_regional(*args, **kwargs):
+ return get_advance_payment_entries(*args, **kwargs)
+
+
def get_advance_payment_entries(
party_type,
party,
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index b1ce539..a76abe2 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -190,10 +190,13 @@
purchase_doc_field = (
"purchase_receipt" if self.doctype == "Purchase Receipt" else "purchase_invoice"
)
- not_cancelled_asset = [
- d.name
- for d in frappe.db.get_all("Asset", {purchase_doc_field: self.return_against, "docstatus": 1})
- ]
+ not_cancelled_asset = []
+ if self.return_against:
+ not_cancelled_asset = [
+ d.name
+ for d in frappe.db.get_all("Asset", {purchase_doc_field: self.return_against, "docstatus": 1})
+ ]
+
if self.is_return and len(not_cancelled_asset):
frappe.throw(
_(
@@ -415,6 +418,10 @@
item.bom = None
def set_qty_as_per_stock_uom(self):
+ allow_to_edit_stock_qty = frappe.db.get_single_value(
+ "Stock Settings", "allow_to_edit_stock_uom_qty_for_purchase"
+ )
+
for d in self.get("items"):
if d.meta.get_field("stock_qty"):
# Check if item code is present
@@ -429,6 +436,11 @@
d.conversion_factor, d.precision("conversion_factor")
)
+ if allow_to_edit_stock_qty:
+ d.stock_qty = flt(d.stock_qty, d.precision("stock_qty"))
+ if d.get("received_stock_qty"):
+ d.received_stock_qty = flt(d.received_stock_qty, d.precision("received_stock_qty"))
+
def validate_purchase_return(self):
for d in self.get("items"):
if self.is_return and flt(d.rejected_qty) != 0:
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 9771f60..9014662 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -194,11 +194,17 @@
frappe.throw(_("Maximum discount for Item {0} is {1}%").format(d.item_code, discount))
def set_qty_as_per_stock_uom(self):
+ allow_to_edit_stock_qty = frappe.db.get_single_value(
+ "Stock Settings", "allow_to_edit_stock_uom_qty_for_sales"
+ )
+
for d in self.get("items"):
if d.meta.get_field("stock_qty"):
if not d.conversion_factor:
frappe.throw(_("Row {0}: Conversion Factor is mandatory").format(d.idx))
d.stock_qty = flt(d.qty) * flt(d.conversion_factor)
+ if allow_to_edit_stock_qty:
+ d.stock_qty = flt(d.stock_qty, d.precision("stock_qty"))
def validate_selling_price(self):
def throw_message(idx, item_name, rate, ref_rate_field):
diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js
deleted file mode 100644
index 7d6b395..0000000
--- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js
+++ /dev/null
@@ -1,74 +0,0 @@
-// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('LinkedIn Settings', {
- onload: function(frm) {
- if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret) {
- frappe.confirm(
- __('Session not valid. Do you want to login?'),
- function(){
- frm.trigger("login");
- },
- function(){
- window.close();
- }
- );
- }
- frm.dashboard.set_headline(__("For more information, {0}.", [`<a target='_blank' href='https://docs.erpnext.com/docs/user/manual/en/CRM/linkedin-settings'>${__('click here')}</a>`]));
- },
- refresh: function(frm) {
- if (frm.doc.session_status=="Expired"){
- let msg = __("Session not active. Save document to login.");
- frm.dashboard.set_headline_alert(
- `<div class="row">
- <div class="col-xs-12">
- <span class="indicator whitespace-nowrap red"><span class="hidden-xs">${msg}</span></span>
- </div>
- </div>`
- );
- }
-
- if (frm.doc.session_status=="Active"){
- let d = new Date(frm.doc.modified);
- d.setDate(d.getDate()+60);
- let dn = new Date();
- let days = d.getTime() - dn.getTime();
- days = Math.floor(days/(1000 * 3600 * 24));
- let msg,color;
-
- if (days>0){
- msg = __("Your session will be expire in {0} days.", [days]);
- color = "green";
- }
- else {
- msg = __("Session is expired. Save doc to login.");
- color = "red";
- }
-
- frm.dashboard.set_headline_alert(
- `<div class="row">
- <div class="col-xs-12">
- <span class="indicator whitespace-nowrap ${color}"><span class="hidden-xs">${msg}</span></span>
- </div>
- </div>`
- );
- }
- },
- login: function(frm) {
- if (frm.doc.consumer_key && frm.doc.consumer_secret){
- frappe.dom.freeze();
- frappe.call({
- doc: frm.doc,
- method: "get_authorization_url",
- callback : function(r) {
- window.location.href = r.message;
- }
- }).fail(function() {
- frappe.dom.unfreeze();
- });
- }
- },
- after_save: function(frm) {
- frm.trigger("login");
- }
-});
diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json
deleted file mode 100644
index f882e36..0000000
--- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json
+++ /dev/null
@@ -1,112 +0,0 @@
-{
- "actions": [],
- "creation": "2020-01-30 13:36:39.492931",
- "doctype": "DocType",
- "documentation": "https://docs.erpnext.com/docs/user/manual/en/CRM/linkedin-settings",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "account_name",
- "column_break_2",
- "company_id",
- "oauth_details",
- "consumer_key",
- "column_break_5",
- "consumer_secret",
- "user_details_section",
- "access_token",
- "person_urn",
- "session_status"
- ],
- "fields": [
- {
- "fieldname": "account_name",
- "fieldtype": "Data",
- "label": "Account Name",
- "read_only": 1
- },
- {
- "fieldname": "oauth_details",
- "fieldtype": "Section Break",
- "label": "OAuth Credentials"
- },
- {
- "fieldname": "consumer_key",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Consumer Key",
- "reqd": 1
- },
- {
- "fieldname": "consumer_secret",
- "fieldtype": "Password",
- "in_list_view": 1,
- "label": "Consumer Secret",
- "reqd": 1
- },
- {
- "fieldname": "access_token",
- "fieldtype": "Data",
- "hidden": 1,
- "label": "Access Token",
- "read_only": 1
- },
- {
- "fieldname": "person_urn",
- "fieldtype": "Data",
- "hidden": 1,
- "label": "Person URN",
- "read_only": 1
- },
- {
- "fieldname": "column_break_5",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "user_details_section",
- "fieldtype": "Section Break",
- "label": "User Details"
- },
- {
- "fieldname": "session_status",
- "fieldtype": "Select",
- "hidden": 1,
- "label": "Session Status",
- "options": "Expired\nActive",
- "read_only": 1
- },
- {
- "fieldname": "column_break_2",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "company_id",
- "fieldtype": "Data",
- "label": "Company ID",
- "reqd": 1
- }
- ],
- "issingle": 1,
- "links": [],
- "modified": "2021-02-18 15:19:21.920725",
- "modified_by": "Administrator",
- "module": "CRM",
- "name": "LinkedIn Settings",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py
deleted file mode 100644
index 64b3a01..0000000
--- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py
+++ /dev/null
@@ -1,208 +0,0 @@
-# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from urllib.parse import urlencode
-
-import frappe
-import requests
-from frappe import _
-from frappe.model.document import Document
-from frappe.utils import get_url_to_form
-from frappe.utils.file_manager import get_file_path
-
-
-class LinkedInSettings(Document):
- @frappe.whitelist()
- def get_authorization_url(self):
- params = urlencode(
- {
- "response_type": "code",
- "client_id": self.consumer_key,
- "redirect_uri": "{0}/api/method/erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback?".format(
- frappe.utils.get_url()
- ),
- "scope": "r_emailaddress w_organization_social r_basicprofile r_liteprofile r_organization_social rw_organization_admin w_member_social",
- }
- )
-
- url = "https://www.linkedin.com/oauth/v2/authorization?{}".format(params)
-
- return url
-
- def get_access_token(self, code):
- url = "https://www.linkedin.com/oauth/v2/accessToken"
- body = {
- "grant_type": "authorization_code",
- "code": code,
- "client_id": self.consumer_key,
- "client_secret": self.get_password(fieldname="consumer_secret"),
- "redirect_uri": "{0}/api/method/erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback?".format(
- frappe.utils.get_url()
- ),
- }
- headers = {"Content-Type": "application/x-www-form-urlencoded"}
-
- response = self.http_post(url=url, data=body, headers=headers)
- response = frappe.parse_json(response.content.decode())
- self.db_set("access_token", response["access_token"])
-
- def get_member_profile(self):
- response = requests.get(url="https://api.linkedin.com/v2/me", headers=self.get_headers())
- response = frappe.parse_json(response.content.decode())
-
- frappe.db.set_value(
- self.doctype,
- self.name,
- {
- "person_urn": response["id"],
- "account_name": response["vanityName"],
- "session_status": "Active",
- },
- )
- frappe.local.response["type"] = "redirect"
- frappe.local.response["location"] = get_url_to_form("LinkedIn Settings", "LinkedIn Settings")
-
- def post(self, text, title, media=None):
- if not media:
- return self.post_text(text, title)
- else:
- media_id = self.upload_image(media)
-
- if media_id:
- return self.post_text(text, title, media_id=media_id)
- else:
- self.log_error("LinkedIn: Failed to upload media")
-
- def upload_image(self, media):
- media = get_file_path(media)
- register_url = "https://api.linkedin.com/v2/assets?action=registerUpload"
- body = {
- "registerUploadRequest": {
- "recipes": ["urn:li:digitalmediaRecipe:feedshare-image"],
- "owner": "urn:li:organization:{0}".format(self.company_id),
- "serviceRelationships": [
- {"relationshipType": "OWNER", "identifier": "urn:li:userGeneratedContent"}
- ],
- }
- }
- headers = self.get_headers()
- response = self.http_post(url=register_url, body=body, headers=headers)
-
- if response.status_code == 200:
- response = response.json()
- asset = response["value"]["asset"]
- upload_url = response["value"]["uploadMechanism"][
- "com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest"
- ]["uploadUrl"]
- headers["Content-Type"] = "image/jpeg"
- response = self.http_post(upload_url, headers=headers, data=open(media, "rb"))
- if response.status_code < 200 and response.status_code > 299:
- frappe.throw(
- _("Error While Uploading Image"),
- title="{0} {1}".format(response.status_code, response.reason),
- )
- return None
- return asset
-
- return None
-
- def post_text(self, text, title, media_id=None):
- url = "https://api.linkedin.com/v2/shares"
- headers = self.get_headers()
- headers["X-Restli-Protocol-Version"] = "2.0.0"
- headers["Content-Type"] = "application/json; charset=UTF-8"
-
- body = {
- "distribution": {"linkedInDistributionTarget": {}},
- "owner": "urn:li:organization:{0}".format(self.company_id),
- "subject": title,
- "text": {"text": text},
- }
-
- reference_url = self.get_reference_url(text)
- if reference_url:
- body["content"] = {"contentEntities": [{"entityLocation": reference_url}]}
-
- if media_id:
- body["content"] = {"contentEntities": [{"entity": media_id}], "shareMediaCategory": "IMAGE"}
-
- response = self.http_post(url=url, headers=headers, body=body)
- return response
-
- def http_post(self, url, headers=None, body=None, data=None):
- try:
- response = requests.post(url=url, json=body, data=data, headers=headers)
- if response.status_code not in [201, 200]:
- raise
-
- except Exception as e:
- self.api_error(response)
-
- return response
-
- def get_headers(self):
- return {"Authorization": "Bearer {}".format(self.access_token)}
-
- def get_reference_url(self, text):
- import re
-
- regex_url = r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+"
- urls = re.findall(regex_url, text)
- if urls:
- return urls[0]
-
- def delete_post(self, post_id):
- try:
- response = requests.delete(
- url="https://api.linkedin.com/v2/shares/urn:li:share:{0}".format(post_id),
- headers=self.get_headers(),
- )
- if response.status_code != 200:
- raise
- except Exception:
- self.api_error(response)
-
- def get_post(self, post_id):
- url = "https://api.linkedin.com/v2/organizationalEntityShareStatistics?q=organizationalEntity&organizationalEntity=urn:li:organization:{0}&shares[0]=urn:li:share:{1}".format(
- self.company_id, post_id
- )
-
- try:
- response = requests.get(url=url, headers=self.get_headers())
- if response.status_code != 200:
- raise
-
- except Exception:
- self.api_error(response)
-
- response = frappe.parse_json(response.content.decode())
- if len(response.elements):
- return response.elements[0]
-
- return None
-
- def api_error(self, response):
- content = frappe.parse_json(response.content.decode())
-
- if response.status_code == 401:
- self.db_set("session_status", "Expired")
- frappe.db.commit()
- frappe.throw(content["message"], title=_("LinkedIn Error - Unauthorized"))
- elif response.status_code == 403:
- frappe.msgprint(_("You didn't have permission to access this API"))
- frappe.throw(content["message"], title=_("LinkedIn Error - Access Denied"))
- else:
- frappe.throw(response.reason, title=response.status_code)
-
-
-@frappe.whitelist(allow_guest=True)
-def callback(code=None, error=None, error_description=None):
- if not error:
- linkedin_settings = frappe.get_doc("LinkedIn Settings")
- linkedin_settings.get_access_token(code)
- linkedin_settings.get_member_profile()
- frappe.db.commit()
- else:
- frappe.local.response["type"] = "redirect"
- frappe.local.response["location"] = get_url_to_form("LinkedIn Settings", "LinkedIn Settings")
diff --git a/erpnext/crm/doctype/linkedin_settings/test_linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/test_linkedin_settings.py
deleted file mode 100644
index 09732e4..0000000
--- a/erpnext/crm/doctype/linkedin_settings/test_linkedin_settings.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-# import frappe
-import unittest
-
-
-class TestLinkedInSettings(unittest.TestCase):
- pass
diff --git a/erpnext/crm/doctype/social_media_post/__init__.py b/erpnext/crm/doctype/social_media_post/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/crm/doctype/social_media_post/__init__.py
+++ /dev/null
diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.js b/erpnext/crm/doctype/social_media_post/social_media_post.js
deleted file mode 100644
index d4ac0ba..0000000
--- a/erpnext/crm/doctype/social_media_post/social_media_post.js
+++ /dev/null
@@ -1,139 +0,0 @@
-// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-frappe.ui.form.on('Social Media Post', {
- validate: function(frm) {
- if (frm.doc.twitter === 0 && frm.doc.linkedin === 0) {
- frappe.throw(__("Select atleast one Social Media Platform to Share on."));
- }
- if (frm.doc.scheduled_time) {
- let scheduled_time = new Date(frm.doc.scheduled_time);
- let date_time = new Date();
- if (scheduled_time.getTime() < date_time.getTime()) {
- frappe.throw(__("Scheduled Time must be a future time."));
- }
- }
- frm.trigger('validate_tweet_length');
- },
-
- text: function(frm) {
- if (frm.doc.text) {
- frm.set_df_property('text', 'description', `${frm.doc.text.length}/280`);
- frm.refresh_field('text');
- frm.trigger('validate_tweet_length');
- }
- },
-
- validate_tweet_length: function(frm) {
- if (frm.doc.text && frm.doc.text.length > 280) {
- frappe.throw(__("Tweet length Must be less than 280."));
- }
- },
-
- onload: function(frm) {
- frm.trigger('make_dashboard');
- },
-
- make_dashboard: function(frm) {
- if (frm.doc.post_status == "Posted") {
- frappe.call({
- doc: frm.doc,
- method: 'get_post',
- freeze: true,
- callback: (r) => {
- if (!r.message) {
- return;
- }
-
- let datasets = [], colors = [];
- if (r.message && r.message.twitter) {
- colors.push('#1DA1F2');
- datasets.push({
- name: 'Twitter',
- values: [r.message.twitter.favorite_count, r.message.twitter.retweet_count]
- });
- }
- if (r.message && r.message.linkedin) {
- colors.push('#0077b5');
- datasets.push({
- name: 'LinkedIn',
- values: [r.message.linkedin.totalShareStatistics.likeCount, r.message.linkedin.totalShareStatistics.shareCount]
- });
- }
-
- if (datasets.length) {
- frm.dashboard.render_graph({
- data: {
- labels: ['Likes', 'Retweets/Shares'],
- datasets: datasets
- },
-
- title: __("Post Metrics"),
- type: 'bar',
- height: 300,
- colors: colors
- });
- }
- }
- });
- }
- },
-
- refresh: function(frm) {
- frm.trigger('text');
-
- if (frm.doc.docstatus === 1) {
- if (!['Posted', 'Deleted'].includes(frm.doc.post_status)) {
- frm.trigger('add_post_btn');
- }
- if (frm.doc.post_status !='Deleted') {
- frm.add_custom_button(__('Delete Post'), function() {
- frappe.confirm(__('Are you sure want to delete the Post from Social Media platforms?'),
- function() {
- frappe.call({
- doc: frm.doc,
- method: 'delete_post',
- freeze: true,
- callback: () => {
- frm.reload_doc();
- }
- });
- }
- );
- });
- }
-
- if (frm.doc.post_status !='Deleted') {
- let html='';
- if (frm.doc.twitter) {
- let color = frm.doc.twitter_post_id ? "green" : "red";
- let status = frm.doc.twitter_post_id ? "Posted" : "Not Posted";
- html += `<div class="col-xs-6">
- <span class="indicator whitespace-nowrap ${color}"><span>Twitter : ${status} </span></span>
- </div>` ;
- }
- if (frm.doc.linkedin) {
- let color = frm.doc.linkedin_post_id ? "green" : "red";
- let status = frm.doc.linkedin_post_id ? "Posted" : "Not Posted";
- html += `<div class="col-xs-6">
- <span class="indicator whitespace-nowrap ${color}"><span>LinkedIn : ${status} </span></span>
- </div>` ;
- }
- html = `<div class="row">${html}</div>`;
- frm.dashboard.set_headline_alert(html);
- }
- }
- },
-
- add_post_btn: function(frm) {
- frm.add_custom_button(__('Post Now'), function() {
- frappe.call({
- doc: frm.doc,
- method: 'post',
- freeze: true,
- callback: function() {
- frm.reload_doc();
- }
- });
- });
- }
-});
diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.json b/erpnext/crm/doctype/social_media_post/social_media_post.json
deleted file mode 100644
index 98e78f9..0000000
--- a/erpnext/crm/doctype/social_media_post/social_media_post.json
+++ /dev/null
@@ -1,208 +0,0 @@
-{
- "actions": [],
- "autoname": "format: CRM-SMP-{YYYY}-{MM}-{DD}-{###}",
- "creation": "2020-01-30 11:53:13.872864",
- "doctype": "DocType",
- "documentation": "https://docs.erpnext.com/docs/user/manual/en/CRM/social-media-post",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "title",
- "campaign_name",
- "scheduled_time",
- "post_status",
- "column_break_6",
- "twitter",
- "linkedin",
- "twitter_post_id",
- "linkedin_post_id",
- "content",
- "text",
- "column_break_14",
- "tweet_preview",
- "linkedin_section",
- "linkedin_post",
- "column_break_15",
- "attachments_section",
- "image",
- "amended_from"
- ],
- "fields": [
- {
- "fieldname": "text",
- "fieldtype": "Small Text",
- "label": "Tweet",
- "mandatory_depends_on": "eval:doc.twitter ==1"
- },
- {
- "fieldname": "image",
- "fieldtype": "Attach Image",
- "label": "Image"
- },
- {
- "default": "1",
- "fieldname": "twitter",
- "fieldtype": "Check",
- "label": "Twitter"
- },
- {
- "default": "1",
- "fieldname": "linkedin",
- "fieldtype": "Check",
- "label": "LinkedIn"
- },
- {
- "fieldname": "amended_from",
- "fieldtype": "Link",
- "label": "Amended From",
- "no_copy": 1,
- "options": "Social Media Post",
- "print_hide": 1,
- "read_only": 1
- },
- {
- "depends_on": "eval:doc.twitter ==1",
- "fieldname": "content",
- "fieldtype": "Section Break",
- "label": "Twitter"
- },
- {
- "allow_on_submit": 1,
- "fieldname": "post_status",
- "fieldtype": "Select",
- "label": "Post Status",
- "no_copy": 1,
- "options": "\nScheduled\nPosted\nCancelled\nDeleted\nError",
- "read_only": 1
- },
- {
- "allow_on_submit": 1,
- "fieldname": "twitter_post_id",
- "fieldtype": "Data",
- "hidden": 1,
- "label": "Twitter Post Id",
- "no_copy": 1,
- "read_only": 1
- },
- {
- "allow_on_submit": 1,
- "fieldname": "linkedin_post_id",
- "fieldtype": "Data",
- "hidden": 1,
- "label": "LinkedIn Post Id",
- "no_copy": 1,
- "read_only": 1
- },
- {
- "fieldname": "campaign_name",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Campaign",
- "options": "Campaign"
- },
- {
- "fieldname": "column_break_6",
- "fieldtype": "Column Break",
- "label": "Share On"
- },
- {
- "fieldname": "column_break_14",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "tweet_preview",
- "fieldtype": "HTML"
- },
- {
- "collapsible": 1,
- "depends_on": "eval:doc.linkedin==1",
- "fieldname": "linkedin_section",
- "fieldtype": "Section Break",
- "label": "LinkedIn"
- },
- {
- "collapsible": 1,
- "fieldname": "attachments_section",
- "fieldtype": "Section Break",
- "label": "Attachments"
- },
- {
- "fieldname": "linkedin_post",
- "fieldtype": "Text",
- "label": "Post",
- "mandatory_depends_on": "eval:doc.linkedin ==1"
- },
- {
- "fieldname": "column_break_15",
- "fieldtype": "Column Break"
- },
- {
- "allow_on_submit": 1,
- "fieldname": "scheduled_time",
- "fieldtype": "Datetime",
- "label": "Scheduled Time",
- "read_only_depends_on": "eval:doc.post_status == \"Posted\""
- },
- {
- "fieldname": "title",
- "fieldtype": "Data",
- "label": "Title",
- "reqd": 1
- }
- ],
- "is_submittable": 1,
- "links": [],
- "modified": "2021-04-14 14:24:59.821223",
- "modified_by": "Administrator",
- "module": "CRM",
- "name": "Social Media Post",
- "owner": "Administrator",
- "permissions": [
- {
- "cancel": 1,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "share": 1,
- "submit": 1,
- "write": 1
- },
- {
- "cancel": 1,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Sales User",
- "share": 1,
- "submit": 1,
- "write": 1
- },
- {
- "cancel": 1,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Sales Manager",
- "share": 1,
- "submit": 1,
- "write": 1
- }
- ],
- "sort_field": "modified",
- "sort_order": "DESC",
- "title_field": "title",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.py b/erpnext/crm/doctype/social_media_post/social_media_post.py
deleted file mode 100644
index 3654d29..0000000
--- a/erpnext/crm/doctype/social_media_post/social_media_post.py
+++ /dev/null
@@ -1,89 +0,0 @@
-# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-import datetime
-
-import frappe
-from frappe import _
-from frappe.model.document import Document
-
-
-class SocialMediaPost(Document):
- def validate(self):
- if not self.twitter and not self.linkedin:
- frappe.throw(_("Select atleast one Social Media Platform to Share on."))
-
- if self.scheduled_time:
- current_time = frappe.utils.now_datetime()
- scheduled_time = frappe.utils.get_datetime(self.scheduled_time)
- if scheduled_time < current_time:
- frappe.throw(_("Scheduled Time must be a future time."))
-
- if self.text and len(self.text) > 280:
- frappe.throw(_("Tweet length must be less than 280."))
-
- def submit(self):
- if self.scheduled_time:
- self.post_status = "Scheduled"
- super(SocialMediaPost, self).submit()
-
- def on_cancel(self):
- self.db_set("post_status", "Cancelled")
-
- @frappe.whitelist()
- def delete_post(self):
- if self.twitter and self.twitter_post_id:
- twitter = frappe.get_doc("Twitter Settings")
- twitter.delete_tweet(self.twitter_post_id)
-
- if self.linkedin and self.linkedin_post_id:
- linkedin = frappe.get_doc("LinkedIn Settings")
- linkedin.delete_post(self.linkedin_post_id)
-
- self.db_set("post_status", "Deleted")
-
- @frappe.whitelist()
- def get_post(self):
- response = {}
- if self.linkedin and self.linkedin_post_id:
- linkedin = frappe.get_doc("LinkedIn Settings")
- response["linkedin"] = linkedin.get_post(self.linkedin_post_id)
- if self.twitter and self.twitter_post_id:
- twitter = frappe.get_doc("Twitter Settings")
- response["twitter"] = twitter.get_tweet(self.twitter_post_id)
-
- return response
-
- @frappe.whitelist()
- def post(self):
- try:
- if self.twitter and not self.twitter_post_id:
- twitter = frappe.get_doc("Twitter Settings")
- twitter_post = twitter.post(self.text, self.image)
- self.db_set("twitter_post_id", twitter_post.id)
- if self.linkedin and not self.linkedin_post_id:
- linkedin = frappe.get_doc("LinkedIn Settings")
- linkedin_post = linkedin.post(self.linkedin_post, self.title, self.image)
- self.db_set("linkedin_post_id", linkedin_post.headers["X-RestLi-Id"])
- self.db_set("post_status", "Posted")
-
- except Exception:
- self.db_set("post_status", "Error")
- self.log_error("Social posting failed")
-
-
-def process_scheduled_social_media_posts():
- posts = frappe.get_all(
- "Social Media Post",
- filters={"post_status": "Scheduled", "docstatus": 1},
- fields=["name", "scheduled_time"],
- )
- start = frappe.utils.now_datetime()
- end = start + datetime.timedelta(minutes=10)
- for post in posts:
- if post.scheduled_time:
- post_time = frappe.utils.get_datetime(post.scheduled_time)
- if post_time > start and post_time <= end:
- sm_post = frappe.get_doc("Social Media Post", post.name)
- sm_post.post()
diff --git a/erpnext/crm/doctype/social_media_post/social_media_post_list.js b/erpnext/crm/doctype/social_media_post/social_media_post_list.js
deleted file mode 100644
index a8c8272..0000000
--- a/erpnext/crm/doctype/social_media_post/social_media_post_list.js
+++ /dev/null
@@ -1,11 +0,0 @@
-frappe.listview_settings['Social Media Post'] = {
- add_fields: ["status", "post_status"],
- get_indicator: function(doc) {
- return [__(doc.post_status), {
- "Scheduled": "orange",
- "Posted": "green",
- "Error": "red",
- "Deleted": "red"
- }[doc.post_status]];
- }
-}
diff --git a/erpnext/crm/doctype/social_media_post/test_social_media_post.py b/erpnext/crm/doctype/social_media_post/test_social_media_post.py
deleted file mode 100644
index 7574476..0000000
--- a/erpnext/crm/doctype/social_media_post/test_social_media_post.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-# import frappe
-import unittest
-
-
-class TestSocialMediaPost(unittest.TestCase):
- pass
diff --git a/erpnext/crm/doctype/twitter_settings/__init__.py b/erpnext/crm/doctype/twitter_settings/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/crm/doctype/twitter_settings/__init__.py
+++ /dev/null
diff --git a/erpnext/crm/doctype/twitter_settings/test_twitter_settings.py b/erpnext/crm/doctype/twitter_settings/test_twitter_settings.py
deleted file mode 100644
index 9dbce8f..0000000
--- a/erpnext/crm/doctype/twitter_settings/test_twitter_settings.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-# import frappe
-import unittest
-
-
-class TestTwitterSettings(unittest.TestCase):
- pass
diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.js b/erpnext/crm/doctype/twitter_settings/twitter_settings.js
deleted file mode 100644
index c322092..0000000
--- a/erpnext/crm/doctype/twitter_settings/twitter_settings.js
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Twitter Settings', {
- onload: function(frm) {
- if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){
- frappe.confirm(
- __('Session not valid, Do you want to login?'),
- function(){
- frm.trigger("login");
- },
- function(){
- window.close();
- }
- );
- }
- frm.dashboard.set_headline(__("For more information, {0}.", [`<a target='_blank' href='https://docs.erpnext.com/docs/user/manual/en/CRM/twitter-settings'>${__('click here')}</a>`]));
- },
- refresh: function(frm) {
- let msg, color, flag=false;
- if (frm.doc.session_status == "Active") {
- msg = __("Session Active");
- color = 'green';
- flag = true;
- }
- else if(frm.doc.consumer_key && frm.doc.consumer_secret) {
- msg = __("Session Not Active. Save doc to login.");
- color = 'red';
- flag = true;
- }
-
- if (flag) {
- frm.dashboard.set_headline_alert(
- `<div class="row">
- <div class="col-xs-12">
- <span class="indicator whitespace-nowrap ${color}"><span class="hidden-xs">${msg}</span></span>
- </div>
- </div>`
- );
- }
- },
- login: function(frm) {
- if (frm.doc.consumer_key && frm.doc.consumer_secret){
- frappe.dom.freeze();
- frappe.call({
- doc: frm.doc,
- method: "get_authorize_url",
- callback : function(r) {
- window.location.href = r.message;
- }
- }).fail(function() {
- frappe.dom.unfreeze();
- });
- }
- },
- after_save: function(frm) {
- frm.trigger("login");
- }
-});
diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.json b/erpnext/crm/doctype/twitter_settings/twitter_settings.json
deleted file mode 100644
index 8d05877..0000000
--- a/erpnext/crm/doctype/twitter_settings/twitter_settings.json
+++ /dev/null
@@ -1,102 +0,0 @@
-{
- "actions": [],
- "creation": "2020-01-30 10:29:08.562108",
- "doctype": "DocType",
- "documentation": "https://docs.erpnext.com/docs/user/manual/en/CRM/twitter-settings",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "account_name",
- "profile_pic",
- "oauth_details",
- "consumer_key",
- "column_break_5",
- "consumer_secret",
- "access_token",
- "access_token_secret",
- "session_status"
- ],
- "fields": [
- {
- "fieldname": "account_name",
- "fieldtype": "Data",
- "label": "Account Name",
- "read_only": 1
- },
- {
- "fieldname": "oauth_details",
- "fieldtype": "Section Break",
- "label": "OAuth Credentials"
- },
- {
- "fieldname": "consumer_key",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "API Key",
- "reqd": 1
- },
- {
- "fieldname": "consumer_secret",
- "fieldtype": "Password",
- "in_list_view": 1,
- "label": "API Secret Key",
- "reqd": 1
- },
- {
- "fieldname": "column_break_5",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "profile_pic",
- "fieldtype": "Attach Image",
- "hidden": 1,
- "read_only": 1
- },
- {
- "fieldname": "session_status",
- "fieldtype": "Select",
- "hidden": 1,
- "label": "Session Status",
- "options": "Expired\nActive",
- "read_only": 1
- },
- {
- "fieldname": "access_token",
- "fieldtype": "Data",
- "hidden": 1,
- "label": "Access Token",
- "read_only": 1
- },
- {
- "fieldname": "access_token_secret",
- "fieldtype": "Data",
- "hidden": 1,
- "label": "Access Token Secret",
- "read_only": 1
- }
- ],
- "image_field": "profile_pic",
- "issingle": 1,
- "links": [],
- "modified": "2021-02-18 15:18:07.900031",
- "modified_by": "Administrator",
- "module": "CRM",
- "name": "Twitter Settings",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.py b/erpnext/crm/doctype/twitter_settings/twitter_settings.py
deleted file mode 100644
index 442aa77..0000000
--- a/erpnext/crm/doctype/twitter_settings/twitter_settings.py
+++ /dev/null
@@ -1,141 +0,0 @@
-# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-import json
-
-import frappe
-import tweepy
-from frappe import _
-from frappe.model.document import Document
-from frappe.utils import get_url_to_form
-from frappe.utils.file_manager import get_file_path
-
-
-class TwitterSettings(Document):
- @frappe.whitelist()
- def get_authorize_url(self):
- callback_url = (
- "{0}/api/method/erpnext.crm.doctype.twitter_settings.twitter_settings.callback?".format(
- frappe.utils.get_url()
- )
- )
- auth = tweepy.OAuth1UserHandler(
- self.consumer_key, self.get_password(fieldname="consumer_secret"), callback_url
- )
- try:
- redirect_url = auth.get_authorization_url()
- return redirect_url
- except (tweepy.TweepyException, tweepy.HTTPException) as e:
- frappe.msgprint(_("Error! Failed to get request token."))
- frappe.throw(
- _("Invalid {0} or {1}").format(frappe.bold("Consumer Key"), frappe.bold("Consumer Secret Key"))
- )
-
- def get_access_token(self, oauth_token, oauth_verifier):
- auth = tweepy.OAuth1UserHandler(
- self.consumer_key, self.get_password(fieldname="consumer_secret")
- )
- auth.request_token = {"oauth_token": oauth_token, "oauth_token_secret": oauth_verifier}
-
- try:
- auth.get_access_token(oauth_verifier)
- self.access_token = auth.access_token
- self.access_token_secret = auth.access_token_secret
- api = self.get_api()
- user = api.me()
- profile_pic = (user._json["profile_image_url"]).replace("_normal", "")
-
- frappe.db.set_value(
- self.doctype,
- self.name,
- {
- "access_token": auth.access_token,
- "access_token_secret": auth.access_token_secret,
- "account_name": user._json["screen_name"],
- "profile_pic": profile_pic,
- "session_status": "Active",
- },
- )
-
- frappe.local.response["type"] = "redirect"
- frappe.local.response["location"] = get_url_to_form("Twitter Settings", "Twitter Settings")
- except (tweepy.TweepyException, tweepy.HTTPException) as e:
- frappe.msgprint(_("Error! Failed to get access token."))
- frappe.throw(_("Invalid Consumer Key or Consumer Secret Key"))
-
- def get_api(self):
- # authentication of consumer key and secret
- auth = tweepy.OAuth1UserHandler(
- self.consumer_key, self.get_password(fieldname="consumer_secret")
- )
- # authentication of access token and secret
- auth.set_access_token(self.access_token, self.access_token_secret)
-
- return tweepy.API(auth)
-
- def post(self, text, media=None):
- if not media:
- return self.send_tweet(text)
-
- if media:
- media_id = self.upload_image(media)
- return self.send_tweet(text, media_id)
-
- def upload_image(self, media):
- media = get_file_path(media)
- api = self.get_api()
- media = api.media_upload(media)
-
- return media.media_id
-
- def send_tweet(self, text, media_id=None):
- api = self.get_api()
- try:
- if media_id:
- response = api.update_status(status=text, media_ids=[media_id])
- else:
- response = api.update_status(status=text)
-
- return response
-
- except (tweepy.TweepyException, tweepy.HTTPException) as e:
- self.api_error(e)
-
- def delete_tweet(self, tweet_id):
- api = self.get_api()
- try:
- api.destroy_status(tweet_id)
- except (tweepy.TweepyException, tweepy.HTTPException) as e:
- self.api_error(e)
-
- def get_tweet(self, tweet_id):
- api = self.get_api()
- try:
- response = api.get_status(tweet_id, trim_user=True, include_entities=True)
- except (tweepy.TweepyException, tweepy.HTTPException) as e:
- self.api_error(e)
-
- return response._json
-
- def api_error(self, e):
- content = json.loads(e.response.content)
- content = content["errors"][0]
- if e.response.status_code == 401:
- self.db_set("session_status", "Expired")
- frappe.db.commit()
- frappe.throw(
- content["message"],
- title=_("Twitter Error {0} : {1}").format(e.response.status_code, e.response.reason),
- )
-
-
-@frappe.whitelist(allow_guest=True)
-def callback(oauth_token=None, oauth_verifier=None):
- if oauth_token and oauth_verifier:
- twitter_settings = frappe.get_single("Twitter Settings")
- twitter_settings.get_access_token(oauth_token, oauth_verifier)
- frappe.db.commit()
- else:
- frappe.local.response["type"] = "redirect"
- frappe.local.response["location"] = get_url_to_form("Twitter Settings", "Twitter Settings")
diff --git a/erpnext/crm/workspace/crm/crm.json b/erpnext/crm/workspace/crm/crm.json
index b107df7..4b5b9af 100644
--- a/erpnext/crm/workspace/crm/crm.json
+++ b/erpnext/crm/workspace/crm/crm.json
@@ -125,131 +125,6 @@
{
"hidden": 0,
"is_query_report": 0,
- "label": "Campaign",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Campaign",
- "link_count": 0,
- "link_to": "Campaign",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Email Campaign",
- "link_count": 0,
- "link_to": "Email Campaign",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Social Media Post",
- "link_count": 0,
- "link_to": "Social Media Post",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "SMS Center",
- "link_count": 0,
- "link_to": "SMS Center",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "SMS Log",
- "link_count": 0,
- "link_to": "SMS Log",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Email Group",
- "link_count": 0,
- "link_to": "Email Group",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Settings",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "CRM Settings",
- "link_count": 0,
- "link_to": "CRM Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "SMS Settings",
- "link_count": 0,
- "link_to": "SMS Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Twitter Settings",
- "link_count": 0,
- "link_to": "Twitter Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "LinkedIn Settings",
- "link_count": 0,
- "link_to": "LinkedIn Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
"label": "Maintenance",
"link_count": 0,
"onboard": 0,
@@ -450,9 +325,101 @@
"link_type": "DocType",
"onboard": 0,
"type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Settings",
+ "link_count": 2,
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "CRM Settings",
+ "link_count": 0,
+ "link_to": "CRM Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "SMS Settings",
+ "link_count": 0,
+ "link_to": "SMS Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Campaign",
+ "link_count": 5,
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Campaign",
+ "link_count": 0,
+ "link_to": "Campaign",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Email Campaign",
+ "link_count": 0,
+ "link_to": "Email Campaign",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "SMS Center",
+ "link_count": 0,
+ "link_to": "SMS Center",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "SMS Log",
+ "link_count": 0,
+ "link_to": "SMS Log",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Email Group",
+ "link_count": 0,
+ "link_to": "Email Group",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
}
],
- "modified": "2023-05-26 16:49:04.298122",
+ "modified": "2023-09-14 12:11:03.968048",
"modified_by": "Administrator",
"module": "CRM",
"name": "CRM",
@@ -463,7 +430,7 @@
"quick_lists": [],
"restrict_to_domain": "",
"roles": [],
- "sequence_id": 10.0,
+ "sequence_id": 17.0,
"shortcuts": [
{
"color": "Blue",
diff --git a/erpnext/e_commerce/doctype/website_item/test_website_item.py b/erpnext/e_commerce/doctype/website_item/test_website_item.py
index 43b2f67..2ba84c0 100644
--- a/erpnext/e_commerce/doctype/website_item/test_website_item.py
+++ b/erpnext/e_commerce/doctype/website_item/test_website_item.py
@@ -312,7 +312,7 @@
# check if stock details are fetched and item not in stock with warehouse set
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
self.assertFalse(bool(data.product_info["in_stock"]))
- self.assertEqual(data.product_info["stock_qty"][0][0], 0)
+ self.assertEqual(data.product_info["stock_qty"], 0)
# disable show stock availability
setup_e_commerce_settings({"show_stock_availability": 0})
@@ -355,7 +355,7 @@
# check if stock details are fetched and item is in stock with warehouse set
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
self.assertTrue(bool(data.product_info["in_stock"]))
- self.assertEqual(data.product_info["stock_qty"][0][0], 2)
+ self.assertEqual(data.product_info["stock_qty"], 2)
# unset warehouse
frappe.db.set_value("Website Item", {"item_code": item_code}, "website_warehouse", "")
@@ -364,7 +364,7 @@
# (even though it has stock in some warehouse)
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
self.assertFalse(bool(data.product_info["in_stock"]))
- self.assertFalse(bool(data.product_info["stock_qty"]))
+ self.assertFalse(data.product_info["stock_qty"])
# disable show stock availability
setup_e_commerce_settings({"show_stock_availability": 0})
diff --git a/erpnext/e_commerce/doctype/website_item/website_item.js b/erpnext/e_commerce/doctype/website_item/website_item.js
index 7b7193e..b6595cc 100644
--- a/erpnext/e_commerce/doctype/website_item/website_item.js
+++ b/erpnext/e_commerce/doctype/website_item/website_item.js
@@ -5,12 +5,6 @@
onload: (frm) => {
// should never check Private
frm.fields_dict["website_image"].df.is_private = 0;
-
- frm.set_query("website_warehouse", () => {
- return {
- filters: {"is_group": 0}
- };
- });
},
refresh: (frm) => {
diff --git a/erpnext/e_commerce/doctype/website_item/website_item.json b/erpnext/e_commerce/doctype/website_item/website_item.json
index 6556eab..6f551a0 100644
--- a/erpnext/e_commerce/doctype/website_item/website_item.json
+++ b/erpnext/e_commerce/doctype/website_item/website_item.json
@@ -135,7 +135,7 @@
"fieldtype": "Column Break"
},
{
- "description": "Show Stock availability based on this warehouse.",
+ "description": "Show Stock availability based on this warehouse. If the parent warehouse is selected, then the system will display the consolidated available quantity of all child warehouses.",
"fieldname": "website_warehouse",
"fieldtype": "Link",
"ignore_user_permissions": 1,
@@ -348,7 +348,7 @@
"index_web_pages_for_search": 1,
"links": [],
"make_attachments_public": 1,
- "modified": "2022-09-30 04:01:52.090732",
+ "modified": "2023-09-12 14:19:22.822689",
"modified_by": "Administrator",
"module": "E-commerce",
"name": "Website Item",
diff --git a/erpnext/e_commerce/product_data_engine/query.py b/erpnext/e_commerce/product_data_engine/query.py
index e6a595a..975f876 100644
--- a/erpnext/e_commerce/product_data_engine/query.py
+++ b/erpnext/e_commerce/product_data_engine/query.py
@@ -259,6 +259,10 @@
)
def get_stock_availability(self, item):
+ from erpnext.templates.pages.wishlist import (
+ get_stock_availability as get_stock_availability_from_template,
+ )
+
"""Modify item object and add stock details."""
item.in_stock = False
warehouse = item.get("website_warehouse")
@@ -274,11 +278,7 @@
else:
item.in_stock = True
elif warehouse:
- # stock item and has warehouse
- actual_qty = frappe.db.get_value(
- "Bin", {"item_code": item.item_code, "warehouse": item.get("website_warehouse")}, "actual_qty"
- )
- item.in_stock = bool(flt(actual_qty))
+ item.in_stock = get_stock_availability_from_template(item.item_code, warehouse)
def get_cart_items(self):
customer = get_customer(silent=True)
diff --git a/erpnext/e_commerce/shopping_cart/cart.py b/erpnext/e_commerce/shopping_cart/cart.py
index c66ae1d..7c7e169 100644
--- a/erpnext/e_commerce/shopping_cart/cart.py
+++ b/erpnext/e_commerce/shopping_cart/cart.py
@@ -111,8 +111,8 @@
item_stock = get_web_item_qty_in_stock(item.item_code, "website_warehouse")
if not cint(item_stock.in_stock):
throw(_("{0} Not in Stock").format(item.item_code))
- if item.qty > item_stock.stock_qty[0][0]:
- throw(_("Only {0} in Stock for item {1}").format(item_stock.stock_qty[0][0], item.item_code))
+ if item.qty > item_stock.stock_qty:
+ throw(_("Only {0} in Stock for item {1}").format(item_stock.stock_qty, item.item_code))
sales_order.flags.ignore_permissions = True
sales_order.insert()
@@ -150,6 +150,10 @@
empty_card = True
else:
+ warehouse = frappe.get_cached_value(
+ "Website Item", {"item_code": item_code}, "website_warehouse"
+ )
+
quotation_items = quotation.get("items", {"item_code": item_code})
if not quotation_items:
quotation.append(
@@ -159,11 +163,13 @@
"item_code": item_code,
"qty": qty,
"additional_notes": additional_notes,
+ "warehouse": warehouse,
},
)
else:
quotation_items[0].qty = qty
quotation_items[0].additional_notes = additional_notes
+ quotation_items[0].warehouse = warehouse
apply_cart_settings(quotation=quotation)
@@ -317,6 +323,10 @@
fields = fields[2:]
d.update(frappe.db.get_value("Website Item", {"item_code": item_code}, fields, as_dict=True))
+ website_warehouse = frappe.get_cached_value(
+ "Website Item", {"item_code": item_code}, "website_warehouse"
+ )
+ d.warehouse = website_warehouse
return doc
diff --git a/erpnext/e_commerce/variant_selector/utils.py b/erpnext/e_commerce/variant_selector/utils.py
index 4466c45..88356f5 100644
--- a/erpnext/e_commerce/variant_selector/utils.py
+++ b/erpnext/e_commerce/variant_selector/utils.py
@@ -104,6 +104,8 @@
@frappe.whitelist(allow_guest=True)
def get_next_attribute_and_values(item_code, selected_attributes):
+ from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
+
"""Find the count of Items that match the selected attributes.
Also, find the attribute values that are not applicable for further searching.
If less than equal to 10 items are found, return item_codes of those items.
@@ -168,7 +170,7 @@
product_info = None
product_id = ""
- website_warehouse = ""
+ warehouse = ""
if exact_match or filtered_items:
if exact_match and len(exact_match) == 1:
product_id = exact_match[0]
@@ -176,16 +178,19 @@
product_id = list(filtered_items)[0]
if product_id:
- website_warehouse = frappe.get_cached_value(
+ warehouse = frappe.get_cached_value(
"Website Item", {"item_code": product_id}, "website_warehouse"
)
available_qty = 0.0
- if website_warehouse:
- available_qty = flt(
- frappe.db.get_value(
- "Bin", {"item_code": product_id, "warehouse": website_warehouse}, "actual_qty"
- )
+ if warehouse and frappe.get_cached_value("Warehouse", warehouse, "is_group") == 1:
+ warehouses = get_child_warehouses(warehouse)
+ else:
+ warehouses = [warehouse] if warehouse else []
+
+ for warehouse in warehouses:
+ available_qty += flt(
+ frappe.db.get_value("Bin", {"item_code": product_id, "warehouse": warehouse}, "actual_qty")
)
return {
diff --git a/erpnext/erpnext_integrations/connectors/__init__.py b/erpnext/erpnext_integrations/connectors/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/erpnext_integrations/connectors/__init__.py
+++ /dev/null
diff --git a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py
deleted file mode 100644
index 2b2da7b..0000000
--- a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py
+++ /dev/null
@@ -1,256 +0,0 @@
-import base64
-import hashlib
-import hmac
-import json
-
-import frappe
-from frappe import _
-from frappe.utils import cstr
-
-
-def verify_request():
- woocommerce_settings = frappe.get_doc("Woocommerce Settings")
- sig = base64.b64encode(
- hmac.new(
- woocommerce_settings.secret.encode("utf8"), frappe.request.data, hashlib.sha256
- ).digest()
- )
-
- if (
- frappe.request.data
- and not sig == frappe.get_request_header("X-Wc-Webhook-Signature", "").encode()
- ):
- frappe.throw(_("Unverified Webhook Data"))
- frappe.set_user(woocommerce_settings.creation_user)
-
-
-@frappe.whitelist(allow_guest=True)
-def order(*args, **kwargs):
- try:
- _order(*args, **kwargs)
- except Exception:
- error_message = (
- frappe.get_traceback() + "\n\n Request Data: \n" + json.loads(frappe.request.data).__str__()
- )
- frappe.log_error("WooCommerce Error", error_message)
- raise
-
-
-def _order(*args, **kwargs):
- woocommerce_settings = frappe.get_doc("Woocommerce Settings")
- if frappe.flags.woocomm_test_order_data:
- order = frappe.flags.woocomm_test_order_data
- event = "created"
- # Ignore the test ping issued during WooCommerce webhook configuration
- # Ref: https://github.com/woocommerce/woocommerce/issues/15642
- if frappe.request.data.decode("utf-8").startswith("webhook_id="):
- return "success"
- elif frappe.request and frappe.request.data:
- verify_request()
- try:
- order = json.loads(frappe.request.data)
- except ValueError:
- # woocommerce returns 'webhook_id=value' for the first request which is not JSON
- order = frappe.request.data
- event = frappe.get_request_header("X-Wc-Webhook-Event")
-
- else:
- return "success"
-
- if event == "created":
- sys_lang = frappe.get_single("System Settings").language or "en"
- raw_billing_data = order.get("billing")
- raw_shipping_data = order.get("shipping")
- customer_name = raw_billing_data.get("first_name") + " " + raw_billing_data.get("last_name")
- link_customer_and_address(raw_billing_data, raw_shipping_data, customer_name)
- link_items(order.get("line_items"), woocommerce_settings, sys_lang)
- create_sales_order(order, woocommerce_settings, customer_name, sys_lang)
-
-
-def link_customer_and_address(raw_billing_data, raw_shipping_data, customer_name):
- customer_woo_com_email = raw_billing_data.get("email")
- customer_exists = frappe.get_value("Customer", {"woocommerce_email": customer_woo_com_email})
- if not customer_exists:
- # Create Customer
- customer = frappe.new_doc("Customer")
- else:
- # Edit Customer
- customer = frappe.get_doc("Customer", {"woocommerce_email": customer_woo_com_email})
- old_name = customer.customer_name
-
- customer.customer_name = customer_name
- customer.woocommerce_email = customer_woo_com_email
- customer.flags.ignore_mandatory = True
- customer.save()
-
- if customer_exists:
- # Fixes https://github.com/frappe/erpnext/issues/33708
- if old_name != customer_name:
- frappe.rename_doc("Customer", old_name, customer_name)
- for address_type in (
- "Billing",
- "Shipping",
- ):
- try:
- address = frappe.get_doc(
- "Address", {"woocommerce_email": customer_woo_com_email, "address_type": address_type}
- )
- rename_address(address, customer)
- except (
- frappe.DoesNotExistError,
- frappe.DuplicateEntryError,
- frappe.ValidationError,
- ):
- pass
- else:
- create_address(raw_billing_data, customer, "Billing")
- create_address(raw_shipping_data, customer, "Shipping")
- create_contact(raw_billing_data, customer)
-
-
-def create_contact(data, customer):
- email = data.get("email", None)
- phone = data.get("phone", None)
-
- if not email and not phone:
- return
-
- contact = frappe.new_doc("Contact")
- contact.first_name = data.get("first_name")
- contact.last_name = data.get("last_name")
- contact.is_primary_contact = 1
- contact.is_billing_contact = 1
-
- if phone:
- contact.add_phone(phone, is_primary_mobile_no=1, is_primary_phone=1)
-
- if email:
- contact.add_email(email, is_primary=1)
-
- contact.append("links", {"link_doctype": "Customer", "link_name": customer.name})
-
- contact.flags.ignore_mandatory = True
- contact.save()
-
-
-def create_address(raw_data, customer, address_type):
- address = frappe.new_doc("Address")
-
- address.address_line1 = raw_data.get("address_1", "Not Provided")
- address.address_line2 = raw_data.get("address_2", "Not Provided")
- address.city = raw_data.get("city", "Not Provided")
- address.woocommerce_email = customer.woocommerce_email
- address.address_type = address_type
- address.country = frappe.get_value("Country", {"code": raw_data.get("country", "IN").lower()})
- address.state = raw_data.get("state")
- address.pincode = raw_data.get("postcode")
- address.phone = raw_data.get("phone")
- address.email_id = customer.woocommerce_email
- address.append("links", {"link_doctype": "Customer", "link_name": customer.name})
-
- address.flags.ignore_mandatory = True
- address.save()
-
-
-def rename_address(address, customer):
- old_address_title = address.name
- new_address_title = customer.name + "-" + address.address_type
- address.address_title = customer.customer_name
- address.save()
-
- frappe.rename_doc("Address", old_address_title, new_address_title)
-
-
-def link_items(items_list, woocommerce_settings, sys_lang):
- for item_data in items_list:
- item_woo_com_id = cstr(item_data.get("product_id"))
-
- if not frappe.db.get_value("Item", {"woocommerce_id": item_woo_com_id}, "name"):
- # Create Item
- item = frappe.new_doc("Item")
- item.item_code = _("woocommerce - {0}", sys_lang).format(item_woo_com_id)
- item.stock_uom = woocommerce_settings.uom or _("Nos", sys_lang)
- item.item_group = _("WooCommerce Products", sys_lang)
-
- item.item_name = item_data.get("name")
- item.woocommerce_id = item_woo_com_id
- item.flags.ignore_mandatory = True
- item.save()
-
-
-def create_sales_order(order, woocommerce_settings, customer_name, sys_lang):
- new_sales_order = frappe.new_doc("Sales Order")
- new_sales_order.customer = customer_name
-
- new_sales_order.po_no = new_sales_order.woocommerce_id = order.get("id")
- new_sales_order.naming_series = woocommerce_settings.sales_order_series or "SO-WOO-"
-
- created_date = order.get("date_created").split("T")
- new_sales_order.transaction_date = created_date[0]
- delivery_after = woocommerce_settings.delivery_after_days or 7
- new_sales_order.delivery_date = frappe.utils.add_days(created_date[0], delivery_after)
-
- new_sales_order.company = woocommerce_settings.company
-
- set_items_in_sales_order(new_sales_order, woocommerce_settings, order, sys_lang)
- new_sales_order.flags.ignore_mandatory = True
- new_sales_order.insert()
- new_sales_order.submit()
-
- frappe.db.commit()
-
-
-def set_items_in_sales_order(new_sales_order, woocommerce_settings, order, sys_lang):
- company_abbr = frappe.db.get_value("Company", woocommerce_settings.company, "abbr")
-
- default_warehouse = _("Stores - {0}", sys_lang).format(company_abbr)
- if not frappe.db.exists("Warehouse", default_warehouse) and not woocommerce_settings.warehouse:
- frappe.throw(_("Please set Warehouse in Woocommerce Settings"))
-
- for item in order.get("line_items"):
- woocomm_item_id = item.get("product_id")
- found_item = frappe.get_doc("Item", {"woocommerce_id": cstr(woocomm_item_id)})
-
- ordered_items_tax = item.get("total_tax")
-
- new_sales_order.append(
- "items",
- {
- "item_code": found_item.name,
- "item_name": found_item.item_name,
- "description": found_item.item_name,
- "delivery_date": new_sales_order.delivery_date,
- "uom": woocommerce_settings.uom or _("Nos", sys_lang),
- "qty": item.get("quantity"),
- "rate": item.get("price"),
- "warehouse": woocommerce_settings.warehouse or default_warehouse,
- },
- )
-
- add_tax_details(
- new_sales_order, ordered_items_tax, "Ordered Item tax", woocommerce_settings.tax_account
- )
-
- # shipping_details = order.get("shipping_lines") # used for detailed order
-
- add_tax_details(
- new_sales_order, order.get("shipping_tax"), "Shipping Tax", woocommerce_settings.f_n_f_account
- )
- add_tax_details(
- new_sales_order,
- order.get("shipping_total"),
- "Shipping Total",
- woocommerce_settings.f_n_f_account,
- )
-
-
-def add_tax_details(sales_order, price, desc, tax_account_head):
- sales_order.append(
- "taxes",
- {
- "charge_type": "Actual",
- "account_head": tax_account_head,
- "tax_amount": price,
- "description": desc,
- },
- )
diff --git a/erpnext/erpnext_integrations/doctype/woocommerce_settings/__init__.py b/erpnext/erpnext_integrations/doctype/woocommerce_settings/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/erpnext_integrations/doctype/woocommerce_settings/__init__.py
+++ /dev/null
diff --git a/erpnext/erpnext_integrations/doctype/woocommerce_settings/test_woocommerce_settings.py b/erpnext/erpnext_integrations/doctype/woocommerce_settings/test_woocommerce_settings.py
deleted file mode 100644
index 9945823..0000000
--- a/erpnext/erpnext_integrations/doctype/woocommerce_settings/test_woocommerce_settings.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import unittest
-
-
-class TestWoocommerceSettings(unittest.TestCase):
- pass
diff --git a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.js b/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.js
deleted file mode 100644
index d7a3d36..0000000
--- a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.js
+++ /dev/null
@@ -1,56 +0,0 @@
-// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Woocommerce Settings', {
- refresh (frm) {
- frm.trigger("add_button_generate_secret");
- frm.trigger("check_enabled");
- frm.set_query("tax_account", ()=>{
- return {
- "filters": {
- "company": frappe.defaults.get_default("company"),
- "is_group": 0
- }
- };
- });
- },
-
- enable_sync (frm) {
- frm.trigger("check_enabled");
- },
-
- add_button_generate_secret(frm) {
- frm.add_custom_button(__('Generate Secret'), () => {
- frappe.confirm(
- __("Apps using current key won't be able to access, are you sure?"),
- () => {
- frappe.call({
- type:"POST",
- method:"erpnext.erpnext_integrations.doctype.woocommerce_settings.woocommerce_settings.generate_secret",
- }).done(() => {
- frm.reload_doc();
- }).fail(() => {
- frappe.msgprint(__("Could not generate Secret"));
- });
- }
- );
- });
- },
-
- check_enabled (frm) {
- frm.set_df_property("woocommerce_server_url", "reqd", frm.doc.enable_sync);
- frm.set_df_property("api_consumer_key", "reqd", frm.doc.enable_sync);
- frm.set_df_property("api_consumer_secret", "reqd", frm.doc.enable_sync);
- }
-});
-
-frappe.ui.form.on("Woocommerce Settings", "onload", function () {
- frappe.call({
- method: "erpnext.erpnext_integrations.doctype.woocommerce_settings.woocommerce_settings.get_series",
- callback: function (r) {
- $.each(r.message, function (key, value) {
- set_field_options(key, value);
- });
- }
- });
-});
diff --git a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.json b/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.json
deleted file mode 100644
index 956ae09..0000000
--- a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.json
+++ /dev/null
@@ -1,175 +0,0 @@
-{
- "creation": "2018-02-12 15:10:05.495713",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "enable_sync",
- "sb_00",
- "woocommerce_server_url",
- "secret",
- "cb_00",
- "api_consumer_key",
- "api_consumer_secret",
- "sb_accounting_details",
- "tax_account",
- "column_break_10",
- "f_n_f_account",
- "defaults_section",
- "creation_user",
- "warehouse",
- "sales_order_series",
- "column_break_14",
- "company",
- "delivery_after_days",
- "uom",
- "endpoints",
- "endpoint"
- ],
- "fields": [
- {
- "default": "0",
- "fieldname": "enable_sync",
- "fieldtype": "Check",
- "label": "Enable Sync"
- },
- {
- "fieldname": "sb_00",
- "fieldtype": "Section Break"
- },
- {
- "fieldname": "woocommerce_server_url",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Woocommerce Server URL"
- },
- {
- "fieldname": "secret",
- "fieldtype": "Code",
- "label": "Secret",
- "read_only": 1
- },
- {
- "fieldname": "cb_00",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "api_consumer_key",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "API consumer key"
- },
- {
- "fieldname": "api_consumer_secret",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "API consumer secret"
- },
- {
- "fieldname": "sb_accounting_details",
- "fieldtype": "Section Break",
- "label": "Accounting Details"
- },
- {
- "fieldname": "tax_account",
- "fieldtype": "Link",
- "label": "Tax Account",
- "options": "Account",
- "reqd": 1
- },
- {
- "fieldname": "column_break_10",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "f_n_f_account",
- "fieldtype": "Link",
- "label": "Freight and Forwarding Account",
- "options": "Account",
- "reqd": 1
- },
- {
- "fieldname": "defaults_section",
- "fieldtype": "Section Break",
- "label": "Defaults"
- },
- {
- "description": "The user that will be used to create Customers, Items and Sales Orders. This user should have the relevant permissions.",
- "fieldname": "creation_user",
- "fieldtype": "Link",
- "label": "Creation User",
- "options": "User",
- "reqd": 1
- },
- {
- "description": "This warehouse will be used to create Sales Orders. The fallback warehouse is \"Stores\".",
- "fieldname": "warehouse",
- "fieldtype": "Link",
- "label": "Warehouse",
- "options": "Warehouse"
- },
- {
- "fieldname": "column_break_14",
- "fieldtype": "Column Break"
- },
- {
- "description": "The fallback series is \"SO-WOO-\".",
- "fieldname": "sales_order_series",
- "fieldtype": "Select",
- "label": "Sales Order Series"
- },
- {
- "description": "This is the default UOM used for items and Sales orders. The fallback UOM is \"Nos\".",
- "fieldname": "uom",
- "fieldtype": "Link",
- "label": "UOM",
- "options": "UOM"
- },
- {
- "fieldname": "endpoints",
- "fieldtype": "Section Break",
- "label": "Endpoints"
- },
- {
- "fieldname": "endpoint",
- "fieldtype": "Code",
- "label": "Endpoint",
- "read_only": 1
- },
- {
- "description": "This company will be used to create Sales Orders.",
- "fieldname": "company",
- "fieldtype": "Link",
- "label": "Company",
- "options": "Company",
- "reqd": 1
- },
- {
- "description": "This is the default offset (days) for the Delivery Date in Sales Orders. The fallback offset is 7 days from the order placement date.",
- "fieldname": "delivery_after_days",
- "fieldtype": "Int",
- "label": "Delivery After (Days)"
- }
- ],
- "issingle": 1,
- "modified": "2019-11-04 00:45:21.232096",
- "modified_by": "Administrator",
- "module": "ERPNext Integrations",
- "name": "Woocommerce Settings",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py b/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py
deleted file mode 100644
index 4aa98aa..0000000
--- a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py
+++ /dev/null
@@ -1,87 +0,0 @@
-# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-from urllib.parse import urlparse
-
-import frappe
-from frappe import _
-from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
-from frappe.model.document import Document
-from frappe.utils.nestedset import get_root_of
-
-
-class WoocommerceSettings(Document):
- def validate(self):
- self.validate_settings()
- self.create_delete_custom_fields()
- self.create_webhook_url()
-
- def create_delete_custom_fields(self):
- if self.enable_sync:
- create_custom_fields(
- {
- ("Customer", "Sales Order", "Item", "Address"): dict(
- fieldname="woocommerce_id",
- label="Woocommerce ID",
- fieldtype="Data",
- read_only=1,
- print_hide=1,
- ),
- ("Customer", "Address"): dict(
- fieldname="woocommerce_email",
- label="Woocommerce Email",
- fieldtype="Data",
- read_only=1,
- print_hide=1,
- ),
- }
- )
-
- if not frappe.get_value("Item Group", {"name": _("WooCommerce Products")}):
- item_group = frappe.new_doc("Item Group")
- item_group.item_group_name = _("WooCommerce Products")
- item_group.parent_item_group = get_root_of("Item Group")
- item_group.insert()
-
- def validate_settings(self):
- if self.enable_sync:
- if not self.secret:
- self.set("secret", frappe.generate_hash())
-
- if not self.woocommerce_server_url:
- frappe.throw(_("Please enter Woocommerce Server URL"))
-
- if not self.api_consumer_key:
- frappe.throw(_("Please enter API Consumer Key"))
-
- if not self.api_consumer_secret:
- frappe.throw(_("Please enter API Consumer Secret"))
-
- def create_webhook_url(self):
- endpoint = "/api/method/erpnext.erpnext_integrations.connectors.woocommerce_connection.order"
-
- try:
- url = frappe.request.url
- except RuntimeError:
- # for CI Test to work
- url = "http://localhost:8000"
-
- server_url = "{uri.scheme}://{uri.netloc}".format(uri=urlparse(url))
-
- delivery_url = server_url + endpoint
- self.endpoint = delivery_url
-
-
-@frappe.whitelist()
-def generate_secret():
- woocommerce_settings = frappe.get_doc("Woocommerce Settings")
- woocommerce_settings.secret = frappe.generate_hash()
- woocommerce_settings.save()
-
-
-@frappe.whitelist()
-def get_series():
- return {
- "sales_order_series": frappe.get_meta("Sales Order").get_options("naming_series") or "SO-WOO-",
- }
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 41db6b3..2155699 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -423,9 +423,6 @@
"erpnext.stock.reorder_item.reorder_item",
],
},
- "all": [
- "erpnext.crm.doctype.social_media_post.social_media_post.process_scheduled_social_media_posts",
- ],
"hourly": [
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
"erpnext.projects.doctype.project.project.project_status_update_reminder",
@@ -433,7 +430,7 @@
"erpnext.projects.doctype.project.project.collect_project_status",
],
"hourly_long": [
- "erpnext.accounts.doctype.subscription.subscription.process_all",
+ "erpnext.accounts.doctype.process_subscription.process_subscription.create_subscription_process",
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries",
"erpnext.bulk_transaction.doctype.bulk_transaction_log.bulk_transaction_log.retry_failing_transaction",
],
@@ -558,8 +555,6 @@
"erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_matching_queries"
)
-get_matching_vouchers_for_bank_reconciliation = "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_matching_vouchers_for_bank_reconciliation"
-
get_amounts_not_reflected_in_system_for_bank_reconciliation_statement = "erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement.get_amounts_not_reflected_in_system_for_bank_reconciliation_statement"
get_payment_entries_for_bank_clearance = (
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js
index f1e6094..6621275 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.js
+++ b/erpnext/manufacturing/doctype/job_card/job_card.js
@@ -23,17 +23,6 @@
}
});
- let sbb_field = frm.get_docfield('serial_and_batch_bundle');
- if (sbb_field) {
- sbb_field.get_route_options_for_new_doc = () => {
- return {
- 'item_code': frm.doc.production_item,
- 'warehouse': frm.doc.wip_warehouse,
- 'voucher_type': frm.doc.doctype,
- }
- };
- }
-
frm.set_indicator_formatter('sub_operation',
function(doc) {
if (doc.status == "Pending") {
@@ -124,6 +113,17 @@
}
});
}
+
+ let sbb_field = frm.get_docfield('serial_and_batch_bundle');
+ if (sbb_field) {
+ sbb_field.get_route_options_for_new_doc = () => {
+ return {
+ 'item_code': frm.doc.production_item,
+ 'warehouse': frm.doc.wip_warehouse,
+ 'voucher_type': frm.doc.doctype,
+ }
+ };
+ }
},
setup_quality_inspection: function(frm) {
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js
index 46c554c..72438dd 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.js
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js
@@ -476,6 +476,15 @@
}
})
}
+ },
+
+ material_request_type(frm, cdt, cdn) {
+ let row = locals[cdt][cdn];
+
+ if (row.from_warehouse &&
+ row.material_request_type !== "Material Transfer") {
+ frappe.model.set_value(cdt, cdn, 'from_warehouse', '');
+ }
}
});
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 7bde29f..e88b791 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -40,6 +40,12 @@
self._rename_temporary_references()
validate_uom_is_integer(self, "stock_uom", "planned_qty")
self.validate_sales_orders()
+ self.validate_material_request_type()
+
+ def validate_material_request_type(self):
+ for row in self.get("mr_items"):
+ if row.from_warehouse and row.material_request_type != "Material Transfer":
+ row.from_warehouse = ""
@frappe.whitelist()
def validate_sales_orders(self, sales_order=None):
@@ -750,7 +756,9 @@
"items",
{
"item_code": item.item_code,
- "from_warehouse": item.from_warehouse,
+ "from_warehouse": item.from_warehouse
+ if material_request_type == "Material Transfer"
+ else None,
"qty": item.quantity,
"schedule_date": schedule_date,
"warehouse": item.warehouse,
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index 6ed7506..5292571 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -1098,6 +1098,41 @@
)
self.assertEqual(reserved_qty_after_mr, before_qty)
+ def test_from_warehouse_for_purchase_material_request(self):
+ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
+ from erpnext.stock.utils import get_or_make_bin
+
+ create_item("RM-TEST-123 For Purchase", valuation_rate=100)
+ bin_name = get_or_make_bin("RM-TEST-123 For Purchase", "_Test Warehouse - _TC")
+ t_warehouse = create_warehouse("_Test Store - _TC")
+ make_stock_entry(
+ item_code="Raw Material Item 1",
+ qty=5,
+ rate=100,
+ target=t_warehouse,
+ )
+
+ plan = create_production_plan(item_code="Test Production Item 1", do_not_save=1)
+ mr_items = get_items_for_material_requests(
+ plan.as_dict(), warehouses=[{"warehouse": t_warehouse}]
+ )
+
+ for d in mr_items:
+ plan.append("mr_items", d)
+
+ plan.save()
+
+ for row in plan.mr_items:
+ if row.material_request_type == "Material Transfer":
+ self.assertEqual(row.from_warehouse, t_warehouse)
+
+ row.material_request_type = "Purchase"
+
+ plan.save()
+
+ for row in plan.mr_items:
+ self.assertFalse(row.from_warehouse)
+
def test_skip_available_qty_for_sub_assembly_items(self):
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index b3d6d3e..e9c056e 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -340,5 +340,9 @@
erpnext.patches.v14_0.single_to_multi_dunning
execute:frappe.db.set_single_value('Selling Settings', 'allow_negative_rates_for_items', 0)
erpnext.patches.v15_0.correct_asset_value_if_je_with_workflow
+erpnext.patches.v15_0.delete_woocommerce_settings_doctype
+erpnext.patches.v14_0.migrate_deferred_accounts_to_item_defaults
+erpnext.patches.v14_0.update_invoicing_period_in_subscription
+execute:frappe.delete_doc("Page", "welcome-to-erpnext")
# below migration patch should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
diff --git a/erpnext/patches/v14_0/delete_education_doctypes.py b/erpnext/patches/v14_0/delete_education_doctypes.py
index 56a596a..aeeda70 100644
--- a/erpnext/patches/v14_0/delete_education_doctypes.py
+++ b/erpnext/patches/v14_0/delete_education_doctypes.py
@@ -47,13 +47,16 @@
for doctype in doctypes:
frappe.delete_doc("DocType", doctype, ignore_missing=True)
- portal_settings = frappe.get_doc("Portal Settings")
-
- for row in portal_settings.get("menu"):
- if row.reference_doctype in doctypes:
- row.delete()
-
- portal_settings.save()
+ titles = [
+ "Fees",
+ "Student Admission",
+ "Grant Application",
+ "Chapter",
+ "Certification Application",
+ ]
+ items = frappe.get_all("Portal Menu Item", filters=[["title", "in", titles]], pluck="name")
+ for item in items:
+ frappe.delete_doc("Portal Menu Item", item, ignore_missing=True, force=True)
frappe.delete_doc("Module Def", "Education", ignore_missing=True, force=True)
diff --git a/erpnext/patches/v14_0/delete_healthcare_doctypes.py b/erpnext/patches/v14_0/delete_healthcare_doctypes.py
index 2c699e4..896a440 100644
--- a/erpnext/patches/v14_0/delete_healthcare_doctypes.py
+++ b/erpnext/patches/v14_0/delete_healthcare_doctypes.py
@@ -41,7 +41,7 @@
for card in cards:
frappe.delete_doc("Number Card", card, ignore_missing=True, force=True)
- titles = ["Lab Test", "Prescription", "Patient Appointment"]
+ titles = ["Lab Test", "Prescription", "Patient Appointment", "Patient"]
items = frappe.get_all("Portal Menu Item", filters=[["title", "in", titles]], pluck="name")
for item in items:
frappe.delete_doc("Portal Menu Item", item, ignore_missing=True, force=True)
diff --git a/erpnext/patches/v14_0/migrate_deferred_accounts_to_item_defaults.py b/erpnext/patches/v14_0/migrate_deferred_accounts_to_item_defaults.py
new file mode 100644
index 0000000..44b830b
--- /dev/null
+++ b/erpnext/patches/v14_0/migrate_deferred_accounts_to_item_defaults.py
@@ -0,0 +1,39 @@
+import frappe
+
+
+def execute():
+ try:
+ item_dict = get_deferred_accounts()
+ add_to_item_defaults(item_dict)
+ except Exception:
+ frappe.db.rollback()
+ frappe.log_error("Failed to migrate deferred accounts in Item Defaults.")
+
+
+def get_deferred_accounts():
+ item = frappe.qb.DocType("Item")
+ return (
+ frappe.qb.from_(item)
+ .select(item.name, item.deferred_expense_account, item.deferred_revenue_account)
+ .where((item.enable_deferred_expense == 1) | (item.enable_deferred_revenue == 1))
+ .run(as_dict=True)
+ )
+
+
+def add_to_item_defaults(item_dict):
+ for item in item_dict:
+ add_company_wise_item_default(item, "deferred_expense_account")
+ add_company_wise_item_default(item, "deferred_revenue_account")
+
+
+def add_company_wise_item_default(item, account_type):
+ company = frappe.get_cached_value("Account", item[account_type], "company")
+ if company and item[account_type]:
+ item_defaults = frappe.get_cached_value("Item", item["name"], "item_defaults")
+ for item_row in item_defaults:
+ if item_row.company == company:
+ frappe.set_value("Item Default", item_row.name, account_type, item[account_type])
+ break
+ else:
+ item_defaults.append({"company": company, account_type: item[account_type]})
+ frappe.set_value("Item", item["name"], "item_defaults", item_defaults)
diff --git a/erpnext/patches/v14_0/update_invoicing_period_in_subscription.py b/erpnext/patches/v14_0/update_invoicing_period_in_subscription.py
new file mode 100644
index 0000000..2879e57
--- /dev/null
+++ b/erpnext/patches/v14_0/update_invoicing_period_in_subscription.py
@@ -0,0 +1,8 @@
+import frappe
+
+
+def execute():
+ subscription = frappe.qb.DocType("Subscription")
+ frappe.qb.update(subscription).set(
+ subscription.generate_invoice_at, "Beginning of the currency subscription period"
+ ).where(subscription.generate_invoice_at_period_start == 1).run()
diff --git a/erpnext/patches/v15_0/delete_woocommerce_settings_doctype.py b/erpnext/patches/v15_0/delete_woocommerce_settings_doctype.py
new file mode 100644
index 0000000..fb92ca5
--- /dev/null
+++ b/erpnext/patches/v15_0/delete_woocommerce_settings_doctype.py
@@ -0,0 +1,5 @@
+import frappe
+
+
+def execute():
+ frappe.delete_doc("DocType", "Woocommerce Settings", ignore_missing=True)
diff --git a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
index 52fa8ab..1f47347 100644
--- a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
+++ b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
@@ -134,12 +134,12 @@
format_row(row) {
return [
- row[1], // Document Type
- row[2], // Document Name
- row[5] || row[8], // Reference Date
- format_currency(row[3], row[9]), // Remaining
- row[4], // Reference Number
- row[6], // Party
+ row["doctype"],
+ row["name"],
+ row["reference_date"] || row["posting_date"],
+ format_currency(row["paid_amount"], row["currency"]),
+ row["reference_no"],
+ row["party"],
];
}
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index 54f0aad..0860d9c 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -9,6 +9,7 @@
erpnext.buying.BuyingController = class BuyingController extends erpnext.TransactionController {
setup() {
super.setup();
+ this.toggle_enable_for_stock_uom("allow_to_edit_stock_uom_qty_for_purchase");
this.frm.email_field = "contact_email";
}
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 80d7b79..b0a9e40 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -134,15 +134,6 @@
}
}
});
-
- let sbb_field = this.frm.get_docfield('items', 'serial_and_batch_bundle');
- if (sbb_field) {
- sbb_field.get_route_options_for_new_doc = (row) => {
- return {
- 'item_code': row.doc.item_code,
- }
- };
- }
}
if(
@@ -207,15 +198,6 @@
});
}
- let batch_no_field = this.frm.get_docfield("items", "batch_no");
- if (batch_no_field) {
- batch_no_field.get_route_options_for_new_doc = function(row) {
- return {
- "item": row.doc.item_code
- }
- };
- }
-
if (this.frm.fields_dict["items"].grid.get_field('blanket_order')) {
this.frm.set_query("blanket_order", "items", function(doc, cdt, cdn) {
var item = locals[cdt][cdn];
@@ -242,6 +224,14 @@
}
}
+
+ toggle_enable_for_stock_uom(field) {
+ frappe.db.get_single_value('Stock Settings', field)
+ .then(value => {
+ this.frm.fields_dict["items"].grid.toggle_enable("stock_qty", value);
+ });
+ }
+
onload() {
var me = this;
@@ -268,6 +258,28 @@
}
]);
}
+
+ if(this.frm.fields_dict['items'].grid.get_field('serial_and_batch_bundle')) {
+ let sbb_field = this.frm.get_docfield('items', 'serial_and_batch_bundle');
+ if (sbb_field) {
+ sbb_field.get_route_options_for_new_doc = (row) => {
+ return {
+ 'item_code': row.doc.item_code,
+ }
+ };
+ }
+ }
+
+ if(this.frm.fields_dict['items'].grid.get_field('batch_no')) {
+ let batch_no_field = this.frm.get_docfield('items', 'batch_no');
+ if (batch_no_field) {
+ batch_no_field.get_route_options_for_new_doc = function(row) {
+ return {
+ 'item': row.doc.item_code
+ }
+ };
+ }
+ }
}
is_return() {
@@ -358,7 +370,6 @@
}
refresh() {
-
erpnext.toggle_naming_series();
erpnext.hide_company();
this.set_dynamic_labels();
@@ -1188,6 +1199,16 @@
]);
}
+ stock_qty(doc, cdt, cdn) {
+ let item = frappe.get_doc(cdt, cdn);
+ item.conversion_factor = 1.0;
+ if (item.stock_qty) {
+ item.conversion_factor = flt(item.stock_qty) / flt(item.qty);
+ }
+
+ refresh_field("conversion_factor", item.name, item.parentfield);
+ }
+
calculate_stock_uom_rate(doc, cdt, cdn) {
let item = frappe.get_doc(cdt, cdn);
item.stock_uom_rate = flt(item.rate)/flt(item.conversion_factor);
diff --git a/erpnext/public/js/erpnext.bundle.js b/erpnext/public/js/erpnext.bundle.js
index 966a9e1..0e1b23b 100644
--- a/erpnext/public/js/erpnext.bundle.js
+++ b/erpnext/public/js/erpnext.bundle.js
@@ -16,7 +16,8 @@
import "./utils/supplier_quick_entry";
import "./call_popup/call_popup";
import "./utils/dimension_tree_filter";
-import "./utils/ledger_preview.js"
+import "./utils/ledger_preview.js";
+import "./utils/unreconcile.js";
import "./utils/barcode_scanner";
import "./telephony";
import "./templates/call_link.html";
diff --git a/erpnext/public/js/help_links.js b/erpnext/public/js/help_links.js
index 1c3f43e..6742761 100644
--- a/erpnext/public/js/help_links.js
+++ b/erpnext/public/js/help_links.js
@@ -5,7 +5,7 @@
frappe.help.help_links["Form/Rename Tool"] = [
{
label: "Bulk Rename",
- url: docsUrl + "user/manual/en/using-erpnext/articles/bulk-rename",
+ url: docsUrl + "user/manual/en/transactions-bulk-rename",
},
];
@@ -14,201 +14,174 @@
frappe.help.help_links["List/User"] = [
{
label: "New User",
- url:
- docsUrl +
- "user/manual/en/setting-up/users-and-permissions/adding-users",
+ url: docsUrl + "user/manual/en/adding-users",
},
{
label: "Rename User",
- url: docsUrl + "user/manual/en/setting-up/articles/rename-user",
+ url: docsUrl + "user/manual/en/renaming-documents",
},
];
frappe.help.help_links["permission-manager"] = [
{
- label: "Role Permissions Manager",
- url:
- docsUrl +
- "user/manual/en/setting-up/users-and-permissions/role-based-permissions",
+ label: "Role Based Permissions",
+ url: docsUrl + "user/manual/en/role-based-permissions",
},
{
label: "Managing Perm Level in Permissions Manager",
- url: docsUrl + "user/manual/en/setting-up/articles/managing-perm-level",
+ url: docsUrl + "user/manual/en/managing-perm-level",
},
{
label: "User Permissions",
- url:
- docsUrl +
- "user/manual/en/setting-up/users-and-permissions/user-permissions",
+ url: docsUrl + "user/manual/en/user-permissions",
},
{
label: "Sharing",
- url:
- docsUrl + "user/manual/en/setting-up/users-and-permissions/sharing",
+ url: docsUrl + "user/manual/en/sharing",
},
{
label: "Password",
- url: docsUrl + "user/manual/en/setting-up/articles/change-password",
+ url: docsUrl + "user/manual/en/change-password",
},
];
frappe.help.help_links["Form/System Settings"] = [
{
label: "System Settings",
- url: docsUrl + "user/manual/en/setting-up/settings/system-settings",
+ url: docsUrl + "user/manual/en/system-settings",
},
];
frappe.help.help_links["Form/Data Import"] = [
{
label: "Importing and Exporting Data",
- url: docsUrl + "user/manual/en/setting-up/data/data-import",
- },
- {
- label: "Overwriting Data from Data Import Tool",
- url:
- docsUrl +
- "user/manual/en/setting-up/articles/overwriting-data-from-data-import-tool",
+ url: docsUrl + "user/manual/en/data-import",
},
];
frappe.help.help_links["List/Data Import"] = [
{
label: "Importing and Exporting Data",
- url: docsUrl + "user/manual/en/setting-up/data/data-import",
- },
- {
- label: "Overwriting Data from Data Import Tool",
- url:
- docsUrl +
- "user/manual/en/setting-up/articles/overwriting-data-from-data-import-tool",
+ url: docsUrl + "user/manual/en/data-import",
},
];
frappe.help.help_links["module_setup"] = [
{
label: "Role Permissions Manager",
- url:
- docsUrl +
- "user/manual/en/setting-up/users-and-permissions/role-based-permissions",
+ url: docsUrl + "user/manual/en/role-based-permissions",
},
];
-frappe.help.help_links["Form/Naming Series"] = [
+frappe.help.help_links["Form/Document Naming Settings"] = [
{
label: "Naming Series",
- url: docsUrl + "user/manual/en/setting-up/settings/naming-series",
- },
- {
- label: "Setting the Current Value for Naming Series",
- url:
- docsUrl +
- "user/manual/en/setting-up/articles/naming-series-current-value",
+ url: docsUrl + "user/manual/en/document-naming-settings",
},
];
frappe.help.help_links["Form/Global Defaults"] = [
{
label: "Global Settings",
- url: docsUrl + "user/manual/en/setting-up/settings/global-defaults",
+ url: docsUrl + "user/manual/en/global-defaults",
},
];
frappe.help.help_links["List/Print Heading"] = [
{
label: "Print Heading",
- url: docsUrl + "user/manual/en/setting-up/print/print-headings",
+ url: docsUrl + "user/manual/en/print-headings",
},
];
frappe.help.help_links["Form/Print Heading"] = [
{
label: "Print Heading",
- url: docsUrl + "user/manual/en/setting-up/print/print-headings",
+ url: docsUrl + "user/manual/en/print-headings",
},
];
frappe.help.help_links["List/Letter Head"] = [
{
label: "Letter Head",
- url: docsUrl + "user/manual/en/setting-up/print/letter-head",
+ url: docsUrl + "user/manual/en/letter-head",
},
];
frappe.help.help_links["List/Address Template"] = [
{
label: "Address Template",
- url: docsUrl + "user/manual/en/setting-up/print/address-template",
+ url: docsUrl + "user/manual/en/address-template",
},
];
frappe.help.help_links["List/Terms and Conditions"] = [
{
label: "Terms and Conditions",
- url: docsUrl + "user/manual/en/setting-up/print/terms-and-conditions",
+ url: docsUrl + "user/manual/en/terms-and-conditions",
},
];
frappe.help.help_links["List/Cheque Print Template"] = [
{
label: "Cheque Print Template",
- url: docsUrl + "user/manual/en/setting-up/print/cheque-print-template",
+ url: docsUrl + "user/manual/en/cheque-print-template",
},
];
frappe.help.help_links["List/Email Account"] = [
{
label: "Email Account",
- url: docsUrl + "user/manual/en/setting-up/email/email-account",
+ url: docsUrl + "user/manual/en/email-account",
},
];
frappe.help.help_links["List/Notification"] = [
{
label: "Notification",
- url: docsUrl + "user/manual/en/setting-up/notifications",
+ url: docsUrl + "user/manual/en/notifications",
},
];
frappe.help.help_links["Form/Notification"] = [
{
label: "Notification",
- url: docsUrl + "user/manual/en/setting-up/notifications",
+ url: docsUrl + "user/manual/en/notifications",
},
];
frappe.help.help_links["Form/Email Digest"] = [
{
label: "Email Digest",
- url: docsUrl + "user/manual/en/setting-up/email/email-digest",
+ url: docsUrl + "user/manual/en/email-digest",
},
];
frappe.help.help_links["Form/Email Digest"] = [
{
label: "Email Digest",
- url: docsUrl + "user/manual/en/setting-up/email/email-digest",
+ url: docsUrl + "user/manual/en/email-digest",
},
];
frappe.help.help_links["List/Auto Email Report"] = [
{
label: "Auto Email Reports",
- url: docsUrl + "user/manual/en/setting-up/email/auto-email-reports",
+ url: docsUrl + "user/manual/en/auto-email-reports",
},
];
frappe.help.help_links["Form/Print Settings"] = [
{
label: "Print Settings",
- url: docsUrl + "user/manual/en/setting-up/print/print-settings",
+ url: docsUrl + "user/manual/en/print-settings",
},
];
frappe.help.help_links["print-format-builder"] = [
{
label: "Print Format Builder",
- url: docsUrl + "user/manual/en/setting-up/print/print-format-builder",
+ url: docsUrl + "user/manual/en/print-format-builder",
},
];
@@ -217,171 +190,160 @@
frappe.help.help_links["Form/PayPal Settings"] = [
{
label: "PayPal Settings",
- url:
- docsUrl +
- "user/manual/en/erpnext_integration/paypal-integration",
+ url: docsUrl + "user/manual/en/paypal-integration",
},
];
frappe.help.help_links["Form/Razorpay Settings"] = [
{
label: "Razorpay Settings",
- url:
- docsUrl +
- "user/manual/en/erpnext_integration/razorpay-integration",
+ url: docsUrl + "user/manual/en/razorpay-integration",
},
];
frappe.help.help_links["Form/Dropbox Settings"] = [
{
label: "Dropbox Settings",
- url: docsUrl + "user/manual/en/erpnext_integration/dropbox-backup",
+ url: docsUrl + "user/manual/en/dropbox-backup",
},
];
frappe.help.help_links["Form/LDAP Settings"] = [
{
label: "LDAP Settings",
- url:
- docsUrl + "user/manual/en/erpnext_integration/ldap-integration",
+ url: docsUrl + "user/manual/en/ldap-integration",
},
];
frappe.help.help_links["Form/Stripe Settings"] = [
{
label: "Stripe Settings",
- url:
- docsUrl +
- "user/manual/en/erpnext_integration/stripe-integration",
+ url: docsUrl + "user/manual/en/stripe-integration",
},
];
//Sales
frappe.help.help_links["Form/Quotation"] = [
- { label: "Quotation", url: docsUrl + "user/manual/en/selling/quotation" },
+ { label: "Quotation", url: docsUrl + "user/manual/en/quotation" },
{
label: "Applying Discount",
- url: docsUrl + "user/manual/en/selling/articles/applying-discount",
+ url: docsUrl + "user/manual/en/applying-discount",
},
{
label: "Sales Person",
- url:
- docsUrl +
- "user/manual/en/selling/articles/sales-persons-in-the-sales-transactions",
+ url: docsUrl + "user/manual/en/sales-persons-in-the-sales-transactions",
},
{
label: "Applying Margin",
- url: docsUrl + "user/manual/en/selling/articles/adding-margin",
+ url: docsUrl + "user/manual/en/adding-margin",
},
];
frappe.help.help_links["List/Customer"] = [
- { label: "Customer", url: docsUrl + "user/manual/en/CRM/customer" },
+ { label: "Customer", url: docsUrl + "user/manual/en/customer" },
{
label: "Credit Limit",
- url: docsUrl + "user/manual/en/accounts/credit-limit",
+ url: docsUrl + "user/manual/en/credit-limit",
},
];
frappe.help.help_links["Form/Customer"] = [
- { label: "Customer", url: docsUrl + "user/manual/en/CRM/customer" },
+ { label: "Customer", url: docsUrl + "user/manual/en/customer" },
{
label: "Credit Limit",
- url: docsUrl + "user/manual/en/accounts/credit-limit",
+ url: docsUrl + "user/manual/en/credit-limit",
},
];
frappe.help.help_links["List/Sales Taxes and Charges Template"] = [
{
label: "Setting Up Taxes",
- url: docsUrl + "user/manual/en/setting-up/setting-up-taxes",
+ url: docsUrl + "user/manual/en/setting-up-taxes",
},
];
frappe.help.help_links["Form/Sales Taxes and Charges Template"] = [
{
label: "Setting Up Taxes",
- url: docsUrl + "user/manual/en/setting-up/setting-up-taxes",
+ url: docsUrl + "user/manual/en/setting-up-taxes",
},
];
frappe.help.help_links["List/Sales Order"] = [
{
label: "Sales Order",
- url: docsUrl + "user/manual/en/selling/sales-order",
+ url: docsUrl + "user/manual/en/sales-order",
},
{
label: "Recurring Sales Order",
- url: docsUrl + "user/manual/en/accounts/articles/recurring-orders-and-invoices",
+ url: docsUrl + "user/manual/en/auto-repeat",
},
{
label: "Applying Discount",
- url: docsUrl + "user/manual/en/selling/articles/applying-discount",
+ url: docsUrl + "user/manual/en/applying-discount",
},
];
frappe.help.help_links["Form/Sales Order"] = [
{
label: "Sales Order",
- url: docsUrl + "user/manual/en/selling/sales-order",
+ url: docsUrl + "user/manual/en/sales-order",
},
{
label: "Recurring Sales Order",
- url: docsUrl + "user/manual/en/accounts/articles/recurring-orders-and-invoices",
+ url: docsUrl + "user/manual/en/auto-repeat",
},
{
label: "Applying Discount",
- url: docsUrl + "user/manual/en/selling/articles/applying-discount",
+ url: docsUrl + "user/manual/en/applying-discount",
},
{
label: "Drop Shipping",
- url: docsUrl + "user/manual/en/selling/articles/drop-shipping",
+ url: docsUrl + "user/manual/en/drop-shipping",
},
{
label: "Sales Person",
- url:
- docsUrl +
- "user/manual/en/selling/articles/sales-persons-in-the-sales-transactions",
+ url: docsUrl + "user/manual/en/sales-persons-in-the-sales-transactions",
},
{
label: "Close Sales Order",
- url: docsUrl + "user/manual/en/selling/articles/close-sales-order",
+ url: docsUrl + "user/manual/en/close-sales-order",
},
{
label: "Applying Margin",
- url: docsUrl + "user/manual/en/selling/articles/adding-margin",
+ url: docsUrl + "user/manual/en/adding-margin",
},
];
frappe.help.help_links["Form/Product Bundle"] = [
{
label: "Product Bundle",
- url: docsUrl + "user/manual/en/selling/product-bundle",
+ url: docsUrl + "user/manual/en/product-bundle",
},
];
frappe.help.help_links["Form/Selling Settings"] = [
{
label: "Selling Settings",
- url: docsUrl + "user/manual/en/selling/selling-settings",
+ url: docsUrl + "user/manual/en/selling-settings",
},
];
//Buying
frappe.help.help_links["List/Supplier"] = [
- { label: "Supplier", url: docsUrl + "user/manual/en/buying/supplier" },
+ { label: "Supplier", url: docsUrl + "user/manual/en/supplier" },
];
frappe.help.help_links["Form/Supplier"] = [
- { label: "Supplier", url: docsUrl + "user/manual/en/buying/supplier" },
+ { label: "Supplier", url: docsUrl + "user/manual/en/supplier" },
];
frappe.help.help_links["Form/Request for Quotation"] = [
{
label: "Request for Quotation",
- url: docsUrl + "user/manual/en/buying/request-for-quotation",
+ url: docsUrl + "user/manual/en/request-for-quotation",
},
{
label: "RFQ Video",
@@ -392,113 +354,105 @@
frappe.help.help_links["Form/Supplier Quotation"] = [
{
label: "Supplier Quotation",
- url: docsUrl + "user/manual/en/buying/supplier-quotation",
+ url: docsUrl + "user/manual/en/supplier-quotation",
},
];
frappe.help.help_links["Form/Buying Settings"] = [
{
label: "Buying Settings",
- url: docsUrl + "user/manual/en/buying/setup/buying-settings",
+ url: docsUrl + "user/manual/en/buying-settings",
},
];
frappe.help.help_links["List/Purchase Order"] = [
{
label: "Purchase Order",
- url: docsUrl + "user/manual/en/buying/purchase-order",
+ url: docsUrl + "user/manual/en/purchase-order",
},
{
label: "Recurring Purchase Order",
- url: docsUrl + "user/manual/en/accounts/articles/recurring-orders-and-invoices",
+ url: docsUrl + "user/manual/en/auto-repeat",
},
];
frappe.help.help_links["Form/Purchase Order"] = [
{
label: "Purchase Order",
- url: docsUrl + "user/manual/en/buying/purchase-order",
+ url: docsUrl + "user/manual/en/purchase-order",
},
{
label: "Item UoM",
- url:
- docsUrl +
- "user/manual/en/buying/articles/purchasing-in-different-unit",
+ url: docsUrl + "user/manual/en/purchasing-in-different-unit",
},
{
label: "Supplier Item Code",
- url:
- docsUrl +
- "user/manual/en/buying/articles/maintaining-suppliers-part-no-in-item",
+ url: docsUrl + "user/manual/en/maintaining-suppliers-part-no-in-item",
},
{
label: "Recurring Purchase Order",
- url: docsUrl + "user/manual/en/accounts/articles/recurring-orders-and-invoices",
+ url: docsUrl + "user/manual/en/auto-repeat",
},
{
label: "Subcontracting",
- url: docsUrl + "user/manual/en/manufacturing/subcontracting",
+ url: docsUrl + "user/manual/en/subcontracting",
},
];
frappe.help.help_links["List/Purchase Taxes and Charges Template"] = [
{
label: "Setting Up Taxes",
- url: docsUrl + "user/manual/en/setting-up/setting-up-taxes",
+ url: docsUrl + "user/manual/en/setting-up-taxes",
},
];
frappe.help.help_links["List/Price List"] = [
{
label: "Price List",
- url: docsUrl + "user/manual/en/stock/price-lists",
+ url: docsUrl + "user/manual/en/price-lists",
},
];
frappe.help.help_links["List/Authorization Rule"] = [
{
label: "Authorization Rule",
- url: docsUrl + "user/manual/en/customize-erpnext/authorization-rule",
+ url: docsUrl + "user/manual/en/authorization-rule",
},
];
frappe.help.help_links["Form/SMS Settings"] = [
{
label: "SMS Settings",
- url: docsUrl + "user/manual/en/setting-up/sms-setting",
+ url: docsUrl + "user/manual/en/sms-setting",
},
];
frappe.help.help_links["List/Stock Reconciliation"] = [
{
label: "Stock Reconciliation",
- url:
- docsUrl +
- "user/manual/en/stock/stock-reconciliation",
+ url: docsUrl + "user/manual/en/stock-reconciliation",
},
];
frappe.help.help_links["Tree/Territory"] = [
{
label: "Territory",
- url: docsUrl + "user/manual/en/selling/territory",
+ url: docsUrl + "user/manual/en/territory",
},
];
frappe.help.help_links["List/Workflow"] = [
- { label: "Workflow", url: docsUrl + "user/manual/en/setting-up/workflows" },
+ { label: "Workflow", url: docsUrl + "user/manual/en/workflows" },
];
frappe.help.help_links["List/Company"] = [
{
label: "Company",
- url: docsUrl + "user/manual/en/setting-up/company-setup",
+ url: docsUrl + "user/manual/en/company-setup",
},
{
label: "Delete All Related Transactions for a Company",
- url:
- docsUrl +
- "user/manual/en/setting-up/articles/delete-a-company-and-all-related-transactions",
+ url: docsUrl + "user/manual/en/delete_company_transactions",
},
];
@@ -507,116 +461,114 @@
frappe.help.help_links["Tree/Account"] = [
{
label: "Chart of Accounts",
- url: docsUrl + "user/manual/en/accounts/chart-of-accounts",
+ url: docsUrl + "user/manual/en/chart-of-accounts",
},
{
label: "Managing Tree Mastes",
- url:
- docsUrl +
- "user/manual/en/setting-up/articles/managing-tree-structure-masters",
+ url: docsUrl + "user/manual/en/managing-tree-structure-masters",
},
];
frappe.help.help_links["Form/Sales Invoice"] = [
{
label: "Sales Invoice",
- url: docsUrl + "user/manual/en/accounts/sales-invoice",
+ url: docsUrl + "user/manual/en/sales-invoice",
},
{
label: "Accounts Opening Balance",
- url: docsUrl + "user/manual/en/accounts/opening-balance",
+ url: docsUrl + "user/manual/en/opening-balance",
},
{
label: "Sales Return",
- url: docsUrl + "user/manual/en/stock/sales-return",
+ url: docsUrl + "user/manual/en/sales-return",
},
{
label: "Recurring Sales Invoice",
- url: docsUrl + "user/manual/en/accounts/articles/recurring-orders-and-invoices",
+ url: docsUrl + "user/manual/en/auto-repeat",
},
];
frappe.help.help_links["List/Sales Invoice"] = [
{
label: "Sales Invoice",
- url: docsUrl + "user/manual/en/accounts/sales-invoice",
+ url: docsUrl + "user/manual/en/sales-invoice",
},
{
label: "Accounts Opening Balance",
- url: docsUrl + "user/manual/en/accounts/opening-balances",
+ url: docsUrl + "user/manual/en/opening-balance",
},
{
label: "Sales Return",
- url: docsUrl + "user/manual/en/stock/sales-return",
+ url: docsUrl + "user/manual/en/sales-return",
},
{
label: "Recurring Sales Invoice",
- url: docsUrl + "user/manual/en/accounts/articles/recurring-orders-and-invoices",
+ url: docsUrl + "user/manual/en/auto-repeat",
},
];
frappe.help.help_links["point-of-sale"] = [
{
label: "Point of Sale Invoice",
- url: docsUrl + "user/manual/en/accounts/point-of-sales",
+ url: docsUrl + "user/manual/en/point-of-sales",
},
];
frappe.help.help_links["List/POS Profile"] = [
{
label: "Point of Sale Profile",
- url: docsUrl + "user/manual/en/accounts/pos-profile",
+ url: docsUrl + "user/manual/en/pos-profile",
},
];
frappe.help.help_links["Form/POS Profile"] = [
{
label: "POS Profile",
- url: docsUrl + "user/manual/en/accounts/pos-profile",
+ url: docsUrl + "user/manual/en/pos-profile",
},
];
frappe.help.help_links["List/Purchase Invoice"] = [
{
label: "Purchase Invoice",
- url: docsUrl + "user/manual/en/accounts/purchase-invoice",
+ url: docsUrl + "user/manual/en/purchase-invoice",
},
{
label: "Accounts Opening Balance",
- url: docsUrl + "user/manual/en/accounts/opening-balance",
+ url: docsUrl + "user/manual/en/opening-balance",
},
{
label: "Recurring Purchase Invoice",
- url: docsUrl + "user/manual/en/accounts/articles/recurring-orders-and-invoices",
+ url: docsUrl + "user/manual/en/auto-repeat",
},
];
frappe.help.help_links["List/Journal Entry"] = [
{
label: "Journal Entry",
- url: docsUrl + "user/manual/en/accounts/journal-entry",
+ url: docsUrl + "user/manual/en/journal-entry",
},
{
label: "Advance Payment Entry",
- url: docsUrl + "user/manual/en/accounts/advance-payment-entry",
+ url: docsUrl + "user/manual/en/advance-payment-entry",
},
{
label: "Accounts Opening Balance",
- url: docsUrl + "user/manual/en/accounts/opening-balance",
+ url: docsUrl + "user/manual/en/opening-balance",
},
];
frappe.help.help_links["List/Payment Entry"] = [
{
label: "Payment Entry",
- url: docsUrl + "user/manual/en/accounts/payment-entry",
+ url: docsUrl + "user/manual/en/payment-entry",
},
];
frappe.help.help_links["List/Payment Request"] = [
{
label: "Payment Request",
- url: docsUrl + "user/manual/en/accounts/payment-request",
+ url: docsUrl + "user/manual/en/payment-request",
},
];
@@ -630,30 +582,29 @@
frappe.help.help_links["List/Asset Category"] = [
{
label: "Asset Category",
- url: docsUrl + "user/manual/en/asset/asset-category",
+ url: docsUrl + "user/manual/en/asset-category",
},
];
frappe.help.help_links["Tree/Cost Center"] = [
- { label: "Budgeting", url: docsUrl + "user/manual/en/accounts/budgeting" },
+ { label: "Budgeting", url: docsUrl + "user/manual/en/budgeting" },
];
//Stock
frappe.help.help_links["List/Item"] = [
- { label: "Item", url: docsUrl + "user/manual/en/stock/item" },
+ { label: "Item", url: docsUrl + "user/manual/en/item" },
{
label: "Item Price",
- url: docsUrl + "user/manual/en/stock/item-price",
+ url: docsUrl + "user/manual/en/item-price",
},
{
label: "Barcode",
- url:
- docsUrl + "user/manual/en/stock/articles/track-items-using-barcode",
+ url: docsUrl + "user/manual/en/track-items-using-barcode",
},
{
label: "Item Wise Taxation",
- url: docsUrl + "user/manual/en/accounts/item-tax-template",
+ url: docsUrl + "user/manual/en/item-tax-template",
},
{
label: "Managing Fixed Assets",
@@ -661,34 +612,33 @@
},
{
label: "Item Codification",
- url: docsUrl + "user/manual/en/stock/articles/item-codification",
+ url: docsUrl + "user/manual/en/item-codification",
},
{
label: "Item Variants",
- url: docsUrl + "user/manual/en/stock/item-variants",
+ url: docsUrl + "user/manual/en/item-variants",
},
{
label: "Item Valuation",
url:
docsUrl +
- "user/manual/en/stock/articles/calculation-of-valuation-rate-in-fifo-and-moving-average",
+ "user/manual/en/calculation-of-valuation-rate-in-fifo-and-moving-average",
},
];
frappe.help.help_links["Form/Item"] = [
- { label: "Item", url: docsUrl + "user/manual/en/stock/item" },
+ { label: "Item", url: docsUrl + "user/manual/en/item" },
{
label: "Item Price",
- url: docsUrl + "user/manual/en/stock/item-price",
+ url: docsUrl + "user/manual/en/item-price",
},
{
label: "Barcode",
- url:
- docsUrl + "user/manual/en/stock/articles/track-items-using-barcode",
+ url: docsUrl + "user/manual/en/track-items-using-barcode",
},
{
label: "Item Wise Taxation",
- url: docsUrl + "user/manual/en/accounts/item-tax-template",
+ url: docsUrl + "user/manual/en/item-tax-template",
},
{
label: "Managing Fixed Assets",
@@ -696,240 +646,226 @@
},
{
label: "Item Codification",
- url: docsUrl + "user/manual/en/stock/articles/item-codification",
+ url: docsUrl + "user/manual/en/item-codification",
},
{
label: "Item Variants",
- url: docsUrl + "user/manual/en/stock/item-variants",
+ url: docsUrl + "user/manual/en/item-variants",
},
{
label: "Item Valuation",
- url:
- docsUrl +
- "user/manual/en/stock/item/item-valuation-fifo-and-moving-average",
+ url: docsUrl + "user/manual/en/item-valuation-transactions",
},
];
frappe.help.help_links["List/Purchase Receipt"] = [
{
label: "Purchase Receipt",
- url: docsUrl + "user/manual/en/stock/purchase-receipt",
+ url: docsUrl + "user/manual/en/purchase-receipt",
},
{
label: "Barcode",
- url:
- docsUrl + "user/manual/en/stock/articles/track-items-using-barcode",
+ url: docsUrl + "user/manual/en/track-items-using-barcode",
},
];
frappe.help.help_links["List/Delivery Note"] = [
{
label: "Delivery Note",
- url: docsUrl + "user/manual/en/stock/delivery-note",
+ url: docsUrl + "user/manual/en/delivery-note",
},
{
label: "Barcode",
- url:
- docsUrl + "user/manual/en/stock/articles/track-items-using-barcode",
+ url: docsUrl + "user/manual/en/track-items-using-barcode",
},
{
label: "Sales Return",
- url: docsUrl + "user/manual/en/stock/sales-return",
+ url: docsUrl + "user/manual/en/sales-return",
},
];
frappe.help.help_links["Form/Delivery Note"] = [
{
label: "Delivery Note",
- url: docsUrl + "user/manual/en/stock/delivery-note",
+ url: docsUrl + "user/manual/en/delivery-note",
},
{
label: "Sales Return",
- url: docsUrl + "user/manual/en/stock/sales-return",
+ url: docsUrl + "user/manual/en/sales-return",
},
{
label: "Barcode",
- url:
- docsUrl + "user/manual/en/stock/articles/track-items-using-barcode",
+ url: docsUrl + "user/manual/en/track-items-using-barcode",
},
];
frappe.help.help_links["List/Installation Note"] = [
{
label: "Installation Note",
- url: docsUrl + "user/manual/en/stock/installation-note",
+ url: docsUrl + "user/manual/en/installation-note",
},
];
frappe.help.help_links["List/Budget"] = [
- { label: "Budgeting", url: docsUrl + "user/manual/en/accounts/budgeting" },
+ { label: "Budgeting", url: docsUrl + "user/manual/en/budgeting" },
];
frappe.help.help_links["List/Material Request"] = [
{
label: "Material Request",
- url: docsUrl + "user/manual/en/stock/material-request",
+ url: docsUrl + "user/manual/en/material-request",
},
{
label: "Auto-creation of Material Request",
- url:
- docsUrl +
- "user/manual/en/stock/articles/auto-creation-of-material-request",
+ url: docsUrl + "user/manual/en/auto-creation-of-material-request",
},
];
frappe.help.help_links["Form/Material Request"] = [
{
label: "Material Request",
- url: docsUrl + "user/manual/en/stock/material-request",
+ url: docsUrl + "user/manual/en/material-request",
},
{
label: "Auto-creation of Material Request",
- url:
- docsUrl +
- "user/manual/en/stock/articles/auto-creation-of-material-request",
+ url: docsUrl + "user/manual/en/auto-creation-of-material-request",
},
];
frappe.help.help_links["Form/Stock Entry"] = [
- { label: "Stock Entry", url: docsUrl + "user/manual/en/stock/stock-entry" },
+ { label: "Stock Entry", url: docsUrl + "user/manual/en/stock-entry" },
{
label: "Stock Entry Types",
- url: docsUrl + "user/manual/en/stock/articles/stock-entry-purpose",
+ url: docsUrl + "user/manual/en/stock-entry-purpose",
},
{
label: "Repack Entry",
- url: docsUrl + "user/manual/en/stock/articles/repack-entry",
+ url: docsUrl + "user/manual/en/repack-entry",
},
{
label: "Opening Stock",
- url: docsUrl + "user/manual/en/stock/opening-stock",
+ url: docsUrl + "user/manual/en/opening-stock",
},
{
label: "Subcontracting",
- url: docsUrl + "user/manual/en/manufacturing/subcontracting",
+ url: docsUrl + "user/manual/en/subcontracting",
},
];
frappe.help.help_links["List/Stock Entry"] = [
- { label: "Stock Entry", url: docsUrl + "user/manual/en/stock/stock-entry" },
+ { label: "Stock Entry", url: docsUrl + "user/manual/en/stock-entry" },
];
frappe.help.help_links["Tree/Warehouse"] = [
- { label: "Warehouse", url: docsUrl + "user/manual/en/stock/warehouse" },
+ { label: "Warehouse", url: docsUrl + "user/manual/en/warehouse" },
];
frappe.help.help_links["List/Serial No"] = [
- { label: "Serial No", url: docsUrl + "user/manual/en/stock/serial-no" },
+ { label: "Serial No", url: docsUrl + "user/manual/en/serial-no" },
];
frappe.help.help_links["Form/Serial No"] = [
- { label: "Serial No", url: docsUrl + "user/manual/en/stock/serial-no" },
+ { label: "Serial No", url: docsUrl + "user/manual/en/serial-no" },
];
frappe.help.help_links["List/Batch"] = [
- { label: "Batch", url: docsUrl + "user/manual/en/stock/batch" },
+ { label: "Batch", url: docsUrl + "user/manual/en/batch" },
];
frappe.help.help_links["Form/Batch"] = [
- { label: "Batch", url: docsUrl + "user/manual/en/stock/batch" },
+ { label: "Batch", url: docsUrl + "user/manual/en/batch" },
];
frappe.help.help_links["Form/Packing Slip"] = [
{
label: "Packing Slip",
- url: docsUrl + "user/manual/en/stock/packing-slip",
+ url: docsUrl + "user/manual/en/packing-slip",
},
];
frappe.help.help_links["Form/Quality Inspection"] = [
{
label: "Quality Inspection",
- url: docsUrl + "user/manual/en/stock/quality-inspection",
+ url: docsUrl + "user/manual/en/quality-inspection",
},
];
frappe.help.help_links["Form/Landed Cost Voucher"] = [
{
label: "Landed Cost Voucher",
- url: docsUrl + "user/manual/en/stock/landed-cost-voucher",
+ url: docsUrl + "user/manual/en/landed-cost-voucher",
},
];
frappe.help.help_links["Tree/Item Group"] = [
{
label: "Item Group",
- url: docsUrl + "user/manual/en/stock/item-group",
+ url: docsUrl + "user/manual/en/item-group",
},
];
frappe.help.help_links["Form/Item Attribute"] = [
{
label: "Item Attribute",
- url: docsUrl + "user/manual/en/stock/item-attribute",
+ url: docsUrl + "user/manual/en/item-attribute",
},
];
frappe.help.help_links["Form/UOM"] = [
{
label: "Fractions in UOM",
- url:
- docsUrl + "user/manual/en/stock/articles/managing-fractions-in-uom",
+ url: docsUrl + "user/manual/en/managing-fractions-in-uom",
},
];
frappe.help.help_links["Form/Stock Reconciliation"] = [
{
label: "Opening Stock Entry",
- url: docsUrl + "user/manual/en/stock/stock-reconciliation",
+ url: docsUrl + "user/manual/en/stock-reconciliation",
},
];
//CRM
frappe.help.help_links["Form/Lead"] = [
- { label: "Lead", url: docsUrl + "user/manual/en/CRM/lead" },
+ { label: "Lead", url: docsUrl + "user/manual/en/lead" },
];
frappe.help.help_links["Form/Opportunity"] = [
- { label: "Opportunity", url: docsUrl + "user/manual/en/CRM/opportunity" },
+ { label: "Opportunity", url: docsUrl + "user/manual/en/opportunity" },
];
frappe.help.help_links["Form/Address"] = [
- { label: "Address", url: docsUrl + "user/manual/en/CRM/address" },
+ { label: "Address", url: docsUrl + "user/manual/en/address" },
];
frappe.help.help_links["Form/Contact"] = [
- { label: "Contact", url: docsUrl + "user/manual/en/CRM/contact" },
+ { label: "Contact", url: docsUrl + "user/manual/en/contact" },
];
frappe.help.help_links["Form/Newsletter"] = [
- { label: "Newsletter", url: docsUrl + "user/manual/en/CRM/newsletter" },
+ { label: "Newsletter", url: docsUrl + "user/manual/en/newsletter" },
];
frappe.help.help_links["Form/Campaign"] = [
- { label: "Campaign", url: docsUrl + "user/manual/en/CRM/campaign" },
+ { label: "Campaign", url: docsUrl + "user/manual/en/campaign" },
];
frappe.help.help_links["Tree/Sales Person"] = [
{
label: "Sales Person",
- url: docsUrl + "user/manual/en/CRM/sales-person",
+ url: docsUrl + "user/manual/en/sales-person",
},
];
frappe.help.help_links["Form/Sales Person"] = [
{
label: "Sales Person Target",
- url:
- docsUrl +
- "user/manual/en/selling/sales-person-target-allocation",
+ url: docsUrl + "user/manual/en/sales-person-target-allocation",
},
{
label: "Sales Person in Transactions",
- url:
- docsUrl +
- "user/manual/en/selling/articles/sales-persons-in-the-sales-transactions",
+ url: docsUrl + "user/manual/en/sales-persons-in-the-sales-transactions",
},
];
@@ -938,41 +874,39 @@
frappe.help.help_links["Form/BOM"] = [
{
label: "Bill of Material",
- url: docsUrl + "user/manual/en/manufacturing/bill-of-materials",
+ url: docsUrl + "user/manual/en/bill-of-materials",
},
{
label: "Nested BOM Structure",
- url:
- docsUrl +
- "user/manual/en/manufacturing/articles/managing-multi-level-bom",
+ url: docsUrl + "user/manual/en/managing-multi-level-bom",
},
];
frappe.help.help_links["Form/Work Order"] = [
{
label: "Work Order",
- url: docsUrl + "user/manual/en/manufacturing/work-order",
+ url: docsUrl + "user/manual/en/work-order",
},
];
frappe.help.help_links["Form/Workstation"] = [
{
label: "Workstation",
- url: docsUrl + "user/manual/en/manufacturing/workstation",
+ url: docsUrl + "user/manual/en/workstation",
},
];
frappe.help.help_links["Form/Operation"] = [
{
label: "Operation",
- url: docsUrl + "user/manual/en/manufacturing/operation",
+ url: docsUrl + "user/manual/en/operation",
},
];
frappe.help.help_links["Form/BOM Update Tool"] = [
{
label: "BOM Update Tool",
- url: docsUrl + "user/manual/en/manufacturing/bom-update-tool",
+ url: docsUrl + "user/manual/en/bom-update-tool",
},
];
@@ -981,24 +915,24 @@
frappe.help.help_links["Form/Customize Form"] = [
{
label: "Custom Field",
- url: docsUrl + "user/manual/en/customize-erpnext/custom-field",
+ url: docsUrl + "user/manual/en/custom-field",
},
{
label: "Customize Field",
- url: docsUrl + "user/manual/en/customize-erpnext/customize-form",
+ url: docsUrl + "user/manual/en/customize-form",
},
];
frappe.help.help_links["List/Custom Field"] = [
{
label: "Custom Field",
- url: docsUrl + "user/manual/en/customize-erpnext/custom-field",
+ url: docsUrl + "user/manual/en/custom-field",
},
];
frappe.help.help_links["Form/Custom Field"] = [
{
label: "Custom Field",
- url: docsUrl + "user/manual/en/customize-erpnext/custom-field",
+ url: docsUrl + "user/manual/en/custom-field",
},
];
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index 89750f8..d435711 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -769,6 +769,9 @@
dialog.show();
}
+
+
+
erpnext.utils.map_current_doc = function(opts) {
function _map() {
if($.isArray(cur_frm.doc.items) && cur_frm.doc.items.length > 0) {
@@ -1097,4 +1100,4 @@
$btn.on("click", function() {
context.show_serial_batch_selector(grid_row.frm, grid_row.doc, "", "", true);
});
-}
\ No newline at end of file
+}
diff --git a/erpnext/public/js/utils/demo.js b/erpnext/public/js/utils/demo.js
index 3ebc5ef..ef83cf6 100644
--- a/erpnext/public/js/utils/demo.js
+++ b/erpnext/public/js/utils/demo.js
@@ -1,91 +1,34 @@
+frappe.provide("erpnext.demo");
+
$(document).on("toolbar_setup", function () {
if (frappe.boot.sysdefaults.demo_company) {
- render_clear_demo_button();
+ render_clear_demo_action();
}
-
- // for first load after setup.
- frappe.realtime.on("demo_data_complete", () => {
- render_clear_demo_button();
- });
});
-function render_clear_demo_button() {
- let wait_for_onboaring_tours = setInterval(() => {
- if ($("#driver-page-overlay").length || $("#show-dialog").length) {
- return;
- }
- setup_clear_demo_button();
- clearInterval(wait_for_onboaring_tours);
- }, 2000);
-}
-
-function setup_clear_demo_button() {
- let message_string = __(
- "Demo data is present on the system, erase data before starting real usage."
+function render_clear_demo_action() {
+ let demo_action = $(
+ `<a class="dropdown-item" onclick="return erpnext.demo.clear_demo()">
+ ${__("Clear Demo Data")}
+ </a>`
);
- let $floatingBar = $(`
- <div class="flex justify-content-center" style="width: 100%;">
- <div class="flex justify-content-center flex-col shadow rounded p-2"
- style="
- background-color: #e0f2fe;
- position: fixed;
- bottom: 20px;
- z-index: 1;">
- <p style="margin: auto 0; padding-left: 10px; margin-right: 20px; font-size: 15px;">
- ${message_string}
- </p>
- <button id="clear-demo" type="button"
- class="
- px-4
- py-2
- border
- border-transparent
- text-white
- "
- style="
- margin: auto 0;
- height: fit-content;
- background-color: #007bff;
- border-radius: 5px;
- margin-right: 10px
- "
- >
- Clear Demo Data
- </button>
- <a type="button" id="dismiss-demo-banner" class="text-muted" style="align-self: center">
- <svg class="icon" style="">
- <use class="" href="#icon-close"></use>
- </svg>
- </a>
- </div>
- </div>
- `);
-
- $("footer").append($floatingBar);
-
- $("#clear-demo").on("click", function () {
- frappe.confirm(
- __("Are you sure you want to clear all demo data?"),
- () => {
- frappe.call({
- method: "erpnext.setup.demo.clear_demo_data",
- freeze: true,
- freeze_message: __("Clearing Demo Data..."),
- callback: function (r) {
- frappe.ui.toolbar.clear_cache();
- frappe.show_alert({
- message: __("Demo data cleared"),
- indicator: "green",
- });
- $("footer").remove($floatingBar);
- },
- });
- }
- );
- });
-
- $("#dismiss-demo-banner").on("click", function () {
- $floatingBar.remove();
- });
+ demo_action.appendTo($("#toolbar-user"));
}
+
+erpnext.demo.clear_demo = function () {
+ frappe.confirm(__("Are you sure you want to clear all demo data?"), () => {
+ frappe.call({
+ method: "erpnext.setup.demo.clear_demo_data",
+ freeze: true,
+ freeze_message: __("Clearing Demo Data..."),
+ callback: function (r) {
+ frappe.ui.toolbar.clear_cache();
+ frappe.show_alert({
+ message: __("Demo data cleared"),
+ indicator: "green",
+ });
+ },
+ });
+ });
+};
diff --git a/erpnext/public/js/utils/sales_common.js b/erpnext/public/js/utils/sales_common.js
index 89dcaa6..1d6daa5 100644
--- a/erpnext/public/js/utils/sales_common.js
+++ b/erpnext/public/js/utils/sales_common.js
@@ -8,6 +8,7 @@
erpnext.selling.SellingController = class SellingController extends erpnext.TransactionController {
setup() {
super.setup();
+ this.toggle_enable_for_stock_uom("allow_to_edit_stock_uom_qty_for_sales");
this.frm.email_field = "contact_email";
}
diff --git a/erpnext/public/js/utils/unreconcile.js b/erpnext/public/js/utils/unreconcile.js
new file mode 100644
index 0000000..fa00ed2
--- /dev/null
+++ b/erpnext/public/js/utils/unreconcile.js
@@ -0,0 +1,127 @@
+frappe.provide('erpnext.accounts');
+
+erpnext.accounts.unreconcile_payments = {
+ add_unreconcile_btn(frm) {
+ if (frm.doc.docstatus == 1) {
+ if(((frm.doc.doctype == "Journal Entry") && (frm.doc.voucher_type != "Journal Entry"))
+ || !["Purchase Invoice", "Sales Invoice", "Journal Entry", "Payment Entry"].includes(frm.doc.doctype)
+ ) {
+ return;
+ }
+
+ frappe.call({
+ "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.doc_has_references",
+ "args": {
+ "doctype": frm.doc.doctype,
+ "docname": frm.doc.name
+ },
+ callback: function(r) {
+ if (r.message) {
+ frm.add_custom_button(__("UnReconcile"), function() {
+ erpnext.accounts.unreconcile_payments.build_unreconcile_dialog(frm);
+ }, __('Actions'));
+ }
+ }
+ });
+ }
+ },
+
+ build_selection_map(frm, selections) {
+ // assuming each row is an individual voucher
+ // pass this to server side method that creates unreconcile doc for each row
+ let selection_map = [];
+ if (['Sales Invoice', 'Purchase Invoice'].includes(frm.doc.doctype)) {
+ selection_map = selections.map(function(elem) {
+ return {
+ company: elem.company,
+ voucher_type: elem.voucher_type,
+ voucher_no: elem.voucher_no,
+ against_voucher_type: frm.doc.doctype,
+ against_voucher_no: frm.doc.name
+ };
+ });
+ } else if (['Payment Entry', 'Journal Entry'].includes(frm.doc.doctype)) {
+ selection_map = selections.map(function(elem) {
+ return {
+ company: elem.company,
+ voucher_type: frm.doc.doctype,
+ voucher_no: frm.doc.name,
+ against_voucher_type: elem.voucher_type,
+ against_voucher_no: elem.voucher_no,
+ };
+ });
+ }
+ return selection_map;
+ },
+
+ build_unreconcile_dialog(frm) {
+ if (['Sales Invoice', 'Purchase Invoice', 'Payment Entry', 'Journal Entry'].includes(frm.doc.doctype)) {
+ let child_table_fields = [
+ { label: __("Voucher Type"), fieldname: "voucher_type", fieldtype: "Dynamic Link", options: "DocType", in_list_view: 1, read_only: 1},
+ { label: __("Voucher No"), fieldname: "voucher_no", fieldtype: "Link", options: "voucher_type", in_list_view: 1, read_only: 1 },
+ { label: __("Allocated Amount"), fieldname: "allocated_amount", fieldtype: "Currency", in_list_view: 1, read_only: 1 , options: "account_currency"},
+ { label: __("Currency"), fieldname: "account_currency", fieldtype: "Currency", read_only: 1},
+ ]
+ let unreconcile_dialog_fields = [
+ {
+ label: __('Allocations'),
+ fieldname: 'allocations',
+ fieldtype: 'Table',
+ read_only: 1,
+ fields: child_table_fields,
+ },
+ ];
+
+ // get linked payments
+ frappe.call({
+ "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.get_linked_payments_for_doc",
+ "args": {
+ "company": frm.doc.company,
+ "doctype": frm.doc.doctype,
+ "docname": frm.doc.name
+ },
+ callback: function(r) {
+ if (r.message) {
+ // populate child table with allocations
+ unreconcile_dialog_fields[0].data = r.message;
+ unreconcile_dialog_fields[0].get_data = function(){ return r.message};
+
+ let d = new frappe.ui.Dialog({
+ title: 'UnReconcile Allocations',
+ fields: unreconcile_dialog_fields,
+ size: 'large',
+ cannot_add_rows: true,
+ primary_action_label: 'UnReconcile',
+ primary_action(values) {
+
+ let selected_allocations = values.allocations.filter(x=>x.__checked);
+ if (selected_allocations.length > 0) {
+ let selection_map = erpnext.accounts.unreconcile_payments.build_selection_map(frm, selected_allocations);
+ erpnext.accounts.unreconcile_payments.create_unreconcile_docs(selection_map);
+ d.hide();
+
+ } else {
+ frappe.msgprint("No Selection");
+ }
+ }
+ });
+
+ d.show();
+ }
+ }
+ });
+ }
+ },
+
+ create_unreconcile_docs(selection_map) {
+ frappe.call({
+ "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.create_unreconcile_doc_for_selection",
+ "args": {
+ "selections": selection_map
+ },
+ });
+ }
+
+
+
+}
diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json
index be8f62f..0f42def 100644
--- a/erpnext/selling/doctype/customer/customer.json
+++ b/erpnext/selling/doctype/customer/customer.json
@@ -48,12 +48,13 @@
"column_break1",
"contact_html",
"primary_address_and_contact_detail",
- "customer_primary_contact",
- "mobile_no",
- "email_id",
"column_break_26",
"customer_primary_address",
"primary_address",
+ "column_break_nwor",
+ "customer_primary_contact",
+ "mobile_no",
+ "email_id",
"tax_tab",
"taxation_section",
"tax_id",
@@ -339,12 +340,12 @@
"label": "Default Accounts"
},
{
- "description": "Mention if non-standard Receivable account",
- "fieldname": "accounts",
- "fieldtype": "Table",
- "label": "Accounts",
- "options": "Party Account"
- },
+ "description": "Mention if non-standard Receivable account",
+ "fieldname": "accounts",
+ "fieldtype": "Table",
+ "label": "Accounts",
+ "options": "Party Account"
+ },
{
"fieldname": "credit_limit_section",
"fieldtype": "Section Break",
@@ -568,6 +569,10 @@
"fieldtype": "Table",
"label": "Customer Portal Users",
"options": "Portal User"
+ },
+ {
+ "fieldname": "column_break_nwor",
+ "fieldtype": "Column Break"
}
],
"icon": "fa fa-user",
@@ -581,7 +586,7 @@
"link_fieldname": "party"
}
],
- "modified": "2023-06-22 13:21:10.678382",
+ "modified": "2023-09-21 12:23:20.706020",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",
diff --git a/erpnext/selling/doctype/installation_note/installation_note.js b/erpnext/selling/doctype/installation_note/installation_note.js
index dd6f8a8..8128c77 100644
--- a/erpnext/selling/doctype/installation_note/installation_note.js
+++ b/erpnext/selling/doctype/installation_note/installation_note.js
@@ -18,6 +18,14 @@
}
}
});
+ },
+ onload: function(frm) {
+ if(!frm.doc.status) {
+ frm.set_value({ status:'Draft'});
+ }
+ if(frm.doc.__islocal) {
+ frm.set_value({inst_date: frappe.datetime.get_today()});
+ }
let sbb_field = frm.get_docfield('items', 'serial_and_batch_bundle');
if (sbb_field) {
@@ -29,14 +37,6 @@
};
}
},
- onload: function(frm) {
- if(!frm.doc.status) {
- frm.set_value({ status:'Draft'});
- }
- if(frm.doc.__islocal) {
- frm.set_value({inst_date: frappe.datetime.get_today()});
- }
- },
customer: function(frm) {
erpnext.utils.get_party_details(frm);
},
diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js
index d18f70a..1bc8d68 100644
--- a/erpnext/selling/doctype/quotation/quotation.js
+++ b/erpnext/selling/doctype/quotation/quotation.js
@@ -48,6 +48,11 @@
}
}
});
+ },
+
+ refresh: function(frm) {
+ frm.trigger("set_label");
+ frm.trigger("set_dynamic_field_label");
let sbb_field = frm.get_docfield('packed_items', 'serial_and_batch_bundle');
if (sbb_field) {
@@ -61,11 +66,6 @@
}
},
- refresh: function(frm) {
- frm.trigger("set_label");
- frm.trigger("set_dynamic_field_label");
- },
-
quotation_to: function(frm) {
frm.trigger("set_label");
frm.trigger("toggle_reqd_lead_customer");
diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.py b/erpnext/setup/doctype/holiday_list/holiday_list.py
index 526bc2b..df5b407 100644
--- a/erpnext/setup/doctype/holiday_list/holiday_list.py
+++ b/erpnext/setup/doctype/holiday_list/holiday_list.py
@@ -19,6 +19,7 @@
def validate(self):
self.validate_days()
self.total_holidays = len(self.holidays)
+ self.validate_dupliacte_date()
@frappe.whitelist()
def get_weekly_off_dates(self):
@@ -124,6 +125,14 @@
def clear_table(self):
self.set("holidays", [])
+ def validate_dupliacte_date(self):
+ unique_dates = []
+ for row in self.holidays:
+ if row.holiday_date in unique_dates:
+ frappe.throw(_("Holiday Date {0} added multiple times").format(frappe.bold(row.holiday_date)))
+
+ unique_dates.append(row.holiday_date)
+
@frappe.whitelist()
def get_events(start, end, filters=None):
diff --git a/erpnext/setup/page/welcome_to_erpnext/__init__.py b/erpnext/setup/page/welcome_to_erpnext/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/setup/page/welcome_to_erpnext/__init__.py
+++ /dev/null
diff --git a/erpnext/setup/page/welcome_to_erpnext/welcome_to_erpnext.css b/erpnext/setup/page/welcome_to_erpnext/welcome_to_erpnext.css
deleted file mode 100644
index 1fbb459..0000000
--- a/erpnext/setup/page/welcome_to_erpnext/welcome_to_erpnext.css
+++ /dev/null
@@ -1,13 +0,0 @@
-#page-welcome-to-erpnext ul li {
- margin: 7px 0px;
-}
-
-#page-welcome-to-erpnext .video-placeholder-image {
- width: 100%;
- cursor: pointer;
-}
-
-#page-welcome-to-erpnext .youtube-icon {
- width: 10%;
- cursor: pointer;
-}
diff --git a/erpnext/setup/page/welcome_to_erpnext/welcome_to_erpnext.html b/erpnext/setup/page/welcome_to_erpnext/welcome_to_erpnext.html
deleted file mode 100644
index 7166ba3..0000000
--- a/erpnext/setup/page/welcome_to_erpnext/welcome_to_erpnext.html
+++ /dev/null
@@ -1,30 +0,0 @@
-<div class="container welcome-to-erpnext text-center" style="padding: 30px 0px;">
- <div class="row">
- <div class="col-md-8 col-md-push-2 col-sm-12">
- <h1>{%= __("Welcome to ERPNext") %}</h1>
- <p class="text-muted">
- {%= __("To get the best out of ERPNext, we recommend that you take some time and watch these help videos.") %}
- <br><br>
- </p>
-
- <div class="embed-responsive embed-responsive-16by9">
- <div class="video-placeholder embed-responsive-item">
- <img class="video-placeholder-image"
- src="/assets/erpnext/images/erpnext-video-placeholder.jpg">
- <img class="centered youtube-icon"
- src="/assets/erpnext/images/YouTube-icon-full_color.png">
- </div>
- </div>
-
- <br>
- <hr>
- <h3>{%= __("Next Steps") %}</h3>
- <ul class="list-unstyled">
- <li><a class="text-muted" href="#">{%= __("Go to the Desktop and start using ERPNext") %}</a></li>
- <li><a class="text-muted" href="https://erpnext.com/docs/user" target="_blank">{%= __("Read the ERPNext Manual") %}</a></li>
- <li><a class="text-muted" href="https://discuss.erpnext.com" target="_blank">{%= __("Community Forum") %}</a></li>
- </ul>
-
- </div>
- </div>
-</div>
diff --git a/erpnext/setup/page/welcome_to_erpnext/welcome_to_erpnext.js b/erpnext/setup/page/welcome_to_erpnext/welcome_to_erpnext.js
deleted file mode 100644
index f072b8d..0000000
--- a/erpnext/setup/page/welcome_to_erpnext/welcome_to_erpnext.js
+++ /dev/null
@@ -1,20 +0,0 @@
-frappe.pages['welcome-to-erpnext'].on_page_load = function(wrapper) {
- var parent = $('<div class="welcome-to-erpnext"></div>').appendTo(wrapper);
-
- parent.html(frappe.render_template("welcome_to_erpnext", {}));
-
- parent.find(".video-placeholder").on("click", function() {
- window.erpnext_welcome_video_started = true;
- parent.find(".video-placeholder").addClass("hidden");
- parent.find(".embed-responsive").append('<iframe class="embed-responsive-item video-playlist" src="https://www.youtube.com/embed/videoseries?list=PL3lFfCEoMxvxDHtYyQFJeUYkWzQpXwFM9&color=white&autoplay=1&enablejsapi=1" allowfullscreen></iframe>')
- });
-
- // pause video on page change
- $(document).on("page-change", function() {
- if (window.erpnext_welcome_video_started && parent) {
- parent.find(".video-playlist").each(function() {
- this.contentWindow.postMessage('{"event":"command","func":"' + 'pauseVideo' + '","args":""}', '*');
- });
- }
- });
-}
diff --git a/erpnext/setup/page/welcome_to_erpnext/welcome_to_erpnext.json b/erpnext/setup/page/welcome_to_erpnext/welcome_to_erpnext.json
deleted file mode 100644
index 0f532aa..0000000
--- a/erpnext/setup/page/welcome_to_erpnext/welcome_to_erpnext.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "content": null,
- "creation": "2015-10-28 16:27:02.197707",
- "docstatus": 0,
- "doctype": "Page",
- "modified": "2015-10-28 16:27:02.197707",
- "modified_by": "Administrator",
- "module": "Setup",
- "name": "welcome-to-erpnext",
- "owner": "Administrator",
- "page_name": "welcome-to-erpnext",
- "roles": [],
- "script": null,
- "standard": "Yes",
- "style": null,
- "title": "Welcome to ERPNext"
-}
\ No newline at end of file
diff --git a/erpnext/stock/dashboard/item_dashboard.py b/erpnext/stock/dashboard/item_dashboard.py
index 62bd61f..e638268 100644
--- a/erpnext/stock/dashboard/item_dashboard.py
+++ b/erpnext/stock/dashboard/item_dashboard.py
@@ -3,7 +3,7 @@
from frappe.utils import cint, flt
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
- get_sre_reserved_qty_for_item_and_warehouse as get_reserved_stock,
+ get_sre_reserved_qty_for_items_and_warehouses as get_reserved_stock_details,
)
@@ -61,7 +61,10 @@
limit_page_length=21,
)
- sre_reserved_stock_details = get_reserved_stock(item_code, warehouse)
+ item_code_list = [item_code] if item_code else [i.item_code for i in items]
+ warehouse_list = [warehouse] if warehouse else [i.warehouse for i in items]
+
+ sre_reserved_stock_details = get_reserved_stock_details(item_code_list, warehouse_list)
precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
for item in items:
@@ -75,7 +78,8 @@
"reserved_qty_for_production": flt(item.reserved_qty_for_production, precision),
"reserved_qty_for_sub_contract": flt(item.reserved_qty_for_sub_contract, precision),
"actual_qty": flt(item.actual_qty, precision),
- "reserved_stock": sre_reserved_stock_details.get((item.item_code, item.warehouse), 0),
+ "reserved_stock": flt(sre_reserved_stock_details.get((item.item_code, item.warehouse))),
}
)
+
return items
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index 76e8866..4ae9bf5 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -350,18 +350,20 @@
}
}
- frm.fields_dict['deferred_revenue_account'].get_query = function() {
+ frm.fields_dict["item_defaults"].grid.get_field("deferred_revenue_account").get_query = function(doc, cdt, cdn) {
return {
filters: {
+ "company": locals[cdt][cdn].company,
'root_type': 'Liability',
"is_group": 0
}
}
}
- frm.fields_dict['deferred_expense_account'].get_query = function() {
+ frm.fields_dict["item_defaults"].grid.get_field("deferred_expense_account").get_query = function(doc, cdt, cdn) {
return {
filters: {
+ "company": locals[cdt][cdn].company,
'root_type': 'Asset',
"is_group": 0
}
diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json
index 756d004..1bcddfa 100644
--- a/erpnext/stock/doctype/item/item.json
+++ b/erpnext/stock/doctype/item/item.json
@@ -69,6 +69,13 @@
"variant_based_on",
"attributes",
"accounting",
+ "deferred_accounting_section",
+ "enable_deferred_expense",
+ "no_of_months_exp",
+ "column_break_9s9o",
+ "enable_deferred_revenue",
+ "no_of_months",
+ "section_break_avcp",
"item_defaults",
"purchasing_tab",
"purchase_uom",
@@ -84,10 +91,6 @@
"delivered_by_supplier",
"column_break2",
"supplier_items",
- "deferred_expense_section",
- "enable_deferred_expense",
- "deferred_expense_account",
- "no_of_months_exp",
"foreign_trade_details",
"country_of_origin",
"column_break_59",
@@ -98,10 +101,6 @@
"is_sales_item",
"column_break3",
"max_discount",
- "deferred_revenue",
- "enable_deferred_revenue",
- "deferred_revenue_account",
- "no_of_months",
"customer_details",
"customer_items",
"item_tax_section_break",
@@ -658,20 +657,6 @@
"oldfieldtype": "Currency"
},
{
- "collapsible": 1,
- "fieldname": "deferred_revenue",
- "fieldtype": "Section Break",
- "label": "Deferred Revenue"
- },
- {
- "depends_on": "enable_deferred_revenue",
- "fieldname": "deferred_revenue_account",
- "fieldtype": "Link",
- "ignore_user_permissions": 1,
- "label": "Deferred Revenue Account",
- "options": "Account"
- },
- {
"default": "0",
"fieldname": "enable_deferred_revenue",
"fieldtype": "Check",
@@ -681,21 +666,7 @@
"depends_on": "enable_deferred_revenue",
"fieldname": "no_of_months",
"fieldtype": "Int",
- "label": "No of Months"
- },
- {
- "collapsible": 1,
- "fieldname": "deferred_expense_section",
- "fieldtype": "Section Break",
- "label": "Deferred Expense"
- },
- {
- "depends_on": "enable_deferred_expense",
- "fieldname": "deferred_expense_account",
- "fieldtype": "Link",
- "ignore_user_permissions": 1,
- "label": "Deferred Expense Account",
- "options": "Account"
+ "label": "No of Months (Revenue)"
},
{
"default": "0",
@@ -904,6 +875,20 @@
"fieldname": "accounting",
"fieldtype": "Tab Break",
"label": "Accounting"
+ },
+ {
+ "fieldname": "column_break_9s9o",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_avcp",
+ "fieldtype": "Section Break"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "deferred_accounting_section",
+ "fieldtype": "Section Break",
+ "label": "Deferred Accounting"
}
],
"icon": "fa fa-tag",
@@ -912,7 +897,7 @@
"index_web_pages_for_search": 1,
"links": [],
"make_attachments_public": 1,
- "modified": "2023-08-28 22:16:40.305094",
+ "modified": "2023-09-11 13:46:32.688051",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",
diff --git a/erpnext/stock/doctype/item_default/item_default.json b/erpnext/stock/doctype/item_default/item_default.json
index 042d398..2895661 100644
--- a/erpnext/stock/doctype/item_default/item_default.json
+++ b/erpnext/stock/doctype/item_default/item_default.json
@@ -19,7 +19,11 @@
"selling_defaults",
"selling_cost_center",
"column_break_12",
- "income_account"
+ "income_account",
+ "deferred_accounting_defaults_section",
+ "deferred_expense_account",
+ "column_break_kwad",
+ "deferred_revenue_account"
],
"fields": [
{
@@ -108,11 +112,34 @@
"fieldtype": "Link",
"label": "Default Provisional Account",
"options": "Account"
+ },
+ {
+ "fieldname": "deferred_accounting_defaults_section",
+ "fieldtype": "Section Break",
+ "label": "Deferred Accounting Defaults"
+ },
+ {
+ "depends_on": "eval: parent.enable_deferred_expense",
+ "fieldname": "deferred_expense_account",
+ "fieldtype": "Link",
+ "label": "Deferred Expense Account",
+ "options": "Account"
+ },
+ {
+ "depends_on": "eval: parent.enable_deferred_revenue",
+ "fieldname": "deferred_revenue_account",
+ "fieldtype": "Link",
+ "label": "Deferred Revenue Account",
+ "options": "Account"
+ },
+ {
+ "fieldname": "column_break_kwad",
+ "fieldtype": "Column Break"
}
],
"istable": 1,
"links": [],
- "modified": "2022-04-10 20:18:54.148195",
+ "modified": "2023-09-04 12:33:14.607267",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Default",
diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js
index 989bfd0..bf3301f 100644
--- a/erpnext/stock/doctype/material_request/material_request.js
+++ b/erpnext/stock/doctype/material_request/material_request.js
@@ -218,7 +218,8 @@
plc_conversion_rate: 1,
rate: item.rate,
uom: item.uom,
- conversion_factor: item.conversion_factor
+ conversion_factor: item.conversion_factor,
+ project: item.project,
},
overwrite_warehouse: overwrite_warehouse
},
diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json
index ffec57c..25c765b 100644
--- a/erpnext/stock/doctype/material_request/material_request.json
+++ b/erpnext/stock/doctype/material_request/material_request.json
@@ -296,6 +296,7 @@
"depends_on": "eval:doc.material_request_type == 'Material Transfer'",
"fieldname": "set_from_warehouse",
"fieldtype": "Link",
+ "ignore_user_permissions": 1,
"label": "Set Source Warehouse",
"options": "Warehouse"
},
@@ -356,7 +357,7 @@
"idx": 70,
"is_submittable": 1,
"links": [],
- "modified": "2023-07-25 17:19:31.662662",
+ "modified": "2023-09-15 12:07:24.789471",
"modified_by": "Administrator",
"module": "Stock",
"name": "Material Request",
diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js
index 4eed285..ae05b80 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.js
+++ b/erpnext/stock/doctype/pick_list/pick_list.js
@@ -65,17 +65,6 @@
}
}
});
-
- let sbb_field = frm.get_docfield('locations', 'serial_and_batch_bundle');
- if (sbb_field) {
- sbb_field.get_route_options_for_new_doc = (row) => {
- return {
- 'item_code': row.doc.item_code,
- 'warehouse': row.doc.warehouse,
- 'voucher_type': frm.doc.doctype,
- }
- };
- }
},
set_item_locations:(frm, save) => {
if (!(frm.doc.locations && frm.doc.locations.length)) {
@@ -132,6 +121,17 @@
}
}
}
+
+ let sbb_field = frm.get_docfield('locations', 'serial_and_batch_bundle');
+ if (sbb_field) {
+ sbb_field.get_route_options_for_new_doc = (row) => {
+ return {
+ 'item_code': row.doc.item_code,
+ 'warehouse': row.doc.warehouse,
+ 'voucher_type': frm.doc.doctype,
+ }
+ };
+ }
},
work_order: (frm) => {
frappe.db.get_value('Work Order',
diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
index d31fec5..5eb3656 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -34,8 +34,8 @@
"sample_quantity",
"tracking_section",
"received_stock_qty",
- "stock_qty",
"col_break_tracking_section",
+ "stock_qty",
"returned_qty",
"rate_and_amount",
"price_list_rate",
@@ -858,7 +858,8 @@
},
{
"fieldname": "tracking_section",
- "fieldtype": "Section Break"
+ "fieldtype": "Section Break",
+ "label": "Qty as Per Stock UOM"
},
{
"fieldname": "col_break_tracking_section",
@@ -1060,7 +1061,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2023-07-26 12:55:15.234477",
+ "modified": "2023-08-11 16:16:16.504549",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index 4fb8a10..d37e8ee 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -117,15 +117,6 @@
}
});
- let sbb_field = frm.get_docfield('items', 'serial_and_batch_bundle');
- if (sbb_field) {
- sbb_field.get_route_options_for_new_doc = (row) => {
- return {
- 'item_code': row.doc.item_code,
- 'voucher_type': frm.doc.doctype,
- }
- };
- }
frm.add_fetch("bom_no", "inspection_required", "inspection_required");
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
@@ -362,6 +353,16 @@
if(!check_should_not_attach_bom_items(frm.doc.bom_no)) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
}
+
+ let sbb_field = frm.get_docfield('items', 'serial_and_batch_bundle');
+ if (sbb_field) {
+ sbb_field.get_route_options_for_new_doc = (row) => {
+ return {
+ 'item_code': row.doc.item_code,
+ 'voucher_type': frm.doc.doctype,
+ }
+ };
+ }
},
get_items_from_transit_entry: function(frm) {
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 26ca012..e36d576 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -346,7 +346,7 @@
"""Raises an exception if there is any reserved stock for the items in the Stock Reconciliation."""
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
- get_sre_reserved_qty_for_item_and_warehouse as get_sre_reserved_qty_details,
+ get_sre_reserved_qty_for_items_and_warehouses as get_sre_reserved_qty_details,
)
item_code_list, warehouse_list = [], []
diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.js b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.js
index 4d96636..c5df319 100644
--- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.js
+++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.js
@@ -1,42 +1,42 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-frappe.ui.form.on("Stock Reservation Entry", {
+frappe.ui.form.on('Stock Reservation Entry', {
refresh(frm) {
- frm.trigger("set_queries");
- frm.trigger("toggle_read_only_fields");
- frm.trigger("hide_rate_related_fields");
- frm.trigger("hide_primary_action_button");
- frm.trigger("make_sb_entries_warehouse_read_only");
+ frm.trigger('set_queries');
+ frm.trigger('toggle_read_only_fields');
+ frm.trigger('hide_rate_related_fields');
+ frm.trigger('hide_primary_action_button');
+ frm.trigger('make_sb_entries_warehouse_read_only');
},
has_serial_no(frm) {
- frm.trigger("toggle_read_only_fields");
+ frm.trigger('toggle_read_only_fields');
},
has_batch_no(frm) {
- frm.trigger("toggle_read_only_fields");
+ frm.trigger('toggle_read_only_fields');
},
warehouse(frm) {
if (frm.doc.warehouse) {
frm.doc.sb_entries.forEach((row) => {
- frappe.model.set_value(row.doctype, row.name, "warehouse", frm.doc.warehouse);
+ frappe.model.set_value(row.doctype, row.name, 'warehouse', frm.doc.warehouse);
});
}
},
set_queries(frm) {
- frm.set_query("warehouse", () => {
+ frm.set_query('warehouse', () => {
return {
filters: {
- "is_group": 0,
- "company": frm.doc.company,
+ 'is_group': 0,
+ 'company': frm.doc.company,
}
};
});
- frm.set_query("serial_no", "sb_entries", function(doc, cdt, cdn) {
+ frm.set_query('serial_no', 'sb_entries', function(doc, cdt, cdn) {
var selected_serial_nos = doc.sb_entries.map(row => {
return row.serial_no;
});
@@ -45,16 +45,16 @@
filters: {
item_code: doc.item_code,
warehouse: row.warehouse,
- status: "Active",
- name: ["not in", selected_serial_nos],
+ status: 'Active',
+ name: ['not in', selected_serial_nos],
}
}
});
- frm.set_query("batch_no", "sb_entries", function(doc, cdt, cdn) {
+ frm.set_query('batch_no', 'sb_entries', function(doc, cdt, cdn) {
let filters = {
item: doc.item_code,
- batch_qty: [">", 0],
+ batch_qty: ['>', 0],
disabled: 0,
}
@@ -63,7 +63,7 @@
return row.batch_no;
});
- filters.name = ["not in", selected_batch_nos];
+ filters.name = ['not in', selected_batch_nos];
}
return { filters: filters }
@@ -74,37 +74,37 @@
if (frm.doc.has_serial_no) {
frm.doc.sb_entries.forEach(row => {
if (row.qty !== 1) {
- frappe.model.set_value(row.doctype, row.name, "qty", 1);
+ frappe.model.set_value(row.doctype, row.name, 'qty', 1);
}
})
}
frm.fields_dict.sb_entries.grid.update_docfield_property(
- "serial_no", "read_only", !frm.doc.has_serial_no
+ 'serial_no', 'read_only', !frm.doc.has_serial_no
);
frm.fields_dict.sb_entries.grid.update_docfield_property(
- "batch_no", "read_only", !frm.doc.has_batch_no
+ 'batch_no', 'read_only', !frm.doc.has_batch_no
);
// Qty will always be 1 for Serial No.
frm.fields_dict.sb_entries.grid.update_docfield_property(
- "qty", "read_only", frm.doc.has_serial_no
+ 'qty', 'read_only', frm.doc.has_serial_no
);
- frm.set_df_property("sb_entries", "allow_on_submit", frm.doc.against_pick_list ? 0 : 1);
+ frm.set_df_property('sb_entries', 'allow_on_submit', frm.doc.against_pick_list ? 0 : 1);
},
hide_rate_related_fields(frm) {
- ["incoming_rate", "outgoing_rate", "stock_value_difference", "is_outward", "stock_queue"].forEach(field => {
+ ['incoming_rate', 'outgoing_rate', 'stock_value_difference', 'is_outward', 'stock_queue'].forEach(field => {
frm.fields_dict.sb_entries.grid.update_docfield_property(
- field, "hidden", 1
+ field, 'hidden', 1
);
});
},
hide_primary_action_button(frm) {
- // Hide "Amend" button on cancelled document
+ // Hide 'Amend' button on cancelled document
if (frm.doc.docstatus == 2) {
frm.page.btn_primary.hide()
}
@@ -112,15 +112,15 @@
make_sb_entries_warehouse_read_only(frm) {
frm.fields_dict.sb_entries.grid.update_docfield_property(
- "warehouse", "read_only", 1
+ 'warehouse', 'read_only', 1
);
},
});
-frappe.ui.form.on("Serial and Batch Entry", {
+frappe.ui.form.on('Serial and Batch Entry', {
sb_entries_add(frm, cdt, cdn) {
if (frm.doc.warehouse) {
- frappe.model.set_value(cdt, cdn, "warehouse", frm.doc.warehouse);
+ frappe.model.set_value(cdt, cdn, 'warehouse', frm.doc.warehouse);
}
},
});
\ No newline at end of file
diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
index bd7bb66..936be3f 100644
--- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
+++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
@@ -14,7 +14,7 @@
self.validate_amended_doc()
self.validate_mandatory()
- self.validate_for_group_warehouse()
+ self.validate_group_warehouse()
validate_disabled_warehouse(self.warehouse)
validate_warehouse_company(self.warehouse, self.company)
self.validate_uom_is_integer()
@@ -74,7 +74,7 @@
msg = _("{0} is required").format(self.meta.get_label(d))
frappe.throw(msg)
- def validate_for_group_warehouse(self) -> None:
+ def validate_group_warehouse(self) -> None:
"""Raises an exception if `Warehouse` is a Group Warehouse."""
if frappe.get_cached_value("Warehouse", self.warehouse, "is_group"):
@@ -544,10 +544,36 @@
return available_serial_nos_list
-def get_sre_reserved_qty_for_item_and_warehouse(
- item_code: str | list, warehouse: str | list = None
-) -> float | dict:
- """Returns `Reserved Qty` for Item and Warehouse combination OR a dict like {("item_code", "warehouse"): "reserved_qty", ... }."""
+def get_sre_reserved_qty_for_item_and_warehouse(item_code: str, warehouse: str = None) -> float:
+ """Returns current `Reserved Qty` for Item and Warehouse combination."""
+
+ sre = frappe.qb.DocType("Stock Reservation Entry")
+ query = (
+ frappe.qb.from_(sre)
+ .select(Sum(sre.reserved_qty - sre.delivered_qty).as_("reserved_qty"))
+ .where(
+ (sre.docstatus == 1)
+ & (sre.item_code == item_code)
+ & (sre.status.notin(["Delivered", "Cancelled"]))
+ )
+ .groupby(sre.item_code, sre.warehouse)
+ )
+
+ if warehouse:
+ query = query.where(sre.warehouse == warehouse)
+
+ reserved_qty = query.run(as_list=True)
+
+ return flt(reserved_qty[0][0]) if reserved_qty else 0.0
+
+
+def get_sre_reserved_qty_for_items_and_warehouses(
+ item_code_list: list, warehouse_list: list = None
+) -> dict:
+ """Returns a dict like {("item_code", "warehouse"): "reserved_qty", ... }."""
+
+ if not item_code_list:
+ return {}
sre = frappe.qb.DocType("Stock Reservation Entry")
query = (
@@ -557,29 +583,20 @@
sre.warehouse,
Sum(sre.reserved_qty - sre.delivered_qty).as_("reserved_qty"),
)
- .where((sre.docstatus == 1) & (sre.status.notin(["Delivered", "Cancelled"])))
+ .where(
+ (sre.docstatus == 1)
+ & sre.item_code.isin(item_code_list)
+ & (sre.status.notin(["Delivered", "Cancelled"]))
+ )
.groupby(sre.item_code, sre.warehouse)
)
- query = (
- query.where(sre.item_code.isin(item_code))
- if isinstance(item_code, list)
- else query.where(sre.item_code == item_code)
- )
-
- if warehouse:
- query = (
- query.where(sre.warehouse.isin(warehouse))
- if isinstance(warehouse, list)
- else query.where(sre.warehouse == warehouse)
- )
+ if warehouse_list:
+ query = query.where(sre.warehouse.isin(warehouse_list))
data = query.run(as_dict=True)
- if isinstance(item_code, str) and isinstance(warehouse, str):
- return data[0]["reserved_qty"] if data else 0.0
- else:
- return {(d["item_code"], d["warehouse"]): d["reserved_qty"] for d in data} if data else {}
+ return {(d["item_code"], d["warehouse"]): d["reserved_qty"] for d in data} if data else {}
def get_sre_reserved_qty_details_for_voucher(voucher_type: str, voucher_no: str) -> dict:
@@ -711,7 +728,7 @@
).run(as_dict=True)
-def get_ssb_bundle_for_voucher(sre: dict) -> object | None:
+def get_ssb_bundle_for_voucher(sre: dict) -> object:
"""Returns a new `Serial and Batch Bundle` against the provided SRE."""
sb_entries = get_serial_batch_entries_for_voucher(sre["name"])
diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry_list.js b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry_list.js
index 442ac39..5b390f7 100644
--- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry_list.js
+++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry_list.js
@@ -4,13 +4,14 @@
frappe.listview_settings['Stock Reservation Entry'] = {
get_indicator: function (doc) {
const status_colors = {
- 'Draft': 'red',
- 'Partially Reserved': 'orange',
- 'Reserved': 'blue',
- 'Partially Delivered': 'purple',
- 'Delivered': 'green',
- 'Cancelled': 'red',
+ 'Draft': 'red',
+ 'Partially Reserved': 'orange',
+ 'Reserved': 'blue',
+ 'Partially Delivered': 'purple',
+ 'Delivered': 'green',
+ 'Cancelled': 'red',
};
- return [__(doc.status), status_colors[doc.status], 'status,=,' + doc.status];
+
+ return [__(doc.status), status_colors[doc.status], 'status,=,' + doc.status];
},
};
\ No newline at end of file
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json
index 88b5575..4fbc0eb 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.json
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.json
@@ -18,6 +18,10 @@
"auto_insert_price_list_rate_if_missing",
"column_break_12",
"update_existing_price_list_rate",
+ "conversion_factor_section",
+ "allow_to_edit_stock_uom_qty_for_sales",
+ "column_break_lznj",
+ "allow_to_edit_stock_uom_qty_for_purchase",
"stock_validations_tab",
"section_break_9",
"over_delivery_receipt_allowance",
@@ -358,10 +362,6 @@
"label": "Allow Partial Reservation"
},
{
- "fieldname": "section_break_plhx",
- "fieldtype": "Section Break"
- },
- {
"fieldname": "column_break_mhzc",
"fieldtype": "Column Break"
},
@@ -400,6 +400,27 @@
"fieldname": "auto_reserve_stock_for_sales_order",
"fieldtype": "Check",
"label": "Auto Reserve Stock for Sales Order"
+ },
+ {
+ "fieldname": "conversion_factor_section",
+ "fieldtype": "Section Break",
+ "label": "Stock UOM Quantity"
+ },
+ {
+ "fieldname": "column_break_lznj",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_to_edit_stock_uom_qty_for_sales",
+ "fieldtype": "Check",
+ "label": "Allow to Edit Stock UOM Qty for Sales Documents"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_to_edit_stock_uom_qty_for_purchase",
+ "fieldtype": "Check",
+ "label": "Allow to Edit Stock UOM Qty for Purchase Documents"
}
],
"icon": "icon-cog",
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py
index 9ad3c9d..c7afb10 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.py
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.py
@@ -56,6 +56,8 @@
self.validate_clean_description_html()
self.validate_pending_reposts()
self.validate_stock_reservation()
+ self.change_precision_for_for_sales()
+ self.change_precision_for_purchase()
def validate_warehouses(self):
warehouse_fields = ["default_warehouse", "sample_retention_warehouse"]
@@ -167,6 +169,56 @@
def on_update(self):
self.toggle_warehouse_field_for_inter_warehouse_transfer()
+ def change_precision_for_for_sales(self):
+ doc_before_save = self.get_doc_before_save()
+ if doc_before_save and (
+ doc_before_save.allow_to_edit_stock_uom_qty_for_sales
+ == self.allow_to_edit_stock_uom_qty_for_sales
+ ):
+ return
+
+ if self.allow_to_edit_stock_uom_qty_for_sales:
+ doctypes = ["Sales Order Item", "Sales Invoice Item", "Delivery Note Item", "Quotation Item"]
+ self.make_property_setter_for_precision(doctypes)
+
+ def change_precision_for_purchase(self):
+ doc_before_save = self.get_doc_before_save()
+ if doc_before_save and (
+ doc_before_save.allow_to_edit_stock_uom_qty_for_purchase
+ == self.allow_to_edit_stock_uom_qty_for_purchase
+ ):
+ return
+
+ if self.allow_to_edit_stock_uom_qty_for_purchase:
+ doctypes = [
+ "Purchase Order Item",
+ "Purchase Receipt Item",
+ "Purchase Invoice Item",
+ "Request for Quotation Item",
+ "Supplier Quotation Item",
+ "Material Request Item",
+ ]
+ self.make_property_setter_for_precision(doctypes)
+
+ @staticmethod
+ def make_property_setter_for_precision(doctypes):
+ for doctype in doctypes:
+ if property_name := frappe.db.exists(
+ "Property Setter",
+ {"doc_type": doctype, "field_name": "conversion_factor", "property": "precision"},
+ ):
+ frappe.db.set_value("Property Setter", property_name, "value", 9)
+ continue
+
+ make_property_setter(
+ doctype,
+ "conversion_factor",
+ "precision",
+ 9,
+ "Float",
+ validate_fields_for_doctype=False,
+ )
+
def toggle_warehouse_field_for_inter_warehouse_transfer(self):
make_property_setter(
"Sales Invoice Item",
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 4f85ac0..a6ab63b 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -696,7 +696,11 @@
def get_default_deferred_account(args, item, fieldname=None):
if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"):
return (
- item.get(fieldname)
+ frappe.get_cached_value(
+ "Item Default",
+ {"parent": args.item_code, "company": args.get("company")},
+ fieldname,
+ )
or args.get(fieldname)
or frappe.get_cached_value("Company", args.company, "default_" + fieldname)
)
@@ -1288,6 +1292,9 @@
@frappe.whitelist()
def get_valuation_rate(item_code, company, warehouse=None):
+ if frappe.get_cached_value("Warehouse", warehouse, "is_group"):
+ return {"valuation_rate": 0.0}
+
item = get_item_defaults(item_code, company)
item_group = get_item_group_defaults(item_code, company)
brand = get_brand_defaults(item_code, company)
diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py
index 337b0ea..a59f9de 100644
--- a/erpnext/stock/report/stock_balance/stock_balance.py
+++ b/erpnext/stock/report/stock_balance/stock_balance.py
@@ -165,7 +165,7 @@
def get_sre_reserved_qty_details(self) -> dict:
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
- get_sre_reserved_qty_for_item_and_warehouse as get_reserved_qty_details,
+ get_sre_reserved_qty_for_items_and_warehouses as get_reserved_qty_details,
)
item_code_list, warehouse_list = [], []
diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js
index 3447e0a..3f67bff 100644
--- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js
+++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js
@@ -2,24 +2,24 @@
// For license information, please see license.txt
-const DIFFERNCE_FIELD_NAMES = [
- "difference_in_qty",
- "fifo_qty_diff",
- "fifo_value_diff",
- "fifo_valuation_diff",
- "valuation_diff",
- "fifo_difference_diff",
- "diff_value_diff"
+const DIFFERENCE_FIELD_NAMES = [
+ 'difference_in_qty',
+ 'fifo_qty_diff',
+ 'fifo_value_diff',
+ 'fifo_valuation_diff',
+ 'valuation_diff',
+ 'fifo_difference_diff',
+ 'diff_value_diff'
];
-frappe.query_reports["Stock Ledger Invariant Check"] = {
- "filters": [
+frappe.query_reports['Stock Ledger Invariant Check'] = {
+ 'filters': [
{
- "fieldname": "item_code",
- "fieldtype": "Link",
- "label": "Item",
- "mandatory": 1,
- "options": "Item",
+ 'fieldname': 'item_code',
+ 'fieldtype': 'Link',
+ 'label': 'Item',
+ 'mandatory': 1,
+ 'options': 'Item',
get_query: function() {
return {
filters: {is_stock_item: 1, has_serial_no: 0}
@@ -27,18 +27,61 @@
}
},
{
- "fieldname": "warehouse",
- "fieldtype": "Link",
- "label": "Warehouse",
- "mandatory": 1,
- "options": "Warehouse",
+ 'fieldname': 'warehouse',
+ 'fieldtype': 'Link',
+ 'label': 'Warehouse',
+ 'mandatory': 1,
+ 'options': 'Warehouse',
}
],
+
formatter (value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
- if (DIFFERNCE_FIELD_NAMES.includes(column.fieldname) && Math.abs(data[column.fieldname]) > 0.001) {
- value = "<span style='color:red'>" + value + "</span>";
+ if (DIFFERENCE_FIELD_NAMES.includes(column.fieldname) && Math.abs(data[column.fieldname]) > 0.001) {
+ value = '<span style="color:red">' + value + '</span>';
}
return value;
},
+
+ get_datatable_options(options) {
+ return Object.assign(options, {
+ checkboxColumn: true,
+ });
+ },
+
+ onload(report) {
+ report.page.add_inner_button(__('Create Reposting Entry'), () => {
+ let message = `
+ <div>
+ <p>
+ Reposting Entry will change the value of
+ accounts Stock In Hand, and Stock Expenses
+ in the Trial Balance report and will also change
+ the Balance Value in the Stock Balance report.
+ </p>
+ <p>Are you sure you want to create a Reposting Entry?</p>
+ </div>`;
+ let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows();
+ let selected_rows = indexes.map(i => frappe.query_report.data[i]);
+
+ if (!selected_rows.length) {
+ frappe.throw(__('Please select a row to create a Reposting Entry'));
+ }
+ else if (selected_rows.length > 1) {
+ frappe.throw(__('Please select only one row to create a Reposting Entry'));
+ }
+ else {
+ frappe.confirm(__(message), () => {
+ frappe.call({
+ method: 'erpnext.stock.report.stock_ledger_invariant_check.stock_ledger_invariant_check.create_reposting_entries',
+ args: {
+ rows: selected_rows,
+ item_code: frappe.query_report.get_filter_values().item_code,
+ warehouse: frappe.query_report.get_filter_values().warehouse,
+ }
+ });
+ });
+ }
+ });
+ },
};
diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
index ed0e2fc..ca15afe 100644
--- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
+++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
@@ -5,6 +5,7 @@
import frappe
from frappe import _
+from frappe.utils import get_link_to_form, parse_json
SLE_FIELDS = (
"name",
@@ -185,7 +186,7 @@
{
"fieldname": "fifo_queue_qty",
"fieldtype": "Float",
- "label": _("(C) Total qty in queue"),
+ "label": _("(C) Total Qty in Queue"),
},
{
"fieldname": "fifo_qty_diff",
@@ -210,51 +211,83 @@
{
"fieldname": "stock_value_difference",
"fieldtype": "Float",
- "label": _("(F) Stock Value Difference"),
+ "label": _("(F) Change in Stock Value"),
},
{
"fieldname": "stock_value_from_diff",
"fieldtype": "Float",
- "label": _("Balance Stock Value using (F)"),
+ "label": _("(G) Sum of Change in Stock Value"),
},
{
"fieldname": "diff_value_diff",
"fieldtype": "Float",
- "label": _("K - D"),
+ "label": _("G - D"),
},
{
"fieldname": "fifo_stock_diff",
"fieldtype": "Float",
- "label": _("(G) Stock Value difference (FIFO queue)"),
+ "label": _("(H) Change in Stock Value (FIFO Queue)"),
},
{
"fieldname": "fifo_difference_diff",
"fieldtype": "Float",
- "label": _("F - G"),
+ "label": _("H - F"),
},
{
"fieldname": "valuation_rate",
"fieldtype": "Float",
- "label": _("(H) Valuation Rate"),
+ "label": _("(I) Valuation Rate"),
},
{
"fieldname": "fifo_valuation_rate",
"fieldtype": "Float",
- "label": _("(I) Valuation Rate as per FIFO"),
+ "label": _("(J) Valuation Rate as per FIFO"),
},
{
"fieldname": "fifo_valuation_diff",
"fieldtype": "Float",
- "label": _("H - I"),
+ "label": _("I - J"),
},
{
"fieldname": "balance_value_by_qty",
"fieldtype": "Float",
- "label": _("(J) Valuation = Value (D) ÷ Qty (A)"),
+ "label": _("(K) Valuation = Value (D) ÷ Qty (A)"),
},
{
"fieldname": "valuation_diff",
"fieldtype": "Float",
- "label": _("H - J"),
+ "label": _("I - K"),
},
]
+
+
+@frappe.whitelist()
+def create_reposting_entries(rows, item_code=None, warehouse=None):
+ if isinstance(rows, str):
+ rows = parse_json(rows)
+
+ entries = []
+ for row in rows:
+ row = frappe._dict(row)
+
+ try:
+ doc = frappe.get_doc(
+ {
+ "doctype": "Repost Item Valuation",
+ "based_on": "Item and Warehouse",
+ "status": "Queued",
+ "item_code": item_code or row.item_code,
+ "warehouse": warehouse or row.warehouse,
+ "posting_date": row.posting_date,
+ "posting_time": row.posting_time,
+ "allow_nagative_stock": 1,
+ }
+ ).submit()
+
+ entries.append(get_link_to_form("Repost Item Valuation", doc.name))
+ except frappe.DuplicateEntryError:
+ continue
+
+ if entries:
+ entries = ", ".join(entries)
+ frappe.msgprint(_("Reposting entries created: {0}").format(entries))
diff --git a/erpnext/crm/doctype/linkedin_settings/__init__.py b/erpnext/stock/report/stock_ledger_variance/__init__.py
similarity index 100%
copy from erpnext/crm/doctype/linkedin_settings/__init__.py
copy to erpnext/stock/report/stock_ledger_variance/__init__.py
diff --git a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js
new file mode 100644
index 0000000..b1e4a74
--- /dev/null
+++ b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js
@@ -0,0 +1,101 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+const DIFFERENCE_FIELD_NAMES = [
+ "difference_in_qty",
+ "fifo_qty_diff",
+ "fifo_value_diff",
+ "fifo_valuation_diff",
+ "valuation_diff",
+ "fifo_difference_diff",
+ "diff_value_diff"
+];
+
+frappe.query_reports["Stock Ledger Variance"] = {
+ "filters": [
+ {
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "label": "Item",
+ "options": "Item",
+ get_query: function() {
+ return {
+ filters: {is_stock_item: 1, has_serial_no: 0}
+ }
+ }
+ },
+ {
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "label": "Warehouse",
+ "options": "Warehouse",
+ get_query: function() {
+ return {
+ filters: {is_group: 0, disabled: 0}
+ }
+ }
+ },
+ {
+ "fieldname": "difference_in",
+ "fieldtype": "Select",
+ "label": "Difference In",
+ "options": [
+ "",
+ "Qty",
+ "Value",
+ "Valuation",
+ ],
+ },
+ {
+ "fieldname": "include_disabled",
+ "fieldtype": "Check",
+ "label": "Include Disabled",
+ }
+ ],
+
+ formatter (value, row, column, data, default_formatter) {
+ value = default_formatter(value, row, column, data);
+
+ if (DIFFERENCE_FIELD_NAMES.includes(column.fieldname) && Math.abs(data[column.fieldname]) > 0.001) {
+ value = "<span style='color:red'>" + value + "</span>";
+ }
+
+ return value;
+ },
+
+ get_datatable_options(options) {
+ return Object.assign(options, {
+ checkboxColumn: true,
+ });
+ },
+
+ onload(report) {
+ report.page.add_inner_button(__('Create Reposting Entries'), () => {
+ let message = `
+ <div>
+ <p>
+ Reposting Entries will change the value of
+ accounts Stock In Hand, and Stock Expenses
+ in the Trial Balance report and will also change
+ the Balance Value in the Stock Balance report.
+ </p>
+ <p>Are you sure you want to create Reposting Entries?</p>
+ </div>`;
+ let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows();
+ let selected_rows = indexes.map(i => frappe.query_report.data[i]);
+
+ if (!selected_rows.length) {
+ frappe.throw(__("Please select rows to create Reposting Entries"));
+ }
+
+ frappe.confirm(__(message), () => {
+ frappe.call({
+ method: 'erpnext.stock.report.stock_ledger_invariant_check.stock_ledger_invariant_check.create_reposting_entries',
+ args: {
+ rows: selected_rows,
+ }
+ });
+ });
+ });
+ },
+};
diff --git a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.json b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.json
new file mode 100644
index 0000000..f36ed1b
--- /dev/null
+++ b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.json
@@ -0,0 +1,22 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2023-09-20 10:44:19.414449",
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "letterhead": null,
+ "modified": "2023-09-20 10:44:19.414449",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Stock Ledger Variance",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Stock Ledger Entry",
+ "report_name": "Stock Ledger Variance",
+ "report_type": "Script Report",
+ "roles": []
+}
\ No newline at end of file
diff --git a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py
new file mode 100644
index 0000000..732f108
--- /dev/null
+++ b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py
@@ -0,0 +1,279 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _
+from frappe.utils import cint, flt
+
+from erpnext.stock.report.stock_ledger_invariant_check.stock_ledger_invariant_check import (
+ get_data as stock_ledger_invariant_check,
+)
+
+
+def execute(filters=None):
+ columns, data = [], []
+
+ filters = frappe._dict(filters or {})
+ columns = get_columns()
+ data = get_data(filters)
+
+ return columns, data
+
+
+def get_columns():
+ return [
+ {
+ "fieldname": "name",
+ "fieldtype": "Link",
+ "label": _("Stock Ledger Entry"),
+ "options": "Stock Ledger Entry",
+ },
+ {
+ "fieldname": "posting_date",
+ "fieldtype": "Data",
+ "label": _("Posting Date"),
+ },
+ {
+ "fieldname": "posting_time",
+ "fieldtype": "Data",
+ "label": _("Posting Time"),
+ },
+ {
+ "fieldname": "creation",
+ "fieldtype": "Data",
+ "label": _("Creation"),
+ },
+ {
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "label": _("Item"),
+ "options": "Item",
+ },
+ {
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "label": _("Warehouse"),
+ "options": "Warehouse",
+ },
+ {
+ "fieldname": "voucher_type",
+ "fieldtype": "Link",
+ "label": _("Voucher Type"),
+ "options": "DocType",
+ },
+ {
+ "fieldname": "voucher_no",
+ "fieldtype": "Dynamic Link",
+ "label": _("Voucher No"),
+ "options": "voucher_type",
+ },
+ {
+ "fieldname": "batch_no",
+ "fieldtype": "Link",
+ "label": _("Batch"),
+ "options": "Batch",
+ },
+ {
+ "fieldname": "use_batchwise_valuation",
+ "fieldtype": "Check",
+ "label": _("Batchwise Valuation"),
+ },
+ {
+ "fieldname": "actual_qty",
+ "fieldtype": "Float",
+ "label": _("Qty Change"),
+ },
+ {
+ "fieldname": "incoming_rate",
+ "fieldtype": "Float",
+ "label": _("Incoming Rate"),
+ },
+ {
+ "fieldname": "consumption_rate",
+ "fieldtype": "Float",
+ "label": _("Consumption Rate"),
+ },
+ {
+ "fieldname": "qty_after_transaction",
+ "fieldtype": "Float",
+ "label": _("(A) Qty After Transaction"),
+ },
+ {
+ "fieldname": "expected_qty_after_transaction",
+ "fieldtype": "Float",
+ "label": _("(B) Expected Qty After Transaction"),
+ },
+ {
+ "fieldname": "difference_in_qty",
+ "fieldtype": "Float",
+ "label": _("A - B"),
+ },
+ {
+ "fieldname": "stock_queue",
+ "fieldtype": "Data",
+ "label": _("FIFO/LIFO Queue"),
+ },
+ {
+ "fieldname": "fifo_queue_qty",
+ "fieldtype": "Float",
+ "label": _("(C) Total Qty in Queue"),
+ },
+ {
+ "fieldname": "fifo_qty_diff",
+ "fieldtype": "Float",
+ "label": _("A - C"),
+ },
+ {
+ "fieldname": "stock_value",
+ "fieldtype": "Float",
+ "label": _("(D) Balance Stock Value"),
+ },
+ {
+ "fieldname": "fifo_stock_value",
+ "fieldtype": "Float",
+ "label": _("(E) Balance Stock Value in Queue"),
+ },
+ {
+ "fieldname": "fifo_value_diff",
+ "fieldtype": "Float",
+ "label": _("D - E"),
+ },
+ {
+ "fieldname": "stock_value_difference",
+ "fieldtype": "Float",
+ "label": _("(F) Change in Stock Value"),
+ },
+ {
+ "fieldname": "stock_value_from_diff",
+ "fieldtype": "Float",
+ "label": _("(G) Sum of Change in Stock Value"),
+ },
+ {
+ "fieldname": "diff_value_diff",
+ "fieldtype": "Float",
+ "label": _("G - D"),
+ },
+ {
+ "fieldname": "fifo_stock_diff",
+ "fieldtype": "Float",
+ "label": _("(H) Change in Stock Value (FIFO Queue)"),
+ },
+ {
+ "fieldname": "fifo_difference_diff",
+ "fieldtype": "Float",
+ "label": _("H - F"),
+ },
+ {
+ "fieldname": "valuation_rate",
+ "fieldtype": "Float",
+ "label": _("(I) Valuation Rate"),
+ },
+ {
+ "fieldname": "fifo_valuation_rate",
+ "fieldtype": "Float",
+ "label": _("(J) Valuation Rate as per FIFO"),
+ },
+ {
+ "fieldname": "fifo_valuation_diff",
+ "fieldtype": "Float",
+ "label": _("I - J"),
+ },
+ {
+ "fieldname": "balance_value_by_qty",
+ "fieldtype": "Float",
+ "label": _("(K) Valuation = Value (D) ÷ Qty (A)"),
+ },
+ {
+ "fieldname": "valuation_diff",
+ "fieldtype": "Float",
+ "label": _("I - K"),
+ },
+ ]
+
+
+def get_data(filters=None):
+ filters = frappe._dict(filters or {})
+ item_warehouse_map = get_item_warehouse_combinations(filters)
+
+ data = []
+ if item_warehouse_map:
+ precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
+
+ for item_warehouse in item_warehouse_map:
+ report_data = stock_ledger_invariant_check(item_warehouse)
+
+ if not report_data:
+ continue
+
+ for row in report_data:
+ if has_difference(row, precision, filters.difference_in):
+ data.append(add_item_warehouse_details(row, item_warehouse))
+ break
+
+ return data
+
+
+def get_item_warehouse_combinations(filters: dict = None) -> dict:
+ filters = frappe._dict(filters or {})
+
+ bin = frappe.qb.DocType("Bin")
+ item = frappe.qb.DocType("Item")
+ warehouse = frappe.qb.DocType("Warehouse")
+
+ query = (
+ frappe.qb.from_(bin)
+ .inner_join(item)
+ .on(bin.item_code == item.name)
+ .inner_join(warehouse)
+ .on(bin.warehouse == warehouse.name)
+ .select(
+ bin.item_code,
+ bin.warehouse,
+ )
+ .where((item.is_stock_item == 1) & (item.has_serial_no == 0) & (warehouse.is_group == 0))
+ )
+
+ if filters.item_code:
+ query = query.where(item.name == filters.item_code)
+ if filters.warehouse:
+ query = query.where(warehouse.name == filters.warehouse)
+ if not filters.include_disabled:
+ query = query.where((item.disabled == 0) & (warehouse.disabled == 0))
+
+ return query.run(as_dict=1)
+
+
+def has_difference(row, precision, difference_in):
+ has_qty_difference = flt(row.difference_in_qty, precision) or flt(row.fifo_qty_diff, precision)
+ has_value_difference = (
+ flt(row.diff_value_diff, precision)
+ or flt(row.fifo_value_diff, precision)
+ or flt(row.fifo_difference_diff, precision)
+ )
+ has_valuation_difference = flt(row.valuation_diff, precision) or flt(
+ row.fifo_valuation_diff, precision
+ )
+
+ if difference_in == "Qty" and has_qty_difference:
+ return True
+ elif difference_in == "Value" and has_value_difference:
+ return True
+ elif difference_in == "Valuation" and has_valuation_difference:
+ return True
+ elif difference_in not in ["Qty", "Value", "Valuation"] and (
+ has_qty_difference or has_value_difference or has_valuation_difference
+ ):
+ return True
+
+ return False
+
+
+def add_item_warehouse_details(row, item_warehouse):
+ row.update(
+ {
+ "item_code": item_warehouse.item_code,
+ "warehouse": item_warehouse.warehouse,
+ }
+ )
+
+ return row
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
index 8ac22e6..19a1c93 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
@@ -77,6 +77,7 @@
}
frm.trigger('setup_quality_inspection');
+ frm.trigger('set_route_options_for_new_doc');
},
set_warehouse: (frm) => {
@@ -87,6 +88,23 @@
set_warehouse_in_children(frm.doc.items, 'rejected_warehouse', frm.doc.rejected_warehouse);
},
+ get_scrap_items: (frm) => {
+ frappe.call({
+ doc: frm.doc,
+ method: 'get_scrap_items',
+ args: {
+ recalculate_rate: true
+ },
+ freeze: true,
+ freeze_message: __('Getting Scrap Items'),
+ callback: (r) => {
+ if (!r.exc) {
+ frm.refresh();
+ }
+ }
+ });
+ },
+
set_queries: (frm) => {
frm.set_query('set_warehouse', () => {
return {
@@ -173,7 +191,28 @@
}
}
});
+ },
+ get_serial_and_batch_bundle_filters: (doc, cdt, cdn) => {
+ let row = locals[cdt][cdn];
+ return {
+ filters: {
+ 'item_code': row.item_code,
+ 'voucher_type': doc.doctype,
+ 'voucher_no': ['in', [doc.name, '']],
+ 'is_cancelled': 0,
+ }
+ }
+ },
+
+ setup_quality_inspection: (frm) => {
+ if (!frm.is_new() && frm.doc.docstatus === 0 && !frm.doc.is_return) {
+ let transaction_controller = new erpnext.TransactionController({ frm: frm });
+ transaction_controller.setup_quality_inspection();
+ }
+ },
+
+ set_route_options_for_new_doc: (frm) => {
let batch_no_field = frm.get_docfield('items', 'batch_no');
if (batch_no_field) {
batch_no_field.get_route_options_for_new_doc = (row) => {
@@ -213,42 +252,6 @@
}
}
},
-
- get_serial_and_batch_bundle_filters: (doc, cdt, cdn) => {
- let row = locals[cdt][cdn];
- return {
- filters: {
- 'item_code': row.item_code,
- 'voucher_type': doc.doctype,
- 'voucher_no': ['in', [doc.name, '']],
- 'is_cancelled': 0,
- }
- }
- },
-
- setup_quality_inspection: (frm) => {
- if (!frm.is_new() && frm.doc.docstatus === 0 && !frm.doc.is_return) {
- let transaction_controller = new erpnext.TransactionController({ frm: frm });
- transaction_controller.setup_quality_inspection();
- }
- },
-
- get_scrap_items: (frm) => {
- frappe.call({
- doc: frm.doc,
- method: 'get_scrap_items',
- args: {
- recalculate_rate: true
- },
- freeze: true,
- freeze_message: __('Getting Scrap Items'),
- callback: (r) => {
- if (!r.exc) {
- frm.refresh();
- }
- }
- });
- },
});
frappe.ui.form.on('Landed Cost Taxes and Charges', {
@@ -303,4 +306,4 @@
if (!r.exc) frm.refresh();
},
});
-}
\ No newline at end of file
+}
diff --git a/erpnext/templates/generators/item/item_add_to_cart.html b/erpnext/templates/generators/item/item_add_to_cart.html
index 1381dfe..9bd3f75 100644
--- a/erpnext/templates/generators/item/item_add_to_cart.html
+++ b/erpnext/templates/generators/item/item_add_to_cart.html
@@ -49,7 +49,7 @@
<span class="in-green has-stock">
{{ _('In stock') }}
{% if product_info.show_stock_qty and product_info.stock_qty %}
- ({{ product_info.stock_qty[0][0] }})
+ ({{ product_info.stock_qty }})
{% endif %}
</span>
{% endif %}
diff --git a/erpnext/templates/pages/wishlist.py b/erpnext/templates/pages/wishlist.py
index d70f27c..17607e4 100644
--- a/erpnext/templates/pages/wishlist.py
+++ b/erpnext/templates/pages/wishlist.py
@@ -25,9 +25,19 @@
def get_stock_availability(item_code, warehouse):
- stock_qty = frappe.utils.flt(
- frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "actual_qty")
- )
+ from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
+
+ if warehouse and frappe.get_cached_value("Warehouse", warehouse, "is_group") == 1:
+ warehouses = get_child_warehouses(warehouse)
+ else:
+ warehouses = [warehouse] if warehouse else []
+
+ stock_qty = 0.0
+ for warehouse in warehouses:
+ stock_qty += frappe.utils.flt(
+ frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "actual_qty")
+ )
+
return bool(stock_qty)
diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv
index 1dddc3c..03e9de4 100644
--- a/erpnext/translations/de.csv
+++ b/erpnext/translations/de.csv
@@ -477,7 +477,7 @@
Chapter information.,Gruppeninformation,
Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount,Kosten für den Typ „Tatsächlich“ in Zeile {0} können nicht in den Artikelpreis oder den bezahlen Betrag einfließen,
Chargeble,Belastung,
-Charges are updated in Purchase Receipt against each item,Kosten werden im Kaufbeleg für jede Position aktualisiert,
+Charges are updated in Purchase Receipt against each item,Kosten werden im Eingangsbeleg für jede Position aktualisiert,
"Charges will be distributed proportionately based on item qty or amount, as per your selection",Die Kosten werden gemäß Ihrer Wahl anteilig verteilt basierend auf Artikelmenge oder -preis,
Chart of Cost Centers,Kostenstellenplan,
Check all,Alle prüfen,
@@ -1202,7 +1202,7 @@
Invalid GSTIN! First 2 digits of GSTIN should match with State number {0}.,Ungültige GSTIN! Die ersten beiden Ziffern von GSTIN sollten mit der Statusnummer {0} übereinstimmen.,
Invalid GSTIN! The input you've entered doesn't match the format of GSTIN.,Ungültige GSTIN! Die von Ihnen eingegebene Eingabe stimmt nicht mit dem Format von GSTIN überein.,
Invalid Posting Time,Ungültige Buchungszeit,
-Invalid Purchase Invoice,Ungültige Einkaufsrechnung,
+Invalid Purchase Invoice,Ungültige Eingangsrechnung,
Invalid attribute {0} {1},Ungültiges Attribut {0} {1},
Invalid quantity specified for item {0}. Quantity should be greater than 0.,Ungültzige Anzahl für Artikel {0} angegeben. Anzahl sollte größer als 0 sein.,
Invalid reference {0} {1},Ungültige Referenz {0} {1},
@@ -1264,7 +1264,7 @@
Item Variants,Artikelvarianten,
Item Variants updated,Artikelvarianten aktualisiert,
Item has variants.,Artikel hat Varianten.,
-Item must be added using 'Get Items from Purchase Receipts' button,"Artikel müssen über die Schaltfläche ""Artikel von Kaufbeleg übernehmen"" hinzugefügt werden",
+Item must be added using 'Get Items from Purchase Receipts' button,"Artikel müssen über die Schaltfläche ""Artikel von Eingangsbeleg übernehmen"" hinzugefügt werden",
Item valuation rate is recalculated considering landed cost voucher amount,Artikelpreis wird unter Einbezug von Belegen über den Einstandspreis neu berechnet,
Item variant {0} exists with same attributes,Artikelvariante {0} mit denselben Attributen existiert,
Item {0} does not exist,Artikel {0} existiert nicht,
@@ -1524,7 +1524,7 @@
New Quality Procedure,Neues Qualitätsverfahren,
New Sales Invoice,Neue Ausgangsrechnung,
New Sales Person Name,Neuer Verkaufspersonenname,
-New Serial No cannot have Warehouse. Warehouse must be set by Stock Entry or Purchase Receipt,"""Neue Seriennummer"" kann keine Lagerangabe enthalten. Lagerangaben müssen durch eine Lagerbuchung oder einen Kaufbeleg erstellt werden",
+New Serial No cannot have Warehouse. Warehouse must be set by Stock Entry or Purchase Receipt,"""Neue Seriennummer"" kann keine Lagerangabe enthalten. Lagerangaben müssen durch eine Lagerbuchung oder einen Eingangsbeleg erstellt werden",
New Warehouse Name,Neuer Lagername,
New credit limit is less than current outstanding amount for the customer. Credit limit has to be atleast {0},Neues Kreditlimit ist weniger als der aktuell ausstehende Betrag für den Kunden. Kreditlimit muss mindestens {0} sein,
New task,Neuer Vorgang,
@@ -1790,7 +1790,7 @@
Please click on 'Generate Schedule',"Bitte auf ""Zeitplan generieren"" klicken",
Please click on 'Generate Schedule' to fetch Serial No added for Item {0},"Bitte auf ""Zeitplan generieren"" klicken, um die Seriennummer für Artikel {0} abzurufen",
Please click on 'Generate Schedule' to get schedule,"Bitte auf ""Zeitplan generieren"" klicken, um den Zeitplan zu erhalten",
-Please create purchase receipt or purchase invoice for the item {0},Bitte erstellen Sie eine Kaufquittung oder eine Kaufrechnung für den Artikel {0},
+Please create purchase receipt or purchase invoice for the item {0},Bitte erstellen Sie eine Kaufquittung oder eine Eingangsrechnungen für den Artikel {0},
Please define grade for Threshold 0%,Bitte definieren Sie Grade for Threshold 0%,
Please enable Applicable on Booking Actual Expenses,Bitte aktivieren Sie Anwendbar bei der Buchung von tatsächlichen Ausgaben,
Please enable Applicable on Purchase Order and Applicable on Booking Actual Expenses,Bitte aktivieren Sie Anwendbar bei Bestellung und Anwendbar bei Buchung von tatsächlichen Ausgaben,
@@ -1811,7 +1811,7 @@
Please enter Planned Qty for Item {0} at row {1},Bitte die geplante Menge für Artikel {0} in Zeile {1} eingeben,
Please enter Preferred Contact Email,Bitte geben Sie Bevorzugte Kontakt per E-Mail,
Please enter Production Item first,Bitte zuerst Herstellungsartikel eingeben,
-Please enter Purchase Receipt first,Bitte zuerst Kaufbeleg eingeben,
+Please enter Purchase Receipt first,Bitte zuerst Eingangsbeleg eingeben,
Please enter Receipt Document,Bitte geben Sie Eingangsbeleg,
Please enter Reference date,Bitte den Stichtag eingeben,
Please enter Reqd by Date,Bitte geben Sie Requd by Date ein,
@@ -2045,8 +2045,8 @@
Purchase Orders are not allowed for {0} due to a scorecard standing of {1}.,Kaufaufträge sind für {0} wegen einer Scorecard von {1} nicht erlaubt.,
Purchase Orders given to Suppliers.,An Lieferanten erteilte Bestellungen,
Purchase Price List,Einkaufspreisliste,
-Purchase Receipt,Kaufbeleg,
-Purchase Receipt {0} is not submitted,Kaufbeleg {0} wurde nicht übertragen,
+Purchase Receipt,Eingangsbeleg,
+Purchase Receipt {0} is not submitted,Eingangsbeleg {0} wurde nicht übertragen,
Purchase Tax Template,Umsatzsteuer-Vorlage,
Purchase User,Nutzer Einkauf,
Purchase orders help you plan and follow up on your purchases,Bestellungen helfen Ihnen bei der Planung und Follow-up auf Ihre Einkäufe,
@@ -2108,7 +2108,7 @@
Reason For Putting On Hold,Grund für das auf Eis legen,
Reason for Hold,Grund für das auf Eis legen,
Reason for hold: ,Grund für das auf Eis legen:,
-Receipt,Kaufbeleg,
+Receipt,Eingangsbeleg,
Receipt document must be submitted,Eingangsbeleg muss vorgelegt werden,
Receivable,Forderung,
Receivable Account,Forderungskonto,
@@ -2571,7 +2571,7 @@
Stock In Hand,Stock In Hand,
Stock Items,Lagerartikel,
Stock Ledger,Lagerbuch,
-Stock Ledger Entries and GL Entries are reposted for the selected Purchase Receipts,Buchungen auf das Lagerbuch und Hauptbuch-Buchungen werden für die gewählten Kaufbelege umgebucht,
+Stock Ledger Entries and GL Entries are reposted for the selected Purchase Receipts,Buchungen auf das Lagerbuch und Hauptbuch-Buchungen werden für die gewählten Eingangsbelege umgebucht,
Stock Levels,Lagerbestände,
Stock Liabilities,Lager-Verbindlichkeiten,
Stock Qty,Lagermenge,
@@ -2583,7 +2583,7 @@
Stock Value,Lagerwert,
Stock balance in Batch {0} will become negative {1} for Item {2} at Warehouse {3},Lagerbestand in Charge {0} wird für Artikel {2} im Lager {3} negativ {1},
Stock cannot be updated against Delivery Note {0},Lager kann nicht mit Lieferschein {0} aktualisiert werden,
-Stock cannot be updated against Purchase Receipt {0},Auf nicht gegen Kaufbeleg aktualisiert werden {0},
+Stock cannot be updated against Purchase Receipt {0},Bestand kann nicht gegen Eingangsbeleg {0} aktualisiert werden,
Stock cannot exist for Item {0} since has variants,"Für Artikel {0} kann es kein Lager geben, da es Varianten gibt",
Stock transactions before {0} are frozen,Lagertransaktionen vor {0} werden gesperrt,
Stop,Anhalten,
@@ -2648,7 +2648,7 @@
Supplier Part No,Lieferant Teile-Nr,
Supplier Quotation,Lieferantenangebot,
Supplier Scorecard,Lieferanten-Scorecard,
-Supplier Warehouse mandatory for sub-contracted Purchase Receipt,Lieferantenlager notwendig für Kaufbeleg aus Unteraufträgen,
+Supplier Warehouse mandatory for sub-contracted Purchase Receipt,Lieferantenlager notwendig für Eingangsbeleg aus Unteraufträgen,
Supplier database.,Lieferantendatenbank,
Supplier {0} not found in {1},Lieferant {0} nicht in {1} gefunden,
Supplier(s),Lieferant(en),
@@ -3356,7 +3356,7 @@
Communication,Kommunikation,
Compact Item Print,Artikel kompakt drucken,
Company,Unternehmen,
-Company of asset {0} and purchase document {1} doesn't matches.,Das Unternehmen von Anlage {0} und Kaufbeleg {1} stimmt nicht überein.,
+Company of asset {0} and purchase document {1} doesn't matches.,Das Unternehmen von Anlage {0} und Eingangsbeleg {1} stimmt nicht überein.,
Compare BOMs for changes in Raw Materials and Operations,Vergleichen Sie Stücklisten auf Änderungen in Rohstoffen und Vorgängen,
Compare List function takes on list arguments,Die Funktion "Liste vergleichen" übernimmt Listenargumente,
Complete,Komplett,
@@ -3616,7 +3616,7 @@
Purchase Invoice cannot be made against an existing asset {0},Eingangsrechnung kann nicht für ein vorhandenes Asset erstellt werden {0},
Purchase Invoices,Eingangsrechnungen,
Purchase Orders,Kauforder,
-Purchase Receipt doesn't have any Item for which Retain Sample is enabled.,"Der Kaufbeleg enthält keinen Artikel, für den die Option "Probe aufbewahren" aktiviert ist.",
+Purchase Receipt doesn't have any Item for which Retain Sample is enabled.,"Der Eingangsbeleg enthält keinen Artikel, für den die Option "Probe aufbewahren" aktiviert ist.",
Purchase Return,Warenrücksendung,
Qty of Finished Goods Item,Menge des Fertigerzeugnisses,
Quality Inspection required for Item {0} to submit,"Qualitätsprüfung erforderlich, damit Artikel {0} eingereicht werden kann",
@@ -3925,7 +3925,7 @@
{0} {1} has accounting entries in currency {2} for company {3}. Please select a receivable or payable account with currency {2}.,{0} {1} hat Buchhaltungseinträge in Währung {2} für Firma {3}. Bitte wählen Sie ein Debitoren- oder Kreditorenkonto mit der Währung {2} aus.,
Invalid Account,Ungültiger Account,
Purchase Order Required,Bestellung erforderlich,
-Purchase Receipt Required,Kaufbeleg notwendig,
+Purchase Receipt Required,Eingangsbeleg notwendig,
Account Missing,Konto fehlt,
Requested,Angefordert,
Partially Paid,Teilweise bezahlt,
@@ -4521,7 +4521,7 @@
POS Field,POS-Feld,
POS Item Group,POS Artikelgruppe,
Company Address,Anschrift des Unternehmens,
-Update Stock,Lagerbestand aktualisieren,
+Update Stock,Lagerbestand aktualisieren,
Ignore Pricing Rule,Preisregel ignorieren,
Applicable for Users,Anwendbar für Benutzer,
Sales Invoice Payment,Ausgangsrechnung-Zahlungen,
@@ -4691,7 +4691,7 @@
Item Tax Rate,Artikelsteuersatz,
Tax detail table fetched from item master as a string and stored in this field.\nUsed for Taxes and Charges,Die Tabelle Steuerdetails wird aus dem Artikelstamm als Zeichenfolge entnommen und in diesem Feld gespeichert. Wird verwendet für Steuern und Abgaben,
Purchase Order Item,Bestellartikel,
-Purchase Receipt Detail,Kaufbelegdetail,
+Purchase Receipt Detail,Eingangsbelegposition,
Item Weight Details,Artikel Gewicht Details,
Weight Per Unit,Gewicht pro Einheit,
Total Weight,Gesamtgewicht,
@@ -4716,7 +4716,6 @@
Include Payment (POS),(POS) Zahlung einschließen,
Offline POS Name,Offline-Verkaufsstellen-Name,
Is Return (Credit Note),ist Rücklieferung (Gutschrift),
-Return Against Sales Invoice,Zurück zur Kundenrechnung,
Update Billed Amount in Sales Order,Aktualisierung des Rechnungsbetrags im Auftrag,
Customer PO Details,Auftragsdetails,
Customer's Purchase Order,Bestellung des Kunden,
@@ -4993,7 +4992,7 @@
Maintenance Required,Wartung erforderlich,
Check if Asset requires Preventive Maintenance or Calibration,"Überprüfen Sie, ob der Vermögenswert eine vorbeugende Wartung oder Kalibrierung erfordert",
Booked Fixed Asset,Gebuchtes Anlagevermögen,
-Purchase Receipt Amount,Kaufbelegbetrag,
+Purchase Receipt Amount,Betrag Eingangsbeleg,
Default Finance Book,Standardfinanzbuch,
Quality Manager,Qualitätsmanager,
Asset Category Name,Name Vermögenswertkategorie,
@@ -5113,7 +5112,7 @@
Stock Uom,Lagermaßeinheit,
Raw Material Item Code,Rohmaterial-Artikelnummer,
Supplied Qty,Gelieferte Anzahl,
-Purchase Receipt Item Supplied,Kaufbeleg-Artikel geliefert,
+Purchase Receipt Item Supplied,Eingangsbeleg-Artikel geliefert,
Current Stock,Aktueller Lagerbestand,
PUR-RFQ-.YYYY.-,PUR-RFQ-.YYYY.-,
For individual supplier,Für einzelne Anbieter,
@@ -5133,7 +5132,7 @@
Represents Company,Repräsentiert das Unternehmen,
Supplier Type,Lieferantentyp,
Allow Purchase Invoice Creation Without Purchase Order,Erstellen von Eingangsrechnung ohne Bestellung zulassen,
-Allow Purchase Invoice Creation Without Purchase Receipt,Erstellen von Eingangsrechnung ohne Kaufbeleg ohne Kaufbeleg zulassen,
+Allow Purchase Invoice Creation Without Purchase Receipt,Erstellen von Eingangsrechnung ohne Eingangsbeleg zulassen,
Warn RFQs,Warnung Ausschreibungen,
Warn POs,Warnen Sie POs,
Prevent RFQs,Vermeidung von Ausschreibungen,
@@ -6692,8 +6691,8 @@
From Lead,Aus Lead,
Account Manager,Kundenberater,
Accounts Manager,Buchhalter,
-Allow Sales Invoice Creation Without Sales Order,Ermöglichen Sie die Erstellung von Kundenrechnungen ohne Auftrag,
-Allow Sales Invoice Creation Without Delivery Note,Ermöglichen Sie die Erstellung einer Ausgangsrechnung ohne Lieferschein,
+Allow Sales Invoice Creation Without Sales Order,Ermöglichen Sie die Erstellung von Ausgangsrechnungen ohne Auftrag,
+Allow Sales Invoice Creation Without Delivery Note,Ermöglichen Sie die Erstellung von Ausgangsrechnungen ohne Lieferschein,
Default Price List,Standardpreisliste,
Primary Address and Contact,Hauptadresse und -kontakt,
"Select, to make the customer searchable with these fields","Wählen Sie, um den Kunden mit diesen Feldern durchsuchbar zu machen",
@@ -7200,13 +7199,13 @@
Receipt Document Type,Receipt Dokumenttyp,
Receipt Document,Eingangsbeleg,
Applicable Charges,Anfallende Gebühren,
-Purchase Receipt Item,Kaufbeleg-Artikel,
-Landed Cost Purchase Receipt,Einstandspreis-Kaufbeleg,
+Purchase Receipt Item,Eingangsbeleg-Artikel,
+Landed Cost Purchase Receipt,Einstandspreis-Eingangsbeleg,
Landed Cost Taxes and Charges,Einstandspreis Steuern und Gebühren,
Landed Cost Voucher,Beleg über Einstandskosten,
-Purchase Receipts,Kaufbelege,
-Purchase Receipt Items,Kaufbeleg-Artikel,
-Get Items From Purchase Receipts,Artikel vom Kaufbeleg übernehmen,
+Purchase Receipts,Eingangsbelege,
+Purchase Receipt Items,Eingangsbeleg-Artikel,
+Get Items From Purchase Receipts,Artikel vom Eingangsbeleg übernehmen,
Distribute Charges Based On,Kosten auf folgender Grundlage verteilen,
Landed Cost Help,Hilfe zum Einstandpreis,
Manufacturers used in Items,Hersteller im Artikel verwendet,
@@ -7254,7 +7253,7 @@
MAT-PRE-.YYYY.-,MAT-PRE-.JJJJ.-,
Supplier Delivery Note,Lieferschein Nr.,
Time at which materials were received,"Zeitpunkt, zu dem Materialien empfangen wurden",
-Return Against Purchase Receipt,Zurück zum Kaufbeleg,
+Return Against Purchase Receipt,Zurück zum Eingangsbeleg,
Rate at which supplier's currency is converted to company's base currency,"Kurs, zu dem die Währung des Lieferanten in die Basiswährung des Unternehmens umgerechnet wird",
Sets 'Accepted Warehouse' in each row of the items table.,Legt 'Akzeptiertes Lager' in jeder Zeile der Artikeltabelle fest.,
Sets 'Rejected Warehouse' in each row of the items table.,Legt 'Abgelehntes Lager' in jeder Zeile der Artikeltabelle fest.,
@@ -7294,7 +7293,7 @@
Quick Stock Balance,Schneller Lagerbestand,
Available Quantity,verfügbare Anzahl,
Distinct unit of an Item,Eindeutige Einheit eines Artikels,
-Warehouse can only be changed via Stock Entry / Delivery Note / Purchase Receipt,Lager kann nur über Lagerbuchung / Lieferschein / Kaufbeleg geändert werden,
+Warehouse can only be changed via Stock Entry / Delivery Note / Purchase Receipt,Lager kann nur über Lagerbuchung / Lieferschein / Eingangsbeleg geändert werden,
Purchase / Manufacture Details,Einzelheiten zu Kauf / Herstellung,
Creation Document Type,Belegerstellungs-Typ,
Creation Document No,Belegerstellungs-Nr.,
@@ -7322,7 +7321,7 @@
Send to Subcontractor,An Subunternehmer senden,
Delivery Note No,Lieferschein-Nummer,
Sales Invoice No,Ausgangsrechnungs-Nr.,
-Purchase Receipt No,Kaufbeleg Nr.,
+Purchase Receipt No,Eingangsbeleg Nr.,
Inspection Required,Prüfung erforderlich,
From BOM,Von Stückliste,
For Quantity,Für Menge,
@@ -7351,7 +7350,7 @@
Against Stock Entry,Gegen Lagerbuchung,
Stock Entry Child,Stock Entry Child,
PO Supplied Item,PO geliefertes Einzelteil,
-Reference Purchase Receipt,Referenz Kaufbeleg,
+Reference Purchase Receipt,Referenz Eingangsbeleg,
Stock Ledger Entry,Buchung im Lagerbuch,
Outgoing Rate,Verkaufspreis,
Actual Qty After Transaction,Tatsächliche Anzahl nach Transaktionen,
@@ -7567,7 +7566,7 @@
Received Qty Amount,Erhaltene Menge Menge,
Billed Qty,Rechnungsmenge,
Purchase Order Trends,Entwicklung Bestellungen,
-Purchase Receipt Trends,Trendanalyse Kaufbelege,
+Purchase Receipt Trends,Trendanalyse Eingangsbelege,
Purchase Register,Übersicht über Einkäufe,
Quotation Trends,Trendanalyse Angebote,
Received Items To Be Billed,"Von Lieferanten gelieferte Artikel, die noch abgerechnet werden müssen",
@@ -7804,8 +7803,8 @@
"By default, the Supplier Name is set as per the Supplier Name entered. If you want Suppliers to be named by a ","Standardmäßig wird der Lieferantenname gemäß dem eingegebenen Lieferantennamen festgelegt. Wenn Sie möchten, dass Lieferanten von a benannt werden",
choose the 'Naming Series' option.,Wählen Sie die Option "Naming Series".,
Configure the default Price List when creating a new Purchase transaction. Item prices will be fetched from this Price List.,Konfigurieren Sie die Standardpreisliste beim Erstellen einer neuen Kauftransaktion. Artikelpreise werden aus dieser Preisliste abgerufen.,
-"If this option is configured 'Yes', ERPNext will prevent you from creating a Purchase Invoice or Receipt without creating a Purchase Order first. This configuration can be overridden for a particular supplier by enabling the 'Allow Purchase Invoice Creation Without Purchase Order' checkbox in the Supplier master.","Wenn diese Option auf 'Ja' gesetzt ist, validiert ERPNext, dass Sie eine Bestellung angelegt haben, bevor Sie eine Eingangsrechnung oder einen Kaufbeleg erfassen können. Diese Konfiguration kann für einzelne Lieferanten überschrieben werden, indem Sie die Option 'Erstellung von Eingangsrechnungen ohne Bestellung zulassen' im Lieferantenstamm aktivieren.",
-"If this option is configured 'Yes', ERPNext will prevent you from creating a Purchase Invoice without creating a Purchase Receipt first. This configuration can be overridden for a particular supplier by enabling the 'Allow Purchase Invoice Creation Without Purchase Receipt' checkbox in the Supplier master.","Wenn diese Option auf 'Ja' gesetzt ist, validiert ERPNext, dass Sie einen Kaufbeleg angelegt haben, bevor Sie eine Eingangsrechnung erfasen können. Diese Konfiguration kann für einzelne Lieferanten überschrieben werden, indem Sie die Option 'Erstellung von Kaufrechnungen ohne Kaufbeleg zulassen' im Lieferantenstamm aktivieren.",
+"If this option is configured 'Yes', ERPNext will prevent you from creating a Purchase Invoice or Receipt without creating a Purchase Order first. This configuration can be overridden for a particular supplier by enabling the 'Allow Purchase Invoice Creation Without Purchase Order' checkbox in the Supplier master.","Wenn diese Option auf 'Ja' gesetzt ist, validiert ERPNext, dass Sie eine Bestellung angelegt haben, bevor Sie eine Eingangsrechnung oder einen Eingangsbeleg erfassen können. Diese Konfiguration kann für einzelne Lieferanten überschrieben werden, indem Sie die Option 'Erstellung von Eingangsrechnungen ohne Bestellung zulassen' im Lieferantenstamm aktivieren.",
+"If this option is configured 'Yes', ERPNext will prevent you from creating a Purchase Invoice without creating a Purchase Receipt first. This configuration can be overridden for a particular supplier by enabling the 'Allow Purchase Invoice Creation Without Purchase Receipt' checkbox in the Supplier master.","Wenn diese Option auf 'Ja' gesetzt ist, validiert ERPNext, dass Sie einen Eingangsbeleg angelegt haben, bevor Sie eine Eingangsrechnung erfassen können. Diese Konfiguration kann für einzelne Lieferanten überschrieben werden, indem Sie die Option 'Erstellung von Kaufrechnungen ohne Eingangsbeleg zulassen' im Lieferantenstamm aktivieren.",
Quantity & Stock,Menge & Lager,
Call Details,Anrufdetails,
Authorised By,Authorisiert von,
@@ -8047,7 +8046,6 @@
Image Description,Bildbeschreibung,
Transfer Status,Übertragungsstatus,
MAT-PR-RET-.YYYY.-,MAT-PR-RET-.YYYY.-,
-Track this Purchase Receipt against any Project,Verfolgen Sie diesen Kaufbeleg für jedes Projekt,
Please Select a Supplier,Bitte wählen Sie einen Lieferanten,
Add to Transit,Zum Transit hinzufügen,
Set Basic Rate Manually,Grundpreis manuell einstellen,
@@ -8472,8 +8470,8 @@
Show Inclusive Tax in Print,Inklusive Steuern im Druck anzeigen,
Only select this if you have set up the Cash Flow Mapper documents,"Wählen Sie diese Option nur, wenn Sie die Cash Flow Mapper-Dokumente eingerichtet haben",
Payment Channel,Zahlungskanal,
-Is Purchase Order Required for Purchase Invoice & Receipt Creation?,Ist für die Erstellung von Kaufrechnungen und Quittungen eine Bestellung erforderlich?,
-Is Purchase Receipt Required for Purchase Invoice Creation?,Ist für die Erstellung der Kaufrechnung ein Kaufbeleg erforderlich?,
+Is Purchase Order Required for Purchase Invoice & Receipt Creation?,Ist für die Erstellung von Eingangsrechnungen und Quittungen eine Bestellung erforderlich?,
+Is Purchase Receipt Required for Purchase Invoice Creation?,Ist für die Erstellung der Eingangsrechnungen ein Eingangsbeleg erforderlich?,
Maintain Same Rate Throughout the Purchase Cycle,Behalten Sie den gleichen Preis während des gesamten Kaufzyklus bei,
Allow Item To Be Added Multiple Times in a Transaction,"Zulassen, dass ein Element in einer Transaktion mehrmals hinzugefügt wird",
Suppliers,Lieferanten,
@@ -8531,8 +8529,8 @@
Select Items,Gegenstände auswählen,
Against Default Supplier,Gegen Standardlieferanten,
Auto close Opportunity after the no. of days mentioned above,Gelegenheit zum automatischen Schließen nach der Nr. der oben genannten Tage,
-Is Sales Order Required for Sales Invoice & Delivery Note Creation?,Ist ein Auftrag für die Erstellung von Kundenrechnungen und Lieferscheinen erforderlich?,
-Is Delivery Note Required for Sales Invoice Creation?,Ist für die Erstellung der Ausgangsrechnung ein Lieferschein erforderlich?,
+Is Sales Order Required for Sales Invoice & Delivery Note Creation?,Ist ein Auftrag für die Erstellung von Ausgangsrechnungen und Lieferscheinen erforderlich?,
+Is Delivery Note Required for Sales Invoice Creation?,Ist ein Lieferschein für die Erstellung von Ausgangsrechnungen erforderlich?,
How often should Project and Company be updated based on Sales Transactions?,Wie oft sollten Projekt und Unternehmen basierend auf Verkaufstransaktionen aktualisiert werden?,
Allow User to Edit Price List Rate in Transactions,Benutzer darf Preisliste in Transaktionen bearbeiten,
Allow Item to Be Added Multiple Times in a Transaction,"Zulassen, dass ein Element in einer Transaktion mehrmals hinzugefügt wird",
@@ -8547,7 +8545,7 @@
Raise Material Request When Stock Reaches Re-order Level,"Erhöhen Sie die Materialanforderung, wenn der Lagerbestand die Nachbestellmenge erreicht",
Notify by Email on Creation of Automatic Material Request,Benachrichtigen Sie per E-Mail über die Erstellung einer automatischen Materialanforderung,
Allow Material Transfer from Delivery Note to Sales Invoice,Materialübertragung vom Lieferschein zur Ausgangsrechnung zulassen,
-Allow Material Transfer from Purchase Receipt to Purchase Invoice,Materialübertragung vom Kaufbeleg zur Eingangsrechnung zulassen,
+Allow Material Transfer from Purchase Receipt to Purchase Invoice,Materialübertragung vom Eingangsbeleg zur Eingangsrechnung zulassen,
Freeze Stocks Older Than (Days),Aktien einfrieren älter als (Tage),
Role Allowed to Edit Frozen Stock,Rolle darf eingefrorenes Material bearbeiten,
The unallocated amount of Payment Entry {0} is greater than the Bank Transaction's unallocated amount,Der nicht zugewiesene Betrag der Zahlungseingabe {0} ist größer als der nicht zugewiesene Betrag der Banküberweisung,
@@ -8672,20 +8670,17 @@
Please set default Cash or Bank account in Mode of Payments {},Bitte setzen Sie das Standard-Bargeld- oder Bankkonto im Zahlungsmodus {},
Please ensure {} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account.,"Bitte stellen Sie sicher, dass das Konto {} ein Bilanzkonto ist. Sie können das übergeordnete Konto in ein Bilanzkonto ändern oder ein anderes Konto auswählen.",
Please ensure {} account is a Payable account. Change the account type to Payable or select a different account.,"Bitte stellen Sie sicher, dass das Konto {} ein zahlbares Konto ist. Ändern Sie den Kontotyp in "Verbindlichkeiten" oder wählen Sie ein anderes Konto aus.",
-Row {}: Expense Head changed to {} ,Zeile {}: Ausgabenkopf geändert in {},
-because account {} is not linked to warehouse {} ,weil das Konto {} nicht mit dem Lager {} verknüpft ist,
-or it is not the default inventory account,oder es ist nicht das Standard-Inventarkonto,
-Expense Head Changed,Ausgabenkopf geändert,
-because expense is booked against this account in Purchase Receipt {},weil die Kosten für dieses Konto im Kaufbeleg {} gebucht werden,
-as no Purchase Receipt is created against Item {}. ,da für Artikel {} kein Kaufbeleg erstellt wird.,
-This is done to handle accounting for cases when Purchase Receipt is created after Purchase Invoice,"Dies erfolgt zur Abrechnung von Fällen, in denen der Kaufbeleg nach der Kaufrechnung erstellt wird",
+"Row {0}: Expense Head changed to {1} because account {2} is not linked to warehouse {3} or it is not the default inventory account","Zeile {0}: Aufwandskonto geändert zu {1}, weil das Konto {2} nicht mit dem Lager {3} verknüpft ist oder es nicht das Standard-Inventarkonto ist",
+Row {0}: Expense Head changed to {1} because expense is booked against this account in Purchase Receipt {2},"Zeile {0}: Aufwandskonto geändert zu {1}, da dieses bereits in Eingangsbeleg {2} verwendet wurde",
+Row {0}: Expense Head changed to {1} as no Purchase Receipt is created against Item {2}.,"Zeile {0}: Aufwandskonto geändert zu {1}, da kein Eingangsbeleg für Artikel {2} erstellt wird.",
+This is done to handle accounting for cases when Purchase Receipt is created after Purchase Invoice,"Dies erfolgt zur Abrechnung von Fällen, in denen der Eingangsbeleg nach der Eingangsrechnung erstellt wird",
Purchase Order Required for item {},Bestellung erforderlich für Artikel {},
To submit the invoice without purchase order please set {} ,"Um die Rechnung ohne Bestellung einzureichen, setzen Sie bitte {}",
as {} in {},wie in {},
Mandatory Purchase Order,Obligatorische Bestellung,
-Purchase Receipt Required for item {},Kaufbeleg für Artikel {} erforderlich,
-To submit the invoice without purchase receipt please set {} ,"Um die Rechnung ohne Kaufbeleg einzureichen, setzen Sie bitte {}",
-Mandatory Purchase Receipt,Obligatorischer Kaufbeleg,
+Purchase Receipt Required for item {},Eingangsbeleg für Artikel {} erforderlich,
+To submit the invoice without purchase receipt please set {} ,"Um die Rechnung ohne Eingangsbeleg einzureichen, setzen Sie bitte {}",
+Mandatory Purchase Receipt,Obligatorischer Eingangsbeleg,
POS Profile {} does not belongs to company {},Das POS-Profil {} gehört nicht zur Firma {},
User {} is disabled. Please select valid user/cashier,Benutzer {} ist deaktiviert. Bitte wählen Sie einen gültigen Benutzer / Kassierer aus,
Row #{}: Original Invoice {} of return invoice {} is {}. ,Zeile # {}: Die Originalrechnung {} der Rücksenderechnung {} ist {}.,
diff --git a/erpnext/translations/nl.csv b/erpnext/translations/nl.csv
index a1928d5..862d585 100644
--- a/erpnext/translations/nl.csv
+++ b/erpnext/translations/nl.csv
@@ -2156,7 +2156,7 @@
Reports,rapporten,
Reqd By Date,Benodigd op datum,
Reqd Qty,Gewenste hoeveelheid,
-Request for Quotation,Offerte,
+Request for Quotation,Offerte-verzoek,
Request for Quotations,Verzoek om offertes,
Request for Raw Materials,Verzoek om grondstoffen,
Request for purchase.,Inkoopaanvraag,
diff --git a/erpnext/utilities/product.py b/erpnext/utilities/product.py
index afe9654..e967f70 100644
--- a/erpnext/utilities/product.py
+++ b/erpnext/utilities/product.py
@@ -6,6 +6,7 @@
from erpnext.accounts.doctype.pricing_rule.pricing_rule import get_pricing_rule_for_item
from erpnext.stock.doctype.batch.batch import get_batch_qty
+from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
def get_web_item_qty_in_stock(item_code, item_warehouse_field, warehouse=None):
@@ -22,23 +23,31 @@
"Website Item", {"item_code": template_item_code}, item_warehouse_field
)
- if warehouse:
- stock_qty = frappe.db.sql(
- """
- select GREATEST(S.actual_qty - S.reserved_qty - S.reserved_qty_for_production - S.reserved_qty_for_sub_contract, 0) / IFNULL(C.conversion_factor, 1)
- from tabBin S
- inner join `tabItem` I on S.item_code = I.Item_code
- left join `tabUOM Conversion Detail` C on I.sales_uom = C.uom and C.parent = I.Item_code
- where S.item_code=%s and S.warehouse=%s""",
- (item_code, warehouse),
- )
+ if warehouse and frappe.get_cached_value("Warehouse", warehouse, "is_group") == 1:
+ warehouses = get_child_warehouses(warehouse)
+ else:
+ warehouses = [warehouse] if warehouse else []
- if stock_qty:
- stock_qty = adjust_qty_for_expired_items(item_code, stock_qty, warehouse)
- in_stock = stock_qty[0][0] > 0 and 1 or 0
+ total_stock = 0.0
+ if warehouses:
+ for warehouse in warehouses:
+ stock_qty = frappe.db.sql(
+ """
+ select GREATEST(S.actual_qty - S.reserved_qty - S.reserved_qty_for_production - S.reserved_qty_for_sub_contract, 0) / IFNULL(C.conversion_factor, 1)
+ from tabBin S
+ inner join `tabItem` I on S.item_code = I.Item_code
+ left join `tabUOM Conversion Detail` C on I.sales_uom = C.uom and C.parent = I.Item_code
+ where S.item_code=%s and S.warehouse=%s""",
+ (item_code, warehouse),
+ )
+
+ if stock_qty:
+ total_stock += adjust_qty_for_expired_items(item_code, stock_qty, warehouse)
+
+ in_stock = total_stock > 0 and 1 or 0
return frappe._dict(
- {"in_stock": in_stock, "stock_qty": stock_qty, "is_stock_item": is_stock_item}
+ {"in_stock": in_stock, "stock_qty": total_stock, "is_stock_item": is_stock_item}
)
@@ -56,7 +65,7 @@
if not stock_qty[0][0]:
break
- return stock_qty
+ return stock_qty[0][0] if stock_qty else 0
def get_expired_batches(batches):