mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-13 03:45:08 +00:00
Merge branch 'develop' of https://github.com/frappe/erpnext into support-52064
This commit is contained in:
@@ -9,13 +9,6 @@ cur_frm.add_fetch("bank", "swift_number", "swift_number");
|
||||
|
||||
frappe.ui.form.on("Bank Guarantee", {
|
||||
setup: function (frm) {
|
||||
frm.set_query("bank", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
frm.set_query("bank_account", function () {
|
||||
return {
|
||||
filters: {
|
||||
|
||||
@@ -71,14 +71,6 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
if (this.frm.doc.supplier && this.frm.doc.__islocal) {
|
||||
this.frm.trigger("supplier");
|
||||
}
|
||||
|
||||
this.frm.set_query("supplier", function () {
|
||||
return {
|
||||
filters: {
|
||||
is_transporter: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
refresh(doc) {
|
||||
|
||||
@@ -847,9 +847,10 @@ class SalesInvoice(SellingController):
|
||||
timesheet.db_update_all()
|
||||
|
||||
def update_billed_qty_in_scio(self):
|
||||
table = frappe.qb.DocType("Subcontracting Inward Order Received Item")
|
||||
fieldname = table.returned_qty if self.is_return else table.billed_qty
|
||||
if self.is_return:
|
||||
return
|
||||
|
||||
table = frappe.qb.DocType("Subcontracting Inward Order Received Item")
|
||||
data = frappe._dict(
|
||||
{
|
||||
item.scio_detail: item.stock_qty if self._action == "submit" else -item.stock_qty
|
||||
@@ -861,8 +862,8 @@ class SalesInvoice(SellingController):
|
||||
if data:
|
||||
case_expr = Case()
|
||||
for name, qty in data.items():
|
||||
case_expr = case_expr.when(table.name == name, fieldname + qty)
|
||||
frappe.qb.update(table).set(fieldname, case_expr).where(
|
||||
case_expr = case_expr.when(table.name == name, table.billed_qty + qty)
|
||||
frappe.qb.update(table).set(table.billed_qty, case_expr).where(
|
||||
(table.name.isin(list(data.keys()))) & (table.docstatus == 1)
|
||||
).run()
|
||||
|
||||
@@ -1281,16 +1282,14 @@ class SalesInvoice(SellingController):
|
||||
table = frappe.qb.DocType("Subcontracting Inward Order Received Item")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(
|
||||
table.required_qty, table.consumed_qty, table.billed_qty, table.returned_qty, table.name
|
||||
)
|
||||
.select(table.required_qty, table.consumed_qty, table.billed_qty, table.name)
|
||||
.where((table.docstatus == 1) & (table.name.isin([item.scio_detail for item in self_rms])))
|
||||
)
|
||||
result = query.run(as_dict=True)
|
||||
data = {item.name: item for item in result}
|
||||
for item in self_rms:
|
||||
row = data.get(item.scio_detail)
|
||||
max_qty = max(row.required_qty, row.consumed_qty) - row.billed_qty - row.returned_qty
|
||||
max_qty = max(row.required_qty, row.consumed_qty) - row.billed_qty
|
||||
if item.stock_qty > max_qty:
|
||||
frappe.throw(
|
||||
_("Row #{0}: Stock quantity {1} ({2}) for item {3} cannot exceed {4}").format(
|
||||
|
||||
@@ -26,16 +26,13 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
{
|
||||
fieldname: "cost_center",
|
||||
label: __("Cost Center"),
|
||||
fieldtype: "Link",
|
||||
options: "Cost Center",
|
||||
get_query: () => {
|
||||
var company = frappe.query_report.get_filter_value("company");
|
||||
return {
|
||||
filters: {
|
||||
company: company,
|
||||
},
|
||||
};
|
||||
fieldtype: "MultiSelectList",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Cost Center", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
});
|
||||
},
|
||||
options: "Cost Center",
|
||||
},
|
||||
{
|
||||
fieldname: "party_account",
|
||||
|
||||
@@ -45,16 +45,13 @@ frappe.query_reports["Accounts Payable Summary"] = {
|
||||
{
|
||||
fieldname: "cost_center",
|
||||
label: __("Cost Center"),
|
||||
fieldtype: "Link",
|
||||
options: "Cost Center",
|
||||
get_query: () => {
|
||||
var company = frappe.query_report.get_filter_value("company");
|
||||
return {
|
||||
filters: {
|
||||
company: company,
|
||||
},
|
||||
};
|
||||
fieldtype: "MultiSelectList",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Cost Center", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
});
|
||||
},
|
||||
options: "Cost Center",
|
||||
},
|
||||
{
|
||||
fieldname: "party_type",
|
||||
|
||||
@@ -28,16 +28,13 @@ frappe.query_reports["Accounts Receivable"] = {
|
||||
{
|
||||
fieldname: "cost_center",
|
||||
label: __("Cost Center"),
|
||||
fieldtype: "Link",
|
||||
options: "Cost Center",
|
||||
get_query: () => {
|
||||
var company = frappe.query_report.get_filter_value("company");
|
||||
return {
|
||||
filters: {
|
||||
company: company,
|
||||
},
|
||||
};
|
||||
fieldtype: "MultiSelectList",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Cost Center", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
});
|
||||
},
|
||||
options: "Cost Center",
|
||||
},
|
||||
{
|
||||
fieldname: "party_type",
|
||||
|
||||
@@ -15,6 +15,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
get_dimension_with_children,
|
||||
)
|
||||
from erpnext.accounts.report.financial_statements import get_cost_centers_with_children
|
||||
from erpnext.accounts.utils import (
|
||||
build_qb_match_conditions,
|
||||
get_advance_payment_doctypes,
|
||||
@@ -666,7 +667,16 @@ class ReceivablePayableReport:
|
||||
invoiced = d.base_payment_amount
|
||||
paid_amount = d.base_paid_amount
|
||||
|
||||
if company_currency == d.party_account_currency or self.filters.get("in_party_currency"):
|
||||
in_party_currency = self.filters.get("in_party_currency")
|
||||
# company, billing, and party account currencies are the same
|
||||
if company_currency == d.currency and company_currency == d.party_account_currency:
|
||||
in_party_currency = False
|
||||
|
||||
# When filtered by party currency and the billing currency not matches the party account currency
|
||||
if in_party_currency and d.currency != d.party_account_currency:
|
||||
in_party_currency = False
|
||||
|
||||
if in_party_currency:
|
||||
invoiced = d.payment_amount
|
||||
paid_amount = d.paid_amount
|
||||
|
||||
@@ -987,11 +997,7 @@ class ReceivablePayableReport:
|
||||
self.add_accounting_dimensions_filters()
|
||||
|
||||
def get_cost_center_conditions(self):
|
||||
lft, rgt = frappe.db.get_value("Cost Center", self.filters.cost_center, ["lft", "rgt"])
|
||||
cost_center_list = [
|
||||
center.name
|
||||
for center in frappe.get_list("Cost Center", filters={"lft": (">=", lft), "rgt": ("<=", rgt)})
|
||||
]
|
||||
cost_center_list = get_cost_centers_with_children(self.filters.cost_center)
|
||||
self.qb_selection_filter.append(self.ple.cost_center.isin(cost_center_list))
|
||||
|
||||
def add_common_filters(self):
|
||||
|
||||
@@ -199,6 +199,81 @@ class TestAccountsReceivable(AccountsTestMixin, IntegrationTestCase):
|
||||
row = report[1]
|
||||
self.assertTrue(len(row) == 0)
|
||||
|
||||
@IntegrationTestCase.change_settings(
|
||||
"Accounts Settings",
|
||||
{"allow_multi_currency_invoices_against_single_party_account": 1},
|
||||
)
|
||||
def test_allow_multi_currency_invoices_against_single_party_account(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"based_on_payment_terms": 1,
|
||||
"report_date": today(),
|
||||
"range": "30, 60, 90, 120",
|
||||
"show_remarks": True,
|
||||
"in_party_currency": 1,
|
||||
}
|
||||
|
||||
# CASE 1: Company currency and party account currency are the same
|
||||
si = self.create_sales_invoice(qty=1, no_payment_schedule=True, do_not_submit=True)
|
||||
si.currency = "USD"
|
||||
si.conversion_rate = 80
|
||||
si.save().submit()
|
||||
|
||||
filters.update(
|
||||
{
|
||||
"party_type": "Customer",
|
||||
"party": [self.customer],
|
||||
}
|
||||
)
|
||||
report = execute(filters)
|
||||
row = report[1][0]
|
||||
|
||||
expected_data = [8000, 8000, "No Remarks"] # Data in company currency
|
||||
|
||||
self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced, row.remarks])
|
||||
|
||||
# CASE 2: Transaction currency and party account currency are the same
|
||||
self.create_customer(
|
||||
"USD Customer", currency="USD", default_account=self.debtors_usd, company=self.company
|
||||
)
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debtors_usd,
|
||||
posting_date=today(),
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
currency="USD",
|
||||
conversion_rate=80,
|
||||
price_list_rate=100,
|
||||
do_not_save=1,
|
||||
)
|
||||
si.save().submit()
|
||||
|
||||
filters.update(
|
||||
{
|
||||
"party_type": "Customer",
|
||||
"party": [self.customer],
|
||||
}
|
||||
)
|
||||
report = execute(filters)
|
||||
row = report[1][0]
|
||||
|
||||
expected_data = [100, 100, "No Remarks"] # Data in Part Account Currency
|
||||
|
||||
self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced, row.remarks])
|
||||
|
||||
# View in Company currency
|
||||
filters.pop("in_party_currency")
|
||||
report = execute(filters)
|
||||
row = report[1][0]
|
||||
|
||||
expected_data = [8000, 8000, "No Remarks"] # Data in Company Currency
|
||||
|
||||
self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced, row.remarks])
|
||||
|
||||
def test_accounts_receivable_with_partial_payment(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
|
||||
@@ -45,16 +45,13 @@ frappe.query_reports["Accounts Receivable Summary"] = {
|
||||
{
|
||||
fieldname: "cost_center",
|
||||
label: __("Cost Center"),
|
||||
fieldtype: "Link",
|
||||
options: "Cost Center",
|
||||
get_query: () => {
|
||||
var company = frappe.query_report.get_filter_value("company");
|
||||
return {
|
||||
filters: {
|
||||
company: company,
|
||||
},
|
||||
};
|
||||
fieldtype: "MultiSelectList",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Cost Center", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
});
|
||||
},
|
||||
options: "Cost Center",
|
||||
},
|
||||
{
|
||||
fieldname: "party_type",
|
||||
|
||||
@@ -85,6 +85,12 @@ frappe.query_reports["Gross Profit"] = {
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "include_returned_invoices",
|
||||
label: __("Include Returned Invoices (Stand-alone)"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
],
|
||||
tree: true,
|
||||
name_field: "parent",
|
||||
|
||||
@@ -859,7 +859,10 @@ class GrossProfitGenerator:
|
||||
if self.filters.to_date:
|
||||
conditions += " and posting_date <= %(to_date)s"
|
||||
|
||||
conditions += " and (is_return = 0 or (is_return=1 and return_against is null))"
|
||||
if self.filters.include_returned_invoices:
|
||||
conditions += " and (is_return = 0 or (is_return=1 and return_against is null))"
|
||||
else:
|
||||
conditions += " and is_return = 0"
|
||||
|
||||
if self.filters.item_group:
|
||||
conditions += f" and {get_item_group_condition(self.filters.item_group)}"
|
||||
|
||||
@@ -447,7 +447,11 @@ class TestGrossProfit(IntegrationTestCase):
|
||||
sinv = sinv.save().submit()
|
||||
|
||||
filters = frappe._dict(
|
||||
company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
|
||||
company=self.company,
|
||||
from_date=nowdate(),
|
||||
to_date=nowdate(),
|
||||
group_by="Invoice",
|
||||
include_returned_invoices=1,
|
||||
)
|
||||
|
||||
columns, data = execute(filters=filters)
|
||||
|
||||
@@ -47,22 +47,23 @@ frappe.query_reports["Trial Balance"] = {
|
||||
{
|
||||
fieldname: "cost_center",
|
||||
label: __("Cost Center"),
|
||||
fieldtype: "Link",
|
||||
options: "Cost Center",
|
||||
get_query: function () {
|
||||
var company = frappe.query_report.get_filter_value("company");
|
||||
return {
|
||||
doctype: "Cost Center",
|
||||
filters: {
|
||||
company: company,
|
||||
},
|
||||
};
|
||||
fieldtype: "MultiSelectList",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Cost Center", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
});
|
||||
},
|
||||
options: "Cost Center",
|
||||
},
|
||||
{
|
||||
fieldname: "project",
|
||||
label: __("Project"),
|
||||
fieldtype: "Link",
|
||||
fieldtype: "MultiSelectList",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Project", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
});
|
||||
},
|
||||
options: "Project",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -15,6 +15,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
from erpnext.accounts.report.financial_statements import (
|
||||
filter_accounts,
|
||||
filter_out_zero_value_rows,
|
||||
get_cost_centers_with_children,
|
||||
set_gl_entries_by_account,
|
||||
)
|
||||
from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency
|
||||
@@ -101,10 +102,6 @@ def get_data(filters):
|
||||
|
||||
opening_balances = get_opening_balances(filters, ignore_is_opening)
|
||||
|
||||
# add filter inside list so that the query in financial_statements.py doesn't break
|
||||
if filters.project:
|
||||
filters.project = [filters.project]
|
||||
|
||||
set_gl_entries_by_account(
|
||||
filters.company,
|
||||
filters.from_date,
|
||||
@@ -297,18 +294,12 @@ def get_opening_balance(
|
||||
opening_balance = opening_balance.where(closing_balance.voucher_type != "Period Closing Voucher")
|
||||
|
||||
if filters.cost_center:
|
||||
lft, rgt = frappe.db.get_value("Cost Center", filters.cost_center, ["lft", "rgt"])
|
||||
cost_center = frappe.qb.DocType("Cost Center")
|
||||
opening_balance = opening_balance.where(
|
||||
closing_balance.cost_center.isin(
|
||||
frappe.qb.from_(cost_center)
|
||||
.select("name")
|
||||
.where((cost_center.lft >= lft) & (cost_center.rgt <= rgt))
|
||||
)
|
||||
closing_balance.cost_center.isin(get_cost_centers_with_children(filters.get("cost_center")))
|
||||
)
|
||||
|
||||
if filters.project:
|
||||
opening_balance = opening_balance.where(closing_balance.project == filters.project)
|
||||
opening_balance = opening_balance.where(closing_balance.project.isin(filters.project))
|
||||
|
||||
if frappe.db.count("Finance Book"):
|
||||
if filters.get("include_default_book_entries"):
|
||||
|
||||
@@ -5,7 +5,9 @@ from erpnext.stock.doctype.item.test_item import create_item
|
||||
|
||||
|
||||
class AccountsTestMixin:
|
||||
def create_customer(self, customer_name="_Test Customer", currency=None):
|
||||
def create_customer(
|
||||
self, customer_name="_Test Customer", currency=None, default_account=None, company=None
|
||||
):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = customer_name
|
||||
@@ -13,9 +15,28 @@ class AccountsTestMixin:
|
||||
|
||||
if currency:
|
||||
customer.default_currency = currency
|
||||
if company and default_account:
|
||||
customer.append(
|
||||
"accounts",
|
||||
{
|
||||
"company": company,
|
||||
"account": default_account,
|
||||
},
|
||||
)
|
||||
customer.save()
|
||||
self.customer = customer.name
|
||||
else:
|
||||
if company and default_account:
|
||||
customer = frappe.get_doc("Customer", customer_name)
|
||||
customer.accounts = []
|
||||
customer.append(
|
||||
"accounts",
|
||||
{
|
||||
"company": company,
|
||||
"account": default_account,
|
||||
},
|
||||
)
|
||||
customer.save()
|
||||
self.customer = customer_name
|
||||
|
||||
def create_supplier(self, supplier_name="_Test Supplier", currency=None):
|
||||
|
||||
@@ -946,7 +946,7 @@ def make_post_gl_entry():
|
||||
assets = frappe.db.sql_list(
|
||||
""" select name from `tabAsset`
|
||||
where asset_category = %s and ifnull(booked_fixed_asset, 0) = 0
|
||||
and available_for_use_date = %s""",
|
||||
and available_for_use_date = %s and docstatus = 1""",
|
||||
(asset_category.name, nowdate()),
|
||||
)
|
||||
|
||||
|
||||
@@ -502,17 +502,6 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
}
|
||||
}
|
||||
|
||||
onload() {
|
||||
super.onload();
|
||||
this.frm.set_query("supplier", function () {
|
||||
return {
|
||||
filters: {
|
||||
is_transporter: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
get_items_from_open_material_requests() {
|
||||
erpnext.utils.map_current_doc({
|
||||
method: "erpnext.stock.doctype.material_request.material_request.make_purchase_order_based_on_supplier",
|
||||
|
||||
@@ -2005,7 +2005,7 @@ class AccountsController(TransactionBase):
|
||||
discount_amount * self.get("conversion_rate"),
|
||||
item.precision("discount_amount"),
|
||||
),
|
||||
"debit_in_account_currency": flt(
|
||||
"debit_in_transaction_currency": flt(
|
||||
discount_amount, item.precision("discount_amount")
|
||||
),
|
||||
"cost_center": item.cost_center,
|
||||
@@ -2026,7 +2026,7 @@ class AccountsController(TransactionBase):
|
||||
discount_amount * self.get("conversion_rate"),
|
||||
item.precision("discount_amount"),
|
||||
),
|
||||
"credit_in_account_currency": flt(
|
||||
"credit_in_transaction_currency": flt(
|
||||
discount_amount, item.precision("discount_amount")
|
||||
),
|
||||
"cost_center": item.cost_center,
|
||||
@@ -3680,8 +3680,15 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
child_doctype = "Sales Order Item" if parent_doctype == "Sales Order" else "Purchase Order Item"
|
||||
return set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row)
|
||||
|
||||
def is_allowed_zero_qty():
|
||||
if parent_doctype == "Sales Order":
|
||||
return frappe.db.get_single_value("Selling Settings", "allow_zero_qty_in_sales_order") or False
|
||||
elif parent_doctype == "Purchase Order":
|
||||
return frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_purchase_order") or False
|
||||
return False
|
||||
|
||||
def validate_quantity(child_item, new_data):
|
||||
if not flt(new_data.get("qty")):
|
||||
if not flt(new_data.get("qty")) and not is_allowed_zero_qty():
|
||||
frappe.throw(
|
||||
_("Row #{0}: Quantity for Item {1} cannot be zero.").format(
|
||||
new_data.get("idx"), frappe.bold(new_data.get("item_code"))
|
||||
@@ -3817,6 +3824,11 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
conv_fac_precision = child_item.precision("conversion_factor") or 2
|
||||
qty_precision = child_item.precision("qty") or 2
|
||||
|
||||
prev_rate, new_rate = flt(child_item.get("rate")), flt(d.get("rate"))
|
||||
rate_unchanged = prev_rate == new_rate
|
||||
if not rate_unchanged and not child_item.get("qty") and is_allowed_zero_qty():
|
||||
frappe.throw(_("Rate of '{}' items cannot be changed").format(frappe.bold(_("Unit Price"))))
|
||||
|
||||
# Amount cannot be lesser than billed amount, except for negative amounts
|
||||
row_rate = flt(d.get("rate"), rate_precision)
|
||||
amount_below_billed_amt = flt(child_item.billed_amt, rate_precision) > flt(
|
||||
|
||||
@@ -484,6 +484,13 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
target_doc.subcontracting_order_item = source_doc.subcontracting_order_item
|
||||
target_doc.rejected_warehouse = source_doc.rejected_warehouse
|
||||
target_doc.subcontracting_receipt_item = source_doc.name
|
||||
if return_against_rejected_qty:
|
||||
target_doc.qty = -1 * flt(source_doc.rejected_qty - (returned_qty_map.get("qty") or 0))
|
||||
target_doc.rejected_qty = 0.0
|
||||
target_doc.rejected_warehouse = ""
|
||||
target_doc.warehouse = source_doc.rejected_warehouse
|
||||
target_doc.received_qty = target_doc.qty
|
||||
target_doc.return_qty_from_rejected_warehouse = 1
|
||||
else:
|
||||
target_doc.purchase_order = source_doc.purchase_order
|
||||
target_doc.purchase_order_item = source_doc.purchase_order_item
|
||||
|
||||
@@ -510,7 +510,6 @@ class SubcontractingInwardController:
|
||||
)
|
||||
.else_(table.qty)
|
||||
- table.delivered_qty
|
||||
- table.returned_qty
|
||||
).as_("max_allowed_qty")
|
||||
)
|
||||
.where((table.name == item.scio_detail) & (table.docstatus == 1))
|
||||
|
||||
@@ -284,9 +284,6 @@ class MasterProductionSchedule(Document):
|
||||
row = data[key]
|
||||
row.cumulative_lead_time = math.ceil(row.cumulative_lead_time)
|
||||
row.order_release_date = add_days(row.delivery_date, -row.cumulative_lead_time)
|
||||
if getdate(row.order_release_date) < getdate(today()):
|
||||
continue
|
||||
|
||||
row.planned_qty = row.qty
|
||||
row.uom = row.stock_uom
|
||||
row.warehouse = row.warehouse or self.parent_warehouse
|
||||
|
||||
@@ -1928,7 +1928,12 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
|
||||
frappe.qb.from_(table)
|
||||
.inner_join(child)
|
||||
.on(table.name == child.parent)
|
||||
.select(Sum(child.quantity * child.conversion_factor))
|
||||
.select(
|
||||
Sum(
|
||||
Case().when(child.quantity == 0, child.required_bom_qty).else_(child.quantity)
|
||||
* child.conversion_factor
|
||||
)
|
||||
)
|
||||
.where(
|
||||
(table.docstatus == 1)
|
||||
& (child.item_code == item_code)
|
||||
|
||||
@@ -84,7 +84,7 @@ class SalesForecast(Document):
|
||||
)
|
||||
|
||||
for index in range(self.demand_number):
|
||||
if self.horizon_type == "Monthly":
|
||||
if self.frequency == "Monthly":
|
||||
delivery_date = add_to_date(self.from_date, months=index + 1)
|
||||
else:
|
||||
delivery_date = add_to_date(self.from_date, weeks=index + 1)
|
||||
@@ -95,15 +95,13 @@ class SalesForecast(Document):
|
||||
"delivery_date": delivery_date,
|
||||
"item_name": item_details.item_name,
|
||||
"uom": item_details.uom,
|
||||
"demand_qty": 0.0,
|
||||
"demand_qty": 1.0,
|
||||
}
|
||||
)
|
||||
|
||||
for demand in forecast_demand:
|
||||
self.append("items", demand)
|
||||
|
||||
self.save()
|
||||
|
||||
@frappe.whitelist()
|
||||
def generate_demand(self):
|
||||
from statsmodels.tsa.holtwinters import ExponentialSmoothing
|
||||
@@ -124,7 +122,7 @@ class SalesForecast(Document):
|
||||
seasonal_periods = self.get_seasonal_periods(data)
|
||||
pd_sales_data = pd.DataFrame({"item": data.item, "date": data.date, "qty": data.qty})
|
||||
|
||||
resample_val = "M" if self.horizon_type == "Monthly" else "W"
|
||||
resample_val = "M" if self.frequency == "Monthly" else "W"
|
||||
_sales_data = pd_sales_data.set_index("date").resample(resample_val).sum()["qty"]
|
||||
|
||||
model = ExponentialSmoothing(
|
||||
@@ -164,7 +162,7 @@ class SalesForecast(Document):
|
||||
|
||||
def get_seasonal_periods(self, data):
|
||||
days = date_diff(data["end_date"], data["start_date"])
|
||||
if self.horizon_type == "Monthly":
|
||||
if self.frequency == "Monthly":
|
||||
months = (days / 365) * 12
|
||||
seasonal_periods = cint(months / 2)
|
||||
if seasonal_periods > 12:
|
||||
|
||||
@@ -77,13 +77,6 @@ frappe.query_reports["Material Requirements Planning Report"] = {
|
||||
default: "All",
|
||||
options: "\nFinished Goods\nRaw Materials\nAll",
|
||||
},
|
||||
{
|
||||
fieldname: "safety_stock_check_frequency",
|
||||
label: __("Safety Stock Check Frequency"),
|
||||
fieldtype: "Select",
|
||||
default: "Weekly",
|
||||
options: "\nDaily\nWeekly\nMonthly",
|
||||
},
|
||||
{
|
||||
fieldname: "add_safety_stock",
|
||||
label: __("Add Safety Stock"),
|
||||
|
||||
@@ -41,8 +41,9 @@ class MaterialRequirementsPlanningReport:
|
||||
self.rm_items = []
|
||||
self.dates = self.get_dates()
|
||||
self.mps_data = self.get_mps_data()
|
||||
|
||||
items = self.get_items_from_mps(self.mps_data)
|
||||
self.update_sales_forecast_data()
|
||||
|
||||
self.item_rm_details = self.get_raw_materials_data(items)
|
||||
|
||||
self._bin_details = self.get_item_wise_bin_details()
|
||||
@@ -52,7 +53,6 @@ class MaterialRequirementsPlanningReport:
|
||||
self._po_details = self.get_purchase_order_data()
|
||||
self._so_details = self.get_sales_order_data()
|
||||
|
||||
self.update_sales_forecast_data()
|
||||
data, chart = self.get_mrp_data()
|
||||
|
||||
return data, chart
|
||||
@@ -850,6 +850,23 @@ class MaterialRequirementsPlanningReport:
|
||||
if row.item_code not in items:
|
||||
items.append(row.item_code)
|
||||
|
||||
if self.filters.mps:
|
||||
sales_forecasts = frappe.get_all(
|
||||
"Master Production Schedule",
|
||||
filters={"name": self.filters.mps},
|
||||
pluck="sales_forecast",
|
||||
)
|
||||
|
||||
if sales_forecasts:
|
||||
sales_forecast_items = frappe.get_all(
|
||||
"Sales Forecast Item",
|
||||
filters={"parent": ("in", sales_forecasts)},
|
||||
pluck="item_code",
|
||||
)
|
||||
|
||||
if sales_forecast_items:
|
||||
items.extend(sales_forecast_items)
|
||||
|
||||
return items
|
||||
|
||||
def get_raw_materials_data(self, items):
|
||||
@@ -1139,7 +1156,12 @@ class MaterialRequirementsPlanningReport:
|
||||
query = query.where(doctype.delivery_date <= self.filters.to_date)
|
||||
|
||||
if self.filters.warehouse:
|
||||
query = query.where(doctype.warehouse == self.filters.warehouse)
|
||||
warehouses = [self.filters.get("warehouse")]
|
||||
if frappe.db.get_value("Warehouse", self.filters.get("warehouse"), "is_group"):
|
||||
warehouses = get_descendants_of("Warehouse", self.filters.get("warehouse"))
|
||||
warehouses.append(self.filters.get("warehouse"))
|
||||
|
||||
query = query.where(forecast_doc.parent_warehouse.isin(warehouses))
|
||||
|
||||
if self.filters.item_code:
|
||||
query = query.where(doctype.item_code == self.filters.item_code)
|
||||
|
||||
@@ -347,7 +347,7 @@
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Production",
|
||||
"link_count": 7,
|
||||
"link_count": 8,
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
@@ -385,16 +385,6 @@
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Master Production Schedule",
|
||||
"link_count": 0,
|
||||
"link_to": "Master Production Schedule",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
@@ -406,6 +396,26 @@
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Item Lead Time",
|
||||
"link_count": 0,
|
||||
"link_to": "Item Lead Time",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Master Production Schedule",
|
||||
"link_count": 0,
|
||||
"link_to": "Master Production Schedule",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
@@ -428,7 +438,7 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2025-10-30 11:49:35.589944",
|
||||
"modified": "2025-11-03 17:02:26.882516",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Manufacturing",
|
||||
|
||||
@@ -75,18 +75,35 @@ erpnext.financial_statements = {
|
||||
},
|
||||
open_general_ledger: function (data) {
|
||||
if (!data.account && !data.accounts) return;
|
||||
let project = $.grep(frappe.query_report.filters, function (e) {
|
||||
let filters = frappe.query_report.filters;
|
||||
|
||||
let project = $.grep(filters, function (e) {
|
||||
return e.df.fieldname == "project";
|
||||
});
|
||||
|
||||
let cost_center = $.grep(filters, function (e) {
|
||||
return e.df.fieldname == "cost_center";
|
||||
});
|
||||
|
||||
frappe.route_options = {
|
||||
account: data.account || data.accounts,
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
from_date: data.from_date || data.year_start_date,
|
||||
to_date: data.to_date || data.year_end_date,
|
||||
project: project && project.length > 0 ? project[0].$input.val() : "",
|
||||
project: project && project.length > 0 ? project[0].get_value() : "",
|
||||
cost_center: cost_center && cost_center.length > 0 ? cost_center[0].get_value() : "",
|
||||
};
|
||||
|
||||
filters.forEach((f) => {
|
||||
if (f.df.fieldtype == "MultiSelectList") {
|
||||
if (f.df.fieldname in frappe.route_options) return;
|
||||
let value = f.get_value();
|
||||
if (value && value.length > 0) {
|
||||
frappe.route_options[f.df.fieldname] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let report = "General Ledger";
|
||||
|
||||
if (["Payable", "Receivable"].includes(data.account_type)) {
|
||||
|
||||
@@ -1361,7 +1361,7 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, a
|
||||
.select(
|
||||
child.required_qty,
|
||||
child.consumed_qty,
|
||||
(child.billed_qty - child.returned_qty).as_("qty"),
|
||||
child.billed_qty,
|
||||
child.rm_item_code,
|
||||
child.stock_uom,
|
||||
child.name,
|
||||
@@ -1377,7 +1377,7 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, a
|
||||
if result:
|
||||
idx = len(doclist.items) + 1
|
||||
for item in result:
|
||||
if (qty := max(item.required_qty, item.consumed_qty) - item.qty) > 0:
|
||||
if (qty := max(item.required_qty, item.consumed_qty) - item.billed_qty) > 0:
|
||||
doclist.append(
|
||||
"items",
|
||||
{
|
||||
|
||||
@@ -119,12 +119,6 @@
|
||||
"default_item_manufacturer",
|
||||
"default_manufacturer_part_no",
|
||||
"total_projected_qty",
|
||||
"lead_time_in_days_section",
|
||||
"procurement_time",
|
||||
"manufacturing_time",
|
||||
"column_break_whvr",
|
||||
"planning_buffer",
|
||||
"cumulative_time",
|
||||
"capacity_in_days_section",
|
||||
"production_capacity"
|
||||
],
|
||||
@@ -894,36 +888,6 @@
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Deferred Accounting"
|
||||
},
|
||||
{
|
||||
"fieldname": "lead_time_in_days_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Lead Time (In Days)"
|
||||
},
|
||||
{
|
||||
"fieldname": "procurement_time",
|
||||
"fieldtype": "Int",
|
||||
"label": "Procurement Time"
|
||||
},
|
||||
{
|
||||
"fieldname": "planning_buffer",
|
||||
"fieldtype": "Int",
|
||||
"label": "Planning Buffer"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_whvr",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "manufacturing_time",
|
||||
"fieldtype": "Int",
|
||||
"label": "Manufacturing Time"
|
||||
},
|
||||
{
|
||||
"fieldname": "cumulative_time",
|
||||
"fieldtype": "Int",
|
||||
"label": "Cumulative Time",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "capacity_in_days_section",
|
||||
"fieldtype": "Section Break",
|
||||
@@ -953,7 +917,7 @@
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2025-10-13 16:58:40.946604",
|
||||
"modified": "2025-11-03 17:01:24.555003",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Item",
|
||||
|
||||
@@ -81,7 +81,6 @@ class Item(Document):
|
||||
brand: DF.Link | None
|
||||
country_of_origin: DF.Link | None
|
||||
create_new_batch: DF.Check
|
||||
cumulative_time: DF.Int
|
||||
customer: DF.Link | None
|
||||
customer_code: DF.SmallText | None
|
||||
customer_items: DF.Table[ItemCustomerDetail]
|
||||
@@ -120,7 +119,6 @@ class Item(Document):
|
||||
item_name: DF.Data | None
|
||||
last_purchase_rate: DF.Float
|
||||
lead_time_days: DF.Int
|
||||
manufacturing_time: DF.Int
|
||||
max_discount: DF.Float
|
||||
min_order_qty: DF.Float
|
||||
naming_series: DF.Literal["STO-ITEM-.YYYY.-"]
|
||||
@@ -129,8 +127,6 @@ class Item(Document):
|
||||
opening_stock: DF.Float
|
||||
over_billing_allowance: DF.Float
|
||||
over_delivery_receipt_allowance: DF.Float
|
||||
planning_buffer: DF.Int
|
||||
procurement_time: DF.Int
|
||||
production_capacity: DF.Int
|
||||
purchase_uom: DF.Link | None
|
||||
quality_inspection_template: DF.Link | None
|
||||
@@ -221,16 +217,10 @@ class Item(Document):
|
||||
self.validate_auto_reorder_enabled_in_stock_settings()
|
||||
self.cant_change()
|
||||
self.validate_item_tax_net_rate_range()
|
||||
self.set_cumulative_time()
|
||||
|
||||
if not self.is_new():
|
||||
self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group")
|
||||
|
||||
def set_cumulative_time(self):
|
||||
self.cumulative_time = (
|
||||
cint(self.procurement_time) + cint(self.manufacturing_time) + cint(self.planning_buffer)
|
||||
)
|
||||
|
||||
def on_update(self):
|
||||
self.update_variants()
|
||||
self.update_item_price()
|
||||
|
||||
@@ -199,17 +199,6 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend
|
||||
super.setup(doc);
|
||||
}
|
||||
|
||||
onload() {
|
||||
super.onload();
|
||||
this.frm.set_query("supplier", function () {
|
||||
return {
|
||||
filters: {
|
||||
is_transporter: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
refresh() {
|
||||
var me = this;
|
||||
super.refresh();
|
||||
|
||||
@@ -262,6 +262,8 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
self.reset_default_field_value("from_warehouse", "items", "s_warehouse")
|
||||
self.reset_default_field_value("to_warehouse", "items", "t_warehouse")
|
||||
|
||||
self.validate_same_source_target_warehouse_during_material_transfer()
|
||||
|
||||
self.validate_closed_subcontracting_order()
|
||||
self.validate_subcontract_order()
|
||||
|
||||
@@ -864,6 +866,53 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
title=_("Missing Item"),
|
||||
)
|
||||
|
||||
def validate_same_source_target_warehouse_during_material_transfer(self):
|
||||
"""
|
||||
Validate Material Transfer entries where source and target warehouses are identical.
|
||||
|
||||
For Material Transfer purpose, if an item has the same source and target warehouse,
|
||||
require that at least one inventory dimension (if configured) differs between source
|
||||
and target to ensure a meaningful transfer is occurring.
|
||||
|
||||
Raises:
|
||||
frappe.ValidationError: If warehouses are same and no inventory dimensions differ
|
||||
"""
|
||||
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
|
||||
|
||||
inventory_dimensions = get_inventory_dimensions()
|
||||
if self.purpose == "Material Transfer":
|
||||
for item in self.items:
|
||||
if cstr(item.s_warehouse) == cstr(item.t_warehouse):
|
||||
if not inventory_dimensions:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Source and Target Warehouse cannot be the same for Material Transfer"
|
||||
).format(item.idx),
|
||||
title=_("Invalid Source and Target Warehouse"),
|
||||
)
|
||||
else:
|
||||
difference_found = False
|
||||
for dimension in inventory_dimensions:
|
||||
fieldname = (
|
||||
dimension.source_fieldname
|
||||
if dimension.source_fieldname.startswith("to_")
|
||||
else f"to_{dimension.source_fieldname}"
|
||||
)
|
||||
if (
|
||||
item.get(dimension.source_fieldname)
|
||||
and item.get(fieldname)
|
||||
and item.get(dimension.source_fieldname) != item.get(fieldname)
|
||||
):
|
||||
difference_found = True
|
||||
break
|
||||
if not difference_found:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Source, Target Warehouse and Inventory Dimensions cannot be the exact same for Material Transfer"
|
||||
).format(item.idx),
|
||||
title=_("Invalid Source and Target Warehouse"),
|
||||
)
|
||||
|
||||
def get_matched_items(self, item_code):
|
||||
for row in self.items:
|
||||
if row.item_code == item_code or row.original_item == item_code:
|
||||
|
||||
@@ -109,6 +109,37 @@ class StockReservationEntry(Document):
|
||||
self.update_status()
|
||||
self.update_reserved_stock_in_bin()
|
||||
|
||||
def before_cancel(self) -> None:
|
||||
self.validate_reserved_entries()
|
||||
|
||||
def validate_reserved_entries(self):
|
||||
entries = frappe.get_all(
|
||||
"Stock Reservation Entry",
|
||||
fields=["voucher_no as name"],
|
||||
filters={
|
||||
"status": "Closed",
|
||||
"docstatus": 1,
|
||||
"from_voucher_type": "Purchase Receipt",
|
||||
"from_voucher_no": self.from_voucher_no,
|
||||
},
|
||||
)
|
||||
|
||||
if entries:
|
||||
work_orders = frappe.get_all(
|
||||
"Work Order",
|
||||
fields=["name"],
|
||||
filters={"production_plan": ("in", [entry.name for entry in entries])},
|
||||
)
|
||||
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot cancel Stock Reservation Entry {0}, as it has used in the work order {1}. Please cancel the work order first or unreserved the stock"
|
||||
).format(
|
||||
", ".join([frappe.bold(entry.name) for entry in entries]),
|
||||
", ".join([frappe.bold(wo.name) for wo in work_orders]),
|
||||
)
|
||||
)
|
||||
|
||||
def update_unreserved_qty_in_sre(self):
|
||||
if self.voucher_type == "Delivery Note":
|
||||
return
|
||||
|
||||
@@ -451,7 +451,7 @@ class SubcontractingInwardOrder(SubcontractingController):
|
||||
qty = (
|
||||
fg_item.produced_qty
|
||||
if allow_over
|
||||
else min(fg_item.qty, fg_item.produced_qty) - fg_item.delivered_qty - fg_item.returned_qty
|
||||
else min(fg_item.qty, fg_item.produced_qty) - fg_item.delivered_qty
|
||||
)
|
||||
if qty < 0:
|
||||
continue
|
||||
|
||||
@@ -120,6 +120,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.is_customer_provided_item",
|
||||
"fieldname": "returned_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Returned Qty",
|
||||
@@ -196,7 +197,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-21 23:44:18.302327",
|
||||
"modified": "2025-11-03 17:46:49.905804",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Subcontracting",
|
||||
"name": "Subcontracting Inward Order Received Item",
|
||||
|
||||
@@ -82,10 +82,53 @@ frappe.ui.form.on("Subcontracting Receipt", {
|
||||
frm.add_custom_button(
|
||||
__("Subcontract Return"),
|
||||
() => {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt.make_subcontract_return",
|
||||
frm: frm,
|
||||
const make_standard_return = () => {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt.make_subcontract_return",
|
||||
frm: frm,
|
||||
});
|
||||
};
|
||||
|
||||
let has_rejected_items = frm.doc.items.filter((item) => {
|
||||
if (item.rejected_qty > 0) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (has_rejected_items && has_rejected_items.length > 0) {
|
||||
frappe.prompt(
|
||||
[
|
||||
{
|
||||
label: __("Return Qty from Rejected Warehouse"),
|
||||
fieldtype: "Check",
|
||||
fieldname: "return_for_rejected_warehouse",
|
||||
default: 1,
|
||||
},
|
||||
],
|
||||
function (values) {
|
||||
if (values.return_for_rejected_warehouse) {
|
||||
frappe.call({
|
||||
method: "erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt.make_subcontract_return_against_rejected_warehouse",
|
||||
args: {
|
||||
source_name: frm.doc.name,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", r.message.doctype, r.message.name);
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
make_standard_return();
|
||||
}
|
||||
},
|
||||
__("Return Qty"),
|
||||
__("Make Return Entry")
|
||||
);
|
||||
} else {
|
||||
make_standard_return();
|
||||
}
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
|
||||
@@ -854,6 +854,13 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
make_purchase_receipt(self, save=True, notify=True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_subcontract_return_against_rejected_warehouse(source_name):
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
|
||||
return make_return_doc("Subcontracting Receipt", source_name, return_against_rejected_qty=True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_subcontract_return(source_name, target_doc=None):
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
|
||||
@@ -1195,6 +1195,136 @@ class TestSubcontractingReceipt(IntegrationTestCase):
|
||||
scr.cancel()
|
||||
self.assertTrue(scr.docstatus == 2)
|
||||
|
||||
def test_subcontract_return_from_rejected_warehouse(self):
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
from erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt import (
|
||||
make_subcontract_return_against_rejected_warehouse,
|
||||
)
|
||||
|
||||
# Create subcontracted item
|
||||
fg_item = make_item(
|
||||
"_Test Subcontract Item Return from Rejected Warehouse",
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"is_sub_contracted_item": 1,
|
||||
},
|
||||
).name
|
||||
|
||||
# Create service item
|
||||
service_item = make_item(
|
||||
"_Test Service Item Return from Rejected Warehouse", properties={"is_stock_item": 0}
|
||||
).name
|
||||
|
||||
# Create BOM for the subcontracted item with required raw materials
|
||||
rm_item1 = make_item(
|
||||
"_Test RM Item 1 Return from Rejected Warehouse", properties={"is_stock_item": 1}
|
||||
).name
|
||||
|
||||
rm_item2 = make_item(
|
||||
"_Test RM Item 2 Return from Rejected Warehouse", properties={"is_stock_item": 1}
|
||||
).name
|
||||
|
||||
make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2])
|
||||
|
||||
# Create warehouses
|
||||
rejected_warehouse = create_warehouse("_Test Subcontract Rejected Warehouse Return Qty Warehouse")
|
||||
|
||||
# Create service items for subcontracting order
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": service_item,
|
||||
"qty": 10,
|
||||
"rate": 100,
|
||||
"fg_item": fg_item,
|
||||
"fg_item_qty": 10,
|
||||
},
|
||||
]
|
||||
|
||||
# Create Subcontracting Order
|
||||
sco = get_subcontracting_order(service_items=service_items)
|
||||
|
||||
# Stock raw materials
|
||||
make_stock_entry(item_code=rm_item1, qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100)
|
||||
make_stock_entry(item_code=rm_item2, qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100)
|
||||
|
||||
# Transfer raw materials
|
||||
rm_items = get_rm_items(sco.supplied_items)
|
||||
itemwise_details = make_stock_in_entry(rm_items=rm_items)
|
||||
make_stock_transfer_entry(
|
||||
sco_no=sco.name,
|
||||
rm_items=rm_items,
|
||||
itemwise_details=copy.deepcopy(itemwise_details),
|
||||
)
|
||||
|
||||
# Step 1: Create Subcontracting Receipt with rejected quantity
|
||||
sr = make_subcontracting_receipt(sco.name)
|
||||
sr.items[0].qty = 8 # Accepted quantity
|
||||
sr.items[0].rejected_qty = 2
|
||||
sr.items[0].rejected_warehouse = rejected_warehouse
|
||||
sr.save()
|
||||
sr.submit()
|
||||
|
||||
# Verify initial state
|
||||
sr.reload()
|
||||
self.assertEqual(sr.items[0].qty, 8)
|
||||
self.assertEqual(sr.items[0].rejected_qty, 2)
|
||||
self.assertEqual(sr.items[0].rejected_warehouse, rejected_warehouse)
|
||||
|
||||
# Step 2: Create Subcontract Return from Rejected Warehouse
|
||||
sr_return = make_subcontract_return_against_rejected_warehouse(sr.name)
|
||||
|
||||
# Verify the return document properties
|
||||
self.assertEqual(sr_return.doctype, "Subcontracting Receipt")
|
||||
self.assertEqual(sr_return.is_return, 1)
|
||||
self.assertEqual(sr_return.return_against, sr.name)
|
||||
|
||||
# Verify item details in return document
|
||||
self.assertEqual(len(sr_return.items), 1)
|
||||
self.assertEqual(sr_return.items[0].item_code, fg_item)
|
||||
self.assertEqual(sr_return.items[0].warehouse, rejected_warehouse)
|
||||
self.assertEqual(sr_return.items[0].qty, -2.0) # Negative for return
|
||||
self.assertEqual(sr_return.items[0].rejected_qty, 0.0)
|
||||
self.assertEqual(sr_return.items[0].rejected_warehouse, "")
|
||||
|
||||
# Check specific fields that should be set for subcontracting returns
|
||||
self.assertEqual(sr_return.items[0].subcontracting_order, sco.name)
|
||||
self.assertEqual(sr_return.items[0].subcontracting_order_item, sr.items[0].subcontracting_order_item)
|
||||
self.assertEqual(sr_return.items[0].return_qty_from_rejected_warehouse, 1)
|
||||
|
||||
# For returns from rejected warehouse, supplied_items might be empty initially
|
||||
# They might get populated when the document is saved/submitted
|
||||
# Or they might not be needed since we're returning finished goods
|
||||
|
||||
# Save and submit the return
|
||||
sr_return.save()
|
||||
sr_return.submit()
|
||||
|
||||
# Verify final state
|
||||
sr_return.reload()
|
||||
self.assertEqual(sr_return.docstatus, 1)
|
||||
self.assertEqual(sr_return.status, "Return")
|
||||
|
||||
# Verify stock ledger entries for the return
|
||||
sle = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
filters={
|
||||
"voucher_type": "Subcontracting Receipt",
|
||||
"voucher_no": sr_return.name,
|
||||
"warehouse": rejected_warehouse,
|
||||
},
|
||||
fields=["item_code", "actual_qty", "warehouse"],
|
||||
)
|
||||
|
||||
self.assertEqual(len(sle), 1)
|
||||
self.assertEqual(sle[0].item_code, fg_item)
|
||||
self.assertEqual(sle[0].actual_qty, -2.0) # Outward entry from rejected warehouse
|
||||
self.assertEqual(sle[0].warehouse, rejected_warehouse)
|
||||
|
||||
# Verify that the original document's rejected quantity is not affected
|
||||
sr.reload()
|
||||
self.assertEqual(sr.items[0].rejected_qty, 2) # Should remain the same
|
||||
|
||||
@IntegrationTestCase.change_settings("Buying Settings", {"auto_create_purchase_receipt": 1})
|
||||
def test_auto_create_purchase_receipt(self):
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
|
||||
Reference in New Issue
Block a user