Merge pull request #47868 from frappe/version-15-hotfix

chore: release v15
This commit is contained in:
ruthra kumar
2025-06-03 17:23:19 +05:30
committed by GitHub
40 changed files with 586 additions and 252 deletions

View File

@@ -60,6 +60,20 @@ frappe.ui.form.on("Process Statement Of Accounts", {
}, },
}; };
}); });
frm.set_query("cost_center", function () {
return {
filters: {
company: frm.doc.company,
},
};
});
frm.set_query("project", function () {
return {
filters: {
company: frm.doc.company,
},
};
});
if (frm.doc.__islocal) { if (frm.doc.__islocal) {
frm.set_value("from_date", frappe.datetime.add_months(frappe.datetime.get_today(), -1)); frm.set_value("from_date", frappe.datetime.add_months(frappe.datetime.get_today(), -1));
frm.set_value("to_date", frappe.datetime.get_today()); frm.set_value("to_date", frappe.datetime.get_today());

View File

@@ -98,9 +98,6 @@ class ReceivablePayableReport:
def get_data(self): def get_data(self):
self.get_sales_invoices_or_customers_based_on_sales_person() self.get_sales_invoices_or_customers_based_on_sales_person()
# Build delivery note map against all sales invoices
self.build_delivery_note_map()
# Get invoice details like bill_no, due_date etc for all invoices # Get invoice details like bill_no, due_date etc for all invoices
self.get_invoice_details() self.get_invoice_details()
@@ -108,7 +105,8 @@ class ReceivablePayableReport:
self.get_future_payments() self.get_future_payments()
# Get return entries # Get return entries
self.get_return_entries() if not self.filters.party_type or self.filters.party_type in ["Customer", "Supplier"]:
self.get_return_entries()
# Get Exchange Rate Revaluations # Get Exchange Rate Revaluations
self.get_exchange_rate_revaluations() self.get_exchange_rate_revaluations()
@@ -122,10 +120,14 @@ class ReceivablePayableReport:
elif self.ple_fetch_method == "UnBuffered Cursor": elif self.ple_fetch_method == "UnBuffered Cursor":
self.fetch_ple_in_unbuffered_cursor() self.fetch_ple_in_unbuffered_cursor()
# Build delivery note map against all sales invoices
self.build_delivery_note_map()
self.build_data() self.build_data()
def fetch_ple_in_buffered_cursor(self): def fetch_ple_in_buffered_cursor(self):
self.ple_entries = frappe.db.sql(self.ple_query.get_sql(), as_dict=True) query, param = self.ple_query.walk()
self.ple_entries = frappe.db.sql(query, param, as_dict=True)
for ple in self.ple_entries: for ple in self.ple_entries:
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
@@ -138,8 +140,9 @@ class ReceivablePayableReport:
def fetch_ple_in_unbuffered_cursor(self): def fetch_ple_in_unbuffered_cursor(self):
self.ple_entries = [] self.ple_entries = []
query, param = self.ple_query.walk()
with frappe.db.unbuffered_cursor(): with frappe.db.unbuffered_cursor():
for ple in frappe.db.sql(self.ple_query.get_sql(), as_dict=True, as_iterator=True): for ple in frappe.db.sql(query, param, as_dict=True, as_iterator=True):
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
self.ple_entries.append(ple) self.ple_entries.append(ple)

View File

@@ -75,7 +75,11 @@ def execute(filters=None):
# add first net income in operations section # add first net income in operations section
if net_profit_loss: if net_profit_loss:
net_profit_loss.update( net_profit_loss.update(
{"indent": 1, "parent_section": cash_flow_sections[0]["section_header"]} {
"indent": 1,
"parent_section": cash_flow_sections[0]["section_header"],
"section": net_profit_loss["account"],
}
) )
data.append(net_profit_loss) data.append(net_profit_loss)
section_data.append(net_profit_loss) section_data.append(net_profit_loss)

View File

@@ -47,12 +47,12 @@
{% for(let j=0, k=data.length; j<k; j++) { %} {% for(let j=0, k=data.length; j<k; j++) { %}
{% {%
var row = data[j]; var row = data[j];
var row_class = data[j].parent_account ? "" : "financial-statements-important"; var row_class = data[j].parent_account || data[j].parent_section ? "" : "financial-statements-important";
row_class += data[j].account_name ? "" : " financial-statements-blank-row"; row_class += data[j].account_name || data[j].section ? "" : " financial-statements-blank-row";
%} %}
<tr class="{%= row_class %}"> <tr class="{%= row_class %}">
<td> <td>
<span style="padding-left: {%= cint(data[j].indent) * 2 %}em">{%= row.account_name %}</span> <span style="padding-left: {%= cint(data[j].indent) * 2 %}em">{%= row.account_name || row.section %}</span>
</td> </td>
{% for(let i=1, l=report_columns.length; i<l; i++) { %} {% for(let i=1, l=report_columns.length; i<l; i++) { %}
<td class="text-right"> <td class="text-right">

View File

@@ -12,21 +12,25 @@
"column_break_4", "column_break_4",
"maintain_same_rate_action", "maintain_same_rate_action",
"role_to_override_stop_action", "role_to_override_stop_action",
"transaction_settings_section", "section_break_xmlt",
"po_required", "po_required",
"pr_required",
"blanket_order_allowance", "blanket_order_allowance",
"column_break_sbwq",
"pr_required",
"project_update_frequency", "project_update_frequency",
"column_break_12", "transaction_settings_section",
"maintain_same_rate", "column_break_fcyl",
"set_landed_cost_based_on_purchase_invoice_rate", "set_landed_cost_based_on_purchase_invoice_rate",
"allow_multiple_items", "allow_zero_qty_in_supplier_quotation",
"bill_for_rejected_quantity_in_purchase_invoice",
"disable_last_purchase_rate",
"show_pay_button",
"use_transaction_date_exchange_rate", "use_transaction_date_exchange_rate",
"allow_zero_qty_in_request_for_quotation", "allow_zero_qty_in_request_for_quotation",
"allow_zero_qty_in_supplier_quotation", "column_break_12",
"maintain_same_rate",
"allow_multiple_items",
"bill_for_rejected_quantity_in_purchase_invoice",
"set_valuation_rate_for_rejected_materials",
"disable_last_purchase_rate",
"show_pay_button",
"allow_zero_qty_in_purchase_order", "allow_zero_qty_in_purchase_order",
"subcontract", "subcontract",
"backflush_raw_materials_of_subcontract_based_on", "backflush_raw_materials_of_subcontract_based_on",
@@ -231,6 +235,26 @@
"fieldname": "allow_zero_qty_in_supplier_quotation", "fieldname": "allow_zero_qty_in_supplier_quotation",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Allow Supplier Quotation with Zero Quantity" "label": "Allow Supplier Quotation with Zero Quantity"
},
{
"fieldname": "section_break_xmlt",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_sbwq",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_fcyl",
"fieldtype": "Column Break"
},
{
"default": "0",
"depends_on": "bill_for_rejected_quantity_in_purchase_invoice",
"description": "If enabled, the system will generate an accounting entry for materials rejected in the Purchase Receipt.",
"fieldname": "set_valuation_rate_for_rejected_materials",
"fieldtype": "Check",
"label": "Set Valuation Rate for Rejected Materials"
} }
], ],
"grid_page_length": 50, "grid_page_length": 50,
@@ -239,7 +263,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2025-05-06 15:21:49.639642", "modified": "2025-05-16 15:56:38.321369",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Buying Settings", "name": "Buying Settings",

View File

@@ -38,6 +38,7 @@ class BuyingSettings(Document):
project_update_frequency: DF.Literal["Each Transaction", "Manual"] project_update_frequency: DF.Literal["Each Transaction", "Manual"]
role_to_override_stop_action: DF.Link | None role_to_override_stop_action: DF.Link | None
set_landed_cost_based_on_purchase_invoice_rate: DF.Check set_landed_cost_based_on_purchase_invoice_rate: DF.Check
set_valuation_rate_for_rejected_materials: DF.Check
show_pay_button: DF.Check show_pay_button: DF.Check
supp_master_name: DF.Literal["Supplier Name", "Naming Series", "Auto Name"] supp_master_name: DF.Literal["Supplier Name", "Naming Series", "Auto Name"]
supplier_group: DF.Link | None supplier_group: DF.Link | None
@@ -57,6 +58,9 @@ class BuyingSettings(Document):
hide_name_field=False, hide_name_field=False,
) )
if not self.bill_for_rejected_quantity_in_purchase_invoice:
self.set_valuation_rate_for_rejected_materials = 0
def before_save(self): def before_save(self):
self.check_maintain_same_rate() self.check_maintain_same_rate()

View File

@@ -10,7 +10,7 @@ frappe.query_reports["Purchase Order Analysis"] = {
width: "80", width: "80",
options: "Company", options: "Company",
reqd: 1, reqd: 1,
default: frappe.defaults.get_default("company"), default: frappe.defaults.get_user_default("company"),
}, },
{ {
fieldname: "from_date", fieldname: "from_date",

View File

@@ -658,6 +658,10 @@ class BuyingController(SubcontractingController):
sl_entries.append(from_warehouse_sle) sl_entries.append(from_warehouse_sle)
if flt(d.rejected_qty) != 0: if flt(d.rejected_qty) != 0:
valuation_rate_for_rejected_item = 0.0
if frappe.db.get_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials"):
valuation_rate_for_rejected_item = d.valuation_rate
sl_entries.append( sl_entries.append(
self.get_sl_entries( self.get_sl_entries(
d, d,
@@ -666,7 +670,8 @@ class BuyingController(SubcontractingController):
"actual_qty": flt( "actual_qty": flt(
flt(d.rejected_qty) * flt(d.conversion_factor), d.precision("stock_qty") flt(d.rejected_qty) * flt(d.conversion_factor), d.precision("stock_qty")
), ),
"incoming_rate": 0.0, "incoming_rate": valuation_rate_for_rejected_item if not self.is_return else 0.0,
"outgoing_rate": valuation_rate_for_rejected_item if self.is_return else 0.0,
"serial_and_batch_bundle": d.rejected_serial_and_batch_bundle, "serial_and_batch_bundle": d.rejected_serial_and_batch_bundle,
}, },
) )

View File

@@ -431,99 +431,6 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
else: else:
doc.run_method("calculate_taxes_and_totals") doc.run_method("calculate_taxes_and_totals")
def update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
returned_serial_nos = []
returned_batches = frappe._dict()
serial_and_batch_field = (
"serial_and_batch_bundle" if qty_field == "stock_qty" else "rejected_serial_and_batch_bundle"
)
old_serial_no_field = "serial_no" if qty_field == "stock_qty" else "rejected_serial_no"
old_batch_no_field = "batch_no"
if (
source_doc.get(serial_and_batch_field)
or source_doc.get(old_serial_no_field)
or source_doc.get(old_batch_no_field)
):
if item_details.has_serial_no:
returned_serial_nos = get_returned_serial_nos(
source_doc, source_parent, serial_no_field=serial_and_batch_field
)
else:
returned_batches = get_returned_batches(
source_doc, source_parent, batch_no_field=serial_and_batch_field
)
type_of_transaction = "Inward"
if source_doc.get(serial_and_batch_field) and (
frappe.db.get_value(
"Serial and Batch Bundle", source_doc.get(serial_and_batch_field), "type_of_transaction"
)
== "Inward"
):
type_of_transaction = "Outward"
elif source_parent.doctype in [
"Purchase Invoice",
"Purchase Receipt",
"Subcontracting Receipt",
]:
type_of_transaction = "Outward"
warehouse = source_doc.warehouse if qty_field == "stock_qty" else source_doc.rejected_warehouse
if source_parent.doctype in [
"Sales Invoice",
"POS Invoice",
"Delivery Note",
] and source_parent.get("is_internal_customer"):
type_of_transaction = "Outward"
warehouse = source_doc.target_warehouse
cls_obj = SerialBatchCreation(
{
"type_of_transaction": type_of_transaction,
"serial_and_batch_bundle": source_doc.get(serial_and_batch_field),
"returned_against": source_doc.name,
"item_code": source_doc.item_code,
"returned_serial_nos": returned_serial_nos,
"voucher_type": source_parent.doctype,
"do_not_submit": True,
"warehouse": warehouse,
"has_serial_no": item_details.has_serial_no,
"has_batch_no": item_details.has_batch_no,
}
)
serial_nos = []
batches = frappe._dict()
if source_doc.get(old_batch_no_field):
batches = frappe._dict({source_doc.batch_no: source_doc.get(qty_field)})
elif source_doc.get(old_serial_no_field):
serial_nos = get_serial_nos(source_doc.get(old_serial_no_field))
elif source_doc.get(serial_and_batch_field):
if item_details.has_serial_no:
serial_nos = get_serial_nos_from_bundle(source_doc.get(serial_and_batch_field))
else:
batches = get_batches_from_bundle(source_doc.get(serial_and_batch_field))
if serial_nos:
cls_obj.serial_nos = sorted(list(set(serial_nos) - set(returned_serial_nos)))
elif batches:
for batch in batches:
if batch in returned_batches:
batches[batch] -= flt(returned_batches.get(batch))
cls_obj.batches = batches
if source_doc.get(serial_and_batch_field):
cls_obj.duplicate_package()
if cls_obj.serial_and_batch_bundle:
target_doc.set(serial_and_batch_field, cls_obj.serial_and_batch_bundle)
else:
target_doc.set(serial_and_batch_field, cls_obj.make_serial_and_batch_bundle().name)
def update_item(source_doc, target_doc, source_parent): def update_item(source_doc, target_doc, source_parent):
target_doc.qty = -1 * source_doc.qty target_doc.qty = -1 * source_doc.qty
target_doc.pricing_rules = None target_doc.pricing_rules = None
@@ -866,8 +773,6 @@ def get_returned_serial_nos(child_doc, parent_doc, serial_no_field=None, ignore_
def get_returned_batches(child_doc, parent_doc, batch_no_field=None, ignore_voucher_detail_no=None): def get_returned_batches(child_doc, parent_doc, batch_no_field=None, ignore_voucher_detail_no=None):
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
batches = frappe._dict() batches = frappe._dict()
old_field = "batch_no" old_field = "batch_no"

View File

@@ -13,6 +13,7 @@ from frappe.utils.deprecations import deprecated
import erpnext import erpnext
from erpnext.accounts.doctype.journal_entry.journal_entry import get_exchange_rate from erpnext.accounts.doctype.journal_entry.journal_entry import get_exchange_rate
from erpnext.accounts.doctype.pricing_rule.utils import get_applied_pricing_rules from erpnext.accounts.doctype.pricing_rule.utils import get_applied_pricing_rules
from erpnext.accounts.utils import get_currency_precision
from erpnext.controllers.accounts_controller import ( from erpnext.controllers.accounts_controller import (
validate_conversion_rate, validate_conversion_rate,
validate_inclusive_tax, validate_inclusive_tax,
@@ -674,7 +675,16 @@ class calculate_taxes_and_totals:
tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(",", ":")) tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(",", ":"))
def set_discount_amount(self): def set_discount_amount(self):
if self.doc.additional_discount_percentage: if self.doc.discount_amount:
self.doc.additional_discount_percentage = flt(
flt(
self.doc.discount_amount / flt(self.doc.get(scrub(self.doc.apply_discount_on))),
get_currency_precision(),
)
* 100,
self.doc.precision("additional_discount_percentage"),
)
elif self.doc.additional_discount_percentage:
self.doc.discount_amount = flt( self.doc.discount_amount = flt(
flt(self.doc.get(scrub(self.doc.apply_discount_on))) flt(self.doc.get(scrub(self.doc.apply_discount_on)))
* self.doc.additional_discount_percentage * self.doc.additional_discount_percentage

View File

@@ -343,6 +343,7 @@ def get_children(doctype=None, parent=None, **kwargs):
fields = [ fields = [
"item_code as value", "item_code as value",
"item_name as title",
"is_expandable as expandable", "is_expandable as expandable",
"parent as parent_id", "parent as parent_id",
"qty", "qty",

View File

@@ -23,6 +23,7 @@ def get_columns():
"""return columns""" """return columns"""
columns = [ columns = [
_("Item") + ":Link/Item:150", _("Item") + ":Link/Item:150",
_("Item Name") + "::240",
_("Description") + "::300", _("Description") + "::300",
_("BOM Qty") + ":Float:160", _("BOM Qty") + ":Float:160",
_("BOM UoM") + "::160", _("BOM UoM") + "::160",
@@ -73,6 +74,7 @@ def get_bom_stock(filters):
.on((BOM_ITEM.item_code == BIN.item_code) & (CONDITIONS)) .on((BOM_ITEM.item_code == BIN.item_code) & (CONDITIONS))
.select( .select(
BOM_ITEM.item_code, BOM_ITEM.item_code,
BOM_ITEM.item_name,
BOM_ITEM.description, BOM_ITEM.description,
BOM_ITEM.stock_qty, BOM_ITEM.stock_qty,
BOM_ITEM.stock_uom, BOM_ITEM.stock_uom,

View File

@@ -94,6 +94,7 @@ def get_expected_data(bom, warehouse, qty_to_produce, show_exploded_view=False):
expected_data.append( expected_data.append(
[ [
item.item_code, item.item_code,
item.item_name,
item.description, item.description,
item.stock_qty, item.stock_qty,
item.stock_uom, item.stock_uom,

View File

@@ -407,3 +407,4 @@ erpnext.patches.v14_0.set_update_price_list_based_on
erpnext.patches.v15_0.set_cancelled_status_to_cancelled_pos_invoice erpnext.patches.v15_0.set_cancelled_status_to_cancelled_pos_invoice
erpnext.patches.v15_0.rename_group_by_to_categorize_by_in_custom_reports erpnext.patches.v15_0.rename_group_by_to_categorize_by_in_custom_reports
erpnext.patches.v14_0.update_full_name_in_contract erpnext.patches.v14_0.update_full_name_in_contract
erpnext.patches.v15_0.drop_sle_indexes

View File

@@ -0,0 +1,17 @@
import click
import frappe
def execute():
table = "tabStock Ledger Entry"
index_list = ["posting_datetime_creation_index", "item_warehouse"]
for index in index_list:
if not frappe.db.has_index(table, index):
continue
try:
frappe.db.sql_ddl(f"ALTER TABLE `{table}` DROP INDEX `{index}`")
click.echo(f"✓ dropped {index} index from {table}")
except Exception:
frappe.log_error("Failed to drop index")

View File

@@ -7,7 +7,7 @@ import json
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import add_to_date, flt, get_datetime, getdate, time_diff_in_hours from frappe.utils import add_to_date, flt, get_datetime, getdate, time_diff_in_hours, time_diff_in_seconds
from erpnext.controllers.queries import get_match_cond from erpnext.controllers.queries import get_match_cond
from erpnext.setup.utils import get_exchange_rate from erpnext.setup.utils import get_exchange_rate
@@ -194,7 +194,7 @@ class Timesheet(Document):
return return
_to_time = get_datetime(add_to_date(data.from_time, hours=data.hours, as_datetime=True)) _to_time = get_datetime(add_to_date(data.from_time, hours=data.hours, as_datetime=True))
if data.to_time != _to_time: if abs(time_diff_in_seconds(_to_time, data.to_time)) >= 1:
data.to_time = _to_time data.to_time = _to_time
def validate_overlap(self, data): def validate_overlap(self, data):

View File

@@ -64,7 +64,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
&& this.frm.doc.is_pos && this.frm.doc.is_pos
&& this.frm.doc.is_return && this.frm.doc.is_return
) { ) {
this.set_total_amount_to_default_mop(); await this.set_total_amount_to_default_mop();
this.calculate_paid_amount(); this.calculate_paid_amount();
} }
@@ -896,23 +896,25 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
it should set the return to that mode of payment only. it should set the return to that mode of payment only.
*/ */
let return_against_mop = await frappe.call({ if(this.frm.doc.return_against){
method: 'erpnext.controllers.sales_and_purchase_return.get_payment_data', let {message : return_against_mop } = await frappe.call({
args: { method: 'erpnext.controllers.sales_and_purchase_return.get_payment_data',
invoice: this.frm.doc.return_against args: {
} invoice: this.frm.doc.return_against
});
if (return_against_mop.message.length === 1) {
this.frm.doc.payments.forEach(payment => {
if (payment.mode_of_payment == return_against_mop.message[0].mode_of_payment) {
payment.amount = total_amount_to_pay;
} else {
payment.amount = 0;
} }
}); });
this.frm.refresh_fields();
return; if (return_against_mop.length === 1) {
this.frm.doc.payments.forEach(payment => {
if (payment.mode_of_payment == return_against_mop[0].mode_of_payment) {
payment.amount = total_amount_to_pay;
} else {
payment.amount = 0;
}
});
this.frm.refresh_fields();
return;
}
} }
this.frm.doc.payments.find(payment => { this.frm.doc.payments.find(payment => {

View File

@@ -870,8 +870,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
frappe.model.set_value(item.doctype, item.name, "stock_qty", valid_serial_nos.length); frappe.model.set_value(item.doctype, item.name, "stock_qty", valid_serial_nos.length);
} }
validate() { async validate() {
this.calculate_taxes_and_totals(false); await this.calculate_taxes_and_totals(false);
} }
update_stock() { update_stock() {

View File

@@ -45,7 +45,7 @@ erpnext.financial_statements = {
} }
} }
if (data && column.fieldname == "account") { if (data && column.fieldname == this.name_field) {
// first column // first column
value = data.section_name || data.account_name || value; value = data.section_name || data.account_name || value;

View File

@@ -174,29 +174,22 @@ class Quotation(SellingController):
) )
def get_ordered_status(self): def get_ordered_status(self):
status = "Open" ordered_items = get_ordered_items(self.name)
ordered_items = frappe._dict(
frappe.db.get_all(
"Sales Order Item",
{"prevdoc_docname": self.name, "docstatus": 1},
["item_code", "sum(qty)"],
group_by="item_code",
as_list=1,
)
)
if not ordered_items: if not ordered_items:
return status return "Open"
has_alternatives = any(row.is_alternative for row in self.get("items")) self._items = (
self._items = self.get_valid_items() if has_alternatives else self.get("items") self.get_valid_items()
if any(row.is_alternative for row in self.get("items"))
else self.get("items")
)
if any(row.qty > ordered_items.get(row.item_code, 0.0) for row in self._items): for row in self._items:
status = "Partially Ordered" if row.name not in ordered_items or row.qty > ordered_items[row.name]:
else: return "Partially Ordered"
status = "Ordered"
return status return "Ordered"
def get_valid_items(self): def get_valid_items(self):
""" """
@@ -371,15 +364,7 @@ def make_sales_order(source_name: str, target_doc=None):
def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
customer = _make_customer(source_name, ignore_permissions) customer = _make_customer(source_name, ignore_permissions)
ordered_items = frappe._dict( ordered_items = get_ordered_items(source_name)
frappe.db.get_all(
"Sales Order Item",
{"prevdoc_docname": source_name, "docstatus": 1},
["item_code", "sum(qty)"],
group_by="item_code",
as_list=1,
)
)
selected_rows = [x.get("name") for x in frappe.flags.get("args", {}).get("selected_items", [])] selected_rows = [x.get("name") for x in frappe.flags.get("args", {}).get("selected_items", [])]
@@ -417,7 +402,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
target.run_method("calculate_taxes_and_totals") target.run_method("calculate_taxes_and_totals")
def update_item(obj, target, source_parent): def update_item(obj, target, source_parent):
balance_qty = obj.qty if is_unit_price_row(obj) else obj.qty - ordered_items.get(obj.item_code, 0.0) balance_qty = obj.qty if is_unit_price_row(obj) else obj.qty - ordered_items.get(obj.name, 0.0)
target.qty = balance_qty if balance_qty > 0 else 0 target.qty = balance_qty if balance_qty > 0 else 0
target.stock_qty = flt(target.qty) * flt(obj.conversion_factor) target.stock_qty = flt(target.qty) * flt(obj.conversion_factor)
@@ -433,10 +418,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
2. If selections: Is Alternative Item/Has Alternative Item: Map if selected and adequate qty 2. If selections: Is Alternative Item/Has Alternative Item: Map if selected and adequate qty
3. If no selections: Simple row: Map if adequate qty 3. If no selections: Simple row: Map if adequate qty
""" """
balance_qty = item.qty - ordered_items.get(item.item_code, 0.0) if not ((item.qty > ordered_items.get(item.name, 0.0)) or is_unit_price_row(item)):
has_valid_qty: bool = (balance_qty > 0) or is_unit_price_row(item)
if not has_valid_qty:
return False return False
if not selected_rows: if not selected_rows:
@@ -603,3 +585,28 @@ def handle_mandatory_error(e, customer, lead_name):
message += _("Please create Customer from Lead {0}.").format(get_link_to_form("Lead", lead_name)) message += _("Please create Customer from Lead {0}.").format(get_link_to_form("Lead", lead_name))
frappe.throw(message, title=_("Mandatory Missing")) frappe.throw(message, title=_("Mandatory Missing"))
def get_ordered_items(quotation: str):
"""
Returns a dict of ordered items with their total qty based on quotation row name.
In `Sales Order Item`, `quotation_item` is the row name of `Quotation Item`.
Example:
```
{
"refsdjhd2": 10,
"ygdhdshrt": 5,
}
```
"""
return frappe._dict(
frappe.get_all(
"Sales Order Item",
filters={"prevdoc_docname": quotation, "docstatus": 1},
fields=["quotation_item", "sum(qty)"],
group_by="quotation_item",
as_list=1,
)
)

View File

@@ -815,6 +815,52 @@ class TestQuotation(FrappeTestCase):
quotation.reload() quotation.reload()
self.assertEqual(quotation.status, "Ordered") self.assertEqual(quotation.status, "Ordered")
def test_duplicate_items_in_quotation(self):
from erpnext.selling.doctype.quotation.quotation import make_sales_order
from erpnext.stock.doctype.item.test_item import make_item
# item code same but description different
make_item("_Test Item 2", {"is_stock_item": 1})
quotation = make_quotation(qty=1, rate=100, do_not_submit=1)
# duplicate items
for qty in [1, 1, 2, 3]:
quotation.append("items", {"item_code": "_Test Item", "qty": qty, "rate": 100})
quotation.append("items", {"item_code": "_Test Item 2", "qty": 5, "rate": 100})
quotation.submit()
sales_order = make_sales_order(quotation.name)
sales_order.delivery_date = nowdate()
self.assertEqual(len(sales_order.items), 6)
self.assertEqual(sales_order.items[0].qty, 1)
self.assertEqual(sales_order.items[-1].qty, 5)
# Row 1: 10, Row 4: 1, Row 5: 1
sales_order.items[0].qty = 10
sales_order.items[3].qty = 1
sales_order.items[4].qty = 1
sales_order.submit()
quotation.reload()
self.assertEqual(quotation.status, "Partially Ordered")
sales_order_2 = make_sales_order(quotation.name)
sales_order_2.delivery_date = nowdate()
self.assertEqual(len(sales_order_2.items), 2)
self.assertEqual(sales_order_2.items[0].qty, 1)
self.assertEqual(sales_order_2.items[1].qty, 2)
self.assertEqual(sales_order_2.items[0].quotation_item, quotation.items[3].name)
self.assertEqual(sales_order_2.items[1].quotation_item, quotation.items[4].name)
sales_order_2.submit()
quotation.reload()
self.assertEqual(quotation.status, "Ordered")
test_records = frappe.get_test_records("Quotation") test_records = frappe.get_test_records("Quotation")

View File

@@ -17,6 +17,7 @@ def get_data():
"Quotation": ["items", "prevdoc_docname"], "Quotation": ["items", "prevdoc_docname"],
"BOM": ["items", "bom_no"], "BOM": ["items", "bom_no"],
"Blanket Order": ["items", "blanket_order"], "Blanket Order": ["items", "blanket_order"],
"Purchase Order": ["items", "purchase_order"],
}, },
"transactions": [ "transactions": [
{ {

View File

@@ -321,25 +321,17 @@ erpnext.PointOfSale.PastOrderSummary = class {
get_condition_btn_map(after_submission) { get_condition_btn_map(after_submission) {
if (after_submission) if (after_submission)
return [ return [{ condition: true, visible_btns: ["Print Receipt", "Email Receipt", "New Order"] }];
{
condition: true,
visible_btns: [__("Print Receipt"), __("Email Receipt"), __("New Order")],
},
];
return [ return [
{ { condition: this.doc.docstatus === 0, visible_btns: ["Edit Order", "Delete Order"] },
condition: this.doc.docstatus === 0,
visible_btns: [__("Edit Order"), __("Delete Order")],
},
{ {
condition: !this.doc.is_return && this.doc.docstatus === 1, condition: !this.doc.is_return && this.doc.docstatus === 1,
visible_btns: [__("Print Receipt"), __("Email Receipt"), __("Return")], visible_btns: ["Print Receipt", "Email Receipt", "Return"],
}, },
{ {
condition: this.doc.is_return && this.doc.docstatus === 1, condition: this.doc.is_return && this.doc.docstatus === 1,
visible_btns: [__("Print Receipt"), __("Email Receipt")], visible_btns: ["Print Receipt", "Email Receipt"],
}, },
]; ];
} }

View File

@@ -496,6 +496,12 @@ class PurchaseReceipt(BuyingController):
outgoing_amount = abs(get_stock_value_difference(self.name, item.name, item.from_warehouse)) outgoing_amount = abs(get_stock_value_difference(self.name, item.name, item.from_warehouse))
credit_amount = outgoing_amount credit_amount = outgoing_amount
if item.get("rejected_qty") and frappe.db.get_single_value(
"Buying Settings", "set_valuation_rate_for_rejected_materials"
):
outgoing_amount += get_stock_value_difference(self.name, item.name, item.rejected_warehouse)
credit_amount = outgoing_amount
if credit_amount: if credit_amount:
if not account: if not account:
validate_account("Stock or Asset Received But Not Billed") validate_account("Stock or Asset Received But Not Billed")
@@ -629,6 +635,12 @@ class PurchaseReceipt(BuyingController):
valuation_amount_as_per_doc - flt(stock_value_diff), item.precision("base_net_amount") valuation_amount_as_per_doc - flt(stock_value_diff), item.precision("base_net_amount")
) )
if item.get("rejected_qty") and frappe.db.get_single_value(
"Buying Settings", "set_valuation_rate_for_rejected_materials"
):
rejected_item_cost = get_stock_value_difference(self.name, item.name, item.rejected_warehouse)
divisional_loss -= rejected_item_cost
if divisional_loss: if divisional_loss:
loss_account = ( loss_account = (
self.get_company_default("default_expense_account", ignore_validation=True) self.get_company_default("default_expense_account", ignore_validation=True)
@@ -726,13 +738,23 @@ class PurchaseReceipt(BuyingController):
make_sub_contracting_gl_entries(d) make_sub_contracting_gl_entries(d)
make_divisional_loss_gl_entry(d, outgoing_amount) make_divisional_loss_gl_entry(d, outgoing_amount)
elif (d.warehouse and d.warehouse not in warehouse_with_no_account) or ( elif (d.warehouse and d.warehouse not in warehouse_with_no_account) or (
d.rejected_warehouse and d.rejected_warehouse not in warehouse_with_no_account not frappe.db.get_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials")
and d.rejected_warehouse
and d.rejected_warehouse not in warehouse_with_no_account
): ):
warehouse_with_no_account.append(d.warehouse or d.rejected_warehouse) warehouse_with_no_account.append(d.warehouse or d.rejected_warehouse)
if d.is_fixed_asset and d.landed_cost_voucher_amount: if d.is_fixed_asset and d.landed_cost_voucher_amount:
self.update_assets(d, d.valuation_rate) self.update_assets(d, d.valuation_rate)
if d.rejected_qty and frappe.db.get_single_value(
"Buying Settings", "set_valuation_rate_for_rejected_materials"
):
stock_value_diff = get_stock_value_difference(self.name, d.name, d.rejected_warehouse)
stock_asset_account_name = warehouse_account[d.rejected_warehouse]["account"]
make_item_asset_inward_gl_entry(d, stock_value_diff, stock_asset_account_name)
if warehouse_with_no_account: if warehouse_with_no_account:
frappe.msgprint( frappe.msgprint(
_("No accounting entries for the following warehouses") _("No accounting entries for the following warehouses")

View File

@@ -4199,6 +4199,126 @@ class TestPurchaseReceipt(FrappeTestCase):
# Test 3 - OverAllowanceError should be thrown as qty is greater than qty in DN # Test 3 - OverAllowanceError should be thrown as qty is greater than qty in DN
self.assertRaises(erpnext.controllers.status_updater.OverAllowanceError, pr.submit) self.assertRaises(erpnext.controllers.status_updater.OverAllowanceError, pr.submit)
def test_valuation_rate_for_rejected_materials(self):
item = make_item("Test Item with Rej Material Valuation", {"is_stock_item": 1})
company = "_Test Company with perpetual inventory"
warehouse = create_warehouse(
"_Test In-ward Warehouse",
company="_Test Company with perpetual inventory",
)
rej_warehouse = create_warehouse(
"_Test Warehouse - Rejected Material",
company="_Test Company with perpetual inventory",
)
frappe.db.set_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", 1)
frappe.db.set_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials", 1)
pr = make_purchase_receipt(
item_code=item.name,
qty=10,
rate=100,
company=company,
warehouse=warehouse,
rejected_qty=5,
rejected_warehouse=rej_warehouse,
)
stock_received_but_not_billed_account = frappe.get_value(
"Company",
company,
"stock_received_but_not_billed",
)
rejected_item_cost = frappe.db.get_value(
"Stock Ledger Entry",
{
"voucher_type": "Purchase Receipt",
"voucher_no": pr.name,
"warehouse": rej_warehouse,
},
"stock_value_difference",
)
self.assertEqual(rejected_item_cost, 500)
srbnb_cost = frappe.db.get_value(
"GL Entry",
{
"voucher_type": "Purchase Receipt",
"voucher_no": pr.name,
"account": stock_received_but_not_billed_account,
},
"credit",
)
self.assertEqual(srbnb_cost, 1500)
frappe.db.set_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", 0)
frappe.db.set_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials", 0)
def test_no_valuation_rate_for_rejected_materials(self):
item = make_item("Test Item with Rej Material No Valuation", {"is_stock_item": 1})
company = "_Test Company with perpetual inventory"
warehouse = create_warehouse(
"_Test In-ward Warehouse",
company="_Test Company with perpetual inventory",
)
rej_warehouse = create_warehouse(
"_Test Warehouse - Rejected Material",
company="_Test Company with perpetual inventory",
)
frappe.db.set_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", 0)
frappe.db.set_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials", 0)
pr = make_purchase_receipt(
item_code=item.name,
qty=10,
rate=100,
company=company,
warehouse=warehouse,
rejected_qty=5,
rejected_warehouse=rej_warehouse,
)
stock_received_but_not_billed_account = frappe.get_value(
"Company",
company,
"stock_received_but_not_billed",
)
rejected_item_cost = frappe.db.get_value(
"Stock Ledger Entry",
{
"voucher_type": "Purchase Receipt",
"voucher_no": pr.name,
"warehouse": rej_warehouse,
},
"stock_value_difference",
)
self.assertEqual(rejected_item_cost, 0.0)
srbnb_cost = frappe.db.get_value(
"GL Entry",
{
"voucher_type": "Purchase Receipt",
"voucher_no": pr.name,
"account": stock_received_but_not_billed_account,
},
"credit",
)
self.assertEqual(srbnb_cost, 1000)
def prepare_data_for_internal_transfer(): def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

@@ -722,19 +722,19 @@ class SerialandBatchBundle(Document):
def reset_qty(self, row, qty_field=None): def reset_qty(self, row, qty_field=None):
qty_field = self.get_qty_field(row, qty_field=qty_field) qty_field = self.get_qty_field(row, qty_field=qty_field)
qty = abs(row.get(qty_field)) qty = abs(flt(row.get(qty_field), self.precision("total_qty")))
idx = None idx = None
while qty > 0: while qty > 0:
for d in self.entries: for d in self.entries:
row_qty = abs(d.qty) row_qty = abs(flt(d.qty, d.precision("qty")))
if row_qty >= qty: if row_qty >= qty:
d.db_set("qty", qty if self.type_of_transaction == "Inward" else qty * -1) d.db_set("qty", qty if self.type_of_transaction == "Inward" else qty * -1)
qty = 0 qty = 0
idx = d.idx idx = d.idx
break break
else: else:
qty -= row_qty qty = flt(qty - row_qty, d.precision("qty"))
idx = d.idx idx = d.idx
if idx and len(self.entries) > idx: if idx and len(self.entries) > idx:

View File

@@ -57,7 +57,6 @@
"options": "Item", "options": "Item",
"print_width": "100px", "print_width": "100px",
"read_only": 1, "read_only": 1,
"search_index": 1,
"width": "100px" "width": "100px"
}, },
{ {
@@ -88,7 +87,6 @@
"options": "Warehouse", "options": "Warehouse",
"print_width": "100px", "print_width": "100px",
"read_only": 1, "read_only": 1,
"search_index": 1,
"width": "100px" "width": "100px"
}, },
{ {
@@ -101,7 +99,6 @@
"oldfieldtype": "Date", "oldfieldtype": "Date",
"print_width": "100px", "print_width": "100px",
"read_only": 1, "read_only": 1,
"search_index": 1,
"width": "100px" "width": "100px"
}, },
{ {
@@ -357,13 +354,14 @@
"search_index": 1 "search_index": 1
} }
], ],
"grid_page_length": 50,
"hide_toolbar": 1, "hide_toolbar": 1,
"icon": "fa fa-list", "icon": "fa fa-list",
"idx": 1, "idx": 1,
"in_create": 1, "in_create": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2024-12-23 18:03:05.171023", "modified": "2025-04-22 12:37:41.304109",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Ledger Entry", "name": "Stock Ledger Entry",

View File

@@ -345,5 +345,4 @@ class StockLedgerEntry(Document):
def on_doctype_update(): def on_doctype_update():
frappe.db.add_index("Stock Ledger Entry", ["voucher_no", "voucher_type"]) frappe.db.add_index("Stock Ledger Entry", ["voucher_no", "voucher_type"])
frappe.db.add_index("Stock Ledger Entry", ["batch_no", "item_code", "warehouse"]) frappe.db.add_index("Stock Ledger Entry", ["batch_no", "item_code", "warehouse"])
frappe.db.add_index("Stock Ledger Entry", ["warehouse", "item_code"], "item_warehouse") frappe.db.add_index("Stock Ledger Entry", ["item_code", "warehouse", "posting_datetime", "creation"])
frappe.db.add_index("Stock Ledger Entry", ["posting_datetime", "creation"])

View File

@@ -6,7 +6,7 @@ from collections import defaultdict
import frappe import frappe
from frappe import _ from frappe import _
from frappe.query_builder.functions import Sum from frappe.query_builder.functions import Sum
from frappe.utils import flt, today from frappe.utils import flt, get_datetime, today
def execute(filters=None): def execute(filters=None):
@@ -167,7 +167,8 @@ def get_query_based_on_filters(query, batch, table, filters):
query = query.where(batch.batch_qty > 0) query = query.where(batch.batch_qty > 0)
else: else:
query = query.where(table.posting_date <= filters.to_date) to_date = get_datetime(str(filters.to_date) + " 23:59:59")
query = query.where(table.posting_datetime <= to_date)
if filters.warehouse: if filters.warehouse:
lft, rgt = frappe.db.get_value("Warehouse", filters.warehouse, ["lft", "rgt"]) lft, rgt = frappe.db.get_value("Warehouse", filters.warehouse, ["lft", "rgt"])

View File

@@ -115,7 +115,6 @@ def get_stock_ledger_entries_for_batch_no(filters):
& (sle.posting_datetime < posting_datetime) & (sle.posting_datetime < posting_datetime)
) )
.groupby(sle.voucher_no, sle.batch_no, sle.item_code, sle.warehouse) .groupby(sle.voucher_no, sle.batch_no, sle.item_code, sle.warehouse)
.orderby(sle.item_code, sle.warehouse)
) )
query = apply_warehouse_filter(query, sle, filters) query = apply_warehouse_filter(query, sle, filters)
@@ -160,7 +159,6 @@ def get_stock_ledger_entries_for_batch_bundle(filters):
& (sle.posting_datetime <= to_date) & (sle.posting_datetime <= to_date)
) )
.groupby(sle.voucher_no, batch_package.batch_no, batch_package.warehouse) .groupby(sle.voucher_no, batch_package.batch_no, batch_package.warehouse)
.orderby(sle.item_code, sle.warehouse)
) )
query = apply_warehouse_filter(query, sle, filters) query = apply_warehouse_filter(query, sle, filters)

View File

@@ -66,7 +66,7 @@ def get_stock_ledger_entries(filters):
"Stock Ledger Entry", "Stock Ledger Entry",
fields=SLE_FIELDS, fields=SLE_FIELDS,
filters=sle_filters, filters=sle_filters,
order_by="timestamp(posting_date, posting_time), creation", order_by="posting_datetime, creation",
) )

View File

@@ -4,7 +4,7 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.query_builder.functions import IfNull from frappe.query_builder.functions import IfNull, Max
from frappe.utils import flt from frappe.utils import flt
from pypika.terms import ExistsCriterion from pypika.terms import ExistsCriterion
@@ -208,30 +208,21 @@ def get_stock_ledger_entries(filters, items):
if not items: if not items:
return [] return []
sle = frappe.qb.DocType("Stock Ledger Entry") max_posting_datetime_query = get_item_wise_max_posting_datetime(filters, items)
sle2 = frappe.qb.DocType("Stock Ledger Entry")
sle = frappe.qb.DocType("Stock Ledger Entry")
query = ( query = (
frappe.qb.from_(sle) frappe.qb.from_(sle)
.left_join(sle2) .join(max_posting_datetime_query)
.on( .on(
(sle.item_code == sle2.item_code) (sle.item_code == max_posting_datetime_query.item_code)
& (sle.warehouse == sle2.warehouse) & (sle.warehouse == max_posting_datetime_query.warehouse)
& (sle.posting_datetime < sle2.posting_datetime) & (sle.posting_datetime == max_posting_datetime_query.posting_datetime)
& (sle.name < sle2.name)
) )
.select(sle.item_code, sle.warehouse, sle.qty_after_transaction, sle.company) .select(sle.item_code, sle.warehouse, sle.qty_after_transaction, sle.company)
.where((sle2.name.isnull()) & (sle.docstatus < 2) & (sle.item_code.isin(items))) .where(sle.is_cancelled == 0)
) )
if filters.get("company"):
query = query.where(sle.company == filters.get("company"))
if date := filters.get("date"):
query = query.where(sle.posting_date <= date)
else:
frappe.throw(_("'Date' is required"))
if filters.get("warehouse"): if filters.get("warehouse"):
warehouse_details = frappe.db.get_value( warehouse_details = frappe.db.get_value(
"Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
@@ -247,4 +238,44 @@ def get_stock_ledger_entries(filters, items):
) )
) )
if filters.get("company"):
query = query.where(sle.company == filters.get("company"))
if filters.get("data"):
query = query.where(sle.posting_date <= filters.get("data"))
return query.run(as_dict=True) return query.run(as_dict=True)
def get_item_wise_max_posting_datetime(filters, items):
"""Get the maximum Stock Ledger Entry name for the given filters and items."""
sle = frappe.qb.DocType("Stock Ledger Entry")
query = (
frappe.qb.from_(sle)
.select(sle.item_code, sle.warehouse, sle.name, Max(sle.posting_datetime).as_("posting_datetime"))
.where(sle.item_code.isin(items) & (sle.is_cancelled == 0))
.groupby(sle.item_code, sle.warehouse)
)
if filters.get("warehouse"):
warehouse_details = frappe.db.get_value(
"Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
)
if warehouse_details:
wh = frappe.qb.DocType("Warehouse")
query = query.where(
sle.warehouse.isin(
frappe.qb.from_(wh)
.select(wh.name)
.where((wh.lft >= warehouse_details.lft) & (wh.rgt <= warehouse_details.rgt))
)
)
if filters.get("company"):
query = query.where(sle.company == filters.get("company"))
if filters.get("data"):
query = query.where(sle.posting_date <= filters.get("data"))
return query

View File

@@ -5,8 +5,8 @@ import datetime
import frappe import frappe
from frappe import _, scrub from frappe import _, scrub
from frappe.query_builder.functions import CombineDatetime from frappe.query_builder.functions import CombineDatetime
from frappe.utils import get_datetime, get_first_day_of_week, get_quarter_start, getdate
from frappe.utils import get_first_day as get_first_day_of_month from frappe.utils import get_first_day as get_first_day_of_month
from frappe.utils import get_first_day_of_week, get_quarter_start, getdate
from frappe.utils.nestedset import get_descendants_of from frappe.utils.nestedset import get_descendants_of
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
@@ -294,9 +294,8 @@ def get_stock_ledger_entries(filters, items):
sle.batch_no, sle.batch_no,
) )
.where((sle.docstatus < 2) & (sle.is_cancelled == 0)) .where((sle.docstatus < 2) & (sle.is_cancelled == 0))
.orderby(CombineDatetime(sle.posting_date, sle.posting_time)) .orderby(sle.posting_datetime)
.orderby(sle.creation) .orderby(sle.creation)
.orderby(sle.actual_qty)
) )
if items: if items:
@@ -314,7 +313,8 @@ def apply_conditions(query, filters):
frappe.throw(_("'From Date' is required")) frappe.throw(_("'From Date' is required"))
if to_date := filters.get("to_date"): if to_date := filters.get("to_date"):
query = query.where(sle.posting_date <= to_date) to_date = get_datetime(str(to_date) + " 23:59:59")
query = query.where(sle.posting_datetime <= to_date)
else: else:
frappe.throw(_("'To Date' is required")) frappe.throw(_("'To Date' is required"))

View File

@@ -44,7 +44,7 @@ def get_stock_ledger_entries(filters):
"Stock Ledger Entry", "Stock Ledger Entry",
fields=SLE_FIELDS, fields=SLE_FIELDS,
filters={"item_code": filters.item_code, "warehouse": filters.warehouse, "is_cancelled": 0}, filters={"item_code": filters.item_code, "warehouse": filters.warehouse, "is_cancelled": 0},
order_by="timestamp(posting_date, posting_time), creation", order_by="posting_datetime, creation",
) )

View File

@@ -1818,7 +1818,7 @@ def get_valuation_rate(
# Get valuation rate from last sle for the same item and warehouse # Get valuation rate from last sle for the same item and warehouse
if last_valuation_rate := frappe.db.sql( # nosemgrep if last_valuation_rate := frappe.db.sql( # nosemgrep
"""select valuation_rate """select valuation_rate
from `tabStock Ledger Entry` force index (item_warehouse) from `tabStock Ledger Entry`
where where
item_code = %s item_code = %s
AND warehouse = %s AND warehouse = %s
@@ -1962,7 +1962,6 @@ def get_next_stock_reco(kwargs):
sle.actual_qty, sle.actual_qty,
sle.has_batch_no, sle.has_batch_no,
) )
.force_index("item_warehouse")
.where( .where(
(sle.item_code == kwargs.get("item_code")) (sle.item_code == kwargs.get("item_code"))
& (sle.warehouse == kwargs.get("warehouse")) & (sle.warehouse == kwargs.get("warehouse"))

View File

@@ -10,6 +10,10 @@ import erpnext
from erpnext.accounts.utils import get_account_currency from erpnext.accounts.utils import get_account_currency
from erpnext.buying.utils import check_on_hold_or_closed_status from erpnext.buying.utils import check_on_hold_or_closed_status
from erpnext.controllers.subcontracting_controller import SubcontractingController from erpnext.controllers.subcontracting_controller import SubcontractingController
from erpnext.setup.doctype.brand.brand import get_brand_defaults
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.stock.doctype.item.item import get_item_defaults
from erpnext.stock.get_item_details import get_default_cost_center, get_default_expense_account
from erpnext.stock.stock_ledger import get_valuation_rate from erpnext.stock.stock_ledger import get_valuation_rate
@@ -140,6 +144,9 @@ class SubcontractingReceipt(SubcontractingController):
self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse") self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
self.get_current_stock() self.get_current_stock()
self.set_supplied_items_expense_account()
self.set_supplied_items_cost_center()
def on_submit(self): def on_submit(self):
self.validate_closed_subcontracting_order() self.validate_closed_subcontracting_order()
self.validate_available_qty_for_consumption() self.validate_available_qty_for_consumption()
@@ -224,6 +231,17 @@ class SubcontractingReceipt(SubcontractingController):
if not item.cost_center: if not item.cost_center:
item.cost_center = cost_center item.cost_center = cost_center
def set_supplied_items_cost_center(self):
for item in self.supplied_items:
if not item.cost_center:
item.cost_center = get_default_cost_center(
{"project": self.project},
get_item_defaults(item.rm_item_code, self.company),
get_item_group_defaults(item.rm_item_code, self.company),
get_brand_defaults(item.rm_item_code, self.company),
self.company,
)
def set_items_expense_account(self): def set_items_expense_account(self):
if self.company: if self.company:
expense_account = self.get_company_default("default_expense_account", ignore_validation=True) expense_account = self.get_company_default("default_expense_account", ignore_validation=True)
@@ -232,6 +250,22 @@ class SubcontractingReceipt(SubcontractingController):
if not item.expense_account: if not item.expense_account:
item.expense_account = expense_account item.expense_account = expense_account
def set_supplied_items_expense_account(self):
for item in self.supplied_items:
if not item.expense_account:
item.expense_account = get_default_expense_account(
frappe._dict(
{
"expense_account": self.get_company_default(
"default_expense_account", ignore_validation=True
)
}
),
get_item_defaults(item.rm_item_code, self.company),
get_item_group_defaults(item.rm_item_code, self.company),
get_brand_defaults(item.rm_item_code, self.company),
)
def reset_supplied_items(self): def reset_supplied_items(self):
if ( if (
frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on") frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on")
@@ -519,6 +553,18 @@ class SubcontractingReceipt(SubcontractingController):
def make_item_gl_entries(self, gl_entries, warehouse_account=None): def make_item_gl_entries(self, gl_entries, warehouse_account=None):
warehouse_with_no_account = [] warehouse_with_no_account = []
supplied_items_details = frappe._dict()
for item in self.supplied_items:
supplied_items_details.setdefault(item.reference_name, []).append(
frappe._dict(
{
"amount": item.amount,
"expense_account": item.expense_account,
"cost_center": item.cost_center,
}
)
)
for item in self.items: for item in self.items:
if flt(item.rate) and flt(item.qty): if flt(item.rate) and flt(item.qty):
if warehouse_account.get(item.warehouse): if warehouse_account.get(item.warehouse):
@@ -568,32 +614,33 @@ class SubcontractingReceipt(SubcontractingController):
) )
if flt(item.rm_supp_cost) and supplier_warehouse_account: if flt(item.rm_supp_cost) and supplier_warehouse_account:
# Supplier Warehouse Account (Credit) for rm_item in supplied_items_details.get(item.name):
self.add_gl_entry( # Supplier Warehouse Account (Credit)
gl_entries=gl_entries, self.add_gl_entry(
account=supplier_warehouse_account, gl_entries=gl_entries,
cost_center=item.cost_center, account=supplier_warehouse_account,
debit=0.0, cost_center=rm_item.cost_center,
credit=flt(item.rm_supp_cost), debit=0.0,
remarks=remarks, credit=flt(rm_item.amount),
against_account=item.expense_account, remarks=remarks,
account_currency=get_account_currency(supplier_warehouse_account), against_account=rm_item.expense_account,
project=item.project, account_currency=get_account_currency(supplier_warehouse_account),
item=item, project=item.project,
) item=item,
# Expense Account (Debit) )
self.add_gl_entry( # Expense Account (Debit)
gl_entries=gl_entries, self.add_gl_entry(
account=item.expense_account, gl_entries=gl_entries,
cost_center=item.cost_center, account=rm_item.expense_account,
debit=flt(item.rm_supp_cost), cost_center=rm_item.cost_center,
credit=0.0, debit=flt(rm_item.amount),
remarks=remarks, credit=0.0,
against_account=supplier_warehouse_account, remarks=remarks,
account_currency=get_account_currency(item.expense_account), against_account=supplier_warehouse_account,
project=item.project, account_currency=get_account_currency(item.expense_account),
item=item, project=item.project,
) item=item,
)
# Expense Account (Debit) # Expense Account (Debit)
if item.additional_cost_per_qty: if item.additional_cost_per_qty:

View File

@@ -372,6 +372,56 @@ class TestSubcontractingReceipt(FrappeTestCase):
self.assertTrue(get_gl_entries("Subcontracting Receipt", scr.name)) self.assertTrue(get_gl_entries("Subcontracting Receipt", scr.name))
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1) frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
@change_settings("Stock Settings", {"use_serial_batch_fields": 0})
def test_subcontracting_receipt_gl_entry_with_different_rm_expense_accounts(self):
service_items = [
{
"warehouse": "Stores - TCP1",
"item_code": "Subcontracted Service Item 7",
"qty": 10,
"rate": 100,
"fg_item": "Subcontracted Item SA4",
"fg_item_qty": 10,
},
]
sco = get_subcontracting_order(
company="_Test Company with perpetual inventory",
warehouse="Stores - TCP1",
supplier_warehouse="Work In Progress - TCP1",
service_items=service_items,
)
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),
)
scr = make_subcontracting_receipt(sco.name)
scr.save()
scr.supplied_items[1].expense_account = "_Test Write Off - TCP1"
scr.save()
scr.submit()
for item in scr.supplied_items:
self.assertTrue(item.expense_account)
gl_entries = get_gl_entries("Subcontracting Receipt", scr.name)
self.assertTrue(gl_entries)
fg_warehouse_ac = get_inventory_account(scr.company, scr.items[0].warehouse)
expense_account = scr.items[0].expense_account
expected_values = {
fg_warehouse_ac: [4000, 3000],
expense_account: [2000, 4000],
"_Test Write Off - TCP1": [1000, 0],
}
for gle in gl_entries:
self.assertEqual(expected_values[gle.account][0], gle.debit)
self.assertEqual(expected_values[gle.account][1], gle.credit)
@change_settings("Stock Settings", {"use_serial_batch_fields": 0}) @change_settings("Stock Settings", {"use_serial_batch_fields": 0})
def test_subcontracting_receipt_with_zero_service_cost(self): def test_subcontracting_receipt_with_zero_service_cost(self):
warehouse = "Stores - TCP1" warehouse = "Stores - TCP1"

View File

@@ -33,7 +33,11 @@
"section_break_zwnh", "section_break_zwnh",
"serial_no", "serial_no",
"column_break_qibi", "column_break_qibi",
"batch_no" "batch_no",
"accounting_details_section",
"expense_account",
"accounting_dimensions_section",
"cost_center"
], ],
"fields": [ "fields": [
{ {
@@ -103,7 +107,7 @@
{ {
"fieldname": "stock_uom", "fieldname": "stock_uom",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Stock Uom", "label": "Stock UOM",
"options": "UOM", "options": "UOM",
"read_only": 1 "read_only": 1
}, },
@@ -231,18 +235,43 @@
"fieldname": "add_serial_batch_bundle", "fieldname": "add_serial_batch_bundle",
"fieldtype": "Button", "fieldtype": "Button",
"label": "Add Serial / Batch Bundle" "label": "Add Serial / Batch Bundle"
},
{
"fieldname": "accounting_details_section",
"fieldtype": "Section Break",
"label": "Accounting Details"
},
{
"fieldname": "expense_account",
"fieldtype": "Link",
"label": "Expense Account",
"options": "Account"
},
{
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
},
{
"depends_on": "eval:cint(erpnext.is_perpetual_inventory_enabled(parent.company))",
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center",
"print_hide": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-03-30 10:26:27.237371", "modified": "2025-05-27 12:33:58.772638",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Subcontracting", "module": "Subcontracting",
"name": "Subcontracting Receipt Supplied Item", "name": "Subcontracting Receipt Supplied Item",
"naming_rule": "Autoincrement", "naming_rule": "Autoincrement",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"row_format": "Dynamic",
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],

View File

@@ -20,8 +20,10 @@ class SubcontractingReceiptSuppliedItem(Document):
bom_detail_no: DF.Data | None bom_detail_no: DF.Data | None
consumed_qty: DF.Float consumed_qty: DF.Float
conversion_factor: DF.Float conversion_factor: DF.Float
cost_center: DF.Link | None
current_stock: DF.Float current_stock: DF.Float
description: DF.TextEditor | None description: DF.TextEditor | None
expense_account: DF.Link | None
item_name: DF.Data | None item_name: DF.Data | None
main_item_code: DF.Link | None main_item_code: DF.Link | None
parent: DF.Data parent: DF.Data

View File

@@ -5,7 +5,6 @@ INDEXED_FIELDS = {
"Bin": ["item_code"], "Bin": ["item_code"],
"GL Entry": ["voucher_no", "posting_date", "company", "party"], "GL Entry": ["voucher_no", "posting_date", "company", "party"],
"Purchase Order Item": ["item_code"], "Purchase Order Item": ["item_code"],
"Stock Ledger Entry": ["warehouse"],
} }