mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-01 12:38:27 +00:00
Merge pull request #47868 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -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) {
|
||||
frm.set_value("from_date", frappe.datetime.add_months(frappe.datetime.get_today(), -1));
|
||||
frm.set_value("to_date", frappe.datetime.get_today());
|
||||
|
||||
@@ -98,9 +98,6 @@ class ReceivablePayableReport:
|
||||
def get_data(self):
|
||||
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
|
||||
self.get_invoice_details()
|
||||
|
||||
@@ -108,7 +105,8 @@ class ReceivablePayableReport:
|
||||
self.get_future_payments()
|
||||
|
||||
# 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
|
||||
self.get_exchange_rate_revaluations()
|
||||
@@ -122,10 +120,14 @@ class ReceivablePayableReport:
|
||||
elif self.ple_fetch_method == "UnBuffered Cursor":
|
||||
self.fetch_ple_in_unbuffered_cursor()
|
||||
|
||||
# Build delivery note map against all sales invoices
|
||||
self.build_delivery_note_map()
|
||||
|
||||
self.build_data()
|
||||
|
||||
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:
|
||||
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
|
||||
@@ -138,8 +140,9 @@ class ReceivablePayableReport:
|
||||
|
||||
def fetch_ple_in_unbuffered_cursor(self):
|
||||
self.ple_entries = []
|
||||
query, param = self.ple_query.walk()
|
||||
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.ple_entries.append(ple)
|
||||
|
||||
|
||||
@@ -75,7 +75,11 @@ def execute(filters=None):
|
||||
# add first net income in operations section
|
||||
if net_profit_loss:
|
||||
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)
|
||||
section_data.append(net_profit_loss)
|
||||
|
||||
@@ -47,12 +47,12 @@
|
||||
{% for(let j=0, k=data.length; j<k; j++) { %}
|
||||
{%
|
||||
var row = data[j];
|
||||
var row_class = data[j].parent_account ? "" : "financial-statements-important";
|
||||
row_class += data[j].account_name ? "" : " financial-statements-blank-row";
|
||||
var row_class = data[j].parent_account || data[j].parent_section ? "" : "financial-statements-important";
|
||||
row_class += data[j].account_name || data[j].section ? "" : " financial-statements-blank-row";
|
||||
%}
|
||||
<tr class="{%= row_class %}">
|
||||
<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>
|
||||
{% for(let i=1, l=report_columns.length; i<l; i++) { %}
|
||||
<td class="text-right">
|
||||
|
||||
@@ -12,21 +12,25 @@
|
||||
"column_break_4",
|
||||
"maintain_same_rate_action",
|
||||
"role_to_override_stop_action",
|
||||
"transaction_settings_section",
|
||||
"section_break_xmlt",
|
||||
"po_required",
|
||||
"pr_required",
|
||||
"blanket_order_allowance",
|
||||
"column_break_sbwq",
|
||||
"pr_required",
|
||||
"project_update_frequency",
|
||||
"column_break_12",
|
||||
"maintain_same_rate",
|
||||
"transaction_settings_section",
|
||||
"column_break_fcyl",
|
||||
"set_landed_cost_based_on_purchase_invoice_rate",
|
||||
"allow_multiple_items",
|
||||
"bill_for_rejected_quantity_in_purchase_invoice",
|
||||
"disable_last_purchase_rate",
|
||||
"show_pay_button",
|
||||
"allow_zero_qty_in_supplier_quotation",
|
||||
"use_transaction_date_exchange_rate",
|
||||
"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",
|
||||
"subcontract",
|
||||
"backflush_raw_materials_of_subcontract_based_on",
|
||||
@@ -231,6 +235,26 @@
|
||||
"fieldname": "allow_zero_qty_in_supplier_quotation",
|
||||
"fieldtype": "Check",
|
||||
"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,
|
||||
@@ -239,7 +263,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-05-06 15:21:49.639642",
|
||||
"modified": "2025-05-16 15:56:38.321369",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying Settings",
|
||||
|
||||
@@ -38,6 +38,7 @@ class BuyingSettings(Document):
|
||||
project_update_frequency: DF.Literal["Each Transaction", "Manual"]
|
||||
role_to_override_stop_action: DF.Link | None
|
||||
set_landed_cost_based_on_purchase_invoice_rate: DF.Check
|
||||
set_valuation_rate_for_rejected_materials: DF.Check
|
||||
show_pay_button: DF.Check
|
||||
supp_master_name: DF.Literal["Supplier Name", "Naming Series", "Auto Name"]
|
||||
supplier_group: DF.Link | None
|
||||
@@ -57,6 +58,9 @@ class BuyingSettings(Document):
|
||||
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):
|
||||
self.check_maintain_same_rate()
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ frappe.query_reports["Purchase Order Analysis"] = {
|
||||
width: "80",
|
||||
options: "Company",
|
||||
reqd: 1,
|
||||
default: frappe.defaults.get_default("company"),
|
||||
default: frappe.defaults.get_user_default("company"),
|
||||
},
|
||||
{
|
||||
fieldname: "from_date",
|
||||
|
||||
@@ -658,6 +658,10 @@ class BuyingController(SubcontractingController):
|
||||
sl_entries.append(from_warehouse_sle)
|
||||
|
||||
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(
|
||||
self.get_sl_entries(
|
||||
d,
|
||||
@@ -666,7 +670,8 @@ class BuyingController(SubcontractingController):
|
||||
"actual_qty": flt(
|
||||
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,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -431,99 +431,6 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
else:
|
||||
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):
|
||||
target_doc.qty = -1 * source_doc.qty
|
||||
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):
|
||||
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
|
||||
|
||||
batches = frappe._dict()
|
||||
|
||||
old_field = "batch_no"
|
||||
|
||||
@@ -13,6 +13,7 @@ from frappe.utils.deprecations import deprecated
|
||||
import erpnext
|
||||
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.utils import get_currency_precision
|
||||
from erpnext.controllers.accounts_controller import (
|
||||
validate_conversion_rate,
|
||||
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=(",", ":"))
|
||||
|
||||
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(
|
||||
flt(self.doc.get(scrub(self.doc.apply_discount_on)))
|
||||
* self.doc.additional_discount_percentage
|
||||
|
||||
@@ -343,6 +343,7 @@ def get_children(doctype=None, parent=None, **kwargs):
|
||||
|
||||
fields = [
|
||||
"item_code as value",
|
||||
"item_name as title",
|
||||
"is_expandable as expandable",
|
||||
"parent as parent_id",
|
||||
"qty",
|
||||
|
||||
@@ -23,6 +23,7 @@ def get_columns():
|
||||
"""return columns"""
|
||||
columns = [
|
||||
_("Item") + ":Link/Item:150",
|
||||
_("Item Name") + "::240",
|
||||
_("Description") + "::300",
|
||||
_("BOM Qty") + ":Float:160",
|
||||
_("BOM UoM") + "::160",
|
||||
@@ -73,6 +74,7 @@ def get_bom_stock(filters):
|
||||
.on((BOM_ITEM.item_code == BIN.item_code) & (CONDITIONS))
|
||||
.select(
|
||||
BOM_ITEM.item_code,
|
||||
BOM_ITEM.item_name,
|
||||
BOM_ITEM.description,
|
||||
BOM_ITEM.stock_qty,
|
||||
BOM_ITEM.stock_uom,
|
||||
|
||||
@@ -94,6 +94,7 @@ def get_expected_data(bom, warehouse, qty_to_produce, show_exploded_view=False):
|
||||
expected_data.append(
|
||||
[
|
||||
item.item_code,
|
||||
item.item_name,
|
||||
item.description,
|
||||
item.stock_qty,
|
||||
item.stock_uom,
|
||||
|
||||
@@ -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.rename_group_by_to_categorize_by_in_custom_reports
|
||||
erpnext.patches.v14_0.update_full_name_in_contract
|
||||
erpnext.patches.v15_0.drop_sle_indexes
|
||||
|
||||
17
erpnext/patches/v15_0/drop_sle_indexes.py
Normal file
17
erpnext/patches/v15_0/drop_sle_indexes.py
Normal 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")
|
||||
@@ -7,7 +7,7 @@ import json
|
||||
import frappe
|
||||
from frappe import _
|
||||
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.setup.utils import get_exchange_rate
|
||||
@@ -194,7 +194,7 @@ class Timesheet(Document):
|
||||
return
|
||||
|
||||
_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
|
||||
|
||||
def validate_overlap(self, data):
|
||||
|
||||
@@ -64,7 +64,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
&& this.frm.doc.is_pos
|
||||
&& this.frm.doc.is_return
|
||||
) {
|
||||
this.set_total_amount_to_default_mop();
|
||||
await this.set_total_amount_to_default_mop();
|
||||
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.
|
||||
*/
|
||||
|
||||
let return_against_mop = await frappe.call({
|
||||
method: 'erpnext.controllers.sales_and_purchase_return.get_payment_data',
|
||||
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;
|
||||
if(this.frm.doc.return_against){
|
||||
let {message : return_against_mop } = await frappe.call({
|
||||
method: 'erpnext.controllers.sales_and_purchase_return.get_payment_data',
|
||||
args: {
|
||||
invoice: this.frm.doc.return_against
|
||||
}
|
||||
});
|
||||
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 => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
validate() {
|
||||
this.calculate_taxes_and_totals(false);
|
||||
async validate() {
|
||||
await this.calculate_taxes_and_totals(false);
|
||||
}
|
||||
|
||||
update_stock() {
|
||||
|
||||
@@ -45,7 +45,7 @@ erpnext.financial_statements = {
|
||||
}
|
||||
}
|
||||
|
||||
if (data && column.fieldname == "account") {
|
||||
if (data && column.fieldname == this.name_field) {
|
||||
// first column
|
||||
value = data.section_name || data.account_name || value;
|
||||
|
||||
|
||||
@@ -174,29 +174,22 @@ class Quotation(SellingController):
|
||||
)
|
||||
|
||||
def get_ordered_status(self):
|
||||
status = "Open"
|
||||
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,
|
||||
)
|
||||
)
|
||||
ordered_items = get_ordered_items(self.name)
|
||||
|
||||
if not ordered_items:
|
||||
return status
|
||||
return "Open"
|
||||
|
||||
has_alternatives = any(row.is_alternative for row in self.get("items"))
|
||||
self._items = self.get_valid_items() if has_alternatives else self.get("items")
|
||||
self._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):
|
||||
status = "Partially Ordered"
|
||||
else:
|
||||
status = "Ordered"
|
||||
for row in self._items:
|
||||
if row.name not in ordered_items or row.qty > ordered_items[row.name]:
|
||||
return "Partially Ordered"
|
||||
|
||||
return status
|
||||
return "Ordered"
|
||||
|
||||
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):
|
||||
customer = _make_customer(source_name, ignore_permissions)
|
||||
ordered_items = frappe._dict(
|
||||
frappe.db.get_all(
|
||||
"Sales Order Item",
|
||||
{"prevdoc_docname": source_name, "docstatus": 1},
|
||||
["item_code", "sum(qty)"],
|
||||
group_by="item_code",
|
||||
as_list=1,
|
||||
)
|
||||
)
|
||||
ordered_items = get_ordered_items(source_name)
|
||||
|
||||
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")
|
||||
|
||||
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.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
|
||||
3. If no selections: Simple row: Map if adequate qty
|
||||
"""
|
||||
balance_qty = item.qty - ordered_items.get(item.item_code, 0.0)
|
||||
has_valid_qty: bool = (balance_qty > 0) or is_unit_price_row(item)
|
||||
|
||||
if not has_valid_qty:
|
||||
if not ((item.qty > ordered_items.get(item.name, 0.0)) or is_unit_price_row(item)):
|
||||
return False
|
||||
|
||||
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))
|
||||
|
||||
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,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -815,6 +815,52 @@ class TestQuotation(FrappeTestCase):
|
||||
quotation.reload()
|
||||
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")
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ def get_data():
|
||||
"Quotation": ["items", "prevdoc_docname"],
|
||||
"BOM": ["items", "bom_no"],
|
||||
"Blanket Order": ["items", "blanket_order"],
|
||||
"Purchase Order": ["items", "purchase_order"],
|
||||
},
|
||||
"transactions": [
|
||||
{
|
||||
|
||||
@@ -321,25 +321,17 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
||||
|
||||
get_condition_btn_map(after_submission) {
|
||||
if (after_submission)
|
||||
return [
|
||||
{
|
||||
condition: true,
|
||||
visible_btns: [__("Print Receipt"), __("Email Receipt"), __("New Order")],
|
||||
},
|
||||
];
|
||||
return [{ condition: true, visible_btns: ["Print Receipt", "Email Receipt", "New Order"] }];
|
||||
|
||||
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,
|
||||
visible_btns: [__("Print Receipt"), __("Email Receipt"), __("Return")],
|
||||
visible_btns: ["Print Receipt", "Email Receipt", "Return"],
|
||||
},
|
||||
{
|
||||
condition: this.doc.is_return && this.doc.docstatus === 1,
|
||||
visible_btns: [__("Print Receipt"), __("Email Receipt")],
|
||||
visible_btns: ["Print Receipt", "Email Receipt"],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -496,6 +496,12 @@ class PurchaseReceipt(BuyingController):
|
||||
outgoing_amount = abs(get_stock_value_difference(self.name, item.name, item.from_warehouse))
|
||||
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 not account:
|
||||
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")
|
||||
)
|
||||
|
||||
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:
|
||||
loss_account = (
|
||||
self.get_company_default("default_expense_account", ignore_validation=True)
|
||||
@@ -726,13 +738,23 @@ class PurchaseReceipt(BuyingController):
|
||||
make_sub_contracting_gl_entries(d)
|
||||
make_divisional_loss_gl_entry(d, outgoing_amount)
|
||||
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)
|
||||
|
||||
if d.is_fixed_asset and d.landed_cost_voucher_amount:
|
||||
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:
|
||||
frappe.msgprint(
|
||||
_("No accounting entries for the following warehouses")
|
||||
|
||||
@@ -4199,6 +4199,126 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
# Test 3 - OverAllowanceError should be thrown as qty is greater than qty in DN
|
||||
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():
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||
|
||||
@@ -722,19 +722,19 @@ class SerialandBatchBundle(Document):
|
||||
|
||||
def reset_qty(self, row, qty_field=None):
|
||||
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
|
||||
while qty > 0:
|
||||
for d in self.entries:
|
||||
row_qty = abs(d.qty)
|
||||
row_qty = abs(flt(d.qty, d.precision("qty")))
|
||||
if row_qty >= qty:
|
||||
d.db_set("qty", qty if self.type_of_transaction == "Inward" else qty * -1)
|
||||
qty = 0
|
||||
idx = d.idx
|
||||
break
|
||||
else:
|
||||
qty -= row_qty
|
||||
qty = flt(qty - row_qty, d.precision("qty"))
|
||||
idx = d.idx
|
||||
|
||||
if idx and len(self.entries) > idx:
|
||||
|
||||
@@ -57,7 +57,6 @@
|
||||
"options": "Item",
|
||||
"print_width": "100px",
|
||||
"read_only": 1,
|
||||
"search_index": 1,
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
@@ -88,7 +87,6 @@
|
||||
"options": "Warehouse",
|
||||
"print_width": "100px",
|
||||
"read_only": 1,
|
||||
"search_index": 1,
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
@@ -101,7 +99,6 @@
|
||||
"oldfieldtype": "Date",
|
||||
"print_width": "100px",
|
||||
"read_only": 1,
|
||||
"search_index": 1,
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
@@ -357,13 +354,14 @@
|
||||
"search_index": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"hide_toolbar": 1,
|
||||
"icon": "fa fa-list",
|
||||
"idx": 1,
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-12-23 18:03:05.171023",
|
||||
"modified": "2025-04-22 12:37:41.304109",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Ledger Entry",
|
||||
|
||||
@@ -345,5 +345,4 @@ class StockLedgerEntry(Document):
|
||||
def on_doctype_update():
|
||||
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", ["warehouse", "item_code"], "item_warehouse")
|
||||
frappe.db.add_index("Stock Ledger Entry", ["posting_datetime", "creation"])
|
||||
frappe.db.add_index("Stock Ledger Entry", ["item_code", "warehouse", "posting_datetime", "creation"])
|
||||
|
||||
@@ -6,7 +6,7 @@ from collections import defaultdict
|
||||
import frappe
|
||||
from frappe import _
|
||||
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):
|
||||
@@ -167,7 +167,8 @@ def get_query_based_on_filters(query, batch, table, filters):
|
||||
query = query.where(batch.batch_qty > 0)
|
||||
|
||||
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:
|
||||
lft, rgt = frappe.db.get_value("Warehouse", filters.warehouse, ["lft", "rgt"])
|
||||
|
||||
@@ -115,7 +115,6 @@ def get_stock_ledger_entries_for_batch_no(filters):
|
||||
& (sle.posting_datetime < posting_datetime)
|
||||
)
|
||||
.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)
|
||||
@@ -160,7 +159,6 @@ def get_stock_ledger_entries_for_batch_bundle(filters):
|
||||
& (sle.posting_datetime <= to_date)
|
||||
)
|
||||
.groupby(sle.voucher_no, batch_package.batch_no, batch_package.warehouse)
|
||||
.orderby(sle.item_code, sle.warehouse)
|
||||
)
|
||||
|
||||
query = apply_warehouse_filter(query, sle, filters)
|
||||
|
||||
@@ -66,7 +66,7 @@ def get_stock_ledger_entries(filters):
|
||||
"Stock Ledger Entry",
|
||||
fields=SLE_FIELDS,
|
||||
filters=sle_filters,
|
||||
order_by="timestamp(posting_date, posting_time), creation",
|
||||
order_by="posting_datetime, creation",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import IfNull
|
||||
from frappe.query_builder.functions import IfNull, Max
|
||||
from frappe.utils import flt
|
||||
from pypika.terms import ExistsCriterion
|
||||
|
||||
@@ -208,30 +208,21 @@ def get_stock_ledger_entries(filters, items):
|
||||
if not items:
|
||||
return []
|
||||
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
sle2 = frappe.qb.DocType("Stock Ledger Entry")
|
||||
max_posting_datetime_query = get_item_wise_max_posting_datetime(filters, items)
|
||||
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
query = (
|
||||
frappe.qb.from_(sle)
|
||||
.left_join(sle2)
|
||||
.join(max_posting_datetime_query)
|
||||
.on(
|
||||
(sle.item_code == sle2.item_code)
|
||||
& (sle.warehouse == sle2.warehouse)
|
||||
& (sle.posting_datetime < sle2.posting_datetime)
|
||||
& (sle.name < sle2.name)
|
||||
(sle.item_code == max_posting_datetime_query.item_code)
|
||||
& (sle.warehouse == max_posting_datetime_query.warehouse)
|
||||
& (sle.posting_datetime == max_posting_datetime_query.posting_datetime)
|
||||
)
|
||||
.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"):
|
||||
warehouse_details = frappe.db.get_value(
|
||||
"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)
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -5,8 +5,8 @@ import datetime
|
||||
import frappe
|
||||
from frappe import _, scrub
|
||||
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_of_week, get_quarter_start, getdate
|
||||
from frappe.utils.nestedset import get_descendants_of
|
||||
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
@@ -294,9 +294,8 @@ def get_stock_ledger_entries(filters, items):
|
||||
sle.batch_no,
|
||||
)
|
||||
.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.actual_qty)
|
||||
)
|
||||
|
||||
if items:
|
||||
@@ -314,7 +313,8 @@ def apply_conditions(query, filters):
|
||||
frappe.throw(_("'From Date' is required"))
|
||||
|
||||
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:
|
||||
frappe.throw(_("'To Date' is required"))
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ def get_stock_ledger_entries(filters):
|
||||
"Stock Ledger Entry",
|
||||
fields=SLE_FIELDS,
|
||||
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",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1818,7 +1818,7 @@ def get_valuation_rate(
|
||||
# Get valuation rate from last sle for the same item and warehouse
|
||||
if last_valuation_rate := frappe.db.sql( # nosemgrep
|
||||
"""select valuation_rate
|
||||
from `tabStock Ledger Entry` force index (item_warehouse)
|
||||
from `tabStock Ledger Entry`
|
||||
where
|
||||
item_code = %s
|
||||
AND warehouse = %s
|
||||
@@ -1962,7 +1962,6 @@ def get_next_stock_reco(kwargs):
|
||||
sle.actual_qty,
|
||||
sle.has_batch_no,
|
||||
)
|
||||
.force_index("item_warehouse")
|
||||
.where(
|
||||
(sle.item_code == kwargs.get("item_code"))
|
||||
& (sle.warehouse == kwargs.get("warehouse"))
|
||||
|
||||
@@ -10,6 +10,10 @@ import erpnext
|
||||
from erpnext.accounts.utils import get_account_currency
|
||||
from erpnext.buying.utils import check_on_hold_or_closed_status
|
||||
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
|
||||
|
||||
|
||||
@@ -140,6 +144,9 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
|
||||
self.get_current_stock()
|
||||
|
||||
self.set_supplied_items_expense_account()
|
||||
self.set_supplied_items_cost_center()
|
||||
|
||||
def on_submit(self):
|
||||
self.validate_closed_subcontracting_order()
|
||||
self.validate_available_qty_for_consumption()
|
||||
@@ -224,6 +231,17 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
if not item.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):
|
||||
if self.company:
|
||||
expense_account = self.get_company_default("default_expense_account", ignore_validation=True)
|
||||
@@ -232,6 +250,22 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
if not item.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):
|
||||
if (
|
||||
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):
|
||||
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:
|
||||
if flt(item.rate) and flt(item.qty):
|
||||
if warehouse_account.get(item.warehouse):
|
||||
@@ -568,32 +614,33 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
)
|
||||
|
||||
if flt(item.rm_supp_cost) and supplier_warehouse_account:
|
||||
# Supplier Warehouse Account (Credit)
|
||||
self.add_gl_entry(
|
||||
gl_entries=gl_entries,
|
||||
account=supplier_warehouse_account,
|
||||
cost_center=item.cost_center,
|
||||
debit=0.0,
|
||||
credit=flt(item.rm_supp_cost),
|
||||
remarks=remarks,
|
||||
against_account=item.expense_account,
|
||||
account_currency=get_account_currency(supplier_warehouse_account),
|
||||
project=item.project,
|
||||
item=item,
|
||||
)
|
||||
# Expense Account (Debit)
|
||||
self.add_gl_entry(
|
||||
gl_entries=gl_entries,
|
||||
account=item.expense_account,
|
||||
cost_center=item.cost_center,
|
||||
debit=flt(item.rm_supp_cost),
|
||||
credit=0.0,
|
||||
remarks=remarks,
|
||||
against_account=supplier_warehouse_account,
|
||||
account_currency=get_account_currency(item.expense_account),
|
||||
project=item.project,
|
||||
item=item,
|
||||
)
|
||||
for rm_item in supplied_items_details.get(item.name):
|
||||
# Supplier Warehouse Account (Credit)
|
||||
self.add_gl_entry(
|
||||
gl_entries=gl_entries,
|
||||
account=supplier_warehouse_account,
|
||||
cost_center=rm_item.cost_center,
|
||||
debit=0.0,
|
||||
credit=flt(rm_item.amount),
|
||||
remarks=remarks,
|
||||
against_account=rm_item.expense_account,
|
||||
account_currency=get_account_currency(supplier_warehouse_account),
|
||||
project=item.project,
|
||||
item=item,
|
||||
)
|
||||
# Expense Account (Debit)
|
||||
self.add_gl_entry(
|
||||
gl_entries=gl_entries,
|
||||
account=rm_item.expense_account,
|
||||
cost_center=rm_item.cost_center,
|
||||
debit=flt(rm_item.amount),
|
||||
credit=0.0,
|
||||
remarks=remarks,
|
||||
against_account=supplier_warehouse_account,
|
||||
account_currency=get_account_currency(item.expense_account),
|
||||
project=item.project,
|
||||
item=item,
|
||||
)
|
||||
|
||||
# Expense Account (Debit)
|
||||
if item.additional_cost_per_qty:
|
||||
|
||||
@@ -372,6 +372,56 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
||||
self.assertTrue(get_gl_entries("Subcontracting Receipt", scr.name))
|
||||
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})
|
||||
def test_subcontracting_receipt_with_zero_service_cost(self):
|
||||
warehouse = "Stores - TCP1"
|
||||
|
||||
@@ -33,7 +33,11 @@
|
||||
"section_break_zwnh",
|
||||
"serial_no",
|
||||
"column_break_qibi",
|
||||
"batch_no"
|
||||
"batch_no",
|
||||
"accounting_details_section",
|
||||
"expense_account",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -103,7 +107,7 @@
|
||||
{
|
||||
"fieldname": "stock_uom",
|
||||
"fieldtype": "Link",
|
||||
"label": "Stock Uom",
|
||||
"label": "Stock UOM",
|
||||
"options": "UOM",
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -231,18 +235,43 @@
|
||||
"fieldname": "add_serial_batch_bundle",
|
||||
"fieldtype": "Button",
|
||||
"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,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-30 10:26:27.237371",
|
||||
"modified": "2025-05-27 12:33:58.772638",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Subcontracting",
|
||||
"name": "Subcontracting Receipt Supplied Item",
|
||||
"naming_rule": "Autoincrement",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
|
||||
@@ -20,8 +20,10 @@ class SubcontractingReceiptSuppliedItem(Document):
|
||||
bom_detail_no: DF.Data | None
|
||||
consumed_qty: DF.Float
|
||||
conversion_factor: DF.Float
|
||||
cost_center: DF.Link | None
|
||||
current_stock: DF.Float
|
||||
description: DF.TextEditor | None
|
||||
expense_account: DF.Link | None
|
||||
item_name: DF.Data | None
|
||||
main_item_code: DF.Link | None
|
||||
parent: DF.Data
|
||||
|
||||
@@ -5,7 +5,6 @@ INDEXED_FIELDS = {
|
||||
"Bin": ["item_code"],
|
||||
"GL Entry": ["voucher_no", "posting_date", "company", "party"],
|
||||
"Purchase Order Item": ["item_code"],
|
||||
"Stock Ledger Entry": ["warehouse"],
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user