mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-11 05:31:48 +00:00
feat: auto-fill subscription accounting dimensions from plan with item fallback (backport #57615) (#57622)
* feat: auto-fill subscription accounting dimensions from plan with item fallback (#57615)
When a plan is selected in the Subscription's Plans table, the Subscription's
accounting dimensions (cost center and any custom dimensions) auto-fill from the
plan, falling back to the plan item's company default (selling cost center for a
Customer, buying for a Supplier). Only empty fields are filled. Stale async
responses are ignored so a quick re-pick of the plan can't be overwritten.
(cherry picked from commit 7febc28ed6)
# Conflicts:
# erpnext/accounts/doctype/subscription/subscription.js
# erpnext/accounts/doctype/subscription/test_subscription.py
* fix: resolve backport merge conflicts for #57615
---------
Co-authored-by: Jatin3128 <140256508+Jatin3128@users.noreply.github.com>
Co-authored-by: Jatin3128 <jatinsarna8@gmail.com>
This commit is contained in:
@@ -96,3 +96,29 @@ frappe.ui.form.on("Subscription", {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frappe.ui.form.on("Subscription Plan Detail", {
|
||||||
|
plan: function (frm, cdt, cdn) {
|
||||||
|
const row = locals[cdt][cdn];
|
||||||
|
if (!row.plan) return;
|
||||||
|
const requested_plan = row.plan;
|
||||||
|
|
||||||
|
frappe.call({
|
||||||
|
method: "erpnext.accounts.doctype.subscription.subscription.get_plan_dimensions",
|
||||||
|
args: {
|
||||||
|
plan: requested_plan,
|
||||||
|
company: frm.doc.company,
|
||||||
|
party_type: frm.doc.party_type,
|
||||||
|
},
|
||||||
|
callback: function (r) {
|
||||||
|
if (!r.message || locals[cdt]?.[cdn]?.plan !== requested_plan) return;
|
||||||
|
// Only fill dimensions left empty, so a manual entry or an earlier plan is never overwritten.
|
||||||
|
for (const [dimension, value] of Object.entries(r.message)) {
|
||||||
|
if (frm.fields_dict[dimension] && !frm.doc[dimension]) {
|
||||||
|
frm.set_value(dimension, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
|||||||
get_accounting_dimensions,
|
get_accounting_dimensions,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
|
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
|
||||||
|
from erpnext.stock.doctype.item.item import get_item_defaults
|
||||||
|
|
||||||
|
|
||||||
class InvoiceCancelled(frappe.ValidationError):
|
class InvoiceCancelled(frappe.ValidationError):
|
||||||
@@ -801,6 +802,39 @@ def get_prorata_factor(
|
|||||||
return diff / plan_days
|
return diff / plan_days
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_plan_dimensions(
|
||||||
|
plan: str, company: str | None = None, party_type: str | None = None
|
||||||
|
) -> dict[str, str]:
|
||||||
|
"""Resolve a plan's accounting dimensions, falling back to the plan item's company defaults."""
|
||||||
|
plan_doc = frappe.get_cached_doc("Subscription Plan", plan)
|
||||||
|
|
||||||
|
dimensions = {}
|
||||||
|
for dimension in ["cost_center", *get_accounting_dimensions()]:
|
||||||
|
value = plan_doc.get(dimension) or get_item_dimension(plan_doc.item, dimension, company, party_type)
|
||||||
|
if value:
|
||||||
|
dimensions[dimension] = value
|
||||||
|
|
||||||
|
return dimensions
|
||||||
|
|
||||||
|
|
||||||
|
def get_item_dimension(
|
||||||
|
item_code: str, dimension: str, company: str | None, party_type: str | None
|
||||||
|
) -> str | None:
|
||||||
|
if not company:
|
||||||
|
return None
|
||||||
|
|
||||||
|
item_defaults = get_item_defaults(item_code, company)
|
||||||
|
if dimension != "cost_center":
|
||||||
|
return item_defaults.get(dimension)
|
||||||
|
|
||||||
|
selling = item_defaults.get("selling_cost_center")
|
||||||
|
buying = item_defaults.get("buying_cost_center")
|
||||||
|
if party_type == "Supplier":
|
||||||
|
return buying or selling
|
||||||
|
return selling or buying
|
||||||
|
|
||||||
|
|
||||||
def process_all(subscription: list, posting_date: DateTimeLikeObject | None = None) -> None:
|
def process_all(subscription: list, posting_date: DateTimeLikeObject | None = None) -> None:
|
||||||
"""
|
"""
|
||||||
Task to updates the status of all `Subscription` apart from those that are cancelled
|
Task to updates the status of all `Subscription` apart from those that are cancelled
|
||||||
|
|||||||
@@ -17,7 +17,12 @@ from frappe.utils.data import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||||
from erpnext.accounts.doctype.subscription.subscription import Subscription, get_prorata_factor, process_all
|
from erpnext.accounts.doctype.subscription.subscription import (
|
||||||
|
Subscription,
|
||||||
|
get_plan_dimensions,
|
||||||
|
get_prorata_factor,
|
||||||
|
process_all,
|
||||||
|
)
|
||||||
from erpnext.accounts.utils import update_subscription_on_invoice_update
|
from erpnext.accounts.utils import update_subscription_on_invoice_update
|
||||||
from erpnext.tests.utils import ERPNextTestSuite
|
from erpnext.tests.utils import ERPNextTestSuite
|
||||||
|
|
||||||
@@ -804,6 +809,48 @@ class TestSubscription(ERPNextTestSuite):
|
|||||||
)
|
)
|
||||||
self.assertEqual(len(subscription.invoices), 0)
|
self.assertEqual(len(subscription.invoices), 0)
|
||||||
|
|
||||||
|
def test_plan_dimensions_resolve_from_plan_then_item(self):
|
||||||
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
|
||||||
|
# Plan-level cost center takes precedence.
|
||||||
|
create_plan(plan_name="_Test Sub Plan CC", cost=100, currency="INR")
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Subscription Plan", "_Test Sub Plan CC", "cost_center", "_Test Cost Center - _TC"
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
get_plan_dimensions("_Test Sub Plan CC", "_Test Company", "Customer").get("cost_center"),
|
||||||
|
"_Test Cost Center - _TC",
|
||||||
|
)
|
||||||
|
|
||||||
|
# No plan cost center: fall back to the item's company default (selling vs buying by party type).
|
||||||
|
item = make_item(
|
||||||
|
"_Test Sub Dimension Item",
|
||||||
|
{
|
||||||
|
"is_stock_item": 0,
|
||||||
|
"item_defaults": [
|
||||||
|
{
|
||||||
|
"company": "_Test Company",
|
||||||
|
"default_warehouse": "_Test Warehouse - _TC",
|
||||||
|
"selling_cost_center": "_Test Cost Center - _TC",
|
||||||
|
"buying_cost_center": "_Test Cost Center 2 - _TC",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
create_plan(plan_name="_Test Sub Plan No CC", cost=100, currency="INR", item=item.name)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
get_plan_dimensions("_Test Sub Plan No CC", "_Test Company", "Customer").get("cost_center"),
|
||||||
|
"_Test Cost Center - _TC",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
get_plan_dimensions("_Test Sub Plan No CC", "_Test Company", "Supplier").get("cost_center"),
|
||||||
|
"_Test Cost Center 2 - _TC",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Without a company the item fallback is skipped.
|
||||||
|
self.assertNotIn("cost_center", get_plan_dimensions("_Test Sub Plan No CC"))
|
||||||
|
|
||||||
|
|
||||||
def make_plans():
|
def make_plans():
|
||||||
create_plan(plan_name="_Test Plan Name", cost=900, currency="INR")
|
create_plan(plan_name="_Test Plan Name", cost=900, currency="INR")
|
||||||
|
|||||||
Reference in New Issue
Block a user