Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,7 @@
"fieldname": "blanket_order_rate",
"fieldtype": "Currency",
"label": "Blanket Order Rate",
"options": "currency",
"print_hide": 1,
"read_only": 1
},
Expand Down Expand Up @@ -947,7 +948,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-03-13 17:27:43.468602",
"modified": "2025-08-27 17:51:19.192728",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",
Expand Down
135 changes: 121 additions & 14 deletions erpnext/manufacturing/doctype/blanket_order/blanket_order.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ frappe.ui.form.on("Blanket Order", {
"Sales Order": "Sales Order",
Quotation: "Quotation",
};

frm.add_fetch("customer", "customer_name", "customer_name");
frm.add_fetch("supplier", "supplier_name", "supplier_name");
},
Expand All @@ -26,44 +25,37 @@ frappe.ui.form.on("Blanket Order", {
frappe.model.open_mapped_doc({
method: "erpnext.manufacturing.doctype.blanket_order.blanket_order.make_order",
frm: frm,
args: {
doctype: "Sales Order",
},
args: { doctype: "Sales Order" },
});
},
__("Create")
);

frm.add_custom_button(
__("Quotation"),
function () {
frappe.model.open_mapped_doc({
method: "erpnext.manufacturing.doctype.blanket_order.blanket_order.make_order",
frm: frm,
args: {
doctype: "Quotation",
},
args: { doctype: "Quotation" },
});
},
__("Create")
);
}

if (frm.doc.supplier && frm.doc.docstatus === 1) {
frm.add_custom_button(
__("Purchase Order"),
function () {
frappe.model.open_mapped_doc({
method: "erpnext.manufacturing.doctype.blanket_order.blanket_order.make_order",
frm: frm,
args: {
doctype: "Purchase Order",
},
args: { doctype: "Purchase Order" },
});
},
__("Create")
);
}
frm.trigger("set_dynamic_grid_labels");
},

onload_post_render: function (frm) {
Expand All @@ -83,7 +75,6 @@ frappe.ui.form.on("Blanket Order", {
frm.set_df_property("customer", "reqd", 1);
frm.set_df_property("supplier", "reqd", 0);
frm.set_value("supplier", "");

frm.set_query("tc_name", function () {
return { filters: { selling: 1 } };
});
Expand All @@ -92,7 +83,6 @@ frappe.ui.form.on("Blanket Order", {
frm.set_df_property("supplier", "reqd", 1);
frm.set_df_property("customer", "reqd", 0);
frm.set_value("customer", "");

frm.set_query("tc_name", function () {
return { filters: { buying: 1 } };
});
Expand All @@ -102,4 +92,121 @@ frappe.ui.form.on("Blanket Order", {
blanket_order_type: function (frm) {
frm.trigger("set_tc_name_filter");
},

company: function (frm) {
set_party_currency(frm);
},

supplier: function (frm) {
set_party_currency(frm);
},

customer: function (frm) {
set_party_currency(frm);
},

currency: function (frm) {
let order_date = frm.doc.from_date || frappe.datetime.get_today();
let company_currency = erpnext.get_currency(frm.doc.company);

frm.trigger("set_dynamic_grid_labels");

if (frm.doc.currency !== company_currency) {
frappe.call({
method: "erpnext.setup.utils.get_exchange_rate",
args: {
transaction_date: order_date,
from_currency: frm.doc.currency,
to_currency: company_currency,
},
freeze: true,
freeze_message: __("Fetching exchange rates ..."),
callback: function (r) {
if (r.message) {
frm.set_value("conversion_rate", r.message);
frm.set_df_property(
"conversion_rate",
"description",
"1 " + frm.doc.currency + " = [?] " + company_currency
);
frm.trigger("calculate_base_rate");
}
},
});
} else {
frm.set_value("conversion_rate", 1.0);
frm.trigger("calculate_base_rate");
}
},

from_date: function (frm) {
frm.trigger("currency");
},

calculate_base_rate: function (frm) {
frm.doc.items.forEach(function (row) {
frappe.model.set_value(
row.doctype,
row.name,
"base_rate",
flt(row.rate * frm.doc.conversion_rate)
);
});
frm.refresh_field("items");
},

set_dynamic_grid_labels: function (frm) {
let company_currency = get_company_currency(frm.doc.company);
let party_currency = frm.doc.currency;

frm.set_currency_labels(["rate"], party_currency, "items");
frm.set_currency_labels(["base_rate"], company_currency, "items");
frm.refresh_fields();
},
});

frappe.ui.form.on("Blanket Order Item", {
calculate: function (frm, cdt, cdn) {
let row = frappe.get_doc(cdt, cdn);
frappe.model.set_value(cdt, cdn, "base_rate", flt(frm.doc.conversion_rate) * flt(row.rate));
},
rate: function (frm, cdt, cdn) {
frm.trigger("calculate", cdt, cdn);
},
qty: function (frm, cdt, cdn) {
frm.trigger("calculate", cdt, cdn);
},
});

const get_company_currency = function (company) {
return erpnext.get_currency(company);
};

const set_party_currency = function (frm) {
var party_type = frm.doc.blanket_order_type === "Purchasing" ? "Supplier" : "Customer";
var party_name = frm.doc[party_type.toLowerCase()];

if (party_name) {
frappe.call({
method: "frappe.client.get_value",
args: {
doctype: party_type,
filters: { name: party_name },
fieldname: "default_currency",
},
callback: function (r) {
if (r.message && r.message.default_currency) {
if (frm.doc.currency !== r.message.default_currency) {
frm.set_value("currency", r.message.default_currency);
} else {
frm.trigger("currency");
}
} else {
frm.set_value("currency", get_company_currency(frm.doc.company));
}
},
});
} else {
frm.set_value("currency", get_company_currency(frm.doc.company));
}
};
35 changes: 33 additions & 2 deletions erpnext/manufacturing/doctype/blanket_order/blanket_order.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
"from_date",
"to_date",
"company",
"currency_section",
"currency",
"column_break_yilv",
"conversion_rate",
"section_break_12",
"items",
"amended_from",
Expand Down Expand Up @@ -142,12 +146,38 @@
"fieldname": "order_date",
"fieldtype": "Date",
"label": "Order Date"
},
{
"collapsible": 1,
"fieldname": "currency_section",
"fieldtype": "Section Break",
"label": "Currency"
},
{
"fieldname": "currency",
"fieldtype": "Link",
"label": "Currency",
"options": "Currency",
"print_hide": 1,
"reqd": 1
},
{
"fieldname": "column_break_yilv",
"fieldtype": "Column Break"
},
{
"fieldname": "conversion_rate",
"fieldtype": "Float",
"label": "Exchange Rate",
"precision": "9",
"print_hide": 1,
"reqd": 1
}
Comment on lines +151 to 175
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Required new fields need migration/backfill and a server-side default path

  • Adding required fields currency and conversion_rate will block edits/amendments of existing Blanket Orders unless you backfill. Provide a patch to set currency = company.default_currency and conversion_rate = 1.0 for existing rows.
  • Also reduce reliance on client scripts by auto-fetching the company’s default currency into currency.

Apply this diff to auto-fetch the company currency:

 {
   "fieldname": "currency",
   "fieldtype": "Link",
   "label": "Currency",
   "options": "Currency",
+  "fetch_from": "company.default_currency",
   "print_hide": 1,
   "reqd": 1
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"collapsible": 1,
"fieldname": "currency_section",
"fieldtype": "Section Break",
"label": "Currency"
},
{
"fieldname": "currency",
"fieldtype": "Link",
"label": "Currency",
"options": "Currency",
"print_hide": 1,
"reqd": 1
},
{
"fieldname": "column_break_yilv",
"fieldtype": "Column Break"
},
{
"fieldname": "conversion_rate",
"fieldtype": "Float",
"label": "Exchange Rate",
"precision": "9",
"print_hide": 1,
"reqd": 1
}
{
"fieldname": "currency_section",
"fieldtype": "Section Break",
"label": "Currency"
},
{
"fieldname": "currency",
"fieldtype": "Link",
"label": "Currency",
"options": "Currency",
"fetch_from": "company.default_currency",
"print_hide": 1,
"reqd": 1
},
{
"fieldname": "column_break_yilv",
"fieldtype": "Column Break"
},
{
"fieldname": "conversion_rate",
"fieldtype": "Float",
"label": "Exchange Rate",
"precision": "9",
"print_hide": 1,
"reqd": 1
}

],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2024-12-05 15:44:21.520093",
"modified": "2025-08-27 11:11:12.214344",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Blanket Order",
Expand All @@ -170,9 +200,10 @@
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"search_fields": "blanket_order_type, to_date",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ class BlanketOrder(Document):
amended_from: DF.Link | None
blanket_order_type: DF.Literal["", "Selling", "Purchasing"]
company: DF.Link
conversion_rate: DF.Float
currency: DF.Link
customer: DF.Link | None
customer_name: DF.Data | None
from_date: DF.Date
Expand All @@ -45,6 +47,7 @@ def validate(self):
self.validate_duplicate_items()
self.validate_item_qty()
self.set_party_item_code()
self.calculate_base_rate()

def validate_dates(self):
if getdate(self.from_date) > getdate(self.to_date):
Expand Down Expand Up @@ -123,6 +126,10 @@ def validate_item_qty(self):
if d.qty < 0:
frappe.throw(_("Row {0}: Quantity cannot be negative.").format(d.idx))

def calculate_base_rate(self):
for d in self.items:
d.base_rate = flt(d.rate * self.conversion_rate, d.precision("base_rate"))

Comment on lines +129 to +132
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Guard against None/empty values in base rate calculation

If rate is None/empty, d.rate * self.conversion_rate raises TypeError before flt() runs.

-    for d in self.items:
-      d.base_rate = flt(d.rate * self.conversion_rate, d.precision("base_rate"))
+    for d in self.items:
+      d.base_rate = flt(flt(d.rate) * flt(self.conversion_rate), d.precision("base_rate"))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def calculate_base_rate(self):
for d in self.items:
d.base_rate = flt(d.rate * self.conversion_rate, d.precision("base_rate"))
def calculate_base_rate(self):
for d in self.items:
d.base_rate = flt(flt(d.rate) * flt(self.conversion_rate), d.precision("base_rate"))
🤖 Prompt for AI Agents
In erpnext/manufacturing/doctype/blanket_order/blanket_order.py around lines
129–132, the base_rate calculation assumes d.rate is set and will raise a
TypeError if d.rate is None/empty; guard this by checking d.rate (and
conversion_rate if needed) before multiplying: if d.rate is falsy set
d.base_rate to 0 (using flt(0, d.precision("base_rate"))) otherwise compute
d.base_rate = flt(d.rate * self.conversion_rate, d.precision("base_rate")).
Ensure you treat empty strings and None as falsy and keep the flt() call for
precision.


@frappe.whitelist()
def make_order(source_name):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,36 @@ def test_party_item_code(self):
bo = make_blanket_order(blanket_order_type="Purchasing", supplier=supplier, item_code=item_code)
self.assertEqual(bo.items[0].party_item_code, "SUPP-PART-1")

def test_multicurrency_blanket_order(self):
from erpnext.buying.doctype.supplier.test_supplier import create_supplier

supplier = create_supplier(supplier_name="_Test BO USD Supplier", default_currency="USD")
bo = make_blanket_order(
blanket_order_type="Purchasing",
supplier=supplier.name,
currency="USD",
conversion_rate=86,
quantity=10,
rate=5,
)
self.assertEqual(bo.items[0].base_rate, 430)

frappe.flags.args.doctype = "Purchase Order"
po = make_order(bo.name)

self.assertEqual(po.currency, "USD")
self.assertEqual(po.conversion_rate, 86)
self.assertEqual(po.items[0].rate, 5)
self.assertEqual(po.items[0].base_rate, 430)

Comment on lines +116 to +137
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Stabilize FX expectations in test to avoid flakiness

The assertions assume USD→company FX is 86 on the test date. Seed a Currency Exchange row to make the test deterministic.

 	def test_multicurrency_blanket_order(self):
 		from erpnext.buying.doctype.supplier.test_supplier import create_supplier
+		from frappe.utils import today
+		company_currency = get_company_currency("_Test Company")
+		# Ensure deterministic FX for today's date
+		if not frappe.db.exists(
+			"Currency Exchange",
+			{"from_currency": "USD", "to_currency": company_currency, "date": today()},
+		):
+			frappe.get_doc({
+				"doctype": "Currency Exchange",
+				"from_currency": "USD",
+				"to_currency": company_currency,
+				"exchange_rate": 86,
+				"date": today(),
+			}).insert()
 
 		supplier = create_supplier(supplier_name="_Test BO USD Supplier", default_currency="USD")

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py around
lines 116 to 137, the test assumes a USD→company conversion rate of 86 and can
be flaky; before creating the blanket order, create (seed) a Currency Exchange
entry for USD→company with the expected rate (86) and a known date used by the
test so conversion_rate calculations are deterministic, then proceed to create
the supplier/blanket order and optionally tear down/remove the seeded Currency
Exchange after the test.


def make_blanket_order(**args):
args = frappe._dict(args)
bo = frappe.new_doc("Blanket Order")
bo.blanket_order_type = args.blanket_order_type
bo.company = args.company or "_Test Company"
bo.currency = args.currency or get_company_currency(bo.company)
bo.conversion_rate = args.conversion_rate or 1.0

if args.blanket_order_type == "Selling":
bo.customer = args.customer or "_Test Customer"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"column_break_3",
"qty",
"rate",
"base_rate",
"ordered_qty",
Comment on lines +14 to 15
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

❓ Verification inconclusive

New base_rate is read_only + reqd — likely to break saves/migrations.

A newly required, read-only field can block saves on existing records unless a server patch backfills and server-side validate always sets it. Recommend making it non-required or ship a patch ensuring population for all existing rows and on validate.

Apply if opting non-required:

   {
     "fieldname": "base_rate",
     "fieldtype": "Currency",
     "label": "Base Rate (Company Currency)",
     "options": "Company:company:default_currency",
     "print_hide": 1,
     "read_only": 1,
-    "reqd": 1
+    "reqd": 0
   }

If keeping required, please add a data patch to compute base_rate = rate * parent.conversion_rate for all existing Blanket Orders and ensure server-side validate sets it when missing.

Also applies to: 77-83


Ensure new base_rate field won’t block existing records

  • The Currency field base_rate is currently defined as read_only: 1 and reqd: 1. Without a backfill or server-side population, saves and migrations for existing Blanket Order Item records will be blocked.

  • To address this, choose one of the following:

    1. Make base_rate optional
      In erpnext/manufacturing/doctype/blanket_order_item/blanket_order_item.json, remove the reqd flag:

        {
          "fieldname": "base_rate",
          "fieldtype": "Currency",
          "label": "Base Rate (Company Currency)",
          "options": "Company:company:default_currency",
          "print_hide": 1,
          "read_only": 1,
      -   "reqd": 1
      +   "reqd": 0
        }
    2. Keep it required but backfill and enforce on save

      • Add a data migration patch under erpnext/patches/ to compute and populate base_rate = rate * parent.conversion_rate for all existing Blanket Order Item records.
      • In the DocType Python class, ensure that on validate (or before_save) any missing base_rate is automatically calculated.
  • Apply the same review to the ordered_qty field definitions (lines 77–83).

Committable suggestion skipped: line range outside the PR's diff.

"section_break_7",
"terms_and_conditions"
Expand Down Expand Up @@ -46,6 +47,7 @@
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Rate",
"options": "currency",
"reqd": 1
},
{
Expand All @@ -70,19 +72,29 @@
"fieldtype": "Data",
"label": "Party Item Code",
"read_only": 1
},
{
"fieldname": "base_rate",
"fieldtype": "Currency",
"label": "Base Rate (Company Currency)",
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1,
"reqd": 1
}
],
"istable": 1,
"links": [],
"modified": "2024-03-27 13:06:40.083042",
"modified": "2025-08-27 17:21:32.870482",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Blanket Order Item",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class BlanketOrderItem(Document):
if TYPE_CHECKING:
from frappe.types import DF

base_rate: DF.Currency
item_code: DF.Link
item_name: DF.Data | None
ordered_qty: DF.Float
Expand Down
Loading
Loading