diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js index cd883e5bf72..4e45dede1d5 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js @@ -57,9 +57,12 @@ frappe.ui.form.on("Accounting Dimension", { } }, + label: function (frm) { + frm.set_value("fieldname", frappe.model.scrub(frm.doc.label)); + }, + document_type: function (frm) { frm.set_value("label", frm.doc.document_type); - frm.set_value("fieldname", frappe.model.scrub(frm.doc.document_type)); frappe.db.get_value( "Accounting Dimension", diff --git a/erpnext/accounts/doctype/budget/budget.py b/erpnext/accounts/doctype/budget/budget.py index d7545ef1520..11f78ae1763 100644 --- a/erpnext/accounts/doctype/budget/budget.py +++ b/erpnext/accounts/doctype/budget/budget.py @@ -219,12 +219,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"]: @@ -240,18 +246,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 @@ -268,6 +283,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 ): @@ -279,6 +296,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 @@ -294,23 +388,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 @@ -324,9 +404,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 @@ -340,7 +420,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 2e66db7bcf3..55d4085c823 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/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index fe2ef1c5a91..4de20a17389 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -6,7 +6,7 @@ import json import frappe from frappe import _, msgprint, scrub -from frappe.utils import cstr, flt, fmt_money, formatdate, get_link_to_form, nowdate +from frappe.utils import comma_and, cstr, flt, fmt_money, formatdate, get_link_to_form, nowdate import erpnext from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts @@ -146,6 +146,7 @@ class JournalEntry(AccountsController): self.validate_empty_accounts_table() self.validate_inter_company_accounts() self.validate_depr_entry_voucher_type() + self.validate_advance_accounts() if self.docstatus == 0: self.apply_tax_withholding() @@ -153,6 +154,20 @@ class JournalEntry(AccountsController): if not self.title: self.title = self.get_title() + def validate_advance_accounts(self): + journal_accounts = set([x.account for x in self.accounts]) + advance_accounts = set() + advance_accounts.add( + frappe.get_cached_value("Company", self.company, "default_advance_received_account") + ) + advance_accounts.add(frappe.get_cached_value("Company", self.company, "default_advance_paid_account")) + if advance_accounts_used := journal_accounts & advance_accounts: + frappe.msgprint( + _( + "Making Journal Entries against advance accounts: {0} is not recommended. These Journals won't be available for Reconciliation." + ).format(frappe.bold(comma_and(advance_accounts_used))) + ) + def validate_for_repost(self): validate_docs_for_voucher_types(["Journal Entry"]) validate_docs_for_deferred_accounting([self.name], []) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index a37a6fe5465..d6ba193aa0e 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -150,6 +150,7 @@ "reqd": 1 }, { + "allow_on_submit": 1, "fieldname": "cost_center", "fieldtype": "Link", "label": "Cost Center", @@ -476,6 +477,7 @@ "label": "More Information" }, { + "allow_on_submit": 1, "fieldname": "project", "fieldtype": "Link", "label": "Project", @@ -776,7 +778,7 @@ "table_fieldname": "payment_entries" } ], - "modified": "2024-01-03 12:46:41.759121", + "modified": "2024-04-11 11:25:07.366347", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 0d1206b0cc7..9a4fbe10bd1 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -2028,6 +2028,8 @@ def get_negative_outstanding_invoices( @frappe.whitelist() def get_party_details(company, party_type, party, date, cost_center=None): bank_account = "" + party_bank_account = "" + if not frappe.db.exists(party_type, party): frappe.throw(_("{0} {1} does not exist").format(_(party_type), party)) @@ -2039,8 +2041,8 @@ def get_party_details(company, party_type, party, date, cost_center=None): party_balance = get_balance_on(party_type=party_type, party=party, cost_center=cost_center) if party_type in ["Customer", "Supplier"]: party_bank_account = get_party_bank_account(party_type, party) + bank_account = get_default_company_bank_account(company, party_type, party) - bank_account = get_default_company_bank_account(company, party_type, party) return { "party_account": party_account, "party_name": party_name, diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 3e24776cea7..cafdaaaa957 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -10,6 +10,7 @@ from frappe.utils import add_days, flt, nowdate from erpnext.accounts.doctype.account.test_account import create_account from erpnext.accounts.doctype.payment_entry.payment_entry import ( get_outstanding_reference_documents, + get_party_details, get_payment_entry, get_reference_details, ) @@ -1684,6 +1685,10 @@ def create_payment_entry(**args): payment_entry.reference_no = "Test001" payment_entry.reference_date = nowdate() + get_party_details( + payment_entry.company, payment_entry.party_type, payment_entry.party, payment_entry.posting_date + ) + if args.get("save"): payment_entry.save() if args.get("submit"): diff --git a/erpnext/accounts/doctype/payment_order/payment_order.js b/erpnext/accounts/doctype/payment_order/payment_order.js index f009de59592..4033fc08233 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_reconciliation/payment_reconciliation.json b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json index 666926f00eb..219d6089a57 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json @@ -195,6 +195,8 @@ }, { "depends_on": "eval:doc.party", + "description": "Only 'Payment Entries' made against this advance account are supported.", + "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/advance-in-separate-party-account", "fieldname": "default_advance_account", "fieldtype": "Link", "label": "Default Advance Account", @@ -229,7 +231,7 @@ "is_virtual": 1, "issingle": 1, "links": [], - "modified": "2023-12-14 13:38:16.264013", + "modified": "2024-04-23 12:38:29.557315", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 9e8bf78a2f1..cf08d08ce4b 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -299,6 +299,7 @@ "remember_last_selected_value": 1 }, { + "allow_on_submit": 1, "fieldname": "cost_center", "fieldtype": "Link", "label": "Cost Center", @@ -1367,6 +1368,7 @@ "read_only": 1 }, { + "allow_on_submit": 1, "fieldname": "project", "fieldtype": "Link", "label": "Project", @@ -1637,7 +1639,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2024-03-20 15:57:00.736868", + "modified": "2024-04-11 11:28:42.802211", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index aa7573e21c1..dbc9ab474d2 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -68,15 +68,11 @@ class PurchaseInvoice(BuyingController): from erpnext.accounts.doctype.purchase_invoice_advance.purchase_invoice_advance import ( PurchaseInvoiceAdvance, ) - from erpnext.accounts.doctype.purchase_invoice_item.purchase_invoice_item import ( - PurchaseInvoiceItem, - ) + from erpnext.accounts.doctype.purchase_invoice_item.purchase_invoice_item import PurchaseInvoiceItem from erpnext.accounts.doctype.purchase_taxes_and_charges.purchase_taxes_and_charges import ( PurchaseTaxesandCharges, ) - from erpnext.accounts.doctype.tax_withheld_vouchers.tax_withheld_vouchers import ( - TaxWithheldVouchers, - ) + from erpnext.accounts.doctype.tax_withheld_vouchers.tax_withheld_vouchers import TaxWithheldVouchers from erpnext.buying.doctype.purchase_receipt_item_supplied.purchase_receipt_item_supplied import ( PurchaseReceiptItemSupplied, ) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 4a3e1758752..51da848d9da 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -289,6 +289,7 @@ "read_only": 1 }, { + "allow_on_submit": 1, "fieldname": "project", "fieldtype": "Link", "hide_days": 1, @@ -354,6 +355,7 @@ "reqd": 1 }, { + "allow_on_submit": 1, "fieldname": "cost_center", "fieldtype": "Link", "hide_days": 1, @@ -2185,7 +2187,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2024-03-22 17:50:34.395602", + "modified": "2024-04-11 11:30:26.272441", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", @@ -2240,4 +2242,4 @@ "title_field": "title", "track_changes": 1, "track_seen": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 530bea416bf..abdfcaeb512 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -55,13 +55,9 @@ class SalesInvoice(SellingController): from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail - from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import ( - SalesInvoiceAdvance, - ) + from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import SalesInvoiceAdvance from erpnext.accounts.doctype.sales_invoice_item.sales_invoice_item import SalesInvoiceItem - from erpnext.accounts.doctype.sales_invoice_payment.sales_invoice_payment import ( - SalesInvoicePayment, - ) + from erpnext.accounts.doctype.sales_invoice_payment.sales_invoice_payment import SalesInvoicePayment from erpnext.accounts.doctype.sales_invoice_timesheet.sales_invoice_timesheet import ( SalesInvoiceTimesheet, ) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 722082fe0a4..70cd8ce1f97 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -751,52 +751,6 @@ def validate_party_frozen_disabled(party_type, party_name): frappe.msgprint(_("{0} {1} is not active").format(party_type, party_name), alert=True) -def get_timeline_data(doctype, name): - """returns timeline data for the past one year""" - from frappe.desk.form.load import get_communication_data - - out = {} - after = add_years(None, -1).strftime("%Y-%m-%d") - - data = get_communication_data( - doctype, - name, - after=after, - group_by="group by communication_date", - fields="C.communication_date as communication_date, count(C.name)", - as_dict=False, - ) - - # fetch and append data from Activity Log - activity_log = frappe.qb.DocType("Activity Log") - data += ( - frappe.qb.from_(activity_log) - .select(activity_log.communication_date, Count(activity_log.name)) - .where( - ( - ((activity_log.reference_doctype == doctype) & (activity_log.reference_name == name)) - | ((activity_log.timeline_doctype == doctype) & (activity_log.timeline_name == name)) - | ( - (activity_log.reference_doctype.isin(["Quotation", "Opportunity"])) - & (activity_log.timeline_name == name) - ) - ) - & (activity_log.status != "Success") - & (activity_log.creation > after) - ) - .groupby(activity_log.communication_date) - .orderby(activity_log.communication_date, order=frappe.qb.desc) - ).run() - - timeline_items = dict(data) - - for date, count in timeline_items.items(): - timestamp = get_timestamp(date) - out.update({timestamp: count}) - - return out - - def get_dashboard_info(party_type, party, loyalty_program=None): current_fiscal_year = get_fiscal_year(nowdate(), as_dict=True) diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index 3a5acbe0322..278da1b08bf 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -305,6 +305,7 @@ def create_asset_repair(**args): "serial_nos": args.serial_no, "posting_date": today(), "posting_time": nowtime(), + "do_not_submit": 1, } ) ).name diff --git a/erpnext/buying/doctype/supplier/supplier_dashboard.py b/erpnext/buying/doctype/supplier/supplier_dashboard.py index 3bd306e6591..9f424dd0c7c 100644 --- a/erpnext/buying/doctype/supplier/supplier_dashboard.py +++ b/erpnext/buying/doctype/supplier/supplier_dashboard.py @@ -3,10 +3,6 @@ from frappe import _ def get_data(): return { - "heatmap": True, - "heatmap_message": _( - "This is based on transactions against this Supplier. See timeline below for details" - ), "fieldname": "supplier", "non_standard_fieldnames": {"Payment Entry": "party", "Bank Account": "party"}, "transactions": [ diff --git a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js index f7d0d947b61..9701e147f05 100644 --- a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js +++ b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js @@ -133,6 +133,13 @@ frappe.query_reports["Supplier Quotation Comparison"] = { return row.supplier_name; }); + let items = []; + report.data.forEach((d) => { + if (!items.includes(d.item_code)) { + items.push(d.item_code); + } + }); + // Create a dialog window for the user to pick their supplier let dialog = new frappe.ui.Dialog({ title: __("Select Default Supplier"), @@ -151,20 +158,34 @@ frappe.query_reports["Supplier Quotation Comparison"] = { }; }, }, + { + reqd: 1, + label: "Item", + fieldtype: "Link", + options: "Item", + fieldname: "item_code", + get_query: () => { + return { + filters: { + name: ["in", items], + }, + }; + }, + }, ], }); dialog.set_primary_action(__("Set Default Supplier"), () => { let values = dialog.get_values(); + if (values) { // Set the default_supplier field of the appropriate Item to the selected supplier frappe.call({ - method: "frappe.client.set_value", + method: "erpnext.buying.report.supplier_quotation_comparison.supplier_quotation_comparison.set_default_supplier", args: { - doctype: "Item", - name: item_code, - fieldname: "default_supplier", - value: values.supplier, + item_code: values.item_code, + supplier: values.supplier, + company: filters.company, }, freeze: true, callback: (r) => { diff --git a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py index 684cd3a0f9e..085f30f84d9 100644 --- a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py +++ b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py @@ -292,3 +292,13 @@ def get_message(): Expires today / Already Expired """ + + +@frappe.whitelist() +def set_default_supplier(item_code, supplier, company): + frappe.db.set_value( + "Item Default", + {"parent": item_code, "company": company}, + "default_supplier", + supplier, + ) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 80b156487bd..a378d8ae606 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1896,7 +1896,7 @@ def sales_order_query(doctype=None, txt=None, searchfield=None, start=None, page query = query.where(so_table.name.isin(filters.get("sales_orders"))) if txt: - query = query.where(table.item_code.like(f"{txt}%")) + query = query.where(table.parent.like(f"%{txt}%")) if page_len: query = query.limit(page_len) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index e13a07b83e8..904e42a6a03 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -948,6 +948,21 @@ class WorkOrder(Document): if not self.qty > 0: frappe.throw(_("Quantity to Manufacture must be greater than 0.")) + if ( + self.stock_uom + and frappe.get_cached_value("UOM", self.stock_uom, "must_be_whole_number") + and abs(cint(self.qty) - flt(self.qty, self.precision("qty"))) > 0.0000001 + ): + frappe.throw( + _( + "Qty To Manufacture ({0}) cannot be a fraction for the UOM {2}. To allow this, disable '{1}' in the UOM {2}." + ).format( + flt(self.qty, self.precision("qty")), + frappe.bold(_("Must be Whole Number")), + frappe.bold(self.stock_uom), + ), + ) + if self.production_plan and self.production_plan_item and not self.production_plan_sub_assembly_item: qty_dict = frappe.db.get_value( "Production Plan Item", self.production_plan_item, ["planned_qty", "ordered_qty"], as_dict=1 diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json index 5917e9b5d26..464b1c9d7a8 100644 --- a/erpnext/projects/doctype/project/project.json +++ b/erpnext/projects/doctype/project/project.json @@ -454,7 +454,7 @@ "index_web_pages_for_search": 1, "links": [], "max_attachments": 4, - "modified": "2024-01-08 16:01:34.598258", + "modified": "2024-04-24 10:56:16.001032", "modified_by": "Administrator", "module": "Projects", "name": "Project", @@ -489,6 +489,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 42d4cc51c40..288b2f6932d 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -906,11 +906,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/selling/doctype/customer/customer_dashboard.py b/erpnext/selling/doctype/customer/customer_dashboard.py index 1b2296381e8..161a3ba0c50 100644 --- a/erpnext/selling/doctype/customer/customer_dashboard.py +++ b/erpnext/selling/doctype/customer/customer_dashboard.py @@ -3,10 +3,6 @@ from frappe import _ def get_data(): return { - "heatmap": True, - "heatmap_message": _( - "This is based on transactions against this Customer. See timeline below for details" - ), "fieldname": "customer", "non_standard_fieldnames": { "Payment Entry": "party", diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 24d7da45b84..c222d6b96a7 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -707,6 +707,8 @@ }, { "depends_on": "eval:doc.book_advance_payments_in_separate_party_account", + "description": "Only 'Payment Entries' made against this advance account are supported.", + "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/advance-in-separate-party-account", "fieldname": "default_advance_received_account", "fieldtype": "Link", "label": "Default Advance Received Account", @@ -715,6 +717,8 @@ }, { "depends_on": "eval:doc.book_advance_payments_in_separate_party_account", + "description": "Only 'Payment Entries' made against this advance account are supported.", + "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/advance-in-separate-party-account", "fieldname": "default_advance_paid_account", "fieldtype": "Link", "label": "Default Advance Paid Account", @@ -782,7 +786,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2023-09-10 21:53:13.860791", + "modified": "2024-04-23 12:38:33.173938", "modified_by": "Administrator", "module": "Setup", "name": "Company", diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 7181e90139e..3523cac822e 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -35,7 +35,7 @@ class Company(NestedSet): auto_exchange_rate_revaluation: DF.Check book_advance_payments_in_separate_party_account: DF.Check capital_work_in_progress_account: DF.Link | None - chart_of_accounts: DF.Literal + chart_of_accounts: DF.Literal[None] company_description: DF.TextEditor | None company_logo: DF.AttachImage | None company_name: DF.Data 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/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index 3f37b7bf098..3ef0e57c25a 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -86,6 +86,7 @@ class TestBatch(FrappeTestCase): "batches": frappe._dict({batch_no: 20}), "type_of_transaction": "Inward", "company": receipt.company, + "do_not_submit": 1, } ) .make_serial_and_batch_bundle() @@ -176,6 +177,7 @@ class TestBatch(FrappeTestCase): "batches": frappe._dict({batch_no: batch_qty}), "type_of_transaction": "Outward", "company": receipt.company, + "do_not_submit": 1, } ) .make_serial_and_batch_bundle() @@ -249,6 +251,7 @@ class TestBatch(FrappeTestCase): "batches": frappe._dict({batch_no: batch_qty}), "type_of_transaction": "Outward", "company": receipt.company, + "do_not_submit": 1, } ) .make_serial_and_batch_bundle() @@ -341,6 +344,7 @@ class TestBatch(FrappeTestCase): "batches": frappe._dict({batch_name: 90}), "type_of_transaction": "Inward", "company": "_Test Company", + "do_not_submit": 1, } ).make_serial_and_batch_bundle() diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 1337c4eae16..bce87b80c51 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -1099,7 +1099,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 6dc92772d8c..2e751ad5251 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -238,8 +238,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() @@ -253,6 +252,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/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index e67736253cd..54a695126c7 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -2968,6 +2968,7 @@ def make_purchase_receipt(**args): "serial_nos": serial_nos, "posting_date": args.posting_date or today(), "posting_time": args.posting_time, + "do_not_submit": 1, } ) ).name diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 286a220c5dd..91951834bf9 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -596,6 +596,13 @@ class SerialandBatchBundle(Document): serial_batches = {} for row in self.entries: + if not row.qty and row.batch_no and not row.serial_no: + frappe.throw( + _("At row {0}: Qty is mandatory for the batch {1}").format( + bold(row.idx), bold(row.batch_no) + ) + ) + if self.has_serial_no and not row.serial_no: frappe.throw( _("At row {0}: Serial No is mandatory for Item {1}").format( @@ -831,7 +838,12 @@ class SerialandBatchBundle(Document): for batch in batches: frappe.db.set_value("Batch", batch.name, {"reference_name": None, "reference_doctype": None}) + def validate_serial_and_batch_data(self): + if not self.voucher_no: + frappe.throw(_("Voucher No is mandatory")) + def before_submit(self): + self.validate_serial_and_batch_data() self.validate_serial_and_batch_no_for_returned() self.set_purchase_document_no() diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index ce50155ffd5..069192fdb16 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -1483,7 +1483,7 @@ def create_delivery_note_entries_for_batchwise_item_valuation_test(dn_entry_list "posting_date": dn.posting_date, "posting_time": dn.posting_time, "voucher_type": "Delivery Note", - "do_not_submit": dn.name, + "do_not_submit": 1, } ) ).name diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 64a6b6503cc..f92d7361f41 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -823,11 +823,9 @@ class StockReconciliation(StockController): else: self._cancel() - def recalculate_current_qty(self, voucher_detail_no, sle_creation, add_new_sle=False): + def recalculate_current_qty(self, voucher_detail_no): from erpnext.stock.stock_ledger import get_valuation_rate - sl_entries = [] - for row in self.items: if voucher_detail_no != row.name: continue @@ -881,32 +879,6 @@ class StockReconciliation(StockController): } ) - if ( - add_new_sle - and not frappe.db.get_value( - "Stock Ledger Entry", - {"voucher_detail_no": row.name, "actual_qty": ("<", 0), "is_cancelled": 0}, - "name", - ) - and (not row.current_serial_and_batch_bundle) - ): - self.set_current_serial_and_batch_bundle(voucher_detail_no, save=True) - row.reload() - - if row.current_qty > 0 and row.current_serial_and_batch_bundle: - new_sle = self.get_sle_for_items(row) - new_sle.actual_qty = row.current_qty * -1 - new_sle.valuation_rate = row.current_valuation_rate - new_sle.creation_time = add_to_date(sle_creation, seconds=-1) - new_sle.serial_and_batch_bundle = row.current_serial_and_batch_bundle - new_sle.qty_after_transaction = 0.0 - sl_entries.append(new_sle) - - if sl_entries: - self.make_sl_entries(sl_entries, allow_negative_stock=self.has_negative_stock_allowed()) - if not frappe.db.exists("Repost Item Valuation", {"voucher_no": self.name, "status": "Queued"}): - self.repost_future_sle_and_gle(force=True) - def has_negative_stock_allowed(self): allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) if allow_negative_stock: diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 0ffcdd55fc8..92a931036e9 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -756,66 +756,6 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): self.assertEqual(flt(sle[0].actual_qty), flt(-100.0)) - def test_backdated_stock_reco_entry_with_batch(self): - item_code = self.make_item( - "Test New Batch Item ABCVSD", - { - "is_stock_item": 1, - "has_batch_no": 1, - "batch_number_series": "BNS9.####", - "create_new_batch": 1, - }, - ).name - - warehouse = "_Test Warehouse - _TC" - - # Stock Reco for 100, Balace Qty 100 - stock_reco = create_stock_reconciliation( - item_code=item_code, - posting_date=nowdate(), - posting_time="11:00:00", - warehouse=warehouse, - qty=100, - rate=100, - ) - - sles = frappe.get_all( - "Stock Ledger Entry", - fields=["actual_qty"], - filters={"voucher_no": stock_reco.name, "is_cancelled": 0}, - ) - - self.assertEqual(len(sles), 1) - - stock_reco.reload() - batch_no = get_batch_from_bundle(stock_reco.items[0].serial_and_batch_bundle) - - # Stock Reco for 100, Balace Qty 100 - stock_reco1 = create_stock_reconciliation( - item_code=item_code, - posting_date=add_days(nowdate(), -1), - posting_time="11:00:00", - batch_no=batch_no, - warehouse=warehouse, - qty=60, - rate=100, - ) - - sles = frappe.get_all( - "Stock Ledger Entry", - fields=["actual_qty"], - filters={"voucher_no": stock_reco.name, "is_cancelled": 0}, - ) - - stock_reco1.reload() - get_batch_from_bundle(stock_reco1.items[0].serial_and_batch_bundle) - - self.assertEqual(len(sles), 2) - - for row in sles: - if row.actual_qty < 0: - self.assertEqual(row.actual_qty, -60) - def test_update_stock_reconciliation_while_reposting(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry @@ -986,6 +926,150 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): active_serial_no = frappe.get_all("Serial No", filters={"status": "Active", "item_code": item_code}) self.assertEqual(len(active_serial_no), 5) + def test_balance_qty_for_batch_with_backdated_stock_reco_and_future_entries(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + item = self.make_item( + "Test Batch Item Original Test", + { + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TEST-BATCH-SRWFEE-.###", + }, + ) + + warehouse = "_Test Warehouse - _TC" + se1 = make_stock_entry( + item_code=item.name, + target=warehouse, + qty=50, + basic_rate=100, + posting_date=add_days(nowdate(), -2), + ) + batch1 = get_batch_from_bundle(se1.items[0].serial_and_batch_bundle) + + se2 = make_stock_entry( + item_code=item.name, + target=warehouse, + qty=50, + basic_rate=100, + posting_date=add_days(nowdate(), -2), + ) + batch2 = get_batch_from_bundle(se2.items[0].serial_and_batch_bundle) + + se3 = make_stock_entry( + item_code=item.name, + target=warehouse, + qty=100, + basic_rate=100, + posting_date=add_days(nowdate(), -2), + ) + batch3 = get_batch_from_bundle(se3.items[0].serial_and_batch_bundle) + + se3 = make_stock_entry( + item_code=item.name, + target=warehouse, + qty=100, + basic_rate=100, + posting_date=nowdate(), + ) + + sle = frappe.get_all( + "Stock Ledger Entry", + filters={ + "item_code": item.name, + "warehouse": warehouse, + "is_cancelled": 0, + "voucher_no": se3.name, + }, + fields=["qty_after_transaction"], + order_by="posting_time desc, creation desc", + ) + + self.assertEqual(flt(sle[0].qty_after_transaction), flt(300.0)) + + sr = create_stock_reconciliation( + item_code=item.name, + warehouse=warehouse, + qty=0, + batch_no=batch1, + posting_date=add_days(nowdate(), -1), + use_serial_batch_fields=1, + do_not_save=1, + ) + + for batch in [batch2, batch3]: + sr.append( + "items", + { + "item_code": item.name, + "warehouse": warehouse, + "qty": 0, + "batch_no": batch, + "use_serial_batch_fields": 1, + }, + ) + + sr.save() + sr.submit() + + sle = frappe.get_all( + "Stock Ledger Entry", + filters={ + "item_code": item.name, + "warehouse": warehouse, + "is_cancelled": 0, + "voucher_no": se3.name, + }, + fields=["qty_after_transaction"], + order_by="posting_time desc, creation desc", + ) + + self.assertEqual(flt(sle[0].qty_after_transaction), flt(100.0)) + + def test_stock_reco_and_backdated_purchase_receipt(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + item = self.make_item( + "Test Batch Item Original STOCK RECO Test", + { + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TEST-BATCH-SRCOSRWFEE-.###", + }, + ) + + warehouse = "_Test Warehouse - _TC" + + sr = create_stock_reconciliation( + item_code=item.name, + warehouse=warehouse, + qty=100, + rate=100, + ) + + sr.reload() + self.assertTrue(sr.items[0].serial_and_batch_bundle) + self.assertFalse(sr.items[0].current_serial_and_batch_bundle) + batch = get_batch_from_bundle(sr.items[0].serial_and_batch_bundle) + + se1 = make_stock_entry( + item_code=item.name, + target=warehouse, + qty=50, + basic_rate=100, + posting_date=add_days(nowdate(), -2), + ) + + batch1 = get_batch_from_bundle(se1.items[0].serial_and_batch_bundle) + self.assertFalse(batch1 == batch) + + sr.reload() + self.assertTrue(sr.items[0].serial_and_batch_bundle) + self.assertFalse(sr.items[0].current_serial_and_batch_bundle) + def create_batch_item_with_batch(item_name, batch_id): batch_item_doc = create_item(item_name, is_stock_item=1) @@ -1085,7 +1169,7 @@ def create_stock_reconciliation(**args): ) bundle_id = None - if not args.use_serial_batch_fields and (args.batch_no or args.serial_no): + if not args.use_serial_batch_fields and (args.batch_no or args.serial_no) and args.qty: batches = frappe._dict({}) if args.batch_no: batches[args.batch_no] = args.qty 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..ddc03120e92 --- /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", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "json": "{}", + "letter_head": "", + "letterhead": null, + "modified": "2024-04-23 17:18:19.779036", + "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..07fcf36c827 --- /dev/null +++ b/erpnext/stock/report/available_batch_report/available_batch_report.py @@ -0,0 +1,178 @@ +# 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) + batchwise_data = get_batchwise_data_from_serial_batch_bundle(batchwise_data, 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_batchwise_data_from_serial_batch_bundle(batchwise_data, filters): + table = frappe.qb.DocType("Stock Ledger Entry") + ch_table = frappe.qb.DocType("Serial and Batch Entry") + batch = frappe.qb.DocType("Batch") + + query = ( + frappe.qb.from_(table) + .inner_join(ch_table) + .on(table.serial_and_batch_bundle == ch_table.parent) + .inner_join(batch) + .on(ch_table.batch_no == batch.name) + .select( + table.item_code, + ch_table.batch_no, + table.warehouse, + Sum(ch_table.qty).as_("balance_qty"), + ) + .where((table.is_cancelled == 0) & (table.docstatus == 1)) + .groupby(ch_table.batch_no, table.item_code, ch_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) + if key in batchwise_data: + batchwise_data[key].balance_qty += flt(d.balance_qty) + else: + 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/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index 97e2f5579d7..04cd1b846b3 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -61,6 +61,8 @@ def execute(filters=None): actual_qty += flt(sle.actual_qty, precision) stock_value += sle.stock_value_difference batch_balance_dict[sle.batch_no] += sle.actual_qty + if filters.get("segregate_serial_batch_bundle"): + actual_qty = batch_balance_dict[sle.batch_no] if sle.voucher_type == "Stock Reconciliation" and not sle.actual_qty: actual_qty = sle.qty_after_transaction diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index ec4c3682cb2..5c5fd83af2f 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -840,7 +840,7 @@ class update_entries_after: def reset_actual_qty_for_stock_reco(self, sle): doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no) - doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0) + doc.recalculate_current_qty(sle.voucher_detail_no) if sle.actual_qty < 0: sle.actual_qty = ( @@ -1728,6 +1728,10 @@ def get_stock_reco_qty_shift(args): stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance) else: stock_reco_qty_shift = flt(args.actual_qty) + + elif args.get("serial_and_batch_bundle"): + stock_reco_qty_shift = flt(args.actual_qty) + else: # reco is being submitted last_balance = get_previous_sle_of_current_voucher(args, "<=", exclude_current_voucher=True).get( @@ -1799,7 +1803,16 @@ def get_datetime_limit_condition(detail): def validate_negative_qty_in_future_sle(args, allow_negative_stock=False): if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code): return - if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"): + + if ( + args.voucher_type == "Stock Reconciliation" + and args.actual_qty < 0 + and args.get("serial_and_batch_bundle") + and frappe.db.get_value("Stock Reconciliation Item", args.voucher_detail_no, "qty") > 0 + ): + return + + if args.actual_qty >= 0 and args.voucher_type != "Stock Reconciliation": return neg_sle = get_future_sle_with_negative_qty(args) 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)),