Merge pull request #37120 from vorasmit/fix-dup-advance
fix: handle multiple references with same name
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/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/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index b3fb03f..38a5209 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -1605,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/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_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/setup/page/welcome_to_erpnext/__init__.py b/erpnext/accounts/doctype/process_subscription/__init__.py
similarity index 100%
copy from erpnext/setup/page/welcome_to_erpnext/__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/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/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/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/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 838fe52..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,6 +207,8 @@
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)
@@ -968,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
)
@@ -2409,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 c302ece..a76abe2 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -418,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
@@ -432,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/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/hooks.py b/erpnext/hooks.py
index 7bf8fb4..2155699 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -430,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",
],
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 dda4d36..e9c056e 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -342,5 +342,7 @@
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/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/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 975adc2..b0a9e40 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -224,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;
@@ -1191,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/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/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/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/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..5a8d84a 100644
--- a/erpnext/stock/dashboard/item_dashboard.py
+++ b/erpnext/stock/dashboard/item_dashboard.py
@@ -75,7 +75,7 @@
"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": sre_reserved_stock_details,
}
)
return items
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_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 79e6488..a6ab63b 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -1292,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_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/setup/page/welcome_to_erpnext/__init__.py b/erpnext/stock/report/stock_ledger_variance/__init__.py
similarity index 100%
rename from erpnext/setup/page/welcome_to_erpnext/__init__.py
rename 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/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..62d19f6 100644
--- a/erpnext/translations/de.csv
+++ b/erpnext/translations/de.csv
@@ -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},
@@ -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,
@@ -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,
@@ -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,
@@ -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",
@@ -8472,8 +8471,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 Kaufbeleg 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 +8530,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",
@@ -8678,7 +8677,7 @@
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",
+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 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 {},
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):