diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index 4354f238a422..c13dbe445f18 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -208,13 +208,17 @@ def auto_set_party(self): if self.party_type and self.party: return - result = AutoMatchParty( - bank_party_account_number=self.bank_party_account_number, - bank_party_iban=self.bank_party_iban, - bank_party_name=self.bank_party_name, - description=self.description, - deposit=self.deposit, - ).match() + result = None + try: + result = AutoMatchParty( + bank_party_account_number=self.bank_party_account_number, + bank_party_iban=self.bank_party_iban, + bank_party_name=self.bank_party_name, + description=self.description, + deposit=self.deposit, + ).match() + except Exception: + frappe.log_error(title=_("Error in party matching for Bank Transaction {0}").format(self.name)) if not result: return diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 68e9eef711a7..db4a4b0f268a 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -153,10 +153,7 @@ def get_nonreconciled_payment_entries(self): self.add_payment_entries(non_reconciled_payments) def get_payment_entries(self): - if self.default_advance_account: - party_account = [self.receivable_payable_account, self.default_advance_account] - else: - party_account = [self.receivable_payable_account] + party_account = [self.receivable_payable_account] order_doctype = "Sales Order" if self.party_type == "Customer" else "Purchase Order" condition = frappe._dict( @@ -187,6 +184,7 @@ def get_payment_entries(self): self.party, party_account, order_doctype, + default_advance_account=self.default_advance_account, against_all_orders=True, limit=self.payment_limit, condition=condition, diff --git a/erpnext/accounts/doctype/payment_request/payment_request_list.js b/erpnext/accounts/doctype/payment_request/payment_request_list.js index 1027385aaaf1..6e4aada66c61 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request_list.js +++ b/erpnext/accounts/doctype/payment_request/payment_request_list.js @@ -1,7 +1,7 @@ const INDICATORS = { "Partially Paid": "orange", Cancelled: "red", - Draft: "gray", + Draft: "red", Failed: "red", Initiated: "green", Paid: "blue", diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 087e2bffa4bd..551eaa3d1ced 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -651,8 +651,17 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None): qty = pricing_rule.free_qty or 1 if pricing_rule.is_recursive: - transaction_qty = (args.get("qty") if args else doc.total_qty) - pricing_rule.apply_recursion_over - if transaction_qty: + transaction_qty = sum( + [ + row.qty + for row in doc.items + if not row.is_free_item + and row.item_code == args.item_code + and row.pricing_rules == args.pricing_rules + ] + ) + transaction_qty = transaction_qty - pricing_rule.apply_recursion_over + if transaction_qty and transaction_qty > 0: qty = flt(transaction_qty) * qty / pricing_rule.recurse_for if pricing_rule.round_free_qty: qty = (flt(transaction_qty) // pricing_rule.recurse_for) * (pricing_rule.free_qty or 1) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js index f971f68a4546..3371a63cca28 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js @@ -15,7 +15,7 @@ frappe.listview_settings["Sales Invoice"] = { ], get_indicator: function (doc) { const status_colors = { - Draft: "grey", + Draft: "red", Unpaid: "orange", Paid: "green", Return: "gray", diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 04df22c9c9c5..69e3d241f126 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -35,9 +35,6 @@ def execute(filters=None): if filters.get("party"): filters.party = frappe.parse_json(filters.get("party")) - if filters.get("voucher_no") and not filters.get("group_by"): - filters.group_by = "Group by Voucher (Consolidated)" - validate_filters(filters, account_details) validate_party(filters) @@ -373,16 +370,21 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension if acc_dict.entries: # opening data.append({"debit_in_transaction_currency": None, "credit_in_transaction_currency": None}) - if filters.get("group_by") != "Group by Voucher": + if (not filters.get("group_by") and not filters.get("voucher_no")) or ( + filters.get("group_by") and filters.get("group_by") != "Group by Voucher" + ): data.append(acc_dict.totals.opening) data += acc_dict.entries # totals - data.append(acc_dict.totals.total) + if filters.get("group_by") or not filters.voucher_no: + data.append(acc_dict.totals.total) # closing - if filters.get("group_by") != "Group by Voucher": + if (not filters.get("group_by") and not filters.get("voucher_no")) or ( + filters.get("group_by") and filters.get("group_by") != "Group by Voucher" + ): data.append(acc_dict.totals.closing) data.append({"debit_in_transaction_currency": None, "credit_in_transaction_currency": None}) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index c6e76abb0b59..21e307b480cf 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -416,7 +416,7 @@ frappe.ui.form.on("Asset", { } frm.dashboard.render_graph({ - title: "Asset Value", + title: __("Asset Value"), data: { labels: x_intervals, datasets: [ diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index c788d5265a73..05d575ac8224 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -410,6 +410,9 @@ def set_depreciation_rate(self): ) def validate_asset_finance_books(self, row): + row.expected_value_after_useful_life = flt( + row.expected_value_after_useful_life, self.precision("gross_purchase_amount") + ) if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount): frappe.throw( _("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount").format( @@ -430,7 +433,10 @@ def validate_asset_finance_books(self, row): self.opening_accumulated_depreciation = 0 self.opening_number_of_booked_depreciations = 0 else: - depreciable_amount = flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life) + depreciable_amount = flt( + flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life), + self.precision("gross_purchase_amount"), + ) if flt(self.opening_accumulated_depreciation) > depreciable_amount: frappe.throw( _("Opening Accumulated Depreciation must be less than or equal to {0}").format( diff --git a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py index 1a250acd4d20..6d2034d18789 100644 --- a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py +++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py @@ -180,7 +180,7 @@ def prepare_data(data, filters): def prepare_chart_data(pending, completed): - labels = ["Amount to Bill", "Billed Amount"] + labels = [_("Amount to Bill"), _("Billed Amount")] return { "data": {"labels": labels, "datasets": [{"values": [pending, completed]}]}, diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index bc0cdde94dee..e1a7d5803c9a 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2916,6 +2916,7 @@ def get_advance_payment_entries( party_account, order_doctype, order_list=None, + default_advance_account=None, include_unallocated=True, against_all_orders=False, limit=None, @@ -2929,6 +2930,7 @@ def get_advance_payment_entries( party_type, party, party_account, + default_advance_account, limit, condition, ) @@ -2952,6 +2954,7 @@ def get_advance_payment_entries( party_type, party, party_account, + default_advance_account, limit, condition, ) @@ -2967,6 +2970,7 @@ def get_common_query( party_type, party, party_account, + default_advance_account, limit, condition, ): @@ -2988,14 +2992,22 @@ def get_common_query( .where(payment_entry.docstatus == 1) ) - if payment_type == "Receive": - q = q.select((payment_entry.paid_from_account_currency).as_("currency")) - q = q.select(payment_entry.paid_from) - q = q.where(payment_entry.paid_from.isin(party_account)) + field = "paid_from" if payment_type == "Receive" else "paid_to" + + q = q.select((payment_entry[f"{field}_account_currency"]).as_("currency")) + q = q.select(payment_entry[field]) + account_condition = payment_entry[field].isin(party_account) + if default_advance_account: + q = q.where( + account_condition + | ( + (payment_entry[field] == default_advance_account) + & (payment_entry.book_advance_payments_in_separate_party_account == 1) + ) + ) + else: - q = q.select((payment_entry.paid_to_account_currency).as_("currency")) - q = q.select(payment_entry.paid_to) - q = q.where(payment_entry.paid_to.isin(party_account)) + q = q.where(account_condition) if payment_type == "Receive": q = q.select((payment_entry.source_exchange_rate).as_("exchange_rate")) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 3a0bcf6b47ef..fe498ca32f96 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -291,6 +291,13 @@ frappe.ui.form.on("BOM", { cur_dialog.refresh(); }, }); + + fields.push({ + fieldtype: "Check", + label: __("Use Multi-Level BOM"), + fieldname: "use_multi_level_bom", + default: frm.doc?.__onload.use_multi_level_bom, + }); } var has_template_rm = frm.doc.items.filter((d) => d.has_variants === 1) || []; diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 395e9e830056..78b95f2c80c0 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -214,6 +214,23 @@ def get_index_for_bom(self, existing_boms): return index + def onload(self): + super().onload() + + self.set_onload_for_muulti_level_bom() + + def set_onload_for_muulti_level_bom(self): + use_multi_level_bom = frappe.db.get_value( + "Property Setter", + {"field_name": "use_multi_level_bom", "doc_type": "Work Order", "property": "default"}, + "value", + ) + + if use_multi_level_bom is None: + use_multi_level_bom = 1 + + self.set_onload("use_multi_level_bom", cint(use_multi_level_bom)) + @staticmethod def get_next_version_index(existing_boms: list[str]) -> int: # split by "/" and "-" @@ -259,6 +276,24 @@ def validate(self): self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate=False, save=False) self.set_process_loss_qty() self.validate_scrap_items() + self.set_default_uom() + + def set_default_uom(self): + if not self.get("items"): + return + + item_wise_uom = frappe._dict( + frappe.get_all( + "Item", + filters={"name": ("in", [item.item_code for item in self.items])}, + fields=["name", "stock_uom"], + as_list=1, + ) + ) + + for row in self.get("items"): + if row.stock_uom != item_wise_uom.get(row.item_code): + row.stock_uom = item_wise_uom.get(row.item_code) def get_context(self, context): context.parents = [{"name": "boms", "title": _("All BOMs")}] diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index d02b51ca6e7a..396a0b107d5d 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -755,6 +755,26 @@ def test_do_not_include_manufacturing_and_fixed_items(self): self.assertTrue("_Test RM Item 2 Fixed Asset Item" not in items) self.assertTrue("_Test RM Item 3 Manufacture Item" in items) + def test_bom_raw_materials_stock_uom(self): + rm_item = make_item( + properties={"is_stock_item": 1, "valuation_rate": 1000.0, "stock_uom": "Nos"} + ).name + fg_item = make_item(properties={"is_stock_item": 1}).name + + from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + + bom = make_bom(item=fg_item, raw_materials=[rm_item], do_not_submit=True) + for row in bom.items: + self.assertEqual(row.stock_uom, "Nos") + + frappe.db.set_value("Item", rm_item, "stock_uom", "Kg") + + bom.items[0].qty = 2 + bom.save() + + for row in bom.items: + self.assertEqual(row.stock_uom, "Kg") + def get_default_bom(item_code="_Test FG Item 2"): return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index 84bbad58c389..22971d4debdf 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -243,7 +243,7 @@ "depends_on": "eval:!doc.__islocal", "fieldname": "download_materials_required", "fieldtype": "Button", - "label": "Download Materials Request Plan" + "label": "Download Required Materials" }, { "fieldname": "get_items_for_mr", @@ -398,7 +398,7 @@ "collapsible": 1, "fieldname": "download_materials_request_plan_section_section", "fieldtype": "Section Break", - "label": "Download Materials Request Plan Section" + "label": "Preview Required Materials" }, { "default": "0", @@ -439,7 +439,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-02-27 13:34:20.692211", + "modified": "2024-12-04 11:55:03.108971", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan", @@ -463,4 +463,4 @@ "sort_field": "modified", "sort_order": "ASC", "states": [] -} \ No newline at end of file +} diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 3f82a75d3020..265f99e47d3c 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -44,9 +44,7 @@ class ProductionPlan(Document): from erpnext.manufacturing.doctype.material_request_plan_item.material_request_plan_item import ( MaterialRequestPlanItem, ) - from erpnext.manufacturing.doctype.production_plan_item.production_plan_item import ( - ProductionPlanItem, - ) + from erpnext.manufacturing.doctype.production_plan_item.production_plan_item import ProductionPlanItem from erpnext.manufacturing.doctype.production_plan_item_reference.production_plan_item_reference import ( ProductionPlanItemReference, ) @@ -1085,24 +1083,33 @@ def download_raw_materials(doc, warehouses=None): frappe.flags.show_qty_in_stock_uom = 1 items = get_items_for_material_requests(doc, warehouses=warehouses, get_parent_warehouse_data=True) + duplicate_item_wh_list = frappe._dict() + for d in items: - item_list.append( - [ - d.get("item_code"), - d.get("item_name"), - d.get("description"), - d.get("stock_uom"), - d.get("warehouse"), - d.get("required_bom_qty"), - d.get("projected_qty"), - d.get("actual_qty"), - d.get("ordered_qty"), - d.get("planned_qty"), - d.get("reserved_qty_for_production"), - d.get("safety_stock"), - d.get("quantity"), - ] - ) + key = (d.get("item_code"), d.get("warehouse")) + if key in duplicate_item_wh_list: + rm_data = duplicate_item_wh_list[key] + rm_data[12] += d.get("quantity") + continue + + rm_data = [ + d.get("item_code"), + d.get("item_name"), + d.get("description"), + d.get("stock_uom"), + d.get("warehouse"), + d.get("required_bom_qty"), + d.get("projected_qty"), + d.get("actual_qty"), + d.get("ordered_qty"), + d.get("planned_qty"), + d.get("reserved_qty_for_production"), + d.get("safety_stock"), + d.get("quantity"), + ] + + duplicate_item_wh_list[key] = rm_data + item_list.append(rm_data) if not doc.get("for_warehouse"): row = {"item_code": d.get("item_code")} diff --git a/erpnext/manufacturing/report/production_analytics/production_analytics.py b/erpnext/manufacturing/report/production_analytics/production_analytics.py index c02c1e6fcd38..dc2b9ad62f36 100644 --- a/erpnext/manufacturing/report/production_analytics/production_analytics.py +++ b/erpnext/manufacturing/report/production_analytics/production_analytics.py @@ -131,11 +131,11 @@ def get_chart_data(periodic_data, columns): pending.append(periodic_data.get("Pending").get(d)) completed.append(periodic_data.get("Completed").get(d)) - datasets.append({"name": "All Work Orders", "values": all_data}) - datasets.append({"name": "Not Started", "values": not_start}) - datasets.append({"name": "Overdue", "values": overdue}) - datasets.append({"name": "Pending", "values": pending}) - datasets.append({"name": "Completed", "values": completed}) + datasets.append({"name": _("All Work Orders"), "values": all_data}) + datasets.append({"name": _("Not Started"), "values": not_start}) + datasets.append({"name": _("Overdue"), "values": overdue}) + datasets.append({"name": _("Pending"), "values": pending}) + datasets.append({"name": _("Completed"), "values": completed}) chart = {"data": {"labels": labels, "datasets": datasets}} chart["type"] = "line" diff --git a/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py index 38e05852ee8d..7e0fcf14cc63 100644 --- a/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py +++ b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py @@ -42,7 +42,7 @@ def get_data(filters): def get_chart_data(periodic_data, columns): - labels = ["Rejected", "Accepted"] + labels = [_("Rejected"), _("Accepted")] status_wise_data = {"Accepted": 0, "Rejected": 0} @@ -53,7 +53,7 @@ def get_chart_data(periodic_data, columns): datasets.append( { - "name": "Qty Wise Chart", + "name": _("Qty Wise Chart"), "values": [status_wise_data.get("Rejected"), status_wise_data.get("Accepted")], } ) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index f53769155bde..a916478d4b37 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -384,3 +384,4 @@ erpnext.patches.v15_0.update_task_assignee_email_field_in_asset_maintenance_log erpnext.patches.v15_0.update_sub_voucher_type_in_gl_entries erpnext.patches.v14_0.update_stock_uom_in_work_order_item erpnext.patches.v15_0.set_is_exchange_gain_loss_in_payment_entry_deductions +erpnext.patches.v15_0.enable_allow_existing_serial_no diff --git a/erpnext/patches/v15_0/enable_allow_existing_serial_no.py b/erpnext/patches/v15_0/enable_allow_existing_serial_no.py new file mode 100644 index 000000000000..e13adc2b1875 --- /dev/null +++ b/erpnext/patches/v15_0/enable_allow_existing_serial_no.py @@ -0,0 +1,6 @@ +import frappe + + +def execute(): + if frappe.get_all("Company", filters={"country": "India"}, limit=1): + frappe.db.set_single_value("Stock Settings", "allow_existing_serial_no", 1) diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js index 9aba75b3ce92..168b891e98c3 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.js +++ b/erpnext/projects/doctype/timesheet/timesheet.js @@ -58,10 +58,10 @@ frappe.ui.form.on("Timesheet", { } if (frm.doc.docstatus < 1) { - let button = "Start Timer"; + let button = __("Start Timer"); $.each(frm.doc.time_logs || [], function (i, row) { if (row.from_time <= frappe.datetime.now_datetime() && !row.completed) { - button = "Resume Timer"; + button = __("Resume Timer"); } }); diff --git a/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py b/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py index 766e40e319c5..dc3da2596622 100644 --- a/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py +++ b/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py @@ -77,7 +77,7 @@ def get_chart_data(data): charts = { "data": { "labels": [_("On Track"), _("Delayed")], - "datasets": [{"name": "Delayed", "values": [on_track, delay]}], + "datasets": [{"name": _("Delayed"), "values": [on_track, delay]}], }, "type": "percentage", "colors": ["#84D5BA", "#CB4B5F"], diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json index 1ea19aaaf560..9dd65a8b4f88 100644 --- a/erpnext/selling/doctype/quotation_item/quotation_item.json +++ b/erpnext/selling/doctype/quotation_item/quotation_item.json @@ -455,6 +455,7 @@ "fieldtype": "Column Break" }, { + "allow_on_submit": 1, "fieldname": "projected_qty", "fieldtype": "Float", "label": "Projected Qty", @@ -690,7 +691,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2024-11-24 15:18:43.952844", + "modified": "2024-12-12 13:49:17.765883", "modified_by": "Administrator", "module": "Selling", "name": "Quotation Item", diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 45f6b3647613..de4053458e47 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -146,7 +146,7 @@ frappe.ui.form.on("Sales Order", { target: frm, setters: [ { - label: "Supplier", + label: __("Supplier"), fieldname: "supplier", fieldtype: "Link", options: "Supplier", @@ -783,7 +783,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex target: me.frm, setters: [ { - label: "Customer", + label: __("Customer"), fieldname: "party_name", fieldtype: "Link", options: "Customer", @@ -838,7 +838,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex } else { const fields = [ { - label: "Items", + label: __("Items"), fieldtype: "Table", fieldname: "items", description: __("Select BOM and Qty for Production"), @@ -1193,7 +1193,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex { fieldname: "items_for_po", fieldtype: "Table", - label: "Select Items", + label: __("Select Items"), fields: [ { fieldtype: "Data", diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index de3259ae2b74..c1127c39cc04 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -30,7 +30,7 @@ erpnext.PointOfSale.Controller = class { fieldname: "mode_of_payment", fieldtype: "Link", in_list_view: 1, - label: "Mode of Payment", + label: __("Mode of Payment"), options: "Mode of Payment", reqd: 1, }, @@ -38,7 +38,7 @@ erpnext.PointOfSale.Controller = class { fieldname: "opening_amount", fieldtype: "Currency", in_list_view: 1, - label: "Opening Amount", + label: __("Opening Amount"), options: "company:company_currency", change: function () { dialog.fields_dict.balance_details.df.data.some((d) => { @@ -87,7 +87,7 @@ erpnext.PointOfSale.Controller = class { { fieldname: "balance_details", fieldtype: "Table", - label: "Opening Balance Details", + label: __("Opening Balance Details"), cannot_add_rows: false, in_place_edit: true, reqd: 1, diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index b808b4f8828d..6342b237f6e0 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -966,13 +966,15 @@ erpnext.PointOfSale.ItemCart = class { if (!res.length) { transaction_container.html( - `
No recent transactions found
` + `
${__("No recent transactions found")}
` ); return; } const elapsed_time = moment(res[0].posting_date + " " + res[0].posting_time).fromNow(); - this.$customer_section.find(".customer-desc").html(`Last transacted ${elapsed_time}`); + this.$customer_section + .find(".customer-desc") + .html(`${__("Last transacted")} ${__(elapsed_time)}`); res.forEach((invoice) => { const posting_datetime = moment(invoice.posting_date + " " + invoice.posting_time).format( @@ -997,7 +999,7 @@ erpnext.PointOfSale.ItemCart = class {
- ${invoice.status} + ${__(invoice.status)}
diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js index c399005643c3..4a2d8911d1a8 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js @@ -46,7 +46,7 @@ erpnext.PointOfSale.PastOrderSummary = class { init_email_print_dialog() { const email_dialog = new frappe.ui.Dialog({ - title: "Email Receipt", + title: __("Email Receipt"), fields: [ { fieldname: "email_id", fieldtype: "Data", options: "Email", label: "Email ID", reqd: 1 }, { fieldname: "content", fieldtype: "Small Text", label: "Message (if any)" }, @@ -59,7 +59,7 @@ erpnext.PointOfSale.PastOrderSummary = class { this.email_dialog = email_dialog; const print_dialog = new frappe.ui.Dialog({ - title: "Print Receipt", + title: __("Print Receipt"), fields: [{ fieldname: "print", fieldtype: "Data", label: "Print Preview" }], primary_action: () => { this.print_receipt(); @@ -112,7 +112,7 @@ erpnext.PointOfSale.PastOrderSummary = class { get_discount_html(doc) { if (doc.discount_amount) { return `
-
Discount (${doc.additional_discount_percentage} %)
+
${__("Discount")} (${doc.additional_discount_percentage} %)
${format_currency(doc.discount_amount, doc.currency)}
`; } else { diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index 9f389dfa81f8..bea1918fa201 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -589,7 +589,7 @@ erpnext.PointOfSale.Payment = class { const remaining = grand_total - doc.paid_amount; const change = doc.change_amount || remaining <= 0 ? -1 * remaining : undefined; const currency = doc.currency; - const label = change ? __("Change") : __("To Be Paid"); + const label = __("Change Amount"); this.$totals.html( `
diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py index cf61a0e35f3b..1b57a6d7390e 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py @@ -270,11 +270,11 @@ def prepare_chart(s_orders): "labels": [term.payment_term for term in s_orders], "datasets": [ { - "name": "Payment Amount", + "name": _("Payment Amount"), "values": [x.base_payment_amount for x in s_orders], }, { - "name": "Paid Amount", + "name": _("Paid Amount"), "values": [x.paid_amount for x in s_orders], }, ], diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py index 70b021a9cab1..8fcf29bd7a6c 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py @@ -206,7 +206,7 @@ def prepare_data(data, so_elapsed_time, filters): def prepare_chart_data(pending, completed): - labels = ["Amount to Bill", "Billed Amount"] + labels = [_("Amount to Bill"), _("Billed Amount")] return { "data": {"labels": labels, "datasets": [{"values": [pending, completed]}]}, diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 0bc6b28fe681..2b4dad137c25 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -1046,7 +1046,7 @@ def get_pending_qty(item_row): automatically_fetch_payment_terms = cint( frappe.db.get_single_value("Accounts Settings", "automatically_fetch_payment_terms") ) - if automatically_fetch_payment_terms: + if automatically_fetch_payment_terms and not doc.is_return: doc.set_payment_schedule() return doc diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py index 4f8a166932d6..661605bdf5f0 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py @@ -214,13 +214,10 @@ def add_custom_fields(self): dimension_fields = [] if self.apply_to_all_doctypes: for doctype in get_inventory_documents(): - if field_exists(doctype[0], self.source_fieldname): - continue - dimension_fields = self.get_dimension_fields(doctype[0]) self.add_transfer_field(doctype[0], dimension_fields) custom_fields.setdefault(doctype[0], dimension_fields) - elif not field_exists(self.document_type, self.source_fieldname): + else: dimension_fields = self.get_dimension_fields() self.add_transfer_field(self.document_type, dimension_fields) @@ -239,8 +236,17 @@ def add_custom_fields(self): dimension_field["fieldname"] = self.target_fieldname custom_fields["Stock Ledger Entry"] = dimension_field + filter_custom_fields = {} if custom_fields: - create_custom_fields(custom_fields) + for doctype, fields in custom_fields.items(): + if isinstance(fields, dict): + fields = [fields] + + for field in fields: + if not field_exists(doctype, field["fieldname"]): + filter_custom_fields.setdefault(doctype, []).append(field) + + create_custom_fields(filter_custom_fields) def add_transfer_field(self, doctype, dimension_fields): if doctype not in [ diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index f04acc85ed54..09c01a0ee883 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -107,6 +107,14 @@ frappe.ui.form.on("Material Request", { if (flt(frm.doc.per_received, precision) < 100) { frm.add_custom_button(__("Stop"), () => frm.events.update_status(frm, "Stopped")); + + if (frm.doc.material_request_type === "Purchase") { + frm.add_custom_button( + __("Purchase Order"), + () => frm.events.make_purchase_order(frm), + __("Create") + ); + } } if (flt(frm.doc.per_ordered, precision) < 100) { @@ -149,14 +157,6 @@ frappe.ui.form.on("Material Request", { ); } - if (frm.doc.material_request_type === "Purchase") { - frm.add_custom_button( - __("Purchase Order"), - () => frm.events.make_purchase_order(frm), - __("Create") - ); - } - if (frm.doc.material_request_type === "Purchase") { frm.add_custom_button( __("Request for Quotation"), @@ -259,18 +259,21 @@ frappe.ui.form.on("Material Request", { }, callback: function (r) { const d = item; - const allow_to_change_fields = [ + let allow_to_change_fields = [ "actual_qty", "projected_qty", "min_order_qty", "item_name", - "description", "stock_uom", "uom", "conversion_factor", "stock_qty", ]; + if (overwrite_warehouse) { + allow_to_change_fields.push("description"); + } + if (!r.exc) { $.each(r.message, function (key, value) { if (!d[key] || allow_to_change_fields.includes(key)) { diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 7bf3ca4d7285..23d289170dba 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -378,7 +378,9 @@ def set_missing_values(source, target_doc): def update_item(obj, target, source_parent): target.conversion_factor = obj.conversion_factor - target.qty = flt(flt(obj.stock_qty) - flt(obj.ordered_qty)) / target.conversion_factor + + qty = obj.received_qty or obj.ordered_qty + target.qty = flt(flt(obj.stock_qty) - flt(qty)) / target.conversion_factor target.stock_qty = target.qty * target.conversion_factor if getdate(target.schedule_date) < getdate(nowdate()): target.schedule_date = None @@ -430,7 +432,9 @@ def select_item(d): filtered_items = args.get("filtered_children", []) child_filter = d.name in filtered_items if filtered_items else True - return d.ordered_qty < d.stock_qty and child_filter + qty = d.received_qty or d.ordered_qty + + return qty < d.stock_qty and child_filter doclist = get_mapped_doc( "Material Request", diff --git a/erpnext/stock/doctype/pick_list/pick_list_list.js b/erpnext/stock/doctype/pick_list/pick_list_list.js index 9cdbfe187205..eca6eece7855 100644 --- a/erpnext/stock/doctype/pick_list/pick_list_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list_list.js @@ -4,7 +4,7 @@ frappe.listview_settings["Pick List"] = { get_indicator: function (doc) { const status_colors = { - Draft: "grey", + Draft: "red", Open: "orange", Completed: "green", Cancelled: "red", diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 4b8d5101f43a..b097c0e6441e 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -3948,6 +3948,105 @@ def test_purchase_return_partial_debit_note(self): self.assertEqual(return_pr.per_billed, 100) self.assertEqual(return_pr.status, "Completed") + def test_do_not_allow_to_inward_same_serial_no_multiple_times(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + frappe.db.set_single_value("Stock Settings", "allow_existing_serial_no", 0) + + item_code = make_item( + "Test Do Not Allow INWD Item 123", {"has_serial_no": 1, "serial_no_series": "SN-TDAISN-.#####"} + ).name + + pr = make_purchase_receipt(item_code=item_code, qty=1, rate=100, use_serial_batch_fields=1) + serial_no = get_serial_nos_from_bundle(pr.items[0].serial_and_batch_bundle)[0] + + status = frappe.db.get_value("Serial No", serial_no, "status") + self.assertTrue(status == "Active") + + make_stock_entry( + item_code=item_code, + source=pr.items[0].warehouse, + qty=1, + serial_no=serial_no, + use_serial_batch_fields=1, + ) + + status = frappe.db.get_value("Serial No", serial_no, "status") + self.assertFalse(status == "Active") + + pr = make_purchase_receipt( + item_code=item_code, qty=1, rate=100, use_serial_batch_fields=1, do_not_submit=1 + ) + pr.items[0].serial_no = serial_no + pr.save() + + self.assertRaises(frappe.exceptions.ValidationError, pr.submit) + + frappe.db.set_single_value("Stock Settings", "allow_existing_serial_no", 1) + + def test_seral_no_return_validation(self): + from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( + make_purchase_return, + ) + + sn_item_code = make_item( + "Test Serial No for Validation", {"has_serial_no": 1, "serial_no_series": "SN-TSNFVAL-.#####"} + ).name + + pr1 = make_purchase_receipt(item_code=sn_item_code, qty=5, rate=100, use_serial_batch_fields=1) + pr1_serial_nos = get_serial_nos_from_bundle(pr1.items[0].serial_and_batch_bundle) + + serial_no_pr = make_purchase_receipt( + item_code=sn_item_code, qty=5, rate=100, use_serial_batch_fields=1 + ) + serial_no_pr_serial_nos = get_serial_nos_from_bundle(serial_no_pr.items[0].serial_and_batch_bundle) + + sn_return = make_purchase_return(serial_no_pr.name) + sn_return.items[0].qty = -1 + sn_return.items[0].received_qty = -1 + sn_return.items[0].serial_no = pr1_serial_nos[0] + sn_return.save() + self.assertRaises(frappe.ValidationError, sn_return.submit) + + sn_return = make_purchase_return(serial_no_pr.name) + sn_return.items[0].qty = -1 + sn_return.items[0].received_qty = -1 + sn_return.items[0].serial_no = serial_no_pr_serial_nos[0] + sn_return.save() + sn_return.submit() + + def test_batch_no_return_validation(self): + from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( + make_purchase_return, + ) + + batch_item_code = make_item( + "Test Batch No for Validation", + {"has_batch_no": 1, "batch_number_series": "BT-TSNFVAL-.#####", "create_new_batch": 1}, + ).name + + pr1 = make_purchase_receipt(item_code=batch_item_code, qty=5, rate=100, use_serial_batch_fields=1) + batch_no = get_batch_from_bundle(pr1.items[0].serial_and_batch_bundle) + + batch_no_pr = make_purchase_receipt( + item_code=batch_item_code, qty=5, rate=100, use_serial_batch_fields=1 + ) + original_batch_no = get_batch_from_bundle(batch_no_pr.items[0].serial_and_batch_bundle) + + batch_return = make_purchase_return(batch_no_pr.name) + batch_return.items[0].qty = -1 + batch_return.items[0].received_qty = -1 + batch_return.items[0].batch_no = batch_no + batch_return.save() + self.assertRaises(frappe.ValidationError, batch_return.submit) + + batch_return = make_purchase_return(batch_no_pr.name) + batch_return.items[0].qty = -1 + batch_return.items[0].received_qty = -1 + batch_return.items[0].batch_no = original_batch_no + batch_return.save() + batch_return.submit() + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier 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 fff9cbfc07d1..dc2071b9ee07 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 @@ -89,6 +89,10 @@ def validate(self): self.validate_serial_and_batch_no() self.validate_duplicate_serial_and_batch_no() self.validate_voucher_no() + + if self.docstatus == 0: + self.allow_existing_serial_nos() + if self.type_of_transaction == "Maintenance": return @@ -102,6 +106,42 @@ def validate(self): self.set_incoming_rate() self.calculate_qty_and_amount() + def allow_existing_serial_nos(self): + if self.type_of_transaction == "Outward" or not self.has_serial_no: + return + + if frappe.db.get_single_value("Stock Settings", "allow_existing_serial_no"): + return + + if self.voucher_type not in ["Purchase Receipt", "Purchase Invoice", "Stock Entry"]: + return + + if self.voucher_type == "Stock Entry" and frappe.get_cached_value( + "Stock Entry", self.voucher_no, "purpose" + ) in ["Material Transfer", "Send to Subcontractor", "Material Transfer for Manufacture"]: + return + + serial_nos = [d.serial_no for d in self.entries if d.serial_no] + + data = frappe.get_all( + "Serial and Batch Entry", + filters={"serial_no": ("in", serial_nos), "docstatus": 1, "qty": ("<", 0)}, + fields=["serial_no", "parent"], + ) + + note = "

Note:
" + for row in data: + frappe.throw( + _( + "You can't process the serial number {0} as it has already been used in the SABB {1}. {2} if you want to inward same serial number multiple times then enabled 'Allow existing Serial No to be Manufactured/Received again' in the {3}" + ).format( + row.serial_no, + get_link_to_form("Serial and Batch Bundle", row.parent), + note, + get_link_to_form("Stock Settings", "Stock Settings"), + ) + ) + def reset_serial_batch_bundle(self): if self.is_new() and self.amended_from: for field in ["is_cancelled", "is_rejected"]: @@ -136,7 +176,12 @@ def validate_serial_nos_inventory(self): return serial_nos = [d.serial_no for d in self.entries if d.serial_no] - kwargs = {"item_code": self.item_code, "warehouse": self.warehouse} + kwargs = { + "item_code": self.item_code, + "warehouse": self.warehouse, + "check_serial_nos": True, + "serial_nos": serial_nos, + } if self.voucher_type == "POS Invoice": kwargs["ignore_voucher_nos"] = [self.voucher_no] @@ -177,6 +222,7 @@ def validate_serial_nos_duplicate(self): "posting_date": self.posting_date, "posting_time": self.posting_time, "serial_nos": serial_nos, + "check_serial_nos": True, } ) @@ -206,8 +252,8 @@ def set_incoming_rate(self, parent=None, row=None, save=False, allow_negative_st ]: return - if return_aginst := self.get_return_aginst(parent=parent): - self.set_valuation_rate_for_return_entry(return_aginst, save) + if return_against := self.get_return_against(parent=parent): + self.set_valuation_rate_for_return_entry(return_against, save) elif self.type_of_transaction == "Outward": self.set_incoming_rate_for_outward_transaction( row, save, allow_negative_stock=allow_negative_stock @@ -215,15 +261,18 @@ def set_incoming_rate(self, parent=None, row=None, save=False, allow_negative_st else: self.set_incoming_rate_for_inward_transaction(row, save) - def set_valuation_rate_for_return_entry(self, return_aginst, save=False): - if valuation_details := self.get_valuation_rate_for_return_entry(return_aginst): + def set_valuation_rate_for_return_entry(self, return_against, save=False): + if valuation_details := self.get_valuation_rate_for_return_entry(return_against): for row in self.entries: + if valuation_details: + self.validate_returned_serial_batch_no(return_against, row, valuation_details) + if row.serial_no: valuation_rate = valuation_details["serial_nos"].get(row.serial_no) else: valuation_rate = valuation_details["batches"].get(row.batch_no) - row.incoming_rate = valuation_rate + row.incoming_rate = flt(valuation_rate) row.stock_value_difference = flt(row.qty) * flt(row.incoming_rate) if save: @@ -234,7 +283,22 @@ def set_valuation_rate_for_return_entry(self, return_aginst, save=False): } ) - def get_valuation_rate_for_return_entry(self, return_aginst): + def validate_returned_serial_batch_no(self, return_against, row, original_inv_details): + if row.serial_no and row.serial_no not in original_inv_details["serial_nos"]: + self.throw_error_message( + _( + "Serial No {0} is not present in the {1} {2}, hence you can't return it against the {1} {2}" + ).format(bold(row.serial_no), self.voucher_type, bold(return_against)) + ) + + if row.batch_no and row.batch_no not in original_inv_details["batches"]: + self.throw_error_message( + _( + "Batch No {0} is not present in the original {1} {2}, hence you can't return it against the {1} {2}" + ).format(bold(row.batch_no), self.voucher_type, bold(return_against)) + ) + + def get_valuation_rate_for_return_entry(self, return_against): valuation_details = frappe._dict( { "serial_nos": defaultdict(float), @@ -250,7 +314,7 @@ def get_valuation_rate_for_return_entry(self, return_aginst): "`tabSerial and Batch Entry`.`incoming_rate`", ], filters=[ - ["Serial and Batch Bundle", "voucher_no", "=", return_aginst], + ["Serial and Batch Bundle", "voucher_no", "=", return_against], ["Serial and Batch Entry", "docstatus", "=", 1], ["Serial and Batch Bundle", "is_cancelled", "=", 0], ["Serial and Batch Bundle", "item_code", "=", self.item_code], @@ -384,8 +448,8 @@ def get_sle_for_outward_transaction(self): return sle - def get_return_aginst(self, parent=None): - return_aginst = None + def get_return_against(self, parent=None): + return_against = None if parent and parent.get("is_return") and parent.get("return_against"): return parent.get("return_against") @@ -409,7 +473,7 @@ def get_return_aginst(self, parent=None): if voucher_details and voucher_details.get("is_return") and voucher_details.get("return_against"): return voucher_details.get("return_against") - return return_aginst + return return_against def set_incoming_rate_for_inward_transaction(self, row=None, save=False): valuation_field = "valuation_rate" @@ -1683,7 +1747,7 @@ def get_serial_nos_based_on_posting_date(kwargs, ignore_serial_nos): serial_nos = set() data = get_stock_ledgers_for_serial_nos(kwargs) - bundle_wise_serial_nos = get_bundle_wise_serial_nos(data) + bundle_wise_serial_nos = get_bundle_wise_serial_nos(data, kwargs) for d in data: if d.serial_and_batch_bundle: if sns := bundle_wise_serial_nos.get(d.serial_and_batch_bundle): @@ -1707,16 +1771,21 @@ def get_serial_nos_based_on_posting_date(kwargs, ignore_serial_nos): return serial_nos -def get_bundle_wise_serial_nos(data): +def get_bundle_wise_serial_nos(data, kwargs): bundle_wise_serial_nos = defaultdict(list) bundles = [d.serial_and_batch_bundle for d in data if d.serial_and_batch_bundle] if not bundles: return bundle_wise_serial_nos + filters = {"parent": ("in", bundles), "docstatus": 1, "serial_no": ("is", "set")} + + if kwargs.get("check_serial_nos") and kwargs.get("serial_nos"): + filters["serial_no"] = ("in", kwargs.get("serial_nos")) + bundle_data = frappe.get_all( "Serial and Batch Entry", fields=["serial_no", "parent"], - filters={"parent": ("in", bundles), "docstatus": 1, "serial_no": ("is", "set")}, + filters=filters, ) for d in bundle_data: @@ -2277,6 +2346,8 @@ def get_ledgers_from_serial_batch_bundle(**kwargs) -> list[frappe._dict]: def get_stock_ledgers_for_serial_nos(kwargs): + from erpnext.stock.utils import get_combine_datetime + stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry") query = ( @@ -2287,15 +2358,16 @@ def get_stock_ledgers_for_serial_nos(kwargs): stock_ledger_entry.serial_and_batch_bundle, ) .where(stock_ledger_entry.is_cancelled == 0) + .orderby(stock_ledger_entry.posting_datetime) ) if kwargs.get("posting_date"): if kwargs.get("posting_time") is None: kwargs.posting_time = nowtime() - timestamp_condition = CombineDatetime( - stock_ledger_entry.posting_date, stock_ledger_entry.posting_time - ) <= CombineDatetime(kwargs.posting_date, kwargs.posting_time) + timestamp_condition = stock_ledger_entry.posting_datetime <= get_combine_datetime( + kwargs.posting_date, kwargs.posting_time + ) query = query.where(timestamp_condition) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 469b865dd591..a26940462bf1 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -1645,6 +1645,46 @@ def test_auto_reorder_level(self): mr.cancel() mr.delete() + def test_auto_reorder_level_with_lead_time_days(self): + from erpnext.stock.reorder_item import reorder_item + + item_doc = make_item( + "Test Auto Reorder Item - 002", + properties={"stock_uom": "Kg", "purchase_uom": "Nos", "is_stock_item": 1, "lead_time_days": 2}, + uoms=[{"uom": "Nos", "conversion_factor": 5}], + ) + + if not frappe.db.exists("Item Reorder", {"parent": item_doc.name}): + item_doc.append( + "reorder_levels", + { + "warehouse_reorder_level": 0, + "warehouse_reorder_qty": 10, + "warehouse": "_Test Warehouse - _TC", + "material_request_type": "Purchase", + }, + ) + + item_doc.save(ignore_permissions=True) + + frappe.db.set_single_value("Stock Settings", "auto_indent", 1) + + mr_list = reorder_item() + + frappe.db.set_single_value("Stock Settings", "auto_indent", 0) + mrs = frappe.get_all( + "Material Request Item", + fields=["schedule_date"], + filters={"item_code": item_doc.name, "uom": "Nos"}, + ) + + for mri in mrs: + self.assertEqual(getdate(mri.schedule_date), getdate(add_days(today(), 2))) + + for mr in mr_list: + mr.cancel() + mr.delete() + def test_use_serial_and_batch_fields(self): item = make_item( "Test Use Serial and Batch Item SN Item", diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index 319856780095..85acc7629692 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -207,6 +207,7 @@ frappe.ui.form.on("Stock Reconciliation", { posting_time: frm.doc.posting_time, batch_no: d.batch_no, row: d, + company: frm.doc.company, }, callback: function (r) { const row = frappe.model.get_doc(cdt, cdn); diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index f671c11712a4..12773a5555db 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -166,6 +166,24 @@ def set_current_serial_and_batch_bundle(self, voucher_detail_no=None, save=False if not frappe.db.exists("Item", item.item_code): frappe.throw(_("Item {0} does not exist").format(item.item_code)) + item_details = frappe.get_cached_value( + "Item", item.item_code, ["has_serial_no", "has_batch_no"], as_dict=1 + ) + + if not (item_details.has_serial_no or item_details.has_batch_no): + continue + + if ( + not item.use_serial_batch_fields + and not item.reconcile_all_serial_batch + and not item.serial_and_batch_bundle + ): + frappe.throw( + _("Row # {0}: Please add Serial and Batch Bundle for Item {1}").format( + item.idx, frappe.bold(item.item_code) + ) + ) + if not item.reconcile_all_serial_batch and item.serial_and_batch_bundle: bundle = self.get_bundle_for_specific_serial_batch(item) item.current_serial_and_batch_bundle = bundle.name @@ -181,13 +199,6 @@ def set_current_serial_and_batch_bundle(self, voucher_detail_no=None, save=False if voucher_detail_no and voucher_detail_no != item.name: continue - item_details = frappe.get_cached_value( - "Item", item.item_code, ["has_serial_no", "has_batch_no"], as_dict=1 - ) - - if not (item_details.has_serial_no or item_details.has_batch_no): - continue - if not item.current_serial_and_batch_bundle: serial_and_batch_bundle = frappe.get_doc( { @@ -400,6 +411,28 @@ def set_new_serial_and_batch_bundle(self): item.qty = bundle_doc.total_qty item.valuation_rate = bundle_doc.avg_rate + elif item.serial_and_batch_bundle and item.qty: + self.update_existing_serial_and_batch_bundle(item) + + def update_existing_serial_and_batch_bundle(self, item): + batch_details = frappe.get_all( + "Serial and Batch Entry", + fields=["batch_no", "qty", "name"], + filters={"parent": item.serial_and_batch_bundle, "batch_no": ("is", "set")}, + ) + + if batch_details and len(batch_details) == 1: + batch = batch_details[0] + if abs(batch.qty) == abs(item.qty): + return + + update_values = { + "qty": item.qty, + "stock_value_difference": flt(item.valuation_rate) * flt(item.qty), + } + + frappe.db.set_value("Serial and Batch Entry", batch.name, update_values) + def remove_items_with_no_change(self): """Remove items if qty or rate is not changed""" self.difference_amount = 0.0 @@ -433,6 +466,7 @@ def _changed(item): batch_no=item.batch_no, inventory_dimensions_dict=inventory_dimensions_dict, row=item, + company=self.company, ) if ( @@ -941,6 +975,7 @@ def recalculate_current_qty(self, voucher_detail_no): self.posting_date, self.posting_time, row=row, + company=self.company, ) current_qty = item_dict.get("qty") @@ -1275,6 +1310,7 @@ def get_stock_balance_for( with_valuation_rate: bool = True, inventory_dimensions_dict=None, row=None, + company=None, ): frappe.has_permission("Stock Reconciliation", "write", throw=True) @@ -1334,6 +1370,21 @@ def get_stock_balance_for( or 0 ) + if row.use_serial_batch_fields and row.batch_no: + rate = get_incoming_rate( + frappe._dict( + { + "item_code": row.item_code, + "warehouse": row.warehouse, + "qty": row.qty * -1, + "batch_no": row.batch_no, + "company": company, + "posting_date": posting_date, + "posting_time": posting_time, + } + ) + ) + return { "qty": qty, "rate": rate, diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index 069e7da41cb3..e542a1582e38 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -49,12 +49,13 @@ "do_not_use_batchwise_valuation", "auto_create_serial_and_batch_bundle_for_outward", "pick_serial_and_batch_based_on", + "naming_series_prefix", "column_break_mhzc", "disable_serial_no_and_batch_selector", "use_naming_series", - "naming_series_prefix", "use_serial_batch_fields", "do_not_update_serial_batch_on_creation_of_auto_bundle", + "allow_existing_serial_no", "stock_planning_tab", "auto_material_request", "auto_indent", @@ -460,6 +461,12 @@ "fieldname": "over_picking_allowance", "fieldtype": "Percent", "label": "Over Picking Allowance" + }, + { + "default": "1", + "fieldname": "allow_existing_serial_no", + "fieldtype": "Check", + "label": "Allow existing Serial No to be Manufactured/Received again" } ], "icon": "icon-cog", @@ -467,7 +474,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2024-07-29 14:55:19.093508", + "modified": "2024-12-09 17:52:36.030456", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index 229ff9447507..b7a317cd66a3 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -25,6 +25,7 @@ class StockSettings(Document): action_if_quality_inspection_is_not_submitted: DF.Literal["Stop", "Warn"] action_if_quality_inspection_is_rejected: DF.Literal["Stop", "Warn"] + allow_existing_serial_no: DF.Check allow_from_dn: DF.Check allow_from_pr: DF.Check allow_internal_transfer_at_arms_length_price: DF.Check diff --git a/erpnext/stock/reorder_item.py b/erpnext/stock/reorder_item.py index ed87906731e4..570dc3a34050 100644 --- a/erpnext/stock/reorder_item.py +++ b/erpnext/stock/reorder_item.py @@ -98,6 +98,7 @@ def add_to_material_request(**kwargs): "description": d.description, "stock_uom": d.stock_uom, "purchase_uom": d.purchase_uom, + "lead_time_days": d.lead_time_days, } ), ) @@ -129,6 +130,7 @@ def get_items_for_reorder() -> dict[str, list]: item_table.brand, item_table.variant_of, item_table.has_variants, + item_table.lead_time_days, ) .where( (item_table.disabled == 0) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index dd459bb30bc4..05b3536c57ba 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -508,7 +508,7 @@ def calculate_stock_value_change(self): serial_nos = self.get_serial_nos() for serial_no in serial_nos: incoming_rate = self.get_incoming_rate_from_bundle(serial_no) - if not incoming_rate: + if incoming_rate is None: continue self.stock_value_change += incoming_rate @@ -553,7 +553,7 @@ def get_incoming_rate_from_bundle(self, serial_no) -> float: query = query.where(timestamp_condition) incoming_rate = query.run() - return flt(incoming_rate[0][0]) if incoming_rate else 0.0 + return flt(incoming_rate[0][0]) if incoming_rate else None def get_serial_nos(self): if self.sle.get("serial_nos"): diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json index b8bda8329832..206e3135dfb6 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json @@ -294,7 +294,7 @@ "fieldname": "total", "fieldtype": "Currency", "label": "Total", - "options": "currency", + "options": "Company:company:default_currency", "read_only": 1 }, { @@ -353,6 +353,7 @@ "fieldname": "total_additional_costs", "fieldtype": "Currency", "label": "Total Additional Costs", + "options": "Company:company:default_currency", "print_hide_if_no_value": 1, "read_only": 1 }, @@ -450,21 +451,21 @@ "options": "Project" }, { - "fieldname": "tab_other_info", - "fieldtype": "Tab Break", - "label": "Other Info" + "fieldname": "tab_other_info", + "fieldtype": "Tab Break", + "label": "Other Info" }, { - "fieldname": "tab_connections", - "fieldtype": "Tab Break", - "label": "Connections", - "show_dashboard": 1 + "fieldname": "tab_connections", + "fieldtype": "Tab Break", + "label": "Connections", + "show_dashboard": 1 } ], "icon": "fa fa-file-text", "is_submittable": 1, "links": [], - "modified": "2024-01-03 20:56:04.670380", + "modified": "2024-12-06 15:21:49.924146", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Order", @@ -519,4 +520,4 @@ "timeline_field": "supplier", "title_field": "supplier_name", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js index 895ffdbf88c3..f5486bbd59d6 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js @@ -4,7 +4,7 @@ frappe.listview_settings["Subcontracting Order"] = { get_indicator: function (doc) { const status_colors = { - Draft: "grey", + Draft: "red", Open: "orange", "Partially Received": "yellow", Completed: "green", diff --git a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json index 1ca90c31654d..502a28b3ec62 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json @@ -185,7 +185,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Rate", - "options": "currency", + "options": "Company:company:default_currency", "read_only": 1, "reqd": 1 }, @@ -199,7 +199,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Amount", - "options": "currency", + "options": "Company:company:default_currency", "read_only": 1, "reqd": 1 }, @@ -269,6 +269,7 @@ "fieldname": "service_cost_per_qty", "fieldtype": "Currency", "label": "Service Cost Per Qty", + "options": "Company:company:default_currency", "read_only": 1, "reqd": 1 }, @@ -277,6 +278,7 @@ "fieldname": "additional_cost_per_qty", "fieldtype": "Currency", "label": "Additional Cost Per Qty", + "options": "Company:company:default_currency", "read_only": 1 }, { @@ -284,6 +286,7 @@ "fieldtype": "Currency", "label": "Raw Material Cost Per Qty", "no_copy": 1, + "options": "Company:company:default_currency", "read_only": 1 }, { @@ -384,7 +387,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-30 15:29:43.744618", + "modified": "2024-12-06 15:23:05.252346", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Order Item", @@ -397,4 +400,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.py b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.py index fcd143c1dd94..7a426f91cb00 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.py +++ b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.py @@ -26,8 +26,11 @@ class SubcontractingOrderItem(Document): include_exploded_items: DF.Check item_code: DF.Link item_name: DF.Data + job_card: DF.Link | None manufacturer: DF.Link | None manufacturer_part_no: DF.Data | None + material_request: DF.Link | None + material_request_item: DF.Data | None page_break: DF.Check parent: DF.Data parentfield: DF.Data diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json index ad03171f29a2..b8bd95bcbcae 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json @@ -336,7 +336,7 @@ "fieldname": "total", "fieldtype": "Currency", "label": "Total", - "options": "currency", + "options": "Company:company:default_currency", "read_only": 1 }, { @@ -618,6 +618,7 @@ "fieldname": "total_additional_costs", "fieldtype": "Currency", "label": "Total Additional Costs", + "options": "Company:company:default_currency", "print_hide_if_no_value": 1, "read_only": 1 }, @@ -656,27 +657,27 @@ "fieldtype": "Column Break" }, { - "fieldname": "tab_other_info", - "fieldtype": "Tab Break", - "label": "Other Info" + "fieldname": "tab_other_info", + "fieldtype": "Tab Break", + "label": "Other Info" }, { - "collapsible": 1, - "fieldname": "order_status_section", - "fieldtype": "Section Break", - "label": "Order Status" + "collapsible": 1, + "fieldname": "order_status_section", + "fieldtype": "Section Break", + "label": "Order Status" }, { - "fieldname": "tab_connections", - "fieldtype": "Tab Break", - "label": "Connections", - "show_dashboard": 1 + "fieldname": "tab_connections", + "fieldtype": "Tab Break", + "label": "Connections", + "show_dashboard": 1 } ], "in_create": 1, "is_submittable": 1, "links": [], - "modified": "2024-05-28 15:02:13.517969", + "modified": "2024-12-06 15:24:38.384232", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt", diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt_list.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt_list.js index be6c0d0b18f3..5f5a6db01e0a 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt_list.js +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt_list.js @@ -4,7 +4,7 @@ frappe.listview_settings["Subcontracting Receipt"] = { get_indicator: function (doc) { const status_colors = { - Draft: "grey", + Draft: "red", Return: "gray", "Return Issued": "grey", Completed: "green", diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json index 75e263e2c1cb..23a7e69669d3 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json @@ -207,7 +207,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Rate", - "options": "currency", + "options": "Company:company:default_currency", "print_width": "100px", "read_only": 1, "width": "100px" @@ -217,7 +217,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Amount", - "options": "currency", + "options": "Company:company:default_currency", "read_only": 1 }, { @@ -231,6 +231,7 @@ "fieldtype": "Currency", "label": "Raw Material Cost Per Qty", "no_copy": 1, + "options": "Company:company:default_currency", "read_only": 1 }, { @@ -239,6 +240,7 @@ "fieldname": "service_cost_per_qty", "fieldtype": "Currency", "label": "Service Cost Per Qty", + "options": "Company:company:default_currency", "read_only": 1, "reqd": 1 }, @@ -248,6 +250,7 @@ "fieldname": "additional_cost_per_qty", "fieldtype": "Currency", "label": "Additional Cost Per Qty", + "options": "Company:company:default_currency", "read_only": 1 }, { @@ -582,7 +585,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2024-03-29 15:42:43.425544", + "modified": "2024-12-06 15:23:58.680169", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt Item", diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py index 1a4ce5b977a2..69f7ae73e7a5 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py @@ -28,6 +28,7 @@ class SubcontractingReceiptItem(Document): is_scrap_item: DF.Check item_code: DF.Link item_name: DF.Data | None + job_card: DF.Link | None manufacturer: DF.Link | None manufacturer_part_no: DF.Data | None page_break: DF.Check diff --git a/erpnext/templates/includes/issue_row.html b/erpnext/templates/includes/issue_row.html index a04f558509f6..b55712ab1894 100644 --- a/erpnext/templates/includes/issue_row.html +++ b/erpnext/templates/includes/issue_row.html @@ -18,7 +18,7 @@ {% if doc.status == "Open" %} {{ doc.priority }} {% else %} - {{ doc.status }} + {{ _(doc.status) }} {%- endif -%}
diff --git a/erpnext/templates/includes/projects/project_row.html b/erpnext/templates/includes/projects/project_row.html index 686637a20146..ccb306afcdb0 100644 --- a/erpnext/templates/includes/projects/project_row.html +++ b/erpnext/templates/includes/projects/project_row.html @@ -20,7 +20,7 @@ {% else %} - {{ doc.status }} + {{ _(doc.status) }} {% endif %} {% if doc["_assign"] %} diff --git a/erpnext/templates/includes/transaction_row.html b/erpnext/templates/includes/transaction_row.html index a498ba0eefaf..061c9bd1796e 100644 --- a/erpnext/templates/includes/transaction_row.html +++ b/erpnext/templates/includes/transaction_row.html @@ -8,7 +8,7 @@
- {{doc.status}} + {{ _(doc.status) }}
diff --git a/erpnext/templates/pages/timelog_info.html b/erpnext/templates/pages/timelog_info.html index be13826444c9..9f9445661a07 100644 --- a/erpnext/templates/pages/timelog_info.html +++ b/erpnext/templates/pages/timelog_info.html @@ -38,7 +38,7 @@

{{ doc.name }}

- +