Merge pull request #33701 from frappe/version-14-hotfix

chore: release v14
This commit is contained in:
Deepesh Garg
2023-01-17 21:31:17 +05:30
committed by GitHub
36 changed files with 581 additions and 205 deletions

View File

@@ -66,7 +66,8 @@ ignore =
F841, F841,
E713, E713,
E712, E712,
B023 B023,
B028
max-line-length = 200 max-line-length = 200

View File

@@ -21,13 +21,22 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
frm.trigger('bank_account'); 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) { refresh: function (frm) {
frappe.require("bank-reconciliation-tool.bundle.js", () => frappe.require("bank-reconciliation-tool.bundle.js", () =>
frm.trigger("make_reconciliation_tool") 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({ frappe.call({
method: method:
"erpnext.accounts.doctype.bank_statement_import.bank_statement_import.upload_bank_statement", "erpnext.accounts.doctype.bank_statement_import.bank_statement_import.upload_bank_statement",
@@ -49,6 +58,20 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
}, },
}) })
); );
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) { after_save: function (frm) {
@@ -160,6 +183,9 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
).$wrapper, ).$wrapper,
bank_statement_from_date: frm.doc.bank_statement_from_date, bank_statement_from_date: frm.doc.bank_statement_from_date,
bank_statement_to_date: frm.doc.bank_statement_to_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: bank_statement_closing_balance:
frm.doc.bank_statement_closing_balance, frm.doc.bank_statement_closing_balance,
cards_manager: frm.cards_manager, cards_manager: frm.cards_manager,

View File

@@ -10,6 +10,9 @@
"column_break_1", "column_break_1",
"bank_statement_from_date", "bank_statement_from_date",
"bank_statement_to_date", "bank_statement_to_date",
"from_reference_date",
"to_reference_date",
"filter_by_reference_date",
"column_break_2", "column_break_2",
"account_opening_balance", "account_opening_balance",
"bank_statement_closing_balance", "bank_statement_closing_balance",
@@ -36,13 +39,13 @@
"fieldtype": "Column Break" "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", "fieldname": "bank_statement_from_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "From 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", "fieldname": "bank_statement_to_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "To Date" "label": "To Date"
@@ -81,14 +84,33 @@
}, },
{ {
"fieldname": "no_bank_transactions", "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, "hide_toolbar": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-04-21 11:13:49.831769", "modified": "2023-01-13 13:00:02.022919",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Bank Reconciliation Tool", "name": "Bank Reconciliation Tool",
@@ -107,5 +129,6 @@
], ],
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC" "sort_order": "DESC",
} "states": []
}

View File

@@ -8,7 +8,7 @@ import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn 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.doctype.bank_transaction.bank_transaction import get_paid_amount
from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import ( from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import (
@@ -50,6 +50,7 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None):
"party", "party",
], ],
filters=filters, filters=filters,
order_by="date",
) )
return transactions return transactions
@@ -261,6 +262,80 @@ def create_payment_entry_bts(
return reconcile_vouchers(bank_transaction.name, vouchers) return reconcile_vouchers(bank_transaction.name, vouchers)
@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() @frappe.whitelist()
def reconcile_vouchers(bank_transaction_name, vouchers): def reconcile_vouchers(bank_transaction_name, vouchers):
# updated clear date of all the vouchers based on the bank transaction # updated clear date of all the vouchers based on the bank transaction
@@ -323,20 +398,58 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
@frappe.whitelist() @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 # get all matching payments for a bank transaction
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name) transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
bank_account = frappe.db.get_values( bank_account = frappe.db.get_values(
"Bank Account", transaction.bank_account, ["account", "company"], as_dict=True "Bank Account", transaction.bank_account, ["account", "company"], as_dict=True
)[0] )[0]
(account, company) = (bank_account.account, bank_account.company) (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 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 # 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 = { filters = {
"amount": transaction.unallocated_amount, "amount": transaction.unallocated_amount,
"payment_type": "Receive" if transaction.deposit > 0 else "Pay", "payment_type": "Receive" if transaction.deposit > 0 else "Pay",
@@ -357,11 +470,20 @@ def check_matching(bank_account, company, transaction, document_types):
filters, filters,
) )
) )
return sorted(matching_vouchers, key=lambda x: x[0], reverse=True) if matching_vouchers else [] 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 # get queries to get matching vouchers
amount_condition = "=" if "exact_match" in document_types else "<=" amount_condition = "=" if "exact_match" in document_types else "<="
account_from_to = "paid_to" if transaction.deposit > 0 else "paid_from" account_from_to = "paid_to" if transaction.deposit > 0 else "paid_from"
@@ -377,6 +499,11 @@ def get_queries(bank_account, company, transaction, document_types):
document_types, document_types,
amount_condition, amount_condition,
account_from_to, account_from_to,
from_date,
to_date,
filter_by_reference_date,
from_reference_date,
to_reference_date,
) )
or [] or []
) )
@@ -385,15 +512,42 @@ def get_queries(bank_account, company, transaction, document_types):
def get_matching_queries( 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 = [] queries = []
if "payment_entry" in document_types: 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]) queries.extend([pe_amount_matching])
if "journal_entry" in document_types: 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]) queries.extend([je_amount_matching])
if transaction.deposit > 0 and "sales_invoice" in document_types: if transaction.deposit > 0 and "sales_invoice" in document_types:
@@ -500,47 +654,81 @@ def get_lr_matching_query(bank_account, amount_condition, filters):
return vouchers 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 # get matching payment entries query
if transaction.deposit > 0: if transaction.deposit > 0:
currency_field = "paid_to_account_currency as currency" currency_field = "paid_to_account_currency as currency"
else: else:
currency_field = "paid_from_account_currency as currency" 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""" return f"""
SELECT SELECT
(CASE WHEN reference_no=%(reference_no)s THEN 1 ELSE 0 END (CASE WHEN reference_no=%(reference_no)s THEN 1 ELSE 0 END
+ CASE WHEN (party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END + CASE WHEN (party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
+ 1 ) AS rank, + 1 ) AS rank,
'Payment Entry' as doctype, 'Payment Entry' as doctype,
name, name,
paid_amount, paid_amount,
reference_no, reference_no,
reference_date, reference_date,
party, party,
party_type, party_type,
posting_date, posting_date,
{currency_field} {currency_field}
FROM FROM
`tabPayment Entry` `tabPayment Entry`
WHERE WHERE
paid_amount {amount_condition} %(amount)s paid_amount {amount_condition} %(amount)s
AND docstatus = 1 AND docstatus = 1
AND payment_type IN (%(payment_type)s, 'Internal Transfer') AND payment_type IN (%(payment_type)s, 'Internal Transfer')
AND ifnull(clearance_date, '') = "" AND ifnull(clearance_date, '') = ""
AND {account_from_to} = %(bank_account)s 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 # get matching journal entry query
# We have mapping at the bank level # We have mapping at the bank level
# So one bank could have both types of bank accounts like asset and liability # 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 # 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" 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""" return f"""
SELECT SELECT
(CASE WHEN je.cheque_no=%(reference_no)s THEN 1 ELSE 0 END (CASE WHEN je.cheque_no=%(reference_no)s THEN 1 ELSE 0 END
+ 1) AS rank , + 1) AS rank ,
@@ -564,6 +752,9 @@ def get_je_matching_query(amount_condition, transaction):
AND jea.account = %(bank_account)s AND jea.account = %(bank_account)s
AND jea.{cr_or_dr}_in_account_currency {amount_condition} %(amount)s AND jea.{cr_or_dr}_in_account_currency {amount_condition} %(amount)s
AND je.docstatus = 1 AND je.docstatus = 1
{filter_by_date}
{filter_by_reference_no}
order by {order_by}
""" """

View File

@@ -5,6 +5,7 @@ import json
import unittest import unittest
import frappe import frappe
from frappe import utils
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase
from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import ( from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
@@ -40,7 +41,12 @@ class TestBankTransaction(FrappeTestCase):
"Bank Transaction", "Bank Transaction",
dict(description="Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic"), 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") 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 # This test validates a simple reconciliation leading to the clearance of the bank transaction and the payment
@@ -81,7 +87,12 @@ class TestBankTransaction(FrappeTestCase):
"Bank Transaction", "Bank Transaction",
dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07"), 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]) self.assertTrue(linked_payments[0][3])
# Check error if already reconciled # Check error if already reconciled

View File

@@ -137,8 +137,7 @@
"fieldname": "finance_book", "fieldname": "finance_book",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Finance Book", "label": "Finance Book",
"options": "Finance Book", "options": "Finance Book"
"read_only": 1
}, },
{ {
"fieldname": "2_add_edit_gl_entries", "fieldname": "2_add_edit_gl_entries",
@@ -539,7 +538,7 @@
"idx": 176, "idx": 176,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-11-28 17:40:01.241908", "modified": "2023-01-17 12:53:53.280620",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Journal Entry", "name": "Journal Entry",

View File

@@ -334,7 +334,7 @@ class PaymentReconciliation(Document):
) )
# Account Currency has balance # Account Currency has balance
dr_or_cr = "debit" if self.party_type == "Customer" else "debit" dr_or_cr = "debit" if self.party_type == "Customer" else "credit"
reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
journal_account = frappe._dict( journal_account = frappe._dict(
@@ -471,6 +471,7 @@ class PaymentReconciliation(Document):
def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False): def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False):
self.common_filter_conditions.clear() self.common_filter_conditions.clear()
self.accounting_dimension_filter_conditions.clear()
self.ple_posting_date_filter.clear() self.ple_posting_date_filter.clear()
ple = qb.DocType("Payment Ledger Entry") ple = qb.DocType("Payment Ledger Entry")

View File

@@ -26,7 +26,7 @@ def start_payment_ledger_repost(docname=None):
""" """
if docname: if docname:
repost_doc = frappe.get_doc("Repost Payment Ledger", 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: try:
for entry in repost_doc.repost_vouchers: for entry in repost_doc.repost_vouchers:
doc = frappe.get_doc(entry.voucher_type, entry.voucher_no) doc = frappe.get_doc(entry.voucher_type, entry.voucher_no)
@@ -101,10 +101,9 @@ def execute_repost_payment_ledger(docname):
job_name = "payment_ledger_repost_" + docname job_name = "payment_ledger_repost_" + docname
if not frappe.utils.background_jobs.is_job_queued(job_name): frappe.enqueue(
frappe.enqueue( method="erpnext.accounts.doctype.repost_payment_ledger.repost_payment_ledger.start_payment_ledger_repost",
method="erpnext.accounts.doctype.repost_payment_ledger.repost_payment_ledger.start_payment_ledger_repost", docname=docname,
docname=docname, is_async=True,
is_async=True, job_name=job_name,
job_name=job_name, )
)

View File

@@ -259,9 +259,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
if tax_deducted: if tax_deducted:
net_total = inv.tax_withholding_net_total net_total = inv.tax_withholding_net_total
if ldc: if ldc:
tax_amount = get_tds_amount_from_ldc( tax_amount = get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total)
ldc, parties, pan_no, tax_details, posting_date, net_total
)
else: else:
tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0 tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0
@@ -538,7 +536,7 @@ def get_invoice_total_without_tcs(inv, tax_details):
return inv.grand_total - tcs_tax_row_amount return inv.grand_total - tcs_tax_row_amount
def get_tds_amount_from_ldc(ldc, parties, pan_no, tax_details, posting_date, net_total): def get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total):
tds_amount = 0 tds_amount = 0
limit_consumed = frappe.db.get_value( limit_consumed = frappe.db.get_value(
"Purchase Invoice", "Purchase Invoice",

View File

@@ -4,6 +4,7 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import flt
def execute(filters=None): def execute(filters=None):
@@ -65,6 +66,12 @@ def get_result(
else: else:
total_amount_credited += entry.credit total_amount_credited += entry.credit
## Check if ldc is applied and show rate as per ldc
actual_rate = (tds_deducted / total_amount_credited) * 100
if flt(actual_rate) < flt(rate):
rate = actual_rate
if tds_deducted: if tds_deducted:
row = { row = {
"pan" "pan"

View File

@@ -439,8 +439,7 @@ def reconcile_against_document(args): # nosemgrep
# cancel advance entry # cancel advance entry
doc = frappe.get_doc(voucher_type, voucher_no) doc = frappe.get_doc(voucher_type, voucher_no)
frappe.flags.ignore_party_validation = True frappe.flags.ignore_party_validation = True
gl_map = doc.build_gl_map() _delete_pl_entries(voucher_type, voucher_no)
create_payment_ledger_entry(gl_map, cancel=1, adv_adj=1)
for entry in entries: for entry in entries:
check_if_advance_entry_modified(entry) check_if_advance_entry_modified(entry)
@@ -452,11 +451,23 @@ def reconcile_against_document(args): # nosemgrep
else: else:
update_reference_in_payment_entry(entry, doc, do_not_save=True) 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) doc.save(ignore_permissions=True)
# re-submit advance entry # re-submit advance entry
doc = frappe.get_doc(entry.voucher_type, entry.voucher_no) doc = frappe.get_doc(entry.voucher_type, entry.voucher_no)
gl_map = doc.build_gl_map() 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 frappe.flags.ignore_party_validation = False

View File

@@ -513,7 +513,7 @@
{ {
"group": "Repair", "group": "Repair",
"link_doctype": "Asset Repair", "link_doctype": "Asset Repair",
"link_fieldname": "asset_name" "link_fieldname": "asset"
}, },
{ {
"group": "Value", "group": "Value",
@@ -521,7 +521,7 @@
"link_fieldname": "asset" "link_fieldname": "asset"
} }
], ],
"modified": "2022-12-05 16:21:30.024060", "modified": "2023-01-16 23:35:37.423100",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset", "name": "Asset",

View File

@@ -86,6 +86,7 @@ def get_data(filters):
"status", "status",
"department", "department",
"cost_center", "cost_center",
"calculate_depreciation",
"purchase_receipt", "purchase_receipt",
"asset_category", "asset_category",
"purchase_date", "purchase_date",
@@ -98,11 +99,7 @@ def get_data(filters):
assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields) assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields)
for asset in assets_record: for asset in assets_record:
asset_value = ( asset_value = get_asset_value(asset, filters.finance_book)
asset.gross_purchase_amount
- flt(asset.opening_accumulated_depreciation)
- flt(depreciation_amount_map.get(asset.name))
)
row = { row = {
"asset_id": asset.asset_id, "asset_id": asset.asset_id,
"asset_name": asset.asset_name, "asset_name": asset.asset_name,
@@ -125,6 +122,21 @@ def get_data(filters):
return data 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): def prepare_chart_data(data, filters):
labels_values_map = {} labels_values_map = {}
date_field = frappe.scrub(filters.date_based_on) date_field = frappe.scrub(filters.date_based_on)

View File

@@ -219,20 +219,16 @@ class PurchaseOrder(BuyingController):
else: else:
if not frappe.get_value("Item", item.fg_item, "is_sub_contracted_item"): if not frappe.get_value("Item", item.fg_item, "is_sub_contracted_item"):
frappe.throw( frappe.throw(
_( _("Row #{0}: Finished Good Item {1} must be a sub-contracted item").format(
"Row #{0}: Finished Good Item {1} must be a sub-contracted item for service item {2}" item.idx, item.fg_item
).format(item.idx, item.fg_item, item.item_code) )
) )
elif not frappe.get_value("Item", item.fg_item, "default_bom"): elif not frappe.get_value("Item", item.fg_item, "default_bom"):
frappe.throw( frappe.throw(
_("Row #{0}: Default BOM not found for FG Item {1}").format(item.idx, item.fg_item) _("Row #{0}: Default BOM not found for FG Item {1}").format(item.idx, item.fg_item)
) )
if not item.fg_item_qty: if not item.fg_item_qty:
frappe.throw( frappe.throw(_("Row #{0}: Finished Good Item Qty can not be zero").format(item.idx))
_("Row #{0}: Finished Good Item Qty is not specified for service item {0}").format(
item.idx, item.item_code
)
)
else: else:
for item in self.items: for item in self.items:
item.set("fg_item", None) item.set("fg_item", None)

View File

@@ -394,7 +394,7 @@ class AccountsController(TransactionBase):
self.get("inter_company_reference") self.get("inter_company_reference")
or self.get("inter_company_invoice_reference") or self.get("inter_company_invoice_reference")
or self.get("inter_company_order_reference") or self.get("inter_company_order_reference")
): ) and not self.get("is_return"):
msg = _("Internal Sale or Delivery Reference missing.") msg = _("Internal Sale or Delivery Reference missing.")
msg += _("Please create purchase from internal sale or delivery document itself") msg += _("Please create purchase from internal sale or delivery document itself")
frappe.throw(msg, title=_("Internal Sales Reference Missing")) frappe.throw(msg, title=_("Internal Sales Reference Missing"))

View File

@@ -37,7 +37,7 @@ def validate_return_against(doc):
if ( if (
ref_doc.company == doc.company ref_doc.company == doc.company
and ref_doc.get(party_type) == doc.get(party_type) 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 # validate posting date time
return_posting_datetime = "%s %s" % (doc.posting_date, doc.get("posting_time") or "00:00:00") return_posting_datetime = "%s %s" % (doc.posting_date, doc.get("posting_time") or "00:00:00")

View File

@@ -74,24 +74,25 @@ class SubcontractingController(StockController):
) )
if not is_stock_item: if not is_stock_item:
msg = f"Item {item.item_name} must be a stock item." frappe.throw(_("Row {0}: Item {1} must be a stock item.").format(item.idx, item.item_name))
frappe.throw(_(msg))
if not is_sub_contracted_item: if not is_sub_contracted_item:
msg = f"Item {item.item_name} must be a subcontracted item." frappe.throw(
frappe.throw(_(msg)) _("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name)
)
if item.bom: if item.bom:
bom = frappe.get_doc("BOM", item.bom) bom = frappe.get_doc("BOM", item.bom)
if not bom.is_active: if not bom.is_active:
msg = f"Please select an active BOM for Item {item.item_name}." frappe.throw(
frappe.throw(_(msg)) _("Row {0}: Please select an active BOM for Item {1}.").format(item.idx, item.item_name)
)
if bom.item != item.item_code: if bom.item != item.item_code:
msg = f"Please select an valid BOM for Item {item.item_name}." frappe.throw(
frappe.throw(_(msg)) _("Row {0}: Please select an valid BOM for Item {1}.").format(item.idx, item.item_name)
)
else: else:
msg = f"Please select a BOM for Item {item.item_name}." frappe.throw(_("Row {0}: Please select a BOM for Item {1}.").format(item.idx, item.item_name))
frappe.throw(_(msg))
def __get_data_before_save(self): def __get_data_before_save(self):
item_dict = {} item_dict = {}

View File

@@ -6,6 +6,7 @@ import json
import frappe import frappe
from frappe import _, scrub from frappe import _, scrub
from frappe.model.document import Document
from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction
import erpnext import erpnext
@@ -20,7 +21,7 @@ from erpnext.stock.get_item_details import _get_item_tax_template
class calculate_taxes_and_totals(object): class calculate_taxes_and_totals(object):
def __init__(self, doc): def __init__(self, doc: Document):
self.doc = doc self.doc = doc
frappe.flags.round_off_applicable_accounts = [] frappe.flags.round_off_applicable_accounts = []
get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts) get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts)
@@ -677,7 +678,7 @@ class calculate_taxes_and_totals(object):
) )
def calculate_total_advance(self): def calculate_total_advance(self):
if self.doc.docstatus < 2: if not self.doc.docstatus.is_cancelled():
total_allocated_amount = sum( total_allocated_amount = sum(
flt(adv.allocated_amount, adv.precision("allocated_amount")) flt(adv.allocated_amount, adv.precision("allocated_amount"))
for adv in self.doc.get("advances") for adv in self.doc.get("advances")
@@ -708,7 +709,7 @@ class calculate_taxes_and_totals(object):
) )
) )
if self.doc.docstatus == 0: if self.doc.docstatus.is_draft():
if self.doc.get("write_off_outstanding_amount_automatically"): if self.doc.get("write_off_outstanding_amount_automatically"):
self.doc.write_off_amount = 0 self.doc.write_off_amount = 0

View File

@@ -13,38 +13,24 @@ frappe.query_reports["Work Order Summary"] = {
reqd: 1 reqd: 1
}, },
{ {
fieldname: "fiscal_year", label: __("Based On"),
label: __("Fiscal Year"), fieldname:"based_on",
fieldtype: "Link", fieldtype: "Select",
options: "Fiscal Year", options: "Creation Date\nPlanned Date\nActual Date",
default: frappe.defaults.get_user_default("fiscal_year"), default: "Creation Date"
reqd: 1,
on_change: function(query_report) {
var fiscal_year = query_report.get_values().fiscal_year;
if (!fiscal_year) {
return;
}
frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) {
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
frappe.query_report.set_filter_value({
from_date: fy.year_start_date,
to_date: fy.year_end_date
});
});
}
}, },
{ {
label: __("From Posting Date"), label: __("From Posting Date"),
fieldname:"from_date", fieldname:"from_date",
fieldtype: "Date", fieldtype: "Date",
default: frappe.defaults.get_user_default("year_start_date"), default: frappe.datetime.add_months(frappe.datetime.get_today(), -3),
reqd: 1 reqd: 1
}, },
{ {
label: __("To Posting Date"), label: __("To Posting Date"),
fieldname:"to_date", fieldname:"to_date",
fieldtype: "Date", fieldtype: "Date",
default: frappe.defaults.get_user_default("year_end_date"), default: frappe.datetime.get_today(),
reqd: 1, reqd: 1,
}, },
{ {

View File

@@ -31,6 +31,7 @@ def get_data(filters):
"sales_order", "sales_order",
"production_item", "production_item",
"qty", "qty",
"creation",
"produced_qty", "produced_qty",
"planned_start_date", "planned_start_date",
"planned_end_date", "planned_end_date",
@@ -47,11 +48,17 @@ def get_data(filters):
if filters.get(field): if filters.get(field):
query_filters[field] = filters.get(field) query_filters[field] = filters.get(field)
query_filters["planned_start_date"] = (">=", filters.get("from_date")) if filters.get("based_on") == "Planned Date":
query_filters["planned_end_date"] = ("<=", filters.get("to_date")) query_filters["planned_start_date"] = (">=", filters.get("from_date"))
query_filters["planned_end_date"] = ("<=", filters.get("to_date"))
elif filters.get("based_on") == "Actual Date":
query_filters["actual_start_date"] = (">=", filters.get("from_date"))
query_filters["actual_end_date"] = ("<=", filters.get("to_date"))
else:
query_filters["creation"] = ("between", [filters.get("from_date"), filters.get("to_date")])
data = frappe.get_all( data = frappe.get_all(
"Work Order", fields=fields, filters=query_filters, order_by="planned_start_date asc" "Work Order", fields=fields, filters=query_filters, order_by="planned_start_date asc", debug=1
) )
res = [] res = []
@@ -213,6 +220,12 @@ def get_columns(filters):
"options": "Sales Order", "options": "Sales Order",
"width": 90, "width": 90,
}, },
{
"label": _("Created On"),
"fieldname": "creation",
"fieldtype": "Date",
"width": 150,
},
{ {
"label": _("Planned Start Date"), "label": _("Planned Start Date"),
"fieldname": "planned_start_date", "fieldname": "planned_start_date",

View File

@@ -195,7 +195,6 @@ erpnext.patches.v13_0.update_project_template_tasks
erpnext.patches.v13_0.convert_qi_parameter_to_link_field erpnext.patches.v13_0.convert_qi_parameter_to_link_field
erpnext.patches.v13_0.add_naming_series_to_old_projects # 1-02-2021 erpnext.patches.v13_0.add_naming_series_to_old_projects # 1-02-2021
erpnext.patches.v13_0.update_payment_terms_outstanding erpnext.patches.v13_0.update_payment_terms_outstanding
erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl
erpnext.patches.v13_0.delete_old_bank_reconciliation_doctypes erpnext.patches.v13_0.delete_old_bank_reconciliation_doctypes
erpnext.patches.v13_0.update_vehicle_no_reqd_condition erpnext.patches.v13_0.update_vehicle_no_reqd_condition
erpnext.patches.v13_0.rename_membership_settings_to_non_profit_settings erpnext.patches.v13_0.rename_membership_settings_to_non_profit_settings
@@ -291,6 +290,7 @@ erpnext.patches.v13_0.update_exchange_rate_settings
erpnext.patches.v14_0.delete_amazon_mws_doctype erpnext.patches.v14_0.delete_amazon_mws_doctype
erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr
erpnext.patches.v13_0.update_accounts_in_loan_docs erpnext.patches.v13_0.update_accounts_in_loan_docs
erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl
erpnext.patches.v14_0.update_batch_valuation_flag erpnext.patches.v14_0.update_batch_valuation_flag
erpnext.patches.v14_0.delete_non_profit_doctypes erpnext.patches.v14_0.delete_non_profit_doctypes
erpnext.patches.v13_0.add_cost_center_in_loans erpnext.patches.v13_0.add_cost_center_in_loans

View File

@@ -7,6 +7,7 @@ from erpnext.stock.stock_ledger import update_entries_after
def execute(): def execute():
doctypes_to_reload = [ doctypes_to_reload = [
("setup", "company"),
("stock", "repost_item_valuation"), ("stock", "repost_item_valuation"),
("stock", "stock_entry_detail"), ("stock", "stock_entry_detail"),
("stock", "purchase_receipt_item"), ("stock", "purchase_receipt_item"),

View File

@@ -5,7 +5,12 @@ erpnext.accounts.bank_reconciliation.DataTableManager = class DataTableManager {
Object.assign(this, opts); Object.assign(this, opts);
this.dialog_manager = new erpnext.accounts.bank_reconciliation.DialogManager( this.dialog_manager = new erpnext.accounts.bank_reconciliation.DialogManager(
this.company, 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(); this.make_dt();
} }
@@ -17,6 +22,8 @@ erpnext.accounts.bank_reconciliation.DataTableManager = class DataTableManager {
"erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_bank_transactions", "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_bank_transactions",
args: { args: {
bank_account: this.bank_account, bank_account: this.bank_account,
from_date: this.bank_statement_from_date,
to_date: this.bank_statement_to_date
}, },
callback: function (response) { callback: function (response) {
me.format_data(response.message); me.format_data(response.message);

View File

@@ -1,12 +1,16 @@
frappe.provide("erpnext.accounts.bank_reconciliation"); frappe.provide("erpnext.accounts.bank_reconciliation");
erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
constructor(company, bank_account) { constructor(company, bank_account, bank_statement_from_date, bank_statement_to_date, filter_by_reference_date, from_reference_date, to_reference_date) {
this.bank_account = bank_account; this.bank_account = bank_account;
this.company = company; this.company = company;
this.make_dialog(); 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) { show_dialog(bank_transaction_name, update_dt_cards) {
this.bank_transaction_name = bank_transaction_name; this.bank_transaction_name = bank_transaction_name;
this.update_dt_cards = update_dt_cards; this.update_dt_cards = update_dt_cards;
@@ -35,13 +39,13 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
if (r.message) { if (r.message) {
this.bank_transaction = r.message; this.bank_transaction = r.message;
r.message.payment_entry = 1; r.message.payment_entry = 1;
r.message.journal_entry = 1;
this.dialog.set_values(r.message); this.dialog.set_values(r.message);
this.dialog.show(); this.dialog.show();
} }
}, },
}); });
} }
get_linked_vouchers(document_types) { get_linked_vouchers(document_types) {
frappe.call({ frappe.call({
method: method:
@@ -49,6 +53,11 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
args: { args: {
bank_transaction_name: this.bank_transaction_name, bank_transaction_name: this.bank_transaction_name,
document_types: document_types, 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) => { callback: (result) => {
@@ -66,6 +75,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
row[1], row[1],
row[2], row[2],
reference_date, reference_date,
row[8],
format_currency(row[3], row[9]), format_currency(row[3], row[9]),
row[6], row[6],
row[4], row[4],
@@ -101,6 +111,11 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
editable: false, editable: false,
width: 120, width: 120,
}, },
{
name: "Posting Date",
editable: false,
width: 120,
},
{ {
name: __("Amount"), name: __("Amount"),
editable: false, editable: false,
@@ -578,4 +593,4 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
} }
} }
}; };

View File

@@ -194,14 +194,7 @@ def get_list_context(context=None):
@frappe.whitelist() @frappe.whitelist()
def make_sales_order(source_name, target_doc=None): def make_sales_order(source_name: str, 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."))
return _make_sales_order(source_name, target_doc) return _make_sales_order(source_name, target_doc)

View File

@@ -136,17 +136,20 @@ class TestQuotation(FrappeTestCase):
sales_order.payment_schedule[1].due_date, getdate(add_days(quotation.transaction_date, 30)) sales_order.payment_schedule[1].due_date, getdate(add_days(quotation.transaction_date, 30))
) )
def test_valid_till(self): def test_valid_till_before_transaction_date(self):
from erpnext.selling.doctype.quotation.quotation import make_sales_order
quotation = frappe.copy_doc(test_records[0]) quotation = frappe.copy_doc(test_records[0])
quotation.valid_till = add_days(quotation.transaction_date, -1) quotation.valid_till = add_days(quotation.transaction_date, -1)
self.assertRaises(frappe.ValidationError, quotation.validate) 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.valid_till = add_days(nowdate(), -1)
quotation.insert() quotation.insert()
quotation.submit() quotation.submit()
self.assertRaises(frappe.ValidationError, make_sales_order, quotation.name)
make_sales_order(quotation.name)
def test_shopping_cart_without_website_item(self): def test_shopping_cart_without_website_item(self):
if frappe.db.exists("Website Item", {"item_code": "_Test Item Home Desktop 100"}): if frappe.db.exists("Website Item", {"item_code": "_Test Item Home Desktop 100"}):

View File

@@ -208,7 +208,7 @@ class SalesOrder(SellingController):
for quotation in set(d.prevdoc_docname for d in self.get("items")): for quotation in set(d.prevdoc_docname for d in self.get("items")):
if quotation: if quotation:
doc = frappe.get_doc("Quotation", quotation) doc = frappe.get_doc("Quotation", quotation)
if doc.docstatus == 2: if doc.docstatus.is_cancelled():
frappe.throw(_("Quotation {0} is cancelled").format(quotation)) frappe.throw(_("Quotation {0} is cancelled").format(quotation))
doc.set_status(update=True) doc.set_status(update=True)

View File

@@ -14,7 +14,6 @@ def get_data():
}, },
"internal_links": { "internal_links": {
"Quotation": ["items", "prevdoc_docname"], "Quotation": ["items", "prevdoc_docname"],
"Material Request": ["items", "material_request"],
}, },
"transactions": [ "transactions": [
{ {

View File

@@ -865,7 +865,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-11-18 11:39:01.741665", "modified": "2022-12-25 02:51:10.247569",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Sales Order Item", "name": "Sales Order Item",
@@ -876,4 +876,4 @@
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -106,7 +106,6 @@ class TestItem(FrappeTestCase):
"conversion_factor": 1.0, "conversion_factor": 1.0,
"reserved_qty": 1, "reserved_qty": 1,
"actual_qty": 5, "actual_qty": 5,
"ordered_qty": 10,
"projected_qty": 14, "projected_qty": 14,
} }

View File

@@ -4,7 +4,7 @@
import json import json
from collections import OrderedDict, defaultdict from collections import OrderedDict, defaultdict
from itertools import groupby from itertools import groupby
from typing import Dict, List, Set from typing import Dict, List
import frappe import frappe
from frappe import _ from frappe import _
@@ -41,7 +41,9 @@ class PickList(Document):
) )
def before_submit(self): def before_submit(self):
update_sales_orders = set() self.validate_picked_items()
def validate_picked_items(self):
for item in self.locations: for item in self.locations:
if self.scan_mode and item.picked_qty < item.stock_qty: if self.scan_mode and item.picked_qty < item.stock_qty:
frappe.throw( frappe.throw(
@@ -50,17 +52,14 @@ class PickList(Document):
).format(item.idx, item.stock_qty - item.picked_qty, item.stock_uom), ).format(item.idx, item.stock_qty - item.picked_qty, item.stock_uom),
title=_("Pick List Incomplete"), 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 # if the user has not entered any picked qty, set it to stock_qty, before submit
item.picked_qty = item.stock_qty 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"): if not frappe.get_cached_value("Item", item.item_code, "has_serial_no"):
continue continue
if not item.serial_no: if not item.serial_no:
frappe.throw( frappe.throw(
_("Row #{0}: {1} does not have any available serial numbers in {2}").format( _("Row #{0}: {1} does not have any available serial numbers in {2}").format(
@@ -68,58 +67,96 @@ class PickList(Document):
), ),
title=_("Serial Nos Required"), 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() if len(item.serial_no.split("\n")) != item.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"))
):
frappe.throw( frappe.throw(
_( _(
"You are picking more than required quantity for {}. Check if there is any other pick list created for {}" "For item {0} at row {1}, count of serial numbers does not match with the picked quantity"
).format(item_code, item.sales_order) ).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()
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)
@staticmethod
def update_sales_order_picking_status(sales_orders: Set[str]) -> None:
for sales_order in sales_orders: 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() @frappe.whitelist()
def set_item_locations(self, save=False): def set_item_locations(self, save=False):
@@ -230,7 +267,8 @@ class PickList(Document):
frappe.throw(_("Qty of Finished Goods Item should be greater than 0.")) frappe.throw(_("Qty of Finished Goods Item should be greater than 0."))
def before_print(self, settings=None): def before_print(self, settings=None):
self.group_similar_items() if self.group_same_items:
self.group_similar_items()
def group_similar_items(self): def group_similar_items(self):
group_item_qty = defaultdict(float) group_item_qty = defaultdict(float)
@@ -308,6 +346,31 @@ class PickList(Document):
return int(flt(min(possible_bundles), precision or 6)) 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): def validate_item_locations(pick_list):
if not pick_list.locations: if not pick_list.locations:
frappe.throw(_("Add items in the Item Locations table")) frappe.throw(_("Add items in the Item Locations table"))

View File

@@ -1,7 +1,10 @@
def get_data(): def get_data():
return { return {
"fieldname": "pick_list", "fieldname": "pick_list",
"internal_links": {
"Sales Order": ["locations", "sales_order"],
},
"transactions": [ "transactions": [
{"items": ["Stock Entry", "Delivery Note"]}, {"items": ["Stock Entry", "Sales Order", "Delivery Note"]},
], ],
} }

View File

@@ -445,10 +445,10 @@ class TestPickList(FrappeTestCase):
pl.before_print() pl.before_print()
self.assertEqual(len(pl.locations), 4) 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( pl = frappe.get_doc(
doctype="Pick List", doctype="Pick List",
group_same_items=True, group_same_items=False,
locations=[ locations=[
_dict(item_code="A", warehouse="X", qty=5, picked_qty=1), _dict(item_code="A", warehouse="X", qty=5, picked_qty=1),
_dict(item_code="B", warehouse="Y", qty=4, picked_qty=2), _dict(item_code="B", warehouse="Y", qty=4, picked_qty=2),
@@ -457,6 +457,11 @@ class TestPickList(FrappeTestCase):
], ],
) )
pl.before_print() 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) self.assertEqual(len(pl.locations), 2)
expected_items = [ expected_items = [

View File

@@ -1181,7 +1181,7 @@ def get_projected_qty(item_code, warehouse):
@frappe.whitelist() @frappe.whitelist()
def get_bin_details(item_code, warehouse, company=None, include_child_warehouses=False): 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: if warehouse:
from frappe.query_builder.functions import Coalesce, Sum from frappe.query_builder.functions import Coalesce, Sum
@@ -1197,7 +1197,6 @@ def get_bin_details(item_code, warehouse, company=None, include_child_warehouses
Coalesce(Sum(bin.projected_qty), 0).as_("projected_qty"), Coalesce(Sum(bin.projected_qty), 0).as_("projected_qty"),
Coalesce(Sum(bin.actual_qty), 0).as_("actual_qty"), Coalesce(Sum(bin.actual_qty), 0).as_("actual_qty"),
Coalesce(Sum(bin.reserved_qty), 0).as_("reserved_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))) .where((bin.item_code == item_code) & (bin.warehouse.isin(warehouses)))
).run(as_dict=True)[0] ).run(as_dict=True)[0]

View File

@@ -41,7 +41,7 @@ def get_data(report_filters):
key = (d.voucher_type, d.voucher_no) key = (d.voucher_type, d.voucher_no)
gl_data = voucher_wise_gl_data.get(key) or {} gl_data = voucher_wise_gl_data.get(key) or {}
d.account_value = gl_data.get("account_value", 0) 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: if abs(d.difference_value) > 0.1:
data.append(d) data.append(d)

View File

@@ -57,11 +57,17 @@ class SubcontractingReceipt(SubcontractingController):
def before_validate(self): def before_validate(self):
super(SubcontractingReceipt, self).before_validate() super(SubcontractingReceipt, self).before_validate()
self.validate_items_qty()
self.set_items_bom() self.set_items_bom()
self.set_items_cost_center() self.set_items_cost_center()
self.set_items_expense_account() self.set_items_expense_account()
def validate(self): 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() super(SubcontractingReceipt, self).validate()
self.set_missing_values() self.set_missing_values()
self.validate_posting_time() self.validate_posting_time()
@@ -157,7 +163,7 @@ class SubcontractingReceipt(SubcontractingController):
total_qty = total_amount = 0 total_qty = total_amount = 0
for item in self.items: 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_supp_cost = rm_supp_cost[item.name]
item.rm_cost_per_qty = item.rm_supp_cost / item.qty item.rm_cost_per_qty = item.rm_supp_cost / item.qty
rm_supp_cost.pop(item.name) rm_supp_cost.pop(item.name)
@@ -194,6 +200,13 @@ class SubcontractingReceipt(SubcontractingController):
).format(item.idx) ).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): def set_items_bom(self):
if self.is_return: if self.is_return:
for item in self.items: for item in self.items: