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

chore: release v15
This commit is contained in:
ruthra kumar
2025-09-30 18:56:45 +05:30
committed by GitHub
33 changed files with 780 additions and 1153 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -116,7 +116,7 @@
{
"allow_on_submit": 1,
"fieldname": "reference_number",
"fieldtype": "Data",
"fieldtype": "Small Text",
"label": "Reference Number"
},
{
@@ -238,7 +238,7 @@
"grid_page_length": 50,
"is_submittable": 1,
"links": [],
"modified": "2025-06-18 17:24:57.044666",
"modified": "2025-09-26 17:06:29.207673",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Transaction",

View File

@@ -36,7 +36,7 @@ class BankTransaction(Document):
party: DF.DynamicLink | None
party_type: DF.Link | None
payment_entries: DF.Table[BankTransactionPayments]
reference_number: DF.Data | None
reference_number: DF.SmallText | None
status: DF.Literal["", "Pending", "Settled", "Unreconciled", "Reconciled", "Cancelled"]
transaction_id: DF.Data | None
transaction_type: DF.Data | None

View File

@@ -253,7 +253,7 @@ class GLEntry(Document):
)
def validate_cost_center(self):
if not self.cost_center:
if not self.cost_center or self.is_cancelled:
return
is_group, company = frappe.get_cached_value("Cost Center", self.cost_center, ["is_group", "company"])

View File

@@ -189,8 +189,8 @@ class JournalEntry(AccountsController):
def on_submit(self):
self.validate_cheque_info()
self.check_credit_limit()
self.make_gl_entries()
self.check_credit_limit()
self.update_asset_value()
self.update_inter_company_jv()
self.update_invoice_discounting()

View File

@@ -11,6 +11,7 @@ from frappe.utils import flt, nowdate
from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.accounts.doctype.journal_entry.journal_entry import StockAccountInvalidTransaction
from erpnext.exceptions import InvalidAccountCurrency
from erpnext.selling.doctype.customer.test_customer import make_customer, set_credit_limit
class TestJournalEntry(unittest.TestCase):
@@ -592,6 +593,15 @@ class TestJournalEntry(unittest.TestCase):
self.assertEqual(jv.pay_to_recd_from, "_Test Receiver 2")
def test_credit_limit_for_customer(self):
customer = make_customer("_Test New Customer")
set_credit_limit("_Test New Customer", "_Test Company", 50)
jv = make_journal_entry(account1="Debtors - _TC", account2="_Test Cash - _TC", amount=100, save=False)
jv.accounts[0].party_type = "Customer"
jv.accounts[0].party = customer
jv.save()
self.assertRaises(frappe.ValidationError, jv.submit)
def make_journal_entry(
account1,

View File

@@ -601,6 +601,7 @@ frappe.ui.form.on("Payment Entry", {
if (frm.doc.payment_type == "Pay") {
frm.events.paid_amount(frm);
}
frm.events.paid_from_account_currency(frm);
}
);
},
@@ -624,6 +625,7 @@ frappe.ui.form.on("Payment Entry", {
frm.events.received_amount(frm);
}
}
frm.events.paid_to_account_currency(frm);
}
);
},

View File

@@ -127,7 +127,13 @@ class PaymentRequest(Document):
existing_payment_request_amount = flt(get_existing_payment_request_amount(ref_doc))
if existing_payment_request_amount + flt(self.grand_total) > ref_amount:
if (
flt(
existing_payment_request_amount + flt(self.grand_total, self.precision("grand_total")),
get_currency_precision(),
)
> ref_amount
):
frappe.throw(
_("Total Payment Request amount cannot be greater than {0} amount").format(
self.reference_doctype

View File

@@ -5,28 +5,33 @@ frappe.query_reports["Balance Sheet"] = $.extend({}, erpnext.financial_statement
erpnext.utils.add_dimensions("Balance Sheet", 10);
frappe.query_reports["Balance Sheet"]["filters"].push({
fieldname: "selected_view",
label: __("Select View"),
fieldtype: "Select",
options: [
{ value: "Report", label: __("Report View") },
{ value: "Growth", label: __("Growth View") },
],
default: "Report",
reqd: 1,
});
frappe.query_reports["Balance Sheet"]["filters"].push({
fieldname: "accumulated_values",
label: __("Accumulated Values"),
fieldtype: "Check",
default: 1,
});
frappe.query_reports["Balance Sheet"]["filters"].push({
fieldname: "include_default_book_entries",
label: __("Include Default FB Entries"),
fieldtype: "Check",
default: 1,
});
frappe.query_reports["Balance Sheet"]["filters"].push(
{
fieldname: "selected_view",
label: __("Select View"),
fieldtype: "Select",
options: [
{ value: "Report", label: __("Report View") },
{ value: "Growth", label: __("Growth View") },
],
default: "Report",
reqd: 1,
},
{
fieldname: "accumulated_values",
label: __("Accumulated Values"),
fieldtype: "Check",
default: 1,
},
{
fieldname: "include_default_book_entries",
label: __("Include Default FB Entries"),
fieldtype: "Check",
default: 1,
},
{
fieldname: "show_zero_values",
label: __("Show zero values"),
fieldtype: "Check",
}
);

View File

@@ -212,7 +212,7 @@ def get_data(
company_currency,
accumulated_values=filters.accumulated_values,
)
out = filter_out_zero_value_rows(out, parent_children_map)
out = filter_out_zero_value_rows(out, parent_children_map, filters.show_zero_values)
if out and total:
add_total_row(out, root_type, balance_must_be, period_list, company_currency)

View File

@@ -677,7 +677,9 @@ class GrossProfitGenerator:
si.name = si_item.parent
and si.docstatus = 1
and si.is_return = 1
and si.posting_date between %(from_date)s and %(to_date)s
""",
{"from_date": self.filters.from_date, "to_date": self.filters.to_date},
as_dict=1,
)

View File

@@ -1,9 +1,9 @@
import frappe
from frappe import qb
from frappe.tests.utils import FrappeTestCase
from frappe.utils import flt, nowdate
from frappe.utils import add_days, flt, get_first_day, get_last_day, nowdate
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note, make_sales_return
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.gross_profit.gross_profit import execute
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
@@ -392,7 +392,6 @@ class TestGrossProfit(FrappeTestCase):
"""
Item Qty for Sales Invoices with multiple instances of same item go in the -ve. Ideally, the credit noteshould cancel out the invoice items.
"""
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
# Invoice with an item added twice
sinv = self.create_sales_invoice(qty=1, rate=100, posting_date=nowdate(), do_not_submit=True)
@@ -635,3 +634,42 @@ class TestGrossProfit(FrappeTestCase):
self.assertEqual(total.buying_amount, 0.0)
self.assertEqual(total.gross_profit, 100.0)
self.assertEqual(total.get("gross_profit_%"), 100.0)
def test_profit_for_later_period_return(self):
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
# create sales invoice on month start date
sinv = self.create_sales_invoice(qty=1, rate=100, do_not_save=True, do_not_submit=True)
sinv.set_posting_time = 1
sinv.posting_date = month_start_date
sinv.save().submit()
# create credit note on next month start date
cr_note = make_sales_return(sinv.name)
cr_note.set_posting_time = 1
cr_note.posting_date = add_days(month_end_date, 1)
cr_note.save().submit()
# apply filters for invoiced period
filters = frappe._dict(
company=self.company, from_date=month_start_date, to_date=month_end_date, group_by="Invoice"
)
_, data = execute(filters=filters)
total = data[-1]
self.assertEqual(total.selling_amount, 100.0)
self.assertEqual(total.buying_amount, 0.0)
self.assertEqual(total.gross_profit, 100.0)
self.assertEqual(total.get("gross_profit_%"), 100.0)
# extend filters upto returned period
filters.update(to_date=add_days(month_end_date, 1))
_, data = execute(filters=filters)
total = data[-1]
self.assertEqual(total.selling_amount, 0.0)
self.assertEqual(total.buying_amount, 0.0)
self.assertEqual(total.gross_profit, 0.0)
self.assertEqual(total.get("gross_profit_%"), 0.0)

View File

@@ -5,29 +5,34 @@ frappe.query_reports["Profit and Loss Statement"] = $.extend({}, erpnext.financi
erpnext.utils.add_dimensions("Profit and Loss Statement", 10);
frappe.query_reports["Profit and Loss Statement"]["filters"].push({
fieldname: "selected_view",
label: __("Select View"),
fieldtype: "Select",
options: [
{ value: "Report", label: __("Report View") },
{ value: "Growth", label: __("Growth View") },
{ value: "Margin", label: __("Margin View") },
],
default: "Report",
reqd: 1,
});
frappe.query_reports["Profit and Loss Statement"]["filters"].push({
fieldname: "accumulated_values",
label: __("Accumulated Values"),
fieldtype: "Check",
default: 1,
});
frappe.query_reports["Profit and Loss Statement"]["filters"].push({
fieldname: "include_default_book_entries",
label: __("Include Default FB Entries"),
fieldtype: "Check",
default: 1,
});
frappe.query_reports["Profit and Loss Statement"]["filters"].push(
{
fieldname: "selected_view",
label: __("Select View"),
fieldtype: "Select",
options: [
{ value: "Report", label: __("Report View") },
{ value: "Growth", label: __("Growth View") },
{ value: "Margin", label: __("Margin View") },
],
default: "Report",
reqd: 1,
},
{
fieldname: "accumulated_values",
label: __("Accumulated Values"),
fieldtype: "Check",
default: 1,
},
{
fieldname: "include_default_book_entries",
label: __("Include Default FB Entries"),
fieldtype: "Check",
default: 1,
},
{
fieldname: "show_zero_values",
label: __("Show zero values"),
fieldtype: "Check",
}
);

View File

@@ -216,7 +216,7 @@ def get_opening_balance(
ignore_is_opening=0,
):
closing_balance = frappe.qb.DocType(doctype)
account = frappe.qb.DocType("Account")
accounts = frappe.db.get_all("Account", filters={"report_type": report_type}, pluck="name")
opening_balance = (
frappe.qb.from_(closing_balance)
@@ -228,14 +228,7 @@ def get_opening_balance(
Sum(closing_balance.debit_in_account_currency).as_("debit_in_account_currency"),
Sum(closing_balance.credit_in_account_currency).as_("credit_in_account_currency"),
)
.where(
(closing_balance.company == filters.company)
& (
closing_balance.account.isin(
frappe.qb.from_(account).select("name").where(account.report_type == report_type)
)
)
)
.where((closing_balance.company == filters.company) & (closing_balance.account.isin(accounts)))
.groupby(closing_balance.account)
)
@@ -290,21 +283,24 @@ def get_opening_balance(
if filters.project:
opening_balance = opening_balance.where(closing_balance.project == filters.project)
if filters.get("include_default_book_entries"):
company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book")
if frappe.db.count("Finance Book"):
if filters.get("include_default_book_entries"):
company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book")
if filters.finance_book and company_fb and cstr(filters.finance_book) != cstr(company_fb):
frappe.throw(_("To use a different finance book, please uncheck 'Include Default FB Entries'"))
if filters.finance_book and company_fb and cstr(filters.finance_book) != cstr(company_fb):
frappe.throw(
_("To use a different finance book, please uncheck 'Include Default FB Entries'")
)
opening_balance = opening_balance.where(
(closing_balance.finance_book.isin([cstr(filters.finance_book), cstr(company_fb), ""]))
| (closing_balance.finance_book.isnull())
)
else:
opening_balance = opening_balance.where(
(closing_balance.finance_book.isin([cstr(filters.finance_book), ""]))
| (closing_balance.finance_book.isnull())
)
opening_balance = opening_balance.where(
(closing_balance.finance_book.isin([cstr(filters.finance_book), cstr(company_fb), ""]))
| (closing_balance.finance_book.isnull())
)
else:
opening_balance = opening_balance.where(
(closing_balance.finance_book.isin([cstr(filters.finance_book), ""]))
| (closing_balance.finance_book.isnull())
)
if accounting_dimensions:
for dimension in accounting_dimensions:

View File

@@ -200,7 +200,11 @@ def validate_quantity(doc, key, args, ref, valid_items, already_returned_items):
current_stock_qty = args.get(column)
elif args.get("return_qty_from_rejected_warehouse"):
reference_qty = ref.get("rejected_qty") * ref.get("conversion_factor", 1.0)
current_stock_qty = args.get(column) * args.get("conversion_factor", 1.0)
current_stock_qty = (
args.get(column) * args.get("conversion_factor", 1.0)
if column != "stock_qty"
else args.get(column)
)
else:
reference_qty = ref.get(column) * ref.get("conversion_factor", 1.0)
current_stock_qty = args.get(column) * args.get("conversion_factor", 1.0)

View File

@@ -12,6 +12,8 @@ from frappe.utils import cint, flt, get_link_to_form
from erpnext.controllers.stock_controller import StockController
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
get_auto_batch_nos,
get_available_serial_nos,
get_voucher_wise_serial_batch_from_bundle,
)
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@@ -52,9 +54,42 @@ class SubcontractingController(StockController):
if self.doctype in ["Subcontracting Order", "Subcontracting Receipt"]:
self.validate_items()
self.create_raw_materials_supplied()
self.set_valuation_rate_for_rm()
else:
super().validate()
def set_valuation_rate_for_rm(self):
rate_changed = False
if self.doctype == "Subcontracting Receipt":
for row in self.supplied_items:
kwargs = frappe._dict(
{
"item_code": row.rm_item_code,
"warehouse": self.supplier_warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"qty": flt(row.consumed_qty) * (-1 if not self.is_return else 1),
"voucher_type": self.doctype,
"voucher_no": self.name,
"company": self.company,
"serial_and_batch_bundle": row.serial_and_batch_bundle,
"voucher_detail_no": row.name,
"batch_no": row.batch_no,
"serial_no": row.serial_no,
"use_serial_batch_fields": row.use_serial_batch_fields,
}
)
rate = get_incoming_rate(kwargs)
precision = frappe.get_precision("Subcontracting Receipt Supplied Item", "rate")
if flt(rate, precision) != flt(row.rate, precision):
row.rate = rate
row.amount = flt(row.consumed_qty) * flt(rate)
rate_changed = True
if rate_changed:
self.calculate_items_qty_and_amount()
def validate_rejected_warehouse(self):
for item in self.get("items"):
if flt(item.rejected_qty) and not item.rejected_warehouse:
@@ -610,6 +645,64 @@ class SubcontractingController(StockController):
self.set_rate_for_supplied_items(rm_obj, item_row)
elif self.backflush_based_on == "BOM":
self.update_rate_for_supplied_items()
self.set_batch_for_supplied_items()
def set_batch_for_supplied_items(self):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos_for_outward
from erpnext.stock.get_item_details import get_filtered_serial_nos
for row in self.supplied_items:
item_details = frappe.get_cached_value(
"Item", row.rm_item_code, ["has_batch_no", "has_serial_no"], as_dict=1
)
if not item_details.has_batch_no and not item_details.has_serial_no:
continue
if not row.use_serial_batch_fields:
continue
kwargs = frappe._dict(
{
"item_code": row.rm_item_code,
"warehouse": self.supplier_warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"qty": flt(row.consumed_qty),
}
)
if item_details.has_serial_no and not row.serial_and_batch_bundle and not row.serial_no:
serial_nos = get_available_serial_nos(kwargs)
if serial_nos:
serial_nos = [sn.get("serial_no") for sn in serial_nos]
serial_nos = get_filtered_serial_nos(serial_nos, self, "supplied_items")
row.serial_no = "\n".join(serial_nos)
elif item_details.has_batch_no and not row.serial_and_batch_bundle and not row.batch_no:
batches = get_auto_batch_nos(kwargs)
if batches:
consumed_qty = row.consumed_qty
for index, d in enumerate(batches):
if consumed_qty <= 0:
break
if index == 0:
row.batch_no = d.get("batch_no")
row.consumed_qty = d.get("qty")
consumed_qty -= d.get("qty")
else:
new_row = self.append("supplied_items", {})
new_row.update(frappe.copy_doc(row).as_dict())
new_row.update(
{
"consumed_qty": d.get("qty"),
"batch_no": d.get("batch_no"),
"rate": row.rate,
"amount": flt(d.get("qty")) * flt(row.rate),
}
)
consumed_qty -= d.get("qty")
def update_rate_for_supplied_items(self):
if self.doctype != "Subcontracting Receipt":

View File

@@ -1308,6 +1308,7 @@ def make_subcontracted_items():
"Subcontracted Item SA7": {},
"Subcontracted Item SA8": {},
"Subcontracted Item SA9": {"stock_uom": "Litre"},
"Subcontracted Item SA10": {},
}
for item, properties in sub_contracted_items.items():
@@ -1329,6 +1330,7 @@ def make_raw_materials():
"Subcontracted SRM Item 5": {"has_serial_no": 1, "serial_no_series": "SRIID.####"},
"Subcontracted SRM Item 8": {},
"Subcontracted SRM Item 9": {"stock_uom": "Litre"},
"Subcontracted SRM Item 10": {},
}
for item, properties in raw_materials.items():
@@ -1357,6 +1359,7 @@ def make_service_items():
"Subcontracted Service Item 7": {},
"Subcontracted Service Item 8": {},
"Subcontracted Service Item 9": {},
"Subcontracted Service Item 10": {},
}
for item, properties in service_items.items():
@@ -1381,6 +1384,7 @@ def make_bom_for_subcontracted_items():
"Subcontracted Item SA6": ["Subcontracted SRM Item 3"],
"Subcontracted Item SA7": ["Subcontracted SRM Item 1"],
"Subcontracted Item SA8": ["Subcontracted SRM Item 8"],
"Subcontracted Item SA10": ["Subcontracted SRM Item 10"],
}
for item_code, raw_materials in boms.items():

View File

@@ -1631,7 +1631,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
}
)
sales_order = doc.get("sales_order")
sales_order = data.get("sales_order")
for item_code, details in item_details.items():
so_item_details.setdefault(sales_order, frappe._dict())

View File

@@ -2137,16 +2137,22 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
this.frm.doc.name).options,
"master_name": this.frm.doc.taxes_and_charges
},
callback: function(r) {
if(!r.exc) {
if(me.frm.doc.shipping_rule && me.frm.doc.taxes) {
for (let tax of r.message) {
callback: function (r) {
if (!r.exc) {
let taxes = r.message;
taxes.forEach((tax) => {
if (me.frm.doc?.cost_center && !tax.cost_center) {
tax.cost_center = me.frm.doc.cost_center;
}
});
if (me.frm.doc.shipping_rule && me.frm.doc.taxes) {
for (let tax of taxes) {
me.frm.add_child("taxes", tax);
}
refresh_field("taxes");
} else {
me.frm.set_value("taxes", r.message);
me.frm.set_value("taxes", taxes);
me.calculate_taxes_and_totals();
}
}

View File

@@ -443,3 +443,14 @@ def create_internal_customer(customer_name=None, represents_company=None, allowe
customer_name = frappe.db.get_value("Customer", customer_name)
return customer_name
def make_customer(customer_name):
if not frappe.db.exists("Customer", customer_name):
customer = frappe.new_doc("Customer")
customer.customer_name = customer_name
customer.customer_type = "Individual"
customer.insert()
return customer.name
else:
return customer_name

View File

@@ -74,8 +74,11 @@ class PickList(TransactionBase):
if self.has_reserved_stock():
self.set_onload("has_reserved_stock", True)
for item in self.get("locations"):
item.update(get_item_details(item.item_code, item.uom, item.warehouse, self.company))
if self.docstatus.is_draft() and not hasattr(self, "_action"):
company = self.company
for item in self.get("locations"):
item.update(get_item_details(item.item_code, item.uom, item.warehouse, company))
def validate(self):
self.validate_expired_batches()

View File

@@ -311,7 +311,7 @@ class SerialandBatchBundle(Document):
def throw_error_message(self, message, exception=frappe.ValidationError):
frappe.throw(_(message), exception, title=_("Error"))
def set_incoming_rate(self, parent=None, row=None, save=False, allow_negative_stock=False):
def set_incoming_rate(self, parent=None, row=None, save=False, allow_negative_stock=False, prev_sle=None):
if self.type_of_transaction not in ["Inward", "Outward"] or self.voucher_type in [
"Installation Note",
"Job Card",
@@ -321,15 +321,15 @@ class SerialandBatchBundle(Document):
return
if return_against := self.get_return_against(parent=parent):
self.set_valuation_rate_for_return_entry(return_against, row, save)
self.set_valuation_rate_for_return_entry(return_against, row, save, prev_sle=prev_sle)
elif self.type_of_transaction == "Outward":
self.set_incoming_rate_for_outward_transaction(
row, save, allow_negative_stock=allow_negative_stock
)
else:
self.set_incoming_rate_for_inward_transaction(row, save)
self.set_incoming_rate_for_inward_transaction(row, save, prev_sle=prev_sle)
def set_valuation_rate_for_return_entry(self, return_against, row, save=False):
def set_valuation_rate_for_return_entry(self, return_against, row, save=False, prev_sle=None):
if valuation_details := self.get_valuation_rate_for_return_entry(return_against):
for row in self.entries:
if valuation_details:
@@ -361,7 +361,7 @@ class SerialandBatchBundle(Document):
)
elif self.type_of_transaction == "Inward":
self.set_incoming_rate_for_inward_transaction(row, save)
self.set_incoming_rate_for_inward_transaction(row, save, prev_sle=prev_sle)
def validate_returned_serial_batch_no(self, return_against, row, original_inv_details):
if frappe.flags.through_repost_item_valuation:
@@ -529,7 +529,11 @@ class SerialandBatchBundle(Document):
if save:
d.db_set(
{"incoming_rate": d.incoming_rate, "stock_value_difference": d.stock_value_difference}
{
"incoming_rate": d.incoming_rate,
"stock_value_difference": d.stock_value_difference,
"stock_queue": d.get("stock_queue"),
}
)
def validate_negative_batch(self, batch_no, available_qty):
@@ -606,7 +610,11 @@ class SerialandBatchBundle(Document):
return return_against
def set_incoming_rate_for_inward_transaction(self, row=None, save=False):
def set_incoming_rate_for_inward_transaction(self, row=None, save=False, prev_sle=None):
from erpnext.stock.utils import get_valuation_method
valuation_method = get_valuation_method(self.item_code)
valuation_field = "valuation_rate"
if self.voucher_type in ["Sales Invoice", "Delivery Note", "Quotation"]:
valuation_field = "incoming_rate"
@@ -630,19 +638,42 @@ class SerialandBatchBundle(Document):
if not rate and self.voucher_detail_no and self.voucher_no:
rate = frappe.db.get_value(child_table, self.voucher_detail_no, valuation_field)
stock_queue = []
batches = []
if prev_sle and prev_sle.stock_queue:
batches = frappe.get_all(
"Batch",
filters={
"name": ("in", [d.batch_no for d in self.entries if d.batch_no]),
"use_batchwise_valuation": 0,
},
pluck="name",
)
if batches and valuation_method == "FIFO":
stock_queue = parse_json(prev_sle.stock_queue)
for d in self.entries:
if self.is_rejected:
rate = 0.0
elif (d.incoming_rate == rate) and d.qty and d.stock_value_difference:
elif (d.incoming_rate == rate) and not stock_queue and d.qty and d.stock_value_difference:
continue
d.incoming_rate = flt(rate)
if d.qty:
d.stock_value_difference = flt(d.qty) * d.incoming_rate
if stock_queue and valuation_method == "FIFO" and d.batch_no in batches:
stock_queue.append([d.qty, d.incoming_rate])
d.stock_queue = json.dumps(stock_queue)
if save:
d.db_set(
{"incoming_rate": d.incoming_rate, "stock_value_difference": d.stock_value_difference}
{
"incoming_rate": d.incoming_rate,
"stock_value_difference": d.stock_value_difference,
"stock_queue": d.get("stock_queue"),
}
)
def set_serial_and_batch_values(self, parent, row, qty_field=None):

View File

@@ -285,6 +285,25 @@ class TestSerialandBatchBundle(FrappeTestCase):
self.assertEqual(flt(sle.stock_value_difference), 1000.00 * -1)
self.assertEqual(json.loads(sle.stock_queue), [[20, 200]])
se = make_stock_entry(
item_code=batch_item_code,
target="_Test Warehouse - _TC",
qty=10,
rate=100,
batch_no=batch_id,
use_serial_batch_fields=True,
)
sle = frappe.db.get_value(
"Serial and Batch Entry",
{"parent": se.items[0].serial_and_batch_bundle, "docstatus": 1},
["stock_value_difference", "stock_queue"],
as_dict=True,
)
self.assertEqual(flt(sle.stock_value_difference), 1000.00)
self.assertEqual(json.loads(sle.stock_queue), [[20, 200], [10, 100]])
se = make_stock_entry(
item_code=batch_item_code,
target="_Test Warehouse - _TC",
@@ -301,7 +320,7 @@ class TestSerialandBatchBundle(FrappeTestCase):
)
self.assertEqual(flt(sle.stock_value_difference), 1000.00)
self.assertEqual(json.loads(sle.stock_queue), [[20, 200]])
self.assertEqual(json.loads(sle.stock_queue), [[20, 200], [10, 100]])
se = make_stock_entry(
item_code=batch_item_code,
@@ -319,6 +338,24 @@ class TestSerialandBatchBundle(FrappeTestCase):
self.assertEqual(flt(sle.stock_value_difference), 5000.00 * -1)
self.assertFalse(json.loads(sle.stock_queue or "[]"))
self.assertEqual(flt(sle.stock_value), 1000.0)
se = make_stock_entry(
item_code=batch_item_code,
source="_Test Warehouse - _TC",
qty=10,
use_serial_batch_fields=False,
)
sle = frappe.db.get_value(
"Stock Ledger Entry",
{"item_code": batch_item_code, "is_cancelled": 0, "voucher_no": se.name},
["stock_value_difference", "stock_queue", "stock_value"],
as_dict=True,
)
self.assertEqual(flt(sle.stock_value_difference), 1000.00 * -1)
self.assertFalse(json.loads(sle.stock_queue or "[]"))
self.assertEqual(flt(sle.stock_value), 0.0)
def test_old_serial_no_valuation(self):

View File

@@ -2041,6 +2041,9 @@ class StockEntry(StockController):
# in case of BOM
to_warehouse = item.get("default_warehouse")
expense_account = item.get("expense_account")
if not expense_account:
expense_account = frappe.get_cached_value("Company", self.company, "stock_adjustment_account")
args = {
"to_warehouse": to_warehouse,
"from_warehouse": "",
@@ -2048,7 +2051,7 @@ class StockEntry(StockController):
"item_name": item.item_name,
"description": item.description,
"stock_uom": item.stock_uom,
"expense_account": item.get("expense_account"),
"expense_account": expense_account,
"cost_center": item.get("buying_cost_center"),
"is_finished_item": 1,
}

View File

@@ -84,7 +84,7 @@ def get_reposting_entries():
def get_stock_ledgers(vouchers):
return frappe.get_all(
"Stock Ledger Entry",
fields=["item_code", "warehouse", "posting_date"],
fields=["item_code", "warehouse", "posting_date", "posting_time", "posting_datetime"],
filters={"voucher_no": ("in", vouchers)},
)

View File

@@ -260,10 +260,13 @@ def filter_batches(batches, doc):
del batches[row.get("batch_no")]
def get_filtered_serial_nos(serial_nos, doc):
def get_filtered_serial_nos(serial_nos, doc, table=None):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
for row in doc.get("items"):
if not table:
table = "items"
for row in doc.get(table):
if row.get("serial_no"):
for serial_no in get_serial_nos(row.get("serial_no")):
if serial_no in serial_nos:

View File

@@ -4,10 +4,11 @@
import frappe
from frappe import _
from frappe.utils import get_link_to_form, parse_json
from frappe.utils import get_datetime, get_link_to_form, parse_json
import erpnext
from erpnext.accounts.utils import get_currency_precision, get_stock_accounts
from erpnext.stock.doctype.stock_reposting_settings.stock_reposting_settings import get_stock_ledgers
from erpnext.stock.doctype.warehouse.warehouse import get_warehouses_based_on_account
@@ -141,18 +142,29 @@ def create_reposting_entries(rows, company):
rows = parse_json(rows)
entries = []
for row in rows:
row = frappe._dict(row)
item_wh = frappe._dict()
vouchers = [row.get("voucher_no") for row in rows]
sles = get_stock_ledgers(vouchers)
for sle in sles:
key = (sle.item_code, sle.warehouse)
if key not in item_wh:
item_wh[key] = sle
elif get_datetime(item_wh.get(key).posting_datetime) > get_datetime(sle.posting_datetime):
item_wh[key] = sle
for key, sle in item_wh.items():
item_code, warehouse = key
try:
doc = frappe.get_doc(
{
"doctype": "Repost Item Valuation",
"based_on": "Transaction",
"based_on": "Item and Warehouse",
"status": "Queued",
"voucher_type": row.voucher_type,
"voucher_no": row.voucher_no,
"posting_date": row.posting_date,
"item_code": item_code,
"warehouse": warehouse,
"posting_date": sle.posting_date,
"posting_time": sle.posting_time,
"company": company,
"allow_nagative_stock": 1,
}

View File

@@ -732,7 +732,6 @@ class update_entries_after:
elif dependant_sle.voucher_type == "Stock Entry" and is_transfer_stock_entry(
dependant_sle.voucher_no
):
print(dependant_sle.voucher_no)
self.distinct_item_warehouses[key] = val
self.new_items_found = True
@@ -1020,7 +1019,9 @@ class update_entries_after:
)
else:
doc = frappe.get_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
doc.set_incoming_rate(save=True, allow_negative_stock=self.allow_negative_stock)
doc.set_incoming_rate(
save=True, allow_negative_stock=self.allow_negative_stock, prev_sle=self.wh_data
)
doc.calculate_qty_and_amount(save=True)
if stock_queue := frappe.get_all(

View File

@@ -252,14 +252,18 @@ class SubcontractingOrder(SubcontractingController):
if si.fg_item:
item = frappe.get_doc("Item", si.fg_item)
po_item = frappe.get_doc("Purchase Order Item", si.purchase_order_item)
available_qty = po_item.qty - po_item.subcontracted_quantity
qty, subcontracted_quantity, fg_item_qty = frappe.db.get_value(
"Purchase Order Item",
si.purchase_order_item,
["qty", "subcontracted_quantity", "fg_item_qty"],
)
available_qty = flt(qty) - flt(subcontracted_quantity)
if available_qty == 0:
continue
si.qty = available_qty
conversion_factor = po_item.qty / po_item.fg_item_qty
conversion_factor = flt(qty) / flt(fg_item_qty)
si.fg_item_qty = flt(
available_qty / conversion_factor, frappe.get_precision("Purchase Order Item", "qty")
)
@@ -338,13 +342,24 @@ class SubcontractingOrder(SubcontractingController):
def update_subcontracted_quantity_in_po(self, cancel=False):
for service_item in self.service_items:
doc = frappe.get_doc("Purchase Order Item", service_item.purchase_order_item)
doc.subcontracted_quantity = (
(doc.subcontracted_quantity + service_item.qty)
if not cancel
else (doc.subcontracted_quantity - service_item.qty)
subcontracted_quantity = flt(
frappe.db.get_value(
"Purchase Order Item", service_item.purchase_order_item, "subcontracted_quantity"
)
)
subcontracted_quantity = (
(subcontracted_quantity + service_item.qty)
if not cancel
else (subcontracted_quantity - service_item.qty)
)
frappe.db.set_value(
"Purchase Order Item",
service_item.purchase_order_item,
"subcontracted_quantity",
subcontracted_quantity,
)
doc.save()
@frappe.whitelist()

View File

@@ -116,7 +116,13 @@ class SubcontractingReceipt(SubcontractingController):
self.validate_items_qty()
self.set_items_bom()
self.set_items_cost_center()
self.set_items_expense_account()
if self.company:
default_expense_account = self.get_company_default(
"default_expense_account", ignore_validation=True
)
self.set_service_expense_account(default_expense_account)
self.set_expense_account_for_subcontracted_items(default_expense_account)
def validate(self):
self.reset_supplied_items()
@@ -196,6 +202,39 @@ class SubcontractingReceipt(SubcontractingController):
if item.subcontracting_order:
check_on_hold_or_closed_status("Subcontracting Order", item.subcontracting_order)
def set_service_expense_account(self, default_expense_account):
for row in self.get("items"):
if not row.service_expense_account and row.purchase_order_item:
service_item = frappe.db.get_value(
"Purchase Order Item", row.purchase_order_item, "item_code"
)
if service_item:
if default := (
get_item_defaults(service_item, self.company)
or get_item_group_defaults(service_item, self.company)
or get_brand_defaults(service_item, self.company)
):
if service_expense_account := default.get("expense_account"):
row.service_expense_account = service_expense_account
if not row.service_expense_account:
row.service_expense_account = default_expense_account
def set_expense_account_for_subcontracted_items(self, default_expense_account):
for row in self.get("items"):
if not row.expense_account:
if default := (
get_item_defaults(row.item_code, self.company)
or get_item_group_defaults(row.item_code, self.company)
or get_brand_defaults(row.item_code, self.company)
):
if expense_account := default.get("expense_account"):
row.expense_account = expense_account
if not row.expense_account:
row.expense_account = default_expense_account
def validate_items_qty(self):
for item in self.items:
if not (item.qty or item.rejected_qty):
@@ -242,14 +281,6 @@ class SubcontractingReceipt(SubcontractingController):
self.company,
)
def set_items_expense_account(self):
if self.company:
expense_account = self.get_company_default("default_expense_account", ignore_validation=True)
for item in self.items:
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:
@@ -599,13 +630,17 @@ class SubcontractingReceipt(SubcontractingController):
project=item.project,
item=item,
)
service_cost = flt(
item.service_cost_per_qty, item.precision("service_cost_per_qty")
) * flt(item.qty, item.precision("qty"))
# Expense Account (Credit)
self.add_gl_entry(
gl_entries=gl_entries,
account=item.expense_account,
cost_center=item.cost_center,
debit=0.0,
credit=stock_value_diff,
credit=flt(stock_value_diff) - service_cost,
remarks=remarks,
against_account=accepted_warehouse_account,
account_currency=get_account_currency(item.expense_account),
@@ -613,6 +648,21 @@ class SubcontractingReceipt(SubcontractingController):
item=item,
)
service_account = item.service_expense_account or item.expense_account
# Expense Account (Credit)
self.add_gl_entry(
gl_entries=gl_entries,
account=service_account,
cost_center=item.cost_center,
debit=0.0,
credit=service_cost,
remarks=remarks,
against_account=accepted_warehouse_account,
account_currency=get_account_currency(service_account),
project=item.project,
item=item,
)
if flt(item.rm_supp_cost) and supplier_warehouse_account:
for rm_item in supplied_items_details.get(item.name):
# Supplier Warehouse Account (Credit)

View File

@@ -422,6 +422,79 @@ class TestSubcontractingReceipt(FrappeTestCase):
self.assertEqual(expected_values[gle.account][0], gle.debit)
self.assertEqual(expected_values[gle.account][1], gle.credit)
def test_subcontracting_receipt_for_service_expense_account(self):
service_expense_account = (
frappe.get_doc(
{
"doctype": "Account",
"account_name": "_Test Service Expense",
"account_type": "Expense Account",
"company": "_Test Company with perpetual inventory",
"is_group": 0,
"parent_account": "Indirect Expenses - TCP1",
}
)
.insert(ignore_if_duplicate=True)
.name
)
service_item_doc = frappe.get_doc("Item", "Subcontracted Service Item 10")
service_item_doc.append(
"item_defaults",
{
"company": "_Test Company with perpetual inventory",
"expense_account": service_expense_account,
"default_warehouse": "Stores - TCP1",
},
)
service_item_doc.save()
service_items = [
{
"warehouse": "Stores - TCP1",
"item_code": "Subcontracted Service Item 10",
"qty": 10,
"rate": 100,
"fg_item": "Subcontracted Item SA10",
"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.submit()
for item in scr.items:
self.assertEqual(item.service_expense_account, service_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: [2000, 1000],
expense_account: [1000, 1000],
service_expense_account: [0, 1000],
}
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"
@@ -740,13 +813,13 @@ class TestSubcontractingReceipt(FrappeTestCase):
for row in scr.supplied_items:
self.assertEqual(row.rate, 300.00)
self.assertTrue(row.serial_and_batch_bundle)
auto_created_serial_batch = frappe.db.get_value(
serial_and_batch_bundle = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_no": scr.name, "voucher_detail_no": row.name},
"auto_created_serial_and_batch_bundle",
"serial_and_batch_bundle",
)
self.assertTrue(auto_created_serial_batch)
self.assertTrue(serial_and_batch_bundle)
self.assertEqual(scr.items[0].rm_cost_per_qty, 900)
self.assertEqual(scr.items[0].service_cost_per_qty, 100)

View File

@@ -64,6 +64,8 @@
"manufacturer_part_no",
"accounting_details_section",
"expense_account",
"column_break_exht",
"service_expense_account",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
@@ -580,12 +582,22 @@
"fieldname": "add_serial_batch_for_rejected_qty",
"fieldtype": "Button",
"label": "Add Serial / Batch No (Rejected Qty)"
},
{
"fieldname": "column_break_exht",
"fieldtype": "Column Break"
},
{
"fieldname": "service_expense_account",
"fieldtype": "Link",
"label": "Service Expense Account",
"options": "Account"
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2024-12-06 15:23:58.680169",
"modified": "2025-09-26 12:00:38.877638",
"modified_by": "Administrator",
"module": "Subcontracting",
"name": "Subcontracting Receipt Item",

View File

@@ -55,6 +55,7 @@ class SubcontractingReceiptItem(Document):
serial_and_batch_bundle: DF.Link | None
serial_no: DF.SmallText | None
service_cost_per_qty: DF.Currency
service_expense_account: DF.Link | None
stock_uom: DF.Link
subcontracting_order: DF.Link | None
subcontracting_order_item: DF.Data | None