mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-01 12:38:27 +00:00
Merge branch 'develop' of https://github.com/frappe/erpnext into erpnext_form_cleanups
This commit is contained in:
@@ -7,6 +7,7 @@ on:
|
||||
paths:
|
||||
- "**.js"
|
||||
- "**.css"
|
||||
- "**.svg"
|
||||
- "**.md"
|
||||
- "**.html"
|
||||
- 'crowdin.yml'
|
||||
|
||||
@@ -33,6 +33,17 @@
|
||||
},
|
||||
"account_number": "1151.000"
|
||||
},
|
||||
"Pajak Dibayar di Muka": {
|
||||
"PPN Masukan": {
|
||||
"account_number": "1152.001",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"PPh 23 Dibayar di Muka": {
|
||||
"account_number": "1152.002",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"account_number": "1152.000"
|
||||
},
|
||||
"account_number": "1150.000"
|
||||
},
|
||||
"Kas": {
|
||||
@@ -97,17 +108,6 @@
|
||||
},
|
||||
"account_number": "1130.000"
|
||||
},
|
||||
"Pajak Dibayar di Muka": {
|
||||
"PPN Masukan": {
|
||||
"account_number": "1151.001",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"PPh 23 Dibayar di Muka": {
|
||||
"account_number": "1152.001",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"account_number": "1150.000"
|
||||
},
|
||||
"account_number": "1100.000"
|
||||
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"entry_type_and_date",
|
||||
"company",
|
||||
"is_system_generated",
|
||||
"title",
|
||||
"voucher_type",
|
||||
@@ -17,7 +18,6 @@
|
||||
"reversal_of",
|
||||
"column_break1",
|
||||
"from_template",
|
||||
"company",
|
||||
"posting_date",
|
||||
"finance_book",
|
||||
"apply_tds",
|
||||
@@ -638,7 +638,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2025-11-13 17:54:14.542903",
|
||||
"modified": "2026-02-03 14:40:39.944524",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
|
||||
@@ -74,8 +74,8 @@ class JournalEntry(AccountsController):
|
||||
mode_of_payment: DF.Link | None
|
||||
multi_currency: DF.Check
|
||||
naming_series: DF.Literal["ACC-JV-.YYYY.-"]
|
||||
party_not_required: DF.Check
|
||||
override_tax_withholding_entries: DF.Check
|
||||
party_not_required: DF.Check
|
||||
pay_to_recd_from: DF.Data | None
|
||||
payment_order: DF.Link | None
|
||||
periodic_entry_difference_account: DF.Link | None
|
||||
@@ -1691,6 +1691,10 @@ def get_exchange_rate(
|
||||
credit=None,
|
||||
exchange_rate=None,
|
||||
):
|
||||
# Ensure exchange_rate is always numeric to avoid calculation errors
|
||||
if isinstance(exchange_rate, str):
|
||||
exchange_rate = flt(exchange_rate) or 1
|
||||
|
||||
account_details = frappe.get_cached_value(
|
||||
"Account", account, ["account_type", "root_type", "account_currency", "company"], as_dict=1
|
||||
)
|
||||
|
||||
@@ -1450,16 +1450,15 @@ frappe.ui.form.on("Payment Entry", {
|
||||
callback: function (r) {
|
||||
if (!r.exc && r.message) {
|
||||
// set taxes table
|
||||
if (r.message) {
|
||||
for (let tax of r.message) {
|
||||
if (tax.charge_type === "On Net Total") {
|
||||
tax.charge_type = "On Paid Amount";
|
||||
}
|
||||
frm.add_child("taxes", tax);
|
||||
let taxes = r.message;
|
||||
taxes.forEach((tax) => {
|
||||
if (tax.charge_type === "On Net Total") {
|
||||
tax.charge_type = "On Paid Amount";
|
||||
}
|
||||
frm.events.apply_taxes(frm);
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
}
|
||||
});
|
||||
frm.set_value("taxes", taxes);
|
||||
frm.events.apply_taxes(frm);
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -897,6 +897,53 @@ class TestPOSInvoice(IntegrationTestCase):
|
||||
if batch.batch_no == batch_no and batch.warehouse == "_Test Warehouse - _TC":
|
||||
self.assertEqual(batch.qty, 5)
|
||||
|
||||
def test_pos_batch_reservation_with_return_qty(self):
|
||||
"""
|
||||
Test POS Invoice reserved qty for batch without bundle with return invoices.
|
||||
"""
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
get_auto_batch_nos,
|
||||
)
|
||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||
create_batch_item_with_batch,
|
||||
)
|
||||
|
||||
create_batch_item_with_batch("_Batch Item Reserve Return", "TestBatch-RR 01")
|
||||
se = make_stock_entry(
|
||||
target="_Test Warehouse - _TC",
|
||||
item_code="_Batch Item Reserve Return",
|
||||
qty=30,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
se.reload()
|
||||
|
||||
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||
|
||||
# POS Invoice for the batch without bundle
|
||||
pos_inv = create_pos_invoice(item="_Batch Item Reserve Return", rate=300, qty=15, do_not_save=1)
|
||||
pos_inv.append(
|
||||
"payments",
|
||||
{"mode_of_payment": "Cash", "amount": 4500},
|
||||
)
|
||||
pos_inv.items[0].batch_no = batch_no
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
|
||||
# POS Invoice return
|
||||
pos_return = make_sales_return(pos_inv.name)
|
||||
|
||||
pos_return.insert()
|
||||
pos_return.submit()
|
||||
|
||||
batches = get_auto_batch_nos(
|
||||
frappe._dict({"item_code": "_Batch Item Reserve Return", "warehouse": "_Test Warehouse - _TC"})
|
||||
)
|
||||
|
||||
for batch in batches:
|
||||
if batch.batch_no == batch_no and batch.warehouse == "_Test Warehouse - _TC":
|
||||
self.assertEqual(batch.qty, 30)
|
||||
|
||||
def test_pos_batch_item_qty_validation(self):
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
BatchNegativeStockError,
|
||||
|
||||
@@ -98,8 +98,7 @@ def get_customers_list(pos_profile=None):
|
||||
|
||||
return (
|
||||
frappe.db.sql(
|
||||
f""" select name, customer_name, customer_group,
|
||||
territory, customer_pos_id from tabCustomer where disabled = 0
|
||||
f""" select name, customer_name, customer_group, territory from tabCustomer where disabled = 0
|
||||
and {cond}""",
|
||||
tuple(customer_groups),
|
||||
as_dict=1,
|
||||
|
||||
@@ -415,8 +415,9 @@ def reconcile(doc: None | str = None) -> None:
|
||||
for x in allocations:
|
||||
pr.append("allocation", x)
|
||||
|
||||
skip_ref_details_update_for_pe = check_multi_currency(pr)
|
||||
# reconcile
|
||||
pr.reconcile_allocations(skip_ref_details_update_for_pe=True)
|
||||
pr.reconcile_allocations(skip_ref_details_update_for_pe=skip_ref_details_update_for_pe)
|
||||
|
||||
# If Payment Entry, update details only for newly linked references
|
||||
# This is for performance
|
||||
@@ -504,6 +505,37 @@ def reconcile(doc: None | str = None) -> None:
|
||||
frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed")
|
||||
|
||||
|
||||
def check_multi_currency(pr_doc):
|
||||
GL = frappe.qb.DocType("GL Entry")
|
||||
Account = frappe.qb.DocType("Account")
|
||||
|
||||
def get_account_currency(voucher_type, voucher_no):
|
||||
currency = (
|
||||
frappe.qb.from_(GL)
|
||||
.join(Account)
|
||||
.on(GL.account == Account.name)
|
||||
.select(Account.account_currency)
|
||||
.where(
|
||||
(GL.voucher_type == voucher_type)
|
||||
& (GL.voucher_no == voucher_no)
|
||||
& (Account.account_type.isin(["Payable", "Receivable"]))
|
||||
)
|
||||
.limit(1)
|
||||
).run(as_dict=True)
|
||||
|
||||
return currency[0].account_currency if currency else None
|
||||
|
||||
for allocation in pr_doc.allocation:
|
||||
reference_currency = get_account_currency(allocation.reference_type, allocation.reference_name)
|
||||
|
||||
invoice_currency = get_account_currency(allocation.invoice_type, allocation.invoice_number)
|
||||
|
||||
if reference_currency != invoice_currency:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def is_any_doc_running(for_filter: str | dict | None = None) -> str | None:
|
||||
running_doc = None
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
"email_append_to": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"company",
|
||||
"title",
|
||||
"naming_series",
|
||||
"supplier",
|
||||
"supplier_name",
|
||||
"tax_id",
|
||||
"company",
|
||||
"column_break_6",
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
@@ -1668,7 +1668,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-29 21:21:53.051193",
|
||||
"modified": "2026-02-03 14:23:47.937128",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -36,7 +36,7 @@ from erpnext.accounts.utils import get_account_currency, get_fiscal_year, update
|
||||
from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
|
||||
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
||||
from erpnext.buying.utils import check_on_hold_or_closed_status
|
||||
from erpnext.controllers.accounts_controller import validate_account_head
|
||||
from erpnext.controllers.accounts_controller import merge_taxes, validate_account_head
|
||||
from erpnext.controllers.buying_controller import BuyingController
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
update_billed_amount_based_on_po,
|
||||
@@ -2005,9 +2005,17 @@ def make_purchase_receipt(source_name, target_doc=None, args=None):
|
||||
args = json.loads(args)
|
||||
|
||||
def post_parent_process(source_parent, target_parent):
|
||||
for row in target_parent.get("items"):
|
||||
if row.get("qty") == 0:
|
||||
target_parent.remove(row)
|
||||
remove_items_with_zero_qty(target_parent)
|
||||
set_missing_values(source_parent, target_parent)
|
||||
|
||||
def remove_items_with_zero_qty(target_parent):
|
||||
target_parent.items = [row for row in target_parent.get("items") if row.get("qty") != 0]
|
||||
|
||||
def set_missing_values(source_parent, target_parent):
|
||||
target_parent.run_method("set_missing_values")
|
||||
if args and args.get("merge_taxes"):
|
||||
merge_taxes(source_parent, target_parent)
|
||||
target_parent.run_method("calculate_taxes_and_totals")
|
||||
|
||||
def update_item(obj, target, source_parent):
|
||||
from erpnext.controllers.sales_and_purchase_return import get_returned_qty_map_for_row
|
||||
@@ -2059,7 +2067,11 @@ def make_purchase_receipt(source_name, target_doc=None, args=None):
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) and select_item(doc),
|
||||
},
|
||||
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges"},
|
||||
"Purchase Taxes and Charges": {
|
||||
"doctype": "Purchase Taxes and Charges",
|
||||
"reset_value": not (args and args.get("merge_taxes")),
|
||||
"ignore": args.get("merge_taxes") if args else 0,
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
post_parent_process,
|
||||
|
||||
@@ -44,6 +44,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
"Unreconcile Payment Entries",
|
||||
"Serial and Batch Bundle",
|
||||
"Bank Transaction",
|
||||
"Packing Slip",
|
||||
];
|
||||
|
||||
if (!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"customer_section",
|
||||
"company",
|
||||
"company_tax_id",
|
||||
"naming_series",
|
||||
"customer",
|
||||
"customer_name",
|
||||
"tax_id",
|
||||
"company",
|
||||
"company_tax_id",
|
||||
"column_break1",
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<table>
|
||||
<tr><td><strong>Date: </strong></td><td>{{ frappe.utils.format_date(doc.creation) }}</td></tr>
|
||||
<tr><td><strong>Date: </strong></td><td>{{ frappe.utils.format_date(doc.posting_date) }}</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -176,7 +176,9 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
|
||||
column_names = get_column_names()
|
||||
|
||||
# to display item as Item Code: Item Name
|
||||
columns[0] = "Sales Invoice:Link/Item:300"
|
||||
columns[0]["fieldname"] = "sales_invoice"
|
||||
columns[0]["options"] = "Item"
|
||||
columns[0]["width"] = 300
|
||||
# removing Item Code and Item Name columns
|
||||
supplier_master_name = frappe.db.get_single_value("Buying Settings", "supp_master_name")
|
||||
customer_master_name = frappe.db.get_single_value("Selling Settings", "cust_master_name")
|
||||
|
||||
@@ -163,11 +163,11 @@ def get_net_profit_loss(income, expense, period_list, company, currency=None, co
|
||||
|
||||
|
||||
def get_chart_data(filters, columns, income, expense, net_profit_loss, currency):
|
||||
labels = [d.get("label") for d in columns[2:]]
|
||||
labels = [d.get("label") for d in columns[4:]]
|
||||
|
||||
income_data, expense_data, net_profit = [], [], []
|
||||
|
||||
for p in columns[2:]:
|
||||
for p in columns[4:]:
|
||||
if income:
|
||||
income_data.append(income[-2].get(p.get("fieldname")))
|
||||
if expense:
|
||||
|
||||
@@ -9,10 +9,9 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"supplier_section",
|
||||
"company",
|
||||
"title",
|
||||
"naming_series",
|
||||
"supplier",
|
||||
"supplier_name",
|
||||
"order_confirmation_no",
|
||||
"order_confirmation_date",
|
||||
"get_items_from_open_material_requests",
|
||||
@@ -21,8 +20,9 @@
|
||||
"transaction_date",
|
||||
"schedule_date",
|
||||
"column_break1",
|
||||
"company",
|
||||
"supplier",
|
||||
"is_subcontracted",
|
||||
"supplier_name",
|
||||
"has_unit_price_items",
|
||||
"supplier_warehouse",
|
||||
"amended_from",
|
||||
@@ -1310,7 +1310,7 @@
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-29 21:22:54.323838",
|
||||
"modified": "2026-02-03 14:44:55.192192",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order",
|
||||
|
||||
@@ -846,12 +846,34 @@ def get_filters(
|
||||
if reference_voucher_detail_no:
|
||||
filters["voucher_detail_no"] = reference_voucher_detail_no
|
||||
|
||||
if voucher_type in ["Purchase Receipt", "Purchase Invoice"] and item_row and item_row.get("warehouse"):
|
||||
filters["warehouse"] = item_row.get("warehouse")
|
||||
warehouses = []
|
||||
if voucher_type in ["Purchase Receipt", "Purchase Invoice"] and item_row:
|
||||
if reference_voucher_detail_no:
|
||||
warehouses = get_warehouses_for_return(voucher_type, reference_voucher_detail_no)
|
||||
|
||||
if item_row.get("warehouse") and item_row.get("warehouse") in warehouses:
|
||||
filters["warehouse"] = item_row.get("warehouse")
|
||||
|
||||
return filters
|
||||
|
||||
|
||||
def get_warehouses_for_return(voucher_type, name):
|
||||
warehouses = []
|
||||
warehouse_details = frappe.get_all(
|
||||
voucher_type + " Item",
|
||||
filters={"name": name, "docstatus": 1},
|
||||
fields=["warehouse", "rejected_warehouse"],
|
||||
)
|
||||
|
||||
for d in warehouse_details:
|
||||
if d.warehouse:
|
||||
warehouses.append(d.warehouse)
|
||||
if d.rejected_warehouse:
|
||||
warehouses.append(d.rejected_warehouse)
|
||||
|
||||
return warehouses
|
||||
|
||||
|
||||
def get_returned_serial_nos(child_doc, parent_doc, serial_no_field=None, ignore_voucher_detail_no=None):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import (
|
||||
get_serial_nos as get_serial_nos_from_serial_no,
|
||||
|
||||
@@ -296,7 +296,7 @@ class SellingController(StockController):
|
||||
_(
|
||||
"""Row #{0}: Selling rate for item {1} is lower than its {2}.
|
||||
Selling {3} should be atleast {4}.<br><br>Alternatively,
|
||||
you can disable selling price validation in {5} to bypass
|
||||
you can disable '{5}' in {6} to bypass
|
||||
this validation."""
|
||||
).format(
|
||||
idx,
|
||||
@@ -304,7 +304,8 @@ class SellingController(StockController):
|
||||
bold(ref_rate_field),
|
||||
bold("net rate"),
|
||||
bold(rate),
|
||||
get_link_to_form("Selling Settings", "Selling Settings"),
|
||||
bold(frappe.get_meta("Selling Settings").get_label("validate_selling_price")),
|
||||
get_link_to_form("Selling Settings"),
|
||||
),
|
||||
title=_("Invalid Selling Price"),
|
||||
)
|
||||
@@ -313,7 +314,6 @@ class SellingController(StockController):
|
||||
return
|
||||
|
||||
is_internal_customer = self.get("is_internal_customer")
|
||||
valuation_rate_map = {}
|
||||
|
||||
for item in self.items:
|
||||
if not item.item_code or item.is_free_item:
|
||||
@@ -323,7 +323,9 @@ class SellingController(StockController):
|
||||
"Item", item.item_code, ("last_purchase_rate", "is_stock_item")
|
||||
)
|
||||
|
||||
last_purchase_rate_in_sales_uom = last_purchase_rate * (item.conversion_factor or 1)
|
||||
last_purchase_rate_in_sales_uom = flt(
|
||||
last_purchase_rate * (item.conversion_factor or 1), item.precision("base_net_rate")
|
||||
)
|
||||
|
||||
if flt(item.base_net_rate) < flt(last_purchase_rate_in_sales_uom):
|
||||
throw_message(item.idx, item.item_name, last_purchase_rate_in_sales_uom, "last purchase rate")
|
||||
@@ -331,50 +333,16 @@ class SellingController(StockController):
|
||||
if is_internal_customer or not is_stock_item:
|
||||
continue
|
||||
|
||||
valuation_rate_map[(item.item_code, item.warehouse)] = None
|
||||
|
||||
if not valuation_rate_map:
|
||||
return
|
||||
|
||||
or_conditions = (
|
||||
f"""(item_code = {frappe.db.escape(valuation_rate[0])}
|
||||
and warehouse = {frappe.db.escape(valuation_rate[1])})"""
|
||||
for valuation_rate in valuation_rate_map
|
||||
)
|
||||
|
||||
valuation_rates = frappe.db.sql(
|
||||
f"""
|
||||
select
|
||||
item_code, warehouse, valuation_rate
|
||||
from
|
||||
`tabBin`
|
||||
where
|
||||
({" or ".join(or_conditions)})
|
||||
and valuation_rate > 0
|
||||
""",
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
for rate in valuation_rates:
|
||||
valuation_rate_map[(rate.item_code, rate.warehouse)] = rate.valuation_rate
|
||||
|
||||
for item in self.items:
|
||||
if not item.item_code or item.is_free_item:
|
||||
continue
|
||||
|
||||
last_valuation_rate = valuation_rate_map.get((item.item_code, item.warehouse))
|
||||
|
||||
if not last_valuation_rate:
|
||||
continue
|
||||
|
||||
last_valuation_rate_in_sales_uom = last_valuation_rate * (item.conversion_factor or 1)
|
||||
|
||||
if flt(item.base_net_rate) < flt(last_valuation_rate_in_sales_uom):
|
||||
if item.get("incoming_rate") and item.base_net_rate < (
|
||||
valuation_rate := flt(
|
||||
item.incoming_rate * (item.conversion_factor or 1), item.precision("base_net_rate")
|
||||
)
|
||||
):
|
||||
throw_message(
|
||||
item.idx,
|
||||
item.item_name,
|
||||
last_valuation_rate_in_sales_uom,
|
||||
"valuation rate (Moving Average)",
|
||||
valuation_rate,
|
||||
"valuation rate",
|
||||
)
|
||||
|
||||
def get_item_list(self):
|
||||
@@ -533,6 +501,8 @@ class SellingController(StockController):
|
||||
if self.doctype not in ("Delivery Note", "Sales Invoice"):
|
||||
return
|
||||
|
||||
from erpnext.stock.serial_batch_bundle import get_batch_nos
|
||||
|
||||
allow_at_arms_length_price = frappe.get_cached_value(
|
||||
"Stock Settings", None, "allow_internal_transfer_at_arms_length_price"
|
||||
)
|
||||
@@ -540,6 +510,7 @@ class SellingController(StockController):
|
||||
"Selling Settings", "set_zero_rate_for_expired_batch"
|
||||
)
|
||||
|
||||
old_doc = self.get_doc_before_save()
|
||||
items = self.get("items") + (self.get("packed_items") or [])
|
||||
for d in items:
|
||||
if not frappe.get_cached_value("Item", d.item_code, "is_stock_item"):
|
||||
@@ -569,6 +540,29 @@ class SellingController(StockController):
|
||||
# Get incoming rate based on original item cost based on valuation method
|
||||
qty = flt(d.get("stock_qty") or d.get("actual_qty") or d.get("qty"))
|
||||
|
||||
if old_doc:
|
||||
old_item = next(
|
||||
(
|
||||
item
|
||||
for item in (old_doc.get("items") + (old_doc.get("packed_items") or []))
|
||||
if item.name == d.name
|
||||
),
|
||||
None,
|
||||
)
|
||||
if old_item:
|
||||
old_qty = flt(
|
||||
old_item.get("stock_qty") or old_item.get("actual_qty") or old_item.get("qty")
|
||||
)
|
||||
if (
|
||||
old_item.item_code != d.item_code
|
||||
or old_item.warehouse != d.warehouse
|
||||
or old_qty != qty
|
||||
or old_item.batch_no != d.batch_no
|
||||
or get_batch_nos(old_item.serial_and_batch_bundle)
|
||||
!= get_batch_nos(d.serial_and_batch_bundle)
|
||||
):
|
||||
d.incoming_rate = 0
|
||||
|
||||
if (
|
||||
not d.incoming_rate
|
||||
or self.is_internal_transfer()
|
||||
|
||||
@@ -91,7 +91,8 @@ status_map = {
|
||||
],
|
||||
"Delivery Note": [
|
||||
["Draft", None],
|
||||
["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"],
|
||||
["To Bill", "eval:self.per_billed == 0 and self.docstatus == 1"],
|
||||
["Partially Billed", "eval:self.per_billed < 100 and self.per_billed > 0 and self.docstatus == 1"],
|
||||
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
|
||||
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
|
||||
["Return", "eval:self.is_return == 1 and self.per_billed == 0 and self.docstatus == 1"],
|
||||
|
||||
@@ -313,10 +313,10 @@ class SubcontractingController(StockController):
|
||||
):
|
||||
for row in frappe.get_all(
|
||||
f"{self.subcontract_data.order_doctype} Item",
|
||||
fields=["item_code", {"SUB": ["qty", "received_qty"], "as": "qty"}, "parent", "name"],
|
||||
fields=["item_code", {"SUB": ["qty", "received_qty"], "as": "qty"}, "parent", "bom"],
|
||||
filters={"docstatus": 1, "parent": ("in", self.subcontract_orders)},
|
||||
):
|
||||
self.qty_to_be_received[(row.item_code, row.parent)] += row.qty
|
||||
self.qty_to_be_received[(row.item_code, row.parent, row.bom)] += row.qty
|
||||
|
||||
def __get_transferred_items(self):
|
||||
se = frappe.qb.DocType("Stock Entry")
|
||||
@@ -922,13 +922,17 @@ class SubcontractingController(StockController):
|
||||
self.__set_serial_nos(item_row, rm_obj)
|
||||
|
||||
def __get_qty_based_on_material_transfer(self, item_row, transfer_item):
|
||||
key = (item_row.item_code, item_row.get(self.subcontract_data.order_field))
|
||||
key = (
|
||||
item_row.item_code,
|
||||
item_row.get(self.subcontract_data.order_field),
|
||||
item_row.get("bom"),
|
||||
)
|
||||
|
||||
if self.qty_to_be_received == item_row.qty:
|
||||
return transfer_item.qty
|
||||
|
||||
if self.qty_to_be_received:
|
||||
qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key, 0))
|
||||
if self.qty_to_be_received.get(key):
|
||||
qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key))
|
||||
transfer_item.item_details.required_qty = transfer_item.qty
|
||||
|
||||
if transfer_item.serial_no or frappe.get_cached_value(
|
||||
@@ -977,7 +981,11 @@ class SubcontractingController(StockController):
|
||||
|
||||
if self.qty_to_be_received:
|
||||
self.qty_to_be_received[
|
||||
(row.item_code, row.get(self.subcontract_data.order_field))
|
||||
(
|
||||
row.item_code,
|
||||
row.get(self.subcontract_data.order_field),
|
||||
row.get("bom"),
|
||||
)
|
||||
] -= row.qty
|
||||
|
||||
def __set_rate_for_serial_and_batch_bundle(self):
|
||||
|
||||
@@ -461,3 +461,4 @@ erpnext.patches.v15_0.create_accounting_dimensions_in_advance_taxes_and_charges
|
||||
execute:frappe.delete_doc_if_exists("Workspace Sidebar", "Opening & Closing")
|
||||
erpnext.patches.v16_0.migrate_transaction_deletion_task_flags_to_status # 2
|
||||
erpnext.patches.v16_0.set_ordered_qty_in_quotation_item
|
||||
erpnext.patches.v16_0.update_company_custom_field_in_bin
|
||||
14
erpnext/patches/v16_0/update_company_custom_field_in_bin.py
Normal file
14
erpnext/patches/v16_0/update_company_custom_field_in_bin.py
Normal file
@@ -0,0 +1,14 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc("stock", "doctype", "bin")
|
||||
|
||||
frappe.db.sql(
|
||||
"""
|
||||
UPDATE `tabBin` b
|
||||
INNER JOIN `tabWarehouse` w ON b.warehouse = w.name
|
||||
SET b.company = w.company
|
||||
WHERE b.company IS NULL OR b.company = ''
|
||||
"""
|
||||
)
|
||||
@@ -308,6 +308,8 @@ class Project(Document):
|
||||
self.gross_margin = flt(self.total_billed_amount) - expense_amount
|
||||
if self.total_billed_amount:
|
||||
self.per_gross_margin = (self.gross_margin / flt(self.total_billed_amount)) * 100
|
||||
else:
|
||||
self.per_gross_margin = 0
|
||||
|
||||
def update_purchase_costing(self):
|
||||
total_purchase_cost = calculate_total_purchase_cost(self.name)
|
||||
|
||||
|
Before Width: | Height: | Size: 787 B After Width: | Height: | Size: 787 B |
|
Before Width: | Height: | Size: 809 B After Width: | Height: | Size: 809 B |
@@ -625,6 +625,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
me.frm.refresh_fields();
|
||||
me.show_batch_dialog_if_required(item);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -635,26 +636,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
|
||||
process_item_selection(doc, cdt, cdn) {
|
||||
var item = frappe.get_doc(cdt, cdn);
|
||||
let update_stock = 0;
|
||||
var me = this;
|
||||
var update_stock = 0,
|
||||
show_batch_dialog = 0;
|
||||
|
||||
item.weight_per_unit = 0;
|
||||
item.weight_uom = "";
|
||||
item.uom = null; // make UOM blank to update the existing UOM when item changes
|
||||
item.conversion_factor = 0;
|
||||
|
||||
if (["Sales Invoice", "Purchase Invoice"].includes(this.frm.doc.doctype)) {
|
||||
update_stock = cint(me.frm.doc.update_stock);
|
||||
show_batch_dialog = update_stock;
|
||||
} else if (this.frm.doc.doctype === "Purchase Receipt" || this.frm.doc.doctype === "Delivery Note") {
|
||||
show_batch_dialog = 1;
|
||||
}
|
||||
|
||||
if (show_batch_dialog && item.use_serial_batch_fields === 1) {
|
||||
show_batch_dialog = 0;
|
||||
}
|
||||
|
||||
item.barcode = null;
|
||||
|
||||
if (item.item_code || item.serial_no) {
|
||||
@@ -765,74 +753,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
},
|
||||
() => me.toggle_conversion_factor(item),
|
||||
() => {
|
||||
if (show_batch_dialog && !frappe.flags.trigger_from_barcode_scanner)
|
||||
return frappe.db
|
||||
.get_value("Item", item.item_code, [
|
||||
"has_batch_no",
|
||||
"has_serial_no",
|
||||
])
|
||||
.then((r) => {
|
||||
if (
|
||||
r.message &&
|
||||
(r.message.has_batch_no || r.message.has_serial_no)
|
||||
) {
|
||||
frappe.flags.hide_serial_batch_dialog = false;
|
||||
} else {
|
||||
show_batch_dialog = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
() => {
|
||||
// check if batch serial selector is disabled or not
|
||||
if (show_batch_dialog && !frappe.flags.hide_serial_batch_dialog)
|
||||
return frappe.db
|
||||
.get_single_value(
|
||||
"Stock Settings",
|
||||
"disable_serial_no_and_batch_selector"
|
||||
)
|
||||
.then((value) => {
|
||||
if (value) {
|
||||
frappe.flags.hide_serial_batch_dialog = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
() => {
|
||||
if (
|
||||
show_batch_dialog &&
|
||||
!frappe.flags.hide_serial_batch_dialog &&
|
||||
!frappe.flags.dialog_set
|
||||
) {
|
||||
var d = locals[cdt][cdn];
|
||||
$.each(r.message, function (k, v) {
|
||||
if (!d[k]) d[k] = v;
|
||||
});
|
||||
|
||||
if (d.has_batch_no && d.has_serial_no) {
|
||||
d.batch_no = undefined;
|
||||
}
|
||||
|
||||
frappe.flags.dialog_set = true;
|
||||
erpnext.show_serial_batch_selector(
|
||||
me.frm,
|
||||
d,
|
||||
(item) => {
|
||||
me.frm.script_manager.trigger("qty", item.doctype, item.name);
|
||||
if (!me.frm.doc.set_warehouse)
|
||||
me.frm.script_manager.trigger(
|
||||
"warehouse",
|
||||
item.doctype,
|
||||
item.name
|
||||
);
|
||||
me.apply_price_list(item, true);
|
||||
},
|
||||
undefined,
|
||||
!frappe.flags.hide_serial_batch_dialog
|
||||
);
|
||||
} else {
|
||||
frappe.flags.dialog_set = false;
|
||||
}
|
||||
},
|
||||
() => me.show_batch_dialog_if_required(item),
|
||||
() => me.conversion_factor(doc, cdt, cdn, true),
|
||||
() => me.remove_pricing_rule(item),
|
||||
() => {
|
||||
@@ -853,6 +774,78 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
}
|
||||
|
||||
show_batch_dialog_if_required(item) {
|
||||
let show_batch_dialog = 0;
|
||||
let update_stock = 0;
|
||||
let me = this;
|
||||
|
||||
if (!item.item_code) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (["Sales Invoice", "Purchase Invoice"].includes(this.frm.doc.doctype)) {
|
||||
update_stock = cint(me.frm.doc.update_stock);
|
||||
show_batch_dialog = update_stock;
|
||||
} else if (this.frm.doc.doctype === "Purchase Receipt" || this.frm.doc.doctype === "Delivery Note") {
|
||||
show_batch_dialog = 1;
|
||||
}
|
||||
|
||||
if (show_batch_dialog && item.use_serial_batch_fields === 1) {
|
||||
show_batch_dialog = 0;
|
||||
}
|
||||
|
||||
frappe.run_serially([
|
||||
() => {
|
||||
if (show_batch_dialog && !frappe.flags.trigger_from_barcode_scanner)
|
||||
return frappe.db
|
||||
.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
|
||||
.then((r) => {
|
||||
if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) {
|
||||
item.has_serial_no = r.message.has_serial_no;
|
||||
item.has_batch_no = r.message.has_batch_no;
|
||||
frappe.flags.hide_serial_batch_dialog = false;
|
||||
} else {
|
||||
show_batch_dialog = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
() => {
|
||||
// check if batch serial selector is disabled or not
|
||||
if (show_batch_dialog && !frappe.flags.hide_serial_batch_dialog)
|
||||
return frappe.db
|
||||
.get_single_value("Stock Settings", "disable_serial_no_and_batch_selector")
|
||||
.then((value) => {
|
||||
if (value) {
|
||||
frappe.flags.hide_serial_batch_dialog = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
() => {
|
||||
if (show_batch_dialog && !frappe.flags.hide_serial_batch_dialog && !frappe.flags.dialog_set) {
|
||||
if (item.has_batch_no && item.has_serial_no) {
|
||||
item.batch_no = undefined;
|
||||
}
|
||||
|
||||
frappe.flags.dialog_set = true;
|
||||
erpnext.show_serial_batch_selector(
|
||||
me.frm,
|
||||
item,
|
||||
(item) => {
|
||||
me.frm.script_manager.trigger("qty", item.doctype, item.name);
|
||||
if (!me.frm.doc.set_warehouse)
|
||||
me.frm.script_manager.trigger("warehouse", item.doctype, item.name);
|
||||
me.apply_price_list(item, true);
|
||||
},
|
||||
undefined,
|
||||
!frappe.flags.hide_serial_batch_dialog
|
||||
);
|
||||
} else {
|
||||
frappe.flags.dialog_set = false;
|
||||
}
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
price_list_rate(doc, cdt, cdn) {
|
||||
var item = frappe.get_doc(cdt, cdn);
|
||||
frappe.model.round_floats_in(item, ["price_list_rate", "discount_percentage"]);
|
||||
|
||||
@@ -994,7 +994,7 @@ erpnext.utils.map_current_doc = function (opts) {
|
||||
|
||||
if (opts.source_doctype) {
|
||||
let data_fields = [];
|
||||
if (["Purchase Receipt", "Delivery Note"].includes(opts.source_doctype)) {
|
||||
if (["Purchase Receipt", "Delivery Note", "Purchase Invoice"].includes(opts.source_doctype)) {
|
||||
let target_meta = frappe.get_meta(cur_frm.doc.doctype);
|
||||
if (target_meta.fields.find((f) => f.fieldname === "taxes")) {
|
||||
data_fields.push({
|
||||
|
||||
@@ -110,12 +110,6 @@ frappe.ui.form.ContactAddressQuickEntryForm = class ContactAddressQuickEntryForm
|
||||
options: "Country",
|
||||
mandatory_depends_on: "eval:doc.city || doc.address_line1",
|
||||
},
|
||||
{
|
||||
label: __("Customer POS Id"),
|
||||
fieldname: "customer_pos_id",
|
||||
fieldtype: "Data",
|
||||
hidden: 1,
|
||||
},
|
||||
];
|
||||
|
||||
return variant_fields;
|
||||
|
||||
@@ -142,7 +142,14 @@ def download_zip(files, output_filename):
|
||||
|
||||
def get_invoice_summary(items, taxes, item_wise_tax_details):
|
||||
summary_data = frappe._dict()
|
||||
taxes_wise_tax_details = {d.tax_row: d for d in item_wise_tax_details}
|
||||
taxes_wise_tax_details = {}
|
||||
|
||||
for d in item_wise_tax_details:
|
||||
if d.tax_row not in taxes_wise_tax_details:
|
||||
taxes_wise_tax_details[d.tax_row] = []
|
||||
|
||||
taxes_wise_tax_details[d.tax_row].append(d)
|
||||
|
||||
for tax in taxes:
|
||||
# Include only VAT charges.
|
||||
if tax.charge_type == "Actual":
|
||||
|
||||
@@ -113,12 +113,37 @@ class Customer(TransactionBase):
|
||||
def get_customer_name(self):
|
||||
self.customer_name = self.customer_name.strip()
|
||||
if frappe.db.get_value("Customer", self.customer_name) and not frappe.flags.in_import:
|
||||
count = frappe.db.sql(
|
||||
"""select ifnull(MAX(CAST(SUBSTRING_INDEX(name, ' ', -1) AS UNSIGNED)), 0) from tabCustomer
|
||||
where name like %s""",
|
||||
f"%{self.customer_name} - %",
|
||||
as_list=1,
|
||||
)[0][0]
|
||||
name_prefix = f"{self.customer_name} - %"
|
||||
|
||||
if frappe.db.db_type == "postgres":
|
||||
# Postgres: extract trailing digits (e.g. "Customer - 3") and cast to int.
|
||||
# NOTE: PostgreSQL is strict about types; MySQL's UNSIGNED cast does not exist.
|
||||
count = frappe.db.sql(
|
||||
"""
|
||||
SELECT COALESCE(
|
||||
MAX(CAST(SUBSTRING(name FROM '\\d+$') AS INTEGER)),
|
||||
0
|
||||
)
|
||||
FROM tabCustomer
|
||||
WHERE name LIKE %(name_prefix)s
|
||||
""",
|
||||
{"name_prefix": name_prefix},
|
||||
as_list=1,
|
||||
)[0][0]
|
||||
else:
|
||||
# MariaDB/MySQL: keep existing behavior.
|
||||
count = frappe.db.sql(
|
||||
"""
|
||||
SELECT COALESCE(
|
||||
MAX(CAST(SUBSTRING_INDEX(name, ' ', -1) AS UNSIGNED)),
|
||||
0
|
||||
)
|
||||
FROM tabCustomer
|
||||
WHERE name LIKE %(name_prefix)s
|
||||
""",
|
||||
{"name_prefix": name_prefix},
|
||||
as_list=1,
|
||||
)[0][0]
|
||||
count = cint(count) + 1
|
||||
|
||||
new_customer_name = f"{self.customer_name} - {cstr(count)}"
|
||||
@@ -506,6 +531,9 @@ def _set_missing_values(source, target):
|
||||
|
||||
if contact:
|
||||
target.contact_person = contact[0].parent
|
||||
target.contact_display, target.contact_email, target.contact_mobile = frappe.get_value(
|
||||
"Contact", contact[0].parent, ["full_name", "email_id", "mobile_no"]
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -11,18 +11,18 @@
|
||||
"field_order": [
|
||||
"customer_section",
|
||||
"column_break0",
|
||||
"company",
|
||||
"naming_series",
|
||||
"customer",
|
||||
"customer_name",
|
||||
"tax_id",
|
||||
"order_type",
|
||||
"column_break_7",
|
||||
"transaction_date",
|
||||
"delivery_date",
|
||||
"column_break1",
|
||||
"customer",
|
||||
"customer_name",
|
||||
"tax_id",
|
||||
"po_no",
|
||||
"po_date",
|
||||
"company",
|
||||
"skip_delivery_note",
|
||||
"has_unit_price_items",
|
||||
"is_subcontracted",
|
||||
@@ -1713,7 +1713,7 @@
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-29 21:23:48.362401",
|
||||
"modified": "2026-02-03 14:45:50.314361",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order",
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"reserved_stock",
|
||||
"section_break_pmrs",
|
||||
"stock_uom",
|
||||
"company",
|
||||
"column_break_0slj",
|
||||
"valuation_rate",
|
||||
"stock_value"
|
||||
@@ -132,6 +133,14 @@
|
||||
"options": "UOM",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "warehouse.company",
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "valuation_rate",
|
||||
"fieldtype": "Float",
|
||||
@@ -186,7 +195,7 @@
|
||||
"idx": 1,
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:06:39.414036",
|
||||
"modified": "2026-02-01 08:11:46.824913",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Bin",
|
||||
@@ -231,8 +240,9 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "item_code,warehouse",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ class Bin(Document):
|
||||
from frappe.types import DF
|
||||
|
||||
actual_qty: DF.Float
|
||||
company: DF.Link | None
|
||||
indented_qty: DF.Float
|
||||
item_code: DF.Link
|
||||
ordered_qty: DF.Float
|
||||
|
||||
@@ -1070,7 +1070,7 @@
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "status",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "\nDraft\nTo Bill\nCompleted\nReturn\nReturn Issued\nCancelled\nClosed",
|
||||
"options": "\nDraft\nTo Bill\nPartially Billed\nCompleted\nReturn\nReturn Issued\nCancelled\nClosed",
|
||||
"print_hide": 1,
|
||||
"print_width": "150px",
|
||||
"read_only": 1,
|
||||
@@ -1434,7 +1434,7 @@
|
||||
"idx": 146,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-29 21:24:11.781261",
|
||||
"modified": "2026-02-03 12:27:19.055918",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Delivery Note",
|
||||
|
||||
@@ -127,7 +127,15 @@ class DeliveryNote(SellingController):
|
||||
shipping_address_name: DF.Link | None
|
||||
shipping_rule: DF.Link | None
|
||||
status: DF.Literal[
|
||||
"", "Draft", "To Bill", "Completed", "Return", "Return Issued", "Cancelled", "Closed"
|
||||
"",
|
||||
"Draft",
|
||||
"To Bill",
|
||||
"Partially Billed",
|
||||
"Completed",
|
||||
"Return",
|
||||
"Return Issued",
|
||||
"Cancelled",
|
||||
"Closed",
|
||||
]
|
||||
tax_category: DF.Link | None
|
||||
tax_id: DF.Data | None
|
||||
|
||||
@@ -18,8 +18,10 @@ frappe.listview_settings["Delivery Note"] = {
|
||||
return [__("Closed"), "green", "status,=,Closed"];
|
||||
} else if (doc.status === "Return Issued") {
|
||||
return [__("Return Issued"), "grey", "status,=,Return Issued"];
|
||||
} else if (flt(doc.per_billed, 2) < 100) {
|
||||
return [__("To Bill"), "orange", "per_billed,<,100|docstatus,=,1"];
|
||||
} else if (flt(doc.per_billed) == 0) {
|
||||
return [__("To Bill"), "orange", "per_billed,=,0|docstatus,=,1"];
|
||||
} else if (flt(doc.per_billed, 2) > 0 && flt(doc.per_billed, 2) < 100) {
|
||||
return [__("Partially Billed"), "yellow", "per_billed,<,100|docstatus,=,1"];
|
||||
} else if (flt(doc.per_billed, 2) === 100) {
|
||||
return [__("Completed"), "green", "per_billed,=,100|docstatus,=,1"];
|
||||
}
|
||||
|
||||
@@ -1101,7 +1101,8 @@ class TestDeliveryNote(IntegrationTestCase):
|
||||
|
||||
self.assertEqual(dn2.get("items")[0].billed_amt, 400)
|
||||
self.assertEqual(dn2.per_billed, 80)
|
||||
self.assertEqual(dn2.status, "To Bill")
|
||||
# Since 20% of DN2 is yet to be billed, it should be classified as partially billed.
|
||||
self.assertEqual(dn2.status, "Partially Billed")
|
||||
|
||||
def test_dn_billing_status_case4(self):
|
||||
# SO -> SI -> DN
|
||||
@@ -2863,6 +2864,23 @@ class TestDeliveryNote(IntegrationTestCase):
|
||||
for entry in sabb.entries:
|
||||
self.assertEqual(entry.incoming_rate, 200)
|
||||
|
||||
@IntegrationTestCase.change_settings("Selling Settings", {"validate_selling_price": 1})
|
||||
def test_validate_selling_price(self):
|
||||
item_code = make_item("VSP Item", properties={"is_stock_item": 1}).name
|
||||
make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=10)
|
||||
make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=1)
|
||||
|
||||
dn = create_delivery_note(
|
||||
item_code=item_code,
|
||||
qty=1,
|
||||
rate=9,
|
||||
do_not_save=True,
|
||||
)
|
||||
self.assertRaises(frappe.ValidationError, dn.save)
|
||||
dn.items[0].incoming_rate = 0
|
||||
dn.items[0].stock_qty = 2
|
||||
dn.save()
|
||||
|
||||
|
||||
def create_delivery_note(**args):
|
||||
dn = frappe.new_doc("Delivery Note")
|
||||
|
||||
@@ -5111,6 +5111,128 @@ class TestPurchaseReceipt(IntegrationTestCase):
|
||||
self.assertEqual(stk_ledger.incoming_rate, 120)
|
||||
self.assertEqual(stk_ledger.stock_value_difference, 600)
|
||||
|
||||
def test_negative_stock_error_for_purchase_return_when_stock_exists_in_future_date(self):
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
from erpnext.stock.stock_ledger import NegativeStockError
|
||||
|
||||
item_code = make_item(
|
||||
"Test Negative Stock for Purchase Return with Future Stock Item",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "TNSPFPRI.#####",
|
||||
},
|
||||
).name
|
||||
|
||||
make_purchase_receipt(
|
||||
item_code=item_code,
|
||||
posting_date=add_days(today(), -4),
|
||||
qty=100,
|
||||
rate=100,
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
pr1 = make_purchase_receipt(
|
||||
item_code=item_code,
|
||||
posting_date=add_days(today(), -3),
|
||||
qty=100,
|
||||
rate=100,
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
batch1 = get_batch_from_bundle(pr1.items[0].serial_and_batch_bundle)
|
||||
|
||||
pr2 = make_purchase_receipt(
|
||||
item_code=item_code,
|
||||
posting_date=add_days(today(), -2),
|
||||
qty=100,
|
||||
rate=100,
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
batch2 = get_batch_from_bundle(pr2.items[0].serial_and_batch_bundle)
|
||||
|
||||
make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=100,
|
||||
posting_date=add_days(today(), -1),
|
||||
source="_Test Warehouse - _TC",
|
||||
target="_Test Warehouse 1 - _TC",
|
||||
batch_no=batch1,
|
||||
use_serial_batch_fields=1,
|
||||
)
|
||||
|
||||
make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=100,
|
||||
posting_date=add_days(today(), -1),
|
||||
source="_Test Warehouse - _TC",
|
||||
target="_Test Warehouse 1 - _TC",
|
||||
batch_no=batch2,
|
||||
use_serial_batch_fields=1,
|
||||
)
|
||||
|
||||
make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=100,
|
||||
posting_date=today(),
|
||||
source="_Test Warehouse 1 - _TC",
|
||||
target="_Test Warehouse - _TC",
|
||||
batch_no=batch1,
|
||||
use_serial_batch_fields=1,
|
||||
)
|
||||
|
||||
make_purchase_entry = make_return_doc("Purchase Receipt", pr1.name)
|
||||
make_purchase_entry.set_posting_time = 1
|
||||
make_purchase_entry.posting_date = pr1.posting_date
|
||||
self.assertRaises(NegativeStockError, make_purchase_entry.submit)
|
||||
|
||||
def test_purchase_return_from_different_warehouse(self):
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
item_code = make_item(
|
||||
"Test Purchase Return From Different Warehouse Item",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "TPRFDWU.#####",
|
||||
},
|
||||
).name
|
||||
|
||||
pr1 = make_purchase_receipt(
|
||||
item_code=item_code,
|
||||
posting_date=add_days(today(), -4),
|
||||
qty=100,
|
||||
rate=100,
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
batch1 = get_batch_from_bundle(pr1.items[0].serial_and_batch_bundle)
|
||||
|
||||
make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=100,
|
||||
posting_date=add_days(today(), -1),
|
||||
source="_Test Warehouse - _TC",
|
||||
target="_Test Warehouse 1 - _TC",
|
||||
batch_no=batch1,
|
||||
use_serial_batch_fields=1,
|
||||
)
|
||||
|
||||
make_purchase_entry = make_return_doc("Purchase Receipt", pr1.name)
|
||||
make_purchase_entry.items[0].warehouse = "_Test Warehouse 1 - _TC"
|
||||
make_purchase_entry.submit()
|
||||
make_purchase_entry.reload()
|
||||
|
||||
sabb = frappe.get_doc("Serial and Batch Bundle", make_purchase_entry.items[0].serial_and_batch_bundle)
|
||||
for row in sabb.entries:
|
||||
self.assertEqual(row.warehouse, "_Test Warehouse 1 - _TC")
|
||||
self.assertEqual(row.incoming_rate, 100)
|
||||
|
||||
|
||||
def prepare_data_for_internal_transfer():
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||
|
||||
@@ -17,6 +17,7 @@ from frappe.utils import (
|
||||
cint,
|
||||
cstr,
|
||||
flt,
|
||||
get_datetime,
|
||||
get_link_to_form,
|
||||
getdate,
|
||||
now,
|
||||
@@ -438,6 +439,8 @@ class SerialandBatchBundle(Document):
|
||||
)
|
||||
|
||||
def get_valuation_rate_for_return_entry(self, return_against):
|
||||
from erpnext.controllers.sales_and_purchase_return import get_warehouses_for_return
|
||||
|
||||
if not self.voucher_detail_no:
|
||||
return {}
|
||||
|
||||
@@ -467,9 +470,11 @@ class SerialandBatchBundle(Document):
|
||||
["Serial and Batch Bundle", "voucher_detail_no", "=", return_against_voucher_detail_no],
|
||||
]
|
||||
|
||||
# Added to handle rejected warehouse case
|
||||
if self.voucher_type in ["Purchase Receipt", "Purchase Invoice"]:
|
||||
# Added to handle rejected warehouse case
|
||||
filters.append(["Serial and Batch Entry", "warehouse", "=", self.warehouse])
|
||||
warehouses = get_warehouses_for_return(self.voucher_type, return_against_voucher_detail_no)
|
||||
if self.warehouse in warehouses:
|
||||
filters.append(["Serial and Batch Entry", "warehouse", "=", self.warehouse])
|
||||
|
||||
bundle_data = frappe.get_all(
|
||||
"Serial and Batch Bundle",
|
||||
@@ -1451,31 +1456,44 @@ class SerialandBatchBundle(Document):
|
||||
for d in self.entries:
|
||||
available_qty = batch_wise_available_qty.get(d.batch_no, 0)
|
||||
if flt(available_qty, precision) < 0:
|
||||
frappe.throw(
|
||||
_(
|
||||
"""
|
||||
The Batch {0} of an item {1} has negative stock in the warehouse {2}. Please add a stock quantity of {3} to proceed with this entry."""
|
||||
).format(
|
||||
bold(d.batch_no),
|
||||
bold(self.item_code),
|
||||
bold(self.warehouse),
|
||||
bold(abs(flt(available_qty, precision))),
|
||||
),
|
||||
title=_("Negative Stock Error"),
|
||||
)
|
||||
self.throw_negative_batch(d.batch_no, available_qty, precision)
|
||||
|
||||
def throw_negative_batch(self, batch_no, available_qty, precision):
|
||||
from erpnext.stock.stock_ledger import NegativeStockError
|
||||
|
||||
frappe.throw(
|
||||
_(
|
||||
"""
|
||||
The Batch {0} of an item {1} has negative stock in the warehouse {2}. Please add a stock quantity of {3} to proceed with this entry."""
|
||||
).format(
|
||||
bold(batch_no),
|
||||
bold(self.item_code),
|
||||
bold(self.warehouse),
|
||||
bold(abs(flt(available_qty, precision))),
|
||||
),
|
||||
title=_("Negative Stock Error"),
|
||||
exc=NegativeStockError,
|
||||
)
|
||||
|
||||
def get_batchwise_available_qty(self):
|
||||
available_qty = self.get_available_qty_from_sabb()
|
||||
available_qty_from_ledger = self.get_available_qty_from_stock_ledger()
|
||||
batchwise_entries = self.get_available_qty_from_sabb()
|
||||
batchwise_entries.extend(self.get_available_qty_from_stock_ledger())
|
||||
|
||||
if not available_qty_from_ledger:
|
||||
return available_qty
|
||||
available_qty = frappe._dict({})
|
||||
batchwise_entries = sorted(
|
||||
batchwise_entries,
|
||||
key=lambda x: (get_datetime(x.get("posting_datetime")), get_datetime(x.get("creation"))),
|
||||
)
|
||||
|
||||
for batch_no, qty in available_qty_from_ledger.items():
|
||||
if batch_no in available_qty:
|
||||
available_qty[batch_no] += qty
|
||||
precision = frappe.get_precision("Serial and Batch Entry", "qty")
|
||||
for row in batchwise_entries:
|
||||
if row.batch_no in available_qty:
|
||||
available_qty[row.batch_no] += flt(row.qty)
|
||||
else:
|
||||
available_qty[batch_no] = qty
|
||||
available_qty[row.batch_no] = flt(row.qty)
|
||||
|
||||
if flt(available_qty[row.batch_no], precision) < 0:
|
||||
self.throw_negative_batch(row.batch_no, available_qty[row.batch_no], precision)
|
||||
|
||||
return available_qty
|
||||
|
||||
@@ -1488,7 +1506,9 @@ class SerialandBatchBundle(Document):
|
||||
frappe.qb.from_(sle)
|
||||
.select(
|
||||
sle.batch_no,
|
||||
Sum(sle.actual_qty).as_("available_qty"),
|
||||
sle.actual_qty.as_("qty"),
|
||||
sle.posting_datetime,
|
||||
sle.creation,
|
||||
)
|
||||
.where(
|
||||
(sle.item_code == self.item_code)
|
||||
@@ -1500,12 +1520,9 @@ class SerialandBatchBundle(Document):
|
||||
& (sle.batch_no.isnotnull())
|
||||
)
|
||||
.for_update()
|
||||
.groupby(sle.batch_no)
|
||||
)
|
||||
|
||||
res = query.run(as_list=True)
|
||||
|
||||
return frappe._dict(res) if res else frappe._dict()
|
||||
return query.run(as_dict=True)
|
||||
|
||||
def get_available_qty_from_sabb(self):
|
||||
batches = [d.batch_no for d in self.entries if d.batch_no]
|
||||
@@ -1516,7 +1533,9 @@ class SerialandBatchBundle(Document):
|
||||
frappe.qb.from_(child)
|
||||
.select(
|
||||
child.batch_no,
|
||||
Sum(child.qty).as_("available_qty"),
|
||||
child.qty,
|
||||
child.posting_datetime,
|
||||
child.creation,
|
||||
)
|
||||
.where(
|
||||
(child.item_code == self.item_code)
|
||||
@@ -1527,13 +1546,10 @@ class SerialandBatchBundle(Document):
|
||||
& (child.type_of_transaction.isin(["Inward", "Outward"]))
|
||||
)
|
||||
.for_update()
|
||||
.groupby(child.batch_no)
|
||||
)
|
||||
query = query.where(child.voucher_type != "Pick List")
|
||||
|
||||
res = query.run(as_list=True)
|
||||
|
||||
return frappe._dict(res) if res else frappe._dict()
|
||||
return query.run(as_dict=True)
|
||||
|
||||
def validate_voucher_no_docstatus(self):
|
||||
if self.voucher_type == "POS Invoice":
|
||||
@@ -2596,11 +2612,11 @@ def get_reserved_batches_for_pos(kwargs) -> dict:
|
||||
|
||||
key = (row.batch_no, row.warehouse)
|
||||
if key in pos_batches:
|
||||
pos_batches[key]["qty"] -= row.qty * -1 if row.is_return else row.qty
|
||||
pos_batches[key]["qty"] += row.qty * -1
|
||||
else:
|
||||
pos_batches[key] = frappe._dict(
|
||||
{
|
||||
"qty": (row.qty * -1 if not row.is_return else row.qty),
|
||||
"qty": row.qty * -1,
|
||||
"warehouse": row.warehouse,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1271,15 +1271,11 @@ def get_items(warehouse, posting_date, posting_time, company, item_code=None, ig
|
||||
|
||||
for d in items:
|
||||
if (d.item_code, d.warehouse) in itemwise_batch_data:
|
||||
valuation_rate = get_stock_balance(
|
||||
d.item_code, d.warehouse, posting_date, posting_time, with_valuation_rate=True
|
||||
)[1]
|
||||
|
||||
for row in itemwise_batch_data.get((d.item_code, d.warehouse)):
|
||||
if ignore_empty_stock and not row.qty:
|
||||
continue
|
||||
|
||||
args = get_item_data(row, row.qty, valuation_rate)
|
||||
args = get_item_data(row, row.qty, row.valuation_rate)
|
||||
res.append(args)
|
||||
else:
|
||||
stock_bal = get_stock_balance(
|
||||
@@ -1413,6 +1409,7 @@ def get_itemwise_batch(warehouse, posting_date, company, item_code=None):
|
||||
"item_code": row[0],
|
||||
"warehouse": row[3],
|
||||
"qty": row[8],
|
||||
"valuation_rate": row[9],
|
||||
"item_name": row[1],
|
||||
"batch_no": row[4],
|
||||
}
|
||||
|
||||
@@ -282,7 +282,11 @@ class StockBalanceReport:
|
||||
for field in self.inventory_dimensions:
|
||||
qty_dict[field] = entry.get(field)
|
||||
|
||||
if entry.voucher_type == "Stock Reconciliation" and (not entry.batch_no or entry.serial_no):
|
||||
if (
|
||||
entry.voucher_type == "Stock Reconciliation"
|
||||
and frappe.get_cached_value(entry.voucher_type, entry.voucher_no, "purpose") != "Opening Stock"
|
||||
and (not entry.batch_no or entry.serial_no)
|
||||
):
|
||||
qty_diff = flt(entry.qty_after_transaction) - flt(qty_dict.bal_qty)
|
||||
else:
|
||||
qty_diff = flt(entry.actual_qty)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe.query_builder.functions import Coalesce, Sum
|
||||
from frappe.utils import cstr, flt, now, nowdate, nowtime
|
||||
|
||||
from erpnext.controllers.stock_controller import create_repost_item_valuation_entry
|
||||
@@ -182,18 +183,67 @@ def get_indented_qty(item_code, warehouse):
|
||||
|
||||
|
||||
def get_ordered_qty(item_code, warehouse):
|
||||
ordered_qty = frappe.db.sql(
|
||||
"""
|
||||
select sum((po_item.qty - po_item.received_qty)*po_item.conversion_factor)
|
||||
from `tabPurchase Order Item` po_item, `tabPurchase Order` po
|
||||
where po_item.item_code=%s and po_item.warehouse=%s
|
||||
and po_item.qty > po_item.received_qty and po_item.parent=po.name
|
||||
and po.status not in ('Closed', 'Delivered') and po.docstatus=1
|
||||
and po_item.delivered_by_supplier = 0""",
|
||||
(item_code, warehouse),
|
||||
"""Return total pending ordered quantity for an item in a warehouse.
|
||||
Includes outstanding quantities from Purchase Orders and Subcontracting Orders"""
|
||||
|
||||
purchase_order_qty = get_purchase_order_qty(item_code, warehouse)
|
||||
subcontracting_order_qty = get_subcontracting_order_qty(item_code, warehouse)
|
||||
|
||||
return flt(purchase_order_qty) + flt(subcontracting_order_qty)
|
||||
|
||||
|
||||
def get_purchase_order_qty(item_code, warehouse):
|
||||
PurchaseOrder = frappe.qb.DocType("Purchase Order")
|
||||
PurchaseOrderItem = frappe.qb.DocType("Purchase Order Item")
|
||||
|
||||
purchase_order_qty = (
|
||||
frappe.qb.from_(PurchaseOrderItem)
|
||||
.join(PurchaseOrder)
|
||||
.on(PurchaseOrderItem.parent == PurchaseOrder.name)
|
||||
.select(
|
||||
Sum(
|
||||
(PurchaseOrderItem.qty - PurchaseOrderItem.received_qty) * PurchaseOrderItem.conversion_factor
|
||||
)
|
||||
)
|
||||
.where(
|
||||
(PurchaseOrderItem.item_code == item_code)
|
||||
& (PurchaseOrderItem.warehouse == warehouse)
|
||||
& (PurchaseOrderItem.qty > PurchaseOrderItem.received_qty)
|
||||
& (PurchaseOrder.status.notin(["Closed", "Delivered"]))
|
||||
& (PurchaseOrder.docstatus == 1)
|
||||
& (Coalesce(PurchaseOrderItem.delivered_by_supplier, 0) == 0)
|
||||
)
|
||||
.run()
|
||||
)
|
||||
|
||||
return flt(ordered_qty[0][0]) if ordered_qty else 0
|
||||
return purchase_order_qty[0][0] if purchase_order_qty else 0
|
||||
|
||||
|
||||
def get_subcontracting_order_qty(item_code, warehouse):
|
||||
SubcontractingOrder = frappe.qb.DocType("Subcontracting Order")
|
||||
SubcontractingOrderItem = frappe.qb.DocType("Subcontracting Order Item")
|
||||
|
||||
subcontracting_order_qty = (
|
||||
frappe.qb.from_(SubcontractingOrderItem)
|
||||
.join(SubcontractingOrder)
|
||||
.on(SubcontractingOrderItem.parent == SubcontractingOrder.name)
|
||||
.select(
|
||||
Sum(
|
||||
(SubcontractingOrderItem.qty - SubcontractingOrderItem.received_qty)
|
||||
* SubcontractingOrderItem.conversion_factor
|
||||
)
|
||||
)
|
||||
.where(
|
||||
(SubcontractingOrderItem.item_code == item_code)
|
||||
& (SubcontractingOrderItem.warehouse == warehouse)
|
||||
& (SubcontractingOrderItem.qty > SubcontractingOrderItem.received_qty)
|
||||
& (SubcontractingOrder.status.notin(["Closed", "Completed"]))
|
||||
& (SubcontractingOrder.docstatus == 1)
|
||||
)
|
||||
.run()
|
||||
)
|
||||
|
||||
return subcontracting_order_qty[0][0] if subcontracting_order_qty else 0
|
||||
|
||||
|
||||
def get_planned_qty(item_code, warehouse):
|
||||
|
||||
@@ -12,7 +12,7 @@ from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry impor
|
||||
StockReservation,
|
||||
has_reserved_stock,
|
||||
)
|
||||
from erpnext.stock.stock_balance import update_bin_qty
|
||||
from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty
|
||||
from erpnext.stock.utils import get_bin
|
||||
|
||||
|
||||
@@ -234,30 +234,7 @@ class SubcontractingOrder(SubcontractingController):
|
||||
):
|
||||
item_wh_list.append([item.item_code, item.warehouse])
|
||||
for item_code, warehouse in item_wh_list:
|
||||
update_bin_qty(item_code, warehouse, {"ordered_qty": self.get_ordered_qty(item_code, warehouse)})
|
||||
|
||||
@staticmethod
|
||||
def get_ordered_qty(item_code, warehouse):
|
||||
table = frappe.qb.DocType("Subcontracting Order")
|
||||
child = frappe.qb.DocType("Subcontracting Order Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.inner_join(child)
|
||||
.on(table.name == child.parent)
|
||||
.select((child.qty - child.received_qty) * child.conversion_factor)
|
||||
.where(
|
||||
(table.docstatus == 1)
|
||||
& (child.item_code == item_code)
|
||||
& (child.warehouse == warehouse)
|
||||
& (child.qty > child.received_qty)
|
||||
& (table.status != "Completed")
|
||||
)
|
||||
)
|
||||
|
||||
query = query.run()
|
||||
|
||||
return flt(query[0][0]) if query else 0
|
||||
update_bin_qty(item_code, warehouse, {"ordered_qty": get_ordered_qty(item_code, warehouse)})
|
||||
|
||||
def update_reserved_qty_for_subcontracting(self, sco_item_rows=None):
|
||||
for item in self.supplied_items:
|
||||
|
||||
@@ -616,6 +616,117 @@ class TestSubcontractingReceipt(IntegrationTestCase):
|
||||
for item in scr.supplied_items:
|
||||
self.assertFalse(item.available_qty_for_consumption)
|
||||
|
||||
def test_supplied_items_consumed_qty_for_similar_finished_goods(self):
|
||||
"""
|
||||
Test that supplied raw material consumption is calculated correctly
|
||||
when multiple subcontracted service items use the same finished good
|
||||
but different BOMs.
|
||||
"""
|
||||
|
||||
from erpnext.controllers.subcontracting_controller import (
|
||||
make_rm_stock_entry as make_subcontract_transfer_entry,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
|
||||
# Configuration: Backflush based on subcontract material transfer
|
||||
set_backflush_based_on("Material Transferred for Subcontract")
|
||||
|
||||
# Create Raw Materials
|
||||
raw_material_1 = make_item("_RM Item 1", properties={"is_stock_item": 1}).name
|
||||
|
||||
raw_material_2 = make_item("_RM Item 2", properties={"is_stock_item": 1}).name
|
||||
|
||||
# Create Subcontracted Finished Good
|
||||
finished_good = make_item("_Finished Good Item", properties={"is_stock_item": 1})
|
||||
finished_good.is_sub_contracted_item = 1
|
||||
finished_good.save()
|
||||
|
||||
# Receive Raw Materials into Warehouse
|
||||
for raw_material in (raw_material_1, raw_material_2):
|
||||
make_stock_entry(
|
||||
item_code=raw_material,
|
||||
qty=10,
|
||||
target="_Test Warehouse - _TC",
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
# Create BOMs for the same Finished Good with different RMs
|
||||
bom_rm_1 = make_bom(
|
||||
item=finished_good.name,
|
||||
quantity=1,
|
||||
raw_materials=[raw_material_1],
|
||||
).name
|
||||
|
||||
_bom_rm_2 = make_bom(
|
||||
item=finished_good.name,
|
||||
quantity=1,
|
||||
raw_materials=[raw_material_2],
|
||||
).name
|
||||
|
||||
# Define Subcontracted Service Items
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 1",
|
||||
"qty": 1,
|
||||
"rate": 100,
|
||||
"fg_item": finished_good.name,
|
||||
"fg_item_qty": 10,
|
||||
},
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 1",
|
||||
"qty": 1,
|
||||
"rate": 150,
|
||||
"fg_item": finished_good.name,
|
||||
"fg_item_qty": 10,
|
||||
},
|
||||
]
|
||||
|
||||
# Create Subcontracting Order
|
||||
subcontracting_order = get_subcontracting_order(
|
||||
service_items=service_items,
|
||||
do_not_save=True,
|
||||
)
|
||||
|
||||
# Assign BOM only to the first service item
|
||||
subcontracting_order.items[0].bom = bom_rm_1
|
||||
subcontracting_order.save()
|
||||
subcontracting_order.submit()
|
||||
|
||||
# Prepare Raw Material Transfer Items
|
||||
raw_material_transfer_items = []
|
||||
for supplied_item in subcontracting_order.supplied_items:
|
||||
raw_material_transfer_items.append(
|
||||
{
|
||||
"item_code": supplied_item.main_item_code,
|
||||
"rm_item_code": supplied_item.rm_item_code,
|
||||
"qty": supplied_item.required_qty,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"stock_uom": "Nos",
|
||||
}
|
||||
)
|
||||
|
||||
# Transfer Raw Materials to Subcontractor Warehouse
|
||||
stock_entry = frappe.get_doc(
|
||||
make_subcontract_transfer_entry(
|
||||
subcontracting_order.name,
|
||||
raw_material_transfer_items,
|
||||
)
|
||||
)
|
||||
stock_entry.to_warehouse = "_Test Warehouse 1 - _TC"
|
||||
stock_entry.save()
|
||||
stock_entry.submit()
|
||||
|
||||
# Create Subcontracting Receipt
|
||||
subcontracting_receipt = make_subcontracting_receipt(subcontracting_order.name)
|
||||
subcontracting_receipt.save()
|
||||
|
||||
# Check consumed_qty for each supplied item
|
||||
self.assertEqual(len(subcontracting_receipt.supplied_items), 2)
|
||||
self.assertEqual(subcontracting_receipt.supplied_items[0].consumed_qty, 10)
|
||||
self.assertEqual(subcontracting_receipt.supplied_items[1].consumed_qty, 10)
|
||||
|
||||
def test_supplied_items_cost_after_reposting(self):
|
||||
# Set Backflush Based On as "BOM"
|
||||
set_backflush_based_on("BOM")
|
||||
|
||||
@@ -219,7 +219,7 @@
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Payment Reconciliaition",
|
||||
"label": "Payment Reconciliation",
|
||||
"link_to": "Payment Reconciliation",
|
||||
"link_type": "DocType",
|
||||
"show_arrow": 0,
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Sales Tax Template",
|
||||
"link_to": "Item Tax Template",
|
||||
"link_to": "Sales Taxes and Charges Template",
|
||||
"link_type": "DocType",
|
||||
"navigate_to_tab": "",
|
||||
"show_arrow": 0,
|
||||
@@ -148,7 +148,7 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2026-01-10 00:06:13.005238",
|
||||
"modified": "2026-02-01 00:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Taxes",
|
||||
|
||||
Reference in New Issue
Block a user