mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-01 03:09:09 +00:00
Merge pull request #55307 from frappe/version-16-hotfix
chore: release v16
This commit is contained in:
@@ -1,126 +0,0 @@
|
||||
{
|
||||
"custom_fields": [
|
||||
{
|
||||
"_assign": null,
|
||||
"_comments": null,
|
||||
"_liked_by": null,
|
||||
"_user_tags": null,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"collapsible_depends_on": null,
|
||||
"columns": 0,
|
||||
"creation": "2018-12-28 22:29:21.828090",
|
||||
"default": null,
|
||||
"depends_on": null,
|
||||
"description": null,
|
||||
"docstatus": 0,
|
||||
"dt": "Address",
|
||||
"fetch_from": null,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "tax_category",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"hide_border": 0,
|
||||
"hide_days": 0,
|
||||
"hide_seconds": 0,
|
||||
"idx": 15,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_preview": 0,
|
||||
"in_standard_filter": 0,
|
||||
"insert_after": "fax",
|
||||
"label": "Tax Category",
|
||||
"length": 0,
|
||||
"mandatory_depends_on": null,
|
||||
"modified": "2018-12-28 22:29:21.828090",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Address-tax_category",
|
||||
"no_copy": 0,
|
||||
"options": "Tax Category",
|
||||
"owner": "Administrator",
|
||||
"parent": null,
|
||||
"parentfield": null,
|
||||
"parenttype": null,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": null,
|
||||
"read_only": 0,
|
||||
"read_only_depends_on": null,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0,
|
||||
"width": null
|
||||
},
|
||||
{
|
||||
"_assign": null,
|
||||
"_comments": null,
|
||||
"_liked_by": null,
|
||||
"_user_tags": null,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"collapsible_depends_on": null,
|
||||
"columns": 0,
|
||||
"creation": "2020-10-14 17:41:40.878179",
|
||||
"default": "0",
|
||||
"depends_on": null,
|
||||
"description": null,
|
||||
"docstatus": 0,
|
||||
"dt": "Address",
|
||||
"fetch_from": null,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "is_your_company_address",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"hide_border": 0,
|
||||
"hide_days": 0,
|
||||
"hide_seconds": 0,
|
||||
"idx": 20,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_preview": 0,
|
||||
"in_standard_filter": 0,
|
||||
"insert_after": "linked_with",
|
||||
"label": "Is Your Company Address",
|
||||
"length": 0,
|
||||
"mandatory_depends_on": null,
|
||||
"modified": "2020-10-14 17:41:40.878179",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Address-is_your_company_address",
|
||||
"no_copy": 0,
|
||||
"options": null,
|
||||
"owner": "Administrator",
|
||||
"parent": null,
|
||||
"parentfield": null,
|
||||
"parenttype": null,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": null,
|
||||
"read_only": 0,
|
||||
"read_only_depends_on": null,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0,
|
||||
"width": null
|
||||
}
|
||||
],
|
||||
"custom_perms": [],
|
||||
"doctype": "Address",
|
||||
"property_setters": [],
|
||||
"sync_on_migrate": 1
|
||||
}
|
||||
@@ -807,11 +807,14 @@ frappe.ui.form.on("Payment Entry", {
|
||||
frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate));
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
if (!frm.doc.received_amount) {
|
||||
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("received_amount", frm.doc.paid_amount);
|
||||
} else if (company_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
|
||||
if (company_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("received_amount", frm.doc.base_paid_amount);
|
||||
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
|
||||
} else if (frm.doc.target_exchange_rate) {
|
||||
frm.set_value(
|
||||
"received_amount",
|
||||
flt(frm.doc.base_paid_amount) / flt(frm.doc.target_exchange_rate)
|
||||
);
|
||||
}
|
||||
}
|
||||
frm.trigger("reset_received_amount");
|
||||
@@ -828,15 +831,14 @@ frappe.ui.form.on("Payment Entry", {
|
||||
);
|
||||
|
||||
if (!frm.doc.paid_amount) {
|
||||
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("paid_amount", frm.doc.received_amount);
|
||||
if (frm.doc.target_exchange_rate) {
|
||||
frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate);
|
||||
}
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
} else if (company_currency == frm.doc.paid_from_account_currency) {
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
if (company_currency == frm.doc.paid_from_account_currency) {
|
||||
frm.set_value("paid_amount", frm.doc.base_received_amount);
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
} else if (frm.doc.source_exchange_rate) {
|
||||
frm.set_value(
|
||||
"paid_amount",
|
||||
flt(frm.doc.base_received_amount) / flt(frm.doc.source_exchange_rate)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1238,9 +1238,9 @@ class PaymentEntry(AccountsController):
|
||||
else:
|
||||
remarks = [
|
||||
_("Amount {0} {1} {2} {3}").format(
|
||||
_(self.paid_to_account_currency)
|
||||
_(self.paid_from_account_currency)
|
||||
if self.payment_type == "Receive"
|
||||
else _(self.paid_from_account_currency),
|
||||
else _(self.paid_to_account_currency),
|
||||
self.paid_amount if self.payment_type == "Receive" else self.received_amount,
|
||||
_("received from") if self.payment_type == "Receive" else _("paid to"),
|
||||
self.party,
|
||||
@@ -1256,7 +1256,7 @@ class PaymentEntry(AccountsController):
|
||||
for d in self.get("references"):
|
||||
if d.allocated_amount:
|
||||
remarks.append(
|
||||
_("Amount {0} {1} against {2} {3}").format(
|
||||
_("Amount {0} {1} adjusted against {2} {3}").format(
|
||||
_(self.party_account_currency),
|
||||
d.allocated_amount,
|
||||
d.reference_doctype,
|
||||
@@ -1267,7 +1267,7 @@ class PaymentEntry(AccountsController):
|
||||
for d in self.get("deductions"):
|
||||
if d.amount:
|
||||
remarks.append(
|
||||
_("Amount {0} {1} deducted against {2}").format(
|
||||
_("Amount {0} {1} as adjustment to {2}").format(
|
||||
_(self.company_currency), d.amount, d.account
|
||||
)
|
||||
)
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.utils import nowdate
|
||||
from frappe.query_builder.functions import Count, Sum
|
||||
from frappe.utils import add_days, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
@@ -90,6 +91,7 @@ class TestPaymentLedgerEntry(ERPNextTestSuite):
|
||||
posting_date = nowdate()
|
||||
|
||||
sinv = create_sales_invoice(
|
||||
posting_date=posting_date,
|
||||
qty=qty,
|
||||
rate=rate,
|
||||
company=self.company,
|
||||
@@ -531,3 +533,82 @@ class TestPaymentLedgerEntry(ERPNextTestSuite):
|
||||
# with references removed, deletion should be possible
|
||||
so.delete()
|
||||
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, so.doctype, so.name)
|
||||
|
||||
@ERPNextTestSuite.change_settings(
|
||||
"Accounts Settings",
|
||||
{"enable_immutable_ledger": 1},
|
||||
)
|
||||
def test_reverse_entries_on_cancel_for_immutable_ledger(self):
|
||||
invoice_posting_date = add_days(nowdate(), -5)
|
||||
gle = qb.DocType("GL Entry")
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
|
||||
si = self.create_sales_invoice(qty=1, rate=100, posting_date=invoice_posting_date)
|
||||
|
||||
gles_before = (
|
||||
qb.from_(gle)
|
||||
.select(
|
||||
Count(gle.name),
|
||||
)
|
||||
.where((gle.voucher_type == si.doctype) & (gle.voucher_no == si.name) & (gle.is_cancelled == 0))
|
||||
.run()[0][0]
|
||||
)
|
||||
ples_before = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
Count(ple.name),
|
||||
)
|
||||
.where((ple.voucher_type == si.doctype) & (ple.voucher_no == si.name) & (ple.delinked.eq(0)))
|
||||
.run()[0][0]
|
||||
)
|
||||
|
||||
si.cancel()
|
||||
|
||||
gles_after = (
|
||||
qb.from_(gle)
|
||||
.select(Count(gle.account))
|
||||
.where((gle.voucher_type == si.doctype) & (gle.voucher_no == si.name) & (gle.is_cancelled == 0))
|
||||
.run()[0][0]
|
||||
)
|
||||
self.assertEqual(gles_after, gles_before * 2)
|
||||
|
||||
ples_after = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
Count(ple.name),
|
||||
)
|
||||
.where((ple.voucher_type == si.doctype) & (ple.voucher_no == si.name) & (ple.delinked.eq(0)))
|
||||
.run()[0][0]
|
||||
)
|
||||
self.assertEqual(ples_after, ples_before * 2)
|
||||
|
||||
# assert debit/credit are reversed
|
||||
gl_entries = (
|
||||
qb.from_(gle)
|
||||
.select(gle.account, Sum(gle.debit).as_("total_debit"), Sum(gle.credit).as_("total_credit"))
|
||||
.where((gle.voucher_type == si.doctype) & (gle.voucher_no == si.name) & (gle.is_cancelled == 0))
|
||||
.groupby(gle.account)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
for gl in gl_entries:
|
||||
with self.subTest(gl=gl):
|
||||
self.assertEqual(gl.total_debit, gl.total_credit)
|
||||
|
||||
# assert amounts are reversed
|
||||
pl_entries = (
|
||||
qb.from_(ple)
|
||||
.select(ple.account, Sum(ple.amount).as_("total_amount"))
|
||||
.where((ple.voucher_type == si.doctype) & (ple.voucher_no == si.name) & (ple.delinked == 0))
|
||||
.groupby(ple.account)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
for pl in pl_entries:
|
||||
with self.subTest(pl=pl):
|
||||
self.assertEqual(pl.total_amount, 0)
|
||||
|
||||
self.assertFalse(
|
||||
frappe.db.exists(
|
||||
"Payment Ledger Entry",
|
||||
{"voucher_type": si.doctype, "voucher_no": si.name, "delinked": 1},
|
||||
)
|
||||
)
|
||||
|
||||
@@ -8,6 +8,7 @@ from frappe.utils import today
|
||||
from erpnext.accounts.doctype.finance_book.test_finance_book import create_finance_book
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.general_ledger import make_reverse_gl_entries
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
@@ -334,6 +335,48 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
|
||||
return pcv
|
||||
|
||||
@ERPNextTestSuite.change_settings(
|
||||
"Accounts Settings",
|
||||
{"enable_immutable_ledger": 1},
|
||||
)
|
||||
def test_immutable_ledger_reverse_entry_uses_passed_posting_date_after_pcv(self):
|
||||
company = create_company()
|
||||
cost_center = create_cost_center("Test Cost Center 1")
|
||||
|
||||
jv = make_journal_entry(
|
||||
posting_date="2021-03-15",
|
||||
amount=400,
|
||||
account1="Cash - TPC",
|
||||
account2="Sales - TPC",
|
||||
cost_center=cost_center,
|
||||
company=company,
|
||||
save=False,
|
||||
)
|
||||
jv.company = company
|
||||
jv.save()
|
||||
jv.submit()
|
||||
|
||||
self.make_period_closing_voucher(posting_date="2021-03-31")
|
||||
|
||||
# Passed posting_date is after PCV end date, so cancellation should not fail.
|
||||
make_reverse_gl_entries(
|
||||
voucher_type="Journal Entry",
|
||||
voucher_no=jv.name,
|
||||
posting_date="2022-01-01",
|
||||
)
|
||||
|
||||
totals_after_cancel = frappe.db.sql(
|
||||
"""
|
||||
select sum(debit) as total_debit, sum(credit) as total_credit
|
||||
from `tabGL Entry`
|
||||
where voucher_type=%s and voucher_no=%s and is_cancelled=0
|
||||
""",
|
||||
("Journal Entry", jv.name),
|
||||
as_dict=True,
|
||||
)[0]
|
||||
|
||||
self.assertEqual(totals_after_cancel.total_debit, totals_after_cancel.total_credit)
|
||||
|
||||
|
||||
def create_company():
|
||||
company = frappe.get_doc(
|
||||
|
||||
@@ -13,52 +13,69 @@
|
||||
"column_break_9",
|
||||
"warehouse",
|
||||
"company_address",
|
||||
"section_break_15",
|
||||
"applicable_for_users",
|
||||
"accounting_tab",
|
||||
"section_break_11",
|
||||
"payments",
|
||||
"set_grand_total_to_default_mop",
|
||||
"price_list_and_currency_section",
|
||||
"currency",
|
||||
"column_break_bptt",
|
||||
"selling_price_list",
|
||||
"write_off_section",
|
||||
"write_off_account",
|
||||
"column_break_ukpz",
|
||||
"write_off_cost_center",
|
||||
"column_break_pkca",
|
||||
"write_off_limit",
|
||||
"income_and_expense_account",
|
||||
"income_account",
|
||||
"column_break_byzk",
|
||||
"expense_account",
|
||||
"taxes_section",
|
||||
"taxes_and_charges",
|
||||
"column_break_cjpp",
|
||||
"tax_category",
|
||||
"section_break_19",
|
||||
"account_for_change_amount",
|
||||
"disable_rounded_total",
|
||||
"column_break_23",
|
||||
"apply_discount_on",
|
||||
"allow_partial_payment",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"pos_configurations_tab",
|
||||
"section_break_14",
|
||||
"hide_images",
|
||||
"hide_unavailable_items",
|
||||
"auto_add_item_to_cart",
|
||||
"validate_stock_on_save",
|
||||
"print_receipt_on_order_complete",
|
||||
"action_on_new_invoice",
|
||||
"validate_stock_on_save",
|
||||
"column_break_16",
|
||||
"update_stock",
|
||||
"ignore_pricing_rule",
|
||||
"print_receipt_on_order_complete",
|
||||
"pos_item_selector_section",
|
||||
"hide_images",
|
||||
"column_break_rpny",
|
||||
"hide_unavailable_items",
|
||||
"column_break_stcl",
|
||||
"auto_add_item_to_cart",
|
||||
"pos_item_details_section",
|
||||
"allow_rate_change",
|
||||
"column_break_hwfg",
|
||||
"allow_discount_change",
|
||||
"set_grand_total_to_default_mop",
|
||||
"allow_partial_payment",
|
||||
"section_break_15",
|
||||
"applicable_for_users",
|
||||
"section_break_23",
|
||||
"item_groups",
|
||||
"column_break_25",
|
||||
"customer_groups",
|
||||
"more_info_tab",
|
||||
"section_break_16",
|
||||
"print_format",
|
||||
"letter_head",
|
||||
"column_break0",
|
||||
"tc_name",
|
||||
"select_print_heading",
|
||||
"section_break_19",
|
||||
"selling_price_list",
|
||||
"currency",
|
||||
"write_off_account",
|
||||
"write_off_cost_center",
|
||||
"write_off_limit",
|
||||
"account_for_change_amount",
|
||||
"disable_rounded_total",
|
||||
"column_break_23",
|
||||
"income_account",
|
||||
"expense_account",
|
||||
"taxes_and_charges",
|
||||
"tax_category",
|
||||
"apply_discount_on",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"utm_analytics_section",
|
||||
"utm_source",
|
||||
"column_break_tvls",
|
||||
@@ -133,8 +150,7 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_14",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Configuration"
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"description": "Only show Items from these Item Groups",
|
||||
@@ -155,6 +171,7 @@
|
||||
"options": "POS Customer Group"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "section_break_16",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Print Settings"
|
||||
@@ -194,7 +211,7 @@
|
||||
{
|
||||
"fieldname": "section_break_19",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting"
|
||||
"label": "Miscellaneous"
|
||||
},
|
||||
{
|
||||
"fieldname": "selling_price_list",
|
||||
@@ -430,6 +447,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Applicable on POS Invoice",
|
||||
"fieldname": "allow_partial_payment",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Partial Payment"
|
||||
@@ -447,6 +465,83 @@
|
||||
"fieldname": "utm_analytics_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Campaign"
|
||||
},
|
||||
{
|
||||
"fieldname": "accounting_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Accounting"
|
||||
},
|
||||
{
|
||||
"fieldname": "more_info_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "More Info"
|
||||
},
|
||||
{
|
||||
"fieldname": "pos_configurations_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "POS Configurations"
|
||||
},
|
||||
{
|
||||
"fieldname": "price_list_and_currency_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Price List & Currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_bptt",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "write_off_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Write Off"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_ukpz",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_pkca",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "income_and_expense_account",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Income and Expense"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_byzk",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "taxes_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Taxes"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_cjpp",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "pos_item_selector_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "POS Item Selector"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_rpny",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_stcl",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "pos_item_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "POS Item Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_hwfg",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -475,7 +570,7 @@
|
||||
"link_fieldname": "pos_profile"
|
||||
}
|
||||
],
|
||||
"modified": "2026-02-10 14:24:48.597412",
|
||||
"modified": "2026-05-26 12:07:48.597412",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Profile",
|
||||
|
||||
@@ -137,9 +137,10 @@ def pause_pcv_processing(docname: str):
|
||||
ppcv = qb.DocType("Process Period Closing Voucher")
|
||||
qb.update(ppcv).set(ppcv.status, "Paused").where(ppcv.name.eq(docname)).run()
|
||||
|
||||
# If a date is stuck in 'Running' state, this will allow it to procced.
|
||||
if queued_dates := frappe.db.get_all(
|
||||
"Process Period Closing Voucher Detail",
|
||||
filters={"parent": docname, "status": "Queued"},
|
||||
filters={"parent": docname, "status": ["in", ["Queued", "Running"]]},
|
||||
pluck="name",
|
||||
):
|
||||
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
|
||||
@@ -173,6 +174,9 @@ def resume_pcv_processing(docname: str):
|
||||
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
|
||||
qb.update(ppcvd).set(ppcvd.status, "Queued").where(ppcvd.name.isin(paused_dates)).run()
|
||||
start_pcv_processing(docname)
|
||||
else:
|
||||
# If a parent doc is stuck in 'Running' state, will allow it to proceed.
|
||||
schedule_next_date(docname)
|
||||
|
||||
|
||||
def update_default_dimensions(dimension_fields, gl_entry, dimension_values):
|
||||
@@ -288,7 +292,21 @@ def schedule_next_date(docname: str):
|
||||
)
|
||||
# Ensure both normal and opening balances are processed for all dates
|
||||
if total_no_of_dates == completed:
|
||||
summarize_and_post_ledger_entries(docname)
|
||||
from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import (
|
||||
is_job_running,
|
||||
)
|
||||
|
||||
job_name = f"summarize_{docname}"
|
||||
if not is_job_running(job_name):
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.summarize_and_post_ledger_entries",
|
||||
queue="long",
|
||||
timeout="3600",
|
||||
is_async=True,
|
||||
job_name=job_name,
|
||||
enqueue_after_commit=True,
|
||||
docname=docname,
|
||||
)
|
||||
|
||||
|
||||
def make_dict_json_compliant(dimension_wise_balance) -> dict:
|
||||
|
||||
@@ -1921,7 +1921,7 @@
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: !doc.is_return",
|
||||
"description": "Issue a debit note with 0 qty against an existing Sales Invoice",
|
||||
"description": "Issue a debit note against an existing Sales Invoice to adjust the rate. The quantity will be retained from the original invoice.",
|
||||
"fieldname": "is_debit_note",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Rate Adjustment Entry (Debit Note)"
|
||||
@@ -2367,7 +2367,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2026-05-01 02:37:29.742764",
|
||||
"modified": "2026-05-21 17:31:11.190958",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -968,9 +968,6 @@ class SalesInvoice(SellingController):
|
||||
if selling_price_list:
|
||||
self.set("selling_price_list", selling_price_list)
|
||||
|
||||
if not for_validate:
|
||||
self.update_stock = cint(pos.get("update_stock"))
|
||||
|
||||
# set pos values in items
|
||||
for item in self.get("items"):
|
||||
if item.get("item_code"):
|
||||
@@ -981,6 +978,10 @@ class SalesInvoice(SellingController):
|
||||
if (not for_validate) or (for_validate and not item.get(fname)):
|
||||
item.set(fname, val)
|
||||
|
||||
if not for_validate:
|
||||
dn_flag = any(d.get("dn_detail") for d in self.get("items"))
|
||||
self.update_stock = 0 if dn_flag else cint(pos.get("update_stock"))
|
||||
|
||||
# fetch terms
|
||||
if self.tc_name and not self.terms:
|
||||
self.terms = frappe.db.get_value("Terms and Conditions", self.tc_name, "terms")
|
||||
|
||||
@@ -14,6 +14,7 @@ from frappe.utils import cstr
|
||||
from frappe.utils.nestedset import get_root_of
|
||||
|
||||
from erpnext.setup.doctype.customer_group.customer_group import get_parent_customer_groups
|
||||
from erpnext.setup.doctype.supplier_group.supplier_group import get_parent_supplier_groups
|
||||
|
||||
|
||||
class IncorrectCustomerGroup(frappe.ValidationError):
|
||||
@@ -176,38 +177,44 @@ def get_party_details(party, party_type, args=None):
|
||||
def get_tax_template(posting_date, args):
|
||||
"""Get matching tax rule"""
|
||||
args = frappe._dict(args)
|
||||
conditions = []
|
||||
|
||||
TaxRule = DocType("Tax Rule")
|
||||
query = frappe.qb.from_(TaxRule).select("*")
|
||||
|
||||
if posting_date:
|
||||
conditions.append(
|
||||
f"""(from_date is null or from_date <= '{posting_date}')
|
||||
and (to_date is null or to_date >= '{posting_date}')"""
|
||||
query = query.where(
|
||||
(TaxRule.from_date.isnull() | (TaxRule.from_date <= posting_date))
|
||||
& (TaxRule.to_date.isnull() | (TaxRule.to_date >= posting_date))
|
||||
)
|
||||
else:
|
||||
conditions.append("(from_date is null) and (to_date is null)")
|
||||
query = query.where(TaxRule.from_date.isnull() & TaxRule.to_date.isnull())
|
||||
|
||||
conditions.append(
|
||||
"ifnull(tax_category, '') = {}".format(frappe.db.escape(cstr(args.get("tax_category")), False))
|
||||
)
|
||||
if "tax_category" in args.keys():
|
||||
del args["tax_category"]
|
||||
def get_group_ancestors(doctype, get_parents, value):
|
||||
if not value:
|
||||
value = get_root_of(doctype)
|
||||
return [""] + [d.name for d in get_parents(value)]
|
||||
|
||||
group_fields = {
|
||||
"customer_group": ("Customer Group", get_parent_customer_groups),
|
||||
"supplier_group": ("Supplier Group", get_parent_supplier_groups),
|
||||
}
|
||||
|
||||
args.setdefault("tax_category", "")
|
||||
|
||||
for key, value in args.items():
|
||||
if key == "use_for_shopping_cart":
|
||||
conditions.append(f"use_for_shopping_cart = {1 if value else 0}")
|
||||
elif key == "customer_group":
|
||||
if not value:
|
||||
value = get_root_of("Customer Group")
|
||||
customer_group_condition = get_customer_group_condition(value)
|
||||
conditions.append(f"ifnull({key}, '') in ('', {customer_group_condition})")
|
||||
query = query.where(TaxRule.use_for_shopping_cart == value)
|
||||
elif key == "tax_category":
|
||||
query = query.where(IfNull(TaxRule.tax_category, "") == (value or ""))
|
||||
elif key in group_fields:
|
||||
doctype, get_parents = group_fields[key]
|
||||
query = query.where(
|
||||
IfNull(TaxRule[key], "").isin(get_group_ancestors(doctype, get_parents, value))
|
||||
)
|
||||
else:
|
||||
conditions.append(f"ifnull({key}, '') in ('', {frappe.db.escape(cstr(value))})")
|
||||
query = query.where(IfNull(TaxRule[key], "").isin(["", value or ""]))
|
||||
|
||||
tax_rule = frappe.db.sql(
|
||||
"""select * from `tabTax Rule`
|
||||
where {}""".format(" and ".join(conditions)),
|
||||
as_dict=True,
|
||||
)
|
||||
tax_rule = query.run(as_dict=True)
|
||||
|
||||
if not tax_rule:
|
||||
return None
|
||||
@@ -236,11 +243,3 @@ def get_tax_template(posting_date, args):
|
||||
return None
|
||||
|
||||
return tax_template
|
||||
|
||||
|
||||
def get_customer_group_condition(customer_group):
|
||||
condition = ""
|
||||
customer_groups = ["%s" % (frappe.db.escape(d.name)) for d in get_parent_customer_groups(customer_group)]
|
||||
if customer_groups:
|
||||
condition = ",".join(["%s"] * len(customer_groups)) % (tuple(customer_groups))
|
||||
return condition
|
||||
|
||||
@@ -62,6 +62,117 @@ class TestTaxRule(ERPNextTestSuite):
|
||||
"_Test Sales Taxes and Charges Template - _TC",
|
||||
)
|
||||
|
||||
def test_for_parent_supplier_group(self):
|
||||
purchase_template = "_Test Purchase Taxes and Charges Template - _TC"
|
||||
if not frappe.db.exists("Purchase Taxes and Charges Template", purchase_template):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Purchase Taxes and Charges Template",
|
||||
"title": "_Test Purchase Taxes and Charges Template",
|
||||
"company": "_Test Company",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"description": "VAT",
|
||||
"doctype": "Purchase Taxes and Charges",
|
||||
"cost_center": "Main - _TC",
|
||||
"rate": 6,
|
||||
}
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
make_tax_rule(
|
||||
supplier_group="All Supplier Groups",
|
||||
tax_type="Purchase",
|
||||
purchase_tax_template=purchase_template,
|
||||
priority=1,
|
||||
use_for_shopping_cart=0,
|
||||
from_date="2015-01-01",
|
||||
save=1,
|
||||
)
|
||||
|
||||
# "_Test Supplier Group" has "All Supplier Groups" as its parent — should match hierarchically
|
||||
self.assertEqual(
|
||||
get_tax_template(
|
||||
"2015-01-01",
|
||||
{
|
||||
"supplier_group": "_Test Supplier Group",
|
||||
"tax_type": "Purchase",
|
||||
"use_for_shopping_cart": 0,
|
||||
},
|
||||
),
|
||||
purchase_template,
|
||||
)
|
||||
|
||||
def test_use_for_shopping_cart_filter(self):
|
||||
city = "Test Cart City"
|
||||
# higher priority ensures this rule wins when use_for_shopping_cart is not filtered
|
||||
make_tax_rule(
|
||||
customer="_Test Customer",
|
||||
billing_city=city,
|
||||
sales_tax_template="_Test Sales Taxes and Charges Template - _TC",
|
||||
use_for_shopping_cart=0,
|
||||
priority=2,
|
||||
save=1,
|
||||
)
|
||||
make_tax_rule(
|
||||
customer="_Test Customer",
|
||||
billing_city=city,
|
||||
sales_tax_template="_Test Sales Taxes and Charges Template 1 - _TC",
|
||||
use_for_shopping_cart=1,
|
||||
priority=1,
|
||||
save=1,
|
||||
)
|
||||
|
||||
# Cart request (use_for_shopping_cart=1) filters to cart rules only
|
||||
self.assertEqual(
|
||||
get_tax_template(
|
||||
"2015-01-01",
|
||||
{"customer": "_Test Customer", "billing_city": city, "use_for_shopping_cart": 1},
|
||||
),
|
||||
"_Test Sales Taxes and Charges Template 1 - _TC",
|
||||
)
|
||||
|
||||
# Non-cart request omits use_for_shopping_cart — no filter is applied, both rules
|
||||
# are candidates; non-cart rule wins by higher priority
|
||||
self.assertEqual(
|
||||
get_tax_template(
|
||||
"2015-01-01",
|
||||
{"customer": "_Test Customer", "billing_city": city},
|
||||
),
|
||||
"_Test Sales Taxes and Charges Template - _TC",
|
||||
)
|
||||
|
||||
def test_use_for_shopping_cart_default(self):
|
||||
city = "Test Default Cart City"
|
||||
# use_for_shopping_cart not set — Check field defaults to 0
|
||||
make_tax_rule(
|
||||
customer="_Test Customer",
|
||||
billing_city=city,
|
||||
sales_tax_template="_Test Sales Taxes and Charges Template - _TC",
|
||||
use_for_shopping_cart=0, # Default is set to 1.
|
||||
save=1,
|
||||
)
|
||||
|
||||
# Non-cart request (no use_for_shopping_cart in args) matches the rule
|
||||
self.assertEqual(
|
||||
get_tax_template(
|
||||
"2015-01-01",
|
||||
{"customer": "_Test Customer", "billing_city": city},
|
||||
),
|
||||
"_Test Sales Taxes and Charges Template - _TC",
|
||||
)
|
||||
|
||||
# Cart request (use_for_shopping_cart=1) does not match — rule has default 0
|
||||
self.assertIsNone(
|
||||
get_tax_template(
|
||||
"2015-01-01",
|
||||
{"customer": "_Test Customer", "billing_city": city, "use_for_shopping_cart": 1},
|
||||
)
|
||||
)
|
||||
|
||||
def test_conflict_with_overlapping_dates(self):
|
||||
tax_rule1 = make_tax_rule(
|
||||
customer="_Test Customer",
|
||||
|
||||
@@ -431,6 +431,8 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False):
|
||||
gle.flags.adv_adj = adv_adj
|
||||
gle.flags.update_outstanding = update_outstanding or "Yes"
|
||||
gle.flags.notify_update = False
|
||||
if gle.is_cancelled or is_immutable_ledger_enabled():
|
||||
gle.flags.ignore_links = True
|
||||
gle.submit()
|
||||
|
||||
if (
|
||||
@@ -717,7 +719,12 @@ def make_reverse_gl_entries(
|
||||
check_freezing_date(gl_entries[0]["posting_date"], adv_adj)
|
||||
|
||||
is_opening = any(d.get("is_opening") == "Yes" for d in gl_entries)
|
||||
validate_against_pcv(is_opening, gl_entries[0]["posting_date"], gl_entries[0]["company"])
|
||||
|
||||
# For reverse entries, use the posting_date parameter if provided and valid
|
||||
# Otherwise fall back to original posting_date
|
||||
validation_date = posting_date if posting_date else gl_entries[0]["posting_date"]
|
||||
validate_against_pcv(is_opening, validation_date, gl_entries[0]["company"])
|
||||
|
||||
if partial_cancel:
|
||||
# Partial cancel is only used by `Advance` in separate account feature.
|
||||
# Only cancel GL entries for unlinked reference using `voucher_detail_no`
|
||||
|
||||
@@ -750,7 +750,7 @@ def set_taxes(
|
||||
args.update({"tax_type": "Purchase"})
|
||||
|
||||
if use_for_shopping_cart:
|
||||
args.update({"use_for_shopping_cart": use_for_shopping_cart})
|
||||
args.update({"use_for_shopping_cart": cint(use_for_shopping_cart)})
|
||||
|
||||
return get_tax_template(posting_date, args)
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ import erpnext
|
||||
from erpnext.accounts.doctype.account.account import get_account_currency
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||
from erpnext.stock import get_warehouse_account_map
|
||||
from erpnext.stock.utils import get_stock_value_on
|
||||
from erpnext.stock.utils import get_combine_datetime, get_stock_value_on
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import RepostItemValuation
|
||||
@@ -1752,31 +1752,31 @@ def sort_stock_vouchers_by_posting_date(
|
||||
|
||||
|
||||
def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, for_items=None, company=None):
|
||||
values = []
|
||||
condition = ""
|
||||
posting_datetime = get_combine_datetime(posting_date, posting_time)
|
||||
|
||||
SLE = DocType("Stock Ledger Entry")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(SLE)
|
||||
.select(SLE.voucher_type, SLE.voucher_no)
|
||||
.distinct()
|
||||
.where(SLE.posting_datetime >= posting_datetime)
|
||||
.where(SLE.is_cancelled == 0)
|
||||
.orderby(SLE.posting_datetime)
|
||||
.orderby(SLE.creation)
|
||||
.for_update()
|
||||
)
|
||||
|
||||
if for_items:
|
||||
condition += " and item_code in ({})".format(", ".join(["%s"] * len(for_items)))
|
||||
values += for_items
|
||||
query = query.where(SLE.item_code.isin(for_items))
|
||||
|
||||
if for_warehouses:
|
||||
condition += " and warehouse in ({})".format(", ".join(["%s"] * len(for_warehouses)))
|
||||
values += for_warehouses
|
||||
query = query.where(SLE.warehouse.isin(for_warehouses))
|
||||
|
||||
if company:
|
||||
condition += " and company = %s"
|
||||
values.append(company)
|
||||
query = query.where(SLE.company == company)
|
||||
|
||||
future_stock_vouchers = frappe.db.sql(
|
||||
f"""select distinct sle.voucher_type, sle.voucher_no
|
||||
from `tabStock Ledger Entry` sle
|
||||
where
|
||||
timestamp(sle.posting_date, sle.posting_time) >= timestamp(%s, %s)
|
||||
and is_cancelled = 0
|
||||
{condition}
|
||||
order by timestamp(sle.posting_date, sle.posting_time) asc, creation asc for update""",
|
||||
tuple([posting_date, posting_time, *values]),
|
||||
as_dict=True,
|
||||
)
|
||||
future_stock_vouchers = query.run(as_dict=True)
|
||||
|
||||
return [(d.voucher_type, d.voucher_no) for d in future_stock_vouchers]
|
||||
|
||||
@@ -2130,8 +2130,9 @@ def create_payment_ledger_entry(
|
||||
ple = frappe.get_doc(entry)
|
||||
|
||||
if cancel:
|
||||
delink_original_entry(ple, partial_cancel=partial_cancel)
|
||||
if is_immutable_ledger_enabled():
|
||||
if not is_immutable_ledger_enabled():
|
||||
delink_original_entry(ple, partial_cancel=partial_cancel)
|
||||
else:
|
||||
ple.delinked = 0
|
||||
ple.posting_date = frappe.form_dict.get("posting_date") or getdate()
|
||||
|
||||
@@ -2220,6 +2221,7 @@ def delink_original_entry(pl_entry, partial_cancel=False):
|
||||
qb.update(ple)
|
||||
.set(ple.modified, now())
|
||||
.set(ple.modified_by, frappe.session.user)
|
||||
.set(ple.delinked, True)
|
||||
.where(
|
||||
(ple.company == pl_entry.company)
|
||||
& (ple.account_type == pl_entry.account_type)
|
||||
@@ -2236,9 +2238,6 @@ def delink_original_entry(pl_entry, partial_cancel=False):
|
||||
if partial_cancel:
|
||||
query = query.where(ple.voucher_detail_no == pl_entry.voucher_detail_no)
|
||||
|
||||
if not is_immutable_ledger_enabled():
|
||||
query = query.set(ple.delinked, True)
|
||||
|
||||
query.run()
|
||||
|
||||
|
||||
|
||||
@@ -551,7 +551,9 @@ frappe.ui.form.on("Asset", {
|
||||
asset_type: function (frm) {
|
||||
if (frm.doc.docstatus == 0) {
|
||||
if (frm.doc.asset_type == "Composite Asset") {
|
||||
frm.set_value("net_purchase_amount", 0);
|
||||
if (!frm.doc.net_purchase_amount) {
|
||||
frm.set_value("net_purchase_amount", 0);
|
||||
}
|
||||
} else {
|
||||
frm.set_df_property("net_purchase_amount", "read_only", 0);
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ from erpnext.stock.get_item_details import (
|
||||
ItemDetailsCtx,
|
||||
_get_item_tax_template,
|
||||
_get_item_tax_template_from_item_group,
|
||||
get_bin_details,
|
||||
get_conversion_factor,
|
||||
get_item_details,
|
||||
get_item_tax_map,
|
||||
@@ -3737,6 +3738,7 @@ def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child
|
||||
child_item.warehouse = get_item_warehouse_(p_doc, item, overwrite_warehouse=True)
|
||||
conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor"))
|
||||
child_item.conversion_factor = flt(trans_item.get("conversion_factor")) or conversion_factor
|
||||
child_item.update(get_bin_details(child_item.item_code, child_item.warehouse, p_doc.get("company")))
|
||||
|
||||
if child_doctype in ["Purchase Order Item", "Supplier Quotation Item"]:
|
||||
# Initialized value will update in parent validation
|
||||
|
||||
@@ -209,7 +209,9 @@ def create_variant(item, args, use_template_image=False):
|
||||
variant_attributes = []
|
||||
|
||||
for d in template.attributes:
|
||||
variant_attributes.append({"attribute": d.attribute, "attribute_value": args.get(_(d.attribute))})
|
||||
attribute_value = args.get(_(d.attribute)) or args.get(d.attribute)
|
||||
if attribute_value:
|
||||
variant_attributes.append({"attribute": d.attribute, "attribute_value": attribute_value})
|
||||
|
||||
variant.set("attributes", variant_attributes)
|
||||
copy_attributes_to_variant(template, variant)
|
||||
@@ -228,6 +230,12 @@ def enqueue_multiple_variant_creation(item, args, use_template_image=False):
|
||||
# There can be innumerable attribute combinations, enqueue
|
||||
if isinstance(args, str):
|
||||
variants = json.loads(args)
|
||||
else:
|
||||
variants = args
|
||||
variants = {key: values for key, values in variants.items() if values}
|
||||
if not variants:
|
||||
frappe.throw(_("Please select at least one attribute value"))
|
||||
|
||||
total_variants = 1
|
||||
for key in variants:
|
||||
total_variants *= len(variants[key])
|
||||
@@ -251,6 +259,7 @@ def create_multiple_variants(item, args, use_template_image=False):
|
||||
count = 0
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
args = {key: values for key, values in args.items() if values}
|
||||
|
||||
template_item = frappe.get_doc("Item", item)
|
||||
args_set = generate_keyed_value_combinations(args)
|
||||
@@ -285,6 +294,9 @@ def generate_keyed_value_combinations(args):
|
||||
|
||||
"""
|
||||
# Return empty list if empty
|
||||
if not args:
|
||||
return []
|
||||
args = {key: values for key, values in args.items() if values}
|
||||
if not args:
|
||||
return []
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ from pypika import Order
|
||||
import erpnext
|
||||
from erpnext.accounts.utils import build_qb_match_conditions
|
||||
from erpnext.stock.get_item_details import ItemDetailsCtx, _get_item_tax_template
|
||||
from erpnext.stock.utils import get_combine_datetime
|
||||
|
||||
|
||||
# searches for active employees
|
||||
@@ -210,16 +211,28 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
|
||||
|
||||
if filters and isinstance(filters, dict):
|
||||
if filters.get("customer") or filters.get("supplier"):
|
||||
party_type = "Customer" if filters.get("customer") else "Supplier"
|
||||
party = filters.get("customer") or filters.get("supplier")
|
||||
group = "Customer Group" if filters.get("customer") else "Supplier Group"
|
||||
item_rules_list = frappe.get_all(
|
||||
"Party Specific Item",
|
||||
filters={
|
||||
"party": ["!=", party],
|
||||
"party_type": "Customer" if filters.get("customer") else "Supplier",
|
||||
"party_type": party_type,
|
||||
},
|
||||
fields=["restrict_based_on", "based_on_value"],
|
||||
)
|
||||
|
||||
party_group_rules_list = frappe.get_all(
|
||||
"Party Specific Item",
|
||||
filters={"party_type": group},
|
||||
fields=["party as party_group", "restrict_based_on", "based_on_value"],
|
||||
)
|
||||
current_party_group = frappe.get_value(party_type, party, frappe.scrub(group))
|
||||
for rule in party_group_rules_list:
|
||||
if current_party_group != rule.party_group:
|
||||
item_rules_list.append(rule)
|
||||
|
||||
filters_dict = {}
|
||||
for rule in item_rules_list:
|
||||
if rule["restrict_based_on"] == "Item":
|
||||
@@ -484,6 +497,13 @@ def get_batches_from_stock_ledger_entries(searchfields, txt, filters, start=0, p
|
||||
.limit(page_len)
|
||||
)
|
||||
|
||||
if not filters.get("is_inward"):
|
||||
if filters.get("posting_date") and filters.get("posting_time"):
|
||||
query = query.where(
|
||||
stock_ledger_entry.posting_datetime
|
||||
<= get_combine_datetime(filters.get("posting_date"), filters.get("posting_time"))
|
||||
)
|
||||
|
||||
if not filters.get("include_expired_batches"):
|
||||
query = query.where((batch_table.expiry_date >= expiry_date) | (batch_table.expiry_date.isnull()))
|
||||
|
||||
@@ -537,6 +557,13 @@ def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start=0
|
||||
.limit(page_len)
|
||||
)
|
||||
|
||||
if not filters.get("is_inward"):
|
||||
if filters.get("posting_date") and filters.get("posting_time"):
|
||||
bundle_query = bundle_query.where(
|
||||
stock_ledger_entry.posting_datetime
|
||||
<= get_combine_datetime(filters.get("posting_date"), filters.get("posting_time"))
|
||||
)
|
||||
|
||||
if not filters.get("include_expired_batches"):
|
||||
bundle_query = bundle_query.where(
|
||||
(batch_table.expiry_date >= expiry_date) | (batch_table.expiry_date.isnull())
|
||||
|
||||
@@ -579,6 +579,7 @@ class SellingController(StockController):
|
||||
or (
|
||||
get_valuation_method(d.item_code, self.company) == "Moving Average"
|
||||
and self.get("is_return")
|
||||
and not is_standalone
|
||||
)
|
||||
):
|
||||
d.incoming_rate = get_incoming_rate(
|
||||
|
||||
@@ -281,10 +281,10 @@ class StatusUpdater(Document):
|
||||
|
||||
# get unique transactions to update
|
||||
for d in self.get_all_children():
|
||||
if hasattr(d, "qty") and d.qty < 0 and not self.get("is_return"):
|
||||
if hasattr(d, "qty") and flt(d.qty) < 0 and not self.get("is_return"):
|
||||
frappe.throw(_("For an item {0}, quantity must be positive number").format(d.item_code))
|
||||
|
||||
if hasattr(d, "qty") and d.qty > 0 and self.get("is_return"):
|
||||
if hasattr(d, "qty") and flt(d.qty) > 0 and self.get("is_return"):
|
||||
frappe.throw(_("For an item {0}, quantity must be negative number").format(d.item_code))
|
||||
|
||||
if (
|
||||
|
||||
@@ -651,20 +651,25 @@ class SubcontractingInwardController:
|
||||
).update_manufacturing_qty_fields()
|
||||
elif self.purpose in ["Subcontracting Delivery", "Subcontracting Return"]:
|
||||
fieldname = "delivered_qty" if self.purpose == "Subcontracting Delivery" else "returned_qty"
|
||||
qty_map = defaultdict(lambda: defaultdict(float))
|
||||
for item in self.items:
|
||||
doctype = (
|
||||
"Subcontracting Inward Order Item"
|
||||
if not item.type and not item.is_legacy_scrap_item
|
||||
else "Subcontracting Inward Order Secondary Item"
|
||||
)
|
||||
frappe.db.set_value(
|
||||
doctype,
|
||||
item.scio_detail,
|
||||
fieldname,
|
||||
frappe.get_value(doctype, item.scio_detail, fieldname)
|
||||
+ (item.transfer_qty if self._action == "submit" else -item.transfer_qty),
|
||||
qty_map[doctype][item.scio_detail] += (
|
||||
item.transfer_qty if self._action == "submit" else -item.transfer_qty
|
||||
)
|
||||
|
||||
for doctype, item_qty_map in qty_map.items():
|
||||
table = frappe.qb.DocType(doctype)
|
||||
field = table[fieldname]
|
||||
doc_updates = {
|
||||
scio_detail: {fieldname: field + qty} for scio_detail, qty in item_qty_map.items()
|
||||
}
|
||||
frappe.db.bulk_update(doctype, doc_updates, chunk_size=len(doc_updates))
|
||||
|
||||
def update_inward_order_received_items(self):
|
||||
if self.subcontracting_inward_order:
|
||||
match self.purpose:
|
||||
@@ -679,14 +684,18 @@ class SubcontractingInwardController:
|
||||
else -item.transfer_qty
|
||||
for item in self.items
|
||||
}
|
||||
case_expr = Case()
|
||||
table = frappe.qb.DocType("Subcontracting Inward Order Received Item")
|
||||
for scio_rm_name, qty in scio_rm_names.items():
|
||||
case_expr = case_expr.when(table.name == scio_rm_name, table.returned_qty + qty)
|
||||
|
||||
frappe.qb.update(table).set(table.returned_qty, case_expr).where(
|
||||
(table.name.isin(list(scio_rm_names.keys()))) & (table.docstatus == 1)
|
||||
).run()
|
||||
doc_updates = {
|
||||
scio_rm_name: {"returned_qty": table.returned_qty + qty}
|
||||
for scio_rm_name, qty in scio_rm_names.items()
|
||||
}
|
||||
if doc_updates:
|
||||
frappe.db.bulk_update(
|
||||
"Subcontracting Inward Order Received Item",
|
||||
doc_updates,
|
||||
chunk_size=len(doc_updates),
|
||||
update_modified=False,
|
||||
)
|
||||
|
||||
def update_inward_order_received_items_for_raw_materials_receipt(self):
|
||||
data = frappe._dict()
|
||||
@@ -737,9 +746,7 @@ class SubcontractingInwardController:
|
||||
fields=["rate", "name", "required_qty", "received_qty"],
|
||||
)
|
||||
|
||||
deleted_docs = []
|
||||
table = frappe.qb.DocType("Subcontracting Inward Order Received Item")
|
||||
case_expr_qty, case_expr_rate = Case(), Case()
|
||||
doc_updates = {}
|
||||
for d in result:
|
||||
current_qty = flt(data[d.name].transfer_qty) * (1 if self._action == "submit" else -1)
|
||||
current_rate = flt(data[d.name].rate)
|
||||
@@ -754,16 +761,17 @@ class SubcontractingInwardController:
|
||||
)
|
||||
|
||||
if not d.required_qty and not d.received_qty:
|
||||
deleted_docs.append(d.name)
|
||||
frappe.delete_doc("Subcontracting Inward Order Received Item", d.name)
|
||||
else:
|
||||
case_expr_qty = case_expr_qty.when(table.name == d.name, d.received_qty)
|
||||
case_expr_rate = case_expr_rate.when(table.name == d.name, d.rate)
|
||||
doc_updates[d.name] = {"received_qty": d.received_qty, "rate": d.rate}
|
||||
|
||||
if final_list := list(set(data.keys()) - set(deleted_docs)):
|
||||
frappe.qb.update(table).set(table.received_qty, case_expr_qty).set(
|
||||
table.rate, case_expr_rate
|
||||
).where((table.name.isin(final_list)) & (table.docstatus == 1)).run()
|
||||
if doc_updates:
|
||||
frappe.db.bulk_update(
|
||||
"Subcontracting Inward Order Received Item",
|
||||
doc_updates,
|
||||
chunk_size=len(doc_updates),
|
||||
update_modified=False,
|
||||
)
|
||||
|
||||
def update_inward_order_received_items_for_manufacture(self):
|
||||
customer_warehouse = frappe.get_cached_value(
|
||||
@@ -815,8 +823,8 @@ class SubcontractingInwardController:
|
||||
)
|
||||
|
||||
if data := data.run(as_dict=True):
|
||||
deleted_docs, used_item_wh = [], []
|
||||
case_expr = Case()
|
||||
used_item_wh = []
|
||||
doc_updates = {}
|
||||
for d in data:
|
||||
if not d.warehouse:
|
||||
d.warehouse = next(
|
||||
@@ -828,15 +836,17 @@ class SubcontractingInwardController:
|
||||
|
||||
qty = d.consumed_qty + item_code_wh[(d.rm_item_code, d.warehouse)]
|
||||
if qty or d.is_customer_provided_item or not d.is_additional_item:
|
||||
case_expr = case_expr.when((table.name == d.name), qty)
|
||||
doc_updates[d.name] = {"consumed_qty": qty}
|
||||
else:
|
||||
deleted_docs.append(d.name)
|
||||
frappe.delete_doc("Subcontracting Inward Order Received Item", d.name)
|
||||
|
||||
if final_list := list(set([d.name for d in data]) - set(deleted_docs)):
|
||||
frappe.qb.update(table).set(table.consumed_qty, case_expr).where(
|
||||
(table.name.isin(final_list)) & (table.docstatus == 1)
|
||||
).run()
|
||||
if doc_updates:
|
||||
frappe.db.bulk_update(
|
||||
"Subcontracting Inward Order Received Item",
|
||||
doc_updates,
|
||||
chunk_size=len(doc_updates),
|
||||
update_modified=False,
|
||||
)
|
||||
|
||||
main_item_code = next(fg for fg in self.items if fg.is_finished_item).item_code
|
||||
for extra_item in [
|
||||
@@ -908,27 +918,25 @@ class SubcontractingInwardController:
|
||||
for d in result
|
||||
}
|
||||
)
|
||||
deleted_docs = []
|
||||
case_expr = Case()
|
||||
table = frappe.qb.DocType("Subcontracting Inward Order Secondary Item")
|
||||
doc_updates = {}
|
||||
for key, value in secondary_items_dict.items():
|
||||
if (
|
||||
self._action == "cancel"
|
||||
and value.produced_qty - abs(secondary_items.get(key)) == 0
|
||||
):
|
||||
deleted_docs.append(value.name)
|
||||
frappe.delete_doc("Subcontracting Inward Order Secondary Item", value.name)
|
||||
else:
|
||||
case_expr = case_expr.when(
|
||||
table.name == value.name, value.produced_qty + secondary_items.get(key)
|
||||
)
|
||||
doc_updates[value.name] = {
|
||||
"produced_qty": value.produced_qty + secondary_items.get(key)
|
||||
}
|
||||
|
||||
if final_list := list(
|
||||
set([v.name for v in secondary_items_dict.values()]) - set(deleted_docs)
|
||||
):
|
||||
frappe.qb.update(table).set(table.produced_qty, case_expr).where(
|
||||
(table.name.isin(final_list)) & (table.docstatus == 1)
|
||||
).run()
|
||||
if doc_updates:
|
||||
frappe.db.bulk_update(
|
||||
"Subcontracting Inward Order Secondary Item",
|
||||
doc_updates,
|
||||
chunk_size=len(doc_updates),
|
||||
update_modified=False,
|
||||
)
|
||||
|
||||
fg_item_code = next(fg for fg in self.items if fg.is_finished_item).item_code
|
||||
for secondary_item in [
|
||||
|
||||
@@ -3,7 +3,11 @@ import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.controllers.item_variant import copy_attributes_to_variant, make_variant_item_code
|
||||
from erpnext.controllers.item_variant import (
|
||||
copy_attributes_to_variant,
|
||||
generate_keyed_value_combinations,
|
||||
make_variant_item_code,
|
||||
)
|
||||
from erpnext.stock.doctype.item.test_item import set_item_variant_settings
|
||||
from erpnext.stock.doctype.quality_inspection.test_quality_inspection import (
|
||||
create_quality_inspection_parameter,
|
||||
@@ -18,6 +22,19 @@ class TestItemVariant(ERPNextTestSuite):
|
||||
variant = make_item_variant()
|
||||
self.assertEqual(variant.get("quality_inspection_template"), "_Test QC Template")
|
||||
|
||||
def test_generate_keyed_value_combinations_ignores_empty_attributes(self):
|
||||
combinations = generate_keyed_value_combinations(
|
||||
{"Test Colour": ["Red", "Blue"], "Test Size": ["Small", "Large"], "Test Fit": []}
|
||||
)
|
||||
|
||||
self.assertEqual(len(combinations), 4)
|
||||
self.assertNotIn("Test Fit", combinations[0])
|
||||
|
||||
single_attribute_combinations = generate_keyed_value_combinations(
|
||||
{"Test Colour": ["Red", "Blue"], "Test Size": []}
|
||||
)
|
||||
self.assertEqual(single_attribute_combinations, [{"Test Colour": "Red"}, {"Test Colour": "Blue"}])
|
||||
|
||||
|
||||
def create_variant_with_tables(item, args):
|
||||
if isinstance(args, str):
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
{
|
||||
"custom_fields": [
|
||||
{
|
||||
"_assign": null,
|
||||
"_comments": null,
|
||||
"_liked_by": null,
|
||||
"_user_tags": null,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"collapsible_depends_on": null,
|
||||
"columns": 0,
|
||||
"creation": "2019-12-02 11:00:03.432994",
|
||||
"default": null,
|
||||
"depends_on": null,
|
||||
"description": null,
|
||||
"docstatus": 0,
|
||||
"dt": "Contact",
|
||||
"fetch_from": null,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "is_billing_contact",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"idx": 27,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"insert_after": "is_primary_contact",
|
||||
"label": "Is Billing Contact",
|
||||
"length": 0,
|
||||
"modified": "2019-12-02 11:00:03.432994",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Contact-is_billing_contact",
|
||||
"no_copy": 0,
|
||||
"options": null,
|
||||
"owner": "Administrator",
|
||||
"parent": null,
|
||||
"parentfield": null,
|
||||
"parenttype": null,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": null,
|
||||
"read_only": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0,
|
||||
"width": null
|
||||
}
|
||||
],
|
||||
"custom_perms": [],
|
||||
"doctype": "Contact",
|
||||
"property_setters": [],
|
||||
"sync_on_migrate": 1
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1638,12 +1638,12 @@ def add_non_stock_items_cost(stock_entry, work_order, expense_account, job_card=
|
||||
)
|
||||
|
||||
|
||||
def add_operating_cost_component_wise(
|
||||
stock_entry, work_order=None, consumed_operating_cost=None, op_expense_account=None, job_card=None
|
||||
):
|
||||
def add_operating_cost_component_wise(stock_entry, work_order=None, op_expense_account=None, job_card=None):
|
||||
if not work_order:
|
||||
return False
|
||||
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry import get_consumed_operating_cost
|
||||
|
||||
cost_added = False
|
||||
for row in work_order.operations:
|
||||
if job_card and job_card.operation_id != row.name:
|
||||
@@ -1661,18 +1661,32 @@ def add_operating_cost_component_wise(
|
||||
},
|
||||
)
|
||||
|
||||
consumed_operating_cost = (
|
||||
get_consumed_operating_cost(work_order.name, stock_entry.bom_no, row.name) or []
|
||||
)
|
||||
for wc in workstation_cost:
|
||||
expense_account = (
|
||||
get_component_account(wc.operating_component, stock_entry.company) or op_expense_account
|
||||
)
|
||||
consumed_op_cost = next(
|
||||
(
|
||||
cost
|
||||
for cost in consumed_operating_cost
|
||||
if cost.get("operating_component") == wc.operating_component
|
||||
),
|
||||
{},
|
||||
)
|
||||
actual_cp_operating_cost = flt(
|
||||
flt(wc.operating_cost) * flt(flt(row.actual_operation_time) / 60.0) - consumed_operating_cost,
|
||||
flt(wc.operating_cost) * flt(flt(row.actual_operation_time) / 60.0)
|
||||
- flt(consumed_op_cost.get("consumed_cost")),
|
||||
row.precision("actual_operating_cost"),
|
||||
)
|
||||
|
||||
per_unit_cost = flt(actual_cp_operating_cost) / flt(row.completed_qty - work_order.produced_qty)
|
||||
remaining_qty = row.completed_qty - consumed_op_cost.get("consumed_qty", 0)
|
||||
per_unit_cost = actual_cp_operating_cost / (remaining_qty or 1)
|
||||
operating_cost = per_unit_cost * stock_entry.fg_completed_qty
|
||||
|
||||
if per_unit_cost:
|
||||
if actual_cp_operating_cost:
|
||||
stock_entry.append(
|
||||
"additional_costs",
|
||||
{
|
||||
@@ -1680,8 +1694,14 @@ def add_operating_cost_component_wise(
|
||||
"description": _("{0} Operating Cost for operation {1}").format(
|
||||
wc.operating_component, row.operation
|
||||
),
|
||||
"amount": per_unit_cost * flt(stock_entry.fg_completed_qty),
|
||||
"amount": flt(
|
||||
min(operating_cost, actual_cp_operating_cost),
|
||||
frappe.get_precision("Landed Cost Taxes and Charges", "amount"),
|
||||
),
|
||||
"has_operating_cost": 1,
|
||||
"operation_id": row.name,
|
||||
"operating_component": wc.operating_component,
|
||||
"qty": min(remaining_qty, stock_entry.fg_completed_qty),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1699,17 +1719,15 @@ def get_component_account(parent, company):
|
||||
|
||||
def add_operations_cost(stock_entry, work_order=None, expense_account=None, job_card=None):
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry import (
|
||||
get_consumed_operating_cost,
|
||||
get_operating_cost_per_unit,
|
||||
get_remaining_operating_cost,
|
||||
)
|
||||
|
||||
operating_cost_per_unit = get_operating_cost_per_unit(work_order, stock_entry.bom_no)
|
||||
remaining_operating_cost = get_remaining_operating_cost(work_order, stock_entry.bom_no)
|
||||
|
||||
if operating_cost_per_unit:
|
||||
if remaining_operating_cost:
|
||||
cost_added = add_operating_cost_component_wise(
|
||||
stock_entry,
|
||||
work_order,
|
||||
get_consumed_operating_cost(work_order.name, stock_entry.bom_no),
|
||||
expense_account,
|
||||
job_card=job_card,
|
||||
)
|
||||
@@ -1720,7 +1738,10 @@ def add_operations_cost(stock_entry, work_order=None, expense_account=None, job_
|
||||
{
|
||||
"expense_account": expense_account,
|
||||
"description": _("Operating Cost as per Work Order / BOM"),
|
||||
"amount": operating_cost_per_unit * flt(stock_entry.fg_completed_qty),
|
||||
"amount": flt(
|
||||
remaining_operating_cost * stock_entry.fg_completed_qty,
|
||||
frappe.get_precision("Landed Cost Taxes and Charges", "amount"),
|
||||
),
|
||||
"has_operating_cost": 1,
|
||||
},
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,30 +1,40 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_bulk_edit": 1,
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2026-03-31 21:06:16.282931",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"section_break_smqo",
|
||||
"job_card_dashboard",
|
||||
"section_break_fsba",
|
||||
"work_order",
|
||||
"column_break_uqjq",
|
||||
"production_item",
|
||||
"column_break_qrpg",
|
||||
"for_quantity",
|
||||
"column_break_yecz",
|
||||
"bom_no",
|
||||
"section_break_oisd",
|
||||
"company",
|
||||
"naming_series",
|
||||
"work_order",
|
||||
"employee",
|
||||
"column_break_4",
|
||||
"posting_date",
|
||||
"project",
|
||||
"bom_no",
|
||||
"is_subcontracted",
|
||||
"semi_finished_good__finished_good_section",
|
||||
"finished_good",
|
||||
"production_item",
|
||||
"semi_fg_bom",
|
||||
"total_completed_qty",
|
||||
"column_break_mcnb",
|
||||
"for_quantity",
|
||||
"transferred_qty",
|
||||
"manufactured_qty",
|
||||
"semi_fg_bom",
|
||||
"section_break_folk",
|
||||
"pending_qty",
|
||||
"column_break_cyjw",
|
||||
"process_loss_qty",
|
||||
"total_completed_qty",
|
||||
"section_break_wpjf",
|
||||
"transferred_qty",
|
||||
"column_break_lgte",
|
||||
"manufactured_qty",
|
||||
"production_section",
|
||||
"operation",
|
||||
"source_warehouse",
|
||||
@@ -35,6 +45,7 @@
|
||||
"workstation_type",
|
||||
"workstation",
|
||||
"target_warehouse",
|
||||
"employee",
|
||||
"section_break_8",
|
||||
"items",
|
||||
"quality_inspection_section",
|
||||
@@ -71,8 +82,10 @@
|
||||
"item_name",
|
||||
"requested_qty",
|
||||
"is_paused",
|
||||
"is_subcontracted",
|
||||
"track_semi_finished_goods",
|
||||
"column_break_20",
|
||||
"project",
|
||||
"remarks",
|
||||
"section_break_dfoc",
|
||||
"status",
|
||||
@@ -155,6 +168,7 @@
|
||||
"fieldname": "wip_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "WIP Warehouse",
|
||||
"link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]",
|
||||
"mandatory_depends_on": "eval:!doc.finished_good || doc.skip_material_transfer === 0 || (doc.skip_material_transfer && doc.backflush_from_wip_warehouse)",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
@@ -506,6 +520,7 @@
|
||||
"fieldname": "target_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Target Warehouse",
|
||||
"link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]",
|
||||
"mandatory_depends_on": "eval:doc.track_semi_finished_goods",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
@@ -518,6 +533,7 @@
|
||||
"fieldname": "source_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Source Warehouse",
|
||||
"link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
@@ -622,12 +638,64 @@
|
||||
"fieldname": "secondary_items_section",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Secondary Items"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_folk",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_cyjw",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "pending_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Pending Qty"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_wpjf",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_lgte",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "job_card_dashboard",
|
||||
"fieldtype": "HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_oisd",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_uqjq",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_qrpg",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_yecz",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_smqo",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_fsba",
|
||||
"fieldtype": "Section Break"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-03-31 21:06:48.987740",
|
||||
"modified": "2026-05-21 18:37:05.688342",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Job Card",
|
||||
|
||||
@@ -103,6 +103,7 @@ class JobCard(Document):
|
||||
operation_id: DF.Data | None
|
||||
operation_row_id: DF.Int
|
||||
operation_row_number: DF.Literal[None]
|
||||
pending_qty: DF.Float
|
||||
posting_date: DF.Date | None
|
||||
process_loss_qty: DF.Float
|
||||
production_item: DF.Link | None
|
||||
@@ -881,7 +882,9 @@ class JobCard(Document):
|
||||
|
||||
precision = self.precision("total_completed_qty")
|
||||
total_completed_qty = flt(
|
||||
flt(self.total_completed_qty, precision) + flt(self.process_loss_qty, precision)
|
||||
flt(self.total_completed_qty, precision)
|
||||
+ flt(self.process_loss_qty, precision)
|
||||
+ flt(self.pending_qty, precision)
|
||||
)
|
||||
|
||||
if self.for_quantity and flt(total_completed_qty, precision) != flt(self.for_quantity, precision):
|
||||
@@ -928,8 +931,10 @@ class JobCard(Document):
|
||||
|
||||
self.process_loss_qty = 0.0
|
||||
if self.total_completed_qty and self.for_quantity > self.total_completed_qty:
|
||||
self.process_loss_qty = flt(self.for_quantity, precision) - flt(
|
||||
self.total_completed_qty, precision
|
||||
self.process_loss_qty = (
|
||||
flt(self.for_quantity, precision)
|
||||
- flt(self.total_completed_qty, precision)
|
||||
- flt(self.pending_qty, precision)
|
||||
)
|
||||
|
||||
def update_work_order(self):
|
||||
@@ -943,13 +948,14 @@ class JobCard(Document):
|
||||
):
|
||||
return
|
||||
|
||||
for_quantity, time_in_mins, process_loss_qty = 0, 0, 0
|
||||
for_quantity, time_in_mins, process_loss_qty, pending_qty = 0, 0, 0, 0
|
||||
|
||||
data = self.get_current_operation_data()
|
||||
if data and len(data) > 0:
|
||||
for_quantity = flt(data[0].completed_qty)
|
||||
time_in_mins = flt(data[0].time_in_mins)
|
||||
process_loss_qty = flt(data[0].process_loss_qty)
|
||||
pending_qty = flt(data[0].pending_qty)
|
||||
|
||||
wo = frappe.get_doc("Work Order", self.work_order)
|
||||
|
||||
@@ -957,8 +963,8 @@ class JobCard(Document):
|
||||
self.update_corrective_in_work_order(wo)
|
||||
|
||||
elif self.operation_id:
|
||||
self.validate_produced_quantity(for_quantity, process_loss_qty, wo)
|
||||
self.update_work_order_data(for_quantity, process_loss_qty, time_in_mins, wo)
|
||||
self.validate_produced_quantity(for_quantity, process_loss_qty, pending_qty, wo)
|
||||
self.update_work_order_data(for_quantity, process_loss_qty, pending_qty, time_in_mins, wo)
|
||||
|
||||
def update_semi_finished_good_details(self):
|
||||
if self.operation_id:
|
||||
@@ -987,11 +993,11 @@ class JobCard(Document):
|
||||
wo.flags.ignore_validate_update_after_submit = True
|
||||
wo.save()
|
||||
|
||||
def validate_produced_quantity(self, for_quantity, process_loss_qty, wo):
|
||||
def validate_produced_quantity(self, for_quantity, process_loss_qty, pending_qty, wo):
|
||||
if self.docstatus < 2:
|
||||
return
|
||||
|
||||
if wo.produced_qty > for_quantity + process_loss_qty:
|
||||
if wo.produced_qty > for_quantity + process_loss_qty + pending_qty:
|
||||
first_part_msg = _(
|
||||
"The {0} {1} is used to calculate the valuation cost for the finished good {2}."
|
||||
).format(frappe.bold(_("Job Card")), frappe.bold(self.name), frappe.bold(self.production_item))
|
||||
@@ -1004,7 +1010,7 @@ class JobCard(Document):
|
||||
_("{0} {1}").format(first_part_msg, second_part_msg), JobCardCancelError, title=_("Error")
|
||||
)
|
||||
|
||||
def update_work_order_data(self, for_quantity, process_loss_qty, time_in_mins, wo):
|
||||
def update_work_order_data(self, for_quantity, process_loss_qty, pending_qty, time_in_mins, wo):
|
||||
workstation_hour_rate = frappe.get_value("Workstation", self.workstation, "hour_rate")
|
||||
jc = frappe.qb.DocType("Job Card")
|
||||
jctl = frappe.qb.DocType("Job Card Time Log")
|
||||
@@ -1026,6 +1032,7 @@ class JobCard(Document):
|
||||
if data.get("name") == self.operation_id:
|
||||
data.completed_qty = for_quantity
|
||||
data.process_loss_qty = process_loss_qty
|
||||
data.pending_qty = pending_qty
|
||||
data.actual_operation_time = time_in_mins
|
||||
data.actual_start_time = time_data[0].start_time if time_data else None
|
||||
data.actual_end_time = time_data[0].end_time if time_data else None
|
||||
@@ -1051,6 +1058,7 @@ class JobCard(Document):
|
||||
{"SUM": "total_time_in_mins", "as": "time_in_mins"},
|
||||
{"SUM": "total_completed_qty", "as": "completed_qty"},
|
||||
{"SUM": "process_loss_qty", "as": "process_loss_qty"},
|
||||
{"SUM": "pending_qty", "as": "pending_qty"},
|
||||
],
|
||||
filters={
|
||||
"docstatus": 1,
|
||||
@@ -1445,10 +1453,19 @@ class JobCard(Document):
|
||||
if isinstance(kwargs, dict):
|
||||
kwargs = frappe._dict(kwargs)
|
||||
|
||||
if kwargs.end_time:
|
||||
if kwargs.for_quantity:
|
||||
self.for_quantity = kwargs.for_quantity
|
||||
if flt(kwargs.pending_qty) and flt(kwargs.pending_qty) < 0:
|
||||
frappe.throw(_("Pending quantity cannot be negative."))
|
||||
|
||||
if flt(kwargs.process_loss_qty) and flt(kwargs.process_loss_qty) < 0:
|
||||
frappe.throw(_("Process loss quantity cannot be negative."))
|
||||
|
||||
if flt(kwargs.pending_qty) and flt(kwargs.pending_qty) > self.for_quantity:
|
||||
frappe.throw(_("Pending quantity cannot be greater than the for quantity."))
|
||||
|
||||
self.pending_qty = flt(kwargs.pending_qty)
|
||||
self.process_loss_qty = flt(kwargs.process_loss_qty)
|
||||
|
||||
if kwargs.end_time:
|
||||
self.add_time_logs(
|
||||
to_time=kwargs.end_time,
|
||||
completed_qty=kwargs.qty,
|
||||
|
||||
@@ -720,6 +720,7 @@ class TestJobCard(ERPNextTestSuite):
|
||||
)
|
||||
|
||||
jc.time_logs[0].completed_qty = 8
|
||||
jc.pending_qty = 0.0
|
||||
jc.save()
|
||||
jc.submit()
|
||||
|
||||
@@ -1080,6 +1081,243 @@ class TestJobCard(ERPNextTestSuite):
|
||||
self.assertEqual(s.items[3].item_code, "_Test Item")
|
||||
self.assertEqual(s.items[3].transfer_qty, 2)
|
||||
|
||||
@ERPNextTestSuite.change_settings(
|
||||
"Manufacturing Settings", {"overproduction_percentage_for_work_order": 100}
|
||||
)
|
||||
def test_operating_cost_with_overproduction(self):
|
||||
from erpnext.manufacturing.doctype.routing.test_routing import (
|
||||
create_routing,
|
||||
setup_bom,
|
||||
setup_operations,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import make_job_card
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
make_stock_entry as make_stock_entry_for_wo,
|
||||
)
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
workstation = make_workstation(
|
||||
workstation_name="Test Workstation for Overproduction", hour_rate_rent=10, hour_rate_labour=10
|
||||
)
|
||||
operations = [
|
||||
{"operation": "Test Operation 1", "workstation": workstation.name, "time_in_mins": 30},
|
||||
{"operation": "Test Operation 2", "workstation": workstation.name, "time_in_mins": 30},
|
||||
]
|
||||
warehouse = create_warehouse("Test Warehouse for Overproduction")
|
||||
setup_operations(operations)
|
||||
|
||||
fg = make_item("Test FG for Overproduction", {"stock_uom": "Nos", "is_stock_item": 1})
|
||||
rm = make_item("Test RM for Overproduction", {"stock_uom": "Nos", "is_stock_item": 1})
|
||||
|
||||
routing_doc = create_routing(routing_name="Testing Route", operations=operations)
|
||||
bom_doc = setup_bom(
|
||||
item_code=fg.name,
|
||||
routing=routing_doc.name,
|
||||
raw_materials=[rm.name],
|
||||
source_warehouse=warehouse,
|
||||
)
|
||||
|
||||
for row in bom_doc.items:
|
||||
make_stock_entry(
|
||||
item_code=row.item_code,
|
||||
target=row.source_warehouse,
|
||||
qty=100,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
wo_doc = make_wo_order_test_record(
|
||||
production_item=fg.name,
|
||||
bom_no=bom_doc.name,
|
||||
qty=10,
|
||||
skip_transfer=1,
|
||||
source_warehouse=warehouse,
|
||||
)
|
||||
|
||||
first_operation = frappe.get_all(
|
||||
"Job Card",
|
||||
filters={"work_order": wo_doc.name, "sequence_id": 1},
|
||||
fields=["name"],
|
||||
order_by="sequence_id",
|
||||
limit=1,
|
||||
)[0].name
|
||||
|
||||
jc = frappe.get_doc("Job Card", first_operation)
|
||||
from_time = add_to_date(now(), days=1)
|
||||
for _ in jc.scheduled_time_logs:
|
||||
jc.append(
|
||||
"time_logs",
|
||||
{
|
||||
"from_time": from_time,
|
||||
"to_time": add_to_date(from_time, days=1),
|
||||
"completed_qty": 4,
|
||||
},
|
||||
)
|
||||
jc.for_quantity = 4
|
||||
jc.save()
|
||||
jc.submit()
|
||||
|
||||
second_operation = frappe.get_all(
|
||||
"Job Card",
|
||||
filters={"work_order": wo_doc.name, "sequence_id": 2},
|
||||
fields=["name"],
|
||||
order_by="sequence_id",
|
||||
limit=1,
|
||||
)[0].name
|
||||
|
||||
jc = frappe.get_doc("Job Card", second_operation)
|
||||
from_time = add_to_date(now(), days=2)
|
||||
for _ in jc.scheduled_time_logs:
|
||||
jc.append(
|
||||
"time_logs",
|
||||
{
|
||||
"from_time": from_time,
|
||||
"to_time": add_to_date(from_time, days=2),
|
||||
"completed_qty": 4,
|
||||
},
|
||||
)
|
||||
jc.for_quantity = 4
|
||||
jc.save()
|
||||
jc.submit()
|
||||
|
||||
s = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 6)) # overproduction
|
||||
s.submit()
|
||||
|
||||
self.assertEqual(s.additional_costs[0].amount, 240)
|
||||
self.assertEqual(s.additional_costs[1].amount, 240)
|
||||
self.assertEqual(s.additional_costs[2].amount, 480)
|
||||
self.assertEqual(s.additional_costs[3].amount, 480)
|
||||
|
||||
make_job_card(
|
||||
wo_doc.name,
|
||||
[
|
||||
{
|
||||
"name": wo_doc.operations[0].name,
|
||||
"operation": "Test Operation 1",
|
||||
"qty": 2,
|
||||
"pending_qty": 2,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
job_card = frappe.get_last_doc("Job Card", {"work_order": wo_doc.name})
|
||||
from_time = add_to_date(now(), days=4)
|
||||
job_card.append(
|
||||
"time_logs",
|
||||
{
|
||||
"from_time": from_time,
|
||||
"to_time": add_to_date(from_time, days=1),
|
||||
"completed_qty": 2,
|
||||
},
|
||||
)
|
||||
job_card.for_quantity = 2
|
||||
job_card.save()
|
||||
job_card.submit()
|
||||
|
||||
make_job_card(
|
||||
wo_doc.name,
|
||||
[
|
||||
{
|
||||
"name": wo_doc.operations[1].name,
|
||||
"operation": "Test Operation 2",
|
||||
"qty": 2,
|
||||
"pending_qty": 2,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
job_card = frappe.get_last_doc("Job Card", {"work_order": wo_doc.name})
|
||||
from_time = add_to_date(now(), days=5)
|
||||
job_card.append(
|
||||
"time_logs",
|
||||
{
|
||||
"from_time": from_time,
|
||||
"to_time": add_to_date(from_time, days=2),
|
||||
"completed_qty": 2,
|
||||
},
|
||||
)
|
||||
job_card.for_quantity = 2
|
||||
job_card.save()
|
||||
job_card.submit()
|
||||
|
||||
s2 = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 1))
|
||||
s2.submit()
|
||||
|
||||
self.assertEqual(s2.additional_costs[0].amount, 120)
|
||||
self.assertEqual(s2.additional_costs[1].amount, 120)
|
||||
self.assertEqual(s2.additional_costs[2].amount, 240)
|
||||
self.assertEqual(s2.additional_costs[3].amount, 240)
|
||||
|
||||
make_job_card(
|
||||
wo_doc.name,
|
||||
[
|
||||
{
|
||||
"name": wo_doc.operations[0].name,
|
||||
"operation": "Test Operation 1",
|
||||
"qty": 2,
|
||||
"pending_qty": 2,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
job_card = frappe.get_last_doc("Job Card", {"work_order": wo_doc.name})
|
||||
from_time = add_to_date(now(), days=7)
|
||||
job_card.append(
|
||||
"time_logs",
|
||||
{
|
||||
"from_time": from_time,
|
||||
"to_time": add_to_date(from_time, days=1),
|
||||
"completed_qty": 2,
|
||||
},
|
||||
)
|
||||
job_card.for_quantity = 2
|
||||
job_card.save()
|
||||
job_card.submit()
|
||||
|
||||
make_job_card(
|
||||
wo_doc.name,
|
||||
[
|
||||
{
|
||||
"name": wo_doc.operations[1].name,
|
||||
"operation": "Test Operation 2",
|
||||
"qty": 2,
|
||||
"pending_qty": 2,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
job_card = frappe.get_last_doc("Job Card", {"work_order": wo_doc.name})
|
||||
from_time = add_to_date(now(), days=8)
|
||||
job_card.append(
|
||||
"time_logs",
|
||||
{
|
||||
"from_time": from_time,
|
||||
"to_time": add_to_date(from_time, days=2),
|
||||
"completed_qty": 2,
|
||||
},
|
||||
)
|
||||
job_card.for_quantity = 2
|
||||
job_card.save()
|
||||
job_card.submit()
|
||||
|
||||
s = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 2))
|
||||
s.submit()
|
||||
|
||||
self.assertEqual(s.additional_costs[0].amount, 240)
|
||||
self.assertEqual(s.additional_costs[1].amount, 240)
|
||||
self.assertEqual(s.additional_costs[2].amount, 480)
|
||||
self.assertEqual(s.additional_costs[3].amount, 480)
|
||||
|
||||
s2.cancel()
|
||||
|
||||
s = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 3))
|
||||
s.submit()
|
||||
|
||||
self.assertEqual(s.additional_costs[0].amount, 240)
|
||||
self.assertEqual(s.additional_costs[1].amount, 240)
|
||||
self.assertEqual(s.additional_costs[2].amount, 480)
|
||||
self.assertEqual(s.additional_costs[3].amount, 480)
|
||||
|
||||
|
||||
def create_bom_with_multiple_operations():
|
||||
"Create a BOM with multiple operations and Material Transfer against Job Card"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_bulk_edit": 1,
|
||||
"creation": "2018-07-09 17:20:44.737289",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
@@ -34,6 +35,7 @@
|
||||
"ignore_user_permissions": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Source Warehouse",
|
||||
"link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
@@ -113,7 +115,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-04 14:30:19.472294",
|
||||
"modified": "2026-05-12 12:22:18.506904",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Job Card Item",
|
||||
|
||||
@@ -1315,6 +1315,7 @@ def get_exploded_items(item_details, company, bom_no, include_non_stock_items, p
|
||||
item_uom.conversion_factor,
|
||||
item.safety_stock,
|
||||
bom.item.as_("main_bom_item"),
|
||||
bom.name.as_("main_bom"),
|
||||
)
|
||||
.where(
|
||||
(bei.docstatus < 2)
|
||||
@@ -1384,6 +1385,7 @@ def get_subitems(
|
||||
item.purchase_uom,
|
||||
item_uom.conversion_factor,
|
||||
bom.item.as_("main_bom_item"),
|
||||
bom.name.as_("main_bom"),
|
||||
bom_item.is_phantom_item,
|
||||
)
|
||||
.where(
|
||||
|
||||
@@ -47,11 +47,3 @@ frappe.ui.form.on("Sales Forecast", {
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Sales Forecast Item", {
|
||||
adjust_qty(frm, cdt, cdn) {
|
||||
let row = locals[cdt][cdn];
|
||||
row.demand_qty = row.forecast_qty + row.adjust_qty;
|
||||
frappe.model.set_value(cdt, cdn, "demand_qty", row.demand_qty);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -10,8 +10,6 @@
|
||||
"item_name",
|
||||
"uom",
|
||||
"delivery_date",
|
||||
"forecast_qty",
|
||||
"adjust_qty",
|
||||
"demand_qty",
|
||||
"warehouse"
|
||||
],
|
||||
@@ -55,22 +53,6 @@
|
||||
"label": "Delivery Date",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "forecast_qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Forecast Qty",
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "adjust_qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Adjust Qty"
|
||||
},
|
||||
{
|
||||
"columns": 3,
|
||||
"fieldname": "demand_qty",
|
||||
@@ -94,7 +76,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-18 21:59:38.859082",
|
||||
"modified": "2026-05-21 12:38:47.636301",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Sales Forecast Item",
|
||||
|
||||
@@ -14,10 +14,8 @@ class SalesForecastItem(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
adjust_qty: DF.Float
|
||||
delivery_date: DF.Date | None
|
||||
demand_qty: DF.Float
|
||||
forecast_qty: DF.Float
|
||||
item_code: DF.Link
|
||||
item_name: DF.Data | None
|
||||
parent: DF.Data
|
||||
|
||||
@@ -12,21 +12,10 @@ frappe.ui.form.on("Work Order", {
|
||||
frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"];
|
||||
|
||||
// Set query for warehouses
|
||||
frm.set_query("wip_warehouse", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("source_warehouse", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
frm.events.set_company_filters(frm, "wip_warehouse");
|
||||
frm.events.set_company_filters(frm, "source_warehouse");
|
||||
frm.events.set_company_filters(frm, "fg_warehouse");
|
||||
frm.events.set_company_filters(frm, "scrap_warehouse");
|
||||
|
||||
frm.set_query("source_warehouse", "required_items", function () {
|
||||
return {
|
||||
@@ -44,24 +33,6 @@ frappe.ui.form.on("Work Order", {
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("fg_warehouse", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("scrap_warehouse", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Set query for BOM
|
||||
frm.set_query("bom_no", function () {
|
||||
if (frm.doc.production_item) {
|
||||
@@ -118,6 +89,16 @@ frappe.ui.form.on("Work Order", {
|
||||
});
|
||||
},
|
||||
|
||||
set_company_filters(frm, fieldname) {
|
||||
frm.set_query(fieldname, () => {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
onload: function (frm) {
|
||||
if (!frm.doc.status) frm.doc.status = "Draft";
|
||||
|
||||
@@ -348,7 +329,7 @@ frappe.ui.form.on("Work Order", {
|
||||
{
|
||||
fieldtype: "Data",
|
||||
fieldname: "name",
|
||||
label: __("Operation Id"),
|
||||
label: __("Operation ID"),
|
||||
},
|
||||
{
|
||||
fieldtype: "Float",
|
||||
@@ -425,6 +406,7 @@ frappe.ui.form.on("Work Order", {
|
||||
|
||||
if (pending_qty) {
|
||||
dialog.fields_dict.operations.df.data.push({
|
||||
__checked: 1,
|
||||
name: data.name,
|
||||
operation: data.operation,
|
||||
workstation: data.workstation,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_bulk_edit": 1,
|
||||
"allow_import": 1,
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2025-04-09 12:09:40.634472",
|
||||
@@ -266,6 +267,7 @@
|
||||
"fieldname": "wip_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Work-in-Progress Warehouse",
|
||||
"link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]",
|
||||
"mandatory_depends_on": "eval:(!doc.skip_transfer || doc.from_wip_warehouse) && !doc.track_semi_finished_goods",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
@@ -274,6 +276,7 @@
|
||||
"fieldname": "fg_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Target Warehouse",
|
||||
"link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]",
|
||||
"options": "Warehouse",
|
||||
"read_only_depends_on": "subcontracting_inward_order"
|
||||
},
|
||||
@@ -286,6 +289,7 @@
|
||||
"fieldname": "scrap_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Scrap Warehouse",
|
||||
"link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
@@ -513,6 +517,7 @@
|
||||
"fieldname": "source_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Source Warehouse",
|
||||
"link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]",
|
||||
"options": "Warehouse",
|
||||
"read_only_depends_on": "eval:doc.subcontracting_inward_order"
|
||||
},
|
||||
@@ -706,7 +711,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-04-17 13:42:12.374055",
|
||||
"modified": "2026-05-19 12:20:38.102403",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Work Order",
|
||||
|
||||
@@ -167,18 +167,19 @@ class WorkOrder(Document):
|
||||
self.set_onload("backflush_raw_materials_based_on", based_on)
|
||||
|
||||
def show_create_job_card_button(self):
|
||||
operation_details = frappe._dict(
|
||||
frappe.get_all(
|
||||
"Job Card",
|
||||
fields=["operation", {"SUM": "for_quantity"}],
|
||||
filters={"docstatus": ("<", 2), "work_order": self.name},
|
||||
as_list=1,
|
||||
group_by="operation_id",
|
||||
)
|
||||
jc_doctype = frappe.qb.DocType("Job Card")
|
||||
query = (
|
||||
frappe.qb.from_(jc_doctype)
|
||||
.select(jc_doctype.operation_id, Sum(jc_doctype.for_quantity - IfNull(jc_doctype.pending_qty, 0)))
|
||||
.where((jc_doctype.docstatus < 2) & (jc_doctype.work_order == self.name))
|
||||
.groupby(jc_doctype.operation_id)
|
||||
)
|
||||
|
||||
operation_details = query.run(as_list=1)
|
||||
operation_details = frappe._dict(operation_details)
|
||||
|
||||
for d in self.operations:
|
||||
job_card_qty = self.qty - flt(operation_details.get(d.operation))
|
||||
job_card_qty = self.qty - flt(operation_details.get(d.name))
|
||||
if job_card_qty > 0:
|
||||
return True
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_bulk_edit": 1,
|
||||
"creation": "2016-04-18 07:38:26.314642",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
@@ -53,6 +54,7 @@
|
||||
"ignore_user_permissions": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Source Warehouse",
|
||||
"link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]",
|
||||
"options": "Warehouse",
|
||||
"read_only_depends_on": "eval:parent.subcontracting_inward_order && doc.is_customer_provided_item"
|
||||
},
|
||||
@@ -207,7 +209,7 @@
|
||||
"grid_page_length": 50,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-02 11:16:05.081613",
|
||||
"modified": "2026-05-12 12:05:16.687866",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Work Order Item",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_bulk_edit": 1,
|
||||
"creation": "2025-04-09 12:12:19.824560",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
@@ -10,6 +11,7 @@
|
||||
"status",
|
||||
"completed_qty",
|
||||
"process_loss_qty",
|
||||
"pending_qty",
|
||||
"column_break_4",
|
||||
"bom",
|
||||
"workstation_type",
|
||||
@@ -301,13 +303,20 @@
|
||||
"fieldname": "quality_inspection_required",
|
||||
"fieldtype": "Check",
|
||||
"label": "Quality Inspection Required"
|
||||
},
|
||||
{
|
||||
"fieldname": "pending_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Pending Qty",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-03-30 17:20:08.874381",
|
||||
"modified": "2026-05-20 13:01:21.827200",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Work Order Operation",
|
||||
|
||||
@@ -32,6 +32,7 @@ class WorkOrderOperation(Document):
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
pending_qty: DF.Float
|
||||
planned_end_time: DF.Datetime | None
|
||||
planned_operating_cost: DF.Currency
|
||||
planned_start_time: DF.Datetime | None
|
||||
|
||||
@@ -482,3 +482,4 @@ erpnext.patches.v16_0.packed_item_inv_dimen
|
||||
erpnext.patches.v16_0.fix_titles
|
||||
erpnext.patches.v16_0.set_not_applicable_on_german_item_tax_templates
|
||||
erpnext.patches.v16_0.clear_procedures_from_receivable_report
|
||||
erpnext.patches.v16_0.migrate_address_contact_custom_fields
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import frappe
|
||||
|
||||
from erpnext.setup.install import create_address_and_contact_custom_fields
|
||||
|
||||
|
||||
def execute():
|
||||
"""Replace fixture-based custom fields on Address and Contact with programmatic ones."""
|
||||
for custom_field in (
|
||||
"Address-tax_category",
|
||||
"Address-is_your_company_address",
|
||||
"Contact-is_billing_contact",
|
||||
):
|
||||
if frappe.db.exists("Custom Field", custom_field):
|
||||
frappe.delete_doc("Custom Field", custom_field, ignore_missing=True, force=True)
|
||||
|
||||
create_address_and_contact_custom_fields()
|
||||
@@ -181,6 +181,7 @@
|
||||
"fieldtype": "Link",
|
||||
"in_global_search": 1,
|
||||
"label": "Customer",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "customer",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Customer",
|
||||
@@ -195,6 +196,7 @@
|
||||
"fieldname": "sales_order",
|
||||
"fieldtype": "Link",
|
||||
"label": "Sales Order",
|
||||
"no_copy": 1,
|
||||
"options": "Sales Order"
|
||||
},
|
||||
{
|
||||
@@ -480,7 +482,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"max_attachments": 4,
|
||||
"modified": "2026-04-14 18:17:40.676750",
|
||||
"modified": "2026-05-22 16:45:50.762759",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Projects",
|
||||
"name": "Project",
|
||||
|
||||
@@ -2266,6 +2266,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
for (const [key, value] of Object.entries(child)) {
|
||||
if (!["doctype", "name"].includes(key)) {
|
||||
if (key === "price_list_rate") {
|
||||
const doc = frappe.get_doc(child.doctype, child.name);
|
||||
if (doc) doc.price_list_rate = value; // silent update so rate trigger uses correct value
|
||||
frappe.model.set_value(child.doctype, child.name, "rate", value);
|
||||
}
|
||||
|
||||
|
||||
@@ -483,6 +483,8 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
warehouse:
|
||||
this.item.s_warehouse || this.item.t_warehouse || this.item.warehouse,
|
||||
is_inward: is_inward,
|
||||
posting_date: this.frm.doc.posting_date,
|
||||
posting_time: this.frm.doc.posting_time,
|
||||
include_expired_batches: include_expired_batches,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,6 +2,21 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Party Specific Item", {
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
setup: function (frm) {
|
||||
frm.trigger("party_type");
|
||||
},
|
||||
|
||||
party_type: function (frm) {
|
||||
if (["Customer Group", "Supplier Group"].includes(frm.doc.party_type)) {
|
||||
frm.set_query("party", function () {
|
||||
return {
|
||||
filters: {
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
} else {
|
||||
frm.set_query("party", null);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_bulk_edit": 1,
|
||||
"allow_import": 1,
|
||||
"creation": "2021-08-27 19:28:07.559978",
|
||||
"doctype": "DocType",
|
||||
@@ -18,7 +19,7 @@
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Party Type",
|
||||
"options": "Customer\nSupplier",
|
||||
"options": "Customer\nCustomer Group\nSupplier\nSupplier Group",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -52,7 +53,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:08.752476",
|
||||
"modified": "2026-05-17 11:46:17.634855",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Party Specific Item",
|
||||
@@ -71,9 +72,10 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "party",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ class PartySpecificItem(Document):
|
||||
|
||||
based_on_value: DF.DynamicLink
|
||||
party: DF.DynamicLink
|
||||
party_type: DF.Literal["Customer", "Supplier"]
|
||||
party_type: DF.Literal["Customer", "Customer Group", "Supplier", "Supplier Group"]
|
||||
restrict_based_on: DF.Literal["Item", "Item Group", "Brand"]
|
||||
# end: auto-generated types
|
||||
|
||||
|
||||
@@ -49,6 +49,23 @@ class TestPartySpecificItem(ERPNextTestSuite):
|
||||
)
|
||||
self.assertTrue(item in flatten(items))
|
||||
|
||||
def test_party_group(self):
|
||||
customer = "_Test Customer With Template"
|
||||
item = "_Test Item"
|
||||
frappe.set_value("Customer", customer, "customer_group", "Government")
|
||||
|
||||
create_party_specific_item(
|
||||
party_type="Customer Group",
|
||||
party="Government",
|
||||
restrict_based_on="Item",
|
||||
based_on_value=item,
|
||||
)
|
||||
filters = {"is_sales_item": 1, "customer": customer}
|
||||
items = item_query(
|
||||
doctype="Item", txt="", searchfield="name", start=0, page_len=20, filters=filters, as_dict=False
|
||||
)
|
||||
self.assertTrue(item in flatten(items))
|
||||
|
||||
|
||||
def flatten(lst):
|
||||
result = []
|
||||
|
||||
@@ -452,7 +452,7 @@ class SalesOrder(SellingController):
|
||||
and not cint(d.delivered_by_supplier)
|
||||
):
|
||||
frappe.throw(
|
||||
_("Delivery warehouse required for stock item {0}").format(d.item_code), WarehouseRequired
|
||||
_("Source warehouse required for stock item {0}").format(d.item_code), WarehouseRequired
|
||||
)
|
||||
|
||||
def validate_with_previous_doc(self):
|
||||
@@ -1798,6 +1798,7 @@ def make_work_orders(items, sales_order, company, project=None):
|
||||
return [p.name for p in out]
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_production_plan(source_name, target_doc=None):
|
||||
sales_order = frappe.get_doc("Sales Order", source_name)
|
||||
|
||||
|
||||
@@ -75,13 +75,11 @@ class CustomerGroup(NestedSet):
|
||||
|
||||
def get_parent_customer_groups(customer_group):
|
||||
lft, rgt = frappe.db.get_value("Customer Group", customer_group, ["lft", "rgt"])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""select name from `tabCustomer Group`
|
||||
where lft <= %s and rgt >= %s
|
||||
order by lft asc""",
|
||||
(lft, rgt),
|
||||
as_dict=True,
|
||||
return frappe.get_all(
|
||||
"Customer Group",
|
||||
filters=[["lft", "<=", lft], ["rgt", ">=", rgt]],
|
||||
fields=["name"],
|
||||
order_by="lft asc",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ frappe.ui.form.on("Employee", {
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
frm.fields_dict.date_of_birth.datepicker.update({ maxDate: new Date() });
|
||||
frm.fields_dict.date_of_birth.datepicker?.update({ maxDate: new Date() });
|
||||
|
||||
if (!frm.is_new() && !frm.doc.user_id) {
|
||||
frm.add_custom_button(__("Create User"), () => {
|
||||
|
||||
@@ -70,3 +70,13 @@ class SupplierGroup(NestedSet):
|
||||
def on_trash(self):
|
||||
NestedSet.validate_if_child_exists(self)
|
||||
frappe.utils.nestedset.update_nsm(self)
|
||||
|
||||
|
||||
def get_parent_supplier_groups(supplier_group):
|
||||
lft, rgt = frappe.db.get_value("Supplier Group", supplier_group, ["lft", "rgt"])
|
||||
return frappe.get_all(
|
||||
"Supplier Group",
|
||||
filters=[["lft", "<=", lft], ["rgt", ">=", rgt]],
|
||||
fields=["name"],
|
||||
order_by="lft asc",
|
||||
)
|
||||
|
||||
@@ -25,6 +25,7 @@ def after_install():
|
||||
setup_repost_defaults()
|
||||
create_print_setting_custom_fields()
|
||||
create_marketing_campaign_custom_fields()
|
||||
create_address_and_contact_custom_fields()
|
||||
create_custom_company_links()
|
||||
add_all_roles_to("Administrator")
|
||||
create_default_success_action()
|
||||
@@ -145,6 +146,37 @@ def create_marketing_campaign_custom_fields():
|
||||
)
|
||||
|
||||
|
||||
def create_address_and_contact_custom_fields():
|
||||
create_custom_fields(
|
||||
{
|
||||
"Address": [
|
||||
{
|
||||
"label": _("Tax Category"),
|
||||
"fieldname": "tax_category",
|
||||
"fieldtype": "Link",
|
||||
"options": "Tax Category",
|
||||
"insert_after": "fax",
|
||||
},
|
||||
{
|
||||
"label": _("Is Your Company Address"),
|
||||
"fieldname": "is_your_company_address",
|
||||
"fieldtype": "Check",
|
||||
"default": "0",
|
||||
"insert_after": "linked_with",
|
||||
},
|
||||
],
|
||||
"Contact": [
|
||||
{
|
||||
"label": _("Is Billing Contact"),
|
||||
"fieldname": "is_billing_contact",
|
||||
"fieldtype": "Check",
|
||||
"insert_after": "is_primary_contact",
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def create_default_success_action():
|
||||
for success_action in get_default_success_action():
|
||||
if not frappe.db.exists("Success Action", success_action.get("ref_doctype")):
|
||||
|
||||
@@ -543,6 +543,7 @@ class TestBatch(ERPNextTestSuite):
|
||||
"plc_conversion_rate": 1,
|
||||
"customer": "_Test Customer",
|
||||
"name": None,
|
||||
"qty": 1,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -286,14 +286,6 @@ frappe.ui.form.on("Item", {
|
||||
frm.set_df_property(fieldname, "read_only", stock_exists);
|
||||
});
|
||||
frm.set_df_property("is_fixed_asset", "read_only", frm.doc.__onload?.asset_exists ? 1 : 0);
|
||||
frm.toggle_reqd("customer", frm.doc.is_customer_provided_item ? 1 : 0);
|
||||
frm.set_query("item_group", () => {
|
||||
return {
|
||||
filters: {
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
validate: function (frm) {
|
||||
@@ -304,10 +296,6 @@ frappe.ui.form.on("Item", {
|
||||
refresh_field("image_view");
|
||||
},
|
||||
|
||||
is_customer_provided_item: function (frm) {
|
||||
frm.toggle_reqd("customer", frm.doc.is_customer_provided_item ? 1 : 0);
|
||||
},
|
||||
|
||||
is_fixed_asset: function (frm) {
|
||||
// set serial no to false & toggles its visibility
|
||||
frm.set_value("has_serial_no", 0);
|
||||
@@ -548,12 +536,6 @@ $.extend(erpnext.item, {
|
||||
};
|
||||
};
|
||||
|
||||
frm.fields_dict["item_group"].get_query = function (doc, cdt, cdn) {
|
||||
return {
|
||||
filters: [["Item Group", "docstatus", "!=", 2]],
|
||||
};
|
||||
};
|
||||
|
||||
frm.fields_dict["item_defaults"].grid.get_field("deferred_revenue_account").get_query = function (
|
||||
doc,
|
||||
cdt,
|
||||
@@ -790,11 +772,10 @@ $.extend(erpnext.item, {
|
||||
default: 0,
|
||||
onchange: function () {
|
||||
let selected_attributes = get_selected_attributes();
|
||||
let lengths = [];
|
||||
Object.keys(selected_attributes).map((key) => {
|
||||
lengths.push(selected_attributes[key].length);
|
||||
let lengths = Object.keys(selected_attributes).map((key) => {
|
||||
return selected_attributes[key].length;
|
||||
});
|
||||
if (lengths.includes(0)) {
|
||||
if (!lengths.length) {
|
||||
me.multiple_variant_dialog.get_primary_btn().html(__("Create Variants"));
|
||||
me.multiple_variant_dialog.disable_primary_action();
|
||||
} else {
|
||||
@@ -831,7 +812,7 @@ $.extend(erpnext.item, {
|
||||
fieldtype: "HTML",
|
||||
fieldname: "help",
|
||||
options: `<label class="control-label">
|
||||
${__("Select at least one value from each of the attributes.")}
|
||||
${__("Select at least one attribute value.")}
|
||||
</label>`,
|
||||
},
|
||||
]
|
||||
@@ -893,6 +874,9 @@ $.extend(erpnext.item, {
|
||||
selected_attributes[attribute_name].push($(opt).attr("data-fieldname"));
|
||||
}
|
||||
});
|
||||
if (!selected_attributes[attribute_name].length) {
|
||||
delete selected_attributes[attribute_name];
|
||||
}
|
||||
});
|
||||
|
||||
return selected_attributes;
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
"column_break_kpmi",
|
||||
"is_purchase_item",
|
||||
"is_customer_provided_item",
|
||||
"customer",
|
||||
"section_break_gjns",
|
||||
"standard_rate",
|
||||
"column_break_ixrh",
|
||||
@@ -632,13 +631,6 @@
|
||||
"read_only_depends_on": "eval: doc.is_purchase_item",
|
||||
"show_description_on_click": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.is_customer_provided_item",
|
||||
"fieldname": "customer",
|
||||
"fieldtype": "Link",
|
||||
"label": "Customer",
|
||||
"options": "Customer"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.is_fixed_asset",
|
||||
"fieldname": "supplier_details",
|
||||
@@ -1077,7 +1069,7 @@
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2026-04-28 17:31:47.613279",
|
||||
"modified": "2026-05-26 10:18:46.862670",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Item",
|
||||
|
||||
@@ -82,7 +82,6 @@ class Item(Document):
|
||||
brand: DF.Link | None
|
||||
country_of_origin: DF.Link | None
|
||||
create_new_batch: DF.Check
|
||||
customer: DF.Link | None
|
||||
customer_code: DF.SmallText | None
|
||||
customer_items: DF.Table[ItemCustomerDetail]
|
||||
customs_tariff_number: DF.Link | None
|
||||
@@ -872,8 +871,13 @@ class Item(Document):
|
||||
if disabled:
|
||||
frappe.throw(_("Attribute {0} is disabled.").format(frappe.bold(d.attribute)))
|
||||
|
||||
if not numeric_values and not frappe.db.exists(
|
||||
"Item Attribute Value", {"parent": d.attribute, "attribute_value": d.attribute_value}
|
||||
if (
|
||||
not numeric_values
|
||||
and d.attribute_value
|
||||
and not frappe.db.exists(
|
||||
"Item Attribute Value",
|
||||
{"parent": d.attribute, "attribute_value": d.attribute_value},
|
||||
)
|
||||
):
|
||||
frappe.throw(
|
||||
_("Attribute Value {0} is not valid for the selected attribute {1}.").format(
|
||||
|
||||
@@ -161,6 +161,7 @@ class TestItem(ERPNextTestSuite):
|
||||
"conversion_factor": 1,
|
||||
"price_list_uom_dependant": 1,
|
||||
"ignore_pricing_rule": 1,
|
||||
"qty": 1,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_bulk_edit": 1,
|
||||
"creation": "2014-07-11 11:51:00.453717",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
@@ -13,7 +14,10 @@
|
||||
"amount",
|
||||
"base_amount",
|
||||
"has_corrective_cost",
|
||||
"has_operating_cost"
|
||||
"has_operating_cost",
|
||||
"operation_id",
|
||||
"qty",
|
||||
"operating_component"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -78,13 +82,38 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Has Operating Cost",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "operation_id",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Operation ID",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "qty",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 1,
|
||||
"label": "Qty",
|
||||
"no_copy": 1,
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "operating_component",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Operating Component",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-16 15:27:59.175530",
|
||||
"modified": "2026-05-19 12:21:07.953801",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Landed Cost Taxes and Charges",
|
||||
|
||||
@@ -22,9 +22,12 @@ class LandedCostTaxesandCharges(Document):
|
||||
expense_account: DF.Link | None
|
||||
has_corrective_cost: DF.Check
|
||||
has_operating_cost: DF.Check
|
||||
operating_component: DF.Data | None
|
||||
operation_id: DF.Data | None
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
qty: DF.Float
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
||||
@@ -1374,7 +1374,7 @@ def get_billed_qty_amount_against_purchase_receipt(pr_doc):
|
||||
.on(parent_table.name == table.parent)
|
||||
.select(
|
||||
table.pr_detail,
|
||||
fn.Sum(table.amount * parent_table.conversion_rate).as_("amount"),
|
||||
fn.Sum(table.base_net_amount).as_("amount"),
|
||||
fn.Sum(table.qty).as_("qty"),
|
||||
)
|
||||
.where((table.pr_detail.isin(pr_names)) & (table.docstatus == 1))
|
||||
@@ -1420,7 +1420,7 @@ def get_billed_qty_amount_against_purchase_order(pr_doc):
|
||||
.select(
|
||||
table.po_detail,
|
||||
fn.Sum(table.qty).as_("qty"),
|
||||
fn.Sum(table.amount * parent_table.conversion_rate).as_("amount"),
|
||||
fn.Sum(table.base_net_amount).as_("amount"),
|
||||
)
|
||||
.where((table.po_detail.isin(po_names)) & (table.docstatus == 1) & (table.pr_detail.isnull()))
|
||||
.groupby(table.po_detail)
|
||||
|
||||
@@ -5606,6 +5606,157 @@ class TestPurchaseReceipt(ERPNextTestSuite):
|
||||
self.assertIn(company.default_inventory_account, gl_accounts)
|
||||
pr.cancel()
|
||||
|
||||
@ERPNextTestSuite.change_settings(
|
||||
"Buying Settings", {"set_landed_cost_based_on_purchase_invoice_rate": 1, "maintain_same_rate": 0}
|
||||
)
|
||||
def test_srbnb_with_inclusive_tax_and_rate_change_in_pi(self):
|
||||
"""
|
||||
When 'Set Landed Cost Based on PI Rate' is enabled and PI has an inclusive tax:
|
||||
- PR: qty=2, rate=1000 INR → base_net_amount=2000
|
||||
- PI: rate changed to 2000, 5% tax included in basic rate
|
||||
→ PI base_net_amount = 2 * 2000 / 1.05 ≈ 3809.52
|
||||
|
||||
The system must use PI's base_net_amount (not amount=4000) so that
|
||||
SRBNB credit on PR = 3809.52, not 4000.
|
||||
"""
|
||||
company = "_Test Company with perpetual inventory"
|
||||
warehouse = "Stores - TCP1"
|
||||
cost_center = "Main - TCP1"
|
||||
|
||||
item_code = make_item(
|
||||
"Test Item for SRBNB Inclusive Tax Rate Change",
|
||||
{"is_stock_item": 1},
|
||||
).name
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
item_code=item_code,
|
||||
qty=2,
|
||||
rate=1000,
|
||||
company=company,
|
||||
warehouse=warehouse,
|
||||
cost_center=cost_center,
|
||||
)
|
||||
|
||||
pi = make_purchase_invoice(pr.name)
|
||||
pi.items[0].rate = 2000
|
||||
pi.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "_Test Account VAT - TCP1",
|
||||
"category": "Total",
|
||||
"add_deduct_tax": "Add",
|
||||
"included_in_print_rate": 1,
|
||||
"rate": 5,
|
||||
"description": "Test Inclusive Tax",
|
||||
"cost_center": cost_center,
|
||||
},
|
||||
)
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
pr.reload()
|
||||
|
||||
# PI base_net_amount = qty * (rate / (1 + tax_rate/100)) = 2 * (2000 / 1.05)
|
||||
pi_base_net_amount = flt(2 * 2000 / 1.05, 2)
|
||||
pr_base_net_amount = flt(pr.items[0].amount, 2) # 2 * 1000 = 2000
|
||||
expected_diff = flt(pi_base_net_amount - pr_base_net_amount, 2)
|
||||
|
||||
self.assertAlmostEqual(pr.items[0].amount_difference_with_purchase_invoice, expected_diff, places=2)
|
||||
|
||||
# Total SRBNB credit = PR base_net_amount + amount_difference = PI base_net_amount
|
||||
srbnb_account = "Stock Received But Not Billed - TCP1"
|
||||
gl_entries = get_gl_entries("Purchase Receipt", pr.name, skip_cancelled=True)
|
||||
srbnb_credit = sum(flt(row.credit) for row in gl_entries if row.account == srbnb_account)
|
||||
self.assertAlmostEqual(srbnb_credit, pi_base_net_amount, places=2)
|
||||
|
||||
@ERPNextTestSuite.change_settings(
|
||||
"Buying Settings", {"set_landed_cost_based_on_purchase_invoice_rate": 1, "maintain_same_rate": 0}
|
||||
)
|
||||
def test_srbnb_with_inclusive_tax_and_exchange_rate_change_in_pi(self):
|
||||
"""
|
||||
When 'Set Landed Cost Based on PI Rate' is enabled, PI has an inclusive tax, and only
|
||||
the exchange rate changes on the PI (rate stays the same):
|
||||
- PR: qty=2, rate=100 USD, conversion_rate=70 → base_net_amount=14000 INR
|
||||
- PI: same rate=100 USD, conversion_rate changed to 90, 5% tax included in basic rate
|
||||
→ PI base_net_amount = 2 * (100 / 1.05) * 90 ≈ 17142.86 INR
|
||||
|
||||
The system must use PI's base_net_amount (not amount = 2*100*90 = 18000) so that
|
||||
SRBNB credit on PR = 17142.86, not 18000.
|
||||
"""
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
|
||||
company = "_Test Company with perpetual inventory"
|
||||
warehouse = "Stores - TCP1"
|
||||
cost_center = "Main - TCP1"
|
||||
|
||||
party_account = create_account(
|
||||
account_name="USD Payable For SRBNB Exchange Rate Test",
|
||||
parent_account="Accounts Payable - TCP1",
|
||||
account_type="Payable",
|
||||
company=company,
|
||||
account_currency="USD",
|
||||
)
|
||||
|
||||
supplier = create_supplier(
|
||||
supplier_name="_Test USD Supplier for SRBNB Exchange Rate",
|
||||
default_currency="USD",
|
||||
party_account=party_account,
|
||||
).name
|
||||
|
||||
item_code = make_item(
|
||||
"Test Item for SRBNB Inclusive Tax Exchange Rate Change",
|
||||
{"is_stock_item": 1},
|
||||
).name
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
item_code=item_code,
|
||||
qty=2,
|
||||
rate=100,
|
||||
currency="USD",
|
||||
conversion_rate=70,
|
||||
company=company,
|
||||
warehouse=warehouse,
|
||||
supplier=supplier,
|
||||
)
|
||||
|
||||
pi = make_purchase_invoice(pr.name)
|
||||
pi.conversion_rate = 90
|
||||
pi.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "_Test Account VAT - TCP1",
|
||||
"category": "Total",
|
||||
"add_deduct_tax": "Add",
|
||||
"included_in_print_rate": 1,
|
||||
"rate": 5,
|
||||
"description": "Test Inclusive Tax",
|
||||
"cost_center": cost_center,
|
||||
},
|
||||
)
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
pr.reload()
|
||||
|
||||
# PI base_net_amount = qty * (rate / (1 + tax_rate/100)) * new_conversion_rate
|
||||
# = 2 * (100 / 1.05) * 90 ≈ 17142.86 INR
|
||||
# PR base_net_amount = qty * rate * pr_conversion_rate = 2 * 100 * 70 = 14000 INR
|
||||
tax_amount_pr = (200 - flt(200 / 1.05, 2)) * 90
|
||||
|
||||
pi_base_net_amount = flt(2 * 100 * 90) - flt(tax_amount_pr)
|
||||
pr_base_net_amount = flt(2 * 100 * 70)
|
||||
expected_diff = flt(pi_base_net_amount - pr_base_net_amount)
|
||||
|
||||
self.assertAlmostEqual(pr.items[0].amount_difference_with_purchase_invoice, expected_diff, places=2)
|
||||
|
||||
# Total SRBNB credit = PR base_net_amount + amount_difference = PI base_net_amount
|
||||
srbnb_account = "Stock Received But Not Billed - TCP1"
|
||||
gl_entries = get_gl_entries("Purchase Receipt", pr.name, skip_cancelled=True)
|
||||
srbnb_credit = sum(flt(row.credit) for row in gl_entries if row.account == srbnb_account)
|
||||
self.assertAlmostEqual(srbnb_credit, pi_base_net_amount, places=2)
|
||||
|
||||
|
||||
def create_asset_category_for_pr_test():
|
||||
category_name = "Test Asset Category for PR"
|
||||
|
||||
@@ -9,7 +9,7 @@ import frappe
|
||||
from frappe import _, bold
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.query_builder.functions import Max, Sum
|
||||
from frappe.utils import (
|
||||
cint,
|
||||
comma_or,
|
||||
@@ -1288,13 +1288,21 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
)
|
||||
|
||||
def get_basic_rate_for_repacked_items(self, finished_item_qty, outgoing_items_cost):
|
||||
finished_items = [d.item_code for d in self.get("items") if d.is_finished_item]
|
||||
finished_items = [
|
||||
d.item_code for d in self.get("items") if d.is_finished_item and not d.set_basic_rate_manually
|
||||
]
|
||||
if len(finished_items) == 1:
|
||||
return flt(outgoing_items_cost / finished_item_qty)
|
||||
else:
|
||||
unique_finished_items = set(finished_items)
|
||||
if len(unique_finished_items) == 1:
|
||||
total_fg_qty = sum([flt(d.transfer_qty) for d in self.items if d.is_finished_item])
|
||||
if unique_finished_items:
|
||||
total_fg_qty = sum(
|
||||
[
|
||||
flt(d.transfer_qty)
|
||||
for d in self.items
|
||||
if d.is_finished_item and not d.set_basic_rate_manually
|
||||
]
|
||||
)
|
||||
return flt(outgoing_items_cost / total_fg_qty)
|
||||
|
||||
def get_basic_rate_for_manufactured_item(self, finished_item_qty, outgoing_items_cost=0) -> float:
|
||||
@@ -3895,28 +3903,33 @@ def get_work_order_details(work_order, company):
|
||||
}
|
||||
|
||||
|
||||
def get_consumed_operating_cost(wo_name, bom_no):
|
||||
def get_consumed_operating_cost(wo_name, bom_no, operation_id):
|
||||
table = frappe.qb.DocType("Stock Entry")
|
||||
child_table = frappe.qb.DocType("Landed Cost Taxes and Charges")
|
||||
query = (
|
||||
frappe.qb.from_(child_table)
|
||||
.join(table)
|
||||
.on(child_table.parent == table.name)
|
||||
.select(Sum(child_table.amount).as_("consumed_cost"))
|
||||
.select(
|
||||
Sum(child_table.amount).as_("consumed_cost"),
|
||||
Sum(child_table.qty).as_("consumed_qty"),
|
||||
child_table.operating_component,
|
||||
)
|
||||
.where(
|
||||
(table.docstatus == 1)
|
||||
& (table.work_order == wo_name)
|
||||
& (table.purpose == "Manufacture")
|
||||
& (table.bom_no == bom_no)
|
||||
& (child_table.has_operating_cost == 1)
|
||||
& (child_table.operation_id == operation_id)
|
||||
)
|
||||
.groupby(child_table.operation_id, child_table.operating_component)
|
||||
)
|
||||
cost = query.run(pluck="consumed_cost")
|
||||
return cost[0] if cost and cost[0] else 0
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_operating_cost_per_unit(work_order=None, bom_no=None):
|
||||
operating_cost_per_unit = 0
|
||||
def get_remaining_operating_cost(work_order=None, bom_no=None):
|
||||
remaining_operating_cost = 0
|
||||
if work_order:
|
||||
if (
|
||||
bom_no
|
||||
@@ -3931,23 +3944,23 @@ def get_operating_cost_per_unit(work_order=None, bom_no=None):
|
||||
bom_no = work_order.bom_no
|
||||
|
||||
for d in work_order.get("operations"):
|
||||
consumed_op_cost = get_consumed_operating_cost(work_order.name, bom_no, d.name) or []
|
||||
cost = 0
|
||||
for row in consumed_op_cost:
|
||||
cost += flt(row.consumed_cost)
|
||||
|
||||
if flt(d.completed_qty):
|
||||
if not (remaining_qty := flt(d.completed_qty - work_order.produced_qty)):
|
||||
continue
|
||||
operating_cost_per_unit += (
|
||||
flt(d.actual_operating_cost - get_consumed_operating_cost(work_order.name, bom_no))
|
||||
/ remaining_qty
|
||||
)
|
||||
remaining_operating_cost += flt(d.actual_operating_cost - cost)
|
||||
elif work_order.qty:
|
||||
operating_cost_per_unit += flt(d.planned_operating_cost) / flt(work_order.qty)
|
||||
remaining_operating_cost += flt(d.planned_operating_cost) / flt(work_order.qty)
|
||||
|
||||
# Get operating cost from BOM if not found in work_order.
|
||||
if not operating_cost_per_unit and bom_no:
|
||||
if not remaining_operating_cost and bom_no:
|
||||
bom = frappe.db.get_value("BOM", bom_no, ["operating_cost", "quantity"], as_dict=1)
|
||||
if bom.quantity:
|
||||
operating_cost_per_unit = flt(bom.operating_cost) / flt(bom.quantity)
|
||||
remaining_operating_cost = flt(bom.operating_cost) / flt(bom.quantity)
|
||||
|
||||
return operating_cost_per_unit
|
||||
return remaining_operating_cost
|
||||
|
||||
|
||||
def get_used_alternative_items(
|
||||
|
||||
@@ -567,15 +567,18 @@ class StockReconciliation(StockController):
|
||||
|
||||
def calculate_difference_amount(self, item, item_dict):
|
||||
qty_precision = item.precision("qty")
|
||||
val_precision = item.precision("valuation_rate")
|
||||
amount_precision = item.precision("amount")
|
||||
|
||||
new_qty = flt(item.qty, qty_precision)
|
||||
new_valuation_rate = flt(item.valuation_rate or item_dict.get("rate"), val_precision)
|
||||
new_valuation_rate = flt(item.valuation_rate or item_dict.get("rate"))
|
||||
|
||||
current_qty = flt(item_dict.get("qty"), qty_precision)
|
||||
current_valuation_rate = flt(item_dict.get("rate"), val_precision)
|
||||
current_valuation_rate = flt(item_dict.get("rate"))
|
||||
|
||||
self.difference_amount += (new_qty * new_valuation_rate) - (current_qty * current_valuation_rate)
|
||||
new_amount = flt(new_qty * new_valuation_rate, amount_precision)
|
||||
current_amount = flt(current_qty * current_valuation_rate, amount_precision)
|
||||
|
||||
self.difference_amount += new_amount - current_amount
|
||||
|
||||
def validate_data(self):
|
||||
def _get_msg(row_num, msg):
|
||||
@@ -885,7 +888,7 @@ class StockReconciliation(StockController):
|
||||
"company": self.company,
|
||||
"stock_uom": frappe.db.get_value("Item", row.item_code, "stock_uom"),
|
||||
"is_cancelled": 1 if self.docstatus == 2 else 0,
|
||||
"valuation_rate": flt(row.valuation_rate, row.precision("valuation_rate")),
|
||||
"valuation_rate": flt(row.valuation_rate),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1045,84 +1048,6 @@ class StockReconciliation(StockController):
|
||||
else:
|
||||
self._cancel()
|
||||
|
||||
def recalculate_current_qty(self, voucher_detail_no, sle_creation, add_new_sle=False):
|
||||
for row in self.items:
|
||||
if voucher_detail_no != row.name:
|
||||
continue
|
||||
|
||||
if row.current_qty < 0:
|
||||
return
|
||||
|
||||
val_rate = 0.0
|
||||
current_qty = 0.0
|
||||
if row.current_serial_and_batch_bundle:
|
||||
current_qty = self.get_current_qty_for_serial_or_batch(row, sle_creation)
|
||||
elif row.serial_no:
|
||||
item_dict = get_stock_balance_for(
|
||||
row.item_code,
|
||||
row.warehouse,
|
||||
self.posting_date,
|
||||
self.posting_time,
|
||||
row=row,
|
||||
company=self.company,
|
||||
)
|
||||
|
||||
current_qty = item_dict.get("qty")
|
||||
row.current_serial_no = item_dict.get("serial_nos")
|
||||
row.current_valuation_rate = item_dict.get("rate")
|
||||
val_rate = item_dict.get("rate")
|
||||
elif row.batch_no:
|
||||
current_qty = get_batch_qty_for_stock_reco(
|
||||
row.item_code,
|
||||
row.warehouse,
|
||||
row.batch_no,
|
||||
self.posting_date,
|
||||
self.posting_time,
|
||||
self.name,
|
||||
sle_creation,
|
||||
)
|
||||
|
||||
precesion = row.precision("current_qty")
|
||||
if flt(current_qty, precesion) != flt(row.current_qty, precesion):
|
||||
if not row.serial_no:
|
||||
val_rate = get_incoming_rate(
|
||||
frappe._dict(
|
||||
{
|
||||
"item_code": row.item_code,
|
||||
"warehouse": row.warehouse,
|
||||
"qty": current_qty * -1,
|
||||
"serial_and_batch_bundle": row.current_serial_and_batch_bundle,
|
||||
"batch_no": row.batch_no,
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"company": self.company,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
row.current_valuation_rate = val_rate
|
||||
row.current_qty = current_qty
|
||||
row.db_set(
|
||||
{
|
||||
"current_qty": row.current_qty,
|
||||
"current_valuation_rate": row.current_valuation_rate,
|
||||
"current_amount": flt(row.current_qty * row.current_valuation_rate),
|
||||
}
|
||||
)
|
||||
|
||||
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",
|
||||
):
|
||||
if 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
|
||||
|
||||
@@ -1044,7 +1044,7 @@ class TestStockReconciliation(ERPNextTestSuite, StockTestMixin):
|
||||
|
||||
sr.reload()
|
||||
self.assertTrue(sr.items[0].serial_and_batch_bundle)
|
||||
self.assertTrue(sr.items[0].current_serial_and_batch_bundle)
|
||||
self.assertFalse(sr.items[0].current_serial_and_batch_bundle)
|
||||
|
||||
def test_not_reconcile_all_batch(self):
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||
|
||||
@@ -320,9 +320,8 @@ def clean_all_descriptions():
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_enable_stock_uom_editing():
|
||||
return frappe.get_cached_value(
|
||||
return frappe.get_single_value(
|
||||
"Stock Settings",
|
||||
None,
|
||||
["allow_to_edit_stock_uom_qty_for_sales", "allow_to_edit_stock_uom_qty_for_purchase"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
@@ -1198,9 +1198,15 @@ def get_item_price(
|
||||
|
||||
if not ignore_party:
|
||||
if pctx.customer:
|
||||
query = query.where(ip.customer == pctx.customer)
|
||||
query = query.where(
|
||||
(ip.customer == pctx.customer)
|
||||
| ((IfNull(ip.customer, "") == "") & (IfNull(ip.supplier, "") == ""))
|
||||
).orderby(IfNull(ip.customer, ""), order=frappe.qb.desc)
|
||||
elif pctx.supplier:
|
||||
query = query.where(ip.supplier == pctx.supplier)
|
||||
query = query.where(
|
||||
(ip.supplier == pctx.supplier)
|
||||
| ((IfNull(ip.customer, "") == "") & (IfNull(ip.supplier, "") == ""))
|
||||
).orderby(IfNull(ip.supplier, ""), order=frappe.qb.desc)
|
||||
else:
|
||||
query = query.where((IfNull(ip.customer, "") == "") & (IfNull(ip.supplier, "") == ""))
|
||||
|
||||
@@ -1258,9 +1264,6 @@ def get_price_list_rate_for(ctx: ItemDetailsCtx, item_code):
|
||||
if desired_qty and check_packing_list(price_list_rate[0].name, desired_qty, item_code):
|
||||
item_price_data = price_list_rate
|
||||
else:
|
||||
for field in ["customer", "supplier"]:
|
||||
del pctx[field]
|
||||
|
||||
general_price_list_rate = get_item_price(pctx, item_code, ignore_party=ctx.get("ignore_party"))
|
||||
|
||||
if not general_price_list_rate and ctx.get("uom") != ctx.get("stock_uom"):
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -868,6 +868,555 @@ class TestStockAgeing(ERPNextTestSuite):
|
||||
range_valuations = range_values[1::2]
|
||||
self.assertEqual(range_valuations, [15, 7.5, 20, 5])
|
||||
|
||||
def test_batch_item_report_formatting_preserves_mixed_fifo_slots(self):
|
||||
item_details = {
|
||||
"Batch Mixed Item": {
|
||||
"details": frappe._dict(
|
||||
name="Batch Mixed Item",
|
||||
item_name="Batch Mixed Item",
|
||||
description="Batch Mixed Item",
|
||||
item_group=None,
|
||||
brand=None,
|
||||
has_batch_no=True,
|
||||
stock_uom="Nos",
|
||||
),
|
||||
"fifo_queue": [
|
||||
["SA-BATCH-MIXED-SLOT", 1, 5.0, "2021-12-01", 50.0],
|
||||
[3.0, "2021-12-02", 30.0],
|
||||
],
|
||||
"has_serial_no": False,
|
||||
"total_qty": 8.0,
|
||||
}
|
||||
}
|
||||
|
||||
report_data = format_report_data(self.filters, item_details, self.filters["to_date"])
|
||||
|
||||
self.assertEqual(report_data[0][7:15], [8.0, 80.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
|
||||
|
||||
def test_serial_transfer_replay_preserves_serial_slots(self):
|
||||
fifo_slots = FIFOSlots(self.filters, [])
|
||||
transfer_key = ("001", "Serial Item", "WH 1")
|
||||
fifo_slots.transferred_item_details[transfer_key] = [[2, "2021-12-01", 20]]
|
||||
|
||||
row = frappe._dict(
|
||||
name="Serial Item",
|
||||
actual_qty=2,
|
||||
stock_value_difference=20,
|
||||
posting_date="2021-12-05",
|
||||
has_serial_no=True,
|
||||
)
|
||||
fifo_queue = []
|
||||
|
||||
fifo_slots._compute_incoming_stock(row, fifo_queue, transfer_key, ["SN-A", "SN-B"], [])
|
||||
|
||||
self.assertEqual(fifo_queue, [["SN-A", "2021-12-01", 10.0], ["SN-B", "2021-12-01", 10.0]])
|
||||
self.assertFalse(fifo_slots.transferred_item_details[transfer_key])
|
||||
|
||||
def test_batch_transfer_replay_removes_zeroed_negative_slot(self):
|
||||
fifo_slots = FIFOSlots(self.filters, [])
|
||||
fifo_queue = [["SA-ZERO-BATCH", 1, -4, "2021-12-01", -40]]
|
||||
|
||||
fifo_slots._add_transfer_slot_to_fifo_queue(fifo_queue, ["SA-ZERO-BATCH", 1, 4, "2021-12-02", 40])
|
||||
|
||||
self.assertEqual(fifo_queue, [])
|
||||
|
||||
def test_batchwise_valuation(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
item_code = make_item(
|
||||
"Test Stock Ageing Batchwise Valuation",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"valuation_method": "FIFO",
|
||||
},
|
||||
).name
|
||||
|
||||
def make_batch(batch_id, use_batchwise_valuation):
|
||||
if not frappe.db.exists("Batch", batch_id):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Batch",
|
||||
"batch_id": batch_id,
|
||||
"item": item_code,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
frappe.db.set_value("Batch", batch_id, "use_batchwise_valuation", use_batchwise_valuation)
|
||||
|
||||
batchwise_above_90 = "SA-BATCHWISE-ABOVE-90"
|
||||
non_batchwise_above_90 = "SA-NON-BATCHWISE-ABOVE-90"
|
||||
batchwise_61_90 = "SA-BATCHWISE-61-90"
|
||||
non_batchwise_61_90 = "SA-NON-BATCHWISE-61-90"
|
||||
batchwise_31_60 = "SA-BATCHWISE-31-60"
|
||||
non_batchwise_31_60 = "SA-NON-BATCHWISE-31-60"
|
||||
batchwise_0_30 = "SA-BATCHWISE-0-30"
|
||||
non_batchwise_0_30 = "SA-NON-BATCHWISE-0-30"
|
||||
|
||||
for batch_id, use_batchwise_valuation in {
|
||||
batchwise_above_90: 1,
|
||||
non_batchwise_above_90: 0,
|
||||
batchwise_61_90: 1,
|
||||
non_batchwise_61_90: 0,
|
||||
batchwise_31_60: 1,
|
||||
non_batchwise_31_60: 0,
|
||||
batchwise_0_30: 1,
|
||||
non_batchwise_0_30: 0,
|
||||
}.items():
|
||||
make_batch(batch_id, use_batchwise_valuation)
|
||||
|
||||
qty_after_transaction = 0
|
||||
|
||||
def make_sle(posting_date, voucher_no, batch_no, actual_qty, stock_value_difference):
|
||||
nonlocal qty_after_transaction
|
||||
|
||||
qty_after_transaction += actual_qty
|
||||
return frappe._dict(
|
||||
name=item_code,
|
||||
actual_qty=actual_qty,
|
||||
qty_after_transaction=qty_after_transaction,
|
||||
stock_value_difference=stock_value_difference,
|
||||
warehouse="WH 1",
|
||||
posting_date=posting_date,
|
||||
voucher_type="Stock Entry",
|
||||
voucher_no=voucher_no,
|
||||
has_serial_no=False,
|
||||
has_batch_no=True,
|
||||
serial_no=None,
|
||||
batch_no=batch_no,
|
||||
valuation_rate=10,
|
||||
)
|
||||
|
||||
sle = [
|
||||
make_sle("2021-08-01", "001", batchwise_above_90, 50, 500),
|
||||
make_sle("2021-08-10", "002", non_batchwise_above_90, 60, 600),
|
||||
make_sle("2021-08-20", "003", batchwise_above_90, -10, -100),
|
||||
make_sle("2021-09-01", "004", non_batchwise_above_90, -15, -150),
|
||||
make_sle("2021-09-20", "005", batchwise_61_90, 40, 400),
|
||||
make_sle("2021-09-25", "006", non_batchwise_61_90, 50, 500),
|
||||
make_sle("2021-09-30", "007", batchwise_61_90, -5, -50),
|
||||
make_sle("2021-10-05", "008", non_batchwise_above_90, -20, -200),
|
||||
make_sle("2021-10-20", "009", batchwise_31_60, 30, 300),
|
||||
make_sle("2021-10-25", "010", non_batchwise_31_60, 40, 400),
|
||||
make_sle("2021-10-30", "011", batchwise_31_60, -8, -80),
|
||||
make_sle("2021-11-05", "012", non_batchwise_above_90, -25, -250),
|
||||
make_sle("2021-11-20", "013", batchwise_0_30, 20, 200),
|
||||
make_sle("2021-11-25", "014", non_batchwise_0_30, 30, 300),
|
||||
make_sle("2021-11-30", "015", batchwise_0_30, -6, -60),
|
||||
make_sle("2021-12-01", "016", non_batchwise_61_90, -10, -100),
|
||||
]
|
||||
|
||||
slots = FIFOSlots(self.filters, sle).generate()
|
||||
item_result = slots[item_code]
|
||||
|
||||
self.assertEqual(item_result["qty_after_transaction"], item_result["total_qty"])
|
||||
self.assertEqual(item_result["total_qty"], 221.0)
|
||||
self.assertEqual(
|
||||
item_result["fifo_queue"],
|
||||
[
|
||||
[batchwise_above_90, 1, 40.0, "2021-08-01", 400.0],
|
||||
[batchwise_61_90, 1, 35.0, "2021-09-20", 350.0],
|
||||
[non_batchwise_61_90, 0, 40.0, "2021-09-25", 400.0],
|
||||
[batchwise_31_60, 1, 22.0, "2021-10-20", 220.0],
|
||||
[non_batchwise_31_60, 0, 40, "2021-10-25", 400],
|
||||
[batchwise_0_30, 1, 14.0, "2021-11-20", 140.0],
|
||||
[non_batchwise_0_30, 0, 30, "2021-11-25", 300],
|
||||
],
|
||||
)
|
||||
|
||||
report_data = format_report_data(self.filters, slots, self.filters["to_date"])
|
||||
range_values = report_data[0][7:15]
|
||||
self.assertEqual(range_values, [44.0, 440.0, 62.0, 620.0, 75.0, 750.0, 40.0, 400.0])
|
||||
|
||||
def test_batchwise_valuation_same_voucher_transfer(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
item_code = make_item(
|
||||
"Test Stock Ageing Batchwise Transfer",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"valuation_method": "FIFO",
|
||||
},
|
||||
).name
|
||||
|
||||
def make_batch(batch_id):
|
||||
if not frappe.db.exists("Batch", batch_id):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Batch",
|
||||
"batch_id": batch_id,
|
||||
"item": item_code,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
frappe.db.set_value("Batch", batch_id, "use_batchwise_valuation", 1)
|
||||
|
||||
source_batch = "SA-BATCHWISE-TRANSFER-SOURCE"
|
||||
target_batch = "SA-BATCHWISE-TRANSFER-TARGET"
|
||||
make_batch(source_batch)
|
||||
make_batch(target_batch)
|
||||
|
||||
sle = [
|
||||
frappe._dict(
|
||||
name=item_code,
|
||||
actual_qty=20,
|
||||
qty_after_transaction=20,
|
||||
stock_value_difference=200,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-09-01",
|
||||
voucher_type="Stock Entry",
|
||||
voucher_no="001",
|
||||
has_serial_no=False,
|
||||
has_batch_no=True,
|
||||
serial_no=None,
|
||||
batch_no=source_batch,
|
||||
valuation_rate=10,
|
||||
),
|
||||
frappe._dict(
|
||||
name=item_code,
|
||||
actual_qty=-15,
|
||||
qty_after_transaction=5,
|
||||
stock_value_difference=-150,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-10-01",
|
||||
voucher_type="Stock Entry",
|
||||
voucher_no="002",
|
||||
has_serial_no=False,
|
||||
has_batch_no=True,
|
||||
serial_no=None,
|
||||
batch_no=source_batch,
|
||||
valuation_rate=10,
|
||||
),
|
||||
frappe._dict(
|
||||
name=item_code,
|
||||
actual_qty=10,
|
||||
qty_after_transaction=15,
|
||||
stock_value_difference=100,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-10-01",
|
||||
voucher_type="Stock Entry",
|
||||
voucher_no="002",
|
||||
has_serial_no=False,
|
||||
has_batch_no=True,
|
||||
serial_no=None,
|
||||
batch_no=target_batch,
|
||||
valuation_rate=10,
|
||||
),
|
||||
]
|
||||
|
||||
fifo_slots = FIFOSlots(self.filters, sle)
|
||||
slots = fifo_slots.generate()
|
||||
item_result = slots[item_code]
|
||||
|
||||
self.assertEqual(item_result["total_qty"], 15.0)
|
||||
self.assertEqual(
|
||||
item_result["fifo_queue"],
|
||||
[
|
||||
[source_batch, 1, 5.0, "2021-09-01", 50.0],
|
||||
[target_batch, 1, 10.0, "2021-09-01", 100.0],
|
||||
],
|
||||
)
|
||||
self.assertEqual(
|
||||
fifo_slots.transferred_item_details[("002", item_code, "WH 1")],
|
||||
[[5.0, "2021-09-01", 50.0]],
|
||||
)
|
||||
|
||||
def test_batchwise_valuation_negative_stock_same_voucher(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
item_code = make_item(
|
||||
"Test Stock Ageing Batchwise Negative Stock",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"valuation_method": "FIFO",
|
||||
},
|
||||
).name
|
||||
|
||||
batch_no = "SA-BATCHWISE-NEGATIVE-STOCK"
|
||||
if not frappe.db.exists("Batch", batch_no):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Batch",
|
||||
"batch_id": batch_no,
|
||||
"item": item_code,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1)
|
||||
|
||||
sle = [
|
||||
frappe._dict(
|
||||
name=item_code,
|
||||
actual_qty=-10,
|
||||
qty_after_transaction=-10,
|
||||
stock_value_difference=-100,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-01",
|
||||
voucher_type="Stock Entry",
|
||||
voucher_no="001",
|
||||
has_serial_no=False,
|
||||
has_batch_no=True,
|
||||
serial_no=None,
|
||||
batch_no=batch_no,
|
||||
valuation_rate=10,
|
||||
)
|
||||
]
|
||||
|
||||
fifo_slots = FIFOSlots(self.filters, sle)
|
||||
slots = fifo_slots.generate()
|
||||
item_result = slots[item_code]
|
||||
|
||||
self.assertEqual(item_result["fifo_queue"], [[batch_no, 1, -10, "2021-12-01", -100]])
|
||||
self.assertEqual(
|
||||
fifo_slots.transferred_item_details[("001", item_code, "WH 1")], [[10, "2021-12-01", 100]]
|
||||
)
|
||||
|
||||
sle.append(
|
||||
frappe._dict(
|
||||
name=item_code,
|
||||
actual_qty=6,
|
||||
qty_after_transaction=-4,
|
||||
stock_value_difference=60,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-01",
|
||||
voucher_type="Stock Entry",
|
||||
voucher_no="001",
|
||||
has_serial_no=False,
|
||||
has_batch_no=True,
|
||||
serial_no=None,
|
||||
batch_no=batch_no,
|
||||
valuation_rate=10,
|
||||
)
|
||||
)
|
||||
|
||||
fifo_slots = FIFOSlots(self.filters, sle)
|
||||
slots = fifo_slots.generate()
|
||||
item_result = slots[item_code]
|
||||
|
||||
self.assertEqual(item_result["fifo_queue"], [[batch_no, 1, -4.0, "2021-12-01", -40.0]])
|
||||
self.assertEqual(
|
||||
fifo_slots.transferred_item_details[("001", item_code, "WH 1")],
|
||||
[[4.0, "2021-12-01", 40.0]],
|
||||
)
|
||||
|
||||
def test_batchwise_valuation_neutralizes_non_head_negative_batch(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
item_code = make_item(
|
||||
"Test Stock Ageing Batchwise Negative Non Head",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"valuation_method": "FIFO",
|
||||
},
|
||||
).name
|
||||
|
||||
buffer_batch = "SA-BATCHWISE-NEGATIVE-BUFFER"
|
||||
negative_batch = "SA-BATCHWISE-NEGATIVE-NON-HEAD"
|
||||
for batch_no in [buffer_batch, negative_batch]:
|
||||
if not frappe.db.exists("Batch", batch_no):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Batch",
|
||||
"batch_id": batch_no,
|
||||
"item": item_code,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1)
|
||||
|
||||
sle = [
|
||||
frappe._dict(
|
||||
name=item_code,
|
||||
actual_qty=5,
|
||||
qty_after_transaction=5,
|
||||
stock_value_difference=50,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-11-30",
|
||||
voucher_type="Stock Entry",
|
||||
voucher_no="001",
|
||||
has_serial_no=False,
|
||||
has_batch_no=True,
|
||||
serial_no=None,
|
||||
batch_no=buffer_batch,
|
||||
valuation_rate=10,
|
||||
),
|
||||
frappe._dict(
|
||||
name=item_code,
|
||||
actual_qty=-10,
|
||||
qty_after_transaction=-5,
|
||||
stock_value_difference=-100,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-01",
|
||||
voucher_type="Stock Entry",
|
||||
voucher_no="002",
|
||||
has_serial_no=False,
|
||||
has_batch_no=True,
|
||||
serial_no=None,
|
||||
batch_no=negative_batch,
|
||||
valuation_rate=10,
|
||||
),
|
||||
frappe._dict(
|
||||
name=item_code,
|
||||
actual_qty=6,
|
||||
qty_after_transaction=1,
|
||||
stock_value_difference=60,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-01",
|
||||
voucher_type="Stock Entry",
|
||||
voucher_no="002",
|
||||
has_serial_no=False,
|
||||
has_batch_no=True,
|
||||
serial_no=None,
|
||||
batch_no=negative_batch,
|
||||
valuation_rate=10,
|
||||
),
|
||||
]
|
||||
|
||||
fifo_slots = FIFOSlots(self.filters, sle)
|
||||
slots = fifo_slots.generate()
|
||||
item_result = slots[item_code]
|
||||
|
||||
self.assertEqual(item_result["qty_after_transaction"], item_result["total_qty"])
|
||||
self.assertEqual(
|
||||
item_result["fifo_queue"],
|
||||
[
|
||||
[buffer_batch, 1, 5, "2021-11-30", 50],
|
||||
[negative_batch, 1, -4.0, "2021-12-01", -40.0],
|
||||
],
|
||||
)
|
||||
self.assertEqual(
|
||||
fifo_slots.transferred_item_details[("002", item_code, "WH 1")],
|
||||
[[4.0, "2021-12-01", 40.0]],
|
||||
)
|
||||
|
||||
def test_batchwise_valuation_negative_stock_later_voucher(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
item_code = make_item(
|
||||
"Test Stock Ageing Batchwise Negative Later Voucher",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"valuation_method": "FIFO",
|
||||
},
|
||||
).name
|
||||
|
||||
batch_no = "SA-BATCHWISE-NEGATIVE-LATER-VOUCHER"
|
||||
if not frappe.db.exists("Batch", batch_no):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Batch",
|
||||
"batch_id": batch_no,
|
||||
"item": item_code,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1)
|
||||
|
||||
sle = [
|
||||
frappe._dict(
|
||||
name=item_code,
|
||||
actual_qty=-10,
|
||||
qty_after_transaction=-10,
|
||||
stock_value_difference=-100,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-11-01",
|
||||
voucher_type="Stock Entry",
|
||||
voucher_no="001",
|
||||
has_serial_no=False,
|
||||
has_batch_no=True,
|
||||
serial_no=None,
|
||||
batch_no=batch_no,
|
||||
valuation_rate=10,
|
||||
),
|
||||
frappe._dict(
|
||||
name=item_code,
|
||||
actual_qty=6,
|
||||
qty_after_transaction=-4,
|
||||
stock_value_difference=60,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-11-10",
|
||||
voucher_type="Stock Entry",
|
||||
voucher_no="002",
|
||||
has_serial_no=False,
|
||||
has_batch_no=True,
|
||||
serial_no=None,
|
||||
batch_no=batch_no,
|
||||
valuation_rate=10,
|
||||
),
|
||||
]
|
||||
|
||||
slots = FIFOSlots(self.filters, sle).generate()
|
||||
item_result = slots[item_code]
|
||||
|
||||
self.assertEqual(item_result["qty_after_transaction"], item_result["total_qty"])
|
||||
self.assertEqual(item_result["total_qty"], -4.0)
|
||||
self.assertEqual(item_result["fifo_queue"], [[batch_no, 1, -4.0, "2021-11-10", -40.0]])
|
||||
|
||||
def test_batchwise_valuation_stock_reconciliation_with_bundle(self):
|
||||
from frappe.utils import add_days, getdate, nowdate
|
||||
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
|
||||
get_batch_from_bundle,
|
||||
)
|
||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||
create_stock_reconciliation,
|
||||
)
|
||||
|
||||
suffix = frappe.generate_hash(length=8).upper()
|
||||
item_code = make_item(
|
||||
f"Test Stock Ageing Batch Reco {suffix}",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": f"SA-RECO-{suffix}-.###",
|
||||
"valuation_method": "FIFO",
|
||||
},
|
||||
).name
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
base_date = nowdate()
|
||||
|
||||
opening_reco = create_stock_reconciliation(
|
||||
item_code=item_code,
|
||||
warehouse=warehouse,
|
||||
qty=12,
|
||||
rate=10,
|
||||
posting_date=add_days(base_date, -2),
|
||||
posting_time="10:00:00",
|
||||
)
|
||||
batch_no = get_batch_from_bundle(opening_reco.items[0].serial_and_batch_bundle)
|
||||
frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1)
|
||||
|
||||
create_stock_reconciliation(
|
||||
item_code=item_code,
|
||||
warehouse=warehouse,
|
||||
qty=5,
|
||||
rate=10,
|
||||
batch_no=batch_no,
|
||||
posting_date=add_days(base_date, -1),
|
||||
posting_time="10:00:00",
|
||||
)
|
||||
|
||||
filters = frappe._dict(
|
||||
company="_Test Company",
|
||||
to_date=base_date,
|
||||
ranges=["30", "60", "90"],
|
||||
item_code=item_code,
|
||||
)
|
||||
slots = FIFOSlots(filters).generate()
|
||||
item_result = slots[item_code]
|
||||
|
||||
self.assertEqual(item_result["qty_after_transaction"], item_result["total_qty"])
|
||||
self.assertEqual(item_result["total_qty"], 5.0)
|
||||
self.assertEqual(
|
||||
item_result["fifo_queue"], [[batch_no.upper(), 1, 5.0, getdate(add_days(base_date, -2)), 50.0]]
|
||||
)
|
||||
|
||||
|
||||
def generate_item_and_item_wh_wise_slots(filters, sle):
|
||||
"Return results with and without 'show_warehouse_wise_stock'"
|
||||
|
||||
@@ -874,15 +874,6 @@ class update_entries_after:
|
||||
if not self.args.get("sle_id"):
|
||||
self.get_dynamic_incoming_outgoing_rate(sle)
|
||||
|
||||
if (
|
||||
sle.voucher_type == "Stock Reconciliation"
|
||||
and (sle.serial_and_batch_bundle)
|
||||
and sle.voucher_detail_no
|
||||
and not self.args.get("sle_id")
|
||||
and sle.is_cancelled == 0
|
||||
):
|
||||
self.reset_actual_qty_for_stock_reco(sle)
|
||||
|
||||
if (
|
||||
sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
|
||||
and sle.voucher_detail_no
|
||||
@@ -1059,31 +1050,6 @@ class update_entries_after:
|
||||
if not allow_zero_rate:
|
||||
self.wh_data.valuation_rate = self.get_fallback_rate(sle)
|
||||
|
||||
def reset_actual_qty_for_stock_reco(self, sle):
|
||||
doc = frappe.get_doc("Stock Reconciliation", sle.voucher_no)
|
||||
doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0)
|
||||
|
||||
if sle.actual_qty < 0:
|
||||
doc.reload()
|
||||
|
||||
sle.actual_qty = (
|
||||
flt(frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "current_qty"))
|
||||
* -1
|
||||
)
|
||||
|
||||
if abs(sle.actual_qty) == 0.0:
|
||||
sle.is_cancelled = 1
|
||||
|
||||
if sle.serial_and_batch_bundle:
|
||||
for row in doc.items:
|
||||
if row.name == sle.voucher_detail_no:
|
||||
row.db_set("current_serial_and_batch_bundle", "")
|
||||
|
||||
sabb_doc = frappe.get_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
|
||||
sabb_doc.voucher_detail_no = None
|
||||
sabb_doc.voucher_no = None
|
||||
sabb_doc.cancel()
|
||||
|
||||
def calculate_valuation_for_serial_batch_bundle(self, sle):
|
||||
if not frappe.db.exists("Serial and Batch Bundle", sle.serial_and_batch_bundle):
|
||||
return
|
||||
|
||||
@@ -3003,6 +3003,9 @@ class ERPNextTestSuite(unittest.TestCase):
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
frappe.local.request_cache.clear()
|
||||
if hasattr(frappe.local, "future_sle"):
|
||||
frappe.local.future_sle.clear()
|
||||
|
||||
def load_test_records(self, doctype):
|
||||
if doctype not in self.globalTestRecords:
|
||||
|
||||
1
frappe-semgrep-rules
Submodule
1
frappe-semgrep-rules
Submodule
Submodule frappe-semgrep-rules added at a05bce32ad
Reference in New Issue
Block a user