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');