mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-14 12:25:09 +00:00
Merge pull request #49801 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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)},
|
||||
)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user