mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-24 17:18:30 +00:00
Merge pull request #50249 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -123,8 +123,7 @@
|
||||
"fieldname": "transaction_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Transaction ID",
|
||||
"read_only": 1,
|
||||
"unique": 1
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
@@ -239,7 +238,7 @@
|
||||
"grid_page_length": 50,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-14 11:53:45.908169",
|
||||
"modified": "2025-10-23 17:32:58.514807",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Transaction",
|
||||
|
||||
@@ -99,7 +99,7 @@ class FiscalYear(Document):
|
||||
)
|
||||
|
||||
overlap = False
|
||||
if not self.get("companies") or not company_for_existing:
|
||||
if not self.get("companies") and not company_for_existing:
|
||||
overlap = True
|
||||
|
||||
for d in self.get("companies"):
|
||||
|
||||
@@ -26,6 +26,27 @@ class TestFiscalYear(unittest.TestCase):
|
||||
|
||||
self.assertRaises(frappe.exceptions.InvalidDates, fy.insert)
|
||||
|
||||
def test_company_fiscal_year_overlap(self):
|
||||
for name in ["_Test Global FY 2001", "_Test Company FY 2001"]:
|
||||
if frappe.db.exists("Fiscal Year", name):
|
||||
frappe.delete_doc("Fiscal Year", name)
|
||||
|
||||
global_fy = frappe.new_doc("Fiscal Year")
|
||||
global_fy.year = "_Test Global FY 2001"
|
||||
global_fy.year_start_date = "2001-04-01"
|
||||
global_fy.year_end_date = "2002-03-31"
|
||||
global_fy.insert()
|
||||
|
||||
company_fy = frappe.new_doc("Fiscal Year")
|
||||
company_fy.year = "_Test Company FY 2001"
|
||||
company_fy.year_start_date = "2001-01-01"
|
||||
company_fy.year_end_date = "2001-12-31"
|
||||
company_fy.append("companies", {"company": "_Test Company"})
|
||||
|
||||
company_fy.insert()
|
||||
self.assertTrue(frappe.db.exists("Fiscal Year", global_fy.name))
|
||||
self.assertTrue(frappe.db.exists("Fiscal Year", company_fy.name))
|
||||
|
||||
|
||||
def test_record_generator():
|
||||
test_records = [
|
||||
|
||||
@@ -106,7 +106,6 @@
|
||||
"fieldname": "account_currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Account Currency",
|
||||
"no_copy": 1,
|
||||
"options": "Currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
@@ -288,7 +287,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-20 17:46:47.344089",
|
||||
"modified": "2025-10-27 13:48:32.805100",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry Account",
|
||||
|
||||
@@ -385,6 +385,16 @@ frappe.ui.form.on("Payment Reconciliation Allocation", {
|
||||
// filter payment
|
||||
let payment = frm.doc.payments.filter((x) => x.reference_name == row.reference_name);
|
||||
|
||||
let amount = payment[0].amount;
|
||||
for (const d of frm.doc.allocation) {
|
||||
if (row.reference_name == d.reference_name && amount) {
|
||||
if (d.allocated_amount <= amount) {
|
||||
d.amount = amount;
|
||||
amount -= d.allocated_amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
frm.call({
|
||||
doc: frm.doc,
|
||||
method: "calculate_difference_on_allocation_change",
|
||||
|
||||
@@ -648,6 +648,7 @@ def make_reverse_gl_entries(
|
||||
adv_adj=False,
|
||||
update_outstanding="Yes",
|
||||
partial_cancel=False,
|
||||
posting_date=None,
|
||||
):
|
||||
"""
|
||||
Get original gl entries of the voucher
|
||||
@@ -745,6 +746,8 @@ def make_reverse_gl_entries(
|
||||
if immutable_ledger_enabled:
|
||||
new_gle["is_cancelled"] = 0
|
||||
new_gle["posting_date"] = frappe.form_dict.get("posting_date") or getdate()
|
||||
elif posting_date:
|
||||
new_gle["posting_date"] = posting_date
|
||||
|
||||
if new_gle["debit"] or new_gle["credit"]:
|
||||
make_entry(new_gle, adv_adj, "Yes")
|
||||
|
||||
@@ -273,6 +273,7 @@ def get_asset_value_adjustment_map_by_category(filters):
|
||||
AND a.company = %(company)s
|
||||
AND a.purchase_date <= %(to_date)s
|
||||
AND gle.account = aca.fixed_asset_account
|
||||
AND gle.is_opening = 'No'
|
||||
GROUP BY a.asset_category
|
||||
""",
|
||||
{"from_date": filters.from_date, "to_date": filters.to_date, "company": filters.company},
|
||||
@@ -543,6 +544,7 @@ def get_asset_value_adjustment_map(filters):
|
||||
AND a.company = %(company)s
|
||||
AND a.purchase_date <= %(to_date)s
|
||||
AND gle.account = aca.fixed_asset_account
|
||||
AND gle.is_opening = 'No'
|
||||
GROUP BY a.name
|
||||
""",
|
||||
{"from_date": filters.from_date, "to_date": filters.to_date, "company": filters.company},
|
||||
|
||||
@@ -231,6 +231,15 @@ def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_
|
||||
|
||||
group_columns = group_wise_columns.get(scrub(filters.group_by))
|
||||
|
||||
# removing customer_name from group columns
|
||||
customer_master_name = frappe.db.get_single_value("Selling Settings", "cust_master_name")
|
||||
supplier_master_name = frappe.db.get_single_value("Buying Settings", "supp_master_name")
|
||||
|
||||
if "customer_name" in group_columns and (
|
||||
supplier_master_name == "Supplier Name" and customer_master_name == "Customer Name"
|
||||
):
|
||||
group_columns = [col for col in group_columns if col != "customer_name"]
|
||||
|
||||
for src in gross_profit_data.grouped_data:
|
||||
total_base_amount += src.base_amount or 0.00
|
||||
total_buying_amount += src.buying_amount or 0.00
|
||||
|
||||
@@ -518,6 +518,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.docstatus > 0",
|
||||
"fieldname": "additional_asset_cost",
|
||||
"fieldtype": "Currency",
|
||||
@@ -596,7 +597,7 @@
|
||||
"link_fieldname": "target_asset"
|
||||
}
|
||||
],
|
||||
"modified": "2025-05-20 00:44:06.229177",
|
||||
"modified": "2025-10-23 22:43:33.634452",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
@@ -641,4 +642,4 @@
|
||||
"states": [],
|
||||
"title_field": "asset_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,7 @@ class Asset(AccountsController):
|
||||
default_finance_book: DF.Link | None
|
||||
department: DF.Link | None
|
||||
depr_entry_posting_status: DF.Literal["", "Successful", "Failed"]
|
||||
depreciation_completed: DF.Check
|
||||
depreciation_method: DF.Literal["", "Straight Line", "Double Declining Balance", "Manual"]
|
||||
disposal_date: DF.Date | None
|
||||
finance_books: DF.Table[AssetFinanceBook]
|
||||
@@ -152,7 +153,9 @@ class Asset(AccountsController):
|
||||
)
|
||||
self.validate_expected_value_after_useful_life()
|
||||
self.set_total_booked_depreciations()
|
||||
self.total_asset_cost = self.gross_purchase_amount
|
||||
|
||||
def before_save(self):
|
||||
self.total_asset_cost = self.gross_purchase_amount + self.additional_asset_cost
|
||||
self.status = self.get_status()
|
||||
|
||||
def on_submit(self):
|
||||
|
||||
@@ -878,6 +878,7 @@ class BuyingController(SubcontractingController):
|
||||
"asset_category": item_data.get("asset_category"),
|
||||
"location": row.asset_location,
|
||||
"company": self.company,
|
||||
"status": "Draft",
|
||||
"supplier": self.supplier,
|
||||
"purchase_date": self.posting_date,
|
||||
"calculate_depreciation": 0,
|
||||
|
||||
@@ -724,6 +724,9 @@ class ProductionPlan(Document):
|
||||
if not wo_list:
|
||||
frappe.msgprint(_("No Work Orders were created"))
|
||||
|
||||
if not po_list:
|
||||
frappe.msgprint(_("No Purchase Orders were created"))
|
||||
|
||||
def make_work_order_for_finished_goods(self, wo_list, default_warehouses):
|
||||
items_data = self.get_production_items()
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ def get_exploded_items(bom, data, indent=0, qty=1):
|
||||
)
|
||||
|
||||
for item in exploded_items:
|
||||
print(item.bom_no, indent)
|
||||
item["indent"] = indent
|
||||
data.append(
|
||||
{
|
||||
|
||||
@@ -422,3 +422,4 @@ execute:frappe.db.set_single_value("Accounts Settings", "fetch_valuation_rate_fo
|
||||
erpnext.patches.v15_0.add_company_payment_gateway_account
|
||||
erpnext.patches.v15_0.update_uae_zero_rated_fetch
|
||||
erpnext.patches.v15_0.update_fieldname_in_accounting_dimension_filter
|
||||
erpnext.patches.v15_0.set_asset_status_if_not_already_set
|
||||
|
||||
13
erpnext/patches/v15_0/set_asset_status_if_not_already_set.py
Normal file
13
erpnext/patches/v15_0/set_asset_status_if_not_already_set.py
Normal file
@@ -0,0 +1,13 @@
|
||||
import frappe
|
||||
from frappe.query_builder import DocType
|
||||
|
||||
|
||||
def execute():
|
||||
Asset = DocType("Asset")
|
||||
|
||||
query = (
|
||||
frappe.qb.update(Asset)
|
||||
.set(Asset.status, "Draft")
|
||||
.where((Asset.docstatus == 0) & ((Asset.status.isnull()) | (Asset.status == "")))
|
||||
)
|
||||
query.run()
|
||||
@@ -245,6 +245,7 @@
|
||||
"fieldname": "act_start_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Actual Start Date (via Timesheet)",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "act_start_date",
|
||||
"oldfieldtype": "Date",
|
||||
"read_only": 1
|
||||
@@ -253,6 +254,7 @@
|
||||
"fieldname": "actual_time",
|
||||
"fieldtype": "Float",
|
||||
"label": "Actual Time in Hours (via Timesheet)",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -263,6 +265,7 @@
|
||||
"fieldname": "act_end_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Actual End Date (via Timesheet)",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "act_end_date",
|
||||
"oldfieldtype": "Date",
|
||||
"read_only": 1
|
||||
@@ -277,6 +280,7 @@
|
||||
"fieldname": "total_costing_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Costing Amount (via Timesheet)",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "actual_budget",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "Company:company:default_currency",
|
||||
@@ -290,6 +294,7 @@
|
||||
"fieldname": "total_billing_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Billable Amount (via Timesheet)",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -399,7 +404,7 @@
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"max_attachments": 5,
|
||||
"modified": "2024-01-08 16:00:41.296203",
|
||||
"modified": "2025-10-16 08:39:12.214577",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Projects",
|
||||
"name": "Task",
|
||||
@@ -429,4 +434,4 @@
|
||||
"timeline_field": "project",
|
||||
"title_field": "subject",
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
|
||||
frappe.query_reports["Delayed Tasks Summary"] = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "project",
|
||||
label: __("Project"),
|
||||
fieldtype: "Link",
|
||||
options: "Project",
|
||||
},
|
||||
{
|
||||
fieldname: "from_date",
|
||||
label: __("From Date"),
|
||||
|
||||
@@ -23,6 +23,7 @@ def get_data(filters):
|
||||
fields=[
|
||||
"name",
|
||||
"subject",
|
||||
"project",
|
||||
"exp_start_date",
|
||||
"exp_end_date",
|
||||
"status",
|
||||
@@ -56,7 +57,7 @@ def get_data(filters):
|
||||
|
||||
def get_conditions(filters):
|
||||
conditions = frappe._dict()
|
||||
keys = ["priority", "status"]
|
||||
keys = ["priority", "status", "project"]
|
||||
for key in keys:
|
||||
if filters.get(key):
|
||||
conditions[key] = filters.get(key)
|
||||
@@ -89,6 +90,13 @@ def get_columns():
|
||||
columns = [
|
||||
{"fieldname": "name", "fieldtype": "Link", "label": _("Task"), "options": "Task", "width": 150},
|
||||
{"fieldname": "subject", "fieldtype": "Data", "label": _("Subject"), "width": 200},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": _("Project"),
|
||||
"options": "Project",
|
||||
"width": 150,
|
||||
},
|
||||
{"fieldname": "status", "fieldtype": "Data", "label": _("Status"), "width": 100},
|
||||
{"fieldname": "priority", "fieldtype": "Data", "label": _("Priority"), "width": 80},
|
||||
{"fieldname": "progress", "fieldtype": "Data", "label": _("Progress (%)"), "width": 120},
|
||||
|
||||
@@ -237,10 +237,10 @@ erpnext.accounts.pos = {
|
||||
frappe.ui.form.on(doctype, {
|
||||
mode_of_payment: function(frm, cdt, cdn) {
|
||||
var d = locals[cdt][cdn];
|
||||
get_payment_mode_account(frm, d.mode_of_payment, function(account){
|
||||
frappe.model.set_value(cdt, cdn, 'account', account)
|
||||
})
|
||||
}
|
||||
erpnext.accounts.pos.get_payment_mode_account(frm, d.mode_of_payment, function (account) {
|
||||
frappe.model.set_value(cdt, cdn, "account", account);
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ frappe.ui.form.on("Driver", {
|
||||
|
||||
transporter: function (frm, cdt, cdn) {
|
||||
// this assumes that supplier's address has same title as supplier's name
|
||||
if (!frm.doc.transporter) return;
|
||||
frappe.db
|
||||
.get_doc("Address", null, { address_title: frm.doc.transporter })
|
||||
.then((r) => {
|
||||
|
||||
@@ -539,7 +539,7 @@ frappe.ui.form.on("Stock Entry", {
|
||||
const item = locals[cdt][cdn];
|
||||
item.transfer_qty = flt(item.qty) * flt(item.conversion_factor);
|
||||
|
||||
const args = {
|
||||
let args = {
|
||||
item_code: item.item_code,
|
||||
posting_date: frm.doc.posting_date,
|
||||
posting_time: frm.doc.posting_time,
|
||||
@@ -553,6 +553,10 @@ frappe.ui.form.on("Stock Entry", {
|
||||
allow_zero_valuation: 1,
|
||||
};
|
||||
|
||||
if (item.batch_no && frm.doc.purpose == "Material Receipt") {
|
||||
args.qty = Math.abs(args.qty) * -1;
|
||||
}
|
||||
|
||||
if (item.item_code || item.serial_no) {
|
||||
frappe.call({
|
||||
method: "erpnext.stock.utils.get_incoming_rate",
|
||||
|
||||
@@ -7,7 +7,10 @@ frappe.provide("erpnext.accounts.dimensions");
|
||||
frappe.ui.form.on("Stock Reconciliation", {
|
||||
setup(frm) {
|
||||
frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"];
|
||||
frm.barcode_scanner = new erpnext.utils.BarcodeScanner({ frm });
|
||||
frm.barcode_scanner = new erpnext.utils.BarcodeScanner({
|
||||
frm: frm,
|
||||
uom_field: "stock_uom",
|
||||
});
|
||||
},
|
||||
|
||||
onload: function (frm) {
|
||||
|
||||
@@ -24,24 +24,26 @@ frappe.query_reports["Incorrect Serial and Batch Bundle"] = {
|
||||
},
|
||||
|
||||
onload(report) {
|
||||
report.page.add_inner_button(__("Remove SABB Entry"), () => {
|
||||
let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows();
|
||||
let selected_rows = indexes.map((i) => frappe.query_report.data[i]);
|
||||
report.page
|
||||
.add_inner_button(__("Fix SABB Entry"), () => {
|
||||
let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows();
|
||||
let selected_rows = indexes.map((i) => frappe.query_report.data[i]);
|
||||
|
||||
if (!selected_rows.length) {
|
||||
frappe.throw(__("Please select a row to create a Reposting Entry"));
|
||||
} else {
|
||||
frappe.call({
|
||||
method: "erpnext.stock.report.incorrect_serial_and_batch_bundle.incorrect_serial_and_batch_bundle.remove_sabb_entry",
|
||||
freeze: true,
|
||||
args: {
|
||||
selected_rows: selected_rows,
|
||||
},
|
||||
callback: function (r) {
|
||||
frappe.query_report.refresh();
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
if (!selected_rows.length) {
|
||||
frappe.throw(__("Please select at least one row to fix"));
|
||||
} else {
|
||||
frappe.call({
|
||||
method: "erpnext.stock.report.incorrect_serial_and_batch_bundle.incorrect_serial_and_batch_bundle.fix_sabb_entries",
|
||||
freeze: true,
|
||||
args: {
|
||||
selected_rows: selected_rows,
|
||||
},
|
||||
callback: function (r) {
|
||||
frappe.query_report.refresh();
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
.addClass("btn-primary");
|
||||
},
|
||||
};
|
||||
|
||||
@@ -13,7 +13,10 @@ def execute(filters: dict | None = None):
|
||||
every time the report is refreshed or a filter is updated.
|
||||
"""
|
||||
columns = get_columns()
|
||||
data = get_data(filters)
|
||||
unlinked_bundles = get_unlinked_serial_batch_bundles(filters) or []
|
||||
linked_cancelled_bundles = get_linked_cancelled_sabb(filters) or []
|
||||
|
||||
data = unlinked_bundles + linked_cancelled_bundles
|
||||
|
||||
return columns, data
|
||||
|
||||
@@ -50,14 +53,17 @@ def get_columns() -> list[dict]:
|
||||
"fieldtype": "Data",
|
||||
"width": 200,
|
||||
},
|
||||
{
|
||||
"label": _("Is Cancelled"),
|
||||
"fieldname": "is_cancelled",
|
||||
"fieldtype": "Check",
|
||||
"width": 200,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def get_data(filters) -> list[list]:
|
||||
"""Return data for the report.
|
||||
|
||||
The report data is a list of rows, with each row being a list of cell values.
|
||||
"""
|
||||
def get_unlinked_serial_batch_bundles(filters) -> list[list]:
|
||||
# SABB has not been linked to any SLE
|
||||
|
||||
SABB = frappe.qb.DocType("Serial and Batch Bundle")
|
||||
SLE = frappe.qb.DocType("Stock Ledger Entry")
|
||||
@@ -77,6 +83,7 @@ def get_data(filters) -> list[list]:
|
||||
SABB.voucher_type,
|
||||
SABB.voucher_no,
|
||||
SABB.voucher_detail_no,
|
||||
SABB.is_cancelled,
|
||||
)
|
||||
.where(
|
||||
(SLE.serial_and_batch_bundle.isnull())
|
||||
@@ -94,14 +101,63 @@ def get_data(filters) -> list[list]:
|
||||
return data
|
||||
|
||||
|
||||
def get_linked_cancelled_sabb(filters):
|
||||
# SABB has cancelled but voucher is not cancelled
|
||||
|
||||
SABB = frappe.qb.DocType("Serial and Batch Bundle")
|
||||
SLE = frappe.qb.DocType("Stock Ledger Entry")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(SABB)
|
||||
.inner_join(SLE)
|
||||
.on(SABB.name == SLE.serial_and_batch_bundle)
|
||||
.select(
|
||||
SABB.name,
|
||||
SABB.voucher_type,
|
||||
SABB.voucher_no,
|
||||
SABB.voucher_detail_no,
|
||||
SABB.is_cancelled,
|
||||
)
|
||||
.where(
|
||||
(SLE.serial_and_batch_bundle.isnotnull())
|
||||
& (SABB.docstatus == 2)
|
||||
& (SABB.is_cancelled == 1)
|
||||
& (SLE.is_cancelled == 0)
|
||||
)
|
||||
)
|
||||
|
||||
for field in filters:
|
||||
query = query.where(SABB[field] == filters[field])
|
||||
|
||||
data = query.run(as_dict=1)
|
||||
return data
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def remove_sabb_entry(selected_rows):
|
||||
def fix_sabb_entries(selected_rows):
|
||||
if isinstance(selected_rows, str):
|
||||
selected_rows = frappe.parse_json(selected_rows)
|
||||
|
||||
for row in selected_rows:
|
||||
doc = frappe.get_doc("Serial and Batch Bundle", row.get("name"))
|
||||
doc.cancel()
|
||||
doc.delete()
|
||||
if doc.is_cancelled == 0 and not frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"serial_and_batch_bundle": doc.name, "is_cancelled": 0},
|
||||
"name",
|
||||
):
|
||||
doc.db_set({"is_cancelled": 1, "docstatus": 2})
|
||||
|
||||
frappe.msgprint(_("Selected Serial and Batch Bundle entries have been removed."))
|
||||
for row in doc.entries:
|
||||
row.db_set("docstatus", 2)
|
||||
|
||||
elif doc.is_cancelled == 1 and frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"serial_and_batch_bundle": doc.name, "is_cancelled": 0},
|
||||
"name",
|
||||
):
|
||||
doc.db_set({"is_cancelled": 0, "docstatus": 1})
|
||||
|
||||
for row in doc.entries:
|
||||
row.db_set("docstatus", 1)
|
||||
|
||||
frappe.msgprint(_("Selected Serial and Batch Bundle entries have been fixed."))
|
||||
|
||||
@@ -18,16 +18,41 @@ def execute(filters=None):
|
||||
|
||||
def get_data(filters):
|
||||
data = get_stock_ledger_entries(filters)
|
||||
serial_nos_data = prepare_serial_nos(data)
|
||||
bundles = get_bundles(data)
|
||||
serial_nos_data = prepare_serial_nos(data, bundles)
|
||||
data = get_incorrect_serial_nos(serial_nos_data)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def prepare_serial_nos(data):
|
||||
def get_bundles(data):
|
||||
bundles = [d.serial_and_batch_bundle for d in data if d.serial_and_batch_bundle]
|
||||
bundle_dict = frappe._dict()
|
||||
serial_nos_data = frappe.get_all(
|
||||
"Serial and Batch Entry",
|
||||
fields=["parent", "serial_no", "incoming_rate", "qty"],
|
||||
filters={"parent": ("in", bundles), "serial_no": ("is", "set")},
|
||||
)
|
||||
|
||||
for entry in serial_nos_data:
|
||||
bundle_dict.setdefault(entry.parent, []).append(entry)
|
||||
|
||||
return bundle_dict
|
||||
|
||||
|
||||
def prepare_serial_nos(data, bundles):
|
||||
serial_no_wise_data = {}
|
||||
for row in data:
|
||||
if not row.serial_nos:
|
||||
if not row.serial_nos and not row.serial_and_batch_bundle:
|
||||
continue
|
||||
|
||||
if row.serial_and_batch_bundle:
|
||||
for bundle in bundles.get(row.serial_and_batch_bundle, []):
|
||||
sle = copy.deepcopy(row)
|
||||
sle.serial_no = bundle.serial_no
|
||||
sle.qty = bundle.qty
|
||||
sle.valuation_rate = bundle.incoming_rate * (1 if sle.qty > 0 else -1)
|
||||
serial_no_wise_data.setdefault(bundle.serial_no, []).append(sle)
|
||||
continue
|
||||
|
||||
for serial_no in get_serial_nos(row.serial_nos):
|
||||
@@ -54,6 +79,9 @@ def get_incorrect_serial_nos(serial_nos_data):
|
||||
total_value.qty += total_dict.qty
|
||||
total_value.valuation_rate += total_dict.valuation_rate
|
||||
|
||||
if total_dict.qty == 0 and abs(total_dict.valuation_rate) == 0:
|
||||
continue
|
||||
|
||||
result.append(total_dict)
|
||||
result.append({})
|
||||
|
||||
@@ -81,6 +109,7 @@ def get_stock_ledger_entries(report_filters):
|
||||
"voucher_no",
|
||||
"item_code",
|
||||
"serial_no as serial_nos",
|
||||
"serial_and_batch_bundle",
|
||||
"actual_qty",
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
@@ -89,7 +118,8 @@ def get_stock_ledger_entries(report_filters):
|
||||
"(stock_value_difference / actual_qty) as valuation_rate",
|
||||
]
|
||||
|
||||
filters = {"serial_no": ("is", "set"), "is_cancelled": 0}
|
||||
filters = {"is_cancelled": 0}
|
||||
or_filters = {"serial_no": ("is", "set"), "serial_and_batch_bundle": ("is", "set")}
|
||||
|
||||
if report_filters.get("item_code"):
|
||||
filters["item_code"] = report_filters.get("item_code")
|
||||
@@ -104,6 +134,7 @@ def get_stock_ledger_entries(report_filters):
|
||||
"Stock Ledger Entry",
|
||||
fields=fields,
|
||||
filters=filters,
|
||||
or_filters=or_filters,
|
||||
order_by="posting_date asc, posting_time asc, creation asc",
|
||||
)
|
||||
|
||||
|
||||
@@ -731,13 +731,55 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
|
||||
for ledger in entries:
|
||||
self.stock_value_differece[ledger.batch_no] += flt(ledger.incoming_rate)
|
||||
self.available_qty[ledger.batch_no] += flt(ledger.qty)
|
||||
self.total_qty[ledger.batch_no] += flt(ledger.total_qty)
|
||||
|
||||
entries = self.get_batch_wise_total_available_qty()
|
||||
for row in entries:
|
||||
self.total_qty[row.batch_no] += flt(row.total_qty)
|
||||
|
||||
self.calculate_avg_rate_from_deprecarated_ledgers()
|
||||
self.calculate_avg_rate_for_non_batchwise_valuation()
|
||||
self.set_stock_value_difference()
|
||||
|
||||
def get_batch_wise_total_available_qty(self) -> list[dict]:
|
||||
# Get total qty of each batch no from Serial and Batch Bundle without checking time condition
|
||||
if not self.batchwise_valuation_batches:
|
||||
return []
|
||||
|
||||
parent = frappe.qb.DocType("Serial and Batch Bundle")
|
||||
child = frappe.qb.DocType("Serial and Batch Entry")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(parent)
|
||||
.inner_join(child)
|
||||
.on(parent.name == child.parent)
|
||||
.select(
|
||||
child.batch_no,
|
||||
Sum(child.qty).as_("total_qty"),
|
||||
)
|
||||
.where(
|
||||
(parent.warehouse == self.sle.warehouse)
|
||||
& (parent.item_code == self.sle.item_code)
|
||||
& (child.batch_no.isin(self.batchwise_valuation_batches))
|
||||
& (parent.docstatus == 1)
|
||||
& (parent.is_cancelled == 0)
|
||||
& (parent.type_of_transaction.isin(["Inward", "Outward"]))
|
||||
)
|
||||
.for_update()
|
||||
.groupby(child.batch_no)
|
||||
)
|
||||
|
||||
# Important to exclude the current voucher detail no / voucher no to calculate the correct stock value difference
|
||||
if self.sle.voucher_detail_no:
|
||||
query = query.where(parent.voucher_detail_no != self.sle.voucher_detail_no)
|
||||
elif self.sle.voucher_no:
|
||||
query = query.where(parent.voucher_no != self.sle.voucher_no)
|
||||
|
||||
query = query.where(parent.voucher_type != "Pick List")
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
def get_batch_no_ledgers(self) -> list[dict]:
|
||||
# Get batch wise stock value difference from Serial and Batch Bundle considering time condition
|
||||
if not self.batchwise_valuation_batches:
|
||||
return []
|
||||
|
||||
@@ -765,11 +807,8 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
|
||||
.on(parent.name == child.parent)
|
||||
.select(
|
||||
child.batch_no,
|
||||
Sum(Case().when(timestamp_condition, child.stock_value_difference).else_(0)).as_(
|
||||
"incoming_rate"
|
||||
),
|
||||
Sum(Case().when(timestamp_condition, child.qty).else_(0)).as_("qty"),
|
||||
Sum(child.qty).as_("total_qty"),
|
||||
Sum(child.stock_value_difference).as_("incoming_rate"),
|
||||
Sum(child.qty).as_("qty"),
|
||||
)
|
||||
.where(
|
||||
(parent.warehouse == self.sle.warehouse)
|
||||
@@ -790,6 +829,8 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
|
||||
query = query.where(parent.voucher_no != self.sle.voucher_no)
|
||||
|
||||
query = query.where(parent.voucher_type != "Pick List")
|
||||
if timestamp_condition:
|
||||
query = query.where(timestamp_condition)
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
@@ -893,7 +893,11 @@ class update_entries_after:
|
||||
sle.stock_queue = json.dumps(self.wh_data.stock_queue)
|
||||
|
||||
sle.stock_value_difference = stock_value_difference
|
||||
if sle.is_adjustment_entry and flt(sle.qty_after_transaction, self.flt_precision) == 0:
|
||||
if (
|
||||
sle.is_adjustment_entry
|
||||
and flt(sle.qty_after_transaction, self.flt_precision) == 0
|
||||
and flt(sle.stock_value, self.currency_precision) != 0
|
||||
):
|
||||
sle.stock_value_difference = (
|
||||
get_stock_value_difference(
|
||||
sle.item_code,
|
||||
@@ -901,6 +905,7 @@ class update_entries_after:
|
||||
sle.posting_date,
|
||||
sle.posting_time,
|
||||
voucher_detail_no=sle.voucher_detail_no,
|
||||
creation=sle.creation,
|
||||
)
|
||||
* -1
|
||||
)
|
||||
@@ -2358,7 +2363,7 @@ def is_internal_transfer(sle):
|
||||
|
||||
|
||||
def get_stock_value_difference(
|
||||
item_code, warehouse, posting_date, posting_time, voucher_no=None, voucher_detail_no=None
|
||||
item_code, warehouse, posting_date, posting_time, voucher_no=None, voucher_detail_no=None, creation=None
|
||||
):
|
||||
table = frappe.qb.DocType("Stock Ledger Entry")
|
||||
posting_datetime = get_combine_datetime(posting_date, posting_time)
|
||||
@@ -2366,12 +2371,7 @@ def get_stock_value_difference(
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(Sum(table.stock_value_difference).as_("value"))
|
||||
.where(
|
||||
(table.is_cancelled == 0)
|
||||
& (table.item_code == item_code)
|
||||
& (table.warehouse == warehouse)
|
||||
& (table.posting_datetime <= posting_datetime)
|
||||
)
|
||||
.where((table.is_cancelled == 0) & (table.item_code == item_code) & (table.warehouse == warehouse))
|
||||
)
|
||||
|
||||
if voucher_detail_no:
|
||||
@@ -2380,6 +2380,14 @@ def get_stock_value_difference(
|
||||
elif voucher_no:
|
||||
query = query.where(table.voucher_no != voucher_no)
|
||||
|
||||
if creation:
|
||||
query = query.where(
|
||||
(table.posting_datetime < posting_datetime)
|
||||
| ((table.posting_datetime == posting_datetime) & (table.creation < creation))
|
||||
)
|
||||
else:
|
||||
query = query.where(table.posting_datetime <= posting_datetime)
|
||||
|
||||
difference_amount = query.run()
|
||||
return flt(difference_amount[0][0]) if difference_amount else 0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user