Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions rmax_custom/inter_branch.py
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,30 @@ def resolve_warehouse_branch(warehouse: str) -> str | None:
return rows[0][0] if rows else None


def resolve_branch_cost_center(branch: str) -> str | None:
"""Look up the default Cost Center for a Branch via Branch Configuration.

Mirrors resolve_warehouse_branch: returns the first (idx-ordered, i.e.
the "default" row per the Branch Configuration Cost Center convention)
Cost Center of the Branch Configuration linked to this branch, or None
when no mapping exists.
"""
if not branch:
return None
rows = frappe.db.sql(
"""
SELECT bcc.cost_center
FROM `tabBranch Configuration Cost Center` bcc
INNER JOIN `tabBranch Configuration` bc ON bc.name = bcc.parent
WHERE bc.branch = %s AND bcc.cost_center IS NOT NULL
ORDER BY bcc.idx ASC
LIMIT 1
""",
(branch,),
)
return rows[0][0] if rows else None


@frappe.whitelist()
def backfill_je_header_source() -> int:
"""Populate `custom_source_doctype` and `custom_source_docname` on the Journal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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()) {
_set_default_source_warehouse(frm);
}
Expand All @@ -14,6 +15,7 @@ frappe.ui.form.on('Inter Branch Stock Transfer', {
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);
},
Expand All @@ -24,6 +26,8 @@ frappe.ui.form.on('Inter Branch Stock Transfer', {

company: function (frm) {
_setup_warehouse_queries(frm);
_setup_taxes_and_charges_query(frm);
_set_default_taxes_and_charges(frm);
},

from_warehouse: function (frm) {
Expand Down Expand Up @@ -92,7 +96,9 @@ frappe.ui.form.on('Inter Branch Stock Transfer Item', {
frappe.model.set_value(cdt, cdn, 't_warehouse', frm.doc.to_warehouse);
}

_fetch_valuation_rate(frm, cdt, cdn);
_fetch_price_list_rate(frm, cdt, cdn, function () {
_fetch_valuation_rate(frm, cdt, cdn);
});
},

s_warehouse: function (frm, cdt, cdn) {
Expand Down Expand Up @@ -252,9 +258,32 @@ function _setup_price_list_query(frm) {
});
}

function _fetch_price_list_rate(frm, cdt, cdn) {
function _setup_taxes_and_charges_query(frm) {
frm.set_query('taxes_and_charges', function () {
return { filters: { company: frm.doc.company, disabled: 0 } };
});
}

function _set_default_taxes_and_charges(frm) {
if (!frm.doc.company || frm.doc.taxes_and_charges) return;
frappe.db.get_value(
'Sales Taxes and Charges Template',
{ company: frm.doc.company, is_default: 1 },
'name',
function (r) {
if (r && r.name) {
frm.set_value('taxes_and_charges', r.name);
}
}
);
}

function _fetch_price_list_rate(frm, cdt, cdn, on_not_found) {
let row = locals[cdt][cdn];
if (!row.item_code || !frm.doc.price_list) return;
if (!row.item_code || !frm.doc.price_list) {
if (on_not_found) on_not_found();
return;
}
frappe.call({
method: 'frappe.client.get_value',
args: {
Expand All @@ -267,6 +296,8 @@ function _fetch_price_list_rate(frm, cdt, cdn) {
frappe.model.set_value(cdt, cdn, 'basic_rate', r.message.price_list_rate);
frappe.model.set_value(cdt, cdn, 'valuation_rate', r.message.price_list_rate);
_recalculate_row_amount(cdt, cdn);
} else if (on_not_found) {
on_not_found();
}
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"customer",
"customer_name",
"price_list",
"taxes_and_charges",
"section_break_items",
"items",
"section_break_refs",
Expand Down Expand Up @@ -99,7 +100,8 @@
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Set Source Warehouse",
"options": "Warehouse"
"options": "Warehouse",
"reqd": 1
},
{
"fieldname": "column_break_wh",
Expand All @@ -110,7 +112,8 @@
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Set Target Warehouse",
"options": "Warehouse"
"options": "Warehouse",
"reqd": 1
},
{
"fieldname": "section_break_customer",
Expand All @@ -121,7 +124,8 @@
"fieldname": "customer",
"fieldtype": "Link",
"label": "Customer",
"options": "Customer"
"options": "Customer",
"reqd": 1
},
{
"fetch_from": "customer.customer_name",
Expand All @@ -136,6 +140,12 @@
"label": "Price List",
"options": "Price List"
},
{
"fieldname": "taxes_and_charges",
"fieldtype": "Link",
"label": "Sales Taxes and Charges Template",
"options": "Sales Taxes and Charges Template"
},
{
"fieldname": "section_break_items",
"fieldtype": "Section Break"
Expand Down Expand Up @@ -189,7 +199,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2026-07-01 00:00:00.000000",
"modified": "2026-07-02 10:42:59.541116",
"modified_by": "Administrator",
"module": "Rmax Custom",
"name": "Inter Branch Stock Transfer",
Expand Down Expand Up @@ -243,4 +253,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class InterBranchStockTransfer(Document):

def validate(self):
self._set_item_warehouses_from_header()
self._set_default_taxes_and_charges()

def _set_item_warehouses_from_header(self):
"""Push header warehouse defaults to item rows that have no row-level value."""
Expand All @@ -23,6 +24,13 @@ def _set_item_warehouses_from_header(self):
if self.to_warehouse and not item.t_warehouse:
item.t_warehouse = self.to_warehouse

def _set_default_taxes_and_charges(self):
"""Default to the company's default Sales Taxes and Charges Template, if unset."""
if self.is_new() and self.company and not self.taxes_and_charges:
self.taxes_and_charges = frappe.db.get_value(
"Sales Taxes and Charges Template", {"is_default": 1, "company": self.company}
)

def before_submit(self):
self._validate_warehouses()
self._validate_stock_availability()
Expand Down Expand Up @@ -89,47 +97,57 @@ def _validate_stock_availability(self):
# ------------------------------------------------------------------

def on_submit(self):
self._create_stock_entry()
source_branch, target_branch = self._resolve_branches_or_throw()
self._create_stock_entry(source_branch, target_branch)
if self.customer:
self._create_delivery_note()
self._create_delivery_note(target_branch)
try:
self._create_journal_entry()
self._create_journal_entry(source_branch, target_branch)
except Exception:
frappe.log_error(
title="Inter-Branch companion JE failed (IBST)",
message=frappe.get_traceback(),
)
raise

def _create_journal_entry(self) -> str | None:
"""Create and submit the inter-branch companion JE for this IBST.

Identical pattern to create_companion_inter_branch_je_for_stock_transfer:
Dr Due from <target_branch> (branch = source_branch)
Cr Due to <source_branch> (branch = target_branch)
def _resolve_branches_or_throw(self) -> tuple[str, str]:
"""Resolve (source_branch, target_branch) from Branch Configuration warehouse mapping.

Requires from_warehouse and to_warehouse to be mapped in Branch
Configuration so resolve_warehouse_branch can identify the branches.
Both warehouses must be mapped, or GL posting for the companion Stock
Entry / Delivery Note fails deep inside submit() with an opaque
"Accounting Dimension Branch is required" error instead of this
actionable one.
"""
from rmax_custom.inter_branch import (
resolve_warehouse_branch,
get_or_create_inter_branch_account,
)
from rmax_custom.inter_branch import resolve_warehouse_branch

source_branch = resolve_warehouse_branch(self.from_warehouse)
target_branch = resolve_warehouse_branch(self.to_warehouse)

if not source_branch or not target_branch:
frappe.throw(
_(
"Journal Entry cannot be created because the branch could not be "
"determined for one or both warehouses.<br><br>"
"Branch could not be determined for one or both warehouses.<br><br>"
"Go to <b>Branch Configuration</b> and add:<br>"
"• <b>{0}</b> under the Source Branch's Warehouse table<br>"
"• <b>{1}</b> under the Target Branch's Warehouse table"
).format(self.from_warehouse or "—", self.to_warehouse or "—"),
title=_("Warehouse Not Mapped to Branch"),
)
return source_branch, target_branch

def _create_journal_entry(
self, source_branch: str | None = None, target_branch: str | None = None
) -> str | None:
"""Create and submit the inter-branch companion JE for this IBST.

Identical pattern to create_companion_inter_branch_je_for_stock_transfer:
Dr Due from <target_branch> (branch = source_branch)
Cr Due to <source_branch> (branch = target_branch)
"""
from rmax_custom.inter_branch import get_or_create_inter_branch_account

if source_branch is None or target_branch is None:
source_branch, target_branch = self._resolve_branches_or_throw()

if source_branch == target_branch:
return None
Expand Down Expand Up @@ -204,10 +222,12 @@ def _create_journal_entry(self) -> str | None:
)
return je.name

def _create_stock_entry(self):
def _create_stock_entry(self, source_branch: str, target_branch: str):
se = frappe.new_doc("Stock Entry")
se.stock_entry_type = "Material Transfer"
se.company = self.company
se.branch = source_branch
se.set_posting_time = 1
se.posting_date = self.posting_date
se.posting_time = self.posting_time
se.from_warehouse = self.from_warehouse
Expand All @@ -226,6 +246,9 @@ def _create_stock_entry(self):
"allow_zero_valuation_rate": item.allow_zero_valuation_rate,
"serial_no": item.serial_no,
"batch_no": item.batch_no,
# Both legs are stamped with source_branch so GL insert passes the
# mandatory-dimension check; the target leg is corrected below.
"branch": source_branch,
})

# Prevent the SE on_submit hook from creating a second inter-branch JE.
Expand All @@ -237,17 +260,35 @@ def _create_stock_entry(self):
se.flags.ignore_permissions = True
se.submit()

if source_branch != target_branch:
# on_stock_entry_submit (which normally does this) is skipped for SEs
# flagged from_stock_transfer, so re-tag the target warehouse's GL
# leg(s) here instead.
from rmax_custom.inter_branch import _retag_se_gl_entries

_retag_se_gl_entries(se, source_branch, target_branch)

self.db_set("stock_entry", se.name, notify=True)
frappe.msgprint(
_('Stock Entry <a href="/app/stock-entry/{0}">{0}</a> created').format(se.name),
alert=True,
indicator="green",
)

def _create_delivery_note(self):
def _create_delivery_note(self, target_branch: str | None = None):
if target_branch is None:
target_branch = self._resolve_branches_or_throw()[1]

from rmax_custom.inter_branch import resolve_branch_cost_center

target_cost_center = resolve_branch_cost_center(target_branch)

dn = frappe.new_doc("Delivery Note")
dn.customer = self.customer
dn.company = self.company
dn.branch = target_branch
dn.cost_center = target_cost_center
dn.set_posting_time = 1
dn.posting_date = self.posting_date
dn.posting_time = self.posting_time
dn.set_warehouse = self.to_warehouse
Expand All @@ -261,11 +302,23 @@ def _create_delivery_note(self):
"uom": item.uom,
"warehouse": item.t_warehouse or self.to_warehouse,
"rate": item.basic_rate,
"cost_center": target_cost_center,
"description": item.description or item.item_name,
"branch": target_branch,
})

if self.taxes_and_charges:
from erpnext.controllers.accounts_controller import get_taxes_and_charges

dn.taxes_and_charges = self.taxes_and_charges
dn.extend(
"taxes",
get_taxes_and_charges("Sales Taxes and Charges Template", self.taxes_and_charges),
)

dn.flags.ignore_permissions = True
dn.insert(ignore_permissions=True)
dn.submit()

self.db_set("delivery_note", dn.name, notify=True)
frappe.msgprint(
Expand Down
Loading