Merge pull request #37131 from ruthra-kumar/more_generic_customer_filter_in_ar_report
refactor: more generic filters in Accounts Receivable report
diff --git a/.github/helper/install.sh b/.github/helper/install.sh
index 48337ce..d1a97f8 100644
--- a/.github/helper/install.sh
+++ b/.github/helper/install.sh
@@ -68,6 +68,6 @@
wait $wkpid
-bench start &> bench_run_logs.txt &
+bench start &>> ~/frappe-bench/bench_start.log &
CI=Yes bench build --app frappe &
bench --site test_site reinstall --yes
diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml
index aae2928..07b8de7 100644
--- a/.github/workflows/patch.yml
+++ b/.github/workflows/patch.yml
@@ -23,7 +23,7 @@
services:
mysql:
- image: mariadb:10.3
+ image: mariadb:10.6
env:
MARIADB_ROOT_PASSWORD: 'root'
ports:
@@ -45,9 +45,7 @@
- name: Setup Python
uses: "actions/setup-python@v4"
with:
- python-version: |
- 3.7
- 3.10
+ python-version: '3.10'
- name: Setup Node
uses: actions/setup-node@v2
@@ -102,40 +100,60 @@
- name: Run Patch Tests
run: |
cd ~/frappe-bench/
- wget https://erpnext.com/files/v10-erpnext.sql.gz
- bench --site test_site --force restore ~/frappe-bench/v10-erpnext.sql.gz
+ bench remove-app payments --force
+ jq 'del(.install_apps)' ~/frappe-bench/sites/test_site/site_config.json > tmp.json
+ mv tmp.json ~/frappe-bench/sites/test_site/site_config.json
+
+ wget https://erpnext.com/files/v13-erpnext.sql.gz
+ bench --site test_site --force restore ~/frappe-bench/v13-erpnext.sql.gz
git -C "apps/frappe" remote set-url upstream https://github.com/frappe/frappe.git
git -C "apps/erpnext" remote set-url upstream https://github.com/frappe/erpnext.git
- for version in $(seq 12 13)
- do
- echo "Updating to v$version"
- branch_name="version-$version-hotfix"
- git -C "apps/frappe" fetch --depth 1 upstream $branch_name:$branch_name
- git -C "apps/erpnext" fetch --depth 1 upstream $branch_name:$branch_name
+ function update_to_version() {
+ version=$1
- git -C "apps/frappe" checkout -q -f $branch_name
- git -C "apps/erpnext" checkout -q -f $branch_name
+ branch_name="version-$version-hotfix"
+ echo "Updating to v$version"
- rm -rf ~/frappe-bench/env
- bench setup env --python python3.7
- bench pip install -e ./apps/payments
- bench pip install -e ./apps/erpnext
+ # Fetch and checkout branches
+ git -C "apps/frappe" fetch --depth 1 upstream $branch_name:$branch_name
+ git -C "apps/erpnext" fetch --depth 1 upstream $branch_name:$branch_name
+ git -C "apps/frappe" checkout -q -f $branch_name
+ git -C "apps/erpnext" checkout -q -f $branch_name
- bench --site test_site migrate
- done
+ # Resetup env and install apps
+ pgrep honcho | xargs kill
+ rm -rf ~/frappe-bench/env
+ bench -v setup env
+ bench pip install -e ./apps/erpnext
+ bench start &>> ~/frappe-bench/bench_start.log &
+ bench --site test_site migrate
+ }
+
+ update_to_version 14
echo "Updating to latest version"
git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}"
git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA"
+ pgrep honcho | xargs kill
rm -rf ~/frappe-bench/env
- bench -v setup env --python python3.10
- bench pip install -e ./apps/payments
+ bench -v setup env
bench pip install -e ./apps/erpnext
+ bench start &>> ~/frappe-bench/bench_start.log &
bench --site test_site migrate
- bench --site test_site install-app payments
+
+ - name: Show bench output
+ if: ${{ always() }}
+ run: |
+ cd ~/frappe-bench
+ cat bench_start.log || true
+ cd logs
+ for f in ./*.log*; do
+ echo "Printing log: $f";
+ cat $f
+ done
diff --git a/.github/workflows/server-tests-mariadb.yml b/.github/workflows/server-tests-mariadb.yml
index 2ce1125..559be06 100644
--- a/.github/workflows/server-tests-mariadb.yml
+++ b/.github/workflows/server-tests-mariadb.yml
@@ -123,6 +123,10 @@
CI_BUILD_ID: ${{ github.run_id }}
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
+ - name: Show bench output
+ if: ${{ always() }}
+ run: cat ~/frappe-bench/bench_start.log || true
+
- name: Upload coverage data
uses: actions/upload-artifact@v3
with:
diff --git a/erpnext/accounts/doctype/account/account.js b/erpnext/accounts/doctype/account/account.js
index 3c0eb85..bcf7efc 100644
--- a/erpnext/accounts/doctype/account/account.js
+++ b/erpnext/accounts/doctype/account/account.js
@@ -137,9 +137,6 @@
args: {
old: frm.doc.name,
new: data.name,
- is_group: frm.doc.is_group,
- root_type: frm.doc.root_type,
- company: frm.doc.company,
},
callback: function (r) {
if (!r.exc) {
diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py
index c1eca72..02e6c20 100644
--- a/erpnext/accounts/doctype/account/account.py
+++ b/erpnext/accounts/doctype/account/account.py
@@ -18,6 +18,10 @@
pass
+class InvalidAccountMergeError(frappe.ValidationError):
+ pass
+
+
class Account(NestedSet):
nsm_parent_field = "parent_account"
@@ -460,25 +464,34 @@
@frappe.whitelist()
-def merge_account(old, new, is_group, root_type, company):
+def merge_account(old, new):
# Validate properties before merging
new_account = frappe.get_cached_doc("Account", new)
+ old_account = frappe.get_cached_doc("Account", old)
if not new_account:
throw(_("Account {0} does not exist").format(new))
- if (new_account.is_group, new_account.root_type, new_account.company) != (
- cint(is_group),
- root_type,
- company,
+ if (
+ cint(new_account.is_group),
+ new_account.root_type,
+ new_account.company,
+ cstr(new_account.account_currency),
+ ) != (
+ cint(old_account.is_group),
+ old_account.root_type,
+ old_account.company,
+ cstr(old_account.account_currency),
):
throw(
- _(
- """Merging is only possible if following properties are same in both records. Is Group, Root Type, Company"""
- )
+ msg=_(
+ """Merging is only possible if following properties are same in both records. Is Group, Root Type, Company and Account Currency"""
+ ),
+ title=("Invalid Accounts"),
+ exc=InvalidAccountMergeError,
)
- if is_group and new_account.parent_account == old:
+ if old_account.is_group and new_account.parent_account == old:
new_account.db_set("parent_account", frappe.get_cached_value("Account", old, "parent_account"))
frappe.rename_doc("Account", old, new, merge=1, force=1)
diff --git a/erpnext/accounts/doctype/account/test_account.py b/erpnext/accounts/doctype/account/test_account.py
index 62303bd..30eebef 100644
--- a/erpnext/accounts/doctype/account/test_account.py
+++ b/erpnext/accounts/doctype/account/test_account.py
@@ -7,7 +7,11 @@
import frappe
from frappe.test_runner import make_test_records
-from erpnext.accounts.doctype.account.account import merge_account, update_account_number
+from erpnext.accounts.doctype.account.account import (
+ InvalidAccountMergeError,
+ merge_account,
+ update_account_number,
+)
from erpnext.stock import get_company_default_inventory_account, get_warehouse_account
test_dependencies = ["Company"]
@@ -47,49 +51,53 @@
frappe.delete_doc("Account", "1211-11-4 - 6 - Debtors 1 - Test - - _TC")
def test_merge_account(self):
- if not frappe.db.exists("Account", "Current Assets - _TC"):
- acc = frappe.new_doc("Account")
- acc.account_name = "Current Assets"
- acc.is_group = 1
- acc.parent_account = "Application of Funds (Assets) - _TC"
- acc.company = "_Test Company"
- acc.insert()
- if not frappe.db.exists("Account", "Securities and Deposits - _TC"):
- acc = frappe.new_doc("Account")
- acc.account_name = "Securities and Deposits"
- acc.parent_account = "Current Assets - _TC"
- acc.is_group = 1
- acc.company = "_Test Company"
- acc.insert()
- if not frappe.db.exists("Account", "Earnest Money - _TC"):
- acc = frappe.new_doc("Account")
- acc.account_name = "Earnest Money"
- acc.parent_account = "Securities and Deposits - _TC"
- acc.company = "_Test Company"
- acc.insert()
- if not frappe.db.exists("Account", "Cash In Hand - _TC"):
- acc = frappe.new_doc("Account")
- acc.account_name = "Cash In Hand"
- acc.is_group = 1
- acc.parent_account = "Current Assets - _TC"
- acc.company = "_Test Company"
- acc.insert()
- if not frappe.db.exists("Account", "Accumulated Depreciation - _TC"):
- acc = frappe.new_doc("Account")
- acc.account_name = "Accumulated Depreciation"
- acc.parent_account = "Fixed Assets - _TC"
- acc.company = "_Test Company"
- acc.account_type = "Accumulated Depreciation"
- acc.insert()
+ create_account(
+ account_name="Current Assets",
+ is_group=1,
+ parent_account="Application of Funds (Assets) - _TC",
+ company="_Test Company",
+ )
- doc = frappe.get_doc("Account", "Securities and Deposits - _TC")
+ create_account(
+ account_name="Securities and Deposits",
+ is_group=1,
+ parent_account="Current Assets - _TC",
+ company="_Test Company",
+ )
+
+ create_account(
+ account_name="Earnest Money",
+ parent_account="Securities and Deposits - _TC",
+ company="_Test Company",
+ )
+
+ create_account(
+ account_name="Cash In Hand",
+ is_group=1,
+ parent_account="Current Assets - _TC",
+ company="_Test Company",
+ )
+
+ create_account(
+ account_name="Receivable INR",
+ parent_account="Current Assets - _TC",
+ company="_Test Company",
+ account_currency="INR",
+ )
+
+ create_account(
+ account_name="Receivable USD",
+ parent_account="Current Assets - _TC",
+ company="_Test Company",
+ account_currency="USD",
+ )
+
parent = frappe.db.get_value("Account", "Earnest Money - _TC", "parent_account")
self.assertEqual(parent, "Securities and Deposits - _TC")
- merge_account(
- "Securities and Deposits - _TC", "Cash In Hand - _TC", doc.is_group, doc.root_type, doc.company
- )
+ merge_account("Securities and Deposits - _TC", "Cash In Hand - _TC")
+
parent = frappe.db.get_value("Account", "Earnest Money - _TC", "parent_account")
# Parent account of the child account changes after merging
@@ -98,30 +106,28 @@
# Old account doesn't exist after merging
self.assertFalse(frappe.db.exists("Account", "Securities and Deposits - _TC"))
- doc = frappe.get_doc("Account", "Current Assets - _TC")
-
# Raise error as is_group property doesn't match
self.assertRaises(
- frappe.ValidationError,
+ InvalidAccountMergeError,
merge_account,
"Current Assets - _TC",
"Accumulated Depreciation - _TC",
- doc.is_group,
- doc.root_type,
- doc.company,
)
- doc = frappe.get_doc("Account", "Capital Stock - _TC")
-
# Raise error as root_type property doesn't match
self.assertRaises(
- frappe.ValidationError,
+ InvalidAccountMergeError,
merge_account,
"Capital Stock - _TC",
"Softwares - _TC",
- doc.is_group,
- doc.root_type,
- doc.company,
+ )
+
+ # Raise error as currency doesn't match
+ self.assertRaises(
+ InvalidAccountMergeError,
+ merge_account,
+ "Receivable INR - _TC",
+ "Receivable USD - _TC",
)
def test_account_sync(self):
@@ -400,11 +406,20 @@
"Account", filters={"account_name": kwargs.get("account_name"), "company": kwargs.get("company")}
)
if account:
- return account
+ account = frappe.get_doc("Account", account)
+ account.update(
+ dict(
+ is_group=kwargs.get("is_group", 0),
+ parent_account=kwargs.get("parent_account"),
+ )
+ )
+ account.save()
+ return account.name
else:
account = frappe.get_doc(
dict(
doctype="Account",
+ is_group=kwargs.get("is_group", 0),
account_name=kwargs.get("account_name"),
account_type=kwargs.get("account_type"),
parent_account=kwargs.get("parent_account"),
diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
index cfe5e6e..3a2c3cb 100644
--- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
+++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
@@ -265,20 +265,21 @@
@frappe.whitelist()
def get_dimensions(with_cost_center_and_project=False):
- dimension_filters = frappe.db.sql(
- """
- SELECT label, fieldname, document_type
- FROM `tabAccounting Dimension`
- WHERE disabled = 0
- """,
- as_dict=1,
- )
- default_dimensions = frappe.db.sql(
- """SELECT p.fieldname, c.company, c.default_dimension
- FROM `tabAccounting Dimension Detail` c, `tabAccounting Dimension` p
- WHERE c.parent = p.name""",
- as_dict=1,
+ c = frappe.qb.DocType("Accounting Dimension Detail")
+ p = frappe.qb.DocType("Accounting Dimension")
+ dimension_filters = (
+ frappe.qb.from_(p)
+ .select(p.label, p.fieldname, p.document_type)
+ .where(p.disabled == 0)
+ .run(as_dict=1)
+ )
+ default_dimensions = (
+ frappe.qb.from_(c)
+ .inner_join(p)
+ .on(c.parent == p.name)
+ .select(p.fieldname, c.company, c.default_dimension)
+ .run(as_dict=1)
)
if isinstance(with_cost_center_and_project, str):
diff --git a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py
index 25ef2ea..cb7f5f5 100644
--- a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py
+++ b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py
@@ -84,12 +84,22 @@
frappe.set_user("Administrator")
if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}):
- frappe.get_doc(
+ dimension = frappe.get_doc(
{
"doctype": "Accounting Dimension",
"document_type": "Department",
}
- ).insert()
+ )
+ dimension.append(
+ "dimension_defaults",
+ {
+ "company": "_Test Company",
+ "reference_document": "Department",
+ "default_dimension": "_Test Department - _TC",
+ },
+ )
+ dimension.insert()
+ dimension.save()
else:
dimension = frappe.get_doc("Accounting Dimension", "Department")
dimension.disabled = 0
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
index 3da5ac3..9a7a9a3 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
@@ -7,7 +7,9 @@
import frappe
from frappe import _
from frappe.model.document import Document
+from frappe.query_builder.custom import ConstantColumn
from frappe.utils import cint, flt
+from pypika.terms import Parameter
from erpnext import get_default_cost_center
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount
@@ -15,7 +17,7 @@
get_amounts_not_reflected_in_system,
get_entries,
)
-from erpnext.accounts.utils import get_balance_on
+from erpnext.accounts.utils import get_account_currency, get_balance_on
class BankReconciliationTool(Document):
@@ -283,68 +285,68 @@
to_reference_date=None,
):
frappe.flags.auto_reconcile_vouchers = True
- document_types = ["payment_entry", "journal_entry"]
+ reconciled, partially_reconciled = set(), set()
+
bank_transactions = get_bank_transactions(bank_account)
- matched_transaction = []
for transaction in bank_transactions:
linked_payments = get_linked_payments(
transaction.name,
- document_types,
+ ["payment_entry", "journal_entry"],
from_date,
to_date,
filter_by_reference_date,
from_reference_date,
to_reference_date,
)
- vouchers = []
- for r in linked_payments:
- vouchers.append(
- {
- "payment_doctype": r[1],
- "payment_name": r[2],
- "amount": r[4],
- }
- )
- transaction = frappe.get_doc("Bank Transaction", transaction.name)
- account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
- matched_trans = 0
- for voucher in vouchers:
- gl_entry = frappe.db.get_value(
- "GL Entry",
- dict(
- account=account, voucher_type=voucher["payment_doctype"], voucher_no=voucher["payment_name"]
- ),
- ["credit", "debit"],
- as_dict=1,
- )
- gl_amount, transaction_amount = (
- (gl_entry.credit, transaction.deposit)
- if gl_entry.credit > 0
- else (gl_entry.debit, transaction.withdrawal)
- )
- allocated_amount = gl_amount if gl_amount >= transaction_amount else transaction_amount
- transaction.append(
- "payment_entries",
- {
- "payment_document": voucher["payment_doctype"],
- "payment_entry": voucher["payment_name"],
- "allocated_amount": allocated_amount,
+
+ if not linked_payments:
+ continue
+
+ vouchers = list(
+ map(
+ lambda entry: {
+ "payment_doctype": entry.get("doctype"),
+ "payment_name": entry.get("name"),
+ "amount": entry.get("paid_amount"),
},
+ linked_payments,
)
- matched_transaction.append(str(transaction.name))
- transaction.save()
- transaction.update_allocations()
- matched_transaction_len = len(set(matched_transaction))
- if matched_transaction_len == 0:
- frappe.msgprint(_("No matching references found for auto reconciliation"))
- elif matched_transaction_len == 1:
- frappe.msgprint(_("{0} transaction is reconcilied").format(matched_transaction_len))
- else:
- frappe.msgprint(_("{0} transactions are reconcilied").format(matched_transaction_len))
+ )
+
+ updated_transaction = reconcile_vouchers(transaction.name, json.dumps(vouchers))
+
+ if updated_transaction.status == "Reconciled":
+ reconciled.add(updated_transaction.name)
+ elif flt(transaction.unallocated_amount) != flt(updated_transaction.unallocated_amount):
+ # Partially reconciled (status = Unreconciled & unallocated amount changed)
+ partially_reconciled.add(updated_transaction.name)
+
+ alert_message, indicator = get_auto_reconcile_message(partially_reconciled, reconciled)
+ frappe.msgprint(title=_("Auto Reconciliation"), msg=alert_message, indicator=indicator)
frappe.flags.auto_reconcile_vouchers = False
+ return reconciled, partially_reconciled
- return frappe.get_doc("Bank Transaction", transaction.name)
+
+def get_auto_reconcile_message(partially_reconciled, reconciled):
+ """Returns alert message and indicator for auto reconciliation depending on result state."""
+ alert_message, indicator = "", "blue"
+ if not partially_reconciled and not reconciled:
+ alert_message = _("No matches occurred via auto reconciliation")
+ return alert_message, indicator
+
+ indicator = "green"
+ if reconciled:
+ alert_message += _("{0} Transaction(s) Reconciled").format(len(reconciled))
+ alert_message += "<br>"
+
+ if partially_reconciled:
+ alert_message += _("{0} {1} Partially Reconciled").format(
+ len(partially_reconciled),
+ _("Transactions") if len(partially_reconciled) > 1 else _("Transaction"),
+ )
+
+ return alert_message, indicator
@frappe.whitelist()
@@ -390,19 +392,13 @@
"Look up & subtract any existing Bank Transaction allocations"
copied = []
for voucher in vouchers:
- rows = get_total_allocated_amount(voucher[1], voucher[2])
- amount = None
- for row in rows:
- if row["gl_account"] == gl_account:
- amount = row["total"]
- break
+ rows = get_total_allocated_amount(voucher.get("doctype"), voucher.get("name"))
+ filtered_row = list(filter(lambda row: row.get("gl_account") == gl_account, rows))
- if amount:
- l = list(voucher)
- l[3] -= amount
- copied.append(tuple(l))
- else:
- copied.append(voucher)
+ if amount := None if not filtered_row else filtered_row[0]["total"]:
+ voucher["paid_amount"] -= amount
+
+ copied.append(voucher)
return copied
@@ -418,6 +414,18 @@
to_reference_date,
):
exact_match = True if "exact_match" in document_types else False
+ queries = get_queries(
+ bank_account,
+ company,
+ transaction,
+ document_types,
+ from_date,
+ to_date,
+ filter_by_reference_date,
+ from_reference_date,
+ to_reference_date,
+ exact_match,
+ )
filters = {
"amount": transaction.unallocated_amount,
@@ -429,30 +437,15 @@
}
matching_vouchers = []
+ for query in queries:
+ matching_vouchers.extend(frappe.db.sql(query, filters, as_dict=True))
- # get matching vouchers from all the apps
- for method_name in frappe.get_hooks("get_matching_vouchers_for_bank_reconciliation"):
- matching_vouchers.extend(
- frappe.get_attr(method_name)(
- bank_account,
- company,
- transaction,
- document_types,
- from_date,
- to_date,
- filter_by_reference_date,
- from_reference_date,
- to_reference_date,
- exact_match,
- filters,
- )
- or []
- )
-
- return sorted(matching_vouchers, key=lambda x: x[0], reverse=True) if matching_vouchers else []
+ return (
+ sorted(matching_vouchers, key=lambda x: x["rank"], reverse=True) if matching_vouchers else []
+ )
-def get_matching_vouchers_for_bank_reconciliation(
+def get_queries(
bank_account,
company,
transaction,
@@ -463,7 +456,6 @@
from_reference_date,
to_reference_date,
exact_match,
- filters,
):
# get queries to get matching vouchers
account_from_to = "paid_to" if transaction.deposit > 0.0 else "paid_from"
@@ -488,17 +480,7 @@
or []
)
- vouchers = []
-
- for query in queries:
- vouchers.extend(
- frappe.db.sql(
- query,
- filters,
- )
- )
-
- return vouchers
+ return queries
def get_matching_queries(
@@ -515,6 +497,8 @@
to_reference_date,
):
queries = []
+ currency = get_account_currency(bank_account)
+
if "payment_entry" in document_types:
query = get_pe_matching_query(
exact_match,
@@ -541,12 +525,12 @@
queries.append(query)
if transaction.deposit > 0.0 and "sales_invoice" in document_types:
- query = get_si_matching_query(exact_match)
+ query = get_si_matching_query(exact_match, currency)
queries.append(query)
if transaction.withdrawal > 0.0:
if "purchase_invoice" in document_types:
- query = get_pi_matching_query(exact_match)
+ query = get_pi_matching_query(exact_match, currency)
queries.append(query)
if "bank_transaction" in document_types:
@@ -560,33 +544,48 @@
# get matching bank transaction query
# find bank transactions in the same bank account with opposite sign
# same bank account must have same company and currency
+ bt = frappe.qb.DocType("Bank Transaction")
+
field = "deposit" if transaction.withdrawal > 0.0 else "withdrawal"
+ amount_equality = getattr(bt, field) == transaction.unallocated_amount
+ amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
+ amount_condition = amount_equality if exact_match else getattr(bt, field) > 0.0
- return f"""
+ ref_rank = (
+ frappe.qb.terms.Case().when(bt.reference_number == transaction.reference_number, 1).else_(0)
+ )
+ unallocated_rank = (
+ frappe.qb.terms.Case().when(bt.unallocated_amount == transaction.unallocated_amount, 1).else_(0)
+ )
- SELECT
- (CASE WHEN reference_number = %(reference_no)s THEN 1 ELSE 0 END
- + CASE WHEN {field} = %(amount)s THEN 1 ELSE 0 END
- + CASE WHEN ( party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
- + CASE WHEN unallocated_amount = %(amount)s THEN 1 ELSE 0 END
- + 1) AS rank,
- 'Bank Transaction' AS doctype,
- name,
- unallocated_amount AS paid_amount,
- reference_number AS reference_no,
- date AS reference_date,
- party,
- party_type,
- date AS posting_date,
- currency
- FROM
- `tabBank Transaction`
- WHERE
- status != 'Reconciled'
- AND name != '{transaction.name}'
- AND bank_account = '{transaction.bank_account}'
- AND {field} {'= %(amount)s' if exact_match else '> 0.0'}
- """
+ party_condition = (
+ (bt.party_type == transaction.party_type)
+ & (bt.party == transaction.party)
+ & bt.party.isnotnull()
+ )
+ party_rank = frappe.qb.terms.Case().when(party_condition, 1).else_(0)
+
+ query = (
+ frappe.qb.from_(bt)
+ .select(
+ (ref_rank + amount_rank + party_rank + unallocated_rank + 1).as_("rank"),
+ ConstantColumn("Bank Transaction").as_("doctype"),
+ bt.name,
+ bt.unallocated_amount.as_("paid_amount"),
+ bt.reference_number.as_("reference_no"),
+ bt.date.as_("reference_date"),
+ bt.party,
+ bt.party_type,
+ bt.date.as_("posting_date"),
+ bt.currency,
+ )
+ .where(bt.status != "Reconciled")
+ .where(bt.name != transaction.name)
+ .where(bt.bank_account == transaction.bank_account)
+ .where(amount_condition)
+ .where(bt.docstatus == 1)
+ )
+ return str(query)
def get_pe_matching_query(
@@ -600,45 +599,56 @@
to_reference_date,
):
# get matching payment entries query
- if transaction.deposit > 0.0:
- currency_field = "paid_to_account_currency as currency"
- else:
- currency_field = "paid_from_account_currency as currency"
- filter_by_date = f"AND posting_date between '{from_date}' and '{to_date}'"
- order_by = " posting_date"
- filter_by_reference_no = ""
+ to_from = "to" if transaction.deposit > 0.0 else "from"
+ currency_field = f"paid_{to_from}_account_currency"
+ payment_type = "Receive" if transaction.deposit > 0.0 else "Pay"
+ pe = frappe.qb.DocType("Payment Entry")
+
+ ref_condition = pe.reference_no == transaction.reference_number
+ ref_rank = frappe.qb.terms.Case().when(ref_condition, 1).else_(0)
+
+ amount_equality = pe.paid_amount == transaction.unallocated_amount
+ amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
+ amount_condition = amount_equality if exact_match else pe.paid_amount > 0.0
+
+ party_condition = (
+ (pe.party_type == transaction.party_type)
+ & (pe.party == transaction.party)
+ & pe.party.isnotnull()
+ )
+ party_rank = frappe.qb.terms.Case().when(party_condition, 1).else_(0)
+
+ filter_by_date = pe.posting_date.between(from_date, to_date)
if cint(filter_by_reference_date):
- filter_by_date = f"AND reference_date between '{from_reference_date}' and '{to_reference_date}'"
- order_by = " reference_date"
+ filter_by_date = pe.reference_date.between(from_reference_date, to_reference_date)
+
+ query = (
+ frappe.qb.from_(pe)
+ .select(
+ (ref_rank + amount_rank + party_rank + 1).as_("rank"),
+ ConstantColumn("Payment Entry").as_("doctype"),
+ pe.name,
+ pe.paid_amount,
+ pe.reference_no,
+ pe.reference_date,
+ pe.party,
+ pe.party_type,
+ pe.posting_date,
+ getattr(pe, currency_field).as_("currency"),
+ )
+ .where(pe.docstatus == 1)
+ .where(pe.payment_type.isin([payment_type, "Internal Transfer"]))
+ .where(pe.clearance_date.isnull())
+ .where(getattr(pe, account_from_to) == Parameter("%(bank_account)s"))
+ .where(amount_condition)
+ .where(filter_by_date)
+ .orderby(pe.reference_date if cint(filter_by_reference_date) else pe.posting_date)
+ )
+
if frappe.flags.auto_reconcile_vouchers == True:
- filter_by_reference_no = f"AND reference_no = '{transaction.reference_number}'"
- return f"""
- SELECT
- (CASE WHEN reference_no=%(reference_no)s THEN 1 ELSE 0 END
- + CASE WHEN (party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
- + CASE WHEN paid_amount = %(amount)s THEN 1 ELSE 0 END
- + 1 ) AS rank,
- 'Payment Entry' as doctype,
- name,
- paid_amount,
- reference_no,
- reference_date,
- party,
- party_type,
- posting_date,
- {currency_field}
- FROM
- `tabPayment Entry`
- WHERE
- docstatus = 1
- AND payment_type IN (%(payment_type)s, 'Internal Transfer')
- AND ifnull(clearance_date, '') = ""
- AND {account_from_to} = %(bank_account)s
- AND paid_amount {'= %(amount)s' if exact_match else '> 0.0'}
- {filter_by_date}
- {filter_by_reference_no}
- order by{order_by}
- """
+ query = query.where(ref_condition)
+
+ return str(query)
def get_je_matching_query(
@@ -655,100 +665,121 @@
# So one bank could have both types of bank accounts like asset and liability
# So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type
cr_or_dr = "credit" if transaction.withdrawal > 0.0 else "debit"
- filter_by_date = f"AND je.posting_date between '{from_date}' and '{to_date}'"
- order_by = " je.posting_date"
- filter_by_reference_no = ""
+ je = frappe.qb.DocType("Journal Entry")
+ jea = frappe.qb.DocType("Journal Entry Account")
+
+ ref_condition = je.cheque_no == transaction.reference_number
+ ref_rank = frappe.qb.terms.Case().when(ref_condition, 1).else_(0)
+
+ amount_field = f"{cr_or_dr}_in_account_currency"
+ amount_equality = getattr(jea, amount_field) == transaction.unallocated_amount
+ amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
+
+ filter_by_date = je.posting_date.between(from_date, to_date)
if cint(filter_by_reference_date):
- filter_by_date = f"AND je.cheque_date between '{from_reference_date}' and '{to_reference_date}'"
- order_by = " je.cheque_date"
- if frappe.flags.auto_reconcile_vouchers == True:
- filter_by_reference_no = f"AND je.cheque_no = '{transaction.reference_number}'"
- return f"""
- SELECT
- (CASE WHEN je.cheque_no=%(reference_no)s THEN 1 ELSE 0 END
- + CASE WHEN jea.{cr_or_dr}_in_account_currency = %(amount)s THEN 1 ELSE 0 END
- + 1) AS rank ,
- 'Journal Entry' AS doctype,
+ filter_by_date = je.cheque_date.between(from_reference_date, to_reference_date)
+
+ query = (
+ frappe.qb.from_(jea)
+ .join(je)
+ .on(jea.parent == je.name)
+ .select(
+ (ref_rank + amount_rank + 1).as_("rank"),
+ ConstantColumn("Journal Entry").as_("doctype"),
je.name,
- jea.{cr_or_dr}_in_account_currency AS paid_amount,
- je.cheque_no AS reference_no,
- je.cheque_date AS reference_date,
- je.pay_to_recd_from AS party,
+ getattr(jea, amount_field).as_("paid_amount"),
+ je.cheque_no.as_("reference_no"),
+ je.cheque_date.as_("reference_date"),
+ je.pay_to_recd_from.as_("party"),
jea.party_type,
je.posting_date,
- jea.account_currency AS currency
- FROM
- `tabJournal Entry Account` AS jea
- JOIN
- `tabJournal Entry` AS je
- ON
- jea.parent = je.name
- WHERE
- je.docstatus = 1
- AND je.voucher_type NOT IN ('Opening Entry')
- AND (je.clearance_date IS NULL OR je.clearance_date='0000-00-00')
- AND jea.account = %(bank_account)s
- AND jea.{cr_or_dr}_in_account_currency {'= %(amount)s' if exact_match else '> 0.0'}
- AND je.docstatus = 1
- {filter_by_date}
- {filter_by_reference_no}
- order by {order_by}
- """
+ jea.account_currency.as_("currency"),
+ )
+ .where(je.docstatus == 1)
+ .where(je.voucher_type != "Opening Entry")
+ .where(je.clearance_date.isnull())
+ .where(jea.account == Parameter("%(bank_account)s"))
+ .where(amount_equality if exact_match else getattr(jea, amount_field) > 0.0)
+ .where(je.docstatus == 1)
+ .where(filter_by_date)
+ .orderby(je.cheque_date if cint(filter_by_reference_date) else je.posting_date)
+ )
+
+ if frappe.flags.auto_reconcile_vouchers == True:
+ query = query.where(ref_condition)
+
+ return str(query)
-def get_si_matching_query(exact_match):
+def get_si_matching_query(exact_match, currency):
# get matching sales invoice query
- return f"""
- SELECT
- ( CASE WHEN si.customer = %(party)s THEN 1 ELSE 0 END
- + CASE WHEN sip.amount = %(amount)s THEN 1 ELSE 0 END
- + 1 ) AS rank,
- 'Sales Invoice' as doctype,
+ si = frappe.qb.DocType("Sales Invoice")
+ sip = frappe.qb.DocType("Sales Invoice Payment")
+
+ amount_equality = sip.amount == Parameter("%(amount)s")
+ amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
+ amount_condition = amount_equality if exact_match else sip.amount > 0.0
+
+ party_condition = si.customer == Parameter("%(party)s")
+ party_rank = frappe.qb.terms.Case().when(party_condition, 1).else_(0)
+
+ query = (
+ frappe.qb.from_(sip)
+ .join(si)
+ .on(sip.parent == si.name)
+ .select(
+ (party_rank + amount_rank + 1).as_("rank"),
+ ConstantColumn("Sales Invoice").as_("doctype"),
si.name,
- sip.amount as paid_amount,
- '' as reference_no,
- '' as reference_date,
- si.customer as party,
- 'Customer' as party_type,
+ sip.amount.as_("paid_amount"),
+ ConstantColumn("").as_("reference_no"),
+ ConstantColumn("").as_("reference_date"),
+ si.customer.as_("party"),
+ ConstantColumn("Customer").as_("party_type"),
si.posting_date,
- si.currency
+ si.currency,
+ )
+ .where(si.docstatus == 1)
+ .where(sip.clearance_date.isnull())
+ .where(sip.account == Parameter("%(bank_account)s"))
+ .where(amount_condition)
+ .where(si.currency == currency)
+ )
- FROM
- `tabSales Invoice Payment` as sip
- JOIN
- `tabSales Invoice` as si
- ON
- sip.parent = si.name
- WHERE
- si.docstatus = 1
- AND (sip.clearance_date is null or sip.clearance_date='0000-00-00')
- AND sip.account = %(bank_account)s
- AND sip.amount {'= %(amount)s' if exact_match else '> 0.0'}
- """
+ return str(query)
-def get_pi_matching_query(exact_match):
+def get_pi_matching_query(exact_match, currency):
# get matching purchase invoice query when they are also used as payment entries (is_paid)
- return f"""
- SELECT
- ( CASE WHEN supplier = %(party)s THEN 1 ELSE 0 END
- + CASE WHEN paid_amount = %(amount)s THEN 1 ELSE 0 END
- + 1 ) AS rank,
- 'Purchase Invoice' as doctype,
- name,
- paid_amount,
- '' as reference_no,
- '' as reference_date,
- supplier as party,
- 'Supplier' as party_type,
- posting_date,
- currency
- FROM
- `tabPurchase Invoice`
- WHERE
- docstatus = 1
- AND is_paid = 1
- AND ifnull(clearance_date, '') = ""
- AND cash_bank_account = %(bank_account)s
- AND paid_amount {'= %(amount)s' if exact_match else '> 0.0'}
- """
+ purchase_invoice = frappe.qb.DocType("Purchase Invoice")
+
+ amount_equality = purchase_invoice.paid_amount == Parameter("%(amount)s")
+ amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
+ amount_condition = amount_equality if exact_match else purchase_invoice.paid_amount > 0.0
+
+ party_condition = purchase_invoice.supplier == Parameter("%(party)s")
+ party_rank = frappe.qb.terms.Case().when(party_condition, 1).else_(0)
+
+ query = (
+ frappe.qb.from_(purchase_invoice)
+ .select(
+ (party_rank + amount_rank + 1).as_("rank"),
+ ConstantColumn("Purchase Invoice").as_("doctype"),
+ purchase_invoice.name,
+ purchase_invoice.paid_amount,
+ ConstantColumn("").as_("reference_no"),
+ ConstantColumn("").as_("reference_date"),
+ purchase_invoice.supplier.as_("party"),
+ ConstantColumn("Supplier").as_("party_type"),
+ purchase_invoice.posting_date,
+ purchase_invoice.currency,
+ )
+ .where(purchase_invoice.docstatus == 1)
+ .where(purchase_invoice.is_paid == 1)
+ .where(purchase_invoice.clearance_date.isnull())
+ .where(purchase_invoice.cash_bank_account == Parameter("%(bank_account)s"))
+ .where(amount_condition)
+ .where(purchase_invoice.currency == currency)
+ )
+
+ return str(query)
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/test_bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/test_bank_reconciliation_tool.py
index 599ced5..5a6bb69 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/test_bank_reconciliation_tool.py
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/test_bank_reconciliation_tool.py
@@ -1,9 +1,100 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-# import frappe
import unittest
+import frappe
+from frappe import qb
+from frappe.tests.utils import FrappeTestCase, change_settings
+from frappe.utils import add_days, flt, getdate, today
-class TestBankReconciliationTool(unittest.TestCase):
- pass
+from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
+ auto_reconcile_vouchers,
+ get_bank_transactions,
+)
+from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
+from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
+
+
+class TestBankReconciliationTool(AccountsTestMixin, FrappeTestCase):
+ def setUp(self):
+ self.create_company()
+ self.create_customer()
+ self.clear_old_entries()
+ bank_dt = qb.DocType("Bank")
+ q = qb.from_(bank_dt).delete().where(bank_dt.name == "HDFC").run()
+ self.create_bank_account()
+
+ def tearDown(self):
+ frappe.db.rollback()
+
+ def create_bank_account(self):
+ bank = frappe.get_doc(
+ {
+ "doctype": "Bank",
+ "bank_name": "HDFC",
+ }
+ ).save()
+
+ self.bank_account = (
+ frappe.get_doc(
+ {
+ "doctype": "Bank Account",
+ "account_name": "HDFC _current_",
+ "bank": bank,
+ "is_company_account": True,
+ "account": self.bank, # account from Chart of Accounts
+ }
+ )
+ .insert()
+ .name
+ )
+
+ def test_auto_reconcile(self):
+ # make payment
+ from_date = add_days(today(), -1)
+ to_date = today()
+ payment = create_payment_entry(
+ company=self.company,
+ posting_date=from_date,
+ payment_type="Receive",
+ party_type="Customer",
+ party=self.customer,
+ paid_from=self.debit_to,
+ paid_to=self.bank,
+ paid_amount=100,
+ ).save()
+ payment.reference_no = "123"
+ payment = payment.save().submit()
+
+ # make bank transaction
+ bank_transaction = (
+ frappe.get_doc(
+ {
+ "doctype": "Bank Transaction",
+ "date": to_date,
+ "deposit": 100,
+ "bank_account": self.bank_account,
+ "reference_number": "123",
+ }
+ )
+ .save()
+ .submit()
+ )
+
+ # assert API output pre reconciliation
+ transactions = get_bank_transactions(self.bank_account, from_date, to_date)
+ self.assertEqual(len(transactions), 1)
+ self.assertEqual(transactions[0].name, bank_transaction.name)
+
+ # auto reconcile
+ auto_reconcile_vouchers(
+ bank_account=self.bank_account,
+ from_date=from_date,
+ to_date=to_date,
+ filter_by_reference_date=False,
+ )
+
+ # assert API output post reconciliation
+ transactions = get_bank_transactions(self.bank_account, from_date, to_date)
+ self.assertEqual(len(transactions), 0)
diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
index 59905da..0c328ff 100644
--- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
@@ -47,7 +47,7 @@
from_date=bank_transaction.date,
to_date=utils.today(),
)
- self.assertTrue(linked_payments[0][6] == "Conrad Electronic")
+ self.assertTrue(linked_payments[0]["party"] == "Conrad Electronic")
# This test validates a simple reconciliation leading to the clearance of the bank transaction and the payment
def test_reconcile(self):
@@ -93,7 +93,7 @@
from_date=bank_transaction.date,
to_date=utils.today(),
)
- self.assertTrue(linked_payments[0][3])
+ self.assertTrue(linked_payments[0]["paid_amount"])
# Check error if already reconciled
def test_already_reconciled(self):
@@ -188,7 +188,7 @@
repayment_entry = create_loan_and_repayment()
linked_payments = get_linked_payments(bank_transaction.name, ["loan_repayment", "exact_match"])
- self.assertEqual(linked_payments[0][2], repayment_entry.name)
+ self.assertEqual(linked_payments[0]["name"], repayment_entry.name)
@if_lending_app_installed
diff --git a/erpnext/accounts/doctype/ledger_merge/ledger_merge.py b/erpnext/accounts/doctype/ledger_merge/ledger_merge.py
index 381083b..362d273 100644
--- a/erpnext/accounts/doctype/ledger_merge/ledger_merge.py
+++ b/erpnext/accounts/doctype/ledger_merge/ledger_merge.py
@@ -48,9 +48,6 @@
merge_account(
row.account,
ledger_merge.account,
- ledger_merge.is_group,
- ledger_merge.root_type,
- ledger_merge.company,
)
row.db_set("merged", 1)
frappe.db.commit()
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index feacb0f..b16b03e 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -1588,6 +1588,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/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/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index efe9741..c8c9ad1 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -161,6 +161,7 @@
}
this.frm.set_df_property("tax_withholding_category", "hidden", doc.apply_tds ? 0 : 1);
+ erpnext.accounts.unreconcile_payments.add_unreconcile_btn(me.frm);
}
unblock_invoice() {
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index ce7ada3..b4dd75a 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -1164,7 +1164,7 @@
item = create_item("_Test Item for Deferred Accounting", is_purchase_item=True)
item.enable_deferred_expense = 1
- item.deferred_expense_account = deferred_account
+ item.item_defaults[0].deferred_expense_account = deferred_account
item.save()
pi = make_purchase_invoice(item=item.name, qty=1, rate=100, do_not_save=True)
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 21b39d7..9ffdaf6 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -2322,7 +2322,7 @@
item = create_item("_Test Item for Deferred Accounting")
item.enable_deferred_revenue = 1
- item.deferred_revenue_account = deferred_account
+ item.item_defaults[0].deferred_revenue_account = deferred_account
item.no_of_months = 12
item.save()
@@ -3102,7 +3102,7 @@
item = create_item("_Test Item for Deferred Accounting")
item.enable_deferred_expense = 1
- item.deferred_revenue_account = deferred_account
+ item.item_defaults[0].deferred_revenue_account = deferred_account
item.save()
si = create_sales_invoice(
diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py
index 28d0c20..7b1a902 100644
--- a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py
+++ b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py
@@ -81,7 +81,7 @@
self.create_item("_Test Internet Subscription", 0, self.warehouse, self.company)
item = frappe.get_doc("Item", self.item)
item.enable_deferred_revenue = 1
- item.deferred_revenue_account = self.deferred_revenue_account
+ item.item_defaults[0].deferred_revenue_account = self.deferred_revenue_account
item.no_of_months = 3
item.save()
@@ -150,7 +150,7 @@
self.create_item("_Test Office Desk", 0, self.warehouse, self.company)
item = frappe.get_doc("Item", self.item)
item.enable_deferred_expense = 1
- item.deferred_expense_account = self.deferred_expense_account
+ item.item_defaults[0].deferred_expense_account = self.deferred_expense_account
item.no_of_months_exp = 3
item.save()
diff --git a/erpnext/accounts/test/accounts_mixin.py b/erpnext/accounts/test/accounts_mixin.py
index 0868860..d503f7b 100644
--- a/erpnext/accounts/test/accounts_mixin.py
+++ b/erpnext/accounts/test/accounts_mixin.py
@@ -158,6 +158,8 @@
"Journal Entry",
"Sales Order",
"Exchange Rate Revaluation",
+ "Bank Account",
+ "Bank Transaction",
]
for doctype in doctype_list:
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js
index dae993a..03afcb9 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.js
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.js
@@ -40,16 +40,6 @@
}
}
});
-
- let sbb_field = frm.get_docfield('stock_items', 'serial_and_batch_bundle');
- if (sbb_field) {
- sbb_field.get_route_options_for_new_doc = (row) => {
- return {
- 'item_code': row.doc.item_code,
- 'voucher_type': frm.doc.doctype,
- }
- };
- }
},
refresh: function(frm) {
@@ -61,6 +51,16 @@
frappe.set_route("query-report", "General Ledger");
});
}
+
+ let sbb_field = frm.get_docfield('stock_items', 'serial_and_batch_bundle');
+ if (sbb_field) {
+ sbb_field.get_route_options_for_new_doc = (row) => {
+ return {
+ 'item_code': row.doc.item_code,
+ 'voucher_type': frm.doc.doctype,
+ }
+ };
+ }
},
repair_status: (frm) => {
diff --git a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py
index 21241e0..0718735 100644
--- a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py
+++ b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py
@@ -7,7 +7,7 @@
import frappe
from frappe import _
from frappe.query_builder.functions import Coalesce, Sum
-from frappe.utils import date_diff, flt, getdate
+from frappe.utils import cint, date_diff, flt, getdate
def execute(filters=None):
@@ -47,8 +47,10 @@
mr.transaction_date.as_("date"),
mr_item.schedule_date.as_("required_date"),
mr_item.item_code.as_("item_code"),
- Sum(Coalesce(mr_item.stock_qty, 0)).as_("qty"),
- Coalesce(mr_item.stock_uom, "").as_("uom"),
+ Sum(Coalesce(mr_item.qty, 0)).as_("qty"),
+ Sum(Coalesce(mr_item.stock_qty, 0)).as_("stock_qty"),
+ Coalesce(mr_item.uom, "").as_("uom"),
+ Coalesce(mr_item.stock_uom, "").as_("stock_uom"),
Sum(Coalesce(mr_item.ordered_qty, 0)).as_("ordered_qty"),
Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"),
(Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.received_qty, 0))).as_(
@@ -96,7 +98,7 @@
def update_qty_columns(row_to_update, data_row):
- fields = ["qty", "ordered_qty", "received_qty", "qty_to_receive", "qty_to_order"]
+ fields = ["qty", "stock_qty", "ordered_qty", "received_qty", "qty_to_receive", "qty_to_order"]
for field in fields:
row_to_update[field] += flt(data_row[field])
@@ -104,16 +106,20 @@
def prepare_data(data, filters):
"""Prepare consolidated Report data and Chart data"""
material_request_map, item_qty_map = {}, {}
+ precision = cint(frappe.db.get_default("float_precision")) or 2
for row in data:
# item wise map for charts
if not row["item_code"] in item_qty_map:
item_qty_map[row["item_code"]] = {
- "qty": row["qty"],
- "ordered_qty": row["ordered_qty"],
- "received_qty": row["received_qty"],
- "qty_to_receive": row["qty_to_receive"],
- "qty_to_order": row["qty_to_order"],
+ "qty": flt(row["stock_qty"], precision),
+ "stock_qty": flt(row["stock_qty"], precision),
+ "stock_uom": row["stock_uom"],
+ "uom": row["uom"],
+ "ordered_qty": flt(row["ordered_qty"], precision),
+ "received_qty": flt(row["received_qty"], precision),
+ "qty_to_receive": flt(row["qty_to_receive"], precision),
+ "qty_to_order": flt(row["qty_to_order"], precision),
}
else:
item_entry = item_qty_map[row["item_code"]]
@@ -200,21 +206,34 @@
{"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 100},
{"label": _("Description"), "fieldname": "description", "fieldtype": "Data", "width": 200},
{
- "label": _("Stock UOM"),
+ "label": _("UOM"),
"fieldname": "uom",
"fieldtype": "Data",
"width": 100,
},
+ {
+ "label": _("Stock UOM"),
+ "fieldname": "stock_uom",
+ "fieldtype": "Data",
+ "width": 100,
+ },
]
)
columns.extend(
[
{
- "label": _("Stock Qty"),
+ "label": _("Qty"),
"fieldname": "qty",
"fieldtype": "Float",
- "width": 120,
+ "width": 140,
+ "convertible": "qty",
+ },
+ {
+ "label": _("Qty in Stock UOM"),
+ "fieldname": "stock_qty",
+ "fieldtype": "Float",
+ "width": 140,
"convertible": "qty",
},
{
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index b1ce539..c302ece 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -190,10 +190,13 @@
purchase_doc_field = (
"purchase_receipt" if self.doctype == "Purchase Receipt" else "purchase_invoice"
)
- not_cancelled_asset = [
- d.name
- for d in frappe.db.get_all("Asset", {purchase_doc_field: self.return_against, "docstatus": 1})
- ]
+ not_cancelled_asset = []
+ if self.return_against:
+ not_cancelled_asset = [
+ d.name
+ for d in frappe.db.get_all("Asset", {purchase_doc_field: self.return_against, "docstatus": 1})
+ ]
+
if self.is_return and len(not_cancelled_asset):
frappe.throw(
_(
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index c7398cc..7bf8fb4 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -555,8 +555,6 @@
"erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_matching_queries"
)
-get_matching_vouchers_for_bank_reconciliation = "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_matching_vouchers_for_bank_reconciliation"
-
get_amounts_not_reflected_in_system_for_bank_reconciliation_statement = "erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement.get_amounts_not_reflected_in_system_for_bank_reconciliation_statement"
get_payment_entries_for_bank_clearance = (
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js
index f1e6094..6621275 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.js
+++ b/erpnext/manufacturing/doctype/job_card/job_card.js
@@ -23,17 +23,6 @@
}
});
- let sbb_field = frm.get_docfield('serial_and_batch_bundle');
- if (sbb_field) {
- sbb_field.get_route_options_for_new_doc = () => {
- return {
- 'item_code': frm.doc.production_item,
- 'warehouse': frm.doc.wip_warehouse,
- 'voucher_type': frm.doc.doctype,
- }
- };
- }
-
frm.set_indicator_formatter('sub_operation',
function(doc) {
if (doc.status == "Pending") {
@@ -124,6 +113,17 @@
}
});
}
+
+ let sbb_field = frm.get_docfield('serial_and_batch_bundle');
+ if (sbb_field) {
+ sbb_field.get_route_options_for_new_doc = () => {
+ return {
+ 'item_code': frm.doc.production_item,
+ 'warehouse': frm.doc.wip_warehouse,
+ 'voucher_type': frm.doc.doctype,
+ }
+ };
+ }
},
setup_quality_inspection: function(frm) {
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 7a07f56..dda4d36 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -339,10 +339,8 @@
erpnext.patches.v15_0.remove_exotel_integration
erpnext.patches.v14_0.single_to_multi_dunning
execute:frappe.db.set_single_value('Selling Settings', 'allow_negative_rates_for_items', 0)
-execute:frappe.delete_doc('DocType', 'Twitter Settings', ignore_missing=True)
-execute:frappe.delete_doc('DocType', 'LinkedIn Settings', ignore_missing=True)
-execute:frappe.delete_doc('DocType', 'Social Media Post', ignore_missing=True)
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
# below migration patch should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
diff --git a/erpnext/patches/v14_0/delete_education_doctypes.py b/erpnext/patches/v14_0/delete_education_doctypes.py
index 56a596a..aeeda70 100644
--- a/erpnext/patches/v14_0/delete_education_doctypes.py
+++ b/erpnext/patches/v14_0/delete_education_doctypes.py
@@ -47,13 +47,16 @@
for doctype in doctypes:
frappe.delete_doc("DocType", doctype, ignore_missing=True)
- portal_settings = frappe.get_doc("Portal Settings")
-
- for row in portal_settings.get("menu"):
- if row.reference_doctype in doctypes:
- row.delete()
-
- portal_settings.save()
+ titles = [
+ "Fees",
+ "Student Admission",
+ "Grant Application",
+ "Chapter",
+ "Certification Application",
+ ]
+ items = frappe.get_all("Portal Menu Item", filters=[["title", "in", titles]], pluck="name")
+ for item in items:
+ frappe.delete_doc("Portal Menu Item", item, ignore_missing=True, force=True)
frappe.delete_doc("Module Def", "Education", ignore_missing=True, force=True)
diff --git a/erpnext/patches/v14_0/delete_healthcare_doctypes.py b/erpnext/patches/v14_0/delete_healthcare_doctypes.py
index 2c699e4..896a440 100644
--- a/erpnext/patches/v14_0/delete_healthcare_doctypes.py
+++ b/erpnext/patches/v14_0/delete_healthcare_doctypes.py
@@ -41,7 +41,7 @@
for card in cards:
frappe.delete_doc("Number Card", card, ignore_missing=True, force=True)
- titles = ["Lab Test", "Prescription", "Patient Appointment"]
+ titles = ["Lab Test", "Prescription", "Patient Appointment", "Patient"]
items = frappe.get_all("Portal Menu Item", filters=[["title", "in", titles]], pluck="name")
for item in items:
frappe.delete_doc("Portal Menu Item", item, ignore_missing=True, force=True)
diff --git a/erpnext/patches/v14_0/migrate_deferred_accounts_to_item_defaults.py b/erpnext/patches/v14_0/migrate_deferred_accounts_to_item_defaults.py
new file mode 100644
index 0000000..44b830b
--- /dev/null
+++ b/erpnext/patches/v14_0/migrate_deferred_accounts_to_item_defaults.py
@@ -0,0 +1,39 @@
+import frappe
+
+
+def execute():
+ try:
+ item_dict = get_deferred_accounts()
+ add_to_item_defaults(item_dict)
+ except Exception:
+ frappe.db.rollback()
+ frappe.log_error("Failed to migrate deferred accounts in Item Defaults.")
+
+
+def get_deferred_accounts():
+ item = frappe.qb.DocType("Item")
+ return (
+ frappe.qb.from_(item)
+ .select(item.name, item.deferred_expense_account, item.deferred_revenue_account)
+ .where((item.enable_deferred_expense == 1) | (item.enable_deferred_revenue == 1))
+ .run(as_dict=True)
+ )
+
+
+def add_to_item_defaults(item_dict):
+ for item in item_dict:
+ add_company_wise_item_default(item, "deferred_expense_account")
+ add_company_wise_item_default(item, "deferred_revenue_account")
+
+
+def add_company_wise_item_default(item, account_type):
+ company = frappe.get_cached_value("Account", item[account_type], "company")
+ if company and item[account_type]:
+ item_defaults = frappe.get_cached_value("Item", item["name"], "item_defaults")
+ for item_row in item_defaults:
+ if item_row.company == company:
+ frappe.set_value("Item Default", item_row.name, account_type, item[account_type])
+ break
+ else:
+ item_defaults.append({"company": company, account_type: item[account_type]})
+ frappe.set_value("Item", item["name"], "item_defaults", item_defaults)
diff --git a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
index 52fa8ab..1f47347 100644
--- a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
+++ b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
@@ -134,12 +134,12 @@
format_row(row) {
return [
- row[1], // Document Type
- row[2], // Document Name
- row[5] || row[8], // Reference Date
- format_currency(row[3], row[9]), // Remaining
- row[4], // Reference Number
- row[6], // Party
+ row["doctype"],
+ row["name"],
+ row["reference_date"] || row["posting_date"],
+ format_currency(row["paid_amount"], row["currency"]),
+ row["reference_no"],
+ row["party"],
];
}
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 80d7b79..975adc2 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -134,15 +134,6 @@
}
}
});
-
- let sbb_field = this.frm.get_docfield('items', 'serial_and_batch_bundle');
- if (sbb_field) {
- sbb_field.get_route_options_for_new_doc = (row) => {
- return {
- 'item_code': row.doc.item_code,
- }
- };
- }
}
if(
@@ -207,15 +198,6 @@
});
}
- let batch_no_field = this.frm.get_docfield("items", "batch_no");
- if (batch_no_field) {
- batch_no_field.get_route_options_for_new_doc = function(row) {
- return {
- "item": row.doc.item_code
- }
- };
- }
-
if (this.frm.fields_dict["items"].grid.get_field('blanket_order')) {
this.frm.set_query("blanket_order", "items", function(doc, cdt, cdn) {
var item = locals[cdt][cdn];
@@ -268,6 +250,28 @@
}
]);
}
+
+ if(this.frm.fields_dict['items'].grid.get_field('serial_and_batch_bundle')) {
+ let sbb_field = this.frm.get_docfield('items', 'serial_and_batch_bundle');
+ if (sbb_field) {
+ sbb_field.get_route_options_for_new_doc = (row) => {
+ return {
+ 'item_code': row.doc.item_code,
+ }
+ };
+ }
+ }
+
+ if(this.frm.fields_dict['items'].grid.get_field('batch_no')) {
+ let batch_no_field = this.frm.get_docfield('items', 'batch_no');
+ if (batch_no_field) {
+ batch_no_field.get_route_options_for_new_doc = function(row) {
+ return {
+ 'item': row.doc.item_code
+ }
+ };
+ }
+ }
}
is_return() {
@@ -358,7 +362,6 @@
}
refresh() {
-
erpnext.toggle_naming_series();
erpnext.hide_company();
this.set_dynamic_labels();
diff --git a/erpnext/public/js/utils/demo.js b/erpnext/public/js/utils/demo.js
index 3ebc5ef..ef83cf6 100644
--- a/erpnext/public/js/utils/demo.js
+++ b/erpnext/public/js/utils/demo.js
@@ -1,91 +1,34 @@
+frappe.provide("erpnext.demo");
+
$(document).on("toolbar_setup", function () {
if (frappe.boot.sysdefaults.demo_company) {
- render_clear_demo_button();
+ render_clear_demo_action();
}
-
- // for first load after setup.
- frappe.realtime.on("demo_data_complete", () => {
- render_clear_demo_button();
- });
});
-function render_clear_demo_button() {
- let wait_for_onboaring_tours = setInterval(() => {
- if ($("#driver-page-overlay").length || $("#show-dialog").length) {
- return;
- }
- setup_clear_demo_button();
- clearInterval(wait_for_onboaring_tours);
- }, 2000);
-}
-
-function setup_clear_demo_button() {
- let message_string = __(
- "Demo data is present on the system, erase data before starting real usage."
+function render_clear_demo_action() {
+ let demo_action = $(
+ `<a class="dropdown-item" onclick="return erpnext.demo.clear_demo()">
+ ${__("Clear Demo Data")}
+ </a>`
);
- let $floatingBar = $(`
- <div class="flex justify-content-center" style="width: 100%;">
- <div class="flex justify-content-center flex-col shadow rounded p-2"
- style="
- background-color: #e0f2fe;
- position: fixed;
- bottom: 20px;
- z-index: 1;">
- <p style="margin: auto 0; padding-left: 10px; margin-right: 20px; font-size: 15px;">
- ${message_string}
- </p>
- <button id="clear-demo" type="button"
- class="
- px-4
- py-2
- border
- border-transparent
- text-white
- "
- style="
- margin: auto 0;
- height: fit-content;
- background-color: #007bff;
- border-radius: 5px;
- margin-right: 10px
- "
- >
- Clear Demo Data
- </button>
- <a type="button" id="dismiss-demo-banner" class="text-muted" style="align-self: center">
- <svg class="icon" style="">
- <use class="" href="#icon-close"></use>
- </svg>
- </a>
- </div>
- </div>
- `);
-
- $("footer").append($floatingBar);
-
- $("#clear-demo").on("click", function () {
- frappe.confirm(
- __("Are you sure you want to clear all demo data?"),
- () => {
- frappe.call({
- method: "erpnext.setup.demo.clear_demo_data",
- freeze: true,
- freeze_message: __("Clearing Demo Data..."),
- callback: function (r) {
- frappe.ui.toolbar.clear_cache();
- frappe.show_alert({
- message: __("Demo data cleared"),
- indicator: "green",
- });
- $("footer").remove($floatingBar);
- },
- });
- }
- );
- });
-
- $("#dismiss-demo-banner").on("click", function () {
- $floatingBar.remove();
- });
+ demo_action.appendTo($("#toolbar-user"));
}
+
+erpnext.demo.clear_demo = function () {
+ frappe.confirm(__("Are you sure you want to clear all demo data?"), () => {
+ frappe.call({
+ method: "erpnext.setup.demo.clear_demo_data",
+ freeze: true,
+ freeze_message: __("Clearing Demo Data..."),
+ callback: function (r) {
+ frappe.ui.toolbar.clear_cache();
+ frappe.show_alert({
+ message: __("Demo data cleared"),
+ indicator: "green",
+ });
+ },
+ });
+ });
+};
diff --git a/erpnext/public/js/utils/unreconcile.js b/erpnext/public/js/utils/unreconcile.js
index acc77a6..bbdd51d 100644
--- a/erpnext/public/js/utils/unreconcile.js
+++ b/erpnext/public/js/utils/unreconcile.js
@@ -19,7 +19,7 @@
if (r.message) {
frm.add_custom_button(__("Un-Reconcile"), function() {
erpnext.accounts.unreconcile_payments.build_unreconcile_dialog(frm);
- });
+ }, __('Actions'));
}
}
});
diff --git a/erpnext/selling/doctype/installation_note/installation_note.js b/erpnext/selling/doctype/installation_note/installation_note.js
index dd6f8a8..8128c77 100644
--- a/erpnext/selling/doctype/installation_note/installation_note.js
+++ b/erpnext/selling/doctype/installation_note/installation_note.js
@@ -18,6 +18,14 @@
}
}
});
+ },
+ onload: function(frm) {
+ if(!frm.doc.status) {
+ frm.set_value({ status:'Draft'});
+ }
+ if(frm.doc.__islocal) {
+ frm.set_value({inst_date: frappe.datetime.get_today()});
+ }
let sbb_field = frm.get_docfield('items', 'serial_and_batch_bundle');
if (sbb_field) {
@@ -29,14 +37,6 @@
};
}
},
- onload: function(frm) {
- if(!frm.doc.status) {
- frm.set_value({ status:'Draft'});
- }
- if(frm.doc.__islocal) {
- frm.set_value({inst_date: frappe.datetime.get_today()});
- }
- },
customer: function(frm) {
erpnext.utils.get_party_details(frm);
},
diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js
index d18f70a..1bc8d68 100644
--- a/erpnext/selling/doctype/quotation/quotation.js
+++ b/erpnext/selling/doctype/quotation/quotation.js
@@ -48,6 +48,11 @@
}
}
});
+ },
+
+ refresh: function(frm) {
+ frm.trigger("set_label");
+ frm.trigger("set_dynamic_field_label");
let sbb_field = frm.get_docfield('packed_items', 'serial_and_batch_bundle');
if (sbb_field) {
@@ -61,11 +66,6 @@
}
},
- refresh: function(frm) {
- frm.trigger("set_label");
- frm.trigger("set_dynamic_field_label");
- },
-
quotation_to: function(frm) {
frm.trigger("set_label");
frm.trigger("toggle_reqd_lead_customer");
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index 76e8866..4ae9bf5 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -350,18 +350,20 @@
}
}
- frm.fields_dict['deferred_revenue_account'].get_query = function() {
+ frm.fields_dict["item_defaults"].grid.get_field("deferred_revenue_account").get_query = function(doc, cdt, cdn) {
return {
filters: {
+ "company": locals[cdt][cdn].company,
'root_type': 'Liability',
"is_group": 0
}
}
}
- frm.fields_dict['deferred_expense_account'].get_query = function() {
+ frm.fields_dict["item_defaults"].grid.get_field("deferred_expense_account").get_query = function(doc, cdt, cdn) {
return {
filters: {
+ "company": locals[cdt][cdn].company,
'root_type': 'Asset',
"is_group": 0
}
diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json
index 756d004..1bcddfa 100644
--- a/erpnext/stock/doctype/item/item.json
+++ b/erpnext/stock/doctype/item/item.json
@@ -69,6 +69,13 @@
"variant_based_on",
"attributes",
"accounting",
+ "deferred_accounting_section",
+ "enable_deferred_expense",
+ "no_of_months_exp",
+ "column_break_9s9o",
+ "enable_deferred_revenue",
+ "no_of_months",
+ "section_break_avcp",
"item_defaults",
"purchasing_tab",
"purchase_uom",
@@ -84,10 +91,6 @@
"delivered_by_supplier",
"column_break2",
"supplier_items",
- "deferred_expense_section",
- "enable_deferred_expense",
- "deferred_expense_account",
- "no_of_months_exp",
"foreign_trade_details",
"country_of_origin",
"column_break_59",
@@ -98,10 +101,6 @@
"is_sales_item",
"column_break3",
"max_discount",
- "deferred_revenue",
- "enable_deferred_revenue",
- "deferred_revenue_account",
- "no_of_months",
"customer_details",
"customer_items",
"item_tax_section_break",
@@ -658,20 +657,6 @@
"oldfieldtype": "Currency"
},
{
- "collapsible": 1,
- "fieldname": "deferred_revenue",
- "fieldtype": "Section Break",
- "label": "Deferred Revenue"
- },
- {
- "depends_on": "enable_deferred_revenue",
- "fieldname": "deferred_revenue_account",
- "fieldtype": "Link",
- "ignore_user_permissions": 1,
- "label": "Deferred Revenue Account",
- "options": "Account"
- },
- {
"default": "0",
"fieldname": "enable_deferred_revenue",
"fieldtype": "Check",
@@ -681,21 +666,7 @@
"depends_on": "enable_deferred_revenue",
"fieldname": "no_of_months",
"fieldtype": "Int",
- "label": "No of Months"
- },
- {
- "collapsible": 1,
- "fieldname": "deferred_expense_section",
- "fieldtype": "Section Break",
- "label": "Deferred Expense"
- },
- {
- "depends_on": "enable_deferred_expense",
- "fieldname": "deferred_expense_account",
- "fieldtype": "Link",
- "ignore_user_permissions": 1,
- "label": "Deferred Expense Account",
- "options": "Account"
+ "label": "No of Months (Revenue)"
},
{
"default": "0",
@@ -904,6 +875,20 @@
"fieldname": "accounting",
"fieldtype": "Tab Break",
"label": "Accounting"
+ },
+ {
+ "fieldname": "column_break_9s9o",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_avcp",
+ "fieldtype": "Section Break"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "deferred_accounting_section",
+ "fieldtype": "Section Break",
+ "label": "Deferred Accounting"
}
],
"icon": "fa fa-tag",
@@ -912,7 +897,7 @@
"index_web_pages_for_search": 1,
"links": [],
"make_attachments_public": 1,
- "modified": "2023-08-28 22:16:40.305094",
+ "modified": "2023-09-11 13:46:32.688051",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",
diff --git a/erpnext/stock/doctype/item_default/item_default.json b/erpnext/stock/doctype/item_default/item_default.json
index 042d398..2895661 100644
--- a/erpnext/stock/doctype/item_default/item_default.json
+++ b/erpnext/stock/doctype/item_default/item_default.json
@@ -19,7 +19,11 @@
"selling_defaults",
"selling_cost_center",
"column_break_12",
- "income_account"
+ "income_account",
+ "deferred_accounting_defaults_section",
+ "deferred_expense_account",
+ "column_break_kwad",
+ "deferred_revenue_account"
],
"fields": [
{
@@ -108,11 +112,34 @@
"fieldtype": "Link",
"label": "Default Provisional Account",
"options": "Account"
+ },
+ {
+ "fieldname": "deferred_accounting_defaults_section",
+ "fieldtype": "Section Break",
+ "label": "Deferred Accounting Defaults"
+ },
+ {
+ "depends_on": "eval: parent.enable_deferred_expense",
+ "fieldname": "deferred_expense_account",
+ "fieldtype": "Link",
+ "label": "Deferred Expense Account",
+ "options": "Account"
+ },
+ {
+ "depends_on": "eval: parent.enable_deferred_revenue",
+ "fieldname": "deferred_revenue_account",
+ "fieldtype": "Link",
+ "label": "Deferred Revenue Account",
+ "options": "Account"
+ },
+ {
+ "fieldname": "column_break_kwad",
+ "fieldtype": "Column Break"
}
],
"istable": 1,
"links": [],
- "modified": "2022-04-10 20:18:54.148195",
+ "modified": "2023-09-04 12:33:14.607267",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Default",
diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json
index ffec57c..25c765b 100644
--- a/erpnext/stock/doctype/material_request/material_request.json
+++ b/erpnext/stock/doctype/material_request/material_request.json
@@ -296,6 +296,7 @@
"depends_on": "eval:doc.material_request_type == 'Material Transfer'",
"fieldname": "set_from_warehouse",
"fieldtype": "Link",
+ "ignore_user_permissions": 1,
"label": "Set Source Warehouse",
"options": "Warehouse"
},
@@ -356,7 +357,7 @@
"idx": 70,
"is_submittable": 1,
"links": [],
- "modified": "2023-07-25 17:19:31.662662",
+ "modified": "2023-09-15 12:07:24.789471",
"modified_by": "Administrator",
"module": "Stock",
"name": "Material Request",
diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js
index 4eed285..ae05b80 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.js
+++ b/erpnext/stock/doctype/pick_list/pick_list.js
@@ -65,17 +65,6 @@
}
}
});
-
- let sbb_field = frm.get_docfield('locations', 'serial_and_batch_bundle');
- if (sbb_field) {
- sbb_field.get_route_options_for_new_doc = (row) => {
- return {
- 'item_code': row.doc.item_code,
- 'warehouse': row.doc.warehouse,
- 'voucher_type': frm.doc.doctype,
- }
- };
- }
},
set_item_locations:(frm, save) => {
if (!(frm.doc.locations && frm.doc.locations.length)) {
@@ -132,6 +121,17 @@
}
}
}
+
+ let sbb_field = frm.get_docfield('locations', 'serial_and_batch_bundle');
+ if (sbb_field) {
+ sbb_field.get_route_options_for_new_doc = (row) => {
+ return {
+ 'item_code': row.doc.item_code,
+ 'warehouse': row.doc.warehouse,
+ 'voucher_type': frm.doc.doctype,
+ }
+ };
+ }
},
work_order: (frm) => {
frappe.db.get_value('Work Order',
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index 4fb8a10..d37e8ee 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -117,15 +117,6 @@
}
});
- let sbb_field = frm.get_docfield('items', 'serial_and_batch_bundle');
- if (sbb_field) {
- sbb_field.get_route_options_for_new_doc = (row) => {
- return {
- 'item_code': row.doc.item_code,
- 'voucher_type': frm.doc.doctype,
- }
- };
- }
frm.add_fetch("bom_no", "inspection_required", "inspection_required");
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
@@ -362,6 +353,16 @@
if(!check_should_not_attach_bom_items(frm.doc.bom_no)) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
}
+
+ let sbb_field = frm.get_docfield('items', 'serial_and_batch_bundle');
+ if (sbb_field) {
+ sbb_field.get_route_options_for_new_doc = (row) => {
+ return {
+ 'item_code': row.doc.item_code,
+ 'voucher_type': frm.doc.doctype,
+ }
+ };
+ }
},
get_items_from_transit_entry: function(frm) {
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 4f85ac0..79e6488 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -696,7 +696,11 @@
def get_default_deferred_account(args, item, fieldname=None):
if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"):
return (
- item.get(fieldname)
+ frappe.get_cached_value(
+ "Item Default",
+ {"parent": args.item_code, "company": args.get("company")},
+ fieldname,
+ )
or args.get(fieldname)
or frappe.get_cached_value("Company", args.company, "default_" + fieldname)
)
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
index 8ac22e6..19a1c93 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
@@ -77,6 +77,7 @@
}
frm.trigger('setup_quality_inspection');
+ frm.trigger('set_route_options_for_new_doc');
},
set_warehouse: (frm) => {
@@ -87,6 +88,23 @@
set_warehouse_in_children(frm.doc.items, 'rejected_warehouse', frm.doc.rejected_warehouse);
},
+ get_scrap_items: (frm) => {
+ frappe.call({
+ doc: frm.doc,
+ method: 'get_scrap_items',
+ args: {
+ recalculate_rate: true
+ },
+ freeze: true,
+ freeze_message: __('Getting Scrap Items'),
+ callback: (r) => {
+ if (!r.exc) {
+ frm.refresh();
+ }
+ }
+ });
+ },
+
set_queries: (frm) => {
frm.set_query('set_warehouse', () => {
return {
@@ -173,7 +191,28 @@
}
}
});
+ },
+ get_serial_and_batch_bundle_filters: (doc, cdt, cdn) => {
+ let row = locals[cdt][cdn];
+ return {
+ filters: {
+ 'item_code': row.item_code,
+ 'voucher_type': doc.doctype,
+ 'voucher_no': ['in', [doc.name, '']],
+ 'is_cancelled': 0,
+ }
+ }
+ },
+
+ setup_quality_inspection: (frm) => {
+ if (!frm.is_new() && frm.doc.docstatus === 0 && !frm.doc.is_return) {
+ let transaction_controller = new erpnext.TransactionController({ frm: frm });
+ transaction_controller.setup_quality_inspection();
+ }
+ },
+
+ set_route_options_for_new_doc: (frm) => {
let batch_no_field = frm.get_docfield('items', 'batch_no');
if (batch_no_field) {
batch_no_field.get_route_options_for_new_doc = (row) => {
@@ -213,42 +252,6 @@
}
}
},
-
- get_serial_and_batch_bundle_filters: (doc, cdt, cdn) => {
- let row = locals[cdt][cdn];
- return {
- filters: {
- 'item_code': row.item_code,
- 'voucher_type': doc.doctype,
- 'voucher_no': ['in', [doc.name, '']],
- 'is_cancelled': 0,
- }
- }
- },
-
- setup_quality_inspection: (frm) => {
- if (!frm.is_new() && frm.doc.docstatus === 0 && !frm.doc.is_return) {
- let transaction_controller = new erpnext.TransactionController({ frm: frm });
- transaction_controller.setup_quality_inspection();
- }
- },
-
- get_scrap_items: (frm) => {
- frappe.call({
- doc: frm.doc,
- method: 'get_scrap_items',
- args: {
- recalculate_rate: true
- },
- freeze: true,
- freeze_message: __('Getting Scrap Items'),
- callback: (r) => {
- if (!r.exc) {
- frm.refresh();
- }
- }
- });
- },
});
frappe.ui.form.on('Landed Cost Taxes and Charges', {
@@ -303,4 +306,4 @@
if (!r.exc) frm.refresh();
},
});
-}
\ No newline at end of file
+}