Merge pull request #35594 from nikkothari22/make-accounting-dimension-filter-values-optional
feat: added support for mandatory dimensions per account without applying restrictions on dimension values
diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml
index d5f0052..e2d8957 100644
--- a/.github/workflows/patch.yml
+++ b/.github/workflows/patch.yml
@@ -43,9 +43,11 @@
fi
- name: Setup Python
- uses: "gabrielfalcao/pyenv-action@v9"
+ uses: "actions/setup-python@v4"
with:
- versions: 3.10:latest, 3.7:latest
+ python-version: |
+ 3.7
+ 3.10
- name: Setup Node
uses: actions/setup-node@v2
@@ -92,7 +94,6 @@
- name: Install
run: |
pip install frappe-bench
- pyenv global $(pyenv versions | grep '3.10')
bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env:
DB: mariadb
@@ -107,7 +108,6 @@
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
- pyenv global $(pyenv versions | grep '3.7')
for version in $(seq 12 13)
do
echo "Updating to v$version"
@@ -120,7 +120,7 @@
git -C "apps/erpnext" checkout -q -f $branch_name
rm -rf ~/frappe-bench/env
- bench setup env
+ bench setup env --python python3.7
bench pip install -e ./apps/payments
bench pip install -e ./apps/erpnext
@@ -132,9 +132,8 @@
git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}"
git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA"
- pyenv global $(pyenv versions | grep '3.10')
rm -rf ~/frappe-bench/env
- bench -v setup env
+ bench -v setup env --python python3.10
bench pip install -e ./apps/payments
bench pip install -e ./apps/erpnext
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
index 09482d7..7cd498d 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
@@ -61,7 +61,10 @@
"column_break_25",
"frozen_accounts_modifier",
"tab_break_dpet",
- "show_balance_in_coa"
+ "show_balance_in_coa",
+ "banking_tab",
+ "enable_party_matching",
+ "enable_fuzzy_matching"
],
"fields": [
{
@@ -383,6 +386,26 @@
"fieldname": "show_taxes_as_table_in_print",
"fieldtype": "Check",
"label": "Show Taxes as Table in Print"
+ },
+ {
+ "fieldname": "banking_tab",
+ "fieldtype": "Tab Break",
+ "label": "Banking"
+ },
+ {
+ "default": "0",
+ "description": "Auto match and set the Party in Bank Transactions",
+ "fieldname": "enable_party_matching",
+ "fieldtype": "Check",
+ "label": "Enable Automatic Party Matching"
+ },
+ {
+ "default": "0",
+ "depends_on": "enable_party_matching",
+ "description": "Approximately match the description/party name against parties",
+ "fieldname": "enable_fuzzy_matching",
+ "fieldtype": "Check",
+ "label": "Enable Fuzzy Matching"
}
],
"icon": "icon-cog",
@@ -390,7 +413,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2023-06-13 18:47:46.430291",
+ "modified": "2023-06-15 16:35:45.123456",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
diff --git a/erpnext/accounts/doctype/bank_account/bank_account.py b/erpnext/accounts/doctype/bank_account/bank_account.py
index b91f0f9..363a277 100644
--- a/erpnext/accounts/doctype/bank_account/bank_account.py
+++ b/erpnext/accounts/doctype/bank_account/bank_account.py
@@ -70,7 +70,6 @@
return doc
-@frappe.whitelist()
def get_party_bank_account(party_type, party):
return frappe.db.get_value(party_type, party, "default_bank_account")
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
index c4a23a6..0eef3e9 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
@@ -10,6 +10,7 @@
from frappe.query_builder.custom import ConstantColumn
from frappe.utils import cint, flt
+from erpnext import get_default_cost_center
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount
from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import (
get_amounts_not_reflected_in_system,
@@ -140,6 +141,9 @@
second_account
)
)
+
+ company = frappe.get_value("Account", company_account, "company")
+
accounts = []
# Multi Currency?
accounts.append(
@@ -149,6 +153,7 @@
"debit_in_account_currency": bank_transaction.withdrawal,
"party_type": party_type,
"party": party,
+ "cost_center": get_default_cost_center(company),
}
)
@@ -158,11 +163,10 @@
"bank_account": bank_transaction.bank_account,
"credit_in_account_currency": bank_transaction.withdrawal,
"debit_in_account_currency": bank_transaction.deposit,
+ "cost_center": get_default_cost_center(company),
}
)
- company = frappe.get_value("Account", company_account, "company")
-
journal_entry_dict = {
"voucher_type": entry_type,
"company": company,
diff --git a/erpnext/accounts/doctype/bank_transaction/auto_match_party.py b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py
new file mode 100644
index 0000000..5d94a08
--- /dev/null
+++ b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py
@@ -0,0 +1,178 @@
+from typing import Tuple, Union
+
+import frappe
+from frappe.utils import flt
+from rapidfuzz import fuzz, process
+
+
+class AutoMatchParty:
+ """
+ Matches by Account/IBAN and then by Party Name/Description sequentially.
+ Returns when a result is obtained.
+
+ Result (if present) is of the form: (Party Type, Party,)
+ """
+
+ def __init__(self, **kwargs) -> None:
+ self.__dict__.update(kwargs)
+
+ def get(self, key):
+ return self.__dict__.get(key, None)
+
+ def match(self) -> Union[Tuple, None]:
+ result = None
+ result = AutoMatchbyAccountIBAN(
+ bank_party_account_number=self.bank_party_account_number,
+ bank_party_iban=self.bank_party_iban,
+ deposit=self.deposit,
+ ).match()
+
+ fuzzy_matching_enabled = frappe.db.get_single_value("Accounts Settings", "enable_fuzzy_matching")
+ if not result and fuzzy_matching_enabled:
+ result = AutoMatchbyPartyNameDescription(
+ bank_party_name=self.bank_party_name, description=self.description, deposit=self.deposit
+ ).match()
+
+ return result
+
+
+class AutoMatchbyAccountIBAN:
+ def __init__(self, **kwargs) -> None:
+ self.__dict__.update(kwargs)
+
+ def get(self, key):
+ return self.__dict__.get(key, None)
+
+ def match(self):
+ if not (self.bank_party_account_number or self.bank_party_iban):
+ return None
+
+ result = self.match_account_in_party()
+ return result
+
+ def match_account_in_party(self) -> Union[Tuple, None]:
+ """Check if there is a IBAN/Account No. match in Customer/Supplier/Employee"""
+ result = None
+ parties = get_parties_in_order(self.deposit)
+ or_filters = self.get_or_filters()
+
+ for party in parties:
+ party_result = frappe.db.get_all(
+ "Bank Account", or_filters=or_filters, pluck="party", limit_page_length=1
+ )
+
+ if party == "Employee" and not party_result:
+ # Search in Bank Accounts first for Employee, and then Employee record
+ if "bank_account_no" in or_filters:
+ or_filters["bank_ac_no"] = or_filters.pop("bank_account_no")
+
+ party_result = frappe.db.get_all(
+ party, or_filters=or_filters, pluck="name", limit_page_length=1
+ )
+
+ if party_result:
+ result = (
+ party,
+ party_result[0],
+ )
+ break
+
+ return result
+
+ def get_or_filters(self) -> dict:
+ or_filters = {}
+ if self.bank_party_account_number:
+ or_filters["bank_account_no"] = self.bank_party_account_number
+
+ if self.bank_party_iban:
+ or_filters["iban"] = self.bank_party_iban
+
+ return or_filters
+
+
+class AutoMatchbyPartyNameDescription:
+ def __init__(self, **kwargs) -> None:
+ self.__dict__.update(kwargs)
+
+ def get(self, key):
+ return self.__dict__.get(key, None)
+
+ def match(self) -> Union[Tuple, None]:
+ # fuzzy search by customer/supplier & employee
+ if not (self.bank_party_name or self.description):
+ return None
+
+ result = self.match_party_name_desc_in_party()
+ return result
+
+ def match_party_name_desc_in_party(self) -> Union[Tuple, None]:
+ """Fuzzy search party name and/or description against parties in the system"""
+ result = None
+ parties = get_parties_in_order(self.deposit)
+
+ for party in parties:
+ filters = {"status": "Active"} if party == "Employee" else {"disabled": 0}
+ names = frappe.get_all(party, filters=filters, pluck=party.lower() + "_name")
+
+ for field in ["bank_party_name", "description"]:
+ if not self.get(field):
+ continue
+
+ result, skip = self.fuzzy_search_and_return_result(party, names, field)
+ if result or skip:
+ break
+
+ if result or skip:
+ # Skip If: It was hard to distinguish between close matches and so match is None
+ # OR if the right match was found
+ break
+
+ return result
+
+ def fuzzy_search_and_return_result(self, party, names, field) -> Union[Tuple, None]:
+ skip = False
+ result = process.extract(query=self.get(field), choices=names, scorer=fuzz.token_set_ratio)
+ party_name, skip = self.process_fuzzy_result(result)
+
+ if not party_name:
+ return None, skip
+
+ return (
+ party,
+ party_name,
+ ), skip
+
+ def process_fuzzy_result(self, result: Union[list, None]):
+ """
+ If there are multiple valid close matches return None as result may be faulty.
+ Return the result only if one accurate match stands out.
+
+ Returns: Result, Skip (whether or not to discontinue matching)
+ """
+ PARTY, SCORE, CUTOFF = 0, 1, 80
+
+ if not result or not len(result):
+ return None, False
+
+ first_result = result[0]
+ if len(result) == 1:
+ return (first_result[PARTY] if first_result[SCORE] > CUTOFF else None), True
+
+ second_result = result[1]
+ if first_result[SCORE] > CUTOFF:
+ # If multiple matches with the same score, return None but discontinue matching
+ # Matches were found but were too close to distinguish between
+ if first_result[SCORE] == second_result[SCORE]:
+ return None, True
+
+ return first_result[PARTY], True
+ else:
+ return None, False
+
+
+def get_parties_in_order(deposit: float) -> list:
+ parties = ["Supplier", "Employee", "Customer"] # most -> least likely to receive
+ if flt(deposit) > 0:
+ parties = ["Customer", "Supplier", "Employee"] # most -> least likely to pay
+
+ return parties
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json
index 768d2f0..b32022e 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json
@@ -33,7 +33,11 @@
"unallocated_amount",
"party_section",
"party_type",
- "party"
+ "party",
+ "column_break_3czf",
+ "bank_party_name",
+ "bank_party_account_number",
+ "bank_party_iban"
],
"fields": [
{
@@ -63,7 +67,7 @@
"fieldtype": "Select",
"in_standard_filter": 1,
"label": "Status",
- "options": "\nPending\nSettled\nUnreconciled\nReconciled"
+ "options": "\nPending\nSettled\nUnreconciled\nReconciled\nCancelled"
},
{
"fieldname": "bank_account",
@@ -202,11 +206,30 @@
"fieldtype": "Data",
"label": "Transaction Type",
"length": 50
+ },
+ {
+ "fieldname": "column_break_3czf",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "bank_party_name",
+ "fieldtype": "Data",
+ "label": "Party Name/Account Holder (Bank Statement)"
+ },
+ {
+ "fieldname": "bank_party_iban",
+ "fieldtype": "Data",
+ "label": "Party IBAN (Bank Statement)"
+ },
+ {
+ "fieldname": "bank_party_account_number",
+ "fieldtype": "Data",
+ "label": "Party Account No. (Bank Statement)"
}
],
"is_submittable": 1,
"links": [],
- "modified": "2022-05-29 18:36:50.475964",
+ "modified": "2023-06-06 13:58:12.821411",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Transaction",
@@ -260,4 +283,4 @@
"states": [],
"title_field": "bank_account",
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
index b441af9..f82337f 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
@@ -15,6 +15,9 @@
self.clear_linked_payment_entries()
self.set_status()
+ if frappe.db.get_single_value("Accounts Settings", "enable_party_matching"):
+ self.auto_set_party()
+
_saving_flag = False
# nosemgrep: frappe-semgrep-rules.rules.frappe-modifying-but-not-comitting
@@ -146,6 +149,26 @@
payment_entry.payment_document, payment_entry.payment_entry, clearance_date, self
)
+ def auto_set_party(self):
+ from erpnext.accounts.doctype.bank_transaction.auto_match_party import AutoMatchParty
+
+ if self.party_type and self.party:
+ return
+
+ result = AutoMatchParty(
+ bank_party_account_number=self.bank_party_account_number,
+ bank_party_iban=self.bank_party_iban,
+ bank_party_name=self.bank_party_name,
+ description=self.description,
+ deposit=self.deposit,
+ ).match()
+
+ if result:
+ party_type, party = result
+ frappe.db.set_value(
+ "Bank Transaction", self.name, field={"party_type": party_type, "party": party}
+ )
+
@frappe.whitelist()
def get_doctypes_for_bank_reconciliation():
diff --git a/erpnext/accounts/doctype/bank_transaction/test_auto_match_party.py b/erpnext/accounts/doctype/bank_transaction/test_auto_match_party.py
new file mode 100644
index 0000000..36ef1fc
--- /dev/null
+++ b/erpnext/accounts/doctype/bank_transaction/test_auto_match_party.py
@@ -0,0 +1,151 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+import frappe
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import nowdate
+
+from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account
+
+
+class TestAutoMatchParty(FrappeTestCase):
+ @classmethod
+ def setUpClass(cls):
+ create_bank_account()
+ frappe.db.set_single_value("Accounts Settings", "enable_party_matching", 1)
+ frappe.db.set_single_value("Accounts Settings", "enable_fuzzy_matching", 1)
+ return super().setUpClass()
+
+ @classmethod
+ def tearDownClass(cls):
+ frappe.db.set_single_value("Accounts Settings", "enable_party_matching", 0)
+ frappe.db.set_single_value("Accounts Settings", "enable_fuzzy_matching", 0)
+
+ def test_match_by_account_number(self):
+ create_supplier_for_match(account_no="000000003716541159")
+ doc = create_bank_transaction(
+ withdrawal=1200,
+ transaction_id="562213b0ca1bf838dab8f2c6a39bbc3b",
+ account_no="000000003716541159",
+ iban="DE02000000003716541159",
+ )
+
+ self.assertEqual(doc.party_type, "Supplier")
+ self.assertEqual(doc.party, "John Doe & Co.")
+
+ def test_match_by_iban(self):
+ create_supplier_for_match(iban="DE02000000003716541159")
+ doc = create_bank_transaction(
+ withdrawal=1200,
+ transaction_id="c5455a224602afaa51592a9d9250600d",
+ account_no="000000003716541159",
+ iban="DE02000000003716541159",
+ )
+
+ self.assertEqual(doc.party_type, "Supplier")
+ self.assertEqual(doc.party, "John Doe & Co.")
+
+ def test_match_by_party_name(self):
+ create_supplier_for_match(supplier_name="Jackson Ella W.")
+ doc = create_bank_transaction(
+ withdrawal=1200,
+ transaction_id="1f6f661f347ff7b1ea588665f473adb1",
+ party_name="Ella Jackson",
+ iban="DE04000000003716545346",
+ )
+ self.assertEqual(doc.party_type, "Supplier")
+ self.assertEqual(doc.party, "Jackson Ella W.")
+
+ def test_match_by_description(self):
+ create_supplier_for_match(supplier_name="Microsoft")
+ doc = create_bank_transaction(
+ description="Auftraggeber: microsoft payments Buchungstext: msft ..e3006b5hdy. ref. j375979555927627/5536",
+ withdrawal=1200,
+ transaction_id="8df880a2d09c3bed3fea358ca5168c5a",
+ party_name="",
+ )
+ self.assertEqual(doc.party_type, "Supplier")
+ self.assertEqual(doc.party, "Microsoft")
+
+ def test_skip_match_if_multiple_close_results(self):
+ create_supplier_for_match(supplier_name="Adithya Medical & General Stores")
+ create_supplier_for_match(supplier_name="Adithya Medical And General Stores")
+
+ doc = create_bank_transaction(
+ description="Paracetamol Consignment, SINV-0009",
+ withdrawal=24.85,
+ transaction_id="3a1da4ee2dc5a980138d56ef3460cbd9",
+ party_name="Adithya Medical & General",
+ )
+
+ # Mapping is skipped as both Supplier names have the same match score
+ self.assertEqual(doc.party_type, None)
+ self.assertEqual(doc.party, None)
+
+
+def create_supplier_for_match(supplier_name="John Doe & Co.", iban=None, account_no=None):
+ if frappe.db.exists("Supplier", {"supplier_name": supplier_name}):
+ # Update related Bank Account details
+ if not (iban or account_no):
+ return
+
+ frappe.db.set_value(
+ dt="Bank Account",
+ dn={"party": supplier_name},
+ field={"iban": iban, "bank_account_no": account_no},
+ )
+ return
+
+ # Create Supplier and Bank Account for the same
+ supplier = frappe.new_doc("Supplier")
+ supplier.supplier_name = supplier_name
+ supplier.supplier_group = "Services"
+ supplier.supplier_type = "Company"
+ supplier.insert()
+
+ if not frappe.db.exists("Bank", "TestBank"):
+ bank = frappe.new_doc("Bank")
+ bank.bank_name = "TestBank"
+ bank.insert(ignore_if_duplicate=True)
+
+ if not frappe.db.exists("Bank Account", supplier.name + " - " + "TestBank"):
+ bank_account = frappe.new_doc("Bank Account")
+ bank_account.account_name = supplier.name
+ bank_account.bank = "TestBank"
+ bank_account.iban = iban
+ bank_account.bank_account_no = account_no
+ bank_account.party_type = "Supplier"
+ bank_account.party = supplier.name
+ bank_account.insert()
+
+
+def create_bank_transaction(
+ description=None,
+ withdrawal=0,
+ deposit=0,
+ transaction_id=None,
+ party_name=None,
+ account_no=None,
+ iban=None,
+):
+ doc = frappe.new_doc("Bank Transaction")
+ doc.update(
+ {
+ "doctype": "Bank Transaction",
+ "description": description or "1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G",
+ "date": nowdate(),
+ "withdrawal": withdrawal,
+ "deposit": deposit,
+ "currency": "INR",
+ "bank_account": "Checking Account - Citi Bank",
+ "transaction_id": transaction_id,
+ "bank_party_name": party_name,
+ "bank_party_account_number": account_no,
+ "bank_party_iban": iban,
+ }
+ )
+ doc.insert()
+ doc.submit()
+ doc.reload()
+
+ return doc
diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js
index f51b90d..1ef5c83 100644
--- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js
+++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js
@@ -37,7 +37,7 @@
validate_rounding_loss: function(frm) {
let allowance = frm.doc.rounding_loss_allowance;
- if (!(allowance > 0 && allowance < 1)) {
+ if (!(allowance >= 0 && allowance < 1)) {
frappe.throw(__("Rounding Loss Allowance should be between 0 and 1"));
}
},
diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.json b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.json
index 2310d12..79428d5 100644
--- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.json
+++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.json
@@ -100,15 +100,16 @@
},
{
"default": "0.05",
- "description": "Only values between 0 and 1 are allowed. \nEx: If allowance is set at 0.07, accounts that have balance of 0.07 in either of the currencies will be considered as zero balance account",
+ "description": "Only values between [0,1) are allowed. Like {0.00, 0.04, 0.09, ...}\nEx: If allowance is set at 0.07, accounts that have balance of 0.07 in either of the currencies will be considered as zero balance account",
"fieldname": "rounding_loss_allowance",
"fieldtype": "Float",
- "label": "Rounding Loss Allowance"
+ "label": "Rounding Loss Allowance",
+ "precision": "9"
}
],
"is_submittable": 1,
"links": [],
- "modified": "2023-06-12 21:02:09.818208",
+ "modified": "2023-06-20 07:29:06.972434",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Exchange Rate Revaluation",
diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
index 5d239c9..598db64 100644
--- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
+++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
@@ -22,7 +22,7 @@
self.set_total_gain_loss()
def validate_rounding_loss_allowance(self):
- if not (self.rounding_loss_allowance > 0 and self.rounding_loss_allowance < 1):
+ if not (self.rounding_loss_allowance >= 0 and self.rounding_loss_allowance < 1):
frappe.throw(_("Rounding Loss Allowance should be between 0 and 1"))
def set_total_gain_loss(self):
@@ -373,6 +373,24 @@
"credit": 0,
}
)
+
+ journal_entry_accounts.append(journal_account)
+
+ journal_entry_accounts.append(
+ {
+ "account": unrealized_exchange_gain_loss_account,
+ "balance": get_balance_on(unrealized_exchange_gain_loss_account),
+ "debit": 0,
+ "credit": 0,
+ "debit_in_account_currency": abs(d.gain_loss) if d.gain_loss < 0 else 0,
+ "credit_in_account_currency": abs(d.gain_loss) if d.gain_loss > 0 else 0,
+ "cost_center": erpnext.get_default_cost_center(self.company),
+ "exchange_rate": 1,
+ "reference_type": "Exchange Rate Revaluation",
+ "reference_name": self.name,
+ }
+ )
+
elif d.get("balance_in_base_currency") and not d.get("new_balance_in_base_currency"):
# Base currency has balance
dr_or_cr = "credit" if d.get("balance_in_base_currency") > 0 else "debit"
@@ -388,22 +406,22 @@
}
)
- journal_entry_accounts.append(journal_account)
+ journal_entry_accounts.append(journal_account)
- journal_entry_accounts.append(
- {
- "account": unrealized_exchange_gain_loss_account,
- "balance": get_balance_on(unrealized_exchange_gain_loss_account),
- "debit": abs(self.gain_loss_booked) if self.gain_loss_booked < 0 else 0,
- "credit": abs(self.gain_loss_booked) if self.gain_loss_booked > 0 else 0,
- "debit_in_account_currency": abs(self.gain_loss_booked) if self.gain_loss_booked < 0 else 0,
- "credit_in_account_currency": self.gain_loss_booked if self.gain_loss_booked > 0 else 0,
- "cost_center": erpnext.get_default_cost_center(self.company),
- "exchange_rate": 1,
- "reference_type": "Exchange Rate Revaluation",
- "reference_name": self.name,
- }
- )
+ journal_entry_accounts.append(
+ {
+ "account": unrealized_exchange_gain_loss_account,
+ "balance": get_balance_on(unrealized_exchange_gain_loss_account),
+ "debit": abs(d.gain_loss) if d.gain_loss < 0 else 0,
+ "credit": abs(d.gain_loss) if d.gain_loss > 0 else 0,
+ "debit_in_account_currency": 0,
+ "credit_in_account_currency": 0,
+ "cost_center": erpnext.get_default_cost_center(self.company),
+ "exchange_rate": 1,
+ "reference_type": "Exchange Rate Revaluation",
+ "reference_name": self.name,
+ }
+ )
journal_entry.set("accounts", journal_entry_accounts)
journal_entry.set_total_debit_credit()
diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation_account/exchange_rate_revaluation_account.json b/erpnext/accounts/doctype/exchange_rate_revaluation_account/exchange_rate_revaluation_account.json
index 2968359..fd2d931 100644
--- a/erpnext/accounts/doctype/exchange_rate_revaluation_account/exchange_rate_revaluation_account.json
+++ b/erpnext/accounts/doctype/exchange_rate_revaluation_account/exchange_rate_revaluation_account.json
@@ -73,6 +73,7 @@
"fieldname": "current_exchange_rate",
"fieldtype": "Float",
"label": "Current Exchange Rate",
+ "precision": "9",
"read_only": 1
},
{
@@ -92,6 +93,7 @@
"fieldtype": "Float",
"in_list_view": 1,
"label": "New Exchange Rate",
+ "precision": "9",
"reqd": 1
},
{
@@ -147,7 +149,7 @@
],
"istable": 1,
"links": [],
- "modified": "2022-12-29 19:38:52.915295",
+ "modified": "2023-06-22 12:39:56.446722",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Exchange Rate Revaluation Account",
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js
index 6d9e320..a51e38e 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.js
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js
@@ -8,7 +8,7 @@
frappe.ui.form.on("Journal Entry", {
setup: function(frm) {
frm.add_fetch("bank_account", "account", "account");
- frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset Depreciation Schedule'];
+ frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement', 'Asset Depreciation Schedule'];
},
refresh: function(frm) {
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index 74fd559..83312db 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -326,12 +326,10 @@
d.db_update()
def unlink_asset_reference(self):
- if self.voucher_type != "Depreciation Entry":
- return
-
for d in self.get("accounts"):
if (
- d.reference_type == "Asset"
+ self.voucher_type == "Depreciation Entry"
+ and d.reference_type == "Asset"
and d.reference_name
and d.account_type == "Depreciation"
and d.debit
@@ -370,6 +368,15 @@
else:
asset.db_set("value_after_depreciation", asset.value_after_depreciation + d.debit)
asset.set_status()
+ elif self.voucher_type == "Journal Entry" and d.reference_type == "Asset" and d.reference_name:
+ journal_entry_for_scrap = frappe.db.get_value(
+ "Asset", d.reference_name, "journal_entry_for_scrap"
+ )
+
+ if journal_entry_for_scrap == self.name:
+ frappe.throw(
+ _("Journal Entry for Asset scrapping cannot be cancelled. Please restore the Asset.")
+ )
def unlink_inter_company_jv(self):
if (
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 2843824..bac84db 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -155,6 +155,7 @@
frm.events.hide_unhide_fields(frm);
frm.events.set_dynamic_labels(frm);
frm.events.show_general_ledger(frm);
+ erpnext.accounts.ledger_preview.show_accounting_ledger_preview(frm);
},
validate_company: (frm) => {
@@ -612,7 +613,7 @@
frm.events.set_unallocated_amount(frm);
},
- get_outstanding_invoice: function(frm) {
+ get_outstanding_invoices_or_orders: function(frm, get_outstanding_invoices, get_orders_to_be_billed) {
const today = frappe.datetime.get_today();
const fields = [
{fieldtype:"Section Break", label: __("Posting Date")},
@@ -642,12 +643,29 @@
{fieldtype:"Check", label: __("Allocate Payment Amount"), fieldname:"allocate_payment_amount", default:1},
];
+ let btn_text = "";
+
+ if (get_outstanding_invoices) {
+ btn_text = "Get Outstanding Invoices";
+ }
+ else if (get_orders_to_be_billed) {
+ btn_text = "Get Outstanding Orders";
+ }
+
frappe.prompt(fields, function(filters){
frappe.flags.allocate_payment_amount = true;
frm.events.validate_filters_data(frm, filters);
frm.doc.cost_center = filters.cost_center;
- frm.events.get_outstanding_documents(frm, filters);
- }, __("Filters"), __("Get Outstanding Documents"));
+ frm.events.get_outstanding_documents(frm, filters, get_outstanding_invoices, get_orders_to_be_billed);
+ }, __("Filters"), __(btn_text));
+ },
+
+ get_outstanding_invoices: function(frm) {
+ frm.events.get_outstanding_invoices_or_orders(frm, true, false);
+ },
+
+ get_outstanding_orders: function(frm) {
+ frm.events.get_outstanding_invoices_or_orders(frm, false, true);
},
validate_filters_data: function(frm, filters) {
@@ -673,7 +691,7 @@
}
},
- get_outstanding_documents: function(frm, filters) {
+ get_outstanding_documents: function(frm, filters, get_outstanding_invoices, get_orders_to_be_billed) {
frm.clear_table("references");
if(!frm.doc.party) {
@@ -697,6 +715,13 @@
args[key] = filters[key];
}
+ if (get_outstanding_invoices) {
+ args["get_outstanding_invoices"] = true;
+ }
+ else if (get_orders_to_be_billed) {
+ args["get_orders_to_be_billed"] = true;
+ }
+
frappe.flags.allocate_payment_amount = filters['allocate_payment_amount'];
return frappe.call({
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json
index 3927eca..6224d40 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.json
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json
@@ -48,7 +48,8 @@
"base_received_amount",
"base_received_amount_after_tax",
"section_break_14",
- "get_outstanding_invoice",
+ "get_outstanding_invoices",
+ "get_outstanding_orders",
"references",
"section_break_34",
"total_allocated_amount",
@@ -356,12 +357,6 @@
"label": "Reference"
},
{
- "depends_on": "eval:doc.docstatus==0",
- "fieldname": "get_outstanding_invoice",
- "fieldtype": "Button",
- "label": "Get Outstanding Invoice"
- },
- {
"fieldname": "references",
"fieldtype": "Table",
"label": "Payment References",
@@ -728,12 +723,24 @@
"fieldname": "section_break_60",
"fieldtype": "Section Break",
"hide_border": 1
+ },
+ {
+ "depends_on": "eval:doc.docstatus==0",
+ "fieldname": "get_outstanding_invoices",
+ "fieldtype": "Button",
+ "label": "Get Outstanding Invoices"
+ },
+ {
+ "depends_on": "eval:doc.docstatus==0",
+ "fieldname": "get_outstanding_orders",
+ "fieldtype": "Button",
+ "label": "Get Outstanding Orders"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2023-02-14 04:52:30.478523",
+ "modified": "2023-06-19 11:38:04.387219",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index b6d3e5a..1f23fe1 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -151,6 +151,19 @@
if self.payment_type == "Internal Transfer":
return
+ if self.party_type in ("Customer", "Supplier"):
+ self.validate_allocated_amount_with_latest_data()
+ else:
+ fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
+ for d in self.get("references"):
+ if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(d.outstanding_amount):
+ frappe.throw(fail_message.format(d.idx))
+
+ # Check for negative outstanding invoices as well
+ if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(d.outstanding_amount):
+ frappe.throw(fail_message.format(d.idx))
+
+ def validate_allocated_amount_with_latest_data(self):
latest_references = get_outstanding_reference_documents(
{
"posting_date": self.posting_date,
@@ -159,6 +172,8 @@
"payment_type": self.payment_type,
"party": self.party,
"party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to,
+ "get_outstanding_invoices": True,
+ "get_orders_to_be_billed": True,
}
)
@@ -168,7 +183,7 @@
d = frappe._dict(d)
latest_lookup.update({(d.voucher_type, d.voucher_no): d})
- for d in self.get("references").copy():
+ for d in self.get("references"):
latest = latest_lookup.get((d.reference_doctype, d.reference_name))
# The reference has already been fully paid
@@ -183,22 +198,18 @@
):
frappe.throw(
_(
- "{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' button to get the latest outstanding amount."
+ "{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts."
).format(d.reference_doctype, d.reference_name)
)
- d.outstanding_amount = latest.outstanding_amount
-
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
- if (flt(d.allocated_amount)) > 0:
- if flt(d.allocated_amount) > flt(d.outstanding_amount):
- frappe.throw(fail_message.format(d.idx))
+ if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
+ frappe.throw(fail_message.format(d.idx))
# Check for negative outstanding invoices as well
- if flt(d.allocated_amount) < 0:
- if flt(d.allocated_amount) < flt(d.outstanding_amount):
- frappe.throw(fail_message.format(d.idx))
+ if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount):
+ frappe.throw(fail_message.format(d.idx))
def delink_advance_entry_references(self):
for reference in self.references:
@@ -1347,62 +1358,75 @@
condition += " and company = {0}".format(frappe.db.escape(args.get("company")))
common_filter.append(ple.company == args.get("company"))
- outstanding_invoices = get_outstanding_invoices(
- args.get("party_type"),
- args.get("party"),
- args.get("party_account"),
- common_filter=common_filter,
- posting_date=posting_and_due_date,
- min_outstanding=args.get("outstanding_amt_greater_than"),
- max_outstanding=args.get("outstanding_amt_less_than"),
- accounting_dimensions=accounting_dimensions_filter,
- )
-
- outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices)
-
- for d in outstanding_invoices:
- d["exchange_rate"] = 1
- if party_account_currency != company_currency:
- if d.voucher_type in frappe.get_hooks("invoice_doctypes"):
- d["exchange_rate"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "conversion_rate")
- elif d.voucher_type == "Journal Entry":
- d["exchange_rate"] = get_exchange_rate(
- party_account_currency, company_currency, d.posting_date
- )
- if d.voucher_type in ("Purchase Invoice"):
- d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no")
-
- # Get all SO / PO which are not fully billed or against which full advance not paid
- orders_to_be_billed = []
- orders_to_be_billed = get_orders_to_be_billed(
- args.get("posting_date"),
- args.get("party_type"),
- args.get("party"),
- args.get("company"),
- party_account_currency,
- company_currency,
- filters=args,
- )
-
- # Get negative outstanding sales /purchase invoices
+ outstanding_invoices = []
negative_outstanding_invoices = []
- if args.get("party_type") != "Employee" and not args.get("voucher_no"):
- negative_outstanding_invoices = get_negative_outstanding_invoices(
+
+ if args.get("get_outstanding_invoices"):
+ outstanding_invoices = get_outstanding_invoices(
args.get("party_type"),
args.get("party"),
args.get("party_account"),
+ common_filter=common_filter,
+ posting_date=posting_and_due_date,
+ min_outstanding=args.get("outstanding_amt_greater_than"),
+ max_outstanding=args.get("outstanding_amt_less_than"),
+ accounting_dimensions=accounting_dimensions_filter,
+ )
+
+ outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices)
+
+ for d in outstanding_invoices:
+ d["exchange_rate"] = 1
+ if party_account_currency != company_currency:
+ if d.voucher_type in frappe.get_hooks("invoice_doctypes"):
+ d["exchange_rate"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "conversion_rate")
+ elif d.voucher_type == "Journal Entry":
+ d["exchange_rate"] = get_exchange_rate(
+ party_account_currency, company_currency, d.posting_date
+ )
+ if d.voucher_type in ("Purchase Invoice"):
+ d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no")
+
+ # Get negative outstanding sales /purchase invoices
+ if args.get("party_type") != "Employee" and not args.get("voucher_no"):
+ negative_outstanding_invoices = get_negative_outstanding_invoices(
+ args.get("party_type"),
+ args.get("party"),
+ args.get("party_account"),
+ party_account_currency,
+ company_currency,
+ condition=condition,
+ )
+
+ # Get all SO / PO which are not fully billed or against which full advance not paid
+ orders_to_be_billed = []
+ if args.get("get_orders_to_be_billed"):
+ orders_to_be_billed = get_orders_to_be_billed(
+ args.get("posting_date"),
+ args.get("party_type"),
+ args.get("party"),
+ args.get("company"),
party_account_currency,
company_currency,
- condition=condition,
+ filters=args,
)
data = negative_outstanding_invoices + outstanding_invoices + orders_to_be_billed
if not data:
+ if args.get("get_outstanding_invoices") and args.get("get_orders_to_be_billed"):
+ ref_document_type = "invoices or orders"
+ elif args.get("get_outstanding_invoices"):
+ ref_document_type = "invoices"
+ elif args.get("get_orders_to_be_billed"):
+ ref_document_type = "orders"
+
frappe.msgprint(
_(
- "No outstanding invoices found for the {0} {1} which qualify the filters you have specified."
- ).format(_(args.get("party_type")).lower(), frappe.bold(args.get("party")))
+ "No outstanding {0} found for the {1} {2} which qualify the filters you have specified."
+ ).format(
+ ref_document_type, _(args.get("party_type")).lower(), frappe.bold(args.get("party"))
+ )
)
return data
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
index 2283677..89fa151 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
@@ -85,25 +85,29 @@
// check for any running reconciliation jobs
if (this.frm.doc.receivable_payable_account) {
- frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments").then((enabled) => {
- if(enabled) {
- this.frm.call({
- 'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.is_any_doc_running",
- "args": {
- for_filter: {
- company: this.frm.doc.company,
- party_type: this.frm.doc.party_type,
- party: this.frm.doc.party,
- receivable_payable_account: this.frm.doc.receivable_payable_account
+ this.frm.call({
+ doc: this.frm.doc,
+ method: 'is_auto_process_enabled',
+ callback: (r) => {
+ if (r.message) {
+ this.frm.call({
+ 'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.is_any_doc_running",
+ "args": {
+ for_filter: {
+ company: this.frm.doc.company,
+ party_type: this.frm.doc.party_type,
+ party: this.frm.doc.party,
+ receivable_payable_account: this.frm.doc.receivable_payable_account
+ }
}
- }
- }).then(r => {
- if (r.message) {
- let doc_link = frappe.utils.get_form_link("Process Payment Reconciliation", r.message, true);
- let msg = __("Payment Reconciliation Job: {0} is running for this party. Can't reconcile now.", [doc_link]);
- this.frm.dashboard.add_comment(msg, "yellow");
- }
- });
+ }).then(r => {
+ if (r.message) {
+ let doc_link = frappe.utils.get_form_link("Process Payment Reconciliation", r.message, true);
+ let msg = __("Payment Reconciliation Job: {0} is running for this party. Can't reconcile now.", [doc_link]);
+ this.frm.dashboard.add_comment(msg, "yellow");
+ }
+ });
+ }
}
});
}
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index 2c8faec..a709740 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -253,6 +253,10 @@
return difference_amount
@frappe.whitelist()
+ def is_auto_process_enabled(self):
+ return frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments")
+
+ @frappe.whitelist()
def calculate_difference_on_allocation_change(self, payment_entry, invoice, allocated_amount):
invoice_exchange_map = self.get_invoice_exchange_map(invoice, payment_entry)
invoice[0]["exchange_rate"] = invoice_exchange_map.get(invoice[0].get("invoice_number"))
@@ -332,6 +336,7 @@
entry_list = []
dr_or_cr_notes = []
+ difference_entries = []
for row in self.get("allocation"):
reconciled_entry = []
if row.invoice_number and row.allocated_amount:
@@ -344,13 +349,15 @@
reconciled_entry.append(payment_details)
if payment_details.difference_amount:
- self.make_difference_entry(payment_details)
+ difference_entries.append(
+ self.make_difference_entry(payment_details, do_not_save_and_submit=bool(dr_or_cr_notes))
+ )
if entry_list:
reconcile_against_document(entry_list, skip_ref_details_update_for_pe)
if dr_or_cr_notes:
- reconcile_dr_cr_note(dr_or_cr_notes, self.company)
+ reconcile_dr_cr_note(dr_or_cr_notes, difference_entries, self.company)
@frappe.whitelist()
def reconcile(self):
@@ -378,7 +385,7 @@
self.get_unreconciled_entries()
- def make_difference_entry(self, row):
+ def make_difference_entry(self, row, do_not_save_and_submit=False):
journal_entry = frappe.new_doc("Journal Entry")
journal_entry.voucher_type = "Exchange Gain Or Loss"
journal_entry.company = self.company
@@ -426,8 +433,11 @@
journal_entry.append("accounts", journal_account)
- journal_entry.save()
- journal_entry.submit()
+ if not do_not_save_and_submit:
+ journal_entry.save()
+ journal_entry.submit()
+
+ return journal_entry
def get_payment_details(self, row, dr_or_cr):
return frappe._dict(
@@ -593,7 +603,14 @@
return condition
-def reconcile_dr_cr_note(dr_cr_notes, company):
+def reconcile_dr_cr_note(dr_cr_notes, difference_entries, company):
+ def find_difference_entry(voucher_type, voucher_no):
+ for jv in difference_entries:
+ accounts = iter(jv.accounts)
+ for account in accounts:
+ if account.reference_type == voucher_type and account.reference_name == voucher_no:
+ return next(accounts)
+
for inv in dr_cr_notes:
voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note"
@@ -638,5 +655,9 @@
],
}
)
+
+ if difference_entry := find_difference_entry(inv.against_voucher_type, inv.against_voucher):
+ jv.append("accounts", difference_entry)
+
jv.flags.ignore_mandatory = True
jv.submit()
diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
index 3be11ae..2ac7df0 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
@@ -11,10 +11,13 @@
from erpnext import get_default_cost_center
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
+from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.party import get_party_account
from erpnext.stock.doctype.item.test_item import create_item
+test_dependencies = ["Item"]
+
class TestPaymentReconciliation(FrappeTestCase):
def setUp(self):
@@ -163,7 +166,9 @@
def create_payment_reconciliation(self):
pr = frappe.new_doc("Payment Reconciliation")
pr.company = self.company
- pr.party_type = "Customer"
+ pr.party_type = (
+ self.party_type if hasattr(self, "party_type") and self.party_type else "Customer"
+ )
pr.party = self.customer
pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company)
pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate()
@@ -890,6 +895,42 @@
self.assertEqual(pr.allocation[0].allocated_amount, 85)
self.assertEqual(pr.allocation[0].difference_amount, 0)
+ def test_reconciliation_purchase_invoice_against_return(self):
+ pi = make_purchase_invoice(
+ supplier="_Test Supplier USD", currency="USD", conversion_rate=50
+ ).submit()
+
+ pi_return = frappe.get_doc(pi.as_dict())
+ pi_return.name = None
+ pi_return.docstatus = 0
+ pi_return.is_return = 1
+ pi_return.conversion_rate = 80
+ pi_return.items[0].qty = -pi_return.items[0].qty
+ pi_return.submit()
+
+ self.company = "_Test Company"
+ self.party_type = "Supplier"
+ self.customer = "_Test Supplier USD"
+
+ pr = self.create_payment_reconciliation()
+ pr.get_unreconciled_entries()
+
+ invoices = []
+ payments = []
+ for invoice in pr.invoices:
+ if invoice.invoice_number == pi.name:
+ invoices.append(invoice.as_dict())
+ break
+ for payment in pr.payments:
+ if payment.reference_name == pi_return.name:
+ payments.append(payment.as_dict())
+ break
+
+ pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+
+ # Should not raise frappe.exceptions.ValidationError: Total Debit must be equal to Total Credit.
+ pr.reconcile()
+
def make_customer(customer_name, currency=None):
if not frappe.db.exists("Customer", customer_name):
diff --git a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js
index ea18ade..6046c13 100644
--- a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js
+++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js
@@ -2,7 +2,11 @@
// For license information, please see license.txt
frappe.ui.form.on('Payment Terms Template', {
- setup: function(frm) {
+ refresh: function(frm) {
+ frm.fields_dict.terms.grid.toggle_reqd("payment_term", frm.doc.allocate_payment_based_on_payment_terms);
+ },
+ allocate_payment_based_on_payment_terms: function(frm) {
+ frm.fields_dict.terms.grid.toggle_reqd("payment_term", frm.doc.allocate_payment_based_on_payment_terms);
}
});
diff --git a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py
index ea3b76c..7b04a68 100644
--- a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py
+++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py
@@ -11,7 +11,7 @@
class PaymentTermsTemplate(Document):
def validate(self):
self.validate_invoice_portion()
- self.check_duplicate_terms()
+ self.validate_terms()
def validate_invoice_portion(self):
total_portion = 0
@@ -23,9 +23,12 @@
_("Combined invoice portion must equal 100%"), raise_exception=1, indicator="red"
)
- def check_duplicate_terms(self):
+ def validate_terms(self):
terms = []
for term in self.terms:
+ if self.allocate_payment_based_on_payment_terms and not term.payment_term:
+ frappe.throw(_("Row {0}: Payment Term is mandatory").format(term.idx))
+
term_info = (term.payment_term, term.credit_days, term.credit_months, term.due_date_based_on)
if term_info in terms:
frappe.msgprint(
diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
index e6d9fe2..a6c0102 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
+++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
@@ -123,22 +123,29 @@
row.expected_amount = row.opening_amount;
}
- const pos_inv_promises = frm.doc.pos_transactions.map(
- row => frappe.db.get_doc("POS Invoice", row.pos_invoice)
- );
-
- const pos_invoices = await Promise.all(pos_inv_promises);
-
- for (let doc of pos_invoices) {
- frm.doc.grand_total += flt(doc.grand_total);
- frm.doc.net_total += flt(doc.net_total);
- frm.doc.total_quantity += flt(doc.total_qty);
- refresh_payments(doc, frm);
- refresh_taxes(doc, frm);
- refresh_fields(frm);
- set_html_data(frm);
- }
-
+ await Promise.all([
+ frappe.call({
+ method: 'erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_pos_invoices',
+ args: {
+ start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date),
+ end: frappe.datetime.get_datetime_as_string(frm.doc.period_end_date),
+ pos_profile: frm.doc.pos_profile,
+ user: frm.doc.user
+ },
+ callback: (r) => {
+ let pos_invoices = r.message;
+ for (let doc of pos_invoices) {
+ frm.doc.grand_total += flt(doc.grand_total);
+ frm.doc.net_total += flt(doc.net_total);
+ frm.doc.total_quantity += flt(doc.total_qty);
+ refresh_payments(doc, frm);
+ refresh_taxes(doc, frm);
+ refresh_fields(frm);
+ set_html_data(frm);
+ }
+ }
+ })
+ ])
frappe.dom.unfreeze();
}
});
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html
index 03abc93..5307ccb 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html
@@ -1,6 +1,6 @@
<div class="page-break">
<div id="header-html" class="hidden-pdf">
- {% if letter_head %}
+ {% if letter_head.content %}
<div class="letter-head text-center">{{ letter_head.content }}</div>
<hr style="height:2px;border-width:0;color:black;background-color:black;">
{% endif %}
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
index 7dd5ef3..cec48c1 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
@@ -65,6 +65,20 @@
frm.set_value('to_date', frappe.datetime.get_today());
}
},
+ report: function(frm){
+ let filters = {
+ 'company': frm.doc.company,
+ }
+ if(frm.doc.report == 'Accounts Receivable'){
+ filters['account_type'] = 'Receivable';
+ }
+ frm.set_query("account", function() {
+ return {
+ filters: filters
+ };
+ });
+
+ },
customer_collection: function(frm){
frm.set_value('collection_name', '');
if(frm.doc.customer_collection){
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json
index e23620f..8004659 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json
@@ -6,17 +6,24 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
+ "report",
"section_break_11",
"from_date",
+ "posting_date",
"company",
"account",
"group_by",
"cost_center",
+ "territory",
"column_break_14",
"to_date",
"finance_book",
"currency",
"project",
+ "payment_terms_template",
+ "sales_partner",
+ "sales_person",
+ "based_on_payment_terms",
"section_break_3",
"customer_collection",
"collection_name",
@@ -67,14 +74,14 @@
"reqd": 1
},
{
- "depends_on": "eval:doc.enable_auto_email == 0;",
+ "depends_on": "eval:(doc.enable_auto_email == 0 && doc.report == 'General Ledger');",
"fieldname": "from_date",
"fieldtype": "Date",
"label": "From Date",
"mandatory_depends_on": "eval:doc.frequency == '';"
},
{
- "depends_on": "eval:doc.enable_auto_email == 0;",
+ "depends_on": "eval:(doc.enable_auto_email == 0 && doc.report == 'General Ledger');",
"fieldname": "to_date",
"fieldtype": "Date",
"label": "To Date",
@@ -87,6 +94,7 @@
"options": "PSOA Cost Center"
},
{
+ "depends_on": "eval: (doc.report == 'General Ledger');",
"fieldname": "project",
"fieldtype": "Table MultiSelect",
"label": "Project",
@@ -104,7 +112,7 @@
{
"fieldname": "section_break_11",
"fieldtype": "Section Break",
- "label": "General Ledger Filters"
+ "label": "Report Filters"
},
{
"fieldname": "column_break_14",
@@ -164,12 +172,14 @@
},
{
"default": "Group by Voucher (Consolidated)",
+ "depends_on": "eval:(doc.report == 'General Ledger');",
"fieldname": "group_by",
"fieldtype": "Select",
"label": "Group By",
"options": "\nGroup by Voucher\nGroup by Voucher (Consolidated)"
},
{
+ "depends_on": "eval: (doc.report == 'General Ledger');",
"fieldname": "currency",
"fieldtype": "Link",
"label": "Currency",
@@ -297,6 +307,7 @@
},
{
"default": "0",
+ "depends_on": "eval: (doc.report == 'General Ledger');",
"fieldname": "show_net_values_in_party_account",
"fieldtype": "Check",
"label": "Show Net Values in Party Account"
@@ -310,10 +321,59 @@
{
"fieldname": "column_break_ocfq",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "report",
+ "fieldtype": "Select",
+ "label": "Report",
+ "options": "General Ledger\nAccounts Receivable",
+ "reqd": 1
+ },
+ {
+ "default": "Today",
+ "depends_on": "eval:(doc.report == 'Accounts Receivable');",
+ "fieldname": "posting_date",
+ "fieldtype": "Date",
+ "label": "Posting Date"
+ },
+ {
+ "depends_on": "eval: (doc.report == 'Accounts Receivable');",
+ "fieldname": "payment_terms_template",
+ "fieldtype": "Link",
+ "label": "Payment Terms Template",
+ "options": "Payment Terms Template"
+ },
+ {
+ "depends_on": "eval: (doc.report == 'Accounts Receivable');",
+ "fieldname": "sales_partner",
+ "fieldtype": "Link",
+ "label": "Sales Partner",
+ "options": "Sales Partner"
+ },
+ {
+ "depends_on": "eval: (doc.report == 'Accounts Receivable');",
+ "fieldname": "sales_person",
+ "fieldtype": "Link",
+ "label": "Sales Person",
+ "options": "Sales Person"
+ },
+ {
+ "depends_on": "eval: (doc.report == 'Accounts Receivable');",
+ "fieldname": "territory",
+ "fieldtype": "Link",
+ "label": "Territory",
+ "options": "Territory"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:(doc.report == 'Accounts Receivable');",
+ "fieldname": "based_on_payment_terms",
+ "fieldtype": "Check",
+ "label": "Based On Payment Terms"
}
],
"links": [],
- "modified": "2023-04-26 12:46:43.645455",
+ "modified": "2023-06-23 10:13:15.051950",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts",
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
index 67dbe09..08f4cf4 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
@@ -15,6 +15,7 @@
from erpnext import get_company_currency
from erpnext.accounts.party import get_party_account_currency
+from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute as get_ar_soa
from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_summary import (
execute as get_ageing,
)
@@ -43,29 +44,10 @@
def get_report_pdf(doc, consolidated=True):
statement_dict = {}
ageing = ""
- base_template_path = "frappe/www/printview.html"
- template_path = (
- "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html"
- )
for entry in doc.customers:
if doc.include_ageing:
- ageing_filters = frappe._dict(
- {
- "company": doc.company,
- "report_date": doc.to_date,
- "ageing_based_on": doc.ageing_based_on,
- "range1": 30,
- "range2": 60,
- "range3": 90,
- "range4": 120,
- "customer": entry.customer,
- }
- )
- col1, ageing = get_ageing(ageing_filters)
-
- if ageing:
- ageing[0]["ageing_based_on"] = doc.ageing_based_on
+ ageing = set_ageing(doc, entry)
tax_id = frappe.get_doc("Customer", entry.customer).tax_id
presentation_currency = (
@@ -73,60 +55,25 @@
or doc.currency
or get_company_currency(doc.company)
)
- if doc.letter_head:
- from frappe.www.printview import get_letter_head
- letter_head = get_letter_head(doc, 0)
+ filters = get_common_filters(doc)
- filters = frappe._dict(
- {
- "from_date": doc.from_date,
- "to_date": doc.to_date,
- "company": doc.company,
- "finance_book": doc.finance_book if doc.finance_book else None,
- "account": [doc.account] if doc.account else None,
- "party_type": "Customer",
- "party": [entry.customer],
- "party_name": [entry.customer_name] if entry.customer_name else None,
- "presentation_currency": presentation_currency,
- "group_by": doc.group_by,
- "currency": doc.currency,
- "cost_center": [cc.cost_center_name for cc in doc.cost_center],
- "project": [p.project_name for p in doc.project],
- "show_opening_entries": 0,
- "include_default_book_entries": 0,
- "tax_id": tax_id if tax_id else None,
- "show_net_values_in_party_account": doc.show_net_values_in_party_account,
- }
- )
- col, res = get_soa(filters)
+ if doc.report == "General Ledger":
+ filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))
+ else:
+ filters.update(get_ar_filters(doc, entry))
- for x in [0, -2, -1]:
- res[x]["account"] = res[x]["account"].replace("'", "")
+ if doc.report == "General Ledger":
+ col, res = get_soa(filters)
+ for x in [0, -2, -1]:
+ res[x]["account"] = res[x]["account"].replace("'", "")
+ if len(res) == 3:
+ continue
+ else:
+ ar_res = get_ar_soa(filters)
+ col, res = ar_res[0], ar_res[1]
- if len(res) == 3:
- continue
-
- html = frappe.render_template(
- template_path,
- {
- "filters": filters,
- "data": res,
- "ageing": ageing[0] if (doc.include_ageing and ageing) else None,
- "letter_head": letter_head if doc.letter_head else None,
- "terms_and_conditions": frappe.db.get_value(
- "Terms and Conditions", doc.terms_and_conditions, "terms"
- )
- if doc.terms_and_conditions
- else None,
- },
- )
-
- html = frappe.render_template(
- base_template_path,
- {"body": html, "css": get_print_style(), "title": "Statement For " + entry.customer},
- )
- statement_dict[entry.customer] = html
+ statement_dict[entry.customer] = get_html(doc, filters, entry, col, res, ageing)
if not bool(statement_dict):
return False
@@ -140,6 +87,110 @@
return statement_dict
+def set_ageing(doc, entry):
+ ageing_filters = frappe._dict(
+ {
+ "company": doc.company,
+ "report_date": doc.to_date,
+ "ageing_based_on": doc.ageing_based_on,
+ "range1": 30,
+ "range2": 60,
+ "range3": 90,
+ "range4": 120,
+ "customer": entry.customer,
+ }
+ )
+ col1, ageing = get_ageing(ageing_filters)
+
+ if ageing:
+ ageing[0]["ageing_based_on"] = doc.ageing_based_on
+
+ return ageing
+
+
+def get_common_filters(doc):
+ return frappe._dict(
+ {
+ "company": doc.company,
+ "finance_book": doc.finance_book if doc.finance_book else None,
+ "account": [doc.account] if doc.account else None,
+ "cost_center": [cc.cost_center_name for cc in doc.cost_center],
+ }
+ )
+
+
+def get_gl_filters(doc, entry, tax_id, presentation_currency):
+ return {
+ "from_date": doc.from_date,
+ "to_date": doc.to_date,
+ "party_type": "Customer",
+ "party": [entry.customer],
+ "party_name": [entry.customer_name] if entry.customer_name else None,
+ "presentation_currency": presentation_currency,
+ "group_by": doc.group_by,
+ "currency": doc.currency,
+ "project": [p.project_name for p in doc.project],
+ "show_opening_entries": 0,
+ "include_default_book_entries": 0,
+ "tax_id": tax_id if tax_id else None,
+ "show_net_values_in_party_account": doc.show_net_values_in_party_account,
+ }
+
+
+def get_ar_filters(doc, entry):
+ return {
+ "report_date": doc.posting_date if doc.posting_date else None,
+ "customer_name": entry.customer,
+ "payment_terms_template": doc.payment_terms_template if doc.payment_terms_template else None,
+ "sales_partner": doc.sales_partner if doc.sales_partner else None,
+ "sales_person": doc.sales_person if doc.sales_person else None,
+ "territory": doc.territory if doc.territory else None,
+ "based_on_payment_terms": doc.based_on_payment_terms,
+ "report_name": "Accounts Receivable",
+ "ageing_based_on": doc.ageing_based_on,
+ "range1": 30,
+ "range2": 60,
+ "range3": 90,
+ "range4": 120,
+ }
+
+
+def get_html(doc, filters, entry, col, res, ageing):
+ base_template_path = "frappe/www/printview.html"
+ template_path = (
+ "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html"
+ if doc.report == "General Ledger"
+ else "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html"
+ )
+
+ if doc.letter_head:
+ from frappe.www.printview import get_letter_head
+
+ letter_head = get_letter_head(doc, 0)
+
+ html = frappe.render_template(
+ template_path,
+ {
+ "filters": filters,
+ "data": res,
+ "report": {"report_name": doc.report, "columns": col},
+ "ageing": ageing[0] if (doc.include_ageing and ageing) else None,
+ "letter_head": letter_head if doc.letter_head else None,
+ "terms_and_conditions": frappe.db.get_value(
+ "Terms and Conditions", doc.terms_and_conditions, "terms"
+ )
+ if doc.terms_and_conditions
+ else None,
+ },
+ )
+
+ html = frappe.render_template(
+ base_template_path,
+ {"body": html, "css": get_print_style(), "title": "Statement For " + entry.customer},
+ )
+ return html
+
+
def get_customers_based_on_territory_or_customer_group(customer_collection, collection_name):
fields_dict = {
"Customer Group": "customer_group",
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html
new file mode 100644
index 0000000..07e1896
--- /dev/null
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html
@@ -0,0 +1,348 @@
+<style>
+ .print-format {
+ padding: 4mm;
+ font-size: 8.0pt !important;
+ }
+ .print-format td {
+ vertical-align:middle !important;
+ }
+ </style>
+
+ <h2 class="text-center" style="margin-top:0">{{ _(report.report_name) }}</h2>
+ <h4 class="text-center">
+ {% if (filters.customer_name) %}
+ {{ filters.customer_name }}
+ {% else %}
+ {{ filters.customer ~ filters.supplier }}
+ {% endif %}
+ </h4>
+ <h6 class="text-center">
+ {% if (filters.tax_id) %}
+ {{ _("Tax Id: ") }}{{ filters.tax_id }}
+ {% endif %}
+ </h6>
+ <h5 class="text-center">
+ {{ _(filters.ageing_based_on) }}
+ {{ _("Until") }}
+ {{ frappe.format(filters.report_date, 'Date') }}
+ </h5>
+
+ <div class="clearfix">
+ <div class="pull-left">
+ {% if(filters.payment_terms) %}
+ <strong>{{ _("Payment Terms") }}:</strong> {{ filters.payment_terms }}
+ {% endif %}
+ </div>
+ <div class="pull-right">
+ {% if(filters.credit_limit) %}
+ <strong>{{ _("Credit Limit") }}:</strong> {{ frappe.utils.fmt_money(filters.credit_limit) }}
+ {% endif %}
+ </div>
+ </div>
+
+ {% if(filters.show_future_payments) %}
+ {% set balance_row = data.slice(-1).pop() %}
+ {% for i in report.columns %}
+ {% if i.fieldname == 'age' %}
+ {% set elem = i %}
+ {% endif %}
+ {% endfor %}
+ {% set start = report.columns.findIndex(elem) %}
+ {% set range1 = report.columns[start].label %}
+ {% set range2 = report.columns[start+1].label %}
+ {% set range3 = report.columns[start+2].label %}
+ {% set range4 = report.columns[start+3].label %}
+ {% set range5 = report.columns[start+4].label %}
+ {% set range6 = report.columns[start+5].label %}
+
+ {% if(balance_row) %}
+ <table class="table table-bordered table-condensed">
+ <caption class="text-right">(Amount in {{ data[0]["currency"] ~ "" }})</caption>
+ <colgroup>
+ <col style="width: 30mm;">
+ <col style="width: 18mm;">
+ <col style="width: 18mm;">
+ <col style="width: 18mm;">
+ <col style="width: 18mm;">
+ <col style="width: 18mm;">
+ <col style="width: 18mm;">
+ <col style="width: 18mm;">
+ </colgroup>
+
+ <thead>
+ <tr>
+ <th>{{ _(" ") }}</th>
+ <th>{{ _(range1) }}</th>
+ <th>{{ _(range2) }}</th>
+ <th>{{ _(range3) }}</th>
+ <th>{{ _(range4) }}</th>
+ <th>{{ _(range5) }}</th>
+ <th>{{ _(range6) }}</th>
+ <th>{{ _("Total") }}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>{{ _("Total Outstanding") }}</td>
+ <td class="text-right">
+ {{ format_number(balance_row["age"], null, 2) }}
+ </td>
+ <td class="text-right">
+ {{ frappe.utils.fmt_money(balance_row["range1"], data[data.length-1]["currency"]) }}
+ </td>
+ <td class="text-right">
+ {{ frappe.utils.fmt_money(balance_row["range2"], data[data.length-1]["currency"]) }}
+ </td>
+ <td class="text-right">
+ {{ frappe.utils.fmt_money(balance_row["range3"], data[data.length-1]["currency"]) }}
+ </td>
+ <td class="text-right">
+ {{ frappe.utils.fmt_money(balance_row["range4"], data[data.length-1]["currency"]) }}
+ </td>
+ <td class="text-right">
+ {{ frappe.utils.fmt_money(balance_row["range5"], data[data.length-1]["currency"]) }}
+ </td>
+ <td class="text-right">
+ {{ frappe.utils.fmt_money(flt(balance_row["outstanding"]), data[data.length-1]["currency"]) }}
+ </td>
+ </tr>
+ <td>{{ _("Future Payments") }}</td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td class="text-right">
+ {{ frappe.utils.fmt_money(flt(balance_row[("future_amount")]), data[data.length-1]["currency"]) }}
+ </td>
+ <tr class="cvs-footer">
+ <th class="text-left">{{ _("Cheques Required") }}</th>
+ <th></th>
+ <th></th>
+ <th></th>
+ <th></th>
+ <th></th>
+ <th></th>
+ <th class="text-right">
+ {{ frappe.utils.fmt_money(flt(balance_row["outstanding"] - balance_row[("future_amount")]), data[data.length-1]["currency"]) }}</th>
+ </tr>
+ </tbody>
+
+ </table>
+ {% endif %}
+ {% endif %}
+ <table class="table table-bordered">
+ <thead>
+ <tr>
+ {% if(report.report_name == "Accounts Receivable" or report.report_name == "Accounts Payable") %}
+ <th style="width: 10%">{{ _("Date") }}</th>
+ <th style="width: 4%">{{ _("Age (Days)") }}</th>
+
+ {% if(report.report_name == "Accounts Receivable" and filters.show_sales_person) %}
+ <th style="width: 14%">{{ _("Reference") }}</th>
+ <th style="width: 10%">{{ _("Sales Person") }}</th>
+ {% else %}
+ <th style="width: 24%">{{ _("Reference") }}</th>
+ {% endif %}
+ {% if not(filters.show_future_payments) %}
+ <th style="width: 20%">
+ {% if (filters.customer or filters.supplier or filters.customer_name) %}
+ {{ _("Remarks") }}
+ {% else %}
+ {{ _("Party") }}
+ {% endif %}
+ </th>
+ {% endif %}
+ <th style="width: 10%; text-align: right">{{ _("Invoiced Amount") }}</th>
+ {% if not(filters.show_future_payments) %}
+ <th style="width: 10%; text-align: right">{{ _("Paid Amount") }}</th>
+ <th style="width: 10%; text-align: right">
+ {% if report.report_name == "Accounts Receivable" %}
+ {{ _('Credit Note') }}
+ {% else %}
+ {{ _('Debit Note') }}
+ {% endif %}
+ </th>
+ {% endif %}
+ <th style="width: 10%; text-align: right">{{ _("Outstanding Amount") }}</th>
+ {% if(filters.show_future_payments) %}
+ {% if(report.report_name == "Accounts Receivable") %}
+ <th style="width: 12%">{{ _("Customer LPO No.") }}</th>
+ {% endif %}
+ <th style="width: 10%">{{ _("Future Payment Ref") }}</th>
+ <th style="width: 10%">{{ _("Future Payment Amount") }}</th>
+ <th style="width: 10%">{{ _("Remaining Balance") }}</th>
+ {% endif %}
+ {% else %}
+ <th style="width: 40%">
+ {% if (filters.customer or filters.supplier or filters.customer_name) %}
+ {{ _("Remarks")}}
+ {% else %}
+ {{ _("Party") }}
+ {% endif %}
+ </th>
+ <th style="width: 15%">{{ _("Total Invoiced Amount") }}</th>
+ <th style="width: 15%">{{ _("Total Paid Amount") }}</th>
+ <th style="width: 15%">
+ {% if report.report_name == "Accounts Receivable Summary" %}
+ {{ _('Credit Note Amount') }}
+ {% else %}
+ {{ _('Debit Note Amount') }}
+ {% endif %}
+ </th>
+ <th style="width: 15%">{{ _("Total Outstanding Amount") }}</th>
+ {% endif %}
+ </tr>
+ </thead>
+ <tbody>
+ {% for i in range(data|length) %}
+ <tr>
+ {% if(report.report_name == "Accounts Receivable" or report.report_name == "Accounts Payable") %}
+ {% if(data[i]["party"]) %}
+ <td>{{ (data[i]["posting_date"]) }}</td>
+ <td style="text-align: right">{{ data[i]["age"] }}</td>
+ <td>
+ {% if not(filters.show_future_payments) %}
+ {{ data[i]["voucher_type"] }}
+ <br>
+ {% endif %}
+ {{ data[i]["voucher_no"] }}
+ </td>
+
+ {% if(report.report_name == "Accounts Receivable" and filters.show_sales_person) %}
+ <td>{{ data[i]["sales_person"] }}</td>
+ {% endif %}
+
+ {% if not (filters.show_future_payments) %}
+ <td>
+ {% if(not(filters.customer or filters.supplier or filters.customer_name)) %}
+ {{ data[i]["party"] }}
+ {% if(data[i]["customer_name"] and data[i]["customer_name"] != data[i]["party"]) %}
+ <br> {{ data[i]["customer_name"] }}
+ {% elif(data[i]["supplier_name"] != data[i]["party"]) %}
+ <br> {{ data[i]["supplier_name"] }}
+ {% endif %}
+ {% endif %}
+ <div>
+ {% if data[i]["remarks"] %}
+ {{ _("Remarks") }}:
+ {{ data[i]["remarks"] }}
+ {% endif %}
+ </div>
+ </td>
+ {% endif %}
+
+ <td style="text-align: right">
+ {{ frappe.utils.fmt_money(data[i]["invoiced"], currency=data[i]["currency"]) }}</td>
+
+ {% if not(filters.show_future_payments) %}
+ <td style="text-align: right">
+ {{ frappe.utils.fmt_money(data[i]["paid"], currency=data[i]["currency"]) }}</td>
+ <td style="text-align: right">
+ {{ frappe.utils.fmt_money(data[i]["credit_note"], currency=data[i]["currency"]) }}</td>
+ {% endif %}
+ <td style="text-align: right">
+ {{ frappe.utils.fmt_money(data[i]["outstanding"], currency=data[i]["currency"]) }}</td>
+
+ {% if(filters.show_future_payments) %}
+ {% if(report.report_name == "Accounts Receivable") %}
+ <td style="text-align: right">
+ {{ data[i]["po_no"] }}</td>
+ {% endif %}
+ <td style="text-align: right">{{ data[i]["future_ref"] }}</td>
+ <td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["future_amount"], currency=data[i]["currency"]) }}</td>
+ <td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["remaining_balance"], currency=data[i]["currency"]) }}</td>
+ {% endif %}
+ {% else %}
+ <td></td>
+ {% if not(filters.show_future_payments) %}
+ <td></td>
+ {% endif %}
+ {% if(report.report_name == "Accounts Receivable" and filters.show_sales_person) %}
+ <td></td>
+ {% endif %}
+ <td></td>
+ <td style="text-align: right"><b>{{ _("Total") }}</b></td>
+ <td style="text-align: right">
+ {{ frappe.utils.fmt_money(data[i]["invoiced"], data[i]["currency"]) }}</td>
+
+ {% if not(filters.show_future_payments) %}
+ <td style="text-align: right">
+ {{ frappe.utils.fmt_money(data[i]["paid"], currency=data[i]["currency"]) }}</td>
+ <td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["credit_note"], currency=data[i]["currency"]) }} </td>
+ {% endif %}
+ <td style="text-align: right">
+ {{ frappe.utils.fmt_money(data[i]["outstanding"], currency=data[i]["currency"]) }}</td>
+
+ {% if(filters.show_future_payments) %}
+ {% if(report.report_name == "Accounts Receivable") %}
+ <td style="text-align: right">
+ {{ data[i]["po_no"] }}</td>
+ {% endif %}
+ <td style="text-align: right">{{ data[i]["future_ref"] }}</td>
+ <td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["future_amount"], currency=data[i]["currency"]) }}</td>
+ <td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["remaining_balance"], currency=data[i]["currency"]) }}</td>
+ {% endif %}
+ {% endif %}
+ {% else %}
+ {% if(data[i]["party"] or " ") %}
+ {% if not(data[i]["is_total_row"]) %}
+ <td>
+ {% if(not(filters.customer | filters.supplier)) %}
+ {{ data[i]["party"] }}
+ {% if(data[i]["customer_name"] and data[i]["customer_name"] != data[i]["party"]) %}
+ <br> {{ data[i]["customer_name"] }}
+ {% elif(data[i]["supplier_name"] != data[i]["party"]) %}
+ <br> {{ data[i]["supplier_name"] }}
+ {% endif %}
+ {% endif %}
+ <br>{{ _("Remarks") }}:
+ {{ data[i]["remarks"] }}
+ </td>
+ {% else %}
+ <td><b>{{ _("Total") }}</b></td>
+ {% endif %}
+ <td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["invoiced"], currency=data[i]["currency"]) }}</td>
+ <td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["paid"], currency=data[i]["currency"]) }}</td>
+ <td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["credit_note"], currency=data[i]["currency"]) }}</td>
+ <td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["outstanding"], currency=data[i]["currency"]) }}</td>
+ {% endif %}
+ {% endif %}
+ </tr>
+ {% endfor %}
+ <td></td>
+ <td></td>
+ <td></td>
+ <td></td>
+ <td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="invoiced"), currency=data[0]["currency"]) }}</b></td>
+ <td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="paid"), currency=data[0]["currency"]) }}</b></td>
+ <td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="credit_note"), currency=data[0]["currency"]) }}</b></td>
+ <td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="outstanding"), currency=data[0]["currency"]) }}</b></td>
+ </tbody>
+ </table>
+ <br>
+ {% if ageing %}
+ <h4 class="text-center">{{ _("Ageing Report based on ") }} {{ ageing.ageing_based_on }}
+ {{ _("up to " ) }} {{ frappe.format(filters.report_date, 'Date')}}
+ </h4>
+ <table class="table table-bordered">
+ <thead>
+ <tr>
+ <th style="width: 25%">30 Days</th>
+ <th style="width: 25%">60 Days</th>
+ <th style="width: 25%">90 Days</th>
+ <th style="width: 25%">120 Days</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>{{ frappe.utils.fmt_money(ageing.range1, currency=data[0]["currency"]) }}</td>
+ <td>{{ frappe.utils.fmt_money(ageing.range2, currency=data[0]["currency"]) }}</td>
+ <td>{{ frappe.utils.fmt_money(ageing.range3, currency=data[0]["currency"]) }}</td>
+ <td>{{ frappe.utils.fmt_money(ageing.range4, currency=data[0]["currency"]) }}</td>
+ </tr>
+ </tbody>
+ </table>
+ {% endif %}
+ <p class="text-right text-muted">{{ _("Printed On ") }}{{ frappe.utils.now() }}</p>
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index ab7884d..6a558ca 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -54,9 +54,11 @@
hide_fields(this.frm.doc);
// Show / Hide button
this.show_general_ledger();
+ erpnext.accounts.ledger_preview.show_accounting_ledger_preview(this.frm);
- if(doc.update_stock==1 && doc.docstatus==1) {
+ if(doc.update_stock==1) {
this.show_stock_ledger();
+ erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm);
}
if(!doc.is_return && doc.docstatus == 1 && doc.outstanding_amount != 0){
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 8cb2950..68407e0 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -88,8 +88,12 @@
}
this.show_general_ledger();
+ erpnext.accounts.ledger_preview.show_accounting_ledger_preview(this.frm);
- if(doc.update_stock) this.show_stock_ledger();
+ if(doc.update_stock){
+ this.show_stock_ledger();
+ erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm);
+ }
if (doc.docstatus == 1 && doc.outstanding_amount!=0
&& !(cint(doc.is_return) && doc.return_against)) {
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 7b68dd4..f0d3f72 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -320,6 +320,7 @@
},
{
"default": "0",
+ "depends_on": "eval: !doc.is_debit_note",
"fieldname": "is_return",
"fieldtype": "Check",
"hide_days": 1,
@@ -1960,6 +1961,7 @@
},
{
"default": "0",
+ "depends_on": "eval: !doc.is_return",
"description": "Issue a debit note with 0 qty against an existing Sales Invoice",
"fieldname": "is_debit_note",
"fieldtype": "Check",
@@ -2155,7 +2157,7 @@
"link_fieldname": "consolidated_invoice"
}
],
- "modified": "2023-06-03 16:22:16.219333",
+ "modified": "2023-06-21 16:02:18.988799",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
diff --git a/erpnext/accounts/doctype/shareholder/shareholder.json b/erpnext/accounts/doctype/shareholder/shareholder.json
index e94aea9..e80b057 100644
--- a/erpnext/accounts/doctype/shareholder/shareholder.json
+++ b/erpnext/accounts/doctype/shareholder/shareholder.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"autoname": "naming_series:",
"creation": "2017-12-25 16:50:53.878430",
"doctype": "DocType",
@@ -111,11 +112,12 @@
"read_only": 1
}
],
- "modified": "2019-11-17 23:24:11.395882",
+ "links": [],
+ "modified": "2023-04-10 22:02:20.406087",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Shareholder",
- "name_case": "Title Case",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
@@ -158,6 +160,7 @@
"search_fields": "folio_no",
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"title_field": "title",
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html
index f2bf942..ed3b991 100644
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html
@@ -284,4 +284,4 @@
{% } %}
</tbody>
</table>
- <p class="text-right text-muted">{{ __("Printed On ") }}{%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}</p>
+ <p class="text-right text-muted">{{ __("Printed On ") }}{%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}</p>
\ No newline at end of file
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 d34c213..924c14b 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
@@ -87,7 +87,7 @@
"project": d.project,
"company": d.company,
"purchase_order": d.purchase_order,
- "purchase_receipt": d.purchase_receipt,
+ "purchase_receipt": purchase_receipt,
"expense_account": expense_account,
"stock_qty": d.stock_qty,
"stock_uom": d.stock_uom,
@@ -241,7 +241,7 @@
},
{
"label": _("Purchase Receipt"),
- "fieldname": "Purchase Receipt",
+ "fieldname": "purchase_receipt",
"fieldtype": "Link",
"options": "Purchase Receipt",
"width": 100,
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 0ee06e8..a5cb324 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -237,11 +237,6 @@
if not (frappe.flags.ignore_account_permission or ignore_account_permission):
acc.check_permission("read")
- if report_type == "Profit and Loss":
- # for pl accounts, get balance within a fiscal year
- cond.append(
- "posting_date >= '%s' and voucher_type != 'Period Closing Voucher'" % year_start_date
- )
# different filter for group and ledger - improved performance
if acc.is_group:
cond.append(
diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py
index bfef57e..259568a 100644
--- a/erpnext/assets/doctype/asset/depreciation.py
+++ b/erpnext/assets/doctype/asset/depreciation.py
@@ -159,15 +159,15 @@
je.flags.ignore_permissions = True
je.flags.planned_depr_entry = True
je.save()
- if not je.meta.get_workflow():
- je.submit()
d.db_set("journal_entry", je.name)
- idx = cint(asset_depr_schedule_doc.finance_book_id)
- row = asset.get("finance_books")[idx - 1]
- row.value_after_depreciation -= d.depreciation_amount
- row.db_update()
+ if not je.meta.get_workflow():
+ je.submit()
+ idx = cint(asset_depr_schedule_doc.finance_book_id)
+ row = asset.get("finance_books")[idx - 1]
+ row.value_after_depreciation -= d.depreciation_amount
+ row.db_update()
asset.db_set("depr_entry_posting_status", "Successful")
diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js
index 01fcb11..6d55d77 100644
--- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js
+++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js
@@ -15,7 +15,6 @@
}
refresh() {
- erpnext.hide_company();
this.show_general_ledger();
if ((this.frm.doc.stock_items && this.frm.doc.stock_items.length) || !this.frm.doc.target_is_fixed_asset) {
this.show_stock_ledger();
@@ -129,10 +128,6 @@
return this.get_target_item_details();
}
- target_asset() {
- return this.get_target_asset_details();
- }
-
item_code(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
if (cdt === "Asset Capitalization Stock Item") {
@@ -247,26 +242,6 @@
}
}
- get_target_asset_details() {
- var me = this;
-
- if (me.frm.doc.target_asset) {
- return me.frm.call({
- method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_target_asset_details",
- child: me.frm.doc,
- args: {
- asset: me.frm.doc.target_asset,
- company: me.frm.doc.company,
- },
- callback: function (r) {
- if (!r.exc) {
- me.frm.refresh_fields();
- }
- }
- });
- }
- }
-
get_consumed_stock_item_details(row) {
var me = this;
diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json
index 01b35f6..04b0c4e 100644
--- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json
+++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json
@@ -11,13 +11,14 @@
"naming_series",
"entry_type",
"target_item_code",
+ "target_asset",
"target_item_name",
"target_is_fixed_asset",
"target_has_batch_no",
"target_has_serial_no",
"column_break_9",
- "target_asset",
"target_asset_name",
+ "target_asset_location",
"target_warehouse",
"target_qty",
"target_stock_uom",
@@ -85,14 +86,13 @@
"fieldtype": "Column Break"
},
{
- "depends_on": "eval:doc.entry_type=='Capitalization'",
"fieldname": "target_asset",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Target Asset",
- "mandatory_depends_on": "eval:doc.entry_type=='Capitalization'",
"no_copy": 1,
- "options": "Asset"
+ "options": "Asset",
+ "read_only": 1
},
{
"depends_on": "eval:doc.entry_type=='Capitalization'",
@@ -108,11 +108,11 @@
"fieldtype": "Column Break"
},
{
- "fetch_from": "asset.company",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
+ "remember_last_selected_value": 1,
"reqd": 1
},
{
@@ -158,7 +158,7 @@
"read_only": 1
},
{
- "depends_on": "eval:doc.docstatus == 0 || (doc.stock_items && doc.stock_items.length)",
+ "depends_on": "eval:doc.entry_type=='Capitalization' && (doc.docstatus == 0 || (doc.stock_items && doc.stock_items.length))",
"fieldname": "section_break_16",
"fieldtype": "Section Break",
"label": "Consumed Stock Items"
@@ -189,7 +189,7 @@
"fieldname": "target_qty",
"fieldtype": "Float",
"label": "Target Qty",
- "read_only_depends_on": "target_is_fixed_asset"
+ "read_only_depends_on": "eval:doc.entry_type=='Capitalization'"
},
{
"fetch_from": "target_item_code.stock_uom",
@@ -227,7 +227,7 @@
"depends_on": "eval:doc.docstatus == 0 || (doc.asset_items && doc.asset_items.length)",
"fieldname": "section_break_26",
"fieldtype": "Section Break",
- "label": "Consumed Asset Items"
+ "label": "Consumed Assets"
},
{
"fieldname": "asset_items",
@@ -266,7 +266,7 @@
"options": "Finance Book"
},
{
- "depends_on": "eval:doc.docstatus == 0 || (doc.service_items && doc.service_items.length)",
+ "depends_on": "eval:doc.entry_type=='Capitalization' && (doc.docstatus == 0 || (doc.service_items && doc.service_items.length))",
"fieldname": "service_expenses_section",
"fieldtype": "Section Break",
"label": "Service Expenses"
@@ -329,12 +329,20 @@
"label": "Target Fixed Asset Account",
"options": "Account",
"read_only": 1
+ },
+ {
+ "depends_on": "eval:doc.entry_type=='Capitalization'",
+ "fieldname": "target_asset_location",
+ "fieldtype": "Link",
+ "label": "Target Asset Location",
+ "mandatory_depends_on": "eval:doc.entry_type=='Capitalization'",
+ "options": "Location"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2022-10-12 15:09:40.771332",
+ "modified": "2023-06-22 14:17:07.995120",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Capitalization",
diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
index 6841c56..a883bec 100644
--- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
+++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
@@ -19,9 +19,6 @@
reverse_depreciation_entry_made_after_disposal,
)
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
-from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
- make_new_active_asset_depr_schedules_and_cancel_current_ones,
-)
from erpnext.controllers.stock_controller import StockController
from erpnext.setup.doctype.brand.brand import get_brand_defaults
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
@@ -45,7 +42,6 @@
"target_has_batch_no",
"target_stock_uom",
"stock_uom",
- "target_fixed_asset_account",
"fixed_asset_account",
"valuation_rate",
]
@@ -56,7 +52,6 @@
self.validate_posting_time()
self.set_missing_values(for_validate=True)
self.validate_target_item()
- self.validate_target_asset()
self.validate_consumed_stock_item()
self.validate_consumed_asset_item()
self.validate_service_item()
@@ -71,11 +66,12 @@
def before_submit(self):
self.validate_source_mandatory()
+ if self.entry_type == "Capitalization":
+ self.create_target_asset()
def on_submit(self):
self.update_stock_ledger()
self.make_gl_entries()
- self.update_target_asset()
def on_cancel(self):
self.ignore_linked_doctypes = (
@@ -86,7 +82,7 @@
)
self.update_stock_ledger()
self.make_gl_entries()
- self.update_target_asset()
+ self.restore_consumed_asset_items()
def set_title(self):
self.title = self.target_asset_name or self.target_item_name or self.target_item_code
@@ -97,15 +93,6 @@
if self.meta.has_field(k) and (not self.get(k) or k in force_fields):
self.set(k, v)
- # Remove asset if item not a fixed asset
- if not self.target_is_fixed_asset:
- self.target_asset = None
-
- target_asset_details = get_target_asset_details(self.target_asset, self.company)
- for k, v in target_asset_details.items():
- if self.meta.has_field(k) and (not self.get(k) or k in force_fields):
- self.set(k, v)
-
for d in self.stock_items:
args = self.as_dict()
args.update(d.as_dict())
@@ -157,9 +144,6 @@
if not target_item.is_stock_item:
self.target_warehouse = None
- if not target_item.is_fixed_asset:
- self.target_asset = None
- self.target_fixed_asset_account = None
if not target_item.has_batch_no:
self.target_batch_no = None
if not target_item.has_serial_no:
@@ -170,17 +154,6 @@
self.validate_item(target_item)
- def validate_target_asset(self):
- if self.target_asset:
- target_asset = self.get_asset_for_validation(self.target_asset)
-
- if target_asset.item_code != self.target_item_code:
- frappe.throw(
- _("Asset {0} does not belong to Item {1}").format(self.target_asset, self.target_item_code)
- )
-
- self.validate_asset(target_asset)
-
def validate_consumed_stock_item(self):
for d in self.stock_items:
if d.item_code:
@@ -386,7 +359,11 @@
gl_entries, target_account, target_against, precision
)
+ if not self.stock_items and not self.service_items and self.are_all_asset_items_non_depreciable:
+ return []
+
self.get_gl_entries_for_target_item(gl_entries, target_against, precision)
+
return gl_entries
def get_target_account(self):
@@ -429,11 +406,14 @@
def get_gl_entries_for_consumed_asset_items(
self, gl_entries, target_account, target_against, precision
):
+ self.are_all_asset_items_non_depreciable = True
+
# Consumed Assets
for item in self.asset_items:
- asset = self.get_asset(item)
+ asset = frappe.get_doc("Asset", item.asset)
if asset.calculate_depreciation:
+ self.are_all_asset_items_non_depreciable = False
notes = _(
"This schedule was created when Asset {0} was consumed through Asset Capitalization {1}."
).format(
@@ -519,40 +499,46 @@
)
)
- def update_target_asset(self):
+ def create_target_asset(self):
total_target_asset_value = flt(self.total_value, self.precision("total_value"))
- if self.docstatus == 1 and self.entry_type == "Capitalization":
- asset_doc = frappe.get_doc("Asset", self.target_asset)
- asset_doc.purchase_date = self.posting_date
- asset_doc.gross_purchase_amount = total_target_asset_value
- asset_doc.purchase_receipt_amount = total_target_asset_value
- notes = _(
- "This schedule was created when target Asset {0} was updated through Asset Capitalization {1}."
- ).format(
- get_link_to_form(asset_doc.doctype, asset_doc.name), get_link_to_form(self.doctype, self.name)
- )
- make_new_active_asset_depr_schedules_and_cancel_current_ones(asset_doc, notes)
- asset_doc.flags.ignore_validate_update_after_submit = True
- asset_doc.save()
- elif self.docstatus == 2:
- for item in self.asset_items:
- asset = self.get_asset(item)
- asset.db_set("disposal_date", None)
- self.set_consumed_asset_status(asset)
+ asset_doc = frappe.new_doc("Asset")
+ asset_doc.company = self.company
+ asset_doc.item_code = self.target_item_code
+ asset_doc.is_existing_asset = 1
+ asset_doc.location = self.target_asset_location
+ asset_doc.available_for_use_date = self.posting_date
+ asset_doc.purchase_date = self.posting_date
+ asset_doc.gross_purchase_amount = total_target_asset_value
+ asset_doc.purchase_receipt_amount = total_target_asset_value
+ asset_doc.flags.ignore_validate = True
+ asset_doc.insert()
- if asset.calculate_depreciation:
- reverse_depreciation_entry_made_after_disposal(asset, self.posting_date)
- notes = _(
- "This schedule was created when Asset {0} was restored on Asset Capitalization {1}'s cancellation."
- ).format(
- get_link_to_form(asset.doctype, asset.name), get_link_to_form(self.doctype, self.name)
- )
- reset_depreciation_schedule(asset, self.posting_date, notes)
+ self.target_asset = asset_doc.name
- def get_asset(self, item):
- asset = frappe.get_doc("Asset", item.asset)
- self.check_finance_books(item, asset)
- return asset
+ self.target_fixed_asset_account = get_asset_category_account(
+ "fixed_asset_account", item=self.target_item_code, company=asset_doc.company
+ )
+
+ frappe.msgprint(
+ _(
+ "Asset {0} has been created. Please set the depreciation details if any and submit it."
+ ).format(get_link_to_form("Asset", asset_doc.name))
+ )
+
+ def restore_consumed_asset_items(self):
+ for item in self.asset_items:
+ asset = frappe.get_doc("Asset", item.asset)
+ asset.db_set("disposal_date", None)
+ self.set_consumed_asset_status(asset)
+
+ if asset.calculate_depreciation:
+ reverse_depreciation_entry_made_after_disposal(asset, self.posting_date)
+ notes = _(
+ "This schedule was created when Asset {0} was restored on Asset Capitalization {1}'s cancellation."
+ ).format(
+ get_link_to_form(asset.doctype, asset.name), get_link_to_form(self.doctype, self.name)
+ )
+ reset_depreciation_schedule(asset, self.posting_date, notes)
def set_consumed_asset_status(self, asset):
if self.docstatus == 1:
@@ -603,33 +589,6 @@
@frappe.whitelist()
-def get_target_asset_details(asset=None, company=None):
- out = frappe._dict()
-
- # Get Asset Details
- asset_details = frappe._dict()
- if asset:
- asset_details = frappe.db.get_value("Asset", asset, ["asset_name", "item_code"], as_dict=1)
- if not asset_details:
- frappe.throw(_("Asset {0} does not exist").format(asset))
-
- # Re-set item code from Asset
- out.target_item_code = asset_details.item_code
-
- # Set Asset Details
- out.asset_name = asset_details.asset_name
-
- if asset_details.item_code:
- out.target_fixed_asset_account = get_asset_category_account(
- "fixed_asset_account", item=asset_details.item_code, company=company
- )
- else:
- out.target_fixed_asset_account = None
-
- return out
-
-
-@frappe.whitelist()
def get_consumed_stock_item_details(args):
if isinstance(args, str):
args = json.loads(args)
diff --git a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py
index 5345d0e..6e0a685 100644
--- a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py
+++ b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py
@@ -47,13 +47,6 @@
total_amount = 103000
- # Create assets
- target_asset = create_asset(
- asset_name="Asset Capitalization Target Asset",
- submit=1,
- warehouse="Stores - TCP1",
- company=company,
- )
consumed_asset = create_asset(
asset_name="Asset Capitalization Consumable Asset",
asset_value=consumed_asset_value,
@@ -65,7 +58,8 @@
# Create and submit Asset Captitalization
asset_capitalization = create_asset_capitalization(
entry_type="Capitalization",
- target_asset=target_asset.name,
+ target_item_code="Macbook Pro",
+ target_asset_location="Test Location",
stock_qty=stock_qty,
stock_rate=stock_rate,
consumed_asset=consumed_asset.name,
@@ -94,7 +88,7 @@
self.assertEqual(asset_capitalization.target_incoming_rate, total_amount)
# Test Target Asset values
- target_asset.reload()
+ target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
self.assertEqual(target_asset.purchase_receipt_amount, total_amount)
@@ -142,13 +136,6 @@
total_amount = 103000
- # Create assets
- target_asset = create_asset(
- asset_name="Asset Capitalization Target Asset",
- submit=1,
- warehouse="Stores - _TC",
- company=company,
- )
consumed_asset = create_asset(
asset_name="Asset Capitalization Consumable Asset",
asset_value=consumed_asset_value,
@@ -160,7 +147,8 @@
# Create and submit Asset Captitalization
asset_capitalization = create_asset_capitalization(
entry_type="Capitalization",
- target_asset=target_asset.name,
+ target_item_code="Macbook Pro",
+ target_asset_location="Test Location",
stock_qty=stock_qty,
stock_rate=stock_rate,
consumed_asset=consumed_asset.name,
@@ -189,7 +177,7 @@
self.assertEqual(asset_capitalization.target_incoming_rate, total_amount)
# Test Target Asset values
- target_asset.reload()
+ target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
self.assertEqual(target_asset.purchase_receipt_amount, total_amount)
@@ -364,6 +352,7 @@
"posting_time": args.posting_time or now.strftime("%H:%M:%S.%f"),
"target_item_code": target_item_code,
"target_asset": target_asset.name,
+ "target_asset_location": "Test Location",
"target_warehouse": target_warehouse,
"target_qty": flt(args.target_qty) or 1,
"target_batch_no": args.target_batch_no,
diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py
index 8303141..641d35f 100644
--- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py
+++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py
@@ -42,7 +42,6 @@
maintenance_log.db_set("maintenance_status", "Cancelled")
-@frappe.whitelist()
def assign_tasks(asset_maintenance_name, assign_to_member, maintenance_task, next_due_date):
team_member = frappe.db.get_value("User", assign_to_member, "email")
args = {
diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js
index 4f7b836..b788a32 100644
--- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js
+++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js
@@ -20,56 +20,6 @@
default: 'In Location'
},
{
- "fieldname":"filter_based_on",
- "label": __("Period Based On"),
- "fieldtype": "Select",
- "options": ["Fiscal Year", "Date Range"],
- "default": "Fiscal Year",
- "reqd": 1
- },
- {
- "fieldname":"from_date",
- "label": __("Start Date"),
- "fieldtype": "Date",
- "default": frappe.datetime.add_months(frappe.datetime.nowdate(), -12),
- "depends_on": "eval: doc.filter_based_on == 'Date Range'",
- "reqd": 1
- },
- {
- "fieldname":"to_date",
- "label": __("End Date"),
- "fieldtype": "Date",
- "default": frappe.datetime.nowdate(),
- "depends_on": "eval: doc.filter_based_on == 'Date Range'",
- "reqd": 1
- },
- {
- "fieldname":"from_fiscal_year",
- "label": __("Start Year"),
- "fieldtype": "Link",
- "options": "Fiscal Year",
- "default": frappe.defaults.get_user_default("fiscal_year"),
- "depends_on": "eval: doc.filter_based_on == 'Fiscal Year'",
- "reqd": 1
- },
- {
- "fieldname":"to_fiscal_year",
- "label": __("End Year"),
- "fieldtype": "Link",
- "options": "Fiscal Year",
- "default": frappe.defaults.get_user_default("fiscal_year"),
- "depends_on": "eval: doc.filter_based_on == 'Fiscal Year'",
- "reqd": 1
- },
- {
- "fieldname":"date_based_on",
- "label": __("Date Based On"),
- "fieldtype": "Select",
- "options": ["Purchase Date", "Available For Use Date"],
- "default": "Purchase Date",
- "reqd": 1
- },
- {
fieldname:"asset_category",
label: __("Asset Category"),
fieldtype: "Link",
@@ -90,21 +40,66 @@
reqd: 1
},
{
+ fieldname:"only_existing_assets",
+ label: __("Only existing assets"),
+ fieldtype: "Check"
+ },
+ {
fieldname:"finance_book",
label: __("Finance Book"),
fieldtype: "Link",
options: "Finance Book",
- depends_on: "eval: doc.filter_by_finance_book == 1",
},
{
- fieldname:"filter_by_finance_book",
- label: __("Filter by Finance Book"),
- fieldtype: "Check"
+ "fieldname": "include_default_book_assets",
+ "label": __("Include Default Book Assets"),
+ "fieldtype": "Check",
+ "default": 1
},
{
- fieldname:"only_existing_assets",
- label: __("Only existing assets"),
- fieldtype: "Check"
+ "fieldname":"filter_based_on",
+ "label": __("Period Based On"),
+ "fieldtype": "Select",
+ "options": ["--Select a period--", "Fiscal Year", "Date Range"],
+ "default": "--Select a period--",
+ },
+ {
+ "fieldname":"from_date",
+ "label": __("Start Date"),
+ "fieldtype": "Date",
+ "default": frappe.datetime.add_months(frappe.datetime.nowdate(), -12),
+ "depends_on": "eval: doc.filter_based_on == 'Date Range'",
+ },
+ {
+ "fieldname":"to_date",
+ "label": __("End Date"),
+ "fieldtype": "Date",
+ "default": frappe.datetime.nowdate(),
+ "depends_on": "eval: doc.filter_based_on == 'Date Range'",
+ },
+ {
+ "fieldname":"from_fiscal_year",
+ "label": __("Start Year"),
+ "fieldtype": "Link",
+ "options": "Fiscal Year",
+ "default": frappe.defaults.get_user_default("fiscal_year"),
+ "depends_on": "eval: doc.filter_based_on == 'Fiscal Year'",
+ },
+ {
+ "fieldname":"to_fiscal_year",
+ "label": __("End Year"),
+ "fieldtype": "Link",
+ "options": "Fiscal Year",
+ "default": frappe.defaults.get_user_default("fiscal_year"),
+ "depends_on": "eval: doc.filter_based_on == 'Fiscal Year'",
+ },
+ {
+ "fieldname":"date_based_on",
+ "label": __("Date Based On"),
+ "fieldtype": "Select",
+ "options": ["Purchase Date", "Available For Use Date"],
+ "default": "Purchase Date",
+ "depends_on": "eval: doc.filter_based_on == 'Date Range' || doc.filter_based_on == 'Fiscal Year'",
},
]
};
diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
index 984b3fd..6911f94 100644
--- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
+++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
@@ -2,9 +2,11 @@
# For license information, please see license.txt
+from itertools import chain
+
import frappe
from frappe import _
-from frappe.query_builder.functions import Sum
+from frappe.query_builder.functions import IfNull, Sum
from frappe.utils import cstr, flt, formatdate, getdate
from erpnext.accounts.report.financial_statements import (
@@ -13,7 +15,6 @@
validate_fiscal_year,
)
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
-from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts
def execute(filters=None):
@@ -64,11 +65,9 @@
def get_data(filters):
-
data = []
conditions = get_conditions(filters)
- depreciation_amount_map = get_finance_book_value_map(filters)
pr_supplier_map = get_purchase_receipt_supplier_map()
pi_supplier_map = get_purchase_invoice_supplier_map()
@@ -102,20 +101,31 @@
]
assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields)
- assets_linked_to_fb = None
+ assets_linked_to_fb = get_assets_linked_to_fb(filters)
- if filters.filter_by_finance_book:
- assets_linked_to_fb = frappe.db.get_all(
- doctype="Asset Finance Book",
- filters={"finance_book": filters.finance_book or ("is", "not set")},
- pluck="parent",
- )
+ company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book")
+
+ if filters.include_default_book_assets and company_fb:
+ finance_book = company_fb
+ elif filters.finance_book:
+ finance_book = filters.finance_book
+ else:
+ finance_book = None
+
+ depreciation_amount_map = get_asset_depreciation_amount_map(filters, finance_book)
for asset in assets_record:
- if assets_linked_to_fb and asset.asset_id not in assets_linked_to_fb:
+ if (
+ assets_linked_to_fb
+ and asset.calculate_depreciation
+ and asset.asset_id not in assets_linked_to_fb
+ ):
continue
- asset_value = get_asset_value_after_depreciation(asset.asset_id, filters.finance_book)
+ asset_value = get_asset_value_after_depreciation(
+ asset.asset_id, finance_book
+ ) or get_asset_value_after_depreciation(asset.asset_id)
+
row = {
"asset_id": asset.asset_id,
"asset_name": asset.asset_name,
@@ -126,7 +136,7 @@
or pi_supplier_map.get(asset.purchase_invoice),
"gross_purchase_amount": asset.gross_purchase_amount,
"opening_accumulated_depreciation": asset.opening_accumulated_depreciation,
- "depreciated_amount": get_depreciation_amount_of_asset(asset, depreciation_amount_map, filters),
+ "depreciated_amount": get_depreciation_amount_of_asset(asset, depreciation_amount_map),
"available_for_use_date": asset.available_for_use_date,
"location": asset.location,
"asset_category": asset.asset_category,
@@ -140,14 +150,23 @@
def prepare_chart_data(data, filters):
labels_values_map = {}
- date_field = frappe.scrub(filters.date_based_on)
+ if filters.filter_based_on not in ("Date Range", "Fiscal Year"):
+ filters_filter_based_on = "Date Range"
+ date_field = "purchase_date"
+ filters_from_date = min(data, key=lambda a: a.get(date_field)).get(date_field)
+ filters_to_date = max(data, key=lambda a: a.get(date_field)).get(date_field)
+ else:
+ filters_filter_based_on = filters.filter_based_on
+ date_field = frappe.scrub(filters.date_based_on)
+ filters_from_date = filters.from_date
+ filters_to_date = filters.to_date
period_list = get_period_list(
filters.from_fiscal_year,
filters.to_fiscal_year,
- filters.from_date,
- filters.to_date,
- filters.filter_based_on,
+ filters_from_date,
+ filters_to_date,
+ filters_filter_based_on,
"Monthly",
company=filters.company,
ignore_fiscal_year=True,
@@ -184,59 +203,76 @@
}
-def get_depreciation_amount_of_asset(asset, depreciation_amount_map, filters):
- if asset.calculate_depreciation:
- depr_amount = depreciation_amount_map.get(asset.asset_id) or 0.0
- else:
- depr_amount = get_manual_depreciation_amount_of_asset(asset, filters)
+def get_assets_linked_to_fb(filters):
+ afb = frappe.qb.DocType("Asset Finance Book")
- return flt(depr_amount, 2)
-
-
-def get_finance_book_value_map(filters):
- date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date
-
- return frappe._dict(
- frappe.db.sql(
- """ Select
- ads.asset, SUM(depreciation_amount)
- FROM `tabAsset Depreciation Schedule` ads, `tabDepreciation Schedule` ds
- WHERE
- ds.parent = ads.name
- AND ifnull(ads.finance_book, '')=%s
- AND ads.docstatus=1
- AND ds.parentfield='depreciation_schedule'
- AND ds.schedule_date<=%s
- AND ds.journal_entry IS NOT NULL
- GROUP BY ads.asset""",
- (cstr(filters.finance_book or ""), date),
- )
+ query = frappe.qb.from_(afb).select(
+ afb.parent,
)
+ if filters.include_default_book_assets:
+ company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book")
-def get_manual_depreciation_amount_of_asset(asset, filters):
+ if filters.finance_book and company_fb and cstr(filters.finance_book) != cstr(company_fb):
+ frappe.throw(_("To use a different finance book, please uncheck 'Include Default Book Assets'"))
+
+ query = query.where(
+ (afb.finance_book.isin([cstr(filters.finance_book), cstr(company_fb), ""]))
+ | (afb.finance_book.isnull())
+ )
+ else:
+ query = query.where(
+ (afb.finance_book.isin([cstr(filters.finance_book), ""])) | (afb.finance_book.isnull())
+ )
+
+ assets_linked_to_fb = list(chain(*query.run(as_list=1)))
+
+ return assets_linked_to_fb
+
+
+def get_depreciation_amount_of_asset(asset, depreciation_amount_map):
+ return depreciation_amount_map.get(asset.asset_id) or 0.0
+
+
+def get_asset_depreciation_amount_map(filters, finance_book):
date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date
- (_, _, depreciation_expense_account) = get_depreciation_accounts(asset)
-
+ asset = frappe.qb.DocType("Asset")
gle = frappe.qb.DocType("GL Entry")
+ aca = frappe.qb.DocType("Asset Category Account")
+ company = frappe.qb.DocType("Company")
- result = (
+ query = (
frappe.qb.from_(gle)
- .select(Sum(gle.debit))
- .where(gle.against_voucher == asset.asset_id)
- .where(gle.account == depreciation_expense_account)
+ .join(asset)
+ .on(gle.against_voucher == asset.name)
+ .join(aca)
+ .on((aca.parent == asset.asset_category) & (aca.company_name == asset.company))
+ .join(company)
+ .on(company.name == asset.company)
+ .select(asset.name.as_("asset"), Sum(gle.debit).as_("depreciation_amount"))
+ .where(
+ gle.account == IfNull(aca.depreciation_expense_account, company.depreciation_expense_account)
+ )
.where(gle.debit != 0)
.where(gle.is_cancelled == 0)
- .where(gle.posting_date <= date)
- ).run()
+ .where(asset.docstatus == 1)
+ .groupby(asset.name)
+ )
- if result and result[0] and result[0][0]:
- depr_amount = result[0][0]
+ if finance_book:
+ query = query.where(
+ (gle.finance_book.isin([cstr(finance_book), ""])) | (gle.finance_book.isnull())
+ )
else:
- depr_amount = 0
+ query = query.where((gle.finance_book.isin([""])) | (gle.finance_book.isnull()))
- return depr_amount
+ if filters.filter_based_on in ("Date Range", "Fiscal Year"):
+ query = query.where(gle.posting_date <= date)
+
+ asset_depr_amount_map = query.run()
+
+ return dict(asset_depr_amount_map)
def get_purchase_receipt_supplier_map():
diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json
index 1bf7f58..f009789 100644
--- a/erpnext/buying/doctype/supplier/supplier.json
+++ b/erpnext/buying/doctype/supplier/supplier.json
@@ -457,7 +457,7 @@
"link_fieldname": "party"
}
],
- "modified": "2023-02-18 11:05:50.592270",
+ "modified": "2023-05-09 15:34:13.408932",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",
diff --git a/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py b/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py
index a8b76db..1967df2 100644
--- a/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py
+++ b/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py
@@ -99,7 +99,6 @@
return mod
-@frappe.whitelist()
def make_supplier_scorecard(source_name, target_doc=None):
def update_criteria_fields(obj, target, source_parent):
target.max_score, target.formula = frappe.db.get_value(
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index 818c789..9546680 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -320,7 +320,9 @@
return data[0]
-def make_return_doc(doctype: str, source_name: str, target_doc=None):
+def make_return_doc(
+ doctype: str, source_name: str, target_doc=None, return_against_rejected_qty=False
+):
from frappe.model.mapper import get_mapped_doc
company = frappe.db.get_value("Delivery Note", source_name, "company")
@@ -471,7 +473,7 @@
target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get("qty") or 0))
- if hasattr(target_doc, "stock_qty"):
+ if hasattr(target_doc, "stock_qty") and not return_against_rejected_qty:
target_doc.stock_qty = -1 * flt(
source_doc.stock_qty - (returned_qty_map.get("stock_qty") or 0)
)
@@ -490,6 +492,13 @@
target_doc.rejected_warehouse = source_doc.rejected_warehouse
target_doc.purchase_receipt_item = source_doc.name
+ if doctype == "Purchase Receipt" and return_against_rejected_qty:
+ target_doc.qty = -1 * flt(source_doc.rejected_qty - (returned_qty_map.get("qty") or 0))
+ target_doc.rejected_qty = 0.0
+ target_doc.rejected_warehouse = ""
+ target_doc.warehouse = source_doc.rejected_warehouse
+ target_doc.received_qty = target_doc.qty
+
elif doctype == "Purchase Invoice":
returned_qty_map = get_returned_qty_map_for_row(
source_parent.name, source_parent.supplier, source_doc.name, doctype
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index cdbf6c7..5137e03 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -845,6 +845,149 @@
gl_entries.append(self.get_gl_dict(gl_entry, item=item))
+@frappe.whitelist()
+def show_accounting_ledger_preview(company, doctype, docname):
+ filters = {"company": company, "include_dimensions": 1}
+ doc = frappe.get_doc(doctype, docname)
+
+ gl_columns, gl_data = get_accounting_ledger_preview(doc, filters)
+
+ frappe.db.rollback()
+
+ return {"gl_columns": gl_columns, "gl_data": gl_data}
+
+
+@frappe.whitelist()
+def show_stock_ledger_preview(company, doctype, docname):
+ filters = {"company": company}
+ doc = frappe.get_doc(doctype, docname)
+
+ sl_columns, sl_data = get_stock_ledger_preview(doc, filters)
+
+ frappe.db.rollback()
+
+ return {
+ "sl_columns": sl_columns,
+ "sl_data": sl_data,
+ }
+
+
+def get_accounting_ledger_preview(doc, filters):
+ from erpnext.accounts.report.general_ledger.general_ledger import get_columns as get_gl_columns
+
+ gl_columns, gl_data = [], []
+ fields = [
+ "posting_date",
+ "account",
+ "debit",
+ "credit",
+ "against",
+ "party",
+ "party_type",
+ "cost_center",
+ "against_voucher_type",
+ "against_voucher",
+ ]
+
+ doc.docstatus = 1
+
+ if doc.get("update_stock") or doc.doctype in ("Purchase Receipt", "Delivery Note"):
+ doc.update_stock_ledger()
+
+ doc.make_gl_entries()
+ columns = get_gl_columns(filters)
+ gl_entries = get_gl_entries_for_preview(doc.doctype, doc.name, fields)
+
+ gl_columns = get_columns(columns, fields)
+ gl_data = get_data(fields, gl_entries)
+
+ return gl_columns, gl_data
+
+
+def get_stock_ledger_preview(doc, filters):
+ from erpnext.stock.report.stock_ledger.stock_ledger import get_columns as get_sl_columns
+
+ sl_columns, sl_data = [], []
+ fields = [
+ "item_code",
+ "stock_uom",
+ "actual_qty",
+ "qty_after_transaction",
+ "warehouse",
+ "incoming_rate",
+ "valuation_rate",
+ "stock_value",
+ "stock_value_difference",
+ ]
+ columns_fields = [
+ "item_code",
+ "stock_uom",
+ "in_qty",
+ "out_qty",
+ "qty_after_transaction",
+ "warehouse",
+ "incoming_rate",
+ "in_out_rate",
+ "stock_value",
+ "stock_value_difference",
+ ]
+
+ if doc.get("update_stock") or doc.doctype in ("Purchase Receipt", "Delivery Note"):
+ doc.docstatus = 1
+ doc.update_stock_ledger()
+ columns = get_sl_columns(filters)
+ sl_entries = get_sl_entries_for_preview(doc.doctype, doc.name, fields)
+
+ sl_columns = get_columns(columns, columns_fields)
+ sl_data = get_data(columns_fields, sl_entries)
+
+ return sl_columns, sl_data
+
+
+def get_sl_entries_for_preview(doctype, docname, fields):
+ sl_entries = frappe.get_all(
+ "Stock Ledger Entry", filters={"voucher_type": doctype, "voucher_no": docname}, fields=fields
+ )
+
+ for entry in sl_entries:
+ if entry.actual_qty > 0:
+ entry["in_qty"] = entry.actual_qty
+ entry["out_qty"] = 0
+ else:
+ entry["out_qty"] = abs(entry.actual_qty)
+ entry["in_qty"] = 0
+
+ entry["in_out_rate"] = entry["valuation_rate"]
+
+ return sl_entries
+
+
+def get_gl_entries_for_preview(doctype, docname, fields):
+ return frappe.get_all(
+ "GL Entry", filters={"voucher_type": doctype, "voucher_no": docname}, fields=fields
+ )
+
+
+def get_columns(raw_columns, fields):
+ return [
+ {"name": d.get("label"), "editable": False, "width": 110}
+ for d in raw_columns
+ if not d.get("hidden") and d.get("fieldname") in fields
+ ]
+
+
+def get_data(raw_columns, raw_data):
+ datatable_data = []
+ for row in raw_data:
+ data_row = []
+ for column in raw_columns:
+ data_row.append(row.get(column) or "")
+
+ datatable_data.append(data_row)
+
+ return datatable_data
+
+
def repost_required_for_queue(doc: StockController) -> bool:
"""check if stock document contains repeated item-warehouse with queue based valuation.
diff --git a/erpnext/e_commerce/variant_selector/utils.py b/erpnext/e_commerce/variant_selector/utils.py
index 1a3e737..4466c45 100644
--- a/erpnext/e_commerce/variant_selector/utils.py
+++ b/erpnext/e_commerce/variant_selector/utils.py
@@ -162,6 +162,7 @@
product_info = get_item_variant_price_dict(exact_match[0], cart_settings)
if product_info:
+ product_info["is_stock_item"] = frappe.get_cached_value("Item", exact_match[0], "is_stock_item")
product_info["allow_items_not_in_stock"] = cint(cart_settings.allow_items_not_in_stock)
else:
product_info = None
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
index e57a30a..61d2ace 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
@@ -161,7 +161,6 @@
frappe.throw(frappe.get_traceback())
-@frappe.whitelist()
def sync_transactions(bank, bank_account):
"""Sync transactions based on the last integration date as the start date, after sync is completed
add the transaction date of the oldest transaction as the last integration date."""
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js
index 45a59cf..4898691 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.js
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js
@@ -99,7 +99,7 @@
}, __('Create'));
}
- if (frm.doc.mr_items && !in_list(['Material Requested', 'Closed'], frm.doc.status)) {
+ if (frm.doc.mr_items && frm.doc.mr_items.length && !in_list(['Material Requested', 'Closed'], frm.doc.status)) {
frm.add_custom_button(__("Material Request"), ()=> {
frm.trigger("make_material_request");
}, __('Create'));
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 0800bdd..6dc1ff6 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -515,6 +515,9 @@
self.show_list_created_message("Work Order", wo_list)
self.show_list_created_message("Purchase Order", po_list)
+ if not wo_list:
+ frappe.msgprint(_("No Work Orders were created"))
+
def make_work_order_for_finished_goods(self, wo_list, default_warehouses):
items_data = self.get_production_items()
@@ -618,6 +621,9 @@
def create_work_order(self, item):
from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError
+ if item.get("qty") <= 0:
+ return
+
wo = frappe.new_doc("Work Order")
wo.update(item)
wo.planned_start_date = item.get("planned_start_date") or item.get("schedule_date")
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index 75b43ec..fcfba7f 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -76,6 +76,13 @@
"Work Order", fields=["name"], filters={"production_plan": pln.name}, as_list=1
)
+ pln.make_work_order()
+ nwork_orders = frappe.get_all(
+ "Work Order", fields=["name"], filters={"production_plan": pln.name}, as_list=1
+ )
+
+ self.assertTrue(len(work_orders), len(nwork_orders))
+
self.assertTrue(len(work_orders), len(pln.po_items))
for name in material_requests:
diff --git a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.js b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.js
index 123a82a..a3f0d00 100644
--- a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.js
+++ b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.js
@@ -30,7 +30,7 @@
"fieldname":"based_on_document",
"label": __("Based On Document"),
"fieldtype": "Select",
- "options": ["Sales Order", "Delivery Note", "Quotation"],
+ "options": ["Sales Order", "Sales Invoice", "Delivery Note", "Quotation"],
"default": "Sales Order",
"reqd": 1
},
diff --git a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py
index d3bce83..daef7f6 100644
--- a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py
+++ b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py
@@ -99,7 +99,9 @@
parent = frappe.qb.DocType(self.doctype)
child = frappe.qb.DocType(self.child_doctype)
- date_field = "posting_date" if self.doctype == "Delivery Note" else "transaction_date"
+ date_field = (
+ "posting_date" if self.doctype in ("Delivery Note", "Sales Invoice") else "transaction_date"
+ )
query = (
frappe.qb.from_(parent)
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 18bd10f..8c0fa6b 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -339,3 +339,4 @@
execute:frappe.delete_doc('DocType', 'Cash Flow Mapping Template', ignore_missing=True)
execute:frappe.delete_doc('DocType', 'Cash Flow Mapping Accounts', ignore_missing=True)
erpnext.patches.v14_0.cleanup_workspaces
+erpnext.patches.v14_0.set_report_in_process_SOA
diff --git a/erpnext/patches/v14_0/set_report_in_process_SOA.py b/erpnext/patches/v14_0/set_report_in_process_SOA.py
new file mode 100644
index 0000000..9eb5e3a
--- /dev/null
+++ b/erpnext/patches/v14_0/set_report_in_process_SOA.py
@@ -0,0 +1,10 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+# License: MIT. See LICENSE
+
+import frappe
+
+
+def execute():
+ process_soa = frappe.qb.DocType("Process Statement Of Accounts")
+ q = frappe.qb.update(process_soa).set(process_soa.report, "General Ledger")
+ q.run()
diff --git a/erpnext/public/js/controllers/stock_controller.js b/erpnext/public/js/controllers/stock_controller.js
index d346357..720423b 100644
--- a/erpnext/public/js/controllers/stock_controller.js
+++ b/erpnext/public/js/controllers/stock_controller.js
@@ -66,7 +66,7 @@
}
show_general_ledger() {
- var me = this;
+ let me = this;
if(this.frm.doc.docstatus > 0) {
cur_frm.add_custom_button(__('Accounting Ledger'), function() {
frappe.route_options = {
diff --git a/erpnext/public/js/erpnext.bundle.js b/erpnext/public/js/erpnext.bundle.js
index cc020fc..4e028e4 100644
--- a/erpnext/public/js/erpnext.bundle.js
+++ b/erpnext/public/js/erpnext.bundle.js
@@ -17,6 +17,7 @@
import "./utils/supplier_quick_entry";
import "./call_popup/call_popup";
import "./utils/dimension_tree_filter";
+import "./utils/ledger_preview.js"
import "./utils/barcode_scanner";
import "./telephony";
import "./templates/call_link.html";
diff --git a/erpnext/public/js/utils/ledger_preview.js b/erpnext/public/js/utils/ledger_preview.js
new file mode 100644
index 0000000..85d4a7d
--- /dev/null
+++ b/erpnext/public/js/utils/ledger_preview.js
@@ -0,0 +1,78 @@
+frappe.provide('erpnext.accounts');
+
+erpnext.accounts.ledger_preview = {
+ show_accounting_ledger_preview(frm) {
+ let me = this;
+ if(!frm.is_new() && frm.doc.docstatus == 0) {
+ frm.add_custom_button(__('Accounting Ledger'), function() {
+ frappe.call({
+ "type": "GET",
+ "method": "erpnext.controllers.stock_controller.show_accounting_ledger_preview",
+ "args": {
+ "company": frm.doc.company,
+ "doctype": frm.doc.doctype,
+ "docname": frm.doc.name
+ },
+ "callback": function(response) {
+ me.make_dialog("Accounting Ledger Preview", "accounting_ledger_preview_html", response.message.gl_columns, response.message.gl_data);
+ }
+ })
+ }, __("Preview"));
+ }
+ },
+
+ show_stock_ledger_preview(frm) {
+ let me = this
+ if(!frm.is_new() && frm.doc.docstatus == 0) {
+ frm.add_custom_button(__('Stock Ledger'), function() {
+ frappe.call({
+ "type": "GET",
+ "method": "erpnext.controllers.stock_controller.show_stock_ledger_preview",
+ "args": {
+ "company": frm.doc.company,
+ "doctype": frm.doc.doctype,
+ "docname": frm.doc.name
+ },
+ "callback": function(response) {
+ me.make_dialog("Stock Ledger Preview", "stock_ledger_preview_html", response.message.sl_columns, response.message.sl_data);
+ }
+ })
+ }, __("Preview"));
+ }
+ },
+
+ make_dialog(label, fieldname, columns, data) {
+ let me = this;
+ let dialog = new frappe.ui.Dialog({
+ "size": "extra-large",
+ "title": __(label),
+ "fields": [
+ {
+ "fieldtype": "HTML",
+ "fieldname": fieldname,
+ },
+ ]
+ });
+
+ setTimeout(function() {
+ me.get_datatable(columns, data, dialog.get_field(fieldname).wrapper);
+ }, 200);
+
+ dialog.show();
+ },
+
+ get_datatable(columns, data, wrapper) {
+ const datatable_options = {
+ columns: columns,
+ data: data,
+ dynamicRowHeight: true,
+ checkboxColumn: false,
+ inlineFilters: true,
+ };
+
+ new frappe.DataTable(
+ wrapper,
+ datatable_options
+ );
+ }
+}
\ No newline at end of file
diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json
index c133cd3..72a1594 100644
--- a/erpnext/selling/doctype/customer/customer.json
+++ b/erpnext/selling/doctype/customer/customer.json
@@ -568,7 +568,7 @@
"link_fieldname": "party"
}
],
- "modified": "2023-02-18 11:04:46.343527",
+ "modified": "2023-05-09 15:38:40.255193",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",
diff --git a/erpnext/setup/doctype/employee/employee.json b/erpnext/setup/doctype/employee/employee.json
index 99693d9..6cb4292 100644
--- a/erpnext/setup/doctype/employee/employee.json
+++ b/erpnext/setup/doctype/employee/employee.json
@@ -78,7 +78,9 @@
"salary_mode",
"bank_details_section",
"bank_name",
+ "column_break_heye",
"bank_ac_no",
+ "iban",
"personal_details",
"marital_status",
"family_background",
@@ -804,17 +806,26 @@
{
"fieldname": "column_break_104",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_heye",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval:doc.salary_mode == 'Bank'",
+ "fieldname": "iban",
+ "fieldtype": "Data",
+ "label": "IBAN"
}
],
"icon": "fa fa-user",
"idx": 24,
"image_field": "image",
"links": [],
- "modified": "2022-09-13 10:27:14.579197",
+ "modified": "2023-03-30 15:57:05.174592",
"modified_by": "Administrator",
"module": "Setup",
"name": "Employee",
- "name_case": "Title Case",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js
index 77545e0..a648195 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.js
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.js
@@ -200,6 +200,9 @@
}
}
+ erpnext.accounts.ledger_preview.show_accounting_ledger_preview(this.frm);
+ erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm);
+
if (doc.docstatus > 0) {
this.show_stock_ledger();
if (erpnext.is_perpetual_inventory_enabled(doc.company)) {
diff --git a/erpnext/stock/doctype/item_reorder/item_reorder.json b/erpnext/stock/doctype/item_reorder/item_reorder.json
index fb4c558..a03bd45 100644
--- a/erpnext/stock/doctype/item_reorder/item_reorder.json
+++ b/erpnext/stock/doctype/item_reorder/item_reorder.json
@@ -81,7 +81,7 @@
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
- "reqd": 1,
+ "reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
@@ -147,7 +147,7 @@
"issingle": 0,
"istable": 1,
"max_attachments": 0,
- "modified": "2016-07-28 19:15:38.270046",
+ "modified": "2023-06-21 15:13:38.270046",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Reorder",
@@ -158,4 +158,4 @@
"read_only_onload": 0,
"sort_order": "ASC",
"track_seen": 0
-}
\ No newline at end of file
+}
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
index 312c166..35aad78 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
@@ -121,6 +121,10 @@
refresh() {
var me = this;
super.refresh();
+
+ erpnext.accounts.ledger_preview.show_accounting_ledger_preview(this.frm);
+ erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm);
+
if(this.frm.doc.docstatus > 0) {
this.show_stock_ledger();
//removed for temporary
@@ -209,10 +213,43 @@
}
make_purchase_return() {
- frappe.model.open_mapped_doc({
- method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_purchase_return",
- frm: cur_frm
+ let me = this;
+
+ let has_rejected_items = cur_frm.doc.items.filter((item) => {
+ if (item.rejected_qty > 0) {
+ return true;
+ }
})
+
+ if (has_rejected_items && has_rejected_items.length > 0) {
+ frappe.prompt([
+ {
+ label: __("Return Qty from Rejected Warehouse"),
+ fieldtype: "Check",
+ fieldname: "return_for_rejected_warehouse",
+ default: 1
+ },
+ ], function(values){
+ if (values.return_for_rejected_warehouse) {
+ frappe.call({
+ method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_purchase_return_against_rejected_warehouse",
+ args: {
+ source_name: cur_frm.doc.name
+ },
+ callback: function(r) {
+ if(r.message) {
+ frappe.model.sync(r.message);
+ frappe.set_route("Form", r.message.doctype, r.message.name);
+ }
+ }
+ })
+ } else {
+ cur_frm.cscript._make_purchase_return();
+ }
+ }, __("Return Qty"), __("Make Return Entry"));
+ } else {
+ cur_frm.cscript._make_purchase_return();
+ }
}
close_purchase_receipt() {
@@ -322,6 +359,13 @@
},
});
+cur_frm.cscript._make_purchase_return = function() {
+ frappe.model.open_mapped_doc({
+ method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_purchase_return",
+ frm: cur_frm
+ });
+}
+
cur_frm.cscript['Make Stock Entry'] = function() {
frappe.model.open_mapped_doc({
method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_stock_entry",
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 387f031..0b5dc05 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -1137,6 +1137,13 @@
@frappe.whitelist()
+def make_purchase_return_against_rejected_warehouse(source_name):
+ from erpnext.controllers.sales_and_purchase_return import make_return_doc
+
+ return make_return_doc("Purchase Receipt", source_name, return_against_rejected_qty=True)
+
+
+@frappe.whitelist()
def make_purchase_return(source_name, target_doc=None):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index ddc0556..c6c84ca 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -72,6 +72,11 @@
self.assertEqual(sl_entry_cancelled[1].actual_qty, -0.5)
def test_make_purchase_invoice(self):
+ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_term
+
+ create_payment_term("_Test Payment Term 1 for Purchase Invoice")
+ create_payment_term("_Test Payment Term 2 for Purchase Invoice")
+
if not frappe.db.exists(
"Payment Terms Template", "_Test Payment Terms Template For Purchase Invoice"
):
@@ -83,12 +88,14 @@
"terms": [
{
"doctype": "Payment Terms Template Detail",
+ "payment_term": "_Test Payment Term 1 for Purchase Invoice",
"invoice_portion": 50.00,
"credit_days_based_on": "Day(s) after invoice date",
"credit_days": 00,
},
{
"doctype": "Payment Terms Template Detail",
+ "payment_term": "_Test Payment Term 2 for Purchase Invoice",
"invoice_portion": 50.00,
"credit_days_based_on": "Day(s) after invoice date",
"credit_days": 30,
@@ -1827,6 +1834,33 @@
self.assertEqual(abs(data["stock_value_difference"]), 400.00)
+ def test_return_from_rejected_warehouse(self):
+ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
+ make_purchase_return_against_rejected_warehouse,
+ )
+
+ item_code = "_Test Item Return from Rejected Warehouse"
+ create_item(item_code)
+
+ warehouse = create_warehouse("_Test Warehouse Return Qty Warehouse")
+ rejected_warehouse = create_warehouse("_Test Rejected Warehouse Return Qty Warehouse")
+
+ # Step 1: Create Purchase Receipt with valuation rate 100
+ pr = make_purchase_receipt(
+ item_code=item_code,
+ warehouse=warehouse,
+ qty=10,
+ rate=100,
+ rejected_qty=2,
+ rejected_warehouse=rejected_warehouse,
+ )
+
+ pr_return = make_purchase_return_against_rejected_warehouse(pr.name)
+ self.assertEqual(pr_return.items[0].warehouse, rejected_warehouse)
+ self.assertEqual(pr_return.items[0].qty, 2.0 * -1)
+ self.assertEqual(pr_return.items[0].rejected_qty, 0.0)
+ self.assertEqual(pr_return.items[0].rejected_warehouse, "")
+
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
index d5fc710..27066b8 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -13,6 +13,7 @@
from rq.timeouts import JobTimeoutException
import erpnext
+from erpnext.accounts.general_ledger import validate_accounting_period
from erpnext.accounts.utils import get_future_stock_vouchers, repost_gle_for_stock_vouchers
from erpnext.stock.stock_ledger import (
get_affected_transactions,
@@ -44,11 +45,49 @@
self.validate_accounts_freeze()
def validate_period_closing_voucher(self):
+ # Period Closing Voucher
year_end_date = self.get_max_year_end_date(self.company)
if year_end_date and getdate(self.posting_date) <= getdate(year_end_date):
- msg = f"Due to period closing, you cannot repost item valuation before {year_end_date}"
+ date = frappe.format(year_end_date, "Date")
+ msg = f"Due to period closing, you cannot repost item valuation before {date}"
frappe.throw(_(msg))
+ # Accounting Period
+ if self.voucher_type:
+ validate_accounting_period(
+ [
+ frappe._dict(
+ {
+ "posting_date": self.posting_date,
+ "company": self.company,
+ "voucher_type": self.voucher_type,
+ }
+ )
+ ]
+ )
+
+ # Closing Stock Balance
+ closing_stock = self.get_closing_stock_balance()
+ if closing_stock and closing_stock[0].name:
+ name = get_link_to_form("Closing Stock Balance", closing_stock[0].name)
+ to_date = frappe.format(closing_stock[0].to_date, "Date")
+ msg = f"Due to closing stock balance {name}, you cannot repost item valuation before {to_date}"
+ frappe.throw(_(msg))
+
+ def get_closing_stock_balance(self):
+ filters = {
+ "company": self.company,
+ "status": "Completed",
+ "docstatus": 1,
+ "to_date": (">=", self.posting_date),
+ }
+
+ for field in ["warehouse", "item_code"]:
+ if self.get(field):
+ filters.update({field: ("in", ["", self.get(field)])})
+
+ return frappe.get_all("Closing Stock Balance", fields=["name", "to_date"], filters=filters)
+
@staticmethod
def get_max_year_end_date(company):
data = frappe.get_all(
diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
index 9c4d997..1853f45 100644
--- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
@@ -392,3 +392,33 @@
pr.cancel()
self.assertTrue(pr.docstatus == 2)
self.assertTrue(frappe.db.exists("Repost Item Valuation", {"voucher_no": pr.name}))
+
+ def test_repost_item_valuation_for_closing_stock_balance(self):
+ from erpnext.stock.doctype.closing_stock_balance.closing_stock_balance import (
+ prepare_closing_stock_balance,
+ )
+
+ doc = frappe.new_doc("Closing Stock Balance")
+ doc.company = "_Test Company"
+ doc.from_date = today()
+ doc.to_date = today()
+ doc.submit()
+
+ prepare_closing_stock_balance(doc.name)
+ doc.load_from_db()
+ self.assertEqual(doc.docstatus, 1)
+ self.assertEqual(doc.status, "Completed")
+
+ riv = frappe.new_doc("Repost Item Valuation")
+ riv.update(
+ {
+ "item_code": "_Test Item",
+ "warehouse": "_Test Warehouse - _TC",
+ "based_on": "Item and Warehouse",
+ "posting_date": today(),
+ "posting_time": "00:01:00",
+ }
+ )
+
+ self.assertRaises(frappe.ValidationError, riv.save)
+ doc.cancel()
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json
index fe42b1f..564c380 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.json
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.json
@@ -125,7 +125,8 @@
"oldfieldname": "purpose",
"oldfieldtype": "Select",
"options": "Material Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor",
- "read_only": 1
+ "read_only": 1,
+ "search_index": 1
},
{
"fieldname": "company",
@@ -678,7 +679,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2023-06-16 14:59:10.917235",
+ "modified": "2023-06-19 18:23:40.748114",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry",
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 64650bc..4f85ac0 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -191,7 +191,6 @@
return args
-@frappe.whitelist()
def get_item_code(barcode=None, serial_no=None):
if barcode:
item_code = frappe.db.get_value("Item Barcode", {"barcode": barcode}, fieldname=["parent"])
diff --git a/erpnext/stock/reorder_item.py b/erpnext/stock/reorder_item.py
index 136c78f..9075608 100644
--- a/erpnext/stock/reorder_item.py
+++ b/erpnext/stock/reorder_item.py
@@ -67,7 +67,7 @@
else:
projected_qty = flt(item_warehouse_projected_qty.get(item_code, {}).get(warehouse))
- if (reorder_level or reorder_qty) and projected_qty < reorder_level:
+ if (reorder_level or reorder_qty) and projected_qty <= reorder_level:
deficiency = reorder_level - projected_qty
if deficiency > reorder_qty:
reorder_qty = deficiency
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index 402f998..0244406 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -475,7 +475,7 @@
for row_idx, row in enumerate(result):
for convertible_col, data in convertible_column_map.items():
- conversion_factor = conversion_factors[row.get("item_code")] or 1
+ conversion_factor = conversion_factors.get(row.get("item_code")) or 1.0
for_type = data.for_type
value_before_conversion = row.get(convertible_col)
if for_type == "rate":
diff --git a/erpnext/templates/generators/item/item_configure.js b/erpnext/templates/generators/item/item_configure.js
index 613c967..9beba3f 100644
--- a/erpnext/templates/generators/item/item_configure.js
+++ b/erpnext/templates/generators/item/item_configure.js
@@ -219,7 +219,8 @@
: ''
}
- ${available_qty === 0 ? '<span class="text-danger">(' + __('Out of Stock') + ')</span>' : ''}
+ ${available_qty === 0 && product_info && product_info?.is_stock_item
+ ? '<span class="text-danger">(' + __('Out of Stock') + ')</span>' : ''}
</div></div>
<a href data-action="btn_clear_values" data-item-code="${one_item}">
@@ -236,7 +237,8 @@
</div>`;
/* eslint-disable indent */
- if (!product_info?.allow_items_not_in_stock && available_qty === 0) {
+ if (!product_info?.allow_items_not_in_stock && available_qty === 0
+ && product_info && product_info?.is_stock_item) {
item_add_to_cart = '';
}
diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv
index 68358c6..f9ec689 100644
--- a/erpnext/translations/de.csv
+++ b/erpnext/translations/de.csv
@@ -9905,3 +9905,7 @@
Select an item from each set to be used in the Sales Order.,"Wählen Sie aus den Alternativen jeweils einen Artikel aus, der in die Auftragsbestätigung übernommen werden soll.",
Is Alternative,Ist Alternative,
Alternative Items,Alternativpositionen,
+Add Template,Vorlage einfügen,
+Prepend the template to the email message,Vorlage oberhalb der Email-Nachricht einfügen,
+Clear & Add Template,Leeren und Vorlage einfügen,
+Clear the email message and add the template,Email-Feld leeren und Vorlage einfügen,
diff --git a/pyproject.toml b/pyproject.toml
index c119ada..012ffb1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -12,6 +12,7 @@
"pycountry~=22.3.5",
"Unidecode~=1.3.6",
"barcodenumber~=0.5.0",
+ "rapidfuzz~=2.15.0",
# integration dependencies
"gocardless-pro~=1.22.0",