Merge pull request #33684 from rohitwaghchaure/fixed-work-order-summary
feat: [minor] date type based on filter in Work Order Summary report
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
index 28e79b5..c083189 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
@@ -21,13 +21,22 @@
frm.trigger('bank_account');
},
+ filter_by_reference_date: function (frm) {
+ if (frm.doc.filter_by_reference_date) {
+ frm.set_value("bank_statement_from_date", "");
+ frm.set_value("bank_statement_to_date", "");
+ } else {
+ frm.set_value("from_reference_date", "");
+ frm.set_value("to_reference_date", "");
+ }
+ },
+
refresh: function (frm) {
frappe.require("bank-reconciliation-tool.bundle.js", () =>
frm.trigger("make_reconciliation_tool")
);
- frm.upload_statement_button = frm.page.set_secondary_action(
- __("Upload Bank Statement"),
- () =>
+
+ frm.add_custom_button(__("Upload Bank Statement"), () =>
frappe.call({
method:
"erpnext.accounts.doctype.bank_statement_import.bank_statement_import.upload_bank_statement",
@@ -49,6 +58,20 @@
},
})
);
+
+ frm.add_custom_button(__('Auto Reconcile'), function() {
+ frappe.call({
+ method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.auto_reconcile_vouchers",
+ args: {
+ bank_account: frm.doc.bank_account,
+ from_date: frm.doc.bank_statement_from_date,
+ to_date: frm.doc.bank_statement_to_date,
+ filter_by_reference_date: frm.doc.filter_by_reference_date,
+ from_reference_date: frm.doc.from_reference_date,
+ to_reference_date: frm.doc.to_reference_date,
+ },
+ })
+ });
},
after_save: function (frm) {
@@ -160,6 +183,9 @@
).$wrapper,
bank_statement_from_date: frm.doc.bank_statement_from_date,
bank_statement_to_date: frm.doc.bank_statement_to_date,
+ filter_by_reference_date: frm.doc.filter_by_reference_date,
+ from_reference_date: frm.doc.from_reference_date,
+ to_reference_date: frm.doc.to_reference_date,
bank_statement_closing_balance:
frm.doc.bank_statement_closing_balance,
cards_manager: frm.cards_manager,
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.json b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.json
index f666101..80993d6 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.json
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.json
@@ -10,6 +10,9 @@
"column_break_1",
"bank_statement_from_date",
"bank_statement_to_date",
+ "from_reference_date",
+ "to_reference_date",
+ "filter_by_reference_date",
"column_break_2",
"account_opening_balance",
"bank_statement_closing_balance",
@@ -36,13 +39,13 @@
"fieldtype": "Column Break"
},
{
- "depends_on": "eval: doc.bank_account",
+ "depends_on": "eval: doc.bank_account && !doc.filter_by_reference_date",
"fieldname": "bank_statement_from_date",
"fieldtype": "Date",
"label": "From Date"
},
{
- "depends_on": "eval: doc.bank_statement_from_date",
+ "depends_on": "eval: doc.bank_account && !doc.filter_by_reference_date",
"fieldname": "bank_statement_to_date",
"fieldtype": "Date",
"label": "To Date"
@@ -81,14 +84,33 @@
},
{
"fieldname": "no_bank_transactions",
- "fieldtype": "HTML"
+ "fieldtype": "HTML",
+ "options": "<div class=\"text-muted text-center\">No Matching Bank Transactions Found</div>"
+ },
+ {
+ "depends_on": "eval:doc.filter_by_reference_date",
+ "fieldname": "from_reference_date",
+ "fieldtype": "Date",
+ "label": "From Reference Date"
+ },
+ {
+ "depends_on": "eval:doc.filter_by_reference_date",
+ "fieldname": "to_reference_date",
+ "fieldtype": "Date",
+ "label": "To Reference Date"
+ },
+ {
+ "default": "0",
+ "fieldname": "filter_by_reference_date",
+ "fieldtype": "Check",
+ "label": "Filter by Reference Date"
}
],
"hide_toolbar": 1,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-04-21 11:13:49.831769",
+ "modified": "2023-01-13 13:00:02.022919",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Reconciliation Tool",
@@ -107,5 +129,6 @@
],
"quick_entry": 1,
"sort_field": "modified",
- "sort_order": "DESC"
-}
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
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 f5f04ae..4ba6146 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
@@ -8,7 +8,7 @@
from frappe import _
from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn
-from frappe.utils import flt
+from frappe.utils import cint, flt
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_paid_amount
from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import (
@@ -50,6 +50,7 @@
"party",
],
filters=filters,
+ order_by="date",
)
return transactions
@@ -266,6 +267,80 @@
@frappe.whitelist()
+def auto_reconcile_vouchers(
+ bank_account,
+ from_date=None,
+ to_date=None,
+ filter_by_reference_date=None,
+ from_reference_date=None,
+ to_reference_date=None,
+):
+ frappe.flags.auto_reconcile_vouchers = True
+ document_types = ["payment_entry", "journal_entry"]
+ bank_transactions = get_bank_transactions(bank_account)
+ matched_transaction = []
+ for transaction in bank_transactions:
+ linked_payments = get_linked_payments(
+ transaction.name,
+ document_types,
+ from_date,
+ to_date,
+ filter_by_reference_date,
+ from_reference_date,
+ to_reference_date,
+ )
+ vouchers = []
+ for r in linked_payments:
+ vouchers.append(
+ {
+ "payment_doctype": r[1],
+ "payment_name": r[2],
+ "amount": r[4],
+ }
+ )
+ transaction = frappe.get_doc("Bank Transaction", transaction.name)
+ account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
+ matched_trans = 0
+ for voucher in vouchers:
+ gl_entry = frappe.db.get_value(
+ "GL Entry",
+ dict(
+ account=account, voucher_type=voucher["payment_doctype"], voucher_no=voucher["payment_name"]
+ ),
+ ["credit", "debit"],
+ as_dict=1,
+ )
+ gl_amount, transaction_amount = (
+ (gl_entry.credit, transaction.deposit)
+ if gl_entry.credit > 0
+ else (gl_entry.debit, transaction.withdrawal)
+ )
+ allocated_amount = gl_amount if gl_amount >= transaction_amount else transaction_amount
+ transaction.append(
+ "payment_entries",
+ {
+ "payment_document": voucher["payment_doctype"],
+ "payment_entry": voucher["payment_name"],
+ "allocated_amount": allocated_amount,
+ },
+ )
+ matched_transaction.append(str(transaction.name))
+ transaction.save()
+ transaction.update_allocations()
+ matched_transaction_len = len(set(matched_transaction))
+ if matched_transaction_len == 0:
+ frappe.msgprint(_("No matching references found for auto reconciliation"))
+ elif matched_transaction_len == 1:
+ frappe.msgprint(_("{0} transaction is reconcilied").format(matched_transaction_len))
+ else:
+ frappe.msgprint(_("{0} transactions are reconcilied").format(matched_transaction_len))
+
+ frappe.flags.auto_reconcile_vouchers = False
+
+ return frappe.get_doc("Bank Transaction", transaction.name)
+
+
+@frappe.whitelist()
def reconcile_vouchers(bank_transaction_name, vouchers):
# updated clear date of all the vouchers based on the bank transaction
vouchers = json.loads(vouchers)
@@ -327,20 +402,58 @@
@frappe.whitelist()
-def get_linked_payments(bank_transaction_name, document_types=None):
+def get_linked_payments(
+ bank_transaction_name,
+ document_types=None,
+ from_date=None,
+ to_date=None,
+ filter_by_reference_date=None,
+ from_reference_date=None,
+ to_reference_date=None,
+):
# get all matching payments for a bank transaction
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
bank_account = frappe.db.get_values(
"Bank Account", transaction.bank_account, ["account", "company"], as_dict=True
)[0]
(account, company) = (bank_account.account, bank_account.company)
- matching = check_matching(account, company, transaction, document_types)
+ matching = check_matching(
+ account,
+ company,
+ transaction,
+ document_types,
+ from_date,
+ to_date,
+ filter_by_reference_date,
+ from_reference_date,
+ to_reference_date,
+ )
return matching
-def check_matching(bank_account, company, transaction, document_types):
+def check_matching(
+ bank_account,
+ company,
+ transaction,
+ document_types,
+ from_date,
+ to_date,
+ filter_by_reference_date,
+ from_reference_date,
+ to_reference_date,
+):
# combine all types of vouchers
- subquery = get_queries(bank_account, company, transaction, document_types)
+ subquery = get_queries(
+ bank_account,
+ company,
+ transaction,
+ document_types,
+ from_date,
+ to_date,
+ filter_by_reference_date,
+ from_reference_date,
+ to_reference_date,
+ )
filters = {
"amount": transaction.unallocated_amount,
"payment_type": "Receive" if transaction.deposit > 0 else "Pay",
@@ -361,11 +474,20 @@
filters,
)
)
-
return sorted(matching_vouchers, key=lambda x: x[0], reverse=True) if matching_vouchers else []
-def get_queries(bank_account, company, transaction, document_types):
+def get_queries(
+ bank_account,
+ company,
+ transaction,
+ document_types,
+ from_date,
+ to_date,
+ filter_by_reference_date,
+ from_reference_date,
+ to_reference_date,
+):
# get queries to get matching vouchers
amount_condition = "=" if "exact_match" in document_types else "<="
account_from_to = "paid_to" if transaction.deposit > 0 else "paid_from"
@@ -381,6 +503,11 @@
document_types,
amount_condition,
account_from_to,
+ from_date,
+ to_date,
+ filter_by_reference_date,
+ from_reference_date,
+ to_reference_date,
)
or []
)
@@ -389,15 +516,42 @@
def get_matching_queries(
- bank_account, company, transaction, document_types, amount_condition, account_from_to
+ bank_account,
+ company,
+ transaction,
+ document_types,
+ amount_condition,
+ account_from_to,
+ from_date,
+ to_date,
+ filter_by_reference_date,
+ from_reference_date,
+ to_reference_date,
):
queries = []
if "payment_entry" in document_types:
- pe_amount_matching = get_pe_matching_query(amount_condition, account_from_to, transaction)
+ pe_amount_matching = get_pe_matching_query(
+ amount_condition,
+ account_from_to,
+ transaction,
+ from_date,
+ to_date,
+ filter_by_reference_date,
+ from_reference_date,
+ to_reference_date,
+ )
queries.extend([pe_amount_matching])
if "journal_entry" in document_types:
- je_amount_matching = get_je_matching_query(amount_condition, transaction)
+ je_amount_matching = get_je_matching_query(
+ amount_condition,
+ transaction,
+ from_date,
+ to_date,
+ filter_by_reference_date,
+ from_reference_date,
+ to_reference_date,
+ )
queries.extend([je_amount_matching])
if transaction.deposit > 0 and "sales_invoice" in document_types:
@@ -504,47 +658,81 @@
return vouchers
-def get_pe_matching_query(amount_condition, account_from_to, transaction):
+def get_pe_matching_query(
+ amount_condition,
+ account_from_to,
+ transaction,
+ from_date,
+ to_date,
+ filter_by_reference_date,
+ from_reference_date,
+ to_reference_date,
+):
# get matching payment entries query
if transaction.deposit > 0:
currency_field = "paid_to_account_currency as currency"
else:
currency_field = "paid_from_account_currency as currency"
+ filter_by_date = f"AND posting_date between '{from_date}' and '{to_date}'"
+ order_by = " posting_date"
+ filter_by_reference_no = ""
+ if cint(filter_by_reference_date):
+ filter_by_date = f"AND reference_date between '{from_reference_date}' and '{to_reference_date}'"
+ order_by = " reference_date"
+ if frappe.flags.auto_reconcile_vouchers == True:
+ filter_by_reference_no = f"AND reference_no = '{transaction.reference_number}'"
return f"""
- SELECT
- (CASE WHEN reference_no=%(reference_no)s THEN 1 ELSE 0 END
- + CASE WHEN (party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
- + 1 ) AS rank,
- 'Payment Entry' as doctype,
- name,
- paid_amount,
- reference_no,
- reference_date,
- party,
- party_type,
- posting_date,
- {currency_field}
- FROM
- `tabPayment Entry`
- WHERE
- paid_amount {amount_condition} %(amount)s
- AND docstatus = 1
- AND payment_type IN (%(payment_type)s, 'Internal Transfer')
- AND ifnull(clearance_date, '') = ""
- AND {account_from_to} = %(bank_account)s
+ SELECT
+ (CASE WHEN reference_no=%(reference_no)s THEN 1 ELSE 0 END
+ + CASE WHEN (party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
+ + 1 ) AS rank,
+ 'Payment Entry' as doctype,
+ name,
+ paid_amount,
+ reference_no,
+ reference_date,
+ party,
+ party_type,
+ posting_date,
+ {currency_field}
+ FROM
+ `tabPayment Entry`
+ WHERE
+ paid_amount {amount_condition} %(amount)s
+ AND docstatus = 1
+ AND payment_type IN (%(payment_type)s, 'Internal Transfer')
+ AND ifnull(clearance_date, '') = ""
+ AND {account_from_to} = %(bank_account)s
+ {filter_by_date}
+ {filter_by_reference_no}
+ order by{order_by}
+
"""
-def get_je_matching_query(amount_condition, transaction):
+def get_je_matching_query(
+ amount_condition,
+ transaction,
+ from_date,
+ to_date,
+ filter_by_reference_date,
+ from_reference_date,
+ to_reference_date,
+):
# get matching journal entry query
-
# We have mapping at the bank level
# So one bank could have both types of bank accounts like asset and liability
# So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type
cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
-
+ filter_by_date = f"AND je.posting_date between '{from_date}' and '{to_date}'"
+ order_by = " je.posting_date"
+ filter_by_reference_no = ""
+ if cint(filter_by_reference_date):
+ filter_by_date = f"AND je.cheque_date between '{from_reference_date}' and '{to_reference_date}'"
+ order_by = " je.cheque_date"
+ if frappe.flags.auto_reconcile_vouchers == True:
+ filter_by_reference_no = f"AND je.cheque_no = '{transaction.reference_number}'"
return f"""
-
SELECT
(CASE WHEN je.cheque_no=%(reference_no)s THEN 1 ELSE 0 END
+ 1) AS rank ,
@@ -568,6 +756,9 @@
AND jea.account = %(bank_account)s
AND jea.{cr_or_dr}_in_account_currency {amount_condition} %(amount)s
AND je.docstatus = 1
+ {filter_by_date}
+ {filter_by_reference_no}
+ order by {order_by}
"""
diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
index a5d0413..f900e07 100644
--- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
@@ -5,6 +5,7 @@
import unittest
import frappe
+from frappe import utils
from frappe.tests.utils import FrappeTestCase
from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
@@ -40,7 +41,12 @@
"Bank Transaction",
dict(description="Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic"),
)
- linked_payments = get_linked_payments(bank_transaction.name, ["payment_entry", "exact_match"])
+ linked_payments = get_linked_payments(
+ bank_transaction.name,
+ ["payment_entry", "exact_match"],
+ from_date=bank_transaction.date,
+ to_date=utils.today(),
+ )
self.assertTrue(linked_payments[0][6] == "Conrad Electronic")
# This test validates a simple reconciliation leading to the clearance of the bank transaction and the payment
@@ -81,7 +87,12 @@
"Bank Transaction",
dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07"),
)
- linked_payments = get_linked_payments(bank_transaction.name, ["payment_entry", "exact_match"])
+ linked_payments = get_linked_payments(
+ bank_transaction.name,
+ ["payment_entry", "exact_match"],
+ from_date=bank_transaction.date,
+ to_date=utils.today(),
+ )
self.assertTrue(linked_payments[0][3])
# Check error if already reconciled
diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json
index 7921fcc..c62b711 100644
--- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json
+++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json
@@ -6,6 +6,7 @@
"engine": "InnoDB",
"field_order": [
"api_details_section",
+ "disabled",
"service_provider",
"api_endpoint",
"url",
@@ -77,12 +78,18 @@
"label": "Service Provider",
"options": "frankfurter.app\nexchangerate.host\nCustom",
"reqd": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "disabled",
+ "fieldtype": "Check",
+ "label": "Disabled"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2022-01-10 15:51:14.521174",
+ "modified": "2023-01-09 12:19:03.955906",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Currency Exchange Settings",
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index ff2690f..1cccbd9 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -247,7 +247,7 @@
self.set_target_exchange_rate(ref_doc)
def set_source_exchange_rate(self, ref_doc=None):
- if self.paid_from and not self.source_exchange_rate:
+ if self.paid_from:
if self.paid_from_account_currency == self.company_currency:
self.source_exchange_rate = 1
else:
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index ac033f7..13712ce 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -471,6 +471,7 @@
def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False):
self.common_filter_conditions.clear()
+ self.accounting_dimension_filter_conditions.clear()
self.ple_posting_date_filter.clear()
ple = qb.DocType("Payment Ledger Entry")
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 0da44a4..3920d4c 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
@@ -49,7 +49,6 @@
<br>
{% endif %}
- {{ _("Against") }}: {{ row.against }}
<br>{{ _("Remarks") }}: {{ row.remarks }}
{% if row.bill_no %}
<br>{{ _("Supplier Invoice No") }}: {{ row.bill_no }}
diff --git a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py
index 9f6828f..209cad4 100644
--- a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py
+++ b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py
@@ -7,7 +7,6 @@
from frappe import _, qb
from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn
-from frappe.utils.background_jobs import is_job_queued
from erpnext.accounts.utils import _delete_pl_entries, create_payment_ledger_entry
@@ -27,7 +26,7 @@
"""
if docname:
repost_doc = frappe.get_doc("Repost Payment Ledger", docname)
- if repost_doc.docstatus == 1 and repost_doc.repost_status in ["Queued", "Failed"]:
+ if repost_doc.docstatus.is_submitted() and repost_doc.repost_status in ["Queued", "Failed"]:
try:
for entry in repost_doc.repost_vouchers:
doc = frappe.get_doc(entry.voucher_type, entry.voucher_no)
@@ -102,10 +101,9 @@
job_name = "payment_ledger_repost_" + docname
- if not is_job_queued(job_name):
- frappe.enqueue(
- method="erpnext.accounts.doctype.repost_payment_ledger.repost_payment_ledger.start_payment_ledger_repost",
- docname=docname,
- is_async=True,
- job_name=job_name,
- )
+ frappe.enqueue(
+ method="erpnext.accounts.doctype.repost_payment_ledger.repost_payment_ledger.start_payment_ledger_repost",
+ docname=docname,
+ is_async=True,
+ job_name=job_name,
+ )
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.html b/erpnext/accounts/report/general_ledger/general_ledger.html
index c04f518..475be92 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.html
+++ b/erpnext/accounts/report/general_ledger/general_ledger.html
@@ -25,8 +25,8 @@
<thead>
<tr>
<th style="width: 12%">{%= __("Date") %}</th>
- <th style="width: 15%">{%= __("Ref") %}</th>
- <th style="width: 25%">{%= __("Party") %}</th>
+ <th style="width: 15%">{%= __("Reference") %}</th>
+ <th style="width: 25%">{%= __("Remarks") %}</th>
<th style="width: 15%">{%= __("Debit") %}</th>
<th style="width: 15%">{%= __("Credit") %}</th>
<th style="width: 18%">{%= __("Balance (Dr - Cr)") %}</th>
@@ -45,7 +45,6 @@
<br>
{% } %}
- {{ __("Against") }}: {%= data[i].against %}
<br>{%= __("Remarks") %}: {%= data[i].remarks %}
{% if(data[i].bill_no) { %}
<br>{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %}
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 445dcc5..a03de9e 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -439,8 +439,7 @@
# cancel advance entry
doc = frappe.get_doc(voucher_type, voucher_no)
frappe.flags.ignore_party_validation = True
- gl_map = doc.build_gl_map()
- create_payment_ledger_entry(gl_map, cancel=1, adv_adj=1)
+ _delete_pl_entries(voucher_type, voucher_no)
for entry in entries:
check_if_advance_entry_modified(entry)
@@ -452,11 +451,23 @@
else:
update_reference_in_payment_entry(entry, doc, do_not_save=True)
+ if doc.doctype == "Journal Entry":
+ try:
+ doc.validate_total_debit_and_credit()
+ except Exception as validation_exception:
+ raise frappe.ValidationError(_(f"Validation Error for {doc.name}")) from validation_exception
+
doc.save(ignore_permissions=True)
# re-submit advance entry
doc = frappe.get_doc(entry.voucher_type, entry.voucher_no)
gl_map = doc.build_gl_map()
- create_payment_ledger_entry(gl_map, cancel=0, adv_adj=1)
+ create_payment_ledger_entry(gl_map, update_outstanding="No", cancel=0, adv_adj=1)
+
+ # Only update outstanding for newly linked vouchers
+ for entry in entries:
+ update_voucher_outstanding(
+ entry.against_voucher_type, entry.against_voucher, entry.account, entry.party_type, entry.party
+ )
frappe.flags.ignore_party_validation = False
diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
index 7d3b645..62a3483 100644
--- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
+++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
@@ -8,7 +8,6 @@
# import erpnext
from frappe import _
from frappe.utils import cint, flt, get_link_to_form
-from six import string_types
import erpnext
from erpnext.assets.doctype.asset.depreciation import (
@@ -626,7 +625,7 @@
@frappe.whitelist()
def get_consumed_stock_item_details(args):
- if isinstance(args, string_types):
+ if isinstance(args, str):
args = json.loads(args)
args = frappe._dict(args)
@@ -678,7 +677,7 @@
@frappe.whitelist()
def get_warehouse_details(args):
- if isinstance(args, string_types):
+ if isinstance(args, str):
args = json.loads(args)
args = frappe._dict(args)
@@ -694,7 +693,7 @@
@frappe.whitelist()
def get_consumed_asset_details(args):
- if isinstance(args, string_types):
+ if isinstance(args, str):
args = json.loads(args)
args = frappe._dict(args)
@@ -746,7 +745,7 @@
@frappe.whitelist()
def get_service_item_details(args):
- if isinstance(args, string_types):
+ if isinstance(args, str):
args = json.loads(args)
args = frappe._dict(args)
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 bb50df0..faffd11 100644
--- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
+++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
@@ -86,6 +86,7 @@
"status",
"department",
"cost_center",
+ "calculate_depreciation",
"purchase_receipt",
"asset_category",
"purchase_date",
@@ -98,11 +99,7 @@
assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields)
for asset in assets_record:
- asset_value = (
- asset.gross_purchase_amount
- - flt(asset.opening_accumulated_depreciation)
- - flt(depreciation_amount_map.get(asset.name))
- )
+ asset_value = get_asset_value(asset, filters.finance_book)
row = {
"asset_id": asset.asset_id,
"asset_name": asset.asset_name,
@@ -125,6 +122,21 @@
return data
+def get_asset_value(asset, finance_book=None):
+ if not asset.calculate_depreciation:
+ return flt(asset.gross_purchase_amount) - flt(asset.opening_accumulated_depreciation)
+
+ finance_book_filter = ["finance_book", "is", "not set"]
+ if finance_book:
+ finance_book_filter = ["finance_book", "=", finance_book]
+
+ return frappe.db.get_value(
+ doctype="Asset Finance Book",
+ filters=[["parent", "=", asset.asset_id], finance_book_filter],
+ fieldname="value_after_depreciation",
+ )
+
+
def prepare_chart_data(data, filters):
labels_values_map = {}
date_field = frappe.scrub(filters.date_based_on)
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index 5a4168a..2415aec 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -219,20 +219,16 @@
else:
if not frappe.get_value("Item", item.fg_item, "is_sub_contracted_item"):
frappe.throw(
- _(
- "Row #{0}: Finished Good Item {1} must be a sub-contracted item for service item {2}"
- ).format(item.idx, item.fg_item, item.item_code)
+ _("Row #{0}: Finished Good Item {1} must be a sub-contracted item").format(
+ item.idx, item.fg_item
+ )
)
elif not frappe.get_value("Item", item.fg_item, "default_bom"):
frappe.throw(
_("Row #{0}: Default BOM not found for FG Item {1}").format(item.idx, item.fg_item)
)
if not item.fg_item_qty:
- frappe.throw(
- _("Row #{0}: Finished Good Item Qty is not specified for service item {0}").format(
- item.idx, item.item_code
- )
- )
+ frappe.throw(_("Row #{0}: Finished Good Item Qty can not be zero").format(item.idx))
else:
for item in self.items:
item.set("fg_item", None)
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index 291d756..572d9d3 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -743,9 +743,9 @@
pe = get_payment_entry("Purchase Order", po_doc.name)
pe.mode_of_payment = "Cash"
pe.paid_from = "Cash - _TC"
- pe.source_exchange_rate = 80
- pe.target_exchange_rate = 1
- pe.paid_amount = po_doc.grand_total
+ pe.source_exchange_rate = 1
+ pe.target_exchange_rate = 80
+ pe.paid_amount = po_doc.base_grand_total
pe.save(ignore_permissions=True)
pe.submit()
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
index dbc3644..8e9ded9 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
@@ -216,6 +216,7 @@
recipients=data.email_id,
sender=sender,
attachments=attachments,
+ print_format=self.meta.default_print_format or "Standard",
send_email=True,
doctype=self.doctype,
name=self.name,
@@ -224,9 +225,7 @@
frappe.msgprint(_("Email Sent to Supplier {0}").format(data.supplier))
def get_attachments(self):
- attachments = [d.name for d in get_attachments(self.doctype, self.name)]
- attachments.append(frappe.attach_print(self.doctype, self.name, doc=self))
- return attachments
+ return [d.name for d in get_attachments(self.doctype, self.name)]
def update_rfq_supplier_status(self, sup_name=None):
for supplier in self.suppliers:
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 788dc49..6fa44c9 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -394,7 +394,7 @@
self.get("inter_company_reference")
or self.get("inter_company_invoice_reference")
or self.get("inter_company_order_reference")
- ):
+ ) and not self.get("is_return"):
msg = _("Internal Sale or Delivery Reference missing.")
msg += _("Please create purchase from internal sale or delivery document itself")
frappe.throw(msg, title=_("Internal Sales Reference Missing"))
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index 15c82af..8bd0998 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -37,7 +37,7 @@
if (
ref_doc.company == doc.company
and ref_doc.get(party_type) == doc.get(party_type)
- and ref_doc.docstatus == 1
+ and ref_doc.docstatus.is_submitted()
):
# validate posting date time
return_posting_datetime = "%s %s" % (doc.posting_date, doc.get("posting_time") or "00:00:00")
diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py
index 335d92f..a9561fe 100644
--- a/erpnext/controllers/subcontracting_controller.py
+++ b/erpnext/controllers/subcontracting_controller.py
@@ -74,24 +74,25 @@
)
if not is_stock_item:
- msg = f"Item {item.item_name} must be a stock item."
- frappe.throw(_(msg))
+ frappe.throw(_("Row {0}: Item {1} must be a stock item.").format(item.idx, item.item_name))
if not is_sub_contracted_item:
- msg = f"Item {item.item_name} must be a subcontracted item."
- frappe.throw(_(msg))
+ frappe.throw(
+ _("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name)
+ )
if item.bom:
bom = frappe.get_doc("BOM", item.bom)
if not bom.is_active:
- msg = f"Please select an active BOM for Item {item.item_name}."
- frappe.throw(_(msg))
+ frappe.throw(
+ _("Row {0}: Please select an active BOM for Item {1}.").format(item.idx, item.item_name)
+ )
if bom.item != item.item_code:
- msg = f"Please select an valid BOM for Item {item.item_name}."
- frappe.throw(_(msg))
+ frappe.throw(
+ _("Row {0}: Please select an valid BOM for Item {1}.").format(item.idx, item.item_name)
+ )
else:
- msg = f"Please select a BOM for Item {item.item_name}."
- frappe.throw(_(msg))
+ frappe.throw(_("Row {0}: Please select a BOM for Item {1}.").format(item.idx, item.item_name))
def __get_data_before_save(self):
item_dict = {}
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index c6a634b..8c403aa 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -6,6 +6,7 @@
import frappe
from frappe import _, scrub
+from frappe.model.document import Document
from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction
import erpnext
@@ -20,7 +21,7 @@
class calculate_taxes_and_totals(object):
- def __init__(self, doc):
+ def __init__(self, doc: Document):
self.doc = doc
frappe.flags.round_off_applicable_accounts = []
get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts)
@@ -677,7 +678,7 @@
)
def calculate_total_advance(self):
- if self.doc.docstatus < 2:
+ if not self.doc.docstatus.is_cancelled():
total_allocated_amount = sum(
flt(adv.allocated_amount, adv.precision("allocated_amount"))
for adv in self.doc.get("advances")
@@ -708,7 +709,7 @@
)
)
- if self.doc.docstatus == 0:
+ if self.doc.docstatus.is_draft():
if self.doc.get("write_off_outstanding_amount_automatically"):
self.doc.write_off_amount = 0
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 7495ab8..cba1ecc 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -269,6 +269,7 @@
erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair
erpnext.patches.v15_0.delete_taxjar_doctypes
erpnext.patches.v15_0.create_asset_depreciation_schedules_from_assets
+erpnext.patches.v14_0.update_reference_due_date_in_journal_entry
[post_model_sync]
execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings')
diff --git a/erpnext/patches/v14_0/update_reference_due_date_in_journal_entry.py b/erpnext/patches/v14_0/update_reference_due_date_in_journal_entry.py
new file mode 100644
index 0000000..7000312
--- /dev/null
+++ b/erpnext/patches/v14_0/update_reference_due_date_in_journal_entry.py
@@ -0,0 +1,12 @@
+import frappe
+
+
+def execute():
+ if frappe.db.get_value("Journal Entry Account", {"reference_due_date": ""}):
+ frappe.db.sql(
+ """
+ UPDATE `tabJournal Entry Account`
+ SET reference_due_date = NULL
+ WHERE reference_due_date = ''
+ """
+ )
diff --git a/erpnext/public/js/bank_reconciliation_tool/data_table_manager.js b/erpnext/public/js/bank_reconciliation_tool/data_table_manager.js
index 9ef8ce6..f7c19a1 100644
--- a/erpnext/public/js/bank_reconciliation_tool/data_table_manager.js
+++ b/erpnext/public/js/bank_reconciliation_tool/data_table_manager.js
@@ -5,7 +5,12 @@
Object.assign(this, opts);
this.dialog_manager = new erpnext.accounts.bank_reconciliation.DialogManager(
this.company,
- this.bank_account
+ this.bank_account,
+ this.bank_statement_from_date,
+ this.bank_statement_to_date,
+ this.filter_by_reference_date,
+ this.from_reference_date,
+ this.to_reference_date
);
this.make_dt();
}
@@ -17,6 +22,8 @@
"erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_bank_transactions",
args: {
bank_account: this.bank_account,
+ from_date: this.bank_statement_from_date,
+ to_date: this.bank_statement_to_date
},
callback: function (response) {
me.format_data(response.message);
diff --git a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
index b5e6ab8..51664f8 100644
--- a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
+++ b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
@@ -5,8 +5,12 @@
this.bank_account = bank_account;
this.company = company;
this.make_dialog();
+ this.bank_statement_from_date = bank_statement_from_date;
+ this.bank_statement_to_date = bank_statement_to_date;
+ this.filter_by_reference_date = filter_by_reference_date;
+ this.from_reference_date = from_reference_date;
+ this.to_reference_date = to_reference_date;
}
-
show_dialog(bank_transaction_name, update_dt_cards) {
this.bank_transaction_name = bank_transaction_name;
this.update_dt_cards = update_dt_cards;
@@ -35,13 +39,13 @@
if (r.message) {
this.bank_transaction = r.message;
r.message.payment_entry = 1;
+ r.message.journal_entry = 1;
this.dialog.set_values(r.message);
this.dialog.show();
}
},
});
}
-
get_linked_vouchers(document_types) {
frappe.call({
method:
@@ -49,6 +53,11 @@
args: {
bank_transaction_name: this.bank_transaction_name,
document_types: document_types,
+ from_date: this.bank_statement_from_date,
+ to_date: this.bank_statement_to_date,
+ filter_by_reference_date: this.filter_by_reference_date,
+ from_reference_date:this.from_reference_date,
+ to_reference_date:this.to_reference_date
},
callback: (result) => {
@@ -66,6 +75,7 @@
row[1],
row[2],
reference_date,
+ row[8],
format_currency(row[3], row[9]),
row[6],
row[4],
@@ -102,6 +112,11 @@
width: 120,
},
{
+ name: "Posting Date",
+ editable: false,
+ width: 120,
+ },
+ {
name: __("Amount"),
editable: false,
width: 100,
@@ -578,4 +593,4 @@
}
}
-};
+};
\ No newline at end of file
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index f2f1ce1..5c1c6d1 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -1691,7 +1691,7 @@
var valid = true;
$.each(["company", "customer"], function(i, fieldname) {
- if(frappe.meta.has_field(me.frm.doc.doctype, fieldname) && me.frm.doc.doctype != "Purchase Order") {
+ if(frappe.meta.has_field(me.frm.doc.doctype, fieldname) && !["Purchase Order","Purchase Invoice"].includes(me.frm.doc.doctype)) {
if (!me.frm.doc[fieldname]) {
frappe.msgprint(__("Please specify") + ": " +
frappe.meta.get_label(me.frm.doc.doctype, fieldname, me.frm.doc.name) +
diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py
index 484b8c9..6836d56 100644
--- a/erpnext/selling/doctype/quotation/quotation.py
+++ b/erpnext/selling/doctype/quotation/quotation.py
@@ -194,14 +194,7 @@
@frappe.whitelist()
-def make_sales_order(source_name, target_doc=None):
- quotation = frappe.db.get_value(
- "Quotation", source_name, ["transaction_date", "valid_till"], as_dict=1
- )
- if quotation.valid_till and (
- quotation.valid_till < quotation.transaction_date or quotation.valid_till < getdate(nowdate())
- ):
- frappe.throw(_("Validity period of this quotation has ended."))
+def make_sales_order(source_name: str, target_doc=None):
return _make_sales_order(source_name, target_doc)
diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py
index b151dd5..5aaba4f 100644
--- a/erpnext/selling/doctype/quotation/test_quotation.py
+++ b/erpnext/selling/doctype/quotation/test_quotation.py
@@ -136,17 +136,20 @@
sales_order.payment_schedule[1].due_date, getdate(add_days(quotation.transaction_date, 30))
)
- def test_valid_till(self):
- from erpnext.selling.doctype.quotation.quotation import make_sales_order
-
+ def test_valid_till_before_transaction_date(self):
quotation = frappe.copy_doc(test_records[0])
quotation.valid_till = add_days(quotation.transaction_date, -1)
self.assertRaises(frappe.ValidationError, quotation.validate)
+ def test_so_from_expired_quotation(self):
+ from erpnext.selling.doctype.quotation.quotation import make_sales_order
+
+ quotation = frappe.copy_doc(test_records[0])
quotation.valid_till = add_days(nowdate(), -1)
quotation.insert()
quotation.submit()
- self.assertRaises(frappe.ValidationError, make_sales_order, quotation.name)
+
+ make_sales_order(quotation.name)
def test_shopping_cart_without_website_item(self):
if frappe.db.exists("Website Item", {"item_code": "_Test Item Home Desktop 100"}):
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 7c0601e..accf5f2 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -208,7 +208,7 @@
for quotation in set(d.prevdoc_docname for d in self.get("items")):
if quotation:
doc = frappe.get_doc("Quotation", quotation)
- if doc.docstatus == 2:
+ if doc.docstatus.is_cancelled():
frappe.throw(_("Quotation {0} is cancelled").format(quotation))
doc.set_status(update=True)
diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py
index 54bd8c3..bab57fe 100644
--- a/erpnext/setup/utils.py
+++ b/erpnext/setup/utils.py
@@ -81,6 +81,11 @@
if entries:
return flt(entries[0].exchange_rate)
+ if frappe.get_cached_value(
+ "Currency Exchange Settings", "Currency Exchange Settings", "disabled"
+ ):
+ return 0.00
+
try:
cache = frappe.cache()
key = "currency_exchange_rate_{0}:{1}:{2}".format(transaction_date, from_currency, to_currency)
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index 7e426ae..53f6b7f 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -106,7 +106,6 @@
"conversion_factor": 1.0,
"reserved_qty": 1,
"actual_qty": 5,
- "ordered_qty": 10,
"projected_qty": 14,
}
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 65a792f..9c6f4f4 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -4,7 +4,7 @@
import json
from collections import OrderedDict, defaultdict
from itertools import groupby
-from typing import Dict, List, Set
+from typing import Dict, List
import frappe
from frappe import _
@@ -41,7 +41,9 @@
)
def before_submit(self):
- update_sales_orders = set()
+ self.validate_picked_items()
+
+ def validate_picked_items(self):
for item in self.locations:
if self.scan_mode and item.picked_qty < item.stock_qty:
frappe.throw(
@@ -50,17 +52,14 @@
).format(item.idx, item.stock_qty - item.picked_qty, item.stock_uom),
title=_("Pick List Incomplete"),
)
- elif not self.scan_mode and item.picked_qty == 0:
+
+ if not self.scan_mode and item.picked_qty == 0:
# if the user has not entered any picked qty, set it to stock_qty, before submit
item.picked_qty = item.stock_qty
- if item.sales_order_item:
- # update the picked_qty in SO Item
- self.update_sales_order_item(item, item.picked_qty, item.item_code)
- update_sales_orders.add(item.sales_order)
-
if not frappe.get_cached_value("Item", item.item_code, "has_serial_no"):
continue
+
if not item.serial_no:
frappe.throw(
_("Row #{0}: {1} does not have any available serial numbers in {2}").format(
@@ -68,58 +67,96 @@
),
title=_("Serial Nos Required"),
)
- if len(item.serial_no.split("\n")) == item.picked_qty:
- continue
- frappe.throw(
- _(
- "For item {0} at row {1}, count of serial numbers does not match with the picked quantity"
- ).format(frappe.bold(item.item_code), frappe.bold(item.idx)),
- title=_("Quantity Mismatch"),
- )
- self.update_bundle_picked_qty()
- self.update_sales_order_picking_status(update_sales_orders)
-
- def before_cancel(self):
- """Deduct picked qty on cancelling pick list"""
- updated_sales_orders = set()
-
- for item in self.get("locations"):
- if item.sales_order_item:
- self.update_sales_order_item(item, -1 * item.picked_qty, item.item_code)
- updated_sales_orders.add(item.sales_order)
-
- self.update_bundle_picked_qty()
- self.update_sales_order_picking_status(updated_sales_orders)
-
- def update_sales_order_item(self, item, picked_qty, item_code):
- item_table = "Sales Order Item" if not item.product_bundle_item else "Packed Item"
- stock_qty_field = "stock_qty" if not item.product_bundle_item else "qty"
-
- already_picked, actual_qty = frappe.db.get_value(
- item_table,
- item.sales_order_item,
- ["picked_qty", stock_qty_field],
- for_update=True,
- )
-
- if self.docstatus == 1:
- if (((already_picked + picked_qty) / actual_qty) * 100) > (
- 100 + flt(frappe.db.get_single_value("Stock Settings", "over_delivery_receipt_allowance"))
- ):
+ if len(item.serial_no.split("\n")) != item.picked_qty:
frappe.throw(
_(
- "You are picking more than required quantity for {}. Check if there is any other pick list created for {}"
- ).format(item_code, item.sales_order)
+ "For item {0} at row {1}, count of serial numbers does not match with the picked quantity"
+ ).format(frappe.bold(item.item_code), frappe.bold(item.idx)),
+ title=_("Quantity Mismatch"),
)
- frappe.db.set_value(item_table, item.sales_order_item, "picked_qty", already_picked + picked_qty)
+ def on_submit(self):
+ self.update_bundle_picked_qty()
+ self.update_reference_qty()
+ self.update_sales_order_picking_status()
- @staticmethod
- def update_sales_order_picking_status(sales_orders: Set[str]) -> None:
+ def on_cancel(self):
+ self.update_bundle_picked_qty()
+ self.update_reference_qty()
+ self.update_sales_order_picking_status()
+
+ def update_reference_qty(self):
+ packed_items = []
+ so_items = []
+
+ for item in self.locations:
+ if item.product_bundle_item:
+ packed_items.append(item.sales_order_item)
+ elif item.sales_order_item:
+ so_items.append(item.sales_order_item)
+
+ if packed_items:
+ self.update_packed_items_qty(packed_items)
+
+ if so_items:
+ self.update_sales_order_item_qty(so_items)
+
+ def update_packed_items_qty(self, packed_items):
+ picked_items = get_picked_items_qty(packed_items)
+ self.validate_picked_qty(picked_items)
+
+ picked_qty = frappe._dict()
+ for d in picked_items:
+ picked_qty[d.sales_order_item] = d.picked_qty
+
+ for packed_item in packed_items:
+ frappe.db.set_value(
+ "Packed Item",
+ packed_item,
+ "picked_qty",
+ flt(picked_qty.get(packed_item)),
+ update_modified=False,
+ )
+
+ def update_sales_order_item_qty(self, so_items):
+ picked_items = get_picked_items_qty(so_items)
+ self.validate_picked_qty(picked_items)
+
+ picked_qty = frappe._dict()
+ for d in picked_items:
+ picked_qty[d.sales_order_item] = d.picked_qty
+
+ for so_item in so_items:
+ frappe.db.set_value(
+ "Sales Order Item",
+ so_item,
+ "picked_qty",
+ flt(picked_qty.get(so_item)),
+ update_modified=False,
+ )
+
+ def update_sales_order_picking_status(self) -> None:
+ sales_orders = []
+ for row in self.locations:
+ if row.sales_order and row.sales_order not in sales_orders:
+ sales_orders.append(row.sales_order)
+
for sales_order in sales_orders:
- if sales_order:
- frappe.get_doc("Sales Order", sales_order, for_update=True).update_picking_status()
+ frappe.get_doc("Sales Order", sales_order, for_update=True).update_picking_status()
+
+ def validate_picked_qty(self, data):
+ over_delivery_receipt_allowance = 100 + flt(
+ frappe.db.get_single_value("Stock Settings", "over_delivery_receipt_allowance")
+ )
+
+ for row in data:
+ if (row.picked_qty / row.stock_qty) * 100 > over_delivery_receipt_allowance:
+ frappe.throw(
+ _(
+ f"You are picking more than required quantity for the item {row.item_code}. Check if there is any other pick list created for the sales order {row.sales_order}."
+ )
+ )
@frappe.whitelist()
def set_item_locations(self, save=False):
@@ -230,7 +267,8 @@
frappe.throw(_("Qty of Finished Goods Item should be greater than 0."))
def before_print(self, settings=None):
- self.group_similar_items()
+ if self.group_same_items:
+ self.group_similar_items()
def group_similar_items(self):
group_item_qty = defaultdict(float)
@@ -308,6 +346,31 @@
return int(flt(min(possible_bundles), precision or 6))
+def get_picked_items_qty(items) -> List[Dict]:
+ return frappe.db.sql(
+ f"""
+ SELECT
+ sales_order_item,
+ item_code,
+ sales_order,
+ SUM(stock_qty) AS stock_qty,
+ SUM(picked_qty) AS picked_qty
+ FROM
+ `tabPick List Item`
+ WHERE
+ sales_order_item IN (
+ {", ".join(frappe.db.escape(d) for d in items)}
+ )
+ AND docstatus = 1
+ GROUP BY
+ sales_order_item,
+ sales_order
+ FOR UPDATE
+ """,
+ as_dict=1,
+ )
+
+
def validate_item_locations(pick_list):
if not pick_list.locations:
frappe.throw(_("Add items in the Item Locations table"))
diff --git a/erpnext/stock/doctype/pick_list/pick_list_dashboard.py b/erpnext/stock/doctype/pick_list/pick_list_dashboard.py
index 92e57be..7fbcbaf 100644
--- a/erpnext/stock/doctype/pick_list/pick_list_dashboard.py
+++ b/erpnext/stock/doctype/pick_list/pick_list_dashboard.py
@@ -1,7 +1,10 @@
def get_data():
return {
"fieldname": "pick_list",
+ "internal_links": {
+ "Sales Order": ["locations", "sales_order"],
+ },
"transactions": [
- {"items": ["Stock Entry", "Delivery Note"]},
+ {"items": ["Stock Entry", "Sales Order", "Delivery Note"]},
],
}
diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py
index f552299..43acdf0 100644
--- a/erpnext/stock/doctype/pick_list/test_pick_list.py
+++ b/erpnext/stock/doctype/pick_list/test_pick_list.py
@@ -445,10 +445,10 @@
pl.before_print()
self.assertEqual(len(pl.locations), 4)
- # grouping should halve the number of items
+ # grouping should not happen if group_same_items is False
pl = frappe.get_doc(
doctype="Pick List",
- group_same_items=True,
+ group_same_items=False,
locations=[
_dict(item_code="A", warehouse="X", qty=5, picked_qty=1),
_dict(item_code="B", warehouse="Y", qty=4, picked_qty=2),
@@ -457,6 +457,11 @@
],
)
pl.before_print()
+ self.assertEqual(len(pl.locations), 4)
+
+ # grouping should halve the number of items
+ pl.group_same_items = True
+ pl.before_print()
self.assertEqual(len(pl.locations), 2)
expected_items = [
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index f7fcb30..363dc0a 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -1181,7 +1181,7 @@
@frappe.whitelist()
def get_bin_details(item_code, warehouse, company=None, include_child_warehouses=False):
- bin_details = {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0, "ordered_qty": 0}
+ bin_details = {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0}
if warehouse:
from frappe.query_builder.functions import Coalesce, Sum
@@ -1197,7 +1197,6 @@
Coalesce(Sum(bin.projected_qty), 0).as_("projected_qty"),
Coalesce(Sum(bin.actual_qty), 0).as_("actual_qty"),
Coalesce(Sum(bin.reserved_qty), 0).as_("reserved_qty"),
- Coalesce(Sum(bin.ordered_qty), 0).as_("ordered_qty"),
)
.where((bin.item_code == item_code) & (bin.warehouse.isin(warehouses)))
).run(as_dict=True)[0]
diff --git a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py
index 99f820e..106e877 100644
--- a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py
+++ b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py
@@ -41,7 +41,7 @@
key = (d.voucher_type, d.voucher_no)
gl_data = voucher_wise_gl_data.get(key) or {}
d.account_value = gl_data.get("account_value", 0)
- d.difference_value = abs(d.stock_value - d.account_value)
+ d.difference_value = d.stock_value - d.account_value
if abs(d.difference_value) > 0.1:
data.append(d)
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 55a11a1..5d75bfd 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -1270,20 +1270,6 @@
(item_code, warehouse, voucher_no, voucher_type),
)
- if not last_valuation_rate:
- # Get valuation rate from last sle for the item against any warehouse
- last_valuation_rate = frappe.db.sql(
- """select valuation_rate
- from `tabStock Ledger Entry` force index (item_code)
- where
- item_code = %s
- AND valuation_rate > 0
- AND is_cancelled = 0
- AND NOT(voucher_no = %s AND voucher_type = %s)
- order by posting_date desc, posting_time desc, name desc limit 1""",
- (item_code, voucher_no, voucher_type),
- )
-
if last_valuation_rate:
return flt(last_valuation_rate[0][0])
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
index bce5360..e8faa48 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
@@ -57,11 +57,17 @@
def before_validate(self):
super(SubcontractingReceipt, self).before_validate()
+ self.validate_items_qty()
self.set_items_bom()
self.set_items_cost_center()
self.set_items_expense_account()
def validate(self):
+ if (
+ frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on")
+ == "BOM"
+ ):
+ self.supplied_items = []
super(SubcontractingReceipt, self).validate()
self.set_missing_values()
self.validate_posting_time()
@@ -157,7 +163,7 @@
total_qty = total_amount = 0
for item in self.items:
- if item.name in rm_supp_cost:
+ if item.qty and item.name in rm_supp_cost:
item.rm_supp_cost = rm_supp_cost[item.name]
item.rm_cost_per_qty = item.rm_supp_cost / item.qty
rm_supp_cost.pop(item.name)
@@ -194,6 +200,13 @@
).format(item.idx)
)
+ def validate_items_qty(self):
+ for item in self.items:
+ if not (item.qty or item.rejected_qty):
+ frappe.throw(
+ _("Row {0}: Accepted Qty and Rejected Qty can't be zero at the same time.").format(item.idx)
+ )
+
def set_items_bom(self):
if self.is_return:
for item in self.items: