diff --git a/rmax_custom/hooks.py b/rmax_custom/hooks.py index 0f3c338..5b151f2 100644 --- a/rmax_custom/hooks.py +++ b/rmax_custom/hooks.py @@ -265,6 +265,7 @@ "before_validate": "rmax_custom.branch_defaults.override_cost_center_from_branch", "validate": "rmax_custom.inter_branch.auto_set_branch_from_warehouse", "before_submit": "rmax_custom.api.delivery_note.before_submit_return_dn_guard", + "on_submit": "rmax_custom.inter_company_dn.on_delivery_note_submit", "on_cancel": "rmax_custom.api.delivery_note.clear_consolidated_return_dn_stamp", }, "Purchase Receipt": { diff --git a/rmax_custom/inter_company_dn.py b/rmax_custom/inter_company_dn.py index f1d9310..7a42e9a 100644 --- a/rmax_custom/inter_company_dn.py +++ b/rmax_custom/inter_company_dn.py @@ -386,6 +386,72 @@ def _validate_batch(dns): ) +def on_delivery_note_submit(doc, method=None): + """Auto-create Inter Company Purchase Receipt on DN submit. + + Triggered for DNs with custom_is_inter_company=1 that don't already + have a linked PR (inter_company_reference). Runs with ignore_permissions + so branch users don't need supplier/warehouse/company access. + """ + if not doc.custom_is_inter_company: + return + if doc.inter_company_reference: + return + + try: + from erpnext.stock.doctype.delivery_note.delivery_note import ( + make_inter_company_purchase_receipt, + ) + + _prev_user = frappe.session.user + frappe.session.user = "Administrator" + try: + pr = make_inter_company_purchase_receipt(doc.name) + finally: + frappe.session.user = _prev_user + + # Inject Purchase Taxes from the Inter Company Branch master + inter_company_branch = doc.get("custom_inter_company_branch") + if inter_company_branch: + taxes_template = frappe.db.get_value( + "Inter Company Branch", inter_company_branch, "purchase_taxes_and_charges" + ) + if taxes_template: + template_doc = frappe.get_doc("Purchase Taxes and Charges Template", taxes_template) + pr.taxes_and_charges = taxes_template + pr.set("taxes", []) + for tax in template_doc.taxes: + pr.append("taxes", { + "charge_type": tax.charge_type, + "account_head": tax.account_head, + "description": tax.description, + "rate": tax.rate, + "cost_center": tax.cost_center, + "included_in_print_rate": tax.included_in_print_rate, + }) + + pr.flags.ignore_permissions = True + pr.insert(ignore_permissions=True) + + doc.db_set("inter_company_reference", pr.name) + + frappe.msgprint( + frappe._('Purchase Receipt {0} created').format(pr.name), + alert=True, + indicator="green", + ) + except Exception: + frappe.log_error( + title="Inter-Company PR auto-creation failed", + message=frappe.get_traceback(), + ) + frappe.msgprint( + frappe._("Could not auto-create Purchase Receipt. Please create manually."), + indicator="orange", + alert=True, + ) + + def _normalise_names(delivery_note_names) -> List[str]: if isinstance(delivery_note_names, str): import json diff --git a/rmax_custom/public/js/branch_user_restrict.js b/rmax_custom/public/js/branch_user_restrict.js index 44a5780..2cdfcf9 100644 --- a/rmax_custom/public/js/branch_user_restrict.js +++ b/rmax_custom/public/js/branch_user_restrict.js @@ -29,6 +29,7 @@ "Purchase Invoice", "Material Request", "Stock Transfer", + "Inter Branch Stock Transfer", "Damage Slip", "Damage Transfer", "Supplier Code", diff --git a/rmax_custom/rmax_custom/doctype/inter_branch_stock_transfer/inter_branch_stock_transfer.js b/rmax_custom/rmax_custom/doctype/inter_branch_stock_transfer/inter_branch_stock_transfer.js index 6ec1987..c678d11 100644 --- a/rmax_custom/rmax_custom/doctype/inter_branch_stock_transfer/inter_branch_stock_transfer.js +++ b/rmax_custom/rmax_custom/doctype/inter_branch_stock_transfer/inter_branch_stock_transfer.js @@ -1,23 +1,32 @@ // Copyright (c) 2026, Enfono and contributors // For license information, please see license.txt +frappe.listview_settings['Inter Branch Stock Transfer'] = { + add_fields: ['status'], + get_indicator: function (doc) { + if (doc.status === 'Return') return [__('Return'), 'grey', 'status,=,Return']; + } +}; + frappe.ui.form.on('Inter Branch Stock Transfer', { onload: function (frm) { _setup_warehouse_queries(frm); - _setup_price_list_query(frm); _setup_taxes_and_charges_query(frm); - if (frm.is_new()) { + if (frm.is_new() && !frm.doc.is_return) { _set_default_source_warehouse(frm); + frm.set_value('to_warehouse', 'HQ Awtad - CL'); + frm.set_value('price_list', 'Inter Company Price'); } + _lock_system_fields(frm); }, refresh: function (frm) { _setup_warehouse_queries(frm); - _setup_price_list_query(frm); _setup_taxes_and_charges_query(frm); _setup_buttons(frm); _toggle_posting_time(frm); + _lock_system_fields(frm); }, set_posting_time: function (frm) { @@ -28,6 +37,12 @@ frappe.ui.form.on('Inter Branch Stock Transfer', { _setup_warehouse_queries(frm); _setup_taxes_and_charges_query(frm); _set_default_taxes_and_charges(frm); + if (!frm.doc.is_return) { + frm.set_value('to_warehouse', 'HQ Awtad - CL'); + } + if (!frm.doc.price_list) { + frm.set_value('price_list', 'Inter Company Price'); + } }, from_warehouse: function (frm) { @@ -139,6 +154,7 @@ function _toggle_posting_time(frm) { function _setup_buttons(frm) { frm.remove_custom_button(__('Create Delivery Note')); + frm.remove_custom_button(__('Create Return')); if (frm.doc.docstatus !== 1) return; @@ -161,28 +177,37 @@ function _setup_buttons(frm) { ); }, __('Create')); } + + // Return button — only if no return created yet and not itself a return + if (!frm.doc.return_ibst && !frm.doc.is_return) { + frm.add_custom_button(__('Create Return'), function () { + frappe.confirm( + __('Create a return document for {0}?', [frm.doc.name]), + function () { + frappe.call({ + method: 'rmax_custom.rmax_custom.doctype.inter_branch_stock_transfer.inter_branch_stock_transfer.create_ibst_return_doc', + args: { ibst_name: frm.doc.name }, + freeze: true, + freeze_message: __('Creating return document...'), + callback: function (r) { + if (r.message) { + frappe.set_route('Form', 'Inter Branch Stock Transfer', r.message); + } + } + }); + } + ); + }, __('Create')); + } } function _setup_warehouse_queries(frm) { frm.set_query('from_warehouse', function () { return { filters: { company: frm.doc.company, is_group: 0 } }; }); - frm.set_query('to_warehouse', function () { - return { - ignore_user_permissions: 1, - filters: { - company: frm.doc.company, - is_group: 0, - name: ['!=', frm.doc.from_warehouse || ''] - } - }; - }); frm.fields_dict['items'].grid.get_field('s_warehouse').get_query = function (doc) { return { filters: { company: doc.company, is_group: 0 } }; }; - frm.fields_dict['items'].grid.get_field('t_warehouse').get_query = function (doc) { - return { ignore_user_permissions: 1, filters: { company: doc.company, is_group: 0 } }; - }; } function _set_default_source_warehouse(frm) { @@ -257,12 +282,6 @@ function _recalculate_row_amount(cdt, cdn) { frappe.model.set_value(cdt, cdn, 'transfer_qty', flt(row.qty) * flt(row.conversion_factor || 1)); } -function _setup_price_list_query(frm) { - frm.set_query('price_list', function () { - return { filters: { selling: 1, enabled: 1 } }; - }); -} - function _setup_taxes_and_charges_query(frm) { frm.set_query('taxes_and_charges', function () { return { filters: { company: frm.doc.company, disabled: 0 } }; @@ -310,3 +329,12 @@ function _fetch_price_list_rates_for_all_items(frm) { } }); } + +function _lock_system_fields(frm) { + // Target warehouse is always HQ — never user-editable (both regular and return) + frm.set_df_property('to_warehouse', 'read_only', 1); + // Price list is always Inter Company Price — never user-editable + frm.set_df_property('price_list', 'read_only', 1); + // Lock t_warehouse in item rows (always pushed from header to_warehouse) + frm.fields_dict['items'] && frm.fields_dict['items'].grid.toggle_enable('t_warehouse', false); +} diff --git a/rmax_custom/rmax_custom/doctype/inter_branch_stock_transfer/inter_branch_stock_transfer.json b/rmax_custom/rmax_custom/doctype/inter_branch_stock_transfer/inter_branch_stock_transfer.json index 0f755bb..89e5d3f 100644 --- a/rmax_custom/rmax_custom/doctype/inter_branch_stock_transfer/inter_branch_stock_transfer.json +++ b/rmax_custom/rmax_custom/doctype/inter_branch_stock_transfer/inter_branch_stock_transfer.json @@ -9,6 +9,9 @@ "naming_series", "company", "amended_from", + "is_return", + "return_against", + "status", "column_break_header", "posting_date", "posting_time", @@ -28,7 +31,8 @@ "stock_entry", "journal_entry", "column_break_refs", - "delivery_note" + "delivery_note", + "return_ibst" ], "fields": [ { @@ -63,6 +67,37 @@ "print_hide": 1, "read_only": 1 }, + { + "default": "0", + "fieldname": "is_return", + "fieldtype": "Check", + "label": "Is Return", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "default": "Submitted", + "fieldname": "status", + "fieldtype": "Select", + "hidden": 1, + "in_list_view": 1, + "label": "Status", + "no_copy": 1, + "options": "Submitted\nReturn", + "print_hide": 1, + "read_only": 1 + }, + { + "depends_on": "eval:doc.is_return", + "fieldname": "return_against", + "fieldtype": "Link", + "label": "Return Against", + "no_copy": 1, + "options": "Inter Branch Stock Transfer", + "print_hide": 1, + "read_only": 1 + }, { "fieldname": "column_break_header", "fieldtype": "Column Break" @@ -113,6 +148,7 @@ "ignore_user_permissions": 1, "label": "Set Target Warehouse", "options": "Warehouse", + "read_only": 1, "reqd": 1 }, { @@ -135,10 +171,12 @@ "read_only": 1 }, { + "default": "Inter Company Price", "fieldname": "price_list", "fieldtype": "Link", "label": "Price List", - "options": "Price List" + "options": "Price List", + "read_only": 1 }, { "fieldname": "taxes_and_charges", @@ -193,6 +231,17 @@ "options": "Delivery Note", "print_hide": 1, "read_only": 1 + }, + { + "depends_on": "eval:doc.return_ibst", + "fieldname": "return_ibst", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Return IBST", + "no_copy": 1, + "options": "Inter Branch Stock Transfer", + "print_hide": 1, + "read_only": 1 } ], "grid_page_length": 50, diff --git a/rmax_custom/rmax_custom/doctype/inter_branch_stock_transfer/inter_branch_stock_transfer.py b/rmax_custom/rmax_custom/doctype/inter_branch_stock_transfer/inter_branch_stock_transfer.py index 07928ab..bed9dc2 100644 --- a/rmax_custom/rmax_custom/doctype/inter_branch_stock_transfer/inter_branch_stock_transfer.py +++ b/rmax_custom/rmax_custom/doctype/inter_branch_stock_transfer/inter_branch_stock_transfer.py @@ -10,9 +10,16 @@ from frappe.utils import flt +HO_WAREHOUSE = "HQ Awtad - CL" +INTER_COMPANY_PRICE_LIST = "Inter Company Price" + + class InterBranchStockTransfer(Document): def validate(self): + # Both regular and return IBST always go TO HQ — branch user only selects source + self.to_warehouse = HO_WAREHOUSE + self.price_list = INTER_COMPANY_PRICE_LIST self._set_item_warehouses_from_header() self._set_default_taxes_and_charges() @@ -21,7 +28,7 @@ def _set_item_warehouses_from_header(self): for item in self.items or []: if self.from_warehouse and not item.s_warehouse: item.s_warehouse = self.from_warehouse - if self.to_warehouse and not item.t_warehouse: + if self.to_warehouse: item.t_warehouse = self.to_warehouse def _set_default_taxes_and_charges(self): @@ -97,6 +104,7 @@ def _validate_stock_availability(self): # ------------------------------------------------------------------ def on_submit(self): + self.db_set("status", "Return" if self.is_return else "Submitted") source_branch, target_branch = self._resolve_branches_or_throw() self._create_stock_entry(source_branch, target_branch) if self.customer: @@ -193,7 +201,7 @@ def _create_journal_entry( "exchange_rate": 1, "debit_in_account_currency": amount, "debit": amount, - "branch": source_branch, + "branch": source_branch or None, "custom_auto_inserted": 1, "custom_source_doctype": "Inter Branch Stock Transfer", "custom_source_docname": self.name, @@ -204,7 +212,7 @@ def _create_journal_entry( "exchange_rate": 1, "credit_in_account_currency": amount, "credit": amount, - "branch": target_branch, + "branch": target_branch or None, "custom_auto_inserted": 1, "custom_source_doctype": "Inter Branch Stock Transfer", "custom_source_docname": self.name, @@ -301,7 +309,8 @@ def _create_delivery_note(self, target_branch: str | None = None): "item_code": item.item_code, "qty": item.qty, "uom": item.uom, - "warehouse": item.t_warehouse or self.to_warehouse, + "warehouse": item.s_warehouse or self.from_warehouse, + "target_warehouse": item.t_warehouse or self.to_warehouse, "rate": item.basic_rate, "cost_center": target_cost_center, "description": item.description or item.item_name, @@ -328,6 +337,45 @@ def _create_delivery_note(self, target_branch: str | None = None): indicator="green", ) + def _create_return_delivery_note(self, original_dn_name: str): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + + # make_return_doc handles dn_detail linking and ERPNext return validations + return_dn = make_return_doc("Delivery Note", original_dn_name) + + # Fix warehouses: internal transfer return needs warehouse ↔ target_warehouse swapped + # Original DN: warehouse=Jeddah, target_warehouse=HQ Awtad + # Return DN: warehouse=Jeddah (same, is_return reverses movement), target_warehouse=HQ Awtad + original_dn = frappe.get_doc("Delivery Note", original_dn_name) + orig_by_detail = {item.name: item for item in original_dn.items} + for ret_item in return_dn.items or []: + orig = orig_by_detail.get(ret_item.dn_detail) + if orig and orig.target_warehouse: + ret_item.warehouse = orig.warehouse + ret_item.target_warehouse = orig.target_warehouse + + # Override qty from return IBST items (partial return support) + ibst_qty_by_code = {item.item_code: flt(item.qty) for item in self.items or []} + for ret_item in return_dn.items or []: + ibst_qty = ibst_qty_by_code.get(ret_item.item_code) + if ibst_qty is not None: + ret_item.qty = -abs(ibst_qty) + + return_dn.set_posting_time = 1 + return_dn.posting_date = self.posting_date + return_dn.posting_time = self.posting_time + return_dn.custom_ibst = self.name + + return_dn.flags.ignore_permissions = True + return_dn.insert(ignore_permissions=True) + return_dn.flags.ignore_permissions = True + return_dn.submit() + self.db_set("delivery_note", return_dn.name, notify=True) + frappe.msgprint( + _('Return Delivery Note {0} created').format(return_dn.name), + alert=True, + indicator="green", + ) # ------------------------------------------------------------------ # Cancel @@ -441,6 +489,60 @@ def make_journal_entry(ibst_name: str) -> str: return je_name +@frappe.whitelist() +def create_ibst_return_doc(ibst_name: str) -> str: + """Create a draft return IBST from a submitted IBST. + + The user can review and edit before submitting. + On submit, the return IBST auto-creates the reverse SE, return DN, and reversing JE. + """ + original = frappe.get_doc("Inter Branch Stock Transfer", ibst_name) + if original.docstatus != 1: + frappe.throw(_("Return can only be created from a submitted document.")) + + existing = frappe.db.get_value( + "Inter Branch Stock Transfer", + {"return_against": ibst_name, "docstatus": ["!=", 2]}, + "name", + ) + if existing: + frappe.throw( + _("Return already exists: {0}").format(existing), + title=_("Duplicate Return"), + ) + + return_doc = frappe.new_doc("Inter Branch Stock Transfer") + return_doc.is_return = 1 + return_doc.return_against = ibst_name + return_doc.company = original.company + return_doc.posting_date = frappe.utils.today() + return_doc.from_warehouse = original.from_warehouse + return_doc.to_warehouse = HO_WAREHOUSE + return_doc.customer = original.customer + return_doc.price_list = original.price_list or INTER_COMPANY_PRICE_LIST + return_doc.taxes_and_charges = original.taxes_and_charges + + for item in original.items or []: + return_doc.append("items", { + "item_code": item.item_code, + "item_name": item.item_name, + "qty": item.qty, + "uom": item.uom, + "conversion_factor": item.conversion_factor or 1, + "s_warehouse": original.from_warehouse, + "t_warehouse": HO_WAREHOUSE, + "basic_rate": item.basic_rate, + "valuation_rate": item.valuation_rate, + }) + + return_doc.flags.ignore_permissions = True + return_doc.insert(ignore_permissions=True) + + frappe.db.set_value("Inter Branch Stock Transfer", ibst_name, "return_ibst", return_doc.name) + + return return_doc.name + + @frappe.whitelist() def get_item_valuation_rate(item_code: str, warehouse: str) -> float: """Return current valuation rate from Bin, falling back to Item master.""" diff --git a/rmax_custom/rmax_custom/doctype/inter_branch_stock_transfer_item/inter_branch_stock_transfer_item.json b/rmax_custom/rmax_custom/doctype/inter_branch_stock_transfer_item/inter_branch_stock_transfer_item.json index 396daa2..7d3a4c7 100644 --- a/rmax_custom/rmax_custom/doctype/inter_branch_stock_transfer_item/inter_branch_stock_transfer_item.json +++ b/rmax_custom/rmax_custom/doctype/inter_branch_stock_transfer_item/inter_branch_stock_transfer_item.json @@ -114,6 +114,7 @@ { "fieldname": "t_warehouse", "fieldtype": "Link", + "ignore_user_permissions": 1, "in_list_view": 1, "label": "Target Warehouse", "options": "Warehouse" @@ -128,7 +129,8 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Rate", - "options": "Company:company:default_currency" + "options": "Company:company:default_currency", + "read_only": 1 }, { "fieldname": "basic_amount", diff --git a/rmax_custom/rmax_custom/doctype/inter_company_branch/inter_company_branch.json b/rmax_custom/rmax_custom/doctype/inter_company_branch/inter_company_branch.json index dbc58aa..7321155 100644 --- a/rmax_custom/rmax_custom/doctype/inter_company_branch/inter_company_branch.json +++ b/rmax_custom/rmax_custom/doctype/inter_company_branch/inter_company_branch.json @@ -7,6 +7,7 @@ "engine": "InnoDB", "field_order": [ "branch_name", + "purchase_taxes_and_charges", "company_cost_centers" ], "fields": [ @@ -18,6 +19,12 @@ "reqd": 1, "unique": 1 }, + { + "fieldname": "purchase_taxes_and_charges", + "fieldtype": "Link", + "label": "Purchase Taxes and Charges Template", + "options": "Purchase Taxes and Charges Template" + }, { "fieldname": "company_cost_centers", "fieldtype": "Table", diff --git a/rmax_custom/rmax_custom/doctype/stock_transfer/stock_transfer.js b/rmax_custom/rmax_custom/doctype/stock_transfer/stock_transfer.js index 8e1235b..d82c529 100644 --- a/rmax_custom/rmax_custom/doctype/stock_transfer/stock_transfer.js +++ b/rmax_custom/rmax_custom/doctype/stock_transfer/stock_transfer.js @@ -10,6 +10,22 @@ frappe.ui.form.on('Stock Transfer', { _setup_st_warehouse_queries(frm); }, refresh: function(frm) { + if (frm.doc.docstatus === 0 && frm.doc.workflow_state && frm.doc.workflow_state !== "Draft" + && !in_list(frappe.user_roles, 'System Manager')) { + frm.disable_save(); + frm.fields.forEach(function(field) { + if (field.df && !field.df.hidden) { + field.df.read_only = 1; + if (field.refresh) field.refresh(); + } + }); + if (frm.fields_dict.items) { + frm.fields_dict.items.grid.df.cannot_add_rows = 1; + frm.fields_dict.items.grid.df.cannot_delete_rows = 1; + frm.fields_dict.items.grid.refresh(); + } + } + if (frm.doc.__islocal && !frm.doc.__source_fixed) { setTimeout(() => { frm.set_value('set_target_warehouse', ''); diff --git a/rmax_custom/rmax_custom/page/rmax_dashboard/rmax_dashboard.js b/rmax_custom/rmax_custom/page/rmax_dashboard/rmax_dashboard.js index 02a0f4c..35c16e1 100644 --- a/rmax_custom/rmax_custom/page/rmax_dashboard/rmax_dashboard.js +++ b/rmax_custom/rmax_custom/page/rmax_dashboard/rmax_dashboard.js @@ -169,6 +169,7 @@ function render_branch_dashboard(data) { html += action_card('file-text', 'Purchase Invoice', 'View invoices', 'list', 'Purchase Invoice'); html += action_card('truck', 'Material Request', 'Request materials', 'list', 'Material Request'); html += action_card('shuffle', 'Stock Transfer', 'Transfer stock', 'list', 'Stock Transfer'); + html += action_card('arrow-right-circle', 'Inter Branch Transfer', 'Branch to HO transfer', 'list', 'Inter Branch Stock Transfer'); html += action_card('corner-down-left', 'Sales Return', 'Process returns', 'list', 'Sales Invoice', 'is_return=1'); html += action_card('corner-down-right', 'Purchase Return', 'Process returns', 'list', 'Purchase Invoice', 'is_return=1'); html += action_card('alert-triangle', 'Report Damage', 'Create damage slip', 'new', 'Damage Slip');