mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-07 23:22:52 +00:00
Merge pull request #47429 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -77,9 +77,12 @@
|
|||||||
"reports_tab",
|
"reports_tab",
|
||||||
"remarks_section",
|
"remarks_section",
|
||||||
"general_ledger_remarks_length",
|
"general_ledger_remarks_length",
|
||||||
"ignore_is_opening_check_for_reporting",
|
|
||||||
"column_break_lvjk",
|
"column_break_lvjk",
|
||||||
"receivable_payable_remarks_length",
|
"receivable_payable_remarks_length",
|
||||||
|
"accounts_receivable_payable_tuning_section",
|
||||||
|
"receivable_payable_fetch_method",
|
||||||
|
"legacy_section",
|
||||||
|
"ignore_is_opening_check_for_reporting",
|
||||||
"payment_request_settings",
|
"payment_request_settings",
|
||||||
"create_pr_in_draft_status"
|
"create_pr_in_draft_status"
|
||||||
],
|
],
|
||||||
@@ -532,6 +535,34 @@
|
|||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Posting Date Inheritance for Exchange Gain / Loss",
|
"label": "Posting Date Inheritance for Exchange Gain / Loss",
|
||||||
"options": "Invoice\nPayment\nReconciliation Date"
|
"options": "Invoice\nPayment\nReconciliation Date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_xrnd",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "If enabled, Sales Invoice will be generated instead of POS Invoice in POS Transactions for real-time update of G/L and Stock Ledger.",
|
||||||
|
"fieldname": "use_sales_invoice_in_pos",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Use Sales Invoice"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "Buffered Cursor",
|
||||||
|
"fieldname": "receivable_payable_fetch_method",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Data Fetch Method",
|
||||||
|
"options": "Buffered Cursor\nUnBuffered Cursor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "accounts_receivable_payable_tuning_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Accounts Receivable / Payable Tuning"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "legacy_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Legacy Fields"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "icon-cog",
|
"icon": "icon-cog",
|
||||||
@@ -539,7 +570,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-01-23 13:15:44.077853",
|
"modified": "2025-05-05 12:29:38.302027",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Accounts Settings",
|
"name": "Accounts Settings",
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ class AccountsSettings(Document):
|
|||||||
merge_similar_account_heads: DF.Check
|
merge_similar_account_heads: DF.Check
|
||||||
over_billing_allowance: DF.Currency
|
over_billing_allowance: DF.Currency
|
||||||
post_change_gl_entries: DF.Check
|
post_change_gl_entries: DF.Check
|
||||||
|
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor"]
|
||||||
receivable_payable_remarks_length: DF.Int
|
receivable_payable_remarks_length: DF.Int
|
||||||
reconciliation_queue_size: DF.Int
|
reconciliation_queue_size: DF.Int
|
||||||
role_allowed_to_over_bill: DF.Link | None
|
role_allowed_to_over_bill: DF.Link | None
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import frappe
|
|||||||
from frappe.utils import add_months, getdate
|
from frappe.utils import add_months, getdate
|
||||||
|
|
||||||
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||||
|
from erpnext.accounts.doctype.mode_of_payment.test_mode_of_payment import (
|
||||||
|
set_default_account_for_mode_of_payment,
|
||||||
|
)
|
||||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||||
@@ -191,11 +194,13 @@ def make_pos_sales_invoice():
|
|||||||
|
|
||||||
customer = make_customer(customer="_Test Customer")
|
customer = make_customer(customer="_Test Customer")
|
||||||
|
|
||||||
|
mode_of_payment = frappe.get_doc("Mode of Payment", "Wire Transfer")
|
||||||
|
|
||||||
|
set_default_account_for_mode_of_payment(mode_of_payment, "_Test Company", "_Test Bank Clearance - _TC")
|
||||||
|
|
||||||
si = create_sales_invoice(customer=customer, item="_Test Item", is_pos=1, qty=1, rate=1000, do_not_save=1)
|
si = create_sales_invoice(customer=customer, item="_Test Item", is_pos=1, qty=1, rate=1000, do_not_save=1)
|
||||||
si.set("payments", [])
|
si.set("payments", [])
|
||||||
si.append(
|
si.append("payments", {"mode_of_payment": "Wire Transfer", "amount": 1000})
|
||||||
"payments", {"mode_of_payment": "Cash", "account": "_Test Bank Clearance - _TC", "amount": 1000}
|
|
||||||
)
|
|
||||||
si.insert()
|
si.insert()
|
||||||
si.submit()
|
si.submit()
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool
|
|||||||
get_linked_payments,
|
get_linked_payments,
|
||||||
reconcile_vouchers,
|
reconcile_vouchers,
|
||||||
)
|
)
|
||||||
|
from erpnext.accounts.doctype.mode_of_payment.test_mode_of_payment import (
|
||||||
|
set_default_account_for_mode_of_payment,
|
||||||
|
)
|
||||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||||
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
||||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||||
@@ -434,15 +437,13 @@ def add_vouchers(gl_account="_Test Bank - _TC"):
|
|||||||
except frappe.DuplicateEntryError:
|
except frappe.DuplicateEntryError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
mode_of_payment = frappe.get_doc({"doctype": "Mode of Payment", "name": "Cash"})
|
mode_of_payment = frappe.get_doc({"doctype": "Mode of Payment", "name": "Wire Transfer"})
|
||||||
|
|
||||||
if not frappe.db.get_value("Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"}):
|
set_default_account_for_mode_of_payment(mode_of_payment, "_Test Company", gl_account)
|
||||||
mode_of_payment.append("accounts", {"company": "_Test Company", "default_account": gl_account})
|
|
||||||
mode_of_payment.save()
|
|
||||||
|
|
||||||
si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_save=1)
|
si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_save=1)
|
||||||
si.is_pos = 1
|
si.is_pos = 1
|
||||||
si.append("payments", {"mode_of_payment": "Cash", "account": gl_account, "amount": 109080})
|
si.append("payments", {"mode_of_payment": "Wire Transfer", "amount": 109080})
|
||||||
si.insert()
|
si.insert()
|
||||||
si.submit()
|
si.submit()
|
||||||
|
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ frappe.ui.form.on("Invoice Discounting", {
|
|||||||
from_date: frm.doc.posting_date,
|
from_date: frm.doc.posting_date,
|
||||||
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
|
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
|
||||||
company: frm.doc.company,
|
company: frm.doc.company,
|
||||||
group_by: "Group by Voucher (Consolidated)",
|
categorize_by: "Categorize by Voucher (Consolidated)",
|
||||||
show_cancelled_entries: frm.doc.docstatus === 2,
|
show_cancelled_entries: frm.doc.docstatus === 2,
|
||||||
};
|
};
|
||||||
frappe.set_route("query-report", "General Ledger");
|
frappe.set_route("query-report", "General Ledger");
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ frappe.ui.form.on("Journal Entry", {
|
|||||||
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
|
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
|
||||||
company: frm.doc.company,
|
company: frm.doc.company,
|
||||||
finance_book: frm.doc.finance_book,
|
finance_book: frm.doc.finance_book,
|
||||||
group_by: "",
|
categorize_by: "",
|
||||||
show_cancelled_entries: frm.doc.docstatus === 2,
|
show_cancelled_entries: frm.doc.docstatus === 2,
|
||||||
};
|
};
|
||||||
frappe.set_route("query-report", "General Ledger");
|
frappe.set_route("query-report", "General Ledger");
|
||||||
|
|||||||
@@ -3,8 +3,25 @@
|
|||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
# test_records = frappe.get_test_records('Mode of Payment')
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
class TestModeofPayment(unittest.TestCase):
|
class TestModeofPayment(unittest.TestCase):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def set_default_account_for_mode_of_payment(mode_of_payment, company, account):
|
||||||
|
mode_of_payment.reload()
|
||||||
|
if frappe.db.exists(
|
||||||
|
"Mode of Payment Account", {"parent": mode_of_payment.mode_of_payment, "company": company}
|
||||||
|
):
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Mode of Payment Account",
|
||||||
|
{"parent": mode_of_payment.mode_of_payment, "company": company},
|
||||||
|
"default_account",
|
||||||
|
account,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
mode_of_payment.append("accounts", {"company": company, "default_account": account})
|
||||||
|
mode_of_payment.save()
|
||||||
|
|||||||
@@ -411,7 +411,7 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
from_date: frm.doc.posting_date,
|
from_date: frm.doc.posting_date,
|
||||||
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
|
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
|
||||||
company: frm.doc.company,
|
company: frm.doc.company,
|
||||||
group_by: "",
|
categorize_by: "",
|
||||||
show_cancelled_entries: frm.doc.docstatus === 2,
|
show_cancelled_entries: frm.doc.docstatus === 2,
|
||||||
};
|
};
|
||||||
frappe.set_route("query-report", "General Ledger");
|
frappe.set_route("query-report", "General Ledger");
|
||||||
|
|||||||
@@ -1994,7 +1994,7 @@ class PaymentEntry(AccountsController):
|
|||||||
|
|
||||||
# Re allocate amount to those references which have PR set (Higher priority)
|
# Re allocate amount to those references which have PR set (Higher priority)
|
||||||
for ref in self.references:
|
for ref in self.references:
|
||||||
if not ref.payment_request:
|
if not (ref.reference_doctype and ref.reference_name and ref.payment_request):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# fetch outstanding_amount of `Reference` (Payment Term) and `Payment Request` to allocate new amount
|
# fetch outstanding_amount of `Reference` (Payment Term) and `Payment Request` to allocate new amount
|
||||||
@@ -2045,7 +2045,7 @@ class PaymentEntry(AccountsController):
|
|||||||
)
|
)
|
||||||
# Re allocate amount to those references which have no PR (Lower priority)
|
# Re allocate amount to those references which have no PR (Lower priority)
|
||||||
for ref in self.references:
|
for ref in self.references:
|
||||||
if ref.payment_request:
|
if ref.payment_request or not (ref.reference_doctype and ref.reference_name):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
key = (ref.reference_doctype, ref.reference_name, ref.get("payment_term"))
|
key = (ref.reference_doctype, ref.reference_name, ref.get("payment_term"))
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ frappe.ui.form.on("Period Closing Voucher", {
|
|||||||
from_date: frm.doc.posting_date,
|
from_date: frm.doc.posting_date,
|
||||||
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
|
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
|
||||||
company: frm.doc.company,
|
company: frm.doc.company,
|
||||||
group_by: "",
|
categorize_by: "",
|
||||||
show_cancelled_entries: frm.doc.docstatus === 2,
|
show_cancelled_entries: frm.doc.docstatus === 2,
|
||||||
};
|
};
|
||||||
frappe.set_route("query-report", "General Ledger");
|
frappe.set_route("query-report", "General Ledger");
|
||||||
|
|||||||
@@ -323,3 +323,15 @@ frappe.ui.form.on("POS Invoice", {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frappe.ui.form.on("Sales Invoice Payment", {
|
||||||
|
mode_of_payment: function (frm) {
|
||||||
|
frappe.call({
|
||||||
|
doc: frm.doc,
|
||||||
|
method: "set_account_for_mode_of_payment",
|
||||||
|
callback: function (r) {
|
||||||
|
refresh_field("payments");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import unittest
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
|
||||||
|
from erpnext.accounts.doctype.mode_of_payment.test_mode_of_payment import (
|
||||||
|
set_default_account_for_mode_of_payment,
|
||||||
|
)
|
||||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import PartialPaymentValidationError, make_sales_return
|
from erpnext.accounts.doctype.pos_invoice.pos_invoice import PartialPaymentValidationError, make_sales_return
|
||||||
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||||
@@ -31,6 +34,8 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
cls.test_user, cls.pos_profile = init_user_and_profile()
|
cls.test_user, cls.pos_profile = init_user_and_profile()
|
||||||
create_opening_entry(cls.pos_profile, cls.test_user)
|
create_opening_entry(cls.pos_profile, cls.test_user)
|
||||||
|
mode_of_payment = frappe.get_doc("Mode of Payment", "Bank Draft")
|
||||||
|
set_default_account_for_mode_of_payment(mode_of_payment, "_Test Company", "_Test Bank - _TC")
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
if frappe.session.user != "Administrator":
|
if frappe.session.user != "Administrator":
|
||||||
@@ -233,12 +238,8 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
pos = create_pos_invoice(qty=10, do_not_save=True)
|
pos = create_pos_invoice(qty=10, do_not_save=True)
|
||||||
|
|
||||||
pos.set("payments", [])
|
pos.set("payments", [])
|
||||||
pos.append(
|
pos.append("payments", {"mode_of_payment": "Bank Draft", "amount": 500})
|
||||||
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 500}
|
pos.append("payments", {"mode_of_payment": "Cash", "amount": 500, "default": 1})
|
||||||
)
|
|
||||||
pos.append(
|
|
||||||
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 500, "default": 1}
|
|
||||||
)
|
|
||||||
pos.insert()
|
pos.insert()
|
||||||
pos.submit()
|
pos.submit()
|
||||||
|
|
||||||
@@ -276,9 +277,7 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
do_not_save=1,
|
do_not_save=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
pos.append(
|
pos.append("payments", {"mode_of_payment": "Cash", "amount": 1000, "default": 1})
|
||||||
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1}
|
|
||||||
)
|
|
||||||
|
|
||||||
pos.insert()
|
pos.insert()
|
||||||
pos.submit()
|
pos.submit()
|
||||||
@@ -318,9 +317,7 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
do_not_save=1,
|
do_not_save=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
pos.append(
|
pos.append("payments", {"mode_of_payment": "Cash", "amount": 2000, "default": 1})
|
||||||
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2000, "default": 1}
|
|
||||||
)
|
|
||||||
|
|
||||||
pos.insert()
|
pos.insert()
|
||||||
pos.submit()
|
pos.submit()
|
||||||
@@ -331,9 +328,7 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
# partial return 1
|
# partial return 1
|
||||||
pos_return1.get("items")[0].qty = -1
|
pos_return1.get("items")[0].qty = -1
|
||||||
pos_return1.set("payments", [])
|
pos_return1.set("payments", [])
|
||||||
pos_return1.append(
|
pos_return1.append("payments", {"mode_of_payment": "Cash", "amount": -1000, "default": 1})
|
||||||
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": -1000, "default": 1}
|
|
||||||
)
|
|
||||||
pos_return1.paid_amount = -1000
|
pos_return1.paid_amount = -1000
|
||||||
pos_return1.submit()
|
pos_return1.submit()
|
||||||
pos_return1.reload()
|
pos_return1.reload()
|
||||||
@@ -350,9 +345,7 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
# partial return 2
|
# partial return 2
|
||||||
pos_return2 = make_sales_return(pos.name)
|
pos_return2 = make_sales_return(pos.name)
|
||||||
pos_return2.set("payments", [])
|
pos_return2.set("payments", [])
|
||||||
pos_return2.append(
|
pos_return2.append("payments", {"mode_of_payment": "Cash", "amount": -1000, "default": 1})
|
||||||
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": -1000, "default": 1}
|
|
||||||
)
|
|
||||||
pos_return2.paid_amount = -1000
|
pos_return2.paid_amount = -1000
|
||||||
pos_return2.submit()
|
pos_return2.submit()
|
||||||
|
|
||||||
@@ -372,10 +365,8 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
pos.set("payments", [])
|
pos.set("payments", [])
|
||||||
pos.append("payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 50})
|
pos.append("payments", {"mode_of_payment": "Bank Draft", "amount": 50})
|
||||||
pos.append(
|
pos.append("payments", {"mode_of_payment": "Cash", "amount": 60, "default": 1})
|
||||||
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 60, "default": 1}
|
|
||||||
)
|
|
||||||
|
|
||||||
pos.insert()
|
pos.insert()
|
||||||
pos.submit()
|
pos.submit()
|
||||||
@@ -393,7 +384,7 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
pos_inv = create_pos_invoice(rate=10000, do_not_save=1)
|
pos_inv = create_pos_invoice(rate=10000, do_not_save=1)
|
||||||
pos_inv.append(
|
pos_inv.append(
|
||||||
"payments",
|
"payments",
|
||||||
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 9000},
|
{"mode_of_payment": "Cash", "amount": 9000},
|
||||||
)
|
)
|
||||||
pos_inv.insert()
|
pos_inv.insert()
|
||||||
self.assertRaises(PartialPaymentValidationError, pos_inv.submit)
|
self.assertRaises(PartialPaymentValidationError, pos_inv.submit)
|
||||||
@@ -424,9 +415,7 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
do_not_save=1,
|
do_not_save=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
pos.append(
|
pos.append("payments", {"mode_of_payment": "Bank Draft", "amount": 1000})
|
||||||
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 1000}
|
|
||||||
)
|
|
||||||
|
|
||||||
pos.insert()
|
pos.insert()
|
||||||
pos.submit()
|
pos.submit()
|
||||||
@@ -445,9 +434,7 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
do_not_save=1,
|
do_not_save=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
pos2.append(
|
pos2.append("payments", {"mode_of_payment": "Bank Draft", "amount": 1000})
|
||||||
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 1000}
|
|
||||||
)
|
|
||||||
|
|
||||||
pos2.insert()
|
pos2.insert()
|
||||||
self.assertRaises(frappe.ValidationError, pos2.submit)
|
self.assertRaises(frappe.ValidationError, pos2.submit)
|
||||||
@@ -496,9 +483,7 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
do_not_save=1,
|
do_not_save=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
pos2.append(
|
pos2.append("payments", {"mode_of_payment": "Bank Draft", "amount": 1000})
|
||||||
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 1000}
|
|
||||||
)
|
|
||||||
|
|
||||||
pos2.insert()
|
pos2.insert()
|
||||||
self.assertRaises(frappe.ValidationError, pos2.submit)
|
self.assertRaises(frappe.ValidationError, pos2.submit)
|
||||||
@@ -561,9 +546,7 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
pos.get("items")[0].has_serial_no = 1
|
pos.get("items")[0].has_serial_no = 1
|
||||||
pos.set("payments", [])
|
pos.set("payments", [])
|
||||||
pos.append(
|
pos.append("payments", {"mode_of_payment": "Cash", "amount": 1000, "default": 1})
|
||||||
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1}
|
|
||||||
)
|
|
||||||
pos = pos.save().submit()
|
pos = pos.save().submit()
|
||||||
|
|
||||||
# make a return
|
# make a return
|
||||||
@@ -609,7 +592,7 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000, do_not_save=1)
|
inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000, do_not_save=1)
|
||||||
inv.append(
|
inv.append(
|
||||||
"payments",
|
"payments",
|
||||||
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 10000},
|
{"mode_of_payment": "Cash", "amount": 10000},
|
||||||
)
|
)
|
||||||
inv.insert()
|
inv.insert()
|
||||||
inv.submit()
|
inv.submit()
|
||||||
@@ -641,7 +624,7 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
pos_inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000, do_not_save=1)
|
pos_inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000, do_not_save=1)
|
||||||
pos_inv.append(
|
pos_inv.append(
|
||||||
"payments",
|
"payments",
|
||||||
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 10000},
|
{"mode_of_payment": "Cash", "amount": 10000},
|
||||||
)
|
)
|
||||||
pos_inv.paid_amount = 10000
|
pos_inv.paid_amount = 10000
|
||||||
pos_inv.submit()
|
pos_inv.submit()
|
||||||
@@ -656,7 +639,7 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
inv.loyalty_amount = inv.loyalty_points * before_lp_details.conversion_factor
|
inv.loyalty_amount = inv.loyalty_points * before_lp_details.conversion_factor
|
||||||
inv.append(
|
inv.append(
|
||||||
"payments",
|
"payments",
|
||||||
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 10000 - inv.loyalty_amount},
|
{"mode_of_payment": "Cash", "amount": 10000 - inv.loyalty_amount},
|
||||||
)
|
)
|
||||||
inv.paid_amount = 10000
|
inv.paid_amount = 10000
|
||||||
inv.submit()
|
inv.submit()
|
||||||
@@ -677,12 +660,12 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||||
test_user, pos_profile = init_user_and_profile()
|
test_user, pos_profile = init_user_and_profile()
|
||||||
pos_inv = create_pos_invoice(rate=300, additional_discount_percentage=10, do_not_submit=1)
|
pos_inv = create_pos_invoice(rate=300, additional_discount_percentage=10, do_not_submit=1)
|
||||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 270})
|
pos_inv.append("payments", {"mode_of_payment": "Cash", "amount": 270})
|
||||||
pos_inv.save()
|
pos_inv.save()
|
||||||
pos_inv.submit()
|
pos_inv.submit()
|
||||||
|
|
||||||
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
|
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
|
||||||
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
|
pos_inv2.append("payments", {"mode_of_payment": "Cash", "amount": 3200})
|
||||||
pos_inv2.save()
|
pos_inv2.save()
|
||||||
pos_inv2.submit()
|
pos_inv2.submit()
|
||||||
|
|
||||||
@@ -703,7 +686,7 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||||
test_user, pos_profile = init_user_and_profile()
|
test_user, pos_profile = init_user_and_profile()
|
||||||
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
|
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
|
||||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300})
|
pos_inv.append("payments", {"mode_of_payment": "Cash", "amount": 300})
|
||||||
pos_inv.append(
|
pos_inv.append(
|
||||||
"taxes",
|
"taxes",
|
||||||
{
|
{
|
||||||
@@ -720,7 +703,7 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
pos_inv2 = create_pos_invoice(rate=300, qty=2, do_not_submit=1)
|
pos_inv2 = create_pos_invoice(rate=300, qty=2, do_not_submit=1)
|
||||||
pos_inv2.additional_discount_percentage = 10
|
pos_inv2.additional_discount_percentage = 10
|
||||||
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 540})
|
pos_inv2.append("payments", {"mode_of_payment": "Cash", "amount": 540})
|
||||||
pos_inv2.append(
|
pos_inv2.append(
|
||||||
"taxes",
|
"taxes",
|
||||||
{
|
{
|
||||||
@@ -758,7 +741,7 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||||
test_user, pos_profile = init_user_and_profile()
|
test_user, pos_profile = init_user_and_profile()
|
||||||
pos_inv = create_pos_invoice(item=item, rate=300, do_not_submit=1)
|
pos_inv = create_pos_invoice(item=item, rate=300, do_not_submit=1)
|
||||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300})
|
pos_inv.append("payments", {"mode_of_payment": "Cash", "amount": 300})
|
||||||
pos_inv.append(
|
pos_inv.append(
|
||||||
"taxes",
|
"taxes",
|
||||||
{
|
{
|
||||||
@@ -773,7 +756,7 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
self.assertRaises(frappe.ValidationError, pos_inv.submit)
|
self.assertRaises(frappe.ValidationError, pos_inv.submit)
|
||||||
|
|
||||||
pos_inv2 = create_pos_invoice(item=item, rate=400, do_not_submit=1)
|
pos_inv2 = create_pos_invoice(item=item, rate=400, do_not_submit=1)
|
||||||
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 400})
|
pos_inv2.append("payments", {"mode_of_payment": "Cash", "amount": 400})
|
||||||
pos_inv2.append(
|
pos_inv2.append(
|
||||||
"taxes",
|
"taxes",
|
||||||
{
|
{
|
||||||
@@ -818,7 +801,7 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
pos_inv1 = create_pos_invoice(item="_BATCH ITEM Test For Reserve", rate=300, qty=15, do_not_save=1)
|
pos_inv1 = create_pos_invoice(item="_BATCH ITEM Test For Reserve", rate=300, qty=15, do_not_save=1)
|
||||||
pos_inv1.append(
|
pos_inv1.append(
|
||||||
"payments",
|
"payments",
|
||||||
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 4500},
|
{"mode_of_payment": "Cash", "amount": 4500},
|
||||||
)
|
)
|
||||||
pos_inv1.items[0].batch_no = batch_no
|
pos_inv1.items[0].batch_no = batch_no
|
||||||
pos_inv1.save()
|
pos_inv1.save()
|
||||||
@@ -839,7 +822,7 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
pos_inv2.append(
|
pos_inv2.append(
|
||||||
"payments",
|
"payments",
|
||||||
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3000},
|
{"mode_of_payment": "Cash", "amount": 3000},
|
||||||
)
|
)
|
||||||
pos_inv2.save()
|
pos_inv2.save()
|
||||||
pos_inv2.submit()
|
pos_inv2.submit()
|
||||||
@@ -879,7 +862,7 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
pos_inv1.append(
|
pos_inv1.append(
|
||||||
"payments",
|
"payments",
|
||||||
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300},
|
{"mode_of_payment": "Cash", "amount": 300},
|
||||||
)
|
)
|
||||||
pos_inv1.save()
|
pos_inv1.save()
|
||||||
pos_inv1.submit()
|
pos_inv1.submit()
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"posting_date",
|
"posting_date",
|
||||||
"company",
|
"company",
|
||||||
"account",
|
"account",
|
||||||
"group_by",
|
"categorize_by",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
"territory",
|
"territory",
|
||||||
"ignore_exchange_rate_revaluation_journals",
|
"ignore_exchange_rate_revaluation_journals",
|
||||||
@@ -174,14 +174,6 @@
|
|||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"label": "Start Date"
|
"label": "Start Date"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"default": "Group by Voucher (Consolidated)",
|
|
||||||
"depends_on": "eval:(doc.report == 'General Ledger');",
|
|
||||||
"fieldname": "group_by",
|
|
||||||
"fieldtype": "Select",
|
|
||||||
"label": "Group By",
|
|
||||||
"options": "\nGroup by Voucher\nGroup by Voucher (Consolidated)"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"depends_on": "eval: (doc.report == 'General Ledger');",
|
"depends_on": "eval: (doc.report == 'General Ledger');",
|
||||||
"fieldname": "currency",
|
"fieldname": "currency",
|
||||||
@@ -397,10 +389,18 @@
|
|||||||
"fieldname": "show_remarks",
|
"fieldname": "show_remarks",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Show Remarks"
|
"label": "Show Remarks"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "Categorize by Voucher (Consolidated)",
|
||||||
|
"depends_on": "eval:(doc.report == 'General Ledger');",
|
||||||
|
"fieldname": "categorize_by",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Categorize By",
|
||||||
|
"options": "\nCategorize by Voucher\nCategorize by Voucher (Consolidated)"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-12-11 12:11:13.543134",
|
"modified": "2025-04-30 14:43:23.643006",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Process Statement Of Accounts",
|
"name": "Process Statement Of Accounts",
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ class ProcessStatementOfAccounts(Document):
|
|||||||
ageing_based_on: DF.Literal["Due Date", "Posting Date"]
|
ageing_based_on: DF.Literal["Due Date", "Posting Date"]
|
||||||
based_on_payment_terms: DF.Check
|
based_on_payment_terms: DF.Check
|
||||||
body: DF.TextEditor | None
|
body: DF.TextEditor | None
|
||||||
|
categorize_by: DF.Literal["", "Categorize by Voucher", "Categorize by Voucher (Consolidated)"]
|
||||||
cc_to: DF.TableMultiSelect[ProcessStatementOfAccountsCC]
|
cc_to: DF.TableMultiSelect[ProcessStatementOfAccountsCC]
|
||||||
collection_name: DF.DynamicLink | None
|
collection_name: DF.DynamicLink | None
|
||||||
company: DF.Link
|
company: DF.Link
|
||||||
@@ -56,7 +57,6 @@ class ProcessStatementOfAccounts(Document):
|
|||||||
finance_book: DF.Link | None
|
finance_book: DF.Link | None
|
||||||
frequency: DF.Literal["Weekly", "Monthly", "Quarterly"]
|
frequency: DF.Literal["Weekly", "Monthly", "Quarterly"]
|
||||||
from_date: DF.Date | None
|
from_date: DF.Date | None
|
||||||
group_by: DF.Literal["", "Group by Voucher", "Group by Voucher (Consolidated)"]
|
|
||||||
ignore_cr_dr_notes: DF.Check
|
ignore_cr_dr_notes: DF.Check
|
||||||
ignore_exchange_rate_revaluation_journals: DF.Check
|
ignore_exchange_rate_revaluation_journals: DF.Check
|
||||||
include_ageing: DF.Check
|
include_ageing: DF.Check
|
||||||
@@ -204,7 +204,7 @@ def get_gl_filters(doc, entry, tax_id, presentation_currency):
|
|||||||
"party": [entry.customer],
|
"party": [entry.customer],
|
||||||
"party_name": [entry.customer_name] if entry.customer_name else None,
|
"party_name": [entry.customer_name] if entry.customer_name else None,
|
||||||
"presentation_currency": presentation_currency,
|
"presentation_currency": presentation_currency,
|
||||||
"group_by": doc.group_by,
|
"categorize_by": doc.categorize_by,
|
||||||
"currency": doc.currency,
|
"currency": doc.currency,
|
||||||
"project": [p.project_name for p in doc.project],
|
"project": [p.project_name for p in doc.project],
|
||||||
"show_opening_entries": 0,
|
"show_opening_entries": 0,
|
||||||
|
|||||||
@@ -1084,6 +1084,18 @@ frappe.ui.form.on("Sales Invoice Timesheet", {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frappe.ui.form.on("Sales Invoice Payment", {
|
||||||
|
mode_of_payment: function (frm) {
|
||||||
|
frappe.call({
|
||||||
|
doc: frm.doc,
|
||||||
|
method: "set_account_for_mode_of_payment",
|
||||||
|
callback: function (r) {
|
||||||
|
refresh_field("payments");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
var set_timesheet_detail_rate = function (cdt, cdn, currency, timelog) {
|
var set_timesheet_detail_rate = function (cdt, cdn, currency, timelog) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "erpnext.projects.doctype.timesheet.timesheet.get_timesheet_detail_rate",
|
method: "erpnext.projects.doctype.timesheet.timesheet.get_timesheet_detail_rate",
|
||||||
|
|||||||
@@ -751,10 +751,10 @@ class SalesInvoice(SellingController):
|
|||||||
self.paid_amount = paid_amount
|
self.paid_amount = paid_amount
|
||||||
self.base_paid_amount = base_paid_amount
|
self.base_paid_amount = base_paid_amount
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
def set_account_for_mode_of_payment(self):
|
def set_account_for_mode_of_payment(self):
|
||||||
for payment in self.payments:
|
for payment in self.payments:
|
||||||
if not payment.account:
|
payment.account = get_bank_cash_account(payment.mode_of_payment, self.company).get("account")
|
||||||
payment.account = get_bank_cash_account(payment.mode_of_payment, self.company).get("account")
|
|
||||||
|
|
||||||
def validate_time_sheets_are_submitted(self):
|
def validate_time_sheets_are_submitted(self):
|
||||||
for data in self.timesheets:
|
for data in self.timesheets:
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ from frappe.utils import add_days, flt, format_date, getdate, nowdate, today
|
|||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
|
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
|
||||||
|
from erpnext.accounts.doctype.mode_of_payment.test_mode_of_payment import (
|
||||||
|
set_default_account_for_mode_of_payment,
|
||||||
|
)
|
||||||
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
||||||
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import WarehouseMissingError
|
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import WarehouseMissingError
|
||||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import (
|
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import (
|
||||||
@@ -54,6 +57,11 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
create_items(["_Test Internal Transfer Item"], uoms=[{"uom": "Box", "conversion_factor": 10}])
|
create_items(["_Test Internal Transfer Item"], uoms=[{"uom": "Box", "conversion_factor": 10}])
|
||||||
create_internal_parties()
|
create_internal_parties()
|
||||||
setup_accounts()
|
setup_accounts()
|
||||||
|
mode_of_payment = frappe.get_doc("Mode of Payment", "Bank Draft")
|
||||||
|
set_default_account_for_mode_of_payment(mode_of_payment, "_Test Company", "_Test Bank - _TC")
|
||||||
|
set_default_account_for_mode_of_payment(
|
||||||
|
mode_of_payment, "_Test Company with perpetual inventory", "_Test Bank - TCP1"
|
||||||
|
)
|
||||||
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
|
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
@@ -964,10 +972,8 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
pos.is_pos = 1
|
pos.is_pos = 1
|
||||||
pos.update_stock = 1
|
pos.update_stock = 1
|
||||||
|
|
||||||
pos.append(
|
pos.append("payments", {"mode_of_payment": "Bank Draft", "amount": 50})
|
||||||
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - TCP1", "amount": 50}
|
pos.append("payments", {"mode_of_payment": "Cash", "amount": 50})
|
||||||
)
|
|
||||||
pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - TCP1", "amount": 50})
|
|
||||||
|
|
||||||
taxes = get_taxes_and_charges()
|
taxes = get_taxes_and_charges()
|
||||||
pos.taxes = []
|
pos.taxes = []
|
||||||
@@ -996,10 +1002,8 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
pos.is_pos = 1
|
pos.is_pos = 1
|
||||||
pos.pos_profile = pos_profile.name
|
pos.pos_profile = pos_profile.name
|
||||||
|
|
||||||
pos.append(
|
pos.append("payments", {"mode_of_payment": "Bank Draft", "amount": 500})
|
||||||
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 500}
|
pos.append("payments", {"mode_of_payment": "Cash", "amount": 500})
|
||||||
)
|
|
||||||
pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 500})
|
|
||||||
pos.insert()
|
pos.insert()
|
||||||
pos.submit()
|
pos.submit()
|
||||||
|
|
||||||
@@ -1042,10 +1046,8 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
pos.is_pos = 1
|
pos.is_pos = 1
|
||||||
pos.update_stock = 1
|
pos.update_stock = 1
|
||||||
|
|
||||||
pos.append(
|
pos.append("payments", {"mode_of_payment": "Bank Draft", "amount": 50})
|
||||||
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - TCP1", "amount": 50}
|
pos.append("payments", {"mode_of_payment": "Cash", "amount": 60})
|
||||||
)
|
|
||||||
pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - TCP1", "amount": 60})
|
|
||||||
|
|
||||||
pos.write_off_outstanding_amount_automatically = 1
|
pos.write_off_outstanding_amount_automatically = 1
|
||||||
pos.insert()
|
pos.insert()
|
||||||
@@ -1085,10 +1087,8 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
pos.is_pos = 1
|
pos.is_pos = 1
|
||||||
pos.update_stock = 1
|
pos.update_stock = 1
|
||||||
|
|
||||||
pos.append(
|
pos.append("payments", {"mode_of_payment": "Bank Draft", "amount": 50})
|
||||||
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - TCP1", "amount": 50}
|
pos.append("payments", {"mode_of_payment": "Cash", "amount": 40})
|
||||||
)
|
|
||||||
pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - TCP1", "amount": 40})
|
|
||||||
|
|
||||||
pos.write_off_outstanding_amount_automatically = 1
|
pos.write_off_outstanding_amount_automatically = 1
|
||||||
pos.insert()
|
pos.insert()
|
||||||
@@ -1102,7 +1102,7 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
|
|
||||||
pos = create_sales_invoice(do_not_save=True)
|
pos = create_sales_invoice(do_not_save=True)
|
||||||
pos.is_pos = 1
|
pos.is_pos = 1
|
||||||
pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100})
|
pos.append("payments", {"mode_of_payment": "Cash", "amount": 100})
|
||||||
pos.save().submit()
|
pos.save().submit()
|
||||||
self.assertEqual(pos.outstanding_amount, 0.0)
|
self.assertEqual(pos.outstanding_amount, 0.0)
|
||||||
self.assertEqual(pos.status, "Paid")
|
self.assertEqual(pos.status, "Paid")
|
||||||
@@ -1173,10 +1173,8 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
for tax in taxes:
|
for tax in taxes:
|
||||||
pos.append("taxes", tax)
|
pos.append("taxes", tax)
|
||||||
|
|
||||||
pos.append(
|
pos.append("payments", {"mode_of_payment": "Bank Draft", "amount": 50})
|
||||||
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - TCP1", "amount": 50}
|
pos.append("payments", {"mode_of_payment": "Cash", "amount": 60})
|
||||||
)
|
|
||||||
pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - TCP1", "amount": 60})
|
|
||||||
|
|
||||||
pos.insert()
|
pos.insert()
|
||||||
pos.submit()
|
pos.submit()
|
||||||
@@ -3904,10 +3902,8 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
pos = create_sales_invoice(qty=10, do_not_save=True)
|
pos = create_sales_invoice(qty=10, do_not_save=True)
|
||||||
pos.is_pos = 1
|
pos.is_pos = 1
|
||||||
pos.pos_profile = pos_profile.name
|
pos.pos_profile = pos_profile.name
|
||||||
pos.append(
|
pos.append("payments", {"mode_of_payment": "Bank Draft", "amount": 500})
|
||||||
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 500}
|
pos.append("payments", {"mode_of_payment": "Cash", "amount": 500})
|
||||||
)
|
|
||||||
pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 500})
|
|
||||||
pos.save().submit()
|
pos.save().submit()
|
||||||
|
|
||||||
pos_return = make_sales_return(pos.name)
|
pos_return = make_sales_return(pos.name)
|
||||||
@@ -4278,7 +4274,7 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
pos.is_pos = 1
|
pos.is_pos = 1
|
||||||
pos.pos_profile = pos_profile.name
|
pos.pos_profile = pos_profile.name
|
||||||
pos.debit_to = "_Test Receivable USD - _TC"
|
pos.debit_to = "_Test Receivable USD - _TC"
|
||||||
pos.append("payments", {"mode_of_payment": "Cash", "account": "_Test Bank - _TC", "amount": 20.35})
|
pos.append("payments", {"mode_of_payment": "Cash", "amount": 20.35})
|
||||||
pos.save().submit()
|
pos.save().submit()
|
||||||
|
|
||||||
pos_return = make_sales_return(pos.name)
|
pos_return = make_sales_return(pos.name)
|
||||||
|
|||||||
@@ -657,34 +657,34 @@ def get_due_date_from_template(template_name, posting_date, bill_date):
|
|||||||
return due_date
|
return due_date
|
||||||
|
|
||||||
|
|
||||||
def validate_due_date(posting_date, due_date, bill_date=None, template_name=None):
|
def validate_due_date(posting_date, due_date, bill_date=None, template_name=None, doctype=None):
|
||||||
if getdate(due_date) < getdate(posting_date):
|
if getdate(due_date) < getdate(posting_date):
|
||||||
frappe.throw(_("Due Date cannot be before Posting / Supplier Invoice Date"))
|
frappe.throw(_("Due Date cannot be before Posting / Supplier Invoice Date"))
|
||||||
else:
|
else:
|
||||||
if not template_name:
|
validate_due_date_with_template(posting_date, due_date, bill_date, template_name, doctype)
|
||||||
return
|
|
||||||
|
|
||||||
default_due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime(
|
|
||||||
"%Y-%m-%d"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not default_due_date:
|
def validate_due_date_with_template(posting_date, due_date, bill_date, template_name, doctype=None):
|
||||||
return
|
if not template_name:
|
||||||
|
return
|
||||||
|
|
||||||
if default_due_date != posting_date and getdate(due_date) > getdate(default_due_date):
|
default_due_date = format(get_due_date_from_template(template_name, posting_date, bill_date))
|
||||||
is_credit_controller = (
|
|
||||||
frappe.db.get_single_value("Accounts Settings", "credit_controller") in frappe.get_roles()
|
if not default_due_date:
|
||||||
|
return
|
||||||
|
|
||||||
|
if default_due_date != posting_date and getdate(due_date) > getdate(default_due_date):
|
||||||
|
if frappe.db.get_single_value("Accounts Settings", "credit_controller") in frappe.get_roles():
|
||||||
|
party_type = "supplier" if doctype == "Purchase Invoice" else "customer"
|
||||||
|
|
||||||
|
msgprint(
|
||||||
|
_("Note: Due Date exceeds allowed {0} credit days by {1} day(s)").format(
|
||||||
|
party_type, date_diff(due_date, default_due_date)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if is_credit_controller:
|
|
||||||
msgprint(
|
else:
|
||||||
_("Note: Due / Reference Date exceeds allowed customer credit days by {0} day(s)").format(
|
frappe.throw(_("Due / Reference Date cannot be after {0}").format(formatdate(default_due_date)))
|
||||||
date_diff(due_date, default_due_date)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
frappe.throw(
|
|
||||||
_("Due / Reference Date cannot be after {0}").format(formatdate(default_due_date))
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@@ -931,12 +931,16 @@ def get_party_shipping_address(doctype: str, name: str) -> str | None:
|
|||||||
["is_shipping_address", "=", 1],
|
["is_shipping_address", "=", 1],
|
||||||
["address_type", "=", "Shipping"],
|
["address_type", "=", "Shipping"],
|
||||||
],
|
],
|
||||||
pluck="name",
|
fields=["name", "is_shipping_address"],
|
||||||
limit=1,
|
|
||||||
order_by="is_shipping_address DESC",
|
order_by="is_shipping_address DESC",
|
||||||
)
|
)
|
||||||
|
|
||||||
return shipping_addresses[0] if shipping_addresses else None
|
if shipping_addresses and shipping_addresses[0].is_shipping_address == 1:
|
||||||
|
return shipping_addresses[0].name
|
||||||
|
if len(shipping_addresses) == 1:
|
||||||
|
return shipping_addresses[0].name
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_partywise_advanced_payment_amount(
|
def get_partywise_advanced_payment_amount(
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ class ReceivablePayableReport:
|
|||||||
self.filters.range = "30, 60, 90, 120"
|
self.filters.range = "30, 60, 90, 120"
|
||||||
self.ranges = [num.strip() for num in self.filters.range.split(",") if num.strip().isdigit()]
|
self.ranges = [num.strip() for num in self.filters.range.split(",") if num.strip().isdigit()]
|
||||||
self.range_numbers = [num for num in range(1, len(self.ranges) + 2)]
|
self.range_numbers = [num for num in range(1, len(self.ranges) + 2)]
|
||||||
|
self.ple_fetch_method = (
|
||||||
|
frappe.db.get_single_value("Accounts Settings", "receivable_payable_fetch_method")
|
||||||
|
or "Buffered Cursor"
|
||||||
|
) # Fail Safe
|
||||||
|
|
||||||
def run(self, args):
|
def run(self, args):
|
||||||
self.filters.update(args)
|
self.filters.update(args)
|
||||||
@@ -90,10 +94,7 @@ class ReceivablePayableReport:
|
|||||||
self.skip_total_row = 1
|
self.skip_total_row = 1
|
||||||
|
|
||||||
def get_data(self):
|
def get_data(self):
|
||||||
self.get_ple_entries()
|
|
||||||
self.get_sales_invoices_or_customers_based_on_sales_person()
|
self.get_sales_invoices_or_customers_based_on_sales_person()
|
||||||
self.voucher_balance = OrderedDict()
|
|
||||||
self.init_voucher_balance() # invoiced, paid, credit_note, outstanding
|
|
||||||
|
|
||||||
# Build delivery note map against all sales invoices
|
# Build delivery note map against all sales invoices
|
||||||
self.build_delivery_note_map()
|
self.build_delivery_note_map()
|
||||||
@@ -110,12 +111,40 @@ class ReceivablePayableReport:
|
|||||||
# Get Exchange Rate Revaluations
|
# Get Exchange Rate Revaluations
|
||||||
self.get_exchange_rate_revaluations()
|
self.get_exchange_rate_revaluations()
|
||||||
|
|
||||||
|
self.prepare_ple_query()
|
||||||
self.data = []
|
self.data = []
|
||||||
|
self.voucher_balance = OrderedDict()
|
||||||
|
|
||||||
|
if self.ple_fetch_method == "Buffered Cursor":
|
||||||
|
self.fetch_ple_in_buffered_cursor()
|
||||||
|
elif self.ple_fetch_method == "UnBuffered Cursor":
|
||||||
|
self.fetch_ple_in_unbuffered_cursor()
|
||||||
|
|
||||||
|
self.build_data()
|
||||||
|
|
||||||
|
def fetch_ple_in_buffered_cursor(self):
|
||||||
|
self.ple_entries = frappe.db.sql(self.ple_query.get_sql(), as_dict=True)
|
||||||
|
|
||||||
|
for ple in self.ple_entries:
|
||||||
|
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
|
||||||
|
|
||||||
|
# This is unavoidable. Initialization and allocation cannot happen in same loop
|
||||||
for ple in self.ple_entries:
|
for ple in self.ple_entries:
|
||||||
self.update_voucher_balance(ple)
|
self.update_voucher_balance(ple)
|
||||||
|
|
||||||
self.build_data()
|
delattr(self, "ple_entries")
|
||||||
|
|
||||||
|
def fetch_ple_in_unbuffered_cursor(self):
|
||||||
|
self.ple_entries = []
|
||||||
|
with frappe.db.unbuffered_cursor():
|
||||||
|
for ple in frappe.db.sql(self.ple_query.get_sql(), as_dict=True, as_iterator=True):
|
||||||
|
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
|
||||||
|
self.ple_entries.append(ple)
|
||||||
|
|
||||||
|
# This is unavoidable. Initialization and allocation cannot happen in same loop
|
||||||
|
for ple in self.ple_entries:
|
||||||
|
self.update_voucher_balance(ple)
|
||||||
|
delattr(self, "ple_entries")
|
||||||
|
|
||||||
def build_voucher_dict(self, ple):
|
def build_voucher_dict(self, ple):
|
||||||
return frappe._dict(
|
return frappe._dict(
|
||||||
@@ -136,26 +165,22 @@ class ReceivablePayableReport:
|
|||||||
outstanding_in_account_currency=0.0,
|
outstanding_in_account_currency=0.0,
|
||||||
)
|
)
|
||||||
|
|
||||||
def init_voucher_balance(self):
|
def init_voucher_balance(self, ple):
|
||||||
# build all keys, since we want to exclude vouchers beyond the report date
|
if self.filters.get("ignore_accounts"):
|
||||||
for ple in self.ple_entries:
|
key = (ple.voucher_type, ple.voucher_no, ple.party)
|
||||||
# get the balance object for voucher_type
|
else:
|
||||||
|
key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
|
||||||
|
|
||||||
if self.filters.get("ignore_accounts"):
|
if key not in self.voucher_balance:
|
||||||
key = (ple.voucher_type, ple.voucher_no, ple.party)
|
self.voucher_balance[key] = self.build_voucher_dict(ple)
|
||||||
else:
|
|
||||||
key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
|
|
||||||
|
|
||||||
if key not in self.voucher_balance:
|
if ple.voucher_type == ple.against_voucher_type and ple.voucher_no == ple.against_voucher_no:
|
||||||
self.voucher_balance[key] = self.build_voucher_dict(ple)
|
self.voucher_balance[key].cost_center = ple.cost_center
|
||||||
|
|
||||||
if ple.voucher_type == ple.against_voucher_type and ple.voucher_no == ple.against_voucher_no:
|
self.get_invoices(ple)
|
||||||
self.voucher_balance[key].cost_center = ple.cost_center
|
|
||||||
|
|
||||||
self.get_invoices(ple)
|
if self.filters.get("group_by_party"):
|
||||||
|
self.init_subtotal_row(ple.party)
|
||||||
if self.filters.get("group_by_party"):
|
|
||||||
self.init_subtotal_row(ple.party)
|
|
||||||
|
|
||||||
if self.filters.get("group_by_party") and not self.filters.get("in_party_currency"):
|
if self.filters.get("group_by_party") and not self.filters.get("in_party_currency"):
|
||||||
self.init_subtotal_row("Total")
|
self.init_subtotal_row("Total")
|
||||||
@@ -778,7 +803,7 @@ class ReceivablePayableReport:
|
|||||||
)
|
)
|
||||||
row["range" + str(index + 1)] = row.outstanding
|
row["range" + str(index + 1)] = row.outstanding
|
||||||
|
|
||||||
def get_ple_entries(self):
|
def prepare_ple_query(self):
|
||||||
# get all the GL entries filtered by the given filters
|
# get all the GL entries filtered by the given filters
|
||||||
|
|
||||||
self.prepare_conditions()
|
self.prepare_conditions()
|
||||||
@@ -831,7 +856,7 @@ class ReceivablePayableReport:
|
|||||||
else:
|
else:
|
||||||
query = query.orderby(self.ple.posting_date, self.ple.party)
|
query = query.orderby(self.ple.posting_date, self.ple.party)
|
||||||
|
|
||||||
self.ple_entries = query.run(as_dict=True)
|
self.ple_query = query
|
||||||
|
|
||||||
def get_sales_invoices_or_customers_based_on_sales_person(self):
|
def get_sales_invoices_or_customers_based_on_sales_person(self):
|
||||||
if self.filters.get("sales_person"):
|
if self.filters.get("sales_person"):
|
||||||
|
|||||||
@@ -144,10 +144,10 @@ class PartyLedgerSummaryReport:
|
|||||||
if self.party_naming_by == "Naming Series":
|
if self.party_naming_by == "Naming Series":
|
||||||
columns.append(
|
columns.append(
|
||||||
{
|
{
|
||||||
"label": _(self.filters.party_type + "Name"),
|
"label": _(self.filters.party_type + " Name"),
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"fieldname": "party_name",
|
"fieldname": "party_name",
|
||||||
"width": 110,
|
"width": 150,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -252,12 +252,13 @@ class PartyLedgerSummaryReport:
|
|||||||
self.party_data = frappe._dict({})
|
self.party_data = frappe._dict({})
|
||||||
for gle in self.gl_entries:
|
for gle in self.gl_entries:
|
||||||
party_details = self.party_details.get(gle.party)
|
party_details = self.party_details.get(gle.party)
|
||||||
|
party_name = party_details.get(f"{scrub(self.filters.party_type)}_name", "")
|
||||||
self.party_data.setdefault(
|
self.party_data.setdefault(
|
||||||
gle.party,
|
gle.party,
|
||||||
frappe._dict(
|
frappe._dict(
|
||||||
{
|
{
|
||||||
**party_details,
|
**party_details,
|
||||||
"party_name": gle.party,
|
"party_name": party_name,
|
||||||
"opening_balance": 0,
|
"opening_balance": 0,
|
||||||
"invoiced_amount": 0,
|
"invoiced_amount": 0,
|
||||||
"paid_amount": 0,
|
"paid_amount": 0,
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ frappe.query_reports["General Ledger"] = {
|
|||||||
label: __("Voucher No"),
|
label: __("Voucher No"),
|
||||||
fieldtype: "Data",
|
fieldtype: "Data",
|
||||||
on_change: function () {
|
on_change: function () {
|
||||||
frappe.query_report.set_filter_value("group_by", "Group by Voucher (Consolidated)");
|
frappe.query_report.set_filter_value("categorize_by", "Categorize by Voucher (Consolidated)");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -112,29 +112,29 @@ frappe.query_reports["General Ledger"] = {
|
|||||||
hidden: 1,
|
hidden: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldname: "group_by",
|
fieldname: "categorize_by",
|
||||||
label: __("Group by"),
|
label: __("Categorize by"),
|
||||||
fieldtype: "Select",
|
fieldtype: "Select",
|
||||||
options: [
|
options: [
|
||||||
"",
|
"",
|
||||||
{
|
{
|
||||||
label: __("Group by Voucher"),
|
label: __("Categorize by Voucher"),
|
||||||
value: "Group by Voucher",
|
value: "Categorize by Voucher",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __("Group by Voucher (Consolidated)"),
|
label: __("Categorize by Voucher (Consolidated)"),
|
||||||
value: "Group by Voucher (Consolidated)",
|
value: "Categorize by Voucher (Consolidated)",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __("Group by Account"),
|
label: __("Categorize by Account"),
|
||||||
value: "Group by Account",
|
value: "Categorize by Account",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __("Group by Party"),
|
label: __("Categorize by Party"),
|
||||||
value: "Group by Party",
|
value: "Categorize by Party",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
default: "Group by Voucher (Consolidated)",
|
default: "Categorize by Voucher (Consolidated)",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldname: "tax_id",
|
fieldname: "tax_id",
|
||||||
|
|||||||
@@ -63,13 +63,17 @@ def validate_filters(filters, account_details):
|
|||||||
if not account_details.get(account):
|
if not account_details.get(account):
|
||||||
frappe.throw(_("Account {0} does not exists").format(account))
|
frappe.throw(_("Account {0} does not exists").format(account))
|
||||||
|
|
||||||
if filters.get("account") and filters.get("group_by") == "Group by Account":
|
if not filters.get("categorize_by") and filters.get("group_by"):
|
||||||
|
filters["categorize_by"] = filters["group_by"]
|
||||||
|
filters["categorize_by"] = filters["categorize_by"].replace("Group by", "Categorize by")
|
||||||
|
|
||||||
|
if filters.get("account") and filters.get("categorize_by") == "Categorize by Account":
|
||||||
filters.account = frappe.parse_json(filters.get("account"))
|
filters.account = frappe.parse_json(filters.get("account"))
|
||||||
for account in filters.account:
|
for account in filters.account:
|
||||||
if account_details[account].is_group == 0:
|
if account_details[account].is_group == 0:
|
||||||
frappe.throw(_("Can not filter based on Child Account, if grouped by Account"))
|
frappe.throw(_("Can not filter based on Child Account, if grouped by Account"))
|
||||||
|
|
||||||
if filters.get("voucher_no") and filters.get("group_by") in ["Group by Voucher"]:
|
if filters.get("voucher_no") and filters.get("categorize_by") in ["Categorize by Voucher"]:
|
||||||
frappe.throw(_("Can not filter based on Voucher No, if grouped by Voucher"))
|
frappe.throw(_("Can not filter based on Voucher No, if grouped by Voucher"))
|
||||||
|
|
||||||
if filters.from_date > filters.to_date:
|
if filters.from_date > filters.to_date:
|
||||||
@@ -163,9 +167,9 @@ def get_gl_entries(filters, accounting_dimensions):
|
|||||||
if filters.get("include_dimensions"):
|
if filters.get("include_dimensions"):
|
||||||
order_by_statement = "order by posting_date, creation"
|
order_by_statement = "order by posting_date, creation"
|
||||||
|
|
||||||
if filters.get("group_by") == "Group by Voucher":
|
if filters.get("categorize_by") == "Categorize by Voucher":
|
||||||
order_by_statement = "order by posting_date, voucher_type, voucher_no"
|
order_by_statement = "order by posting_date, voucher_type, voucher_no"
|
||||||
if filters.get("group_by") == "Group by Account":
|
if filters.get("categorize_by") == "Categorize by Account":
|
||||||
order_by_statement = "order by account, posting_date, creation"
|
order_by_statement = "order by account, posting_date, creation"
|
||||||
|
|
||||||
if filters.get("include_default_book_entries"):
|
if filters.get("include_default_book_entries"):
|
||||||
@@ -260,7 +264,7 @@ def get_conditions(filters):
|
|||||||
if filters.get("voucher_no_not_in"):
|
if filters.get("voucher_no_not_in"):
|
||||||
conditions.append("voucher_no not in %(voucher_no_not_in)s")
|
conditions.append("voucher_no not in %(voucher_no_not_in)s")
|
||||||
|
|
||||||
if filters.get("group_by") == "Group by Party" and not filters.get("party_type"):
|
if filters.get("categorize_by") == "Categorize by Party" and not filters.get("party_type"):
|
||||||
conditions.append("party_type in ('Customer', 'Supplier')")
|
conditions.append("party_type in ('Customer', 'Supplier')")
|
||||||
|
|
||||||
if filters.get("party_type"):
|
if filters.get("party_type"):
|
||||||
@@ -272,7 +276,7 @@ def get_conditions(filters):
|
|||||||
if not (
|
if not (
|
||||||
filters.get("account")
|
filters.get("account")
|
||||||
or filters.get("party")
|
or filters.get("party")
|
||||||
or filters.get("group_by") in ["Group by Account", "Group by Party"]
|
or filters.get("categorize_by") in ["Categorize by Account", "Categorize by Party"]
|
||||||
):
|
):
|
||||||
if not ignore_is_opening:
|
if not ignore_is_opening:
|
||||||
conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')")
|
conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')")
|
||||||
@@ -374,26 +378,26 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension
|
|||||||
# Opening for filtered account
|
# Opening for filtered account
|
||||||
data.append(totals.opening)
|
data.append(totals.opening)
|
||||||
|
|
||||||
if filters.get("group_by") != "Group by Voucher (Consolidated)":
|
if filters.get("categorize_by") != "Categorize by Voucher (Consolidated)":
|
||||||
for _acc, acc_dict in gle_map.items():
|
for _acc, acc_dict in gle_map.items():
|
||||||
# acc
|
# acc
|
||||||
if acc_dict.entries:
|
if acc_dict.entries:
|
||||||
# opening
|
# opening
|
||||||
data.append({"debit_in_transaction_currency": None, "credit_in_transaction_currency": None})
|
data.append({"debit_in_transaction_currency": None, "credit_in_transaction_currency": None})
|
||||||
if (not filters.get("group_by") and not filters.get("voucher_no")) or (
|
if (not filters.get("categorize_by") and not filters.get("voucher_no")) or (
|
||||||
filters.get("group_by") and filters.get("group_by") != "Group by Voucher"
|
filters.get("categorize_by") and filters.get("categorize_by") != "Categorize by Voucher"
|
||||||
):
|
):
|
||||||
data.append(acc_dict.totals.opening)
|
data.append(acc_dict.totals.opening)
|
||||||
|
|
||||||
data += acc_dict.entries
|
data += acc_dict.entries
|
||||||
|
|
||||||
# totals
|
# totals
|
||||||
if filters.get("group_by") or not filters.voucher_no:
|
if filters.get("categorize_by") or not filters.voucher_no:
|
||||||
data.append(acc_dict.totals.total)
|
data.append(acc_dict.totals.total)
|
||||||
|
|
||||||
# closing
|
# closing
|
||||||
if (not filters.get("group_by") and not filters.get("voucher_no")) or (
|
if (not filters.get("categorize_by") and not filters.get("voucher_no")) or (
|
||||||
filters.get("group_by") and filters.get("group_by") != "Group by Voucher"
|
filters.get("categorize_by") and filters.get("categorize_by") != "Categorize by Voucher"
|
||||||
):
|
):
|
||||||
data.append(acc_dict.totals.closing)
|
data.append(acc_dict.totals.closing)
|
||||||
|
|
||||||
@@ -430,9 +434,9 @@ def get_totals_dict():
|
|||||||
|
|
||||||
|
|
||||||
def group_by_field(group_by):
|
def group_by_field(group_by):
|
||||||
if group_by == "Group by Party":
|
if group_by == "Categorize by Party":
|
||||||
return "party"
|
return "party"
|
||||||
elif group_by in ["Group by Voucher (Consolidated)", "Group by Account"]:
|
elif group_by in ["Categorize by Voucher (Consolidated)", "Categorize by Account"]:
|
||||||
return "account"
|
return "account"
|
||||||
else:
|
else:
|
||||||
return "voucher_no"
|
return "voucher_no"
|
||||||
@@ -440,7 +444,7 @@ def group_by_field(group_by):
|
|||||||
|
|
||||||
def initialize_gle_map(gl_entries, filters, totals_dict):
|
def initialize_gle_map(gl_entries, filters, totals_dict):
|
||||||
gle_map = OrderedDict()
|
gle_map = OrderedDict()
|
||||||
group_by = group_by_field(filters.get("group_by"))
|
group_by = group_by_field(filters.get("categorize_by"))
|
||||||
|
|
||||||
for gle in gl_entries:
|
for gle in gl_entries:
|
||||||
gle_map.setdefault(gle.get(group_by), _dict(totals=copy.deepcopy(totals_dict), entries=[]))
|
gle_map.setdefault(gle.get(group_by), _dict(totals=copy.deepcopy(totals_dict), entries=[]))
|
||||||
@@ -450,8 +454,8 @@ def initialize_gle_map(gl_entries, filters, totals_dict):
|
|||||||
def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, totals):
|
def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, totals):
|
||||||
entries = []
|
entries = []
|
||||||
consolidated_gle = OrderedDict()
|
consolidated_gle = OrderedDict()
|
||||||
group_by = group_by_field(filters.get("group_by"))
|
group_by = group_by_field(filters.get("categorize_by"))
|
||||||
group_by_voucher_consolidated = filters.get("group_by") == "Group by Voucher (Consolidated)"
|
group_by_voucher_consolidated = filters.get("categorize_by") == "Categorize by Voucher (Consolidated)"
|
||||||
|
|
||||||
if filters.get("show_net_values_in_party_account"):
|
if filters.get("show_net_values_in_party_account"):
|
||||||
account_type_map = get_account_type_map(filters.get("company"))
|
account_type_map = get_account_type_map(filters.get("company"))
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ class TestGeneralLedger(FrappeTestCase):
|
|||||||
"from_date": today(),
|
"from_date": today(),
|
||||||
"to_date": today(),
|
"to_date": today(),
|
||||||
"account": [account.name],
|
"account": [account.name],
|
||||||
"group_by": "Group by Voucher (Consolidated)",
|
"categorize_by": "Categorize by Voucher (Consolidated)",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -246,7 +246,7 @@ class TestGeneralLedger(FrappeTestCase):
|
|||||||
"from_date": today(),
|
"from_date": today(),
|
||||||
"to_date": today(),
|
"to_date": today(),
|
||||||
"account": [account.name],
|
"account": [account.name],
|
||||||
"group_by": "Group by Voucher (Consolidated)",
|
"categorize_by": "Categorize by Voucher (Consolidated)",
|
||||||
"ignore_err": True,
|
"ignore_err": True,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -261,7 +261,7 @@ class TestGeneralLedger(FrappeTestCase):
|
|||||||
"from_date": today(),
|
"from_date": today(),
|
||||||
"to_date": today(),
|
"to_date": today(),
|
||||||
"account": [account.name],
|
"account": [account.name],
|
||||||
"group_by": "Group by Voucher (Consolidated)",
|
"categorize_by": "Categorize by Voucher (Consolidated)",
|
||||||
"ignore_err": False,
|
"ignore_err": False,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -308,7 +308,7 @@ class TestGeneralLedger(FrappeTestCase):
|
|||||||
"from_date": si.posting_date,
|
"from_date": si.posting_date,
|
||||||
"to_date": si.posting_date,
|
"to_date": si.posting_date,
|
||||||
"account": [si.debit_to],
|
"account": [si.debit_to],
|
||||||
"group_by": "Group by Voucher (Consolidated)",
|
"categorize_by": "Categorize by Voucher (Consolidated)",
|
||||||
"ignore_cr_dr_notes": False,
|
"ignore_cr_dr_notes": False,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -325,7 +325,7 @@ class TestGeneralLedger(FrappeTestCase):
|
|||||||
"from_date": si.posting_date,
|
"from_date": si.posting_date,
|
||||||
"to_date": si.posting_date,
|
"to_date": si.posting_date,
|
||||||
"account": [si.debit_to],
|
"account": [si.debit_to],
|
||||||
"group_by": "Group by Voucher (Consolidated)",
|
"categorize_by": "Categorize by Voucher (Consolidated)",
|
||||||
"ignore_cr_dr_notes": True,
|
"ignore_cr_dr_notes": True,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ DEFAULT_FILTERS = {
|
|||||||
|
|
||||||
|
|
||||||
REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [
|
REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [
|
||||||
("General Ledger", {"group_by": "Group by Voucher (Consolidated)"}),
|
("General Ledger", {"categorize_by": "Categorize by Voucher (Consolidated)"}),
|
||||||
("General Ledger", {"group_by": "Group by Voucher (Consolidated)", "include_dimensions": 1}),
|
("General Ledger", {"categorize_by": "Categorize by Voucher (Consolidated)", "include_dimensions": 1}),
|
||||||
("Accounts Payable", {"range": "30, 60, 90, 120"}),
|
("Accounts Payable", {"range": "30, 60, 90, 120"}),
|
||||||
("Accounts Receivable", {"range": "30, 60, 90, 120"}),
|
("Accounts Receivable", {"range": "30, 60, 90, 120"}),
|
||||||
("Consolidated Financial Statement", {"report": "Balance Sheet"}),
|
("Consolidated Financial Statement", {"report": "Balance Sheet"}),
|
||||||
|
|||||||
@@ -718,16 +718,15 @@ def get_gl_entries_on_asset_disposal(
|
|||||||
|
|
||||||
|
|
||||||
def get_asset_details(asset, finance_book=None):
|
def get_asset_details(asset, finance_book=None):
|
||||||
fixed_asset_account, accumulated_depr_account, _ = get_depreciation_accounts(
|
value_after_depreciation = asset.get_value_after_depreciation(finance_book)
|
||||||
asset.asset_category, asset.company
|
accumulated_depr_amount = flt(asset.gross_purchase_amount) - flt(value_after_depreciation)
|
||||||
|
|
||||||
|
fixed_asset_account, accumulated_depr_account, _ = get_asset_accounts(
|
||||||
|
asset.asset_category, asset.company, accumulated_depr_amount
|
||||||
)
|
)
|
||||||
disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company)
|
disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company)
|
||||||
depreciation_cost_center = asset.cost_center or depreciation_cost_center
|
depreciation_cost_center = asset.cost_center or depreciation_cost_center
|
||||||
|
|
||||||
value_after_depreciation = asset.get_value_after_depreciation(finance_book)
|
|
||||||
|
|
||||||
accumulated_depr_amount = flt(asset.gross_purchase_amount) - flt(value_after_depreciation)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
fixed_asset_account,
|
fixed_asset_account,
|
||||||
asset,
|
asset,
|
||||||
@@ -739,6 +738,48 @@ def get_asset_details(asset, finance_book=None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_asset_accounts(asset_category, company, accumulated_depr_amount):
|
||||||
|
fixed_asset_account = accumulated_depreciation_account = depreciation_expense_account = None
|
||||||
|
|
||||||
|
accounts = frappe.db.get_value(
|
||||||
|
"Asset Category Account",
|
||||||
|
filters={"parent": asset_category, "company_name": company},
|
||||||
|
fieldname=[
|
||||||
|
"fixed_asset_account",
|
||||||
|
"accumulated_depreciation_account",
|
||||||
|
"depreciation_expense_account",
|
||||||
|
],
|
||||||
|
as_dict=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
if accounts:
|
||||||
|
fixed_asset_account = accounts.fixed_asset_account
|
||||||
|
accumulated_depreciation_account = accounts.accumulated_depreciation_account
|
||||||
|
depreciation_expense_account = accounts.depreciation_expense_account
|
||||||
|
|
||||||
|
if not fixed_asset_account:
|
||||||
|
frappe.throw(_("Please set Fixed Asset Account in Asset Category {0}").format(asset_category))
|
||||||
|
|
||||||
|
if accumulated_depr_amount:
|
||||||
|
accounts = frappe.get_cached_value(
|
||||||
|
"Company", company, ["accumulated_depreciation_account", "depreciation_expense_account"]
|
||||||
|
)
|
||||||
|
|
||||||
|
if not accumulated_depreciation_account:
|
||||||
|
accumulated_depreciation_account = accounts[0]
|
||||||
|
if not depreciation_expense_account:
|
||||||
|
depreciation_expense_account = accounts[1]
|
||||||
|
|
||||||
|
if not accumulated_depreciation_account or not depreciation_expense_account:
|
||||||
|
frappe.throw(
|
||||||
|
_("Please set Depreciation related Accounts in Asset Category {0} or Company {1}").format(
|
||||||
|
asset_category, company
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return fixed_asset_account, accumulated_depreciation_account, depreciation_expense_account
|
||||||
|
|
||||||
|
|
||||||
def get_profit_gl_entries(
|
def get_profit_gl_entries(
|
||||||
asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center, date=None
|
asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center, date=None
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -76,14 +76,14 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldname: "group_by",
|
fieldname: "categorize_by",
|
||||||
label: __("Group by"),
|
label: __("Categorize by"),
|
||||||
fieldtype: "Select",
|
fieldtype: "Select",
|
||||||
options: [
|
options: [
|
||||||
{ label: __("Group by Supplier"), value: "Group by Supplier" },
|
{ label: __("Categorize by Supplier"), value: "Categorize by Supplier" },
|
||||||
{ label: __("Group by Item"), value: "Group by Item" },
|
{ label: __("Categorize by Item"), value: "Categorize by Item" },
|
||||||
],
|
],
|
||||||
default: __("Group by Supplier"),
|
default: __("Categorize by Supplier"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldtype: "Check",
|
fieldtype: "Check",
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ def execute(filters=None):
|
|||||||
if not filters:
|
if not filters:
|
||||||
return [], []
|
return [], []
|
||||||
|
|
||||||
|
validate_filters(filters)
|
||||||
|
|
||||||
columns = get_columns(filters)
|
columns = get_columns(filters)
|
||||||
supplier_quotation_data = get_data(filters)
|
supplier_quotation_data = get_data(filters)
|
||||||
|
|
||||||
@@ -24,6 +26,12 @@ def execute(filters=None):
|
|||||||
return columns, data, message, chart_data
|
return columns, data, message, chart_data
|
||||||
|
|
||||||
|
|
||||||
|
def validate_filters(filters):
|
||||||
|
if not filters.get("categorize_by") and filters.get("group_by"):
|
||||||
|
filters["categorize_by"] = filters["group_by"]
|
||||||
|
filters["categorize_by"] = filters["categorize_by"].replace("Group by", "Categorize by")
|
||||||
|
|
||||||
|
|
||||||
def get_data(filters):
|
def get_data(filters):
|
||||||
sq = frappe.qb.DocType("Supplier Quotation")
|
sq = frappe.qb.DocType("Supplier Quotation")
|
||||||
sq_item = frappe.qb.DocType("Supplier Quotation Item")
|
sq_item = frappe.qb.DocType("Supplier Quotation Item")
|
||||||
@@ -82,7 +90,9 @@ def prepare_data(supplier_quotation_data, filters):
|
|||||||
group_wise_map = defaultdict(list)
|
group_wise_map = defaultdict(list)
|
||||||
supplier_qty_price_map = {}
|
supplier_qty_price_map = {}
|
||||||
|
|
||||||
group_by_field = "supplier_name" if filters.get("group_by") == "Group by Supplier" else "item_code"
|
group_by_field = (
|
||||||
|
"supplier_name" if filters.get("categorize_by") == "Categorize by Supplier" else "item_code"
|
||||||
|
)
|
||||||
float_precision = cint(frappe.db.get_default("float_precision")) or 2
|
float_precision = cint(frappe.db.get_default("float_precision")) or 2
|
||||||
|
|
||||||
for data in supplier_quotation_data:
|
for data in supplier_quotation_data:
|
||||||
@@ -266,7 +276,7 @@ def get_columns(filters):
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
if filters.get("group_by") == "Group by Item":
|
if filters.get("categorize_by") == "Categorize by Item":
|
||||||
group_by_columns.reverse()
|
group_by_columns.reverse()
|
||||||
|
|
||||||
columns[0:0] = group_by_columns # add positioned group by columns to the report
|
columns[0:0] = group_by_columns # add positioned group by columns to the report
|
||||||
|
|||||||
@@ -779,17 +779,10 @@ class AccountsController(TransactionBase):
|
|||||||
if not self.due_date:
|
if not self.due_date:
|
||||||
frappe.throw(_("Due Date is mandatory"))
|
frappe.throw(_("Due Date is mandatory"))
|
||||||
|
|
||||||
validate_due_date(
|
validate_due_date(posting_date, self.due_date, None, self.payment_terms_template, self.doctype)
|
||||||
posting_date,
|
|
||||||
self.due_date,
|
|
||||||
self.payment_terms_template,
|
|
||||||
)
|
|
||||||
elif self.doctype == "Purchase Invoice":
|
elif self.doctype == "Purchase Invoice":
|
||||||
validate_due_date(
|
validate_due_date(
|
||||||
posting_date,
|
posting_date, self.due_date, self.bill_date, self.payment_terms_template, self.doctype
|
||||||
self.due_date,
|
|
||||||
self.bill_date,
|
|
||||||
self.payment_terms_template,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_price_list_currency(self, buying_or_selling):
|
def set_price_list_currency(self, buying_or_selling):
|
||||||
|
|||||||
@@ -402,3 +402,6 @@ erpnext.patches.v15_0.recalculate_amount_difference_field #2025-03-18
|
|||||||
erpnext.patches.v15_0.rename_sla_fields #2025-03-12
|
erpnext.patches.v15_0.rename_sla_fields #2025-03-12
|
||||||
erpnext.patches.v15_0.set_purchase_receipt_row_item_to_capitalization_stock_item
|
erpnext.patches.v15_0.set_purchase_receipt_row_item_to_capitalization_stock_item
|
||||||
erpnext.patches.v15_0.update_payment_schedule_fields_in_invoices
|
erpnext.patches.v15_0.update_payment_schedule_fields_in_invoices
|
||||||
|
erpnext.patches.v15_0.rename_group_by_to_categorize_by
|
||||||
|
execute:frappe.db.set_single_value("Accounts Settings", "receivable_payable_fetch_method", "Buffered Cursor")
|
||||||
|
erpnext.patches.v14_0.set_update_price_list_based_on
|
||||||
|
|||||||
14
erpnext/patches/v14_0/set_update_price_list_based_on.py
Normal file
14
erpnext/patches/v14_0/set_update_price_list_based_on.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import frappe
|
||||||
|
from frappe.utils import cint
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.db.set_single_value(
|
||||||
|
"Stock Settings",
|
||||||
|
"update_price_list_based_on",
|
||||||
|
(
|
||||||
|
"Price List Rate"
|
||||||
|
if cint(frappe.db.get_single_value("Selling Settings", "editable_price_list_rate"))
|
||||||
|
else "Rate"
|
||||||
|
),
|
||||||
|
)
|
||||||
20
erpnext/patches/v15_0/rename_group_by_to_categorize_by.py
Normal file
20
erpnext/patches/v15_0/rename_group_by_to_categorize_by.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import frappe
|
||||||
|
from frappe.model.utils.rename_field import rename_field
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
rename_field("Process Statement Of Accounts", "group_by", "categorize_by")
|
||||||
|
|
||||||
|
frappe.db.sql(
|
||||||
|
"""
|
||||||
|
UPDATE
|
||||||
|
`tabProcess Statement Of Accounts`
|
||||||
|
SET
|
||||||
|
categorize_by = CASE
|
||||||
|
WHEN categorize_by = 'Group by Voucher (Consolidated)' THEN 'Categorize by Voucher (Consolidated)'
|
||||||
|
WHEN categorize_by = 'Group by Voucher' THEN 'Categorize by Voucher'
|
||||||
|
END
|
||||||
|
WHERE
|
||||||
|
categorize_by IN ('Group by Voucher (Consolidated)', 'Group by Voucher')
|
||||||
|
"""
|
||||||
|
)
|
||||||
@@ -513,7 +513,6 @@ def get_timesheets_list(doctype, txt, filters, limit_start, limit_page_length=20
|
|||||||
user = frappe.session.user
|
user = frappe.session.user
|
||||||
# find customer name from contact.
|
# find customer name from contact.
|
||||||
customer = ""
|
customer = ""
|
||||||
timesheets = []
|
|
||||||
|
|
||||||
contact = frappe.db.exists("Contact", {"user": user})
|
contact = frappe.db.exists("Contact", {"user": user})
|
||||||
if contact:
|
if contact:
|
||||||
@@ -522,31 +521,43 @@ def get_timesheets_list(doctype, txt, filters, limit_start, limit_page_length=20
|
|||||||
customer = contact.get_link_for("Customer")
|
customer = contact.get_link_for("Customer")
|
||||||
|
|
||||||
if customer:
|
if customer:
|
||||||
sales_invoices = [
|
sales_invoices = frappe.get_all("Sales Invoice", filters={"customer": customer}, pluck="name")
|
||||||
d.name for d in frappe.get_all("Sales Invoice", filters={"customer": customer})
|
projects = frappe.get_all("Project", filters={"customer": customer}, pluck="name")
|
||||||
] or [None]
|
|
||||||
projects = [d.name for d in frappe.get_all("Project", filters={"customer": customer})]
|
|
||||||
# Return timesheet related data to web portal.
|
|
||||||
timesheets = frappe.db.sql(
|
|
||||||
f"""
|
|
||||||
SELECT
|
|
||||||
ts.name, tsd.activity_type, ts.status, ts.total_billable_hours,
|
|
||||||
COALESCE(ts.sales_invoice, tsd.sales_invoice) AS sales_invoice, tsd.project
|
|
||||||
FROM `tabTimesheet` ts, `tabTimesheet Detail` tsd
|
|
||||||
WHERE tsd.parent = ts.name AND
|
|
||||||
(
|
|
||||||
ts.sales_invoice IN %(sales_invoices)s OR
|
|
||||||
tsd.sales_invoice IN %(sales_invoices)s OR
|
|
||||||
tsd.project IN %(projects)s
|
|
||||||
)
|
|
||||||
ORDER BY `end_date` ASC
|
|
||||||
LIMIT {limit_page_length} offset {limit_start}
|
|
||||||
""",
|
|
||||||
dict(sales_invoices=sales_invoices, projects=projects),
|
|
||||||
as_dict=True,
|
|
||||||
) # nosec
|
|
||||||
|
|
||||||
return timesheets
|
# Return timesheet related data to web portal.
|
||||||
|
table = frappe.qb.DocType("Timesheet")
|
||||||
|
child_table = frappe.qb.DocType("Timesheet Detail")
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(table)
|
||||||
|
.join(child_table)
|
||||||
|
.on(table.name == child_table.parent)
|
||||||
|
.select(
|
||||||
|
table.name,
|
||||||
|
child_table.activity_type,
|
||||||
|
table.status,
|
||||||
|
table.total_billable_hours,
|
||||||
|
(table.sales_invoice | child_table.sales_invoice).as_("sales_invoice"),
|
||||||
|
child_table.project,
|
||||||
|
)
|
||||||
|
.orderby(table.end_date)
|
||||||
|
.limit(limit_page_length)
|
||||||
|
.offset(limit_start)
|
||||||
|
)
|
||||||
|
|
||||||
|
conditions = []
|
||||||
|
if sales_invoices:
|
||||||
|
conditions.extend(
|
||||||
|
[table.sales_invoice.isin(sales_invoices), child_table.sales_invoice.isin(sales_invoices)]
|
||||||
|
)
|
||||||
|
if projects:
|
||||||
|
conditions.append(child_table.project.isin(projects))
|
||||||
|
|
||||||
|
if conditions:
|
||||||
|
query = query.where(frappe.qb.terms.Criterion.any(conditions))
|
||||||
|
|
||||||
|
return query.run(as_dict=True)
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def get_list_context(context=None):
|
def get_list_context(context=None):
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ erpnext.stock.StockController = class StockController extends frappe.ui.form.Con
|
|||||||
from_date: me.frm.doc.posting_date,
|
from_date: me.frm.doc.posting_date,
|
||||||
to_date: moment(me.frm.doc.modified).format('YYYY-MM-DD'),
|
to_date: moment(me.frm.doc.modified).format('YYYY-MM-DD'),
|
||||||
company: me.frm.doc.company,
|
company: me.frm.doc.company,
|
||||||
group_by: "Group by Voucher (Consolidated)",
|
categorize_by: "Categorize by Voucher (Consolidated)",
|
||||||
show_cancelled_entries: me.frm.doc.docstatus === 2,
|
show_cancelled_entries: me.frm.doc.docstatus === 2,
|
||||||
ignore_prepared_report: true
|
ignore_prepared_report: true
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1203,9 +1203,9 @@ function set_time_to_resolve_and_response(frm, apply_sla_for_resolution) {
|
|||||||
if (apply_sla_for_resolution) {
|
if (apply_sla_for_resolution) {
|
||||||
let time_to_resolve;
|
let time_to_resolve;
|
||||||
if (!frm.doc.resolution_date) {
|
if (!frm.doc.resolution_date) {
|
||||||
time_to_resolve = get_time_left(frm.doc.resolution_by, frm.doc.agreement_status);
|
time_to_resolve = get_time_left(frm.doc.sla_resolution_by, frm.doc.agreement_status);
|
||||||
} else {
|
} else {
|
||||||
time_to_resolve = get_status(frm.doc.resolution_by, frm.doc.resolution_date);
|
time_to_resolve = get_status(frm.doc.sla_resolution_by, frm.doc.sla_resolution_date);
|
||||||
}
|
}
|
||||||
|
|
||||||
alert += `
|
alert += `
|
||||||
|
|||||||
@@ -850,7 +850,13 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
|||||||
def test_auto_insert_price(self):
|
def test_auto_insert_price(self):
|
||||||
make_item("_Test Item for Auto Price List", {"is_stock_item": 0})
|
make_item("_Test Item for Auto Price List", {"is_stock_item": 0})
|
||||||
make_item("_Test Item for Auto Price List with Discount Percentage", {"is_stock_item": 0})
|
make_item("_Test Item for Auto Price List with Discount Percentage", {"is_stock_item": 0})
|
||||||
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 1)
|
frappe.db.set_single_value(
|
||||||
|
"Stock Settings",
|
||||||
|
{
|
||||||
|
"auto_insert_price_list_rate_if_missing": 1,
|
||||||
|
"update_price_list_based_on": "Price List Rate",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
item_price = frappe.db.get_value(
|
item_price = frappe.db.get_value(
|
||||||
"Item Price", {"price_list": "_Test Price List", "item_code": "_Test Item for Auto Price List"}
|
"Item Price", {"price_list": "_Test Price List", "item_code": "_Test Item for Auto Price List"}
|
||||||
@@ -862,6 +868,7 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
|||||||
item_code="_Test Item for Auto Price List", selling_price_list="_Test Price List", rate=100
|
item_code="_Test Item for Auto Price List", selling_price_list="_Test Price List", rate=100
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ensure price gets inserted based on rate if price list rate is not defined by user
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
frappe.db.get_value(
|
frappe.db.get_value(
|
||||||
"Item Price",
|
"Item Price",
|
||||||
@@ -871,6 +878,8 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
|||||||
100,
|
100,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ensure price gets insterted based on user-defined *Price List Rate*
|
||||||
|
# if update_price_list_based_on is set to Price List Rate
|
||||||
make_sales_order(
|
make_sales_order(
|
||||||
item_code="_Test Item for Auto Price List with Discount Percentage",
|
item_code="_Test Item for Auto Price List with Discount Percentage",
|
||||||
selling_price_list="_Test Price List",
|
selling_price_list="_Test Price List",
|
||||||
@@ -878,18 +887,43 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
|||||||
discount_percentage=20,
|
discount_percentage=20,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(
|
item_price = frappe.db.get_value(
|
||||||
frappe.db.get_value(
|
"Item Price",
|
||||||
"Item Price",
|
{
|
||||||
{
|
"price_list": "_Test Price List",
|
||||||
"price_list": "_Test Price List",
|
"item_code": "_Test Item for Auto Price List with Discount Percentage",
|
||||||
"item_code": "_Test Item for Auto Price List with Discount Percentage",
|
},
|
||||||
},
|
("name", "price_list_rate"),
|
||||||
"price_list_rate",
|
as_dict=True,
|
||||||
),
|
|
||||||
200,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.assertEqual(item_price.price_list_rate, 200)
|
||||||
|
frappe.delete_doc("Item Price", item_price.name)
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Stock Settings", "update_price_list_based_on", "Rate")
|
||||||
|
|
||||||
|
# ensure price gets insterted based on user-defined *Rate*
|
||||||
|
# if update_price_list_based_on is set to Rate
|
||||||
|
make_sales_order(
|
||||||
|
item_code="_Test Item for Auto Price List with Discount Percentage",
|
||||||
|
selling_price_list="_Test Price List",
|
||||||
|
price_list_rate=200,
|
||||||
|
discount_percentage=20,
|
||||||
|
)
|
||||||
|
|
||||||
|
item_price = frappe.db.get_value(
|
||||||
|
"Item Price",
|
||||||
|
{
|
||||||
|
"price_list": "_Test Price List",
|
||||||
|
"item_code": "_Test Item for Auto Price List with Discount Percentage",
|
||||||
|
},
|
||||||
|
("name", "price_list_rate"),
|
||||||
|
as_dict=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(item_price.price_list_rate, 160)
|
||||||
|
frappe.delete_doc("Item Price", item_price.name)
|
||||||
|
|
||||||
# do not update price list
|
# do not update price list
|
||||||
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0)
|
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0)
|
||||||
|
|
||||||
@@ -914,6 +948,63 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
|||||||
|
|
||||||
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 1)
|
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 1)
|
||||||
|
|
||||||
|
def test_update_existing_item_price(self):
|
||||||
|
item_code = "_Test Item for Price List Updation"
|
||||||
|
price_list = "_Test Price List"
|
||||||
|
|
||||||
|
make_item(item_code, {"is_stock_item": 0})
|
||||||
|
|
||||||
|
frappe.db.set_single_value(
|
||||||
|
"Stock Settings",
|
||||||
|
{
|
||||||
|
"auto_insert_price_list_rate_if_missing": 1,
|
||||||
|
"update_existing_price_list_rate": 1,
|
||||||
|
"update_price_list_based_on": "Rate",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# setup: price creation
|
||||||
|
make_sales_order(item_code=item_code, selling_price_list=price_list, rate=100)
|
||||||
|
|
||||||
|
# test price updation based on Rate
|
||||||
|
make_sales_order(item_code=item_code, selling_price_list=price_list, rate=90)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
frappe.db.get_value(
|
||||||
|
"Item Price",
|
||||||
|
{"price_list": price_list, "item_code": item_code},
|
||||||
|
"price_list_rate",
|
||||||
|
),
|
||||||
|
90,
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.db.set_single_value(
|
||||||
|
"Stock Settings",
|
||||||
|
{
|
||||||
|
"update_price_list_based_on": "Price List Rate",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# test price updation based on Price List Rate
|
||||||
|
make_sales_order(
|
||||||
|
item_code=item_code,
|
||||||
|
selling_price_list=price_list,
|
||||||
|
price_list_rate=200,
|
||||||
|
discount_percentage=20,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
frappe.db.get_value(
|
||||||
|
"Item Price",
|
||||||
|
{"price_list": price_list, "item_code": item_code},
|
||||||
|
"price_list_rate",
|
||||||
|
),
|
||||||
|
200,
|
||||||
|
)
|
||||||
|
|
||||||
|
# reset `update_existing_price_list_rate` to 0
|
||||||
|
frappe.db.set_single_value("Stock Settings", "update_existing_price_list_rate", 0)
|
||||||
|
|
||||||
def test_drop_shipping(self):
|
def test_drop_shipping(self):
|
||||||
from erpnext.buying.doctype.purchase_order.purchase_order import update_status
|
from erpnext.buying.doctype.purchase_order.purchase_order import update_status
|
||||||
from erpnext.selling.doctype.sales_order.sales_order import (
|
from erpnext.selling.doctype.sales_order.sales_order import (
|
||||||
|
|||||||
@@ -2,5 +2,7 @@
|
|||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on("Selling Settings", {
|
frappe.ui.form.on("Selling Settings", {
|
||||||
refresh: function (frm) {},
|
after_save(frm) {
|
||||||
|
frappe.boot.user.defaults.editable_price_list_rate = frm.doc.editable_price_list_rate;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ def set_default_settings(args):
|
|||||||
stock_settings.stock_uom = _("Nos")
|
stock_settings.stock_uom = _("Nos")
|
||||||
stock_settings.auto_indent = 1
|
stock_settings.auto_indent = 1
|
||||||
stock_settings.auto_insert_price_list_rate_if_missing = 1
|
stock_settings.auto_insert_price_list_rate_if_missing = 1
|
||||||
|
stock_settings.update_price_list_based_on = "Rate"
|
||||||
stock_settings.set_qty_in_transactions_based_on_serial_no_input = 1
|
stock_settings.set_qty_in_transactions_based_on_serial_no_input = 1
|
||||||
stock_settings.save()
|
stock_settings.save()
|
||||||
|
|
||||||
|
|||||||
@@ -502,6 +502,7 @@ def update_stock_settings():
|
|||||||
stock_settings.stock_uom = _("Nos")
|
stock_settings.stock_uom = _("Nos")
|
||||||
stock_settings.auto_indent = 1
|
stock_settings.auto_indent = 1
|
||||||
stock_settings.auto_insert_price_list_rate_if_missing = 1
|
stock_settings.auto_insert_price_list_rate_if_missing = 1
|
||||||
|
stock_settings.update_price_list_based_on = "Rate"
|
||||||
stock_settings.set_qty_in_transactions_based_on_serial_no_input = 1
|
stock_settings.set_qty_in_transactions_based_on_serial_no_input = 1
|
||||||
stock_settings.save()
|
stock_settings.save()
|
||||||
|
|
||||||
|
|||||||
@@ -223,6 +223,7 @@ def get_batch_qty(
|
|||||||
ignore_voucher_nos=None,
|
ignore_voucher_nos=None,
|
||||||
for_stock_levels=False,
|
for_stock_levels=False,
|
||||||
consider_negative_batches=False,
|
consider_negative_batches=False,
|
||||||
|
do_not_check_future_batches=False,
|
||||||
):
|
):
|
||||||
"""Returns batch actual qty if warehouse is passed,
|
"""Returns batch actual qty if warehouse is passed,
|
||||||
or returns dict of qty by warehouse if warehouse is None
|
or returns dict of qty by warehouse if warehouse is None
|
||||||
@@ -249,6 +250,7 @@ def get_batch_qty(
|
|||||||
"ignore_voucher_nos": ignore_voucher_nos,
|
"ignore_voucher_nos": ignore_voucher_nos,
|
||||||
"for_stock_levels": for_stock_levels,
|
"for_stock_levels": for_stock_levels,
|
||||||
"consider_negative_batches": consider_negative_batches,
|
"consider_negative_batches": consider_negative_batches,
|
||||||
|
"do_not_check_future_batches": do_not_check_future_batches,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,35 @@ const SALES_DOCTYPES = ["Quotation", "Sales Order", "Delivery Note", "Sales Invo
|
|||||||
const PURCHASE_DOCTYPES = ["Purchase Order", "Purchase Receipt", "Purchase Invoice"];
|
const PURCHASE_DOCTYPES = ["Purchase Order", "Purchase Receipt", "Purchase Invoice"];
|
||||||
|
|
||||||
frappe.ui.form.on("Item", {
|
frappe.ui.form.on("Item", {
|
||||||
|
valuation_method(frm) {
|
||||||
|
if (!frm.is_new() && frm.doc.valuation_method === "Moving Average") {
|
||||||
|
let stock_exists = frm.doc.__onload && frm.doc.__onload.stock_exists ? 1 : 0;
|
||||||
|
let current_valuation_method = frm.doc.__onload.current_valuation_method;
|
||||||
|
|
||||||
|
if (stock_exists && current_valuation_method !== frm.doc.valuation_method) {
|
||||||
|
let msg = __(
|
||||||
|
"Changing the valuation method to Moving Average will affect new transactions. If backdated entries are added, earlier FIFO-based entries will be reposted, which may change closing balances."
|
||||||
|
);
|
||||||
|
msg += "<br>";
|
||||||
|
msg += __(
|
||||||
|
"Also you can't switch back to FIFO after setting the valuation method to Moving Average for this item."
|
||||||
|
);
|
||||||
|
msg += "<br>";
|
||||||
|
msg += __("Do you want to change valuation method?");
|
||||||
|
|
||||||
|
frappe.confirm(
|
||||||
|
msg,
|
||||||
|
() => {
|
||||||
|
frm.set_value("valuation_method", "Moving Average");
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
frm.set_value("valuation_method", current_valuation_method);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
setup: function (frm) {
|
setup: function (frm) {
|
||||||
frm.add_fetch("attribute", "numeric_values", "numeric_values");
|
frm.add_fetch("attribute", "numeric_values", "numeric_values");
|
||||||
frm.add_fetch("attribute", "from_range", "from_range");
|
frm.add_fetch("attribute", "from_range", "from_range");
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ from erpnext.controllers.item_variant import (
|
|||||||
validate_item_variant_attributes,
|
validate_item_variant_attributes,
|
||||||
)
|
)
|
||||||
from erpnext.stock.doctype.item_default.item_default import ItemDefault
|
from erpnext.stock.doctype.item_default.item_default import ItemDefault
|
||||||
|
from erpnext.stock.utils import get_valuation_method
|
||||||
|
|
||||||
|
|
||||||
class DuplicateReorderRows(frappe.ValidationError):
|
class DuplicateReorderRows(frappe.ValidationError):
|
||||||
@@ -153,6 +154,7 @@ class Item(Document):
|
|||||||
def onload(self):
|
def onload(self):
|
||||||
self.set_onload("stock_exists", self.stock_ledger_created())
|
self.set_onload("stock_exists", self.stock_ledger_created())
|
||||||
self.set_onload("asset_naming_series", get_asset_naming_series())
|
self.set_onload("asset_naming_series", get_asset_naming_series())
|
||||||
|
self.set_onload("current_valuation_method", get_valuation_method(self.name))
|
||||||
|
|
||||||
def autoname(self):
|
def autoname(self):
|
||||||
if frappe.db.get_default("item_naming_by") == "Naming Series":
|
if frappe.db.get_default("item_naming_by") == "Naming Series":
|
||||||
|
|||||||
@@ -462,6 +462,7 @@ frappe.ui.form.on("Stock Entry", {
|
|||||||
docstatus: 1,
|
docstatus: 1,
|
||||||
purpose: "Material Transfer",
|
purpose: "Material Transfer",
|
||||||
add_to_transit: 1,
|
add_to_transit: 1,
|
||||||
|
per_transferred: ["<", 100],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -503,17 +503,29 @@ class StockEntry(StockController):
|
|||||||
).format(frappe.bold(self.company))
|
).format(frappe.bold(self.company))
|
||||||
)
|
)
|
||||||
|
|
||||||
elif (
|
acc_details = frappe.get_cached_value(
|
||||||
self.is_opening == "Yes"
|
"Account",
|
||||||
and frappe.db.get_value("Account", d.expense_account, "report_type") == "Profit and Loss"
|
d.expense_account,
|
||||||
):
|
["account_type", "report_type"],
|
||||||
|
as_dict=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.is_opening == "Yes" and acc_details.report_type == "Profit and Loss":
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_(
|
_(
|
||||||
"Difference Account must be a Asset/Liability type account, since this Stock Entry is an Opening Entry"
|
"Difference Account must be a Asset/Liability type account (Temporary Opening), since this Stock Entry is an Opening Entry"
|
||||||
),
|
),
|
||||||
OpeningEntryAccountError,
|
OpeningEntryAccountError,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if acc_details.account_type == "Stock":
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"At row {0}: the Difference Account must not be a Stock type account, please change the Account Type for the account {1} or select a different account"
|
||||||
|
).format(d.idx, get_link_to_form("Account", d.expense_account)),
|
||||||
|
OpeningEntryAccountError,
|
||||||
|
)
|
||||||
|
|
||||||
def validate_warehouse(self):
|
def validate_warehouse(self):
|
||||||
"""perform various (sometimes conditional) validations on warehouse"""
|
"""perform various (sometimes conditional) validations on warehouse"""
|
||||||
|
|
||||||
|
|||||||
@@ -1314,7 +1314,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
|
|||||||
# To deliver 100 qty we fall short of 11.0073 qty (11.007 with precision 3)
|
# To deliver 100 qty we fall short of 11.0073 qty (11.007 with precision 3)
|
||||||
# Stock up with 11.007 (balance in db becomes 99.9997, on UI it will show as 100)
|
# Stock up with 11.007 (balance in db becomes 99.9997, on UI it will show as 100)
|
||||||
make_stock_entry(item_code=item_code, target=warehouse, qty=11.007, rate=100)
|
make_stock_entry(item_code=item_code, target=warehouse, qty=11.007, rate=100)
|
||||||
self.assertEqual(get_stock_balance(item_code, warehouse), 99.9997)
|
self.assertEqual(get_stock_balance(item_code, warehouse), 100.0)
|
||||||
|
|
||||||
# See if delivery note goes through
|
# See if delivery note goes through
|
||||||
# Negative qty error should not be raised as 99.9997 is 100 with precision 3 (system precision)
|
# Negative qty error should not be raised as 99.9997 is 100 with precision 3 (system precision)
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _, bold, json, msgprint
|
from frappe import _, bold, json, msgprint
|
||||||
from frappe.query_builder.functions import CombineDatetime, Sum
|
from frappe.query_builder.functions import CombineDatetime, Sum
|
||||||
from frappe.utils import add_to_date, cint, cstr, flt
|
from frappe.utils import add_to_date, cint, cstr, flt, get_datetime
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.accounts.utils import get_company_default
|
from erpnext.accounts.utils import get_company_default
|
||||||
from erpnext.controllers.stock_controller import StockController
|
from erpnext.controllers.stock_controller import StockController, create_repost_item_valuation_entry
|
||||||
from erpnext.stock.doctype.batch.batch import get_available_batches, get_batch_qty
|
from erpnext.stock.doctype.batch.batch import get_available_batches, get_batch_qty
|
||||||
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
|
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
|
||||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||||
@@ -186,9 +186,35 @@ class StockReconciliation(StockController):
|
|||||||
|
|
||||||
if not item.reconcile_all_serial_batch and item.serial_and_batch_bundle:
|
if not item.reconcile_all_serial_batch and item.serial_and_batch_bundle:
|
||||||
bundle = self.get_bundle_for_specific_serial_batch(item)
|
bundle = self.get_bundle_for_specific_serial_batch(item)
|
||||||
|
if not bundle:
|
||||||
|
continue
|
||||||
|
|
||||||
item.current_serial_and_batch_bundle = bundle.name
|
item.current_serial_and_batch_bundle = bundle.name
|
||||||
item.current_valuation_rate = abs(bundle.avg_rate)
|
item.current_valuation_rate = abs(bundle.avg_rate)
|
||||||
|
|
||||||
|
if bundle.total_qty:
|
||||||
|
item.current_qty = abs(bundle.total_qty)
|
||||||
|
|
||||||
|
if save:
|
||||||
|
if not item.current_qty:
|
||||||
|
frappe.throw(
|
||||||
|
_("Row # {0}: Please enter quantity for Item {1} as it is not zero.").format(
|
||||||
|
item.idx, item.item_code
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.docstatus == 1:
|
||||||
|
bundle.voucher_no = self.name
|
||||||
|
bundle.submit()
|
||||||
|
|
||||||
|
item.db_set(
|
||||||
|
{
|
||||||
|
"current_serial_and_batch_bundle": item.current_serial_and_batch_bundle,
|
||||||
|
"current_qty": item.current_qty,
|
||||||
|
"current_valuation_rate": item.current_valuation_rate,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if not item.valuation_rate:
|
if not item.valuation_rate:
|
||||||
item.valuation_rate = item.current_valuation_rate
|
item.valuation_rate = item.current_valuation_rate
|
||||||
continue
|
continue
|
||||||
@@ -333,20 +359,26 @@ class StockReconciliation(StockController):
|
|||||||
entry.batch_no,
|
entry.batch_no,
|
||||||
row.warehouse,
|
row.warehouse,
|
||||||
row.item_code,
|
row.item_code,
|
||||||
|
ignore_voucher_nos=[self.name],
|
||||||
posting_date=self.posting_date,
|
posting_date=self.posting_date,
|
||||||
posting_time=self.posting_time,
|
posting_time=self.posting_time,
|
||||||
for_stock_levels=True,
|
for_stock_levels=True,
|
||||||
consider_negative_batches=True,
|
consider_negative_batches=True,
|
||||||
|
do_not_check_future_batches=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not current_qty:
|
||||||
|
continue
|
||||||
|
|
||||||
total_current_qty += current_qty
|
total_current_qty += current_qty
|
||||||
entry.qty = current_qty * -1
|
entry.qty = current_qty * -1
|
||||||
|
|
||||||
reco_obj.save()
|
if total_current_qty:
|
||||||
|
reco_obj.save()
|
||||||
|
|
||||||
row.current_qty = total_current_qty
|
row.current_qty = total_current_qty
|
||||||
|
|
||||||
return reco_obj
|
return reco_obj
|
||||||
|
|
||||||
def has_change_in_serial_batch(self, row) -> bool:
|
def has_change_in_serial_batch(self, row) -> bool:
|
||||||
bundles = {row.serial_and_batch_bundle: [], row.current_serial_and_batch_bundle: []}
|
bundles = {row.serial_and_batch_bundle: [], row.current_serial_and_batch_bundle: []}
|
||||||
@@ -968,7 +1000,7 @@ class StockReconciliation(StockController):
|
|||||||
else:
|
else:
|
||||||
self._cancel()
|
self._cancel()
|
||||||
|
|
||||||
def recalculate_current_qty(self, voucher_detail_no):
|
def recalculate_current_qty(self, voucher_detail_no, sle_creation, add_new_sle=False):
|
||||||
from erpnext.stock.stock_ledger import get_valuation_rate
|
from erpnext.stock.stock_ledger import get_valuation_rate
|
||||||
|
|
||||||
for row in self.items:
|
for row in self.items:
|
||||||
@@ -1036,6 +1068,49 @@ class StockReconciliation(StockController):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
add_new_sle
|
||||||
|
and not frappe.db.get_value(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
{"voucher_detail_no": row.name, "actual_qty": ("<", 0), "is_cancelled": 0},
|
||||||
|
"name",
|
||||||
|
)
|
||||||
|
and not row.current_serial_and_batch_bundle
|
||||||
|
):
|
||||||
|
self.set_current_serial_and_batch_bundle(voucher_detail_no, save=True)
|
||||||
|
row.reload()
|
||||||
|
|
||||||
|
self.add_missing_stock_ledger_entry(row, voucher_detail_no, sle_creation)
|
||||||
|
|
||||||
|
def add_missing_stock_ledger_entry(self, row, voucher_detail_no, sle_creation):
|
||||||
|
if row.current_qty == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
new_sle = frappe.get_doc(self.get_sle_for_items(row))
|
||||||
|
new_sle.actual_qty = row.current_qty * -1
|
||||||
|
new_sle.valuation_rate = row.current_valuation_rate
|
||||||
|
new_sle.serial_and_batch_bundle = row.current_serial_and_batch_bundle
|
||||||
|
new_sle.submit()
|
||||||
|
|
||||||
|
creation = add_to_date(sle_creation, seconds=-1)
|
||||||
|
new_sle.db_set("creation", creation)
|
||||||
|
|
||||||
|
if not frappe.db.exists(
|
||||||
|
"Repost Item Valuation",
|
||||||
|
{"item": row.item_code, "warehouse": row.warehouse, "docstatus": 1, "status": "Queued"},
|
||||||
|
):
|
||||||
|
create_repost_item_valuation_entry(
|
||||||
|
{
|
||||||
|
"based_on": "Item and Warehouse",
|
||||||
|
"item_code": row.item_code,
|
||||||
|
"warehouse": row.warehouse,
|
||||||
|
"company": self.company,
|
||||||
|
"allow_negative_stock": 1,
|
||||||
|
"posting_date": self.posting_date,
|
||||||
|
"posting_time": self.posting_time,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def has_negative_stock_allowed(self):
|
def has_negative_stock_allowed(self):
|
||||||
allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
|
allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
|
||||||
if allow_negative_stock:
|
if allow_negative_stock:
|
||||||
@@ -1109,6 +1184,7 @@ class StockReconciliation(StockController):
|
|||||||
ignore_voucher_nos=[doc.voucher_no],
|
ignore_voucher_nos=[doc.voucher_no],
|
||||||
for_stock_levels=True,
|
for_stock_levels=True,
|
||||||
consider_negative_batches=True,
|
consider_negative_batches=True,
|
||||||
|
do_not_check_future_batches=True,
|
||||||
)
|
)
|
||||||
or 0
|
or 0
|
||||||
) * -1
|
) * -1
|
||||||
|
|||||||
@@ -1069,7 +1069,7 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
|||||||
|
|
||||||
sr.reload()
|
sr.reload()
|
||||||
self.assertTrue(sr.items[0].serial_and_batch_bundle)
|
self.assertTrue(sr.items[0].serial_and_batch_bundle)
|
||||||
self.assertFalse(sr.items[0].current_serial_and_batch_bundle)
|
self.assertTrue(sr.items[0].current_serial_and_batch_bundle)
|
||||||
|
|
||||||
def test_not_reconcile_all_batch(self):
|
def test_not_reconcile_all_batch(self):
|
||||||
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||||
@@ -1446,6 +1446,74 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
|||||||
self.assertEqual(sr.difference_amount, 100 * -1)
|
self.assertEqual(sr.difference_amount, 100 * -1)
|
||||||
self.assertTrue(sr.items[0].qty == 0)
|
self.assertTrue(sr.items[0].qty == 0)
|
||||||
|
|
||||||
|
def test_stock_reco_recalculate_qty_for_backdated_entry(self):
|
||||||
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||||
|
|
||||||
|
item_code = self.make_item(
|
||||||
|
"Test Batch Item Stock Reco Recalculate Qty",
|
||||||
|
{
|
||||||
|
"is_stock_item": 1,
|
||||||
|
"has_batch_no": 1,
|
||||||
|
"create_new_batch": 1,
|
||||||
|
"batch_number_series": "TEST-BATCH-RRQ-.###",
|
||||||
|
},
|
||||||
|
).name
|
||||||
|
|
||||||
|
warehouse = "_Test Warehouse - _TC"
|
||||||
|
|
||||||
|
sr = create_stock_reconciliation(
|
||||||
|
item_code=item_code,
|
||||||
|
warehouse=warehouse,
|
||||||
|
qty=10,
|
||||||
|
rate=100,
|
||||||
|
use_serial_batch_fields=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
sr.reload()
|
||||||
|
self.assertEqual(sr.items[0].current_qty, 0)
|
||||||
|
self.assertEqual(sr.items[0].current_valuation_rate, 0)
|
||||||
|
|
||||||
|
batch_no = get_batch_from_bundle(sr.items[0].serial_and_batch_bundle)
|
||||||
|
stock_ledgers = frappe.get_all(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
filters={"voucher_no": sr.name, "is_cancelled": 0},
|
||||||
|
pluck="name",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(len(stock_ledgers) == 1)
|
||||||
|
|
||||||
|
make_stock_entry(
|
||||||
|
item_code=item_code,
|
||||||
|
target=warehouse,
|
||||||
|
qty=10,
|
||||||
|
basic_rate=100,
|
||||||
|
use_serial_batch_fields=1,
|
||||||
|
batch_no=batch_no,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make backdated stock reconciliation entry
|
||||||
|
create_stock_reconciliation(
|
||||||
|
item_code=item_code,
|
||||||
|
warehouse=warehouse,
|
||||||
|
qty=10,
|
||||||
|
rate=100,
|
||||||
|
use_serial_batch_fields=1,
|
||||||
|
batch_no=batch_no,
|
||||||
|
posting_date=add_days(nowdate(), -1),
|
||||||
|
)
|
||||||
|
|
||||||
|
stock_ledgers = frappe.get_all(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
filters={"voucher_no": sr.name, "is_cancelled": 0},
|
||||||
|
pluck="name",
|
||||||
|
)
|
||||||
|
|
||||||
|
sr.reload()
|
||||||
|
self.assertEqual(sr.items[0].current_qty, 10)
|
||||||
|
self.assertEqual(sr.items[0].current_valuation_rate, 100)
|
||||||
|
|
||||||
|
self.assertTrue(len(stock_ledgers) == 2)
|
||||||
|
|
||||||
|
|
||||||
def create_batch_item_with_batch(item_name, batch_id):
|
def create_batch_item_with_batch(item_name, batch_id):
|
||||||
batch_item_doc = create_item(item_name, is_stock_item=1)
|
batch_item_doc = create_item(item_name, is_stock_item=1)
|
||||||
|
|||||||
@@ -51,4 +51,30 @@ frappe.ui.form.on("Stock Settings", {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
auto_insert_price_list_rate_if_missing(frm) {
|
||||||
|
if (!frm.doc.auto_insert_price_list_rate_if_missing) return;
|
||||||
|
|
||||||
|
frm.set_value(
|
||||||
|
"update_price_list_based_on",
|
||||||
|
cint(frappe.defaults.get_default("editable_price_list_rate")) ? "Price List Rate" : "Rate"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
update_price_list_based_on(frm) {
|
||||||
|
if (
|
||||||
|
frm.doc.update_price_list_based_on === "Price List Rate" &&
|
||||||
|
!cint(frappe.defaults.get_default("editable_price_list_rate"))
|
||||||
|
) {
|
||||||
|
const dialog = frappe.warn(
|
||||||
|
__("Incompatible Setting Detected"),
|
||||||
|
__(
|
||||||
|
"<p>Price List Rate has not been set as editable in Selling Settings. In this scenario, setting <strong>Update Price List Based On</strong> to <strong>Price List Rate</strong> will prevent auto-updation of Item Price.</p>Are you sure you want to continue?"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
dialog.set_secondary_action(() => {
|
||||||
|
frm.set_value("update_price_list_based_on", "Rate");
|
||||||
|
dialog.hide();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"stock_uom",
|
"stock_uom",
|
||||||
"price_list_defaults_section",
|
"price_list_defaults_section",
|
||||||
"auto_insert_price_list_rate_if_missing",
|
"auto_insert_price_list_rate_if_missing",
|
||||||
|
"update_price_list_based_on",
|
||||||
"column_break_12",
|
"column_break_12",
|
||||||
"update_existing_price_list_rate",
|
"update_existing_price_list_rate",
|
||||||
"conversion_factor_section",
|
"conversion_factor_section",
|
||||||
@@ -531,6 +532,15 @@
|
|||||||
"fieldname": "allow_to_make_quality_inspection_after_purchase_or_delivery",
|
"fieldname": "allow_to_make_quality_inspection_after_purchase_or_delivery",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Allow to Make Quality Inspection after Purchase / Delivery"
|
"label": "Allow to Make Quality Inspection after Purchase / Delivery"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "Rate",
|
||||||
|
"depends_on": "eval: doc.auto_insert_price_list_rate_if_missing",
|
||||||
|
"fieldname": "update_price_list_based_on",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Update Price List Based On",
|
||||||
|
"mandatory_depends_on": "eval: doc.auto_insert_price_list_rate_if_missing",
|
||||||
|
"options": "Rate\nPrice List Rate"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "icon-cog",
|
"icon": "icon-cog",
|
||||||
@@ -538,7 +548,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-04-11 18:56:35.781929",
|
"modified": "2025-05-06 02:39:24.284587",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Settings",
|
"name": "Stock Settings",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ class StockSettings(Document):
|
|||||||
stock_frozen_upto_days: DF.Int
|
stock_frozen_upto_days: DF.Int
|
||||||
stock_uom: DF.Link | None
|
stock_uom: DF.Link | None
|
||||||
update_existing_price_list_rate: DF.Check
|
update_existing_price_list_rate: DF.Check
|
||||||
|
update_price_list_based_on: DF.Literal["Rate", "Price List Rate"]
|
||||||
use_naming_series: DF.Check
|
use_naming_series: DF.Check
|
||||||
use_serial_batch_fields: DF.Check
|
use_serial_batch_fields: DF.Check
|
||||||
valuation_method: DF.Literal["FIFO", "Moving Average", "LIFO"]
|
valuation_method: DF.Literal["FIFO", "Moving Average", "LIFO"]
|
||||||
|
|||||||
@@ -911,8 +911,8 @@ def get_price_list_rate(args, item_doc, out=None):
|
|||||||
price_list_rate = get_price_list_rate_for(args, item_doc.variant_of)
|
price_list_rate = get_price_list_rate_for(args, item_doc.variant_of)
|
||||||
|
|
||||||
# insert in database
|
# insert in database
|
||||||
if price_list_rate is None or frappe.db.get_single_value(
|
if price_list_rate is None or frappe.get_cached_value(
|
||||||
"Stock Settings", "update_existing_price_list_rate"
|
"Stock Settings", "Stock Settings", "update_existing_price_list_rate"
|
||||||
):
|
):
|
||||||
insert_item_price(args)
|
insert_item_price(args)
|
||||||
|
|
||||||
@@ -946,54 +946,71 @@ def insert_item_price(args):
|
|||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
if frappe.db.get_value("Price List", args.price_list, "currency", cache=True) == args.currency and cint(
|
stock_settings = frappe.get_cached_doc("Stock Settings")
|
||||||
frappe.db.get_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing")
|
|
||||||
):
|
|
||||||
if frappe.has_permission("Item Price", "write"):
|
|
||||||
price_list_rate = (
|
|
||||||
(flt(args.rate) + flt(args.discount_amount)) / args.get("conversion_factor")
|
|
||||||
if args.get("conversion_factor")
|
|
||||||
else (flt(args.rate) + flt(args.discount_amount))
|
|
||||||
)
|
|
||||||
|
|
||||||
item_price = frappe.db.get_value(
|
if (
|
||||||
"Item Price",
|
not frappe.db.get_value("Price List", args.price_list, "currency", cache=True) == args.currency
|
||||||
{
|
or not stock_settings.auto_insert_price_list_rate_if_missing
|
||||||
"item_code": args.item_code,
|
or not frappe.has_permission("Item Price", "write")
|
||||||
"price_list": args.price_list,
|
):
|
||||||
"currency": args.currency,
|
return
|
||||||
"uom": args.stock_uom,
|
|
||||||
},
|
item_price = frappe.db.get_value(
|
||||||
["name", "price_list_rate"],
|
"Item Price",
|
||||||
as_dict=1,
|
{
|
||||||
)
|
"item_code": args.item_code,
|
||||||
if item_price and item_price.name:
|
"price_list": args.price_list,
|
||||||
if item_price.price_list_rate != price_list_rate and frappe.db.get_single_value(
|
"currency": args.currency,
|
||||||
"Stock Settings", "update_existing_price_list_rate"
|
"uom": args.stock_uom,
|
||||||
):
|
},
|
||||||
frappe.db.set_value("Item Price", item_price.name, "price_list_rate", price_list_rate)
|
["name", "price_list_rate"],
|
||||||
frappe.msgprint(
|
as_dict=1,
|
||||||
_("Item Price updated for {0} in Price List {1}").format(
|
)
|
||||||
args.item_code, args.price_list
|
|
||||||
),
|
update_based_on_price_list_rate = stock_settings.update_price_list_based_on == "Price List Rate"
|
||||||
alert=True,
|
|
||||||
)
|
if item_price and item_price.name:
|
||||||
else:
|
if not stock_settings.update_existing_price_list_rate:
|
||||||
item_price = frappe.get_doc(
|
return
|
||||||
{
|
|
||||||
"doctype": "Item Price",
|
rate_to_consider = flt(args.price_list_rate) if update_based_on_price_list_rate else flt(args.rate)
|
||||||
"price_list": args.price_list,
|
price_list_rate = _get_stock_uom_rate(rate_to_consider, args)
|
||||||
"item_code": args.item_code,
|
|
||||||
"currency": args.currency,
|
if not price_list_rate or item_price.price_list_rate == price_list_rate:
|
||||||
"price_list_rate": price_list_rate,
|
return
|
||||||
"uom": args.stock_uom,
|
|
||||||
}
|
frappe.db.set_value("Item Price", item_price.name, "price_list_rate", price_list_rate)
|
||||||
)
|
frappe.msgprint(
|
||||||
item_price.insert()
|
_("Item Price updated for {0} in Price List {1}").format(args.item_code, args.price_list),
|
||||||
frappe.msgprint(
|
alert=True,
|
||||||
_("Item Price added for {0} in Price List {1}").format(args.item_code, args.price_list),
|
)
|
||||||
alert=True,
|
else:
|
||||||
)
|
rate_to_consider = (
|
||||||
|
(flt(args.price_list_rate) or flt(args.rate))
|
||||||
|
if update_based_on_price_list_rate
|
||||||
|
else flt(args.rate)
|
||||||
|
)
|
||||||
|
price_list_rate = _get_stock_uom_rate(rate_to_consider, args)
|
||||||
|
|
||||||
|
item_price = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Item Price",
|
||||||
|
"price_list": args.price_list,
|
||||||
|
"item_code": args.item_code,
|
||||||
|
"currency": args.currency,
|
||||||
|
"price_list_rate": price_list_rate,
|
||||||
|
"uom": args.stock_uom,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
item_price.insert()
|
||||||
|
frappe.msgprint(
|
||||||
|
_("Item Price added for {0} in Price List {1}").format(args.item_code, args.price_list),
|
||||||
|
alert=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_stock_uom_rate(rate, args):
|
||||||
|
return rate / args.conversion_factor if args.conversion_factor else rate
|
||||||
|
|
||||||
|
|
||||||
def get_item_price(args, item_code, ignore_party=False, force_batch_no=False) -> list[dict]:
|
def get_item_price(args, item_code, ignore_party=False, force_batch_no=False) -> list[dict]:
|
||||||
|
|||||||
@@ -743,6 +743,9 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
|
|||||||
if not self.sle.actual_qty:
|
if not self.sle.actual_qty:
|
||||||
self.sle.actual_qty = self.get_actual_qty()
|
self.sle.actual_qty = self.get_actual_qty()
|
||||||
|
|
||||||
|
if not self.sle.actual_qty:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
return abs(flt(self.stock_value_change) / flt(self.sle.actual_qty))
|
return abs(flt(self.stock_value_change) / flt(self.sle.actual_qty))
|
||||||
|
|
||||||
def get_actual_qty(self):
|
def get_actual_qty(self):
|
||||||
|
|||||||
@@ -894,7 +894,7 @@ class update_entries_after:
|
|||||||
self.wh_data.prev_stock_value = self.wh_data.stock_value
|
self.wh_data.prev_stock_value = self.wh_data.stock_value
|
||||||
|
|
||||||
# update current sle
|
# update current sle
|
||||||
sle.qty_after_transaction = self.wh_data.qty_after_transaction
|
sle.qty_after_transaction = flt(self.wh_data.qty_after_transaction, self.flt_precision)
|
||||||
sle.valuation_rate = self.wh_data.valuation_rate
|
sle.valuation_rate = self.wh_data.valuation_rate
|
||||||
sle.stock_value = self.wh_data.stock_value
|
sle.stock_value = self.wh_data.stock_value
|
||||||
sle.stock_queue = json.dumps(self.wh_data.stock_queue)
|
sle.stock_queue = json.dumps(self.wh_data.stock_queue)
|
||||||
@@ -962,7 +962,7 @@ class update_entries_after:
|
|||||||
|
|
||||||
def reset_actual_qty_for_stock_reco(self, sle):
|
def reset_actual_qty_for_stock_reco(self, sle):
|
||||||
doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no)
|
doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no)
|
||||||
doc.recalculate_current_qty(sle.voucher_detail_no)
|
doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0)
|
||||||
|
|
||||||
if sle.actual_qty < 0:
|
if sle.actual_qty < 0:
|
||||||
sle.actual_qty = (
|
sle.actual_qty = (
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ frappe.ui.form.on("Subcontracting Receipt", {
|
|||||||
from_date: frm.doc.posting_date,
|
from_date: frm.doc.posting_date,
|
||||||
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
|
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
|
||||||
company: frm.doc.company,
|
company: frm.doc.company,
|
||||||
group_by: "Group by Voucher (Consolidated)",
|
categorize_by: "Categorize by Voucher (Consolidated)",
|
||||||
show_cancelled_entries: frm.doc.docstatus === 2,
|
show_cancelled_entries: frm.doc.docstatus === 2,
|
||||||
};
|
};
|
||||||
frappe.set_route("query-report", "General Ledger");
|
frappe.set_route("query-report", "General Ledger");
|
||||||
|
|||||||
Reference in New Issue
Block a user