diff --git a/erpnext/accounts/doctype/budget/budget.py b/erpnext/accounts/doctype/budget/budget.py index f7b2c377f3a..22b8d64971c 100644 --- a/erpnext/accounts/doctype/budget/budget.py +++ b/erpnext/accounts/doctype/budget/budget.py @@ -194,12 +194,18 @@ def validate_expense_against_budget(args, expense_amount=0): def validate_budget_records(args, budget_records, expense_amount): for budget in budget_records: if flt(budget.budget_amount): - amount = expense_amount or get_amount(args, budget) yearly_action, monthly_action = get_actions(args, budget) + args["for_material_request"] = budget.for_material_request + args["for_purchase_order"] = budget.for_purchase_order if yearly_action in ("Stop", "Warn"): compare_expense_with_budget( - args, flt(budget.budget_amount), _("Annual"), yearly_action, budget.budget_against, amount + args, + flt(budget.budget_amount), + _("Annual"), + yearly_action, + budget.budget_against, + expense_amount, ) if monthly_action in ["Stop", "Warn"]: @@ -215,18 +221,27 @@ def validate_budget_records(args, budget_records, expense_amount): _("Accumulated Monthly"), monthly_action, budget.budget_against, - amount, + expense_amount, ) def compare_expense_with_budget(args, budget_amount, action_for, action, budget_against, amount=0): - actual_expense = get_actual_expense(args) - total_expense = actual_expense + amount + args.actual_expense, args.requested_amount, args.ordered_amount = get_actual_expense(args), 0, 0 + if not amount: + args.requested_amount, args.ordered_amount = get_requested_amount(args), get_ordered_amount(args) + + if args.get("doctype") == "Material Request" and args.for_material_request: + amount = args.requested_amount + args.ordered_amount + + elif args.get("doctype") == "Purchase Order" and args.for_purchase_order: + amount = args.ordered_amount + + total_expense = args.actual_expense + amount if total_expense > budget_amount: - if actual_expense > budget_amount: + if args.actual_expense > budget_amount: error_tense = _("is already") - diff = actual_expense - budget_amount + diff = args.actual_expense - budget_amount else: error_tense = _("will be") diff = total_expense - budget_amount @@ -243,6 +258,8 @@ def compare_expense_with_budget(args, budget_amount, action_for, action, budget_ frappe.bold(fmt_money(diff, currency=currency)), ) + msg += get_expense_breakup(args, currency, budget_against) + if frappe.flags.exception_approver_role and frappe.flags.exception_approver_role in frappe.get_roles( frappe.session.user ): @@ -254,6 +271,83 @@ def compare_expense_with_budget(args, budget_amount, action_for, action, budget_ frappe.msgprint(msg, indicator="orange", title=_("Budget Exceeded")) +def get_expense_breakup(args, currency, budget_against): + msg = "
Total Expenses booked through - " + ) + + return msg + + def get_actions(args, budget): yearly_action = budget.action_if_annual_budget_exceeded monthly_action = budget.action_if_accumulated_monthly_budget_exceeded @@ -269,23 +363,9 @@ def get_actions(args, budget): return yearly_action, monthly_action -def get_amount(args, budget): - amount = 0 - - if args.get("doctype") == "Material Request" and budget.for_material_request: - amount = ( - get_requested_amount(args, budget) + get_ordered_amount(args, budget) + get_actual_expense(args) - ) - - elif args.get("doctype") == "Purchase Order" and budget.for_purchase_order: - amount = get_ordered_amount(args, budget) + get_actual_expense(args) - - return amount - - -def get_requested_amount(args, budget): +def get_requested_amount(args): item_code = args.get("item_code") - condition = get_other_condition(args, budget, "Material Request") + condition = get_other_condition(args, "Material Request") data = frappe.db.sql( """ select ifnull((sum(child.stock_qty - child.ordered_qty) * rate), 0) as amount @@ -299,9 +379,9 @@ def get_requested_amount(args, budget): return data[0][0] if data else 0 -def get_ordered_amount(args, budget): +def get_ordered_amount(args): item_code = args.get("item_code") - condition = get_other_condition(args, budget, "Purchase Order") + condition = get_other_condition(args, "Purchase Order") data = frappe.db.sql( f""" select ifnull(sum(child.amount - child.billed_amt), 0) as amount @@ -315,7 +395,7 @@ def get_ordered_amount(args, budget): return data[0][0] if data else 0 -def get_other_condition(args, budget, for_doc): +def get_other_condition(args, for_doc): condition = "expense_account = '%s'" % (args.expense_account) budget_against_field = args.get("budget_against_field") diff --git a/erpnext/accounts/doctype/cost_center/cost_center.json b/erpnext/accounts/doctype/cost_center/cost_center.json index 7cbb290947e..135f5fdcb3e 100644 --- a/erpnext/accounts/doctype/cost_center/cost_center.json +++ b/erpnext/accounts/doctype/cost_center/cost_center.json @@ -125,7 +125,7 @@ "idx": 1, "is_tree": 1, "links": [], - "modified": "2022-01-31 13:22:58.916273", + "modified": "2024-04-24 10:55:54.083042", "modified_by": "Administrator", "module": "Accounts", "name": "Cost Center", @@ -163,6 +163,15 @@ { "read": 1, "role": "Purchase User" + }, + { + "email": 1, + "export": 1, + "print": 1, + "report": 1, + "role": "Employee", + "select": 1, + "share": 1 } ], "search_fields": "parent_cost_center, is_group", diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.json b/erpnext/accounts/doctype/fiscal_year/fiscal_year.json index 5ab91f2506c..ff4ac50850f 100644 --- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.json +++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.json @@ -118,9 +118,17 @@ { "read": 1, "role": "Employee" + }, + { + "read": 1, + "role": "Accounts Manager" + }, + { + "read": 1, + "role": "Stock Manager" } ], "show_name_in_global_search": 1, "sort_field": "name", "sort_order": "DESC" -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/payment_order/payment_order.js b/erpnext/accounts/doctype/payment_order/payment_order.js index 9d4988c19c9..a041f290639 100644 --- a/erpnext/accounts/doctype/payment_order/payment_order.js +++ b/erpnext/accounts/doctype/payment_order/payment_order.js @@ -71,6 +71,7 @@ frappe.ui.form.on("Payment Order", { target: frm, date_field: "posting_date", setters: { + party_type: "Supplier", party: frm.doc.supplier || "", }, get_query_filters: { @@ -91,6 +92,7 @@ frappe.ui.form.on("Payment Order", { source_doctype: "Payment Request", target: frm, setters: { + party_type: "Supplier", party: frm.doc.supplier || "", }, get_query_filters: { diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 3291bcb2612..583735b1cc6 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -37,7 +37,7 @@ class PaymentRequest(Document): self.status = "Draft" self.validate_reference_document() self.validate_payment_request_amount() - self.validate_currency() + # self.validate_currency() self.validate_subscription_details() def validate_reference_document(self): @@ -276,21 +276,17 @@ class PaymentRequest(Document): } ) + if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency: + amount = payment_entry.base_paid_amount + else: + amount = self.grand_total + + payment_entry.received_amount = amount + payment_entry.get("references")[0].allocated_amount = amount + for dimension in get_accounting_dimensions(): payment_entry.update({dimension: self.get(dimension)}) - if payment_entry.difference_amount: - company_details = get_company_defaults(ref_doc.company) - - payment_entry.append( - "deductions", - { - "account": company_details.exchange_gain_loss_account, - "cost_center": company_details.cost_center, - "amount": payment_entry.difference_amount, - }, - ) - if submit: payment_entry.insert(ignore_permissions=True) payment_entry.submit() @@ -432,6 +428,12 @@ def make_payment_request(**args): pr = frappe.get_doc("Payment Request", draft_payment_request) else: pr = frappe.new_doc("Payment Request") + + if not args.get("payment_request_type"): + args["payment_request_type"] = ( + "Outward" if args.get("dt") in ["Purchase Order", "Purchase Invoice"] else "Inward" + ) + pr.update( { "payment_gateway_account": gateway_account.get("name"), @@ -490,9 +492,9 @@ def get_amount(ref_doc, payment_account=None): elif dt in ["Sales Invoice", "Purchase Invoice"]: if not ref_doc.get("is_pos"): if ref_doc.party_account_currency == ref_doc.currency: - grand_total = flt(ref_doc.outstanding_amount) + grand_total = flt(ref_doc.grand_total) else: - grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate + grand_total = flt(ref_doc.base_grand_total) / ref_doc.conversion_rate elif dt == "Sales Invoice": for pay in ref_doc.payments: if pay.type == "Phone" and pay.account == payment_account: diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index 70de886ba4d..932060895b0 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -86,6 +86,8 @@ class TestPaymentRequest(unittest.TestCase): pr = make_payment_request( dt="Purchase Invoice", dn=si_usd.name, + party_type="Supplier", + party="_Test Supplier USD", recipient_id="user@example.com", mute_email=1, payment_gateway_account="_Test Gateway - USD", @@ -98,6 +100,51 @@ class TestPaymentRequest(unittest.TestCase): self.assertEqual(pr.status, "Paid") + def test_multiple_payment_entry_against_purchase_invoice(self): + purchase_invoice = make_purchase_invoice( + customer="_Test Supplier USD", + debit_to="_Test Payable USD - _TC", + currency="USD", + conversion_rate=50, + ) + + pr = make_payment_request( + dt="Purchase Invoice", + party_type="Supplier", + party="_Test Supplier USD", + dn=purchase_invoice.name, + recipient_id="user@example.com", + mute_email=1, + payment_gateway_account="_Test Gateway - USD", + return_doc=1, + ) + + pr.grand_total = pr.grand_total / 2 + + pr.submit() + pr.create_payment_entry() + + purchase_invoice.load_from_db() + self.assertEqual(purchase_invoice.status, "Partly Paid") + + pr = make_payment_request( + dt="Purchase Invoice", + party_type="Supplier", + party="_Test Supplier USD", + dn=purchase_invoice.name, + recipient_id="user@example.com", + mute_email=1, + payment_gateway_account="_Test Gateway - USD", + return_doc=1, + ) + + pr.save() + pr.submit() + pr.create_payment_entry() + + purchase_invoice.load_from_db() + self.assertEqual(purchase_invoice.status, "Paid") + def test_payment_entry(self): frappe.db.set_value( "Company", "_Test Company", "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC" diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json index 8238af0ad16..2c74088a19e 100644 --- a/erpnext/projects/doctype/project/project.json +++ b/erpnext/projects/doctype/project/project.json @@ -452,7 +452,7 @@ "index_web_pages_for_search": 1, "links": [], "max_attachments": 4, - "modified": "2023-02-14 04:54:25.819620", + "modified": "2024-04-24 10:56:16.001032", "modified_by": "Administrator", "module": "Projects", "name": "Project", @@ -487,6 +487,15 @@ "role": "Projects Manager", "share": 1, "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "report": 1, + "role": "Employee", + "select": 1, + "share": 1 } ], "quick_entry": 1, diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index a52a9dbeac2..f6ba4ae9fab 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -823,11 +823,14 @@ erpnext.utils.map_current_doc = function (opts) { if (opts.source_doctype) { let data_fields = []; if (["Purchase Receipt", "Delivery Note"].includes(opts.source_doctype)) { - data_fields.push({ - fieldname: "merge_taxes", - fieldtype: "Check", - label: __("Merge taxes from multiple documents"), - }); + let target_meta = frappe.get_meta(cur_frm.doc.doctype); + if (target_meta.fields.find((f) => f.fieldname === "taxes")) { + data_fields.push({ + fieldname: "merge_taxes", + fieldtype: "Check", + label: __("Merge taxes from multiple documents"), + }); + } } const d = new frappe.ui.form.MultiSelectDialog({ doctype: opts.source_doctype, diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json index d332b4e76bd..b94cfe673b6 100644 --- a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json +++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json @@ -135,14 +135,51 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-04-18 08:25:35.302081", + "modified": "2024-04-18 15:25:25.808355", "modified_by": "Administrator", "module": "Regional", "name": "Lower Deduction Certificate", "naming_rule": "By fieldname", "owner": "Administrator", - "permissions": [], - "sort_field": "modified", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "write": 1 + } + ], + "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 diff --git a/erpnext/setup/doctype/employee/employee.js b/erpnext/setup/doctype/employee/employee.js index d165d429f44..7a1efa82fa0 100755 --- a/erpnext/setup/doctype/employee/employee.js +++ b/erpnext/setup/doctype/employee/employee.js @@ -18,18 +18,6 @@ erpnext.setup.EmployeeController = class EmployeeController extends frappe.ui.fo refresh() { erpnext.toggle_naming_series(); } - - salutation() { - if (this.frm.doc.salutation) { - this.frm.set_value( - "gender", - { - Mr: "Male", - Ms: "Female", - }[this.frm.doc.salutation] - ); - } - } }; frappe.ui.form.on("Employee", { diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 9ff22d384e7..2ad3f485d08 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -772,7 +772,7 @@ def make_delivery_trip(source_name, target_doc=None): @frappe.whitelist() -def make_installation_note(source_name, target_doc=None): +def make_installation_note(source_name, target_doc=None, kwargs=None): def update_item(obj, target, source_parent): target.qty = flt(obj.qty) - flt(obj.installed_qty) target.serial_no = obj.serial_no diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 0beb98563cf..a6fd929ea34 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -126,8 +126,7 @@ class PurchaseReceipt(BuyingController): self.po_required() self.validate_items_quality_inspection() self.validate_with_previous_doc() - self.validate_uom_is_integer("uom", ["qty", "received_qty"]) - self.validate_uom_is_integer("stock_uom", "stock_qty") + self.validate_uom_is_integer() self.validate_cwip_accounts() self.validate_provisional_expense_account() @@ -141,6 +140,10 @@ class PurchaseReceipt(BuyingController): self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse") self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") + def validate_uom_is_integer(self): + super().validate_uom_is_integer("uom", ["qty", "received_qty"], "Purchase Receipt Item") + super().validate_uom_is_integer("stock_uom", "stock_qty", "Purchase Receipt Item") + def validate_cwip_accounts(self): for item in self.get("items"): if item.is_fixed_asset and is_cwip_accounting_enabled(item.asset_category): diff --git a/erpnext/stock/report/available_batch_report/__init__.py b/erpnext/stock/report/available_batch_report/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/report/available_batch_report/available_batch_report.js b/erpnext/stock/report/available_batch_report/available_batch_report.js new file mode 100644 index 00000000000..011f7e09ca2 --- /dev/null +++ b/erpnext/stock/report/available_batch_report/available_batch_report.js @@ -0,0 +1,91 @@ +// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.query_reports["Available Batch Report"] = { + filters: [ + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + width: "80", + options: "Company", + default: frappe.defaults.get_default("company"), + }, + { + fieldname: "to_date", + label: __("On This Date"), + fieldtype: "Date", + width: "80", + reqd: 1, + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + }, + { + fieldname: "item_code", + label: __("Item"), + fieldtype: "Link", + width: "80", + options: "Item", + get_query: () => { + return { + filters: { + has_batch_no: 1, + disabled: 0, + }, + }; + }, + }, + { + fieldname: "warehouse", + label: __("Warehouse"), + fieldtype: "Link", + width: "80", + options: "Warehouse", + get_query: () => { + let warehouse_type = frappe.query_report.get_filter_value("warehouse_type"); + let company = frappe.query_report.get_filter_value("company"); + + return { + filters: { + ...(warehouse_type && { warehouse_type }), + ...(company && { company }), + }, + }; + }, + }, + { + fieldname: "warehouse_type", + label: __("Warehouse Type"), + fieldtype: "Link", + width: "80", + options: "Warehouse Type", + }, + { + fieldname: "batch_no", + label: __("Batch No"), + fieldtype: "Link", + width: "80", + options: "Batch", + get_query: () => { + let item = frappe.query_report.get_filter_value("item_code"); + + return { + filters: { + ...(item && { item }), + }, + }; + }, + }, + { + fieldname: "include_expired_batches", + label: __("Include Expired Batches"), + fieldtype: "Check", + width: "80", + }, + { + fieldname: "show_item_name", + label: __("Show Item Name"), + fieldtype: "Check", + width: "80", + }, + ], +}; diff --git a/erpnext/stock/report/available_batch_report/available_batch_report.json b/erpnext/stock/report/available_batch_report/available_batch_report.json new file mode 100644 index 00000000000..0125a96fe70 --- /dev/null +++ b/erpnext/stock/report/available_batch_report/available_batch_report.json @@ -0,0 +1,31 @@ +{ + "add_total_row": 1, + "columns": [], + "creation": "2024-04-11 17:03:32.253275", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "json": "{}", + "letter_head": "", + "modified": "2024-04-23 17:09:54.595566", + "modified_by": "Administrator", + "module": "Stock", + "name": "Available Batch Report", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Stock Ledger Entry", + "report_name": "Available Batch Report", + "report_type": "Script Report", + "roles": [ + { + "role": "Stock User" + }, + { + "role": "Accounts Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/stock/report/available_batch_report/available_batch_report.py b/erpnext/stock/report/available_batch_report/available_batch_report.py new file mode 100644 index 00000000000..cb651dd7972 --- /dev/null +++ b/erpnext/stock/report/available_batch_report/available_batch_report.py @@ -0,0 +1,144 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from collections import defaultdict + +import frappe +from frappe import _ +from frappe.query_builder.functions import Sum +from frappe.utils import flt, today + + +def execute(filters=None): + columns, data = [], [] + data = get_data(filters) + columns = get_columns(filters) + return columns, data + + +def get_columns(filters): + columns = [ + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 200, + } + ] + + if filters.show_item_name: + columns.append( + { + "label": _("Item Name"), + "fieldname": "item_name", + "fieldtype": "Link", + "options": "Item", + "width": 200, + } + ) + + columns.extend( + [ + { + "label": _("Warehouse"), + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "width": 200, + }, + { + "label": _("Batch No"), + "fieldname": "batch_no", + "fieldtype": "Link", + "width": 150, + "options": "Batch", + }, + {"label": _("Balance Qty"), "fieldname": "balance_qty", "fieldtype": "Float", "width": 150}, + ] + ) + + return columns + + +def get_data(filters): + data = [] + batchwise_data = get_batchwise_data_from_stock_ledger(filters) + + data = parse_batchwise_data(batchwise_data) + + return data + + +def parse_batchwise_data(batchwise_data): + data = [] + for key in batchwise_data: + d = batchwise_data[key] + if d.balance_qty == 0: + continue + + data.append(d) + + return data + + +def get_batchwise_data_from_stock_ledger(filters): + batchwise_data = frappe._dict({}) + + table = frappe.qb.DocType("Stock Ledger Entry") + batch = frappe.qb.DocType("Batch") + + query = ( + frappe.qb.from_(table) + .inner_join(batch) + .on(table.batch_no == batch.name) + .select( + table.item_code, + table.batch_no, + table.warehouse, + Sum(table.actual_qty).as_("balance_qty"), + ) + .where(table.is_cancelled == 0) + .groupby(table.batch_no, table.item_code, table.warehouse) + ) + + query = get_query_based_on_filters(query, batch, table, filters) + + for d in query.run(as_dict=True): + key = (d.item_code, d.warehouse, d.batch_no) + batchwise_data.setdefault(key, d) + + return batchwise_data + + +def get_query_based_on_filters(query, batch, table, filters): + if filters.item_code: + query = query.where(table.item_code == filters.item_code) + + if filters.batch_no: + query = query.where(batch.name == filters.batch_no) + + if not filters.include_expired_batches: + query = query.where((batch.expiry_date >= today()) | (batch.expiry_date.isnull())) + if filters.to_date == today(): + query = query.where(batch.batch_qty > 0) + + if filters.warehouse: + lft, rgt = frappe.db.get_value("Warehouse", filters.warehouse, ["lft", "rgt"]) + warehouses = frappe.get_all( + "Warehouse", filters={"lft": (">=", lft), "rgt": ("<=", rgt), "is_group": 0}, pluck="name" + ) + + query = query.where(table.warehouse.isin(warehouses)) + + elif filters.warehouse_type: + warehouses = frappe.get_all( + "Warehouse", filters={"warehouse_type": filters.warehouse_type, "is_group": 0}, pluck="name" + ) + + query = query.where(table.warehouse.isin(warehouses)) + + if filters.show_item_name: + query = query.select(batch.item_name) + + return query diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js index 1694abe7c08..401ebe43028 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js @@ -40,16 +40,26 @@ frappe.query_reports["Batch-Wise Balance History"] = { }; }, }, + { + fieldname: "warehouse_type", + label: __("Warehouse Type"), + fieldtype: "Link", + width: "80", + options: "Warehouse Type", + }, { fieldname: "warehouse", label: __("Warehouse"), fieldtype: "Link", options: "Warehouse", get_query: function () { - let company = frappe.query_report.get_filter_value("company"); + let warehouse_type = frappe.query_report.get_filter_value("warehouse_type"); + const company = frappe.query_report.get_filter_value("company"); + return { filters: { - company: company, + ...(warehouse_type && { warehouse_type }), + ...(company && { company }), }, }; }, diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py index 50b5efd6a08..c8c26fd66cb 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py @@ -113,6 +113,16 @@ def get_stock_ledger_entries(filters): ) query = apply_warehouse_filter(query, sle, filters) + if filters.warehouse_type and not filters.warehouse: + warehouses = frappe.get_all( + "Warehouse", + filters={"warehouse_type": filters.warehouse_type, "is_group": 0}, + pluck="name", + ) + + if warehouses: + query = query.where(sle.warehouse.isin(warehouses)) + for field in ["item_code", "batch_no", "company"]: if filters.get(field): query = query.where(sle[field] == filters.get(field)) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.js b/erpnext/stock/report/stock_ageing/stock_ageing.js index 641084149ab..726b507663d 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.js +++ b/erpnext/stock/report/stock_ageing/stock_ageing.js @@ -18,15 +18,25 @@ frappe.query_reports["Stock Ageing"] = { default: frappe.datetime.get_today(), reqd: 1, }, + { + fieldname: "warehouse_type", + label: __("Warehouse Type"), + fieldtype: "Link", + width: "80", + options: "Warehouse Type", + }, { fieldname: "warehouse", label: __("Warehouse"), fieldtype: "Link", options: "Warehouse", get_query: () => { + let warehouse_type = frappe.query_report.get_filter_value("warehouse_type"); const company = frappe.query_report.get_filter_value("company"); + return { filters: { + ...(warehouse_type && { warehouse_type }), ...(company && { company }), }, }; diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index a1aee987cb6..26bf99e1ed7 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -227,25 +227,30 @@ class FIFOSlots: consumed/updated and maintained via FIFO. ** } """ - if self.sle is None: - self.sle = self.__get_stock_ledger_entries() + stock_ledger_entries = self.sle - for d in self.sle: - key, fifo_queue, transferred_item_key = self.__init_key_stores(d) + with frappe.db.unbuffered_cursor(): + if self.sle is None: + self.sle = self.__get_stock_ledger_entries() - if d.voucher_type == "Stock Reconciliation": - # get difference in qty shift as actual qty - prev_balance_qty = self.item_details[key].get("qty_after_transaction", 0) - d.actual_qty = flt(d.qty_after_transaction) - flt(prev_balance_qty) + for d in self.sle: + key, fifo_queue, transferred_item_key = self.__init_key_stores(d) - serial_nos = get_serial_nos(d.serial_no) if d.serial_no else [] + if d.voucher_type == "Stock Reconciliation": + # get difference in qty shift as actual qty + prev_balance_qty = self.item_details[key].get("qty_after_transaction", 0) + d.actual_qty = flt(d.qty_after_transaction) - flt(prev_balance_qty) - if d.actual_qty > 0: - self.__compute_incoming_stock(d, fifo_queue, transferred_item_key, serial_nos) - else: - self.__compute_outgoing_stock(d, fifo_queue, transferred_item_key, serial_nos) + serial_nos = get_serial_nos(d.serial_no) if d.serial_no else [] - self.__update_balances(d, key) + if d.actual_qty > 0: + self.__compute_incoming_stock(d, fifo_queue, transferred_item_key, serial_nos) + else: + self.__compute_outgoing_stock(d, fifo_queue, transferred_item_key, serial_nos) + + self.__update_balances(d, key) + + del stock_ledger_entries if not self.filters.get("show_warehouse_wise_stock"): # (Item 1, WH 1), (Item 1, WH 2) => (Item 1) @@ -412,10 +417,19 @@ class FIFOSlots: if self.filters.get("warehouse"): sle_query = self.__get_warehouse_conditions(sle, sle_query) + elif self.filters.get("warehouse_type"): + warehouses = frappe.get_all( + "Warehouse", + filters={"warehouse_type": self.filters.get("warehouse_type"), "is_group": 0}, + pluck="name", + ) + + if warehouses: + sle_query = sle_query.where(sle.warehouse.isin(warehouses)) sle_query = sle_query.orderby(sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty) - return sle_query.run(as_dict=True) + return sle_query.run(as_dict=True, as_iterator=True) def __get_item_query(self) -> str: item_table = frappe.qb.DocType("Item") diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 127e6eec882..c0235f9b85c 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -329,7 +329,9 @@ class SubcontractingReceipt(SubcontractingController): ) accepted_warehouse_account = warehouse_account[item.warehouse]["account"] - supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get("account") + supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get( + "account" + ) remarks = self.get("remarks") or _("Accounting Entry for Stock") # Accepted Warehouse Account (Debit) @@ -401,7 +403,9 @@ class SubcontractingReceipt(SubcontractingController): ) if divisional_loss := flt(item.amount - stock_value_diff, item.precision("amount")): - loss_account = self.get_company_default("stock_adjustment_account", ignore_validation=True) + loss_account = self.get_company_default( + "stock_adjustment_account", ignore_validation=True + ) # Loss Account (Credit) self.add_gl_entry( diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index d89095ef3d3..3b7812f96c2 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -30,8 +30,8 @@ class TransactionBase(StatusUpdater): except ValueError: frappe.throw(_("Invalid Posting Time")) - def validate_uom_is_integer(self, uom_field, qty_fields): - validate_uom_is_integer(self, uom_field, qty_fields) + def validate_uom_is_integer(self, uom_field, qty_fields, child_dt=None): + validate_uom_is_integer(self, uom_field, qty_fields, child_dt) def validate_with_previous_doc(self, ref): self.exclude_fields = ["conversion_factor", "uom"] if self.get("is_return") else [] @@ -210,12 +210,13 @@ def validate_uom_is_integer(doc, uom_field, qty_fields, child_dt=None): for f in qty_fields: qty = d.get(f) if qty: - if abs(cint(qty) - flt(qty, d.precision(f))) > 0.0000001: + precision = d.precision(f) + if abs(cint(qty) - flt(qty, precision)) > 0.0000001: frappe.throw( _( "Row {1}: Quantity ({0}) cannot be a fraction. To allow this, disable '{2}' in UOM {3}." ).format( - flt(qty, d.precision(f)), + flt(qty, precision), d.idx, frappe.bold(_("Must be Whole Number")), frappe.bold(d.get(uom_field)),