@@ -159,15 +164,9 @@ erpnext.PointOfSale.PastOrderSummary = class {
let taxes_html = doc.taxes
.map((t) => {
- // if tax rate is 0, don't print it.
- const description = /[0-9]+/.test(t.description)
- ? t.description
- : t.rate != 0
- ? `${t.description} @ ${t.rate}%`
- : t.description;
return `
`;
@@ -243,6 +242,10 @@ erpnext.PointOfSale.PastOrderSummary = class {
this.$summary_container.on("click", ".print-btn", () => {
this.print_receipt();
});
+
+ this.$summary_container.on("click", ".open-btn", () => {
+ this.events.open_in_form_view(this.doc.doctype, this.doc.name);
+ });
}
print_receipt() {
@@ -361,7 +364,14 @@ erpnext.PointOfSale.PastOrderSummary = class {
return [
{ condition: this.doc.docstatus === 0, visible_btns: ["Edit Order", "Delete Order"] },
{
- condition: !this.doc.is_return && this.doc.docstatus === 1,
+ condition: ["Partly Paid", "Overdue", "Unpaid"].includes(this.doc.status),
+ visible_btns: ["Print Receipt", "Email Receipt", "Open in Form View"],
+ },
+ {
+ condition:
+ !this.doc.is_return &&
+ this.doc.docstatus === 1 &&
+ !["Partly Paid", "Overdue", "Unpaid"].includes(this.doc.status),
visible_btns: ["Print Receipt", "Email Receipt", "Return"],
},
{
diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js
index b4851586557..242a49b536b 100644
--- a/erpnext/selling/page/point_of_sale/pos_payment.js
+++ b/erpnext/selling/page/point_of_sale/pos_payment.js
@@ -5,6 +5,7 @@ erpnext.PointOfSale.Payment = class {
this.events = events;
this.set_gt_to_default_mop = settings.set_grand_total_to_default_mop;
this.invoice_fields = settings.invoice_fields;
+ this.allow_partial_payment = settings.allow_partial_payment;
this.init_component();
}
@@ -129,21 +130,51 @@ erpnext.PointOfSale.Payment = class {
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
- [".", 0, "Delete"],
+ ["+/-", 0, "Delete"],
],
});
this.numpad_value = "";
}
- on_numpad_clicked($btn) {
- const button_value = $btn.attr("data-button-value");
+ on_numpad_clicked($btn, from_numpad = true) {
+ const button_value = from_numpad ? $btn.attr("data-button-value") : $btn;
- highlight_numpad_btn($btn);
- this.numpad_value =
- button_value === "delete" ? this.numpad_value.slice(0, -1) : this.numpad_value + button_value;
- this.selected_mode.$input.get(0).focus();
- this.selected_mode.set_value(this.numpad_value);
+ from_numpad && highlight_numpad_btn($btn);
+ if (!this.selected_mode) {
+ frappe.show_alert({
+ message: __("Select a Payment Method."),
+ indicator: "yellow",
+ });
+ return;
+ }
+
+ const number_format_details = get_number_format_info(frappe.sys_defaults.number_format);
+ const precision = frappe.sys_defaults.currency_precision || number_format_details.precision;
+ this.numpad_value = "0";
+ if (this.selected_mode.get_value()) {
+ this.numpad_value = (this.selected_mode.get_value() * 10 ** precision).toFixed(0).toString();
+ }
+
+ let valid_input = true;
+ if (button_value === "delete" || button_value === "Backspace") {
+ this.numpad_value = this.numpad_value.slice(0, -1);
+ } else if (button_value === "+/-") {
+ this.numpad_value = `${this.numpad_value * -1}`;
+ } else if (button_value === "+") {
+ this.numpad_value =
+ Number(this.numpad_value) >= 0 ? this.numpad_value : `${this.numpad_value * -1}`;
+ } else if (button_value === "-") {
+ this.numpad_value =
+ Number(this.numpad_value) <= 0 ? this.numpad_value : `${this.numpad_value * -1}`;
+ } else if (!isNaN(button_value)) {
+ this.numpad_value = this.numpad_value + button_value;
+ } else {
+ valid_input = false;
+ }
+ valid_input && frappe.utils.play_sound("numpad-touch");
+
+ this.selected_mode.set_value(this.numpad_value / 10 ** precision);
function highlight_numpad_btn($btn) {
$btn.addClass("shadow-base-inner bg-selected");
@@ -161,39 +192,54 @@ erpnext.PointOfSale.Payment = class {
// if clicked element doesn't have .mode-of-payment class then return
if (!$(e.target).is(mode_clicked)) return;
- const scrollLeft =
- mode_clicked.offset().left - me.$payment_modes.offset().left + me.$payment_modes.scrollLeft();
- me.$payment_modes.animate({ scrollLeft });
-
const mode = mode_clicked.attr("data-mode");
// hide all control fields and shortcuts
$(`.mode-of-payment-control`).css("display", "none");
- $(`.cash-shortcuts`).css("display", "none");
me.$payment_modes.find(`.pay-amount`).css("display", "inline");
me.$payment_modes.find(`.loyalty-amount-name`).css("display", "none");
// remove highlight from all mode-of-payments
$(".mode-of-payment").removeClass("border-primary");
- if (mode_clicked.hasClass("border-primary")) {
+ me.hide_zero_amount();
+
+ if (me.selected_mode?._label === me[`${mode}_control`]?._label) {
// clicked one is selected then unselect it
mode_clicked.removeClass("border-primary");
me.selected_mode = "";
} else {
// clicked one is not selected then select it
mode_clicked.addClass("border-primary");
- mode_clicked.find(".mode-of-payment-control").css("display", "flex");
- mode_clicked.find(".cash-shortcuts").css("display", "grid");
- me.$payment_modes.find(`.${mode}-amount`).css("display", "none");
- me.$payment_modes.find(`.${mode}-name`).css("display", "inline");
me.selected_mode = me[`${mode}_control`];
- me.selected_mode && me.selected_mode.$input.get(0).focus();
+ const mode_clicked_amount = mode_clicked.find(`.${mode}-amount`).get(0);
+ if (!mode_clicked_amount.innerHTML) {
+ mode_clicked_amount.innerHTML = format_currency(0, me.events.get_frm().doc.currency);
+ }
me.auto_set_remaining_amount();
}
});
+ // change payment amount for selected mode on key press from keyboard
+ $(document).on("keydown", function (e) {
+ if (me.selected_mode) {
+ me.on_numpad_clicked(e.key, false);
+ }
+ });
+
+ // deselect payment method if mode of payment or numpad is not clicked
+ $(document).on("click", function (e) {
+ const mode_of_payment_click = $(e.target).closest(".mode-of-payment").length;
+ const numpad_btn_click = $(e.target).closest(".numpad-btn").length;
+
+ if (!mode_of_payment_click && !numpad_btn_click && me.selected_mode) {
+ me.selected_mode = "";
+ me.hide_zero_amount();
+ $(".mode-of-payment").removeClass("border-primary");
+ }
+ });
+
frappe.ui.form.on("POS Invoice", "contact_mobile", (frm) => {
const contact = frm.doc.contact_mobile;
const request_button = $(this.request_for_payment_field?.$input[0]);
@@ -224,7 +270,12 @@ erpnext.PointOfSale.Payment = class {
const paid_amount = doc.paid_amount;
const items = doc.items;
- if (!items.length || (paid_amount == 0 && doc.additional_discount_percentage != 100)) {
+ if (
+ !items.length ||
+ (paid_amount == 0 &&
+ doc.additional_discount_percentage != 100 &&
+ this.allow_partial_payment === 0)
+ ) {
const message = items.length
? __("You cannot submit the order without payment.")
: __("You cannot submit empty order.");
@@ -290,11 +341,6 @@ erpnext.PointOfSale.Payment = class {
bind_paid_amount_event(frm) {
this.update_totals_section(frm.doc);
-
- // need to re calculate cash shortcuts after discount is applied
- const is_cash_shortcuts_invisible = !this.$payment_modes.find(".cash-shortcuts").is(":visible");
- this.attach_cash_shortcuts(frm.doc);
- !is_cash_shortcuts_invisible && this.$payment_modes.find(".cash-shortcuts").css("display", "grid");
this.render_payment_mode_dom();
}
@@ -336,6 +382,16 @@ erpnext.PointOfSale.Payment = class {
});
}
+ hide_zero_amount() {
+ const payment_methods = this.$payment_modes.find(`.mode-of-payment`);
+ for (let i = 0; i < payment_methods.length; i++) {
+ const mode = payment_methods.get(i).getAttribute("data-mode");
+ if (this[`${mode}_control`]?.value === 0) {
+ this.$payment_modes.find(`.${mode}-amount`).get(0).innerHTML = "";
+ }
+ }
+ }
+
auto_set_remaining_amount() {
const doc = this.events.get_frm().doc;
const grand_total = cint(frappe.sys_defaults.disable_rounded_total)
@@ -451,8 +507,10 @@ erpnext.PointOfSale.Payment = class {
.map((p, i) => {
const mode = this.sanitize_mode_of_payment(p.mode_of_payment);
const payment_type = p.type;
- const margin = i % 2 === 0 ? "pr-2" : "pl-2";
- const amount = p.amount > 0 ? format_currency(p.amount, currency) : "";
+ const amount =
+ p.mode_of_payment === this.selected_mode?._label || p.amount !== 0
+ ? format_currency(p.amount, currency)
+ : "";
return `
@@ -493,10 +551,9 @@ erpnext.PointOfSale.Payment = class {
this[`${mode}_control`].toggle_label(false);
this[`${mode}_control`].set_value(p.amount);
});
+ this.highlight_selected_mode();
this.render_loyalty_points_payment_mode();
-
- this.attach_cash_shortcuts(doc);
}
focus_on_default_mop() {
@@ -512,45 +569,6 @@ erpnext.PointOfSale.Payment = class {
});
}
- attach_cash_shortcuts(doc) {
- const grand_total = cint(frappe.sys_defaults.disable_rounded_total)
- ? doc.grand_total
- : doc.rounded_total;
- const currency = doc.currency;
-
- const shortcuts = this.get_cash_shortcuts(flt(grand_total));
-
- this.$payment_modes.find(".cash-shortcuts").remove();
- let shortcuts_html = shortcuts
- .map((s) => {
- return `
${format_currency(s, currency)}
`;
- })
- .join("");
-
- this.$payment_modes
- .find('[data-payment-type="Cash"]')
- .find(".mode-of-payment-control")
- .after(`
${shortcuts_html}
`);
- }
-
- get_cash_shortcuts(grand_total) {
- let steps = [1, 5, 10];
- const digits = String(Math.round(grand_total)).length;
-
- steps = steps.map((x) => x * 10 ** (digits - 2));
-
- const get_nearest = (amount, x) => {
- let nearest_x = Math.ceil(amount / x) * x;
- return nearest_x === amount ? nearest_x + x : nearest_x;
- };
-
- return steps.reduce((finalArr, x) => {
- let nearest_x = get_nearest(grand_total, x);
- nearest_x = finalArr.indexOf(nearest_x) != -1 ? nearest_x + x : nearest_x;
- return [...finalArr, nearest_x];
- }, []);
- }
-
render_loyalty_points_payment_mode() {
const me = this;
const doc = this.events.get_frm().doc;
@@ -628,9 +646,17 @@ erpnext.PointOfSale.Payment = class {
});
this["loyalty-amount_control"].toggle_label(false);
+ this.highlight_selected_mode();
// this.render_add_payment_method_dom();
}
+ highlight_selected_mode() {
+ if (this.selected_mode) {
+ const mode = this.sanitize_mode_of_payment(this.selected_mode.df.label);
+ this.$payment_modes.find(`.mode-of-payment[data-mode="${mode}"]`).addClass("border-primary");
+ }
+ }
+
render_add_payment_method_dom() {
const docstatus = this.events.get_frm().doc.docstatus;
if (docstatus === 0)
@@ -665,7 +691,10 @@ erpnext.PointOfSale.Payment = class {
${label}
-
${format_currency(change || remaining, currency)}
+
${format_currency(
+ change || remaining,
+ currency
+ )}
`
);
}
diff --git a/erpnext/setup/doctype/email_digest/email_digest.py b/erpnext/setup/doctype/email_digest/email_digest.py
index 3a5933ebf45..ba837c6d9dc 100644
--- a/erpnext/setup/doctype/email_digest/email_digest.py
+++ b/erpnext/setup/doctype/email_digest/email_digest.py
@@ -395,7 +395,7 @@ class EmailDigest(Document):
label = get_link_to_report(
"General Ledger",
- self.meta.get_label("income"),
+ _(self.meta.get_label("income")),
filters={
"from_date": self.future_from_date,
"to_date": self.future_to_date,
@@ -427,7 +427,7 @@ class EmailDigest(Document):
filters = {"currency": self.currency}
label = get_link_to_report(
"Profit and Loss Statement",
- label=self.meta.get_label(root_type + "_year_to_date"),
+ label=_(self.meta.get_label(root_type + "_year_to_date")),
filters=filters,
)
@@ -435,7 +435,7 @@ class EmailDigest(Document):
filters = {"currency": self.currency}
label = get_link_to_report(
"Profit and Loss Statement",
- label=self.meta.get_label(root_type + "_year_to_date"),
+ label=_(self.meta.get_label(root_type + "_year_to_date")),
filters=filters,
)
@@ -466,7 +466,7 @@ class EmailDigest(Document):
label = get_link_to_report(
"General Ledger",
- self.meta.get_label("expenses_booked"),
+ _(self.meta.get_label("expenses_booked")),
filters={
"company": self.company,
"from_date": self.future_from_date,
@@ -500,7 +500,7 @@ class EmailDigest(Document):
label = get_link_to_report(
"Sales Order",
- label=self.meta.get_label("sales_orders_to_bill"),
+ label=_(self.meta.get_label("sales_orders_to_bill")),
report_type="Report Builder",
doctype="Sales Order",
filters={
@@ -526,7 +526,7 @@ class EmailDigest(Document):
label = get_link_to_report(
"Sales Order",
- label=self.meta.get_label("sales_orders_to_deliver"),
+ label=_(self.meta.get_label("sales_orders_to_deliver")),
report_type="Report Builder",
doctype="Sales Order",
filters={
@@ -552,7 +552,7 @@ class EmailDigest(Document):
label = get_link_to_report(
"Purchase Order",
- label=self.meta.get_label("purchase_orders_to_receive"),
+ label=_(self.meta.get_label("purchase_orders_to_receive")),
report_type="Report Builder",
doctype="Purchase Order",
filters={
@@ -578,7 +578,7 @@ class EmailDigest(Document):
label = get_link_to_report(
"Purchase Order",
- label=self.meta.get_label("purchase_orders_to_bill"),
+ label=_(self.meta.get_label("purchase_orders_to_bill")),
report_type="Report Builder",
doctype="Purchase Order",
filters={
@@ -630,7 +630,7 @@ class EmailDigest(Document):
"company": self.company,
}
label = get_link_to_report(
- "Account Balance", label=self.meta.get_label(fieldname), filters=filters
+ "Account Balance", label=_(self.meta.get_label(fieldname)), filters=filters
)
else:
filters = {
@@ -640,7 +640,7 @@ class EmailDigest(Document):
"company": self.company,
}
label = get_link_to_report(
- "Account Balance", label=self.meta.get_label(fieldname), filters=filters
+ "Account Balance", label=_(self.meta.get_label(fieldname)), filters=filters
)
return {"label": label, "value": balance, "last_value": prev_balance}
@@ -648,17 +648,17 @@ class EmailDigest(Document):
if account_type == "Payable":
label = get_link_to_report(
"Accounts Payable",
- label=self.meta.get_label(fieldname),
+ label=_(self.meta.get_label(fieldname)),
filters={"report_date": self.future_to_date, "company": self.company},
)
elif account_type == "Receivable":
label = get_link_to_report(
"Accounts Receivable",
- label=self.meta.get_label(fieldname),
+ label=_(self.meta.get_label(fieldname)),
filters={"report_date": self.future_to_date, "company": self.company},
)
else:
- label = self.meta.get_label(fieldname)
+ label = _(self.meta.get_label(fieldname))
return {"label": label, "value": balance, "last_value": prev_balance, "count": count}
@@ -748,7 +748,7 @@ class EmailDigest(Document):
label = get_link_to_report(
"Quotation",
- label=self.meta.get_label(fieldname),
+ label=_(self.meta.get_label(fieldname)),
report_type="Report Builder",
doctype="Quotation",
filters={
@@ -779,7 +779,7 @@ class EmailDigest(Document):
label = get_link_to_report(
doc_type,
- label=self.meta.get_label(fieldname),
+ label=_(self.meta.get_label(fieldname)),
report_type="Report Builder",
filters=filters,
doctype=doc_type,
diff --git a/erpnext/setup/doctype/employee/employee.json b/erpnext/setup/doctype/employee/employee.json
index 8ab81d787fc..635f359d651 100644
--- a/erpnext/setup/doctype/employee/employee.json
+++ b/erpnext/setup/doctype/employee/employee.json
@@ -600,7 +600,7 @@
"collapsible": 1,
"fieldname": "exit",
"fieldtype": "Tab Break",
- "label": "Exit",
+ "label": "Employee Exit",
"oldfieldtype": "Section Break"
},
{
@@ -822,7 +822,7 @@
"image_field": "image",
"is_tree": 1,
"links": [],
- "modified": "2025-02-07 13:54:40.122345",
+ "modified": "2025-07-04 08:29:34.347269",
"modified_by": "Administrator",
"module": "Setup",
"name": "Employee",
diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json
index 7c4ef57889b..343bc057f0c 100644
--- a/erpnext/setup/setup_wizard/data/country_wise_tax.json
+++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json
@@ -60,10 +60,155 @@
},
"Australia": {
- "Australia GST": {
- "account_name": "GST 10%",
- "tax_rate": 10.00,
- "default": 1
+ "tax_categories" :[
+ {
+ "title" : "Domestic GST Supplier"
+ },
+ {
+ "title" : "Domestic GST Customer"
+ },
+ {
+ "title" : "Export Customer"
+ },
+ {
+ "title" : "GST Free Customer"
+ },
+ {
+ "title" : "Capital Goods Supplier"
+ },
+ {
+ "title" : "Import / GST Free Supplier"
+ }
+ ],
+ "chart_of_accounts": {
+ "Australia - Chart of Accounts with Account Numbers": {
+ "sales_tax_templates": [
+ {
+ "title": "AU Sales - GST",
+ "taxes": [
+ {
+ "account_head": {
+ "account_name": "GST Collected (Payable)",
+ "account_number": "22010",
+ "account_type": "Tax",
+ "tax_rate": "10"
+ },
+ "is_default" : 1,
+ "description": "GST Collected (Payable)",
+ "rate": 10
+ }
+ ]
+ },
+ {
+ "title": "Export Sales - GST Free",
+ "taxes": [
+ {
+ "account_head": {
+ "account_name": "GST Collected (Payable)",
+ "account_number": "22010",
+ "account_type": "Tax",
+ "tax_rate": "10"
+ },
+ "description": "GST Collected (Payable)",
+ "rate": 0
+ }
+ ]
+ },
+ {
+ "title": "AU Sales - GST Free",
+ "taxes": [
+ {
+ "account_head": {
+ "account_name": "GST Collected (Payable)",
+ "account_number": "22010",
+ "account_type": "Tax",
+ "tax_rate": "10"
+ },
+ "description": "GST Collected (Payable)",
+ "rate": 0
+ }
+ ]
+ }
+ ],
+ "purchase_tax_templates": [
+ {
+ "title": "AU Capital Purchase - GST",
+ "taxes": [
+ {
+ "account_head": {
+ "account_name": "GST Paid (Receivable)",
+ "account_number": "22020",
+ "account_type": "Tax",
+ "tax_rate": "10"
+ },
+ "description": "GST Paid (Receivable)",
+ "rate": 10
+ }
+ ]
+ },
+ {
+ "title": "Import & GST-Free Purchase",
+ "taxes": [
+ {
+ "account_head": {
+ "account_name": "GST Paid (Receivable)",
+ "account_number": "22020",
+ "account_type": "Tax",
+ "tax_rate": "10"
+ },
+ "description": "GST Paid (Receivable)",
+ "rate": 0
+ }
+ ]
+ },
+ {
+ "title": "AU Non Capital Purchase - GST",
+ "taxes": [
+ {
+ "account_head": {
+ "account_name": "GST Paid (Receivable)",
+ "account_number": "22020",
+ "account_type": "Tax",
+ "tax_rate": "10"
+ },
+ "description": "GST Paid (Receivable)",
+ "is_default" :1,
+ "rate": 10
+ }
+ ]
+ }
+ ],
+ "item_tax_templates": [
+ {
+ "title": "GST Exempt Sales",
+ "taxes": [
+ {
+ "tax_type": {
+ "account_name": "GST Collected (Payable)",
+ "account_number": "22010",
+ "root_type": "Liability",
+ "tax_rate": "10"
+ },
+ "tax_rate": 0
+ }
+ ]
+ },
+ {
+ "title" : "GST Exempt Purchase",
+ "taxes": [
+ {
+ "tax_type": {
+ "account_name": "GST Paid (Receivable)",
+ "account_number": "22020",
+ "root_type": "Liability",
+ "tax_rate": "10"
+ },
+ "tax_rate": 0
+ }
+ ]
+ }
+ ]
+ }
}
},
diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py
index 814cf8cb148..25a08438460 100644
--- a/erpnext/setup/utils.py
+++ b/erpnext/setup/utils.py
@@ -102,7 +102,7 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No
if not transaction_date:
transaction_date = nowdate()
- currency_settings = frappe.get_doc("Accounts Settings").as_dict()
+ currency_settings = frappe.get_cached_doc("Accounts Settings")
allow_stale_rates = currency_settings.get("allow_stale")
filters = [
diff --git a/erpnext/setup/workspace/home/home.json b/erpnext/setup/workspace/home/home.json
index 4945dced4bc..8c011f32d29 100644
--- a/erpnext/setup/workspace/home/home.json
+++ b/erpnext/setup/workspace/home/home.json
@@ -1,4 +1,5 @@
{
+ "app": "erpnext",
"charts": [],
"content": "[{\"id\":\"aCk49ShVRs\",\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Home\",\"col\":12}},{\"id\":\"kb3XPLg8lb\",\"type\":\"header\",\"data\":{\"text\":\"
Your Shortcuts\",\"col\":12}},{\"id\":\"nWd2KJPW8l\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"id\":\"snrzfbFr5Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customer\",\"col\":3}},{\"id\":\"SHJKakmLLf\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Supplier\",\"col\":3}},{\"id\":\"CPxEyhaf3G\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":3}},{\"id\":\"WU4F-HUcIQ\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leaderboard\",\"col\":3}},{\"id\":\"d_KVM1gsf9\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"JVu8-FJZCu\",\"type\":\"header\",\"data\":{\"text\":\"
Reports & Masters\",\"col\":12}},{\"id\":\"JiuSi0ubOg\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounting\",\"col\":4}},{\"id\":\"ji2Jlm3Q8i\",\"type\":\"card\",\"data\":{\"card_name\":\"Stock\",\"col\":4}},{\"id\":\"N61oiXpuwK\",\"type\":\"card\",\"data\":{\"card_name\":\"CRM\",\"col\":4}},{\"id\":\"6J0CVl1mPo\",\"type\":\"card\",\"data\":{\"card_name\":\"Data Import and Settings\",\"col\":4}}]",
"creation": "2020-01-23 13:46:38.833076",
@@ -232,7 +233,7 @@
"type": "Link"
}
],
- "modified": "2025-02-17 10:55:02.213683",
+ "modified": "2025-07-02 14:12:28.407612",
"modified_by": "Administrator",
"module": "Setup",
"name": "Home",
@@ -264,12 +265,8 @@
"label": "Sales Invoice",
"link_to": "Sales Invoice",
"type": "DocType"
- },
- {
- "label": "Leaderboard",
- "link_to": "leaderboard",
- "type": "Page"
}
],
- "title": "Home"
-}
\ No newline at end of file
+ "title": "Home",
+ "type": "Workspace"
+}
diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js
index 6fc9e6666a2..17f65ce270c 100644
--- a/erpnext/stock/dashboard/item_dashboard.js
+++ b/erpnext/stock/dashboard/item_dashboard.js
@@ -51,7 +51,7 @@ erpnext.stock.ItemDashboard = class ItemDashboard {
let stock_uom = unescape(element.attr("data-stock-uom"));
if (disable_quick_entry) {
- open_stock_entry(item, warehouse, entry_type);
+ open_stock_entry(item, warehouse, entry_type, stock_uom);
} else {
if (action === "Add") {
let rate = unescape($(this).attr("data-rate"));
@@ -66,7 +66,7 @@ erpnext.stock.ItemDashboard = class ItemDashboard {
}
}
- function open_stock_entry(item, warehouse, entry_type) {
+ function open_stock_entry(item, warehouse, entry_type, stock_uom) {
frappe.model.with_doctype("Stock Entry", function () {
var doc = frappe.model.get_new_doc("Stock Entry");
if (entry_type) {
@@ -75,6 +75,9 @@ erpnext.stock.ItemDashboard = class ItemDashboard {
var row = frappe.model.add_child(doc, "items");
row.item_code = item;
+ row.uom = stock_uom;
+ row.stock_uom = stock_uom;
+ row.conversion_factor = 1;
if (entry_type === "Material Transfer") {
row.s_warehouse = warehouse;
diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py
index 45c997390a8..0611511ba0e 100644
--- a/erpnext/stock/deprecated_serial_batch.py
+++ b/erpnext/stock/deprecated_serial_batch.py
@@ -318,6 +318,12 @@ class DeprecatedBatchNoValuation:
if self.sle.name:
query = query.where(sle.name != self.sle.name)
+ if self.sle.serial_and_batch_bundle:
+ query = query.where(
+ (sle.serial_and_batch_bundle != self.sle.serial_and_batch_bundle)
+ | (sle.serial_and_batch_bundle.isnull())
+ )
+
data = query.run(as_dict=True)
return data[0] if data else frappe._dict()
diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py
index 8d56ce8b1ee..d48ad47b100 100644
--- a/erpnext/stock/doctype/batch/test_batch.py
+++ b/erpnext/stock/doctype/batch/test_batch.py
@@ -305,8 +305,18 @@ class TestBatch(IntegrationTestCase):
self.assertEqual(
get_batch_qty(item_code="ITEM-BATCH-2", warehouse="_Test Warehouse - _TC"),
[
- {"batch_no": "batch a", "qty": 90.0, "warehouse": "_Test Warehouse - _TC"},
- {"batch_no": "batch b", "qty": 90.0, "warehouse": "_Test Warehouse - _TC"},
+ {
+ "batch_no": "batch a",
+ "qty": 90.0,
+ "warehouse": "_Test Warehouse - _TC",
+ "expiry_date": None,
+ },
+ {
+ "batch_no": "batch b",
+ "qty": 90.0,
+ "warehouse": "_Test Warehouse - _TC",
+ "expiry_date": None,
+ },
],
)
diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py
index 338fd863ffc..79e9776b91c 100644
--- a/erpnext/stock/doctype/bin/bin.py
+++ b/erpnext/stock/doctype/bin/bin.py
@@ -264,7 +264,7 @@ def update_qty(bin_name, args):
actual_qty = bin_details.actual_qty or 0.0
# actual qty is not up to date in case of backdated transaction
- if future_sle_exists(args, allow_force_reposting=False):
+ if future_sle_exists(args):
actual_qty = get_actual_qty(args.get("item_code"), args.get("warehouse"))
ordered_qty = flt(bin_details.ordered_qty) + flt(args.get("ordered_qty"))
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 3b2ec5c04c3..364fc46e6ab 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -1276,6 +1276,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
"doctype": target_doctype,
"postprocess": update_details,
"field_no_map": ["taxes_and_charges", "set_warehouse"],
+ "field_map": {"shipping_address_name": "shipping_address"},
},
doctype + " Item": {
"doctype": target_doctype + " Item",
diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js
index c819d17b1e7..1b5f4a5743f 100644
--- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js
+++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js
@@ -75,7 +75,9 @@ frappe.ui.form.on("Inventory Dimension", {
set_parent_fields(frm) {
if (frm.doc.apply_to_all_doctypes) {
- frm.set_df_property("fetch_from_parent", "options", frm.doc.reference_document);
+ let options = ["\n", frm.doc.reference_document];
+
+ frm.set_df_property("fetch_from_parent", "options", options);
} else if (frm.doc.document_type && frm.doc.istable) {
frappe.call({
method: "erpnext.stock.doctype.inventory_dimension.inventory_dimension.get_parent_fields",
@@ -85,7 +87,7 @@ frappe.ui.form.on("Inventory Dimension", {
},
callback: (r) => {
if (r.message && r.message.length) {
- frm.set_df_property("fetch_from_parent", "options", [""].concat(r.message));
+ frm.set_df_property("fetch_from_parent", "options", ["\n"].concat(r.message));
} else {
frm.set_df_property("fetch_from_parent", "hidden", 1);
}
diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json
index 62105477efb..376b09f9370 100644
--- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json
+++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json
@@ -143,7 +143,6 @@
"fieldtype": "Column Break"
},
{
- "depends_on": "eval:!doc.apply_to_all_doctypes",
"description": "Set fieldname from which you want to fetch the data from the parent form.",
"fieldname": "fetch_from_parent",
"fieldtype": "Select",
@@ -189,7 +188,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2024-07-08 08:58:50.228211",
+ "modified": "2025-07-07 15:51:29.329064",
"modified_by": "Administrator",
"module": "Stock",
"name": "Inventory Dimension",
@@ -225,7 +224,8 @@
"role": "Stock User"
}
],
+ "row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
-}
\ No newline at end of file
+}
diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py
index 3480926f0e6..f6c53beca2b 100644
--- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py
+++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py
@@ -65,16 +65,11 @@ class InventoryDimension(Document):
self.reset_value()
self.set_source_and_target_fieldname()
self.set_type_of_transaction()
- self.set_fetch_value_from()
def set_type_of_transaction(self):
if self.apply_to_all_doctypes:
self.type_of_transaction = "Both"
- def set_fetch_value_from(self):
- if self.apply_to_all_doctypes:
- self.fetch_from_parent = self.reference_document
-
def do_not_update_document(self):
if self.is_new() or not self.has_stock_ledger():
return
diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py
index 0eca78649a9..98d4a43ff94 100644
--- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py
+++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py
@@ -152,6 +152,8 @@ class TestInventoryDimension(IntegrationTestCase):
reference_document="Rack", dimension_name="Rack", apply_to_all_doctypes=1
)
+ inv_dimension.db_set("fetch_from_parent", "Rack")
+
self.assertEqual(inv_dimension.type_of_transaction, "Both")
self.assertEqual(inv_dimension.fetch_from_parent, "Rack")
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index 9a74777f8cd..90b23733b1d 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -638,7 +638,7 @@ class Item(Document):
if new_properties != [cstr(self.get(field)) for field in field_list]:
msg = _("To merge, following properties must be same for both items")
- msg += ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list])
+ msg += ": \n" + ", ".join([_(self.meta.get_label(fld)) for fld in field_list])
frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError)
def validate_duplicate_product_bundles_before_merge(self, old_name, new_name):
@@ -977,7 +977,7 @@ class Item(Document):
return
if linked_doc := self._get_linked_submitted_documents(changed_fields):
- changed_field_labels = [frappe.bold(self.meta.get_label(f)) for f in changed_fields]
+ changed_field_labels = [frappe.bold(_(self.meta.get_label(f))) for f in changed_fields]
msg = _(
"As there are existing submitted transactions against item {0}, you can not change the value of {1}."
).format(self.name, ", ".join(changed_field_labels))
diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
index 16bb2c2bcb7..345ecd49176 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
@@ -359,6 +359,7 @@ def get_pr_items(purchase_receipt):
(pr_item.parent == purchase_receipt.receipt_document)
& ((item.is_stock_item == 1) | (item.is_fixed_asset == 1))
)
+ .orderby(pr_item.idx)
)
if purchase_receipt.receipt_document_type == "Subcontracting Receipt":
diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js
index 5c1a7935dff..10b91b4bf0f 100644
--- a/erpnext/stock/doctype/material_request/material_request.js
+++ b/erpnext/stock/doctype/material_request/material_request.js
@@ -42,6 +42,15 @@ frappe.ui.form.on("Material Request", {
},
};
});
+
+ frm.set_query("price_list", () => {
+ return {
+ filters: {
+ buying: 1,
+ enabled: 1,
+ },
+ };
+ });
},
schedule_date(frm) {
@@ -78,6 +87,7 @@ frappe.ui.form.on("Material Request", {
});
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
+ frm.doc.price_list = frappe.defaults.get_default("buying_price_list");
},
company: function (frm) {
@@ -259,7 +269,9 @@ frappe.ui.form.on("Material Request", {
from_warehouse: item.from_warehouse,
warehouse: item.warehouse,
doctype: frm.doc.doctype,
- buying_price_list: frappe.defaults.get_default("buying_price_list"),
+ buying_price_list: frm.doc.price_list
+ ? frm.doc.price_list
+ : frappe.defaults.get_default("buying_price_list"),
currency: frappe.defaults.get_default("Currency"),
name: frm.doc.name,
qty: item.qty || 1,
diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json
index 1684d531889..76dcd71ecdd 100644
--- a/erpnext/stock/doctype/material_request/material_request.json
+++ b/erpnext/stock/doctype/material_request/material_request.json
@@ -16,6 +16,7 @@
"column_break_2",
"transaction_date",
"schedule_date",
+ "price_list",
"amended_from",
"warehouse_section",
"scan_barcode",
@@ -351,13 +352,19 @@
{
"fieldname": "column_break_13",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "price_list",
+ "fieldtype": "Link",
+ "label": "Price List",
+ "options": "Price List"
}
],
"icon": "fa fa-ticket",
"idx": 70,
"is_submittable": 1,
"links": [],
- "modified": "2024-12-16 12:46:02.262167",
+ "modified": "2025-07-07 13:15:28.615984",
"modified_by": "Administrator",
"module": "Stock",
"name": "Material Request",
@@ -424,10 +431,11 @@
}
],
"quick_entry": 1,
+ "row_format": "Dynamic",
"search_fields": "status,transaction_date",
"show_name_in_global_search": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "title"
-}
\ No newline at end of file
+}
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index 5a6ef50f060..f78c53c0d1e 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -8,6 +8,7 @@
import json
import frappe
+import frappe.defaults
from frappe import _, msgprint
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder.functions import Sum
@@ -53,6 +54,7 @@ class MaterialRequest(BuyingController):
naming_series: DF.Literal["MAT-MR-.YYYY.-"]
per_ordered: DF.Percent
per_received: DF.Percent
+ price_list: DF.Link | None
scan_barcode: DF.Data | None
schedule_date: DF.Date | None
select_print_heading: DF.Link | None
@@ -161,6 +163,9 @@ class MaterialRequest(BuyingController):
self.validate_pp_qty()
+ if not self.price_list:
+ self.price_list = frappe.defaults.get_defaults().buying_price_list
+
def validate_pp_qty(self):
items_from_pp = [item for item in self.items if item.material_request_plan_item]
if items_from_pp:
@@ -833,10 +838,11 @@ def raise_work_orders(material_request):
"material_request_item": d.name,
"planned_start_date": mr.transaction_date,
"company": mr.company,
+ "project": d.project,
}
)
- wo_order.set_work_order_operations()
+ wo_order.get_items_and_operations_from_bom()
wo_order.flags.ignore_validate = True
wo_order.flags.ignore_mandatory = True
wo_order.save()
diff --git a/erpnext/stock/doctype/material_request/material_request_list.js b/erpnext/stock/doctype/material_request/material_request_list.js
index e270278a3d9..2993d1587be 100644
--- a/erpnext/stock/doctype/material_request/material_request_list.js
+++ b/erpnext/stock/doctype/material_request/material_request_list.js
@@ -6,11 +6,11 @@ frappe.listview_settings["Material Request"] = {
return [__("Stopped"), "red", "status,=,Stopped"];
} else if (doc.transfer_status && doc.docstatus != 2) {
if (doc.transfer_status == "Not Started") {
- return [__("Not Started"), "orange"];
+ return [__("Not Started"), "orange", "transfer_status,=,Not Started"];
} else if (doc.transfer_status == "In Transit") {
- return [__("In Transit"), "yellow"];
+ return [__("In Transit"), "yellow", "transfer_status,=,In Transit"];
} else if (doc.transfer_status == "Completed") {
- return [__("Completed"), "green"];
+ return [__("Completed"), "green", "transfer_status,=,Completed"];
}
} else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) == 0) {
return [__("Pending"), "orange", "per_ordered,=,0|docstatus,=,1"];
diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py
index 8c31f9156d6..d5fe895089d 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.py
+++ b/erpnext/stock/doctype/packed_item/packed_item.py
@@ -7,6 +7,7 @@
import json
import frappe
+import frappe.defaults
from frappe.model.document import Document
from frappe.utils import flt
@@ -348,12 +349,20 @@ def on_doctype_update():
@frappe.whitelist()
-def get_items_from_product_bundle(row):
+def get_items_from_product_bundle(row, price_list):
row, items = ItemDetailsCtx(json.loads(row)), []
bundled_items = get_product_bundle_items(row["item_code"])
for item in bundled_items:
- row.update({"item_code": item.item_code, "qty": flt(row["quantity"]) * flt(item.qty)})
+ row.update(
+ {
+ "item_code": item.item_code,
+ "qty": flt(row["quantity"]) * flt(item.qty),
+ "conversion_rate": 1,
+ "price_list": price_list,
+ "currency": frappe.defaults.get_defaults().currency,
+ }
+ )
items.append(get_item_details(row))
return items
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
index 021b7b1cf17..58aa18359df 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
@@ -97,51 +97,25 @@ class QualityInspection(Document):
if self.reference_type == "Stock Entry":
doctype = "Stock Entry Detail"
- child_row_references = frappe.get_all(
- doctype,
- filters={"parent": self.reference_name, "item_code": self.item_code},
- pluck="name",
- )
+ child_doc = frappe.qb.DocType(doctype)
+ qi_doc = frappe.qb.DocType("Quality Inspection")
- if not child_row_references:
- return
+ child_row_references = (
+ frappe.qb.from_(child_doc)
+ .left_join(qi_doc)
+ .on(child_doc.name == qi_doc.child_row_reference)
+ .select(child_doc.name)
+ .where(
+ (child_doc.item_code == self.item_code)
+ & (child_doc.parent == self.reference_name)
+ & (child_doc.docstatus < 2)
+ & (qi_doc.name.isnull())
+ )
+ .orderby(child_doc.idx)
+ ).run(pluck=True)
- if len(child_row_references) == 1:
+ if len(child_row_references):
self.child_row_reference = child_row_references[0]
- else:
- self.distribute_child_row_reference(child_row_references)
-
- def distribute_child_row_reference(self, child_row_references):
- quality_inspections = frappe.get_all(
- "Quality Inspection",
- filters={
- "reference_name": self.reference_name,
- "item_code": self.item_code,
- "docstatus": ("<", 2),
- },
- fields=["name", "child_row_reference", "docstatus"],
- order_by="child_row_reference desc",
- )
-
- for row in quality_inspections:
- if not child_row_references:
- break
-
- if row.child_row_reference and row.child_row_reference in child_row_references:
- child_row_references.remove(row.child_row_reference)
- continue
-
- if row.docstatus == 1:
- continue
-
- if row.name == self.name:
- self.child_row_reference = child_row_references[0]
- else:
- frappe.db.set_value(
- "Quality Inspection", row.name, "child_row_reference", child_row_references[0]
- )
-
- child_row_references.remove(child_row_references[0])
def validate_inspection_required(self):
if frappe.db.get_single_value(
@@ -413,7 +387,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql(
f"""
- SELECT item_code
+ SELECT distinct item_code, item_name, item_group
FROM `tab{from_doctype}`
WHERE parent=%(parent)s and docstatus < 2 and item_code like %(txt)s
{qi_condition} {cond} {mcond}
@@ -444,10 +418,11 @@ def quality_inspection_query(doctype, txt, searchfield, start, page_len, filters
limit_start=start,
limit_page_length=page_len,
filters={
- "docstatus": 1,
+ "docstatus": ("<", 2),
"name": ("like", "%%%s%%" % txt),
"item_code": filters.get("item_code"),
"reference_name": ("in", [filters.get("reference_name", ""), ""]),
+ "child_row_reference": ("in", [filters.get("child_row_reference", ""), ""]),
},
as_list=1,
)
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
index c9dc82fc612..0096ab82580 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
@@ -11,11 +11,11 @@ from frappe.model.document import Document
from frappe.model.naming import make_autoname
from frappe.query_builder.functions import CombineDatetime, Sum
from frappe.utils import (
- add_days,
cint,
cstr,
flt,
get_link_to_form,
+ getdate,
now,
nowtime,
parse_json,
@@ -808,7 +808,7 @@ class SerialandBatchBundle(Document):
if qty_field == "qty" and row.get("stock_qty"):
qty = row.get("stock_qty")
- precision = row.precision
+ precision = row.precision(qty_field)
if abs(abs(flt(self.total_qty, precision)) - abs(flt(qty, precision))) > 0.01:
total_qty = frappe.format_value(abs(flt(self.total_qty)), "Float", row)
set_qty = frappe.format_value(abs(flt(row.get(qty_field))), "Float", row)
@@ -2222,6 +2222,9 @@ def get_auto_batch_nos(kwargs):
picked_batches,
)
+ if kwargs.based_on == "Expiry":
+ available_batches = sorted(available_batches, key=lambda x: (x.expiry_date or getdate("9999-12-31")))
+
if not kwargs.get("do_not_check_future_batches") and available_batches and kwargs.get("posting_date"):
filter_zero_near_batches(available_batches, kwargs)
@@ -2321,6 +2324,7 @@ def get_available_batches(kwargs):
batch_ledger.batch_no,
batch_ledger.warehouse,
Sum(batch_ledger.qty).as_("qty"),
+ batch_table.expiry_date,
)
.where(batch_table.disabled == 0)
.where(stock_ledger_entry.is_cancelled == 0)
@@ -2611,6 +2615,7 @@ def get_stock_ledgers_batches(kwargs):
stock_ledger_entry.item_code,
Sum(stock_ledger_entry.actual_qty).as_("qty"),
stock_ledger_entry.batch_no,
+ batch_table.expiry_date,
)
.where((stock_ledger_entry.is_cancelled == 0) & (stock_ledger_entry.batch_no.isnotnull()))
.groupby(stock_ledger_entry.batch_no, stock_ledger_entry.warehouse)
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index 63597dd3e72..6a1faa193ba 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -179,6 +179,7 @@ frappe.ui.form.on("Stock Entry", {
inspection_type: "Incoming",
reference_type: frm.doc.doctype,
reference_name: frm.doc.name,
+ child_row_reference: row.doc.name,
item_code: row.doc.item_code,
description: row.doc.description,
item_serial_no: row.doc.serial_no ? row.doc.serial_no.split("\n")[0] : null,
@@ -194,6 +195,7 @@ frappe.ui.form.on("Stock Entry", {
filters: {
item_code: d.item_code,
reference_name: doc.name,
+ child_row_reference: d.name,
},
};
});
@@ -1079,6 +1081,8 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
}
erpnext.hide_company(this.frm);
erpnext.utils.add_item(this.frm);
+ erpnext.accounts.ledger_preview.show_accounting_ledger_preview(this.frm);
+ erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm);
}
serial_no(doc, cdt, cdn) {
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 866f0bb778f..9072f7b25c2 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -28,6 +28,7 @@ from erpnext.buying.utils import check_on_hold_or_closed_status
from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals
from erpnext.manufacturing.doctype.bom.bom import (
add_additional_cost,
+ get_bom_items_as_dict,
get_op_cost_from_sub_assemblies,
get_scrap_items_from_sub_assemblies,
validate_bom_no,
@@ -249,6 +250,7 @@ class StockEntry(StockController):
self.validate_closed_subcontracting_order()
self.make_bundle_using_old_serial_batch_fields()
self.update_work_order()
+ self.update_disassembled_order()
self.update_stock_ledger()
self.make_stock_reserve_for_wip_and_fg()
@@ -270,6 +272,7 @@ class StockEntry(StockController):
self.set_material_request_transfer_status("Completed")
def on_cancel(self):
+ self.delink_asset_repair_sabb()
self.validate_closed_subcontracting_order()
self.update_subcontract_order_supplied_items()
self.update_subcontracting_order_status()
@@ -279,6 +282,7 @@ class StockEntry(StockController):
self.validate_work_order_status()
self.update_work_order()
+ self.update_disassembled_order(is_cancel=True)
self.update_stock_ledger()
self.ignore_linked_doctypes = (
@@ -380,6 +384,27 @@ class StockEntry(StockController):
):
frappe.delete_doc("Stock Entry", d.name)
+ def delink_asset_repair_sabb(self):
+ if not self.asset_repair:
+ return
+
+ for row in self.items:
+ if row.serial_and_batch_bundle:
+ voucher_detail_no = frappe.db.get_value(
+ "Asset Repair Consumed Item",
+ {"parent": self.asset_repair, "serial_and_batch_bundle": row.serial_and_batch_bundle},
+ "name",
+ )
+
+ doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle)
+ doc.db_set(
+ {
+ "voucher_type": "Asset Repair",
+ "voucher_no": self.asset_repair,
+ "voucher_detail_no": voucher_detail_no,
+ }
+ )
+
def set_transfer_qty(self):
self.validate_qty_is_not_zero()
for item in self.get("items"):
@@ -1721,6 +1746,13 @@ class StockEntry(StockController):
if not pro_doc.operations:
pro_doc.set_actual_dates()
+ def update_disassembled_order(self, is_cancel=False):
+ if not self.work_order:
+ return
+ if self.purpose == "Disassemble" and self.fg_completed_qty:
+ pro_doc = frappe.get_doc("Work Order", self.work_order)
+ pro_doc.run_method("update_disassembled_qty", self.fg_completed_qty, is_cancel)
+
def make_stock_reserve_for_wip_and_fg(self):
if self.is_stock_reserve_for_work_order():
pro_doc = frappe.get_doc("Work Order", self.work_order)
@@ -1897,7 +1929,7 @@ class StockEntry(StockController):
},
)
- def get_items_for_disassembly(self):
+ def get_items_for_disassembly(self, disassemble_qty, production_item):
"""Get items for Disassembly Order"""
if not self.work_order:
@@ -1905,9 +1937,9 @@ class StockEntry(StockController):
items = self.get_items_from_manufacture_entry()
- s_warehouse = ""
- if self.work_order:
- s_warehouse = frappe.db.get_value("Work Order", self.work_order, "fg_warehouse")
+ s_warehouse = frappe.db.get_value("Work Order", self.work_order, "fg_warehouse")
+
+ items_dict = get_bom_items_as_dict(self.bom_no, self.company, disassemble_qty)
for row in items:
child_row = self.append("items", {})
@@ -1915,6 +1947,15 @@ class StockEntry(StockController):
if value is not None:
child_row.set(field, value)
+ # update qty and amount from BOM items
+ bom_items = items_dict.get(row.item_code)
+ if bom_items:
+ child_row.qty = bom_items.get("qty", child_row.qty)
+ child_row.amount = bom_items.get("amount", child_row.amount)
+
+ if row.item_code == production_item:
+ child_row.qty = disassemble_qty
+
child_row.s_warehouse = (self.from_warehouse or s_warehouse) if row.is_finished_item else ""
child_row.t_warehouse = self.to_warehouse if not row.is_finished_item else ""
child_row.is_finished_item = 0 if row.is_finished_item else 1
@@ -1947,12 +1988,12 @@ class StockEntry(StockController):
)
@frappe.whitelist()
- def get_items(self):
+ def get_items(self, qty=None, production_item=None):
self.set("items", [])
self.validate_work_order()
- if self.purpose == "Disassemble":
- return self.get_items_for_disassembly()
+ if self.purpose == "Disassemble" and qty is not None:
+ return self.get_items_for_disassembly(qty, production_item)
if not self.posting_date or not self.posting_time:
frappe.throw(_("Posting date and posting time is mandatory"))
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
index 9e446d84a21..b4e945b5782 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -192,7 +192,7 @@ class StockLedgerEntry(Document):
mandatory = ["warehouse", "posting_date", "voucher_type", "voucher_no", "company"]
for k in mandatory:
if not self.get(k):
- frappe.throw(_("{0} is required").format(self.meta.get_label(k)))
+ frappe.throw(_("{0} is required").format(_(self.meta.get_label(k))))
if self.voucher_type != "Stock Reconciliation" and not self.actual_qty:
frappe.throw(_("Actual Qty is mandatory"))
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 47fed1531ea..37ff8bef313 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -895,10 +895,6 @@ class StockReconciliation(StockController):
self.update_inventory_dimensions(row, data)
- if self.docstatus == 1 and has_dimensions and (not row.batch_no or not row.serial_and_batch_bundle):
- data.qty_after_transaction = data.actual_qty
- data.actual_qty = 0.0
-
return data
def make_sle_on_cancel(self):
@@ -1261,12 +1257,12 @@ def get_items(warehouse, posting_date, posting_time, company, item_code=None, ig
itemwise_batch_data = get_itemwise_batch(warehouse, posting_date, company, item_code)
for d in items:
- if d.item_code in itemwise_batch_data:
+ if (d.item_code, d.warehouse) in itemwise_batch_data:
valuation_rate = get_stock_balance(
d.item_code, d.warehouse, posting_date, posting_time, with_valuation_rate=True
)[1]
- for row in itemwise_batch_data.get(d.item_code):
+ for row in itemwise_batch_data.get((d.item_code, d.warehouse)):
if ignore_empty_stock and not row.qty:
continue
@@ -1398,7 +1394,7 @@ def get_itemwise_batch(warehouse, posting_date, company, item_code=None):
columns, data = execute(filters)
for row in data:
- itemwise_batch_data.setdefault(row[0], []).append(
+ itemwise_batch_data.setdefault((row[0], row[3]), []).append(
frappe._dict(
{
"item_code": row[0],
diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json
index 5fc6e79273c..faf70b6cb0d 100644
--- a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json
+++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json
@@ -13,7 +13,6 @@
"end_time",
"limits_dont_apply_on",
"item_based_reposting",
- "do_reposting_for_each_stock_transaction",
"errors_notification_section",
"notify_reposting_error_to_role"
],
@@ -66,18 +65,12 @@
"fieldname": "errors_notification_section",
"fieldtype": "Section Break",
"label": "Errors Notification"
- },
- {
- "default": "0",
- "fieldname": "do_reposting_for_each_stock_transaction",
- "fieldtype": "Check",
- "label": "Do reposting for each Stock Transaction"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2024-04-24 12:19:40.204888",
+ "modified": "2025-07-08 11:27:46.659056",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reposting Settings",
@@ -94,8 +87,9 @@
"write": 1
}
],
+ "row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py
index eb3d38bfbfc..50f39817fff 100644
--- a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py
+++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py
@@ -16,7 +16,6 @@ class StockRepostingSettings(Document):
if TYPE_CHECKING:
from frappe.types import DF
- do_reposting_for_each_stock_transaction: DF.Check
end_time: DF.Time | None
item_based_reposting: DF.Check
limit_reposting_timeslot: DF.Check
@@ -30,10 +29,6 @@ class StockRepostingSettings(Document):
def validate(self):
self.set_minimum_reposting_time_slot()
- def before_save(self):
- if self.do_reposting_for_each_stock_transaction:
- self.item_based_reposting = 1
-
def set_minimum_reposting_time_slot(self):
"""Ensure that timeslot for reposting is at least 12 hours."""
if not self.limit_reposting_timeslot:
diff --git a/erpnext/stock/doctype/stock_reposting_settings/test_stock_reposting_settings.py b/erpnext/stock/doctype/stock_reposting_settings/test_stock_reposting_settings.py
index 854914c2775..74cf9f08b77 100644
--- a/erpnext/stock/doctype/stock_reposting_settings/test_stock_reposting_settings.py
+++ b/erpnext/stock/doctype/stock_reposting_settings/test_stock_reposting_settings.py
@@ -38,51 +38,3 @@ class TestStockRepostingSettings(IntegrationTestCase):
users = get_recipients()
self.assertTrue(user in users)
-
- def test_do_reposting_for_each_stock_transaction(self):
- from erpnext.stock.doctype.item.test_item import make_item
- from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
-
- frappe.db.set_single_value("Stock Reposting Settings", "do_reposting_for_each_stock_transaction", 1)
- if frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"):
- frappe.db.set_single_value("Stock Reposting Settings", "item_based_reposting", 0)
-
- item = make_item(
- "_Test item for reposting check for each transaction", properties={"is_stock_item": 1}
- ).name
-
- stock_entry = make_stock_entry(
- item_code=item,
- qty=1,
- rate=100,
- stock_entry_type="Material Receipt",
- target="_Test Warehouse - _TC",
- )
-
- riv = frappe.get_all("Repost Item Valuation", filters={"voucher_no": stock_entry.name}, pluck="name")
- self.assertTrue(riv)
-
- frappe.db.set_single_value("Stock Reposting Settings", "do_reposting_for_each_stock_transaction", 0)
-
- def test_do_not_reposting_for_each_stock_transaction(self):
- from erpnext.stock.doctype.item.test_item import make_item
- from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
-
- frappe.db.set_single_value("Stock Reposting Settings", "do_reposting_for_each_stock_transaction", 0)
- if frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"):
- frappe.db.set_single_value("Stock Reposting Settings", "item_based_reposting", 0)
-
- item = make_item(
- "_Test item for do not reposting check for each transaction", properties={"is_stock_item": 1}
- ).name
-
- stock_entry = make_stock_entry(
- item_code=item,
- qty=1,
- rate=100,
- stock_entry_type="Material Receipt",
- target="_Test Warehouse - _TC",
- )
-
- riv = frappe.get_all("Repost Item Valuation", filters={"voucher_no": stock_entry.name}, pluck="name")
- self.assertFalse(riv)
diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json
index 79837e5513f..e6fef3e0c87 100644
--- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json
+++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json
@@ -93,7 +93,6 @@
"fieldname": "voucher_no",
"fieldtype": "Dynamic Link",
"in_filter": 1,
- "in_list_view": 1,
"in_standard_filter": 1,
"label": "Voucher No",
"no_copy": 1,
@@ -173,7 +172,7 @@
"fieldtype": "Select",
"label": "Status",
"no_copy": 1,
- "options": "Draft\nPartially Reserved\nReserved\nPartially Delivered\nDelivered\nCancelled",
+ "options": "Draft\nPartially Reserved\nReserved\nPartially Delivered\nDelivered\nCancelled\nClosed",
"read_only": 1
},
{
@@ -345,7 +344,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2025-04-30 22:15:22.998138",
+ "modified": "2025-06-24 00:24:40.394164",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reservation Entry",
@@ -455,5 +454,6 @@
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
+ "title_field": "voucher_no",
"track_changes": 1
}
diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
index cad29e6fd9c..b4a165d9e61 100644
--- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
+++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
@@ -1,11 +1,13 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
+from collections import defaultdict
from typing import Literal
import frappe
from frappe import _
from frappe.model.document import Document
+from frappe.query_builder import Case
from frappe.query_builder.functions import Sum
from frappe.utils import cint, flt, nowdate, nowtime, parse_json
@@ -41,7 +43,13 @@ class StockReservationEntry(Document):
reserved_qty: DF.Float
sb_entries: DF.Table[SerialandBatchEntry]
status: DF.Literal[
- "Draft", "Partially Reserved", "Reserved", "Partially Delivered", "Delivered", "Cancelled"
+ "Draft",
+ "Partially Reserved",
+ "Reserved",
+ "Partially Delivered",
+ "Delivered",
+ "Cancelled",
+ "Closed",
]
stock_uom: DF.Link | None
transferred_qty: DF.Float
@@ -114,6 +122,11 @@ class StockReservationEntry(Document):
index = 0
for row in sres:
status = "Reserved"
+
+ if self.has_batch_no or self.has_serial_no:
+ serial_batch_data = self.get_serial_batch_entries()
+ update_serial_batch_delivered_qty(serial_batch_data, row.name, is_cancelled=True)
+
if delivered_qty <= 0 or index == 0:
frappe.db.set_value(
"Stock Reservation Entry",
@@ -143,6 +156,22 @@ class StockReservationEntry(Document):
delivered_qty -= row.reserved_qty
+ def get_serial_batch_entries(self):
+ serial_nos = []
+ batches = defaultdict(float)
+ for entry in self.sb_entries:
+ if entry.serial_no:
+ serial_nos.append(entry.serial_no)
+ elif entry.batch_no:
+ batches[entry.batch_no] += entry.qty
+
+ return frappe._dict(
+ {
+ "serial_nos": serial_nos,
+ "batches": batches,
+ }
+ )
+
def get_from_voucher_reservation_entries(self):
return frappe.get_all(
"Stock Reservation Entry",
@@ -181,7 +210,7 @@ class StockReservationEntry(Document):
]
for d in mandatory:
if not self.get(d):
- msg = _("{0} is required").format(self.meta.get_label(d))
+ msg = _("{0} is required").format(_(self.meta.get_label(d)))
frappe.throw(msg)
def validate_group_warehouse(self) -> None:
@@ -1235,7 +1264,6 @@ class StockReservation:
return available_qty
def transfer_reservation_entries_to(self, docnames, from_doctype, to_doctype):
- delivery_qty_to_update = frappe._dict()
if isinstance(docnames, str):
docnames = [docnames]
@@ -1247,54 +1275,88 @@ class StockReservation:
if not reservation_entries:
return
- entries_to_reserve = []
+ entries_to_reserve = frappe._dict({})
for row in reservation_entries:
+ reserved_qty_field = "reserved_qty" if row.reservation_based_on == "Qty" else "sabb_qty"
+ delivered_qty_field = (
+ "delivered_qty" if row.reservation_based_on == "Qty" else "sabb_delivered_qty"
+ )
+ available_qty = row.get(reserved_qty_field) - row.get(delivered_qty_field)
+
for entry in items_to_reserve:
if not (
row.item_code == entry.item_code and row.warehouse == entry.warehouse and entry.qty > 0
):
continue
- available_qty = row.reserved_qty - row.delivered_qty
if available_qty <= 0:
continue
+ key = (row.item_code, row.warehouse)
+
+ if key not in entries_to_reserve:
+ entries_to_reserve.setdefault(
+ key,
+ frappe._dict(
+ {
+ "qty_to_reserve": 0.0,
+ "item_code": row.item_code,
+ "warehouse": row.warehouse,
+ "voucher_type": entry.voucher_type,
+ "voucher_no": entry.voucher_no,
+ "voucher_detail_no": entry.voucher_detail_no,
+ "serial_nos": [],
+ "sre_names": defaultdict(float),
+ "batches": defaultdict(float),
+ "against_row": row,
+ "company": self.doc.company,
+ }
+ ),
+ )
+
# transfer qty
if available_qty > entry.qty:
qty_to_reserve = entry.qty
- row.delivered_qty += available_qty - entry.qty
- delivery_qty_to_update.setdefault(row.name, row.delivered_qty)
else:
qty_to_reserve = available_qty
- row.delivered_qty += qty_to_reserve
- delivery_qty_to_update.setdefault(row.name, row.delivered_qty)
-
- entries_to_reserve.append([entry, row, qty_to_reserve])
+ available_qty -= qty_to_reserve
entry.qty -= qty_to_reserve
- if delivery_qty_to_update:
- self.update_delivered_qty(delivery_qty_to_update)
+ entries_to_reserve[key]["qty_to_reserve"] += qty_to_reserve
+ if row.has_batch_no:
+ entries_to_reserve[key]["batches"][row.batch_no] += qty_to_reserve
- for entry, row, qty_to_reserve in entries_to_reserve:
- self.make_stock_reservation_entry(entry, row, qty_to_reserve)
+ if row.has_serial_no:
+ entries_to_reserve[key]["serial_nos"].append(row.serial_no)
- def update_delivered_qty(self, delivery_qty_to_update):
- for name, delivered_qty in delivery_qty_to_update.items():
+ if row.name:
+ entries_to_reserve[key]["sre_names"][row.name] += qty_to_reserve
+
+ for key in entries_to_reserve:
+ data = entries_to_reserve[key]
+ self.update_delivered_qty(data)
+ self.make_stock_reservation_entry(data)
+
+ def update_delivered_qty(self, data):
+ for name, delivered_qty in data.get("sre_names").items():
doctype = frappe.qb.DocType("Stock Reservation Entry")
query = (
frappe.qb.update(doctype)
- .set(doctype.delivered_qty, delivered_qty)
+ .set(doctype.delivered_qty, doctype.delivered_qty + delivered_qty)
.set(
doctype.status,
- "Delivered" if doctype.reserved_qty == doctype.delivered_qty else "Reserved",
+ Case().when((doctype.reserved_qty == doctype.delivered_qty), "Closed").else_("Reserved"),
)
.where(doctype.name == name)
)
query.run()
- def make_stock_reservation_entry(self, row, against_row, reserved_qty):
+ if data.serial_nos or data.batches:
+ update_serial_batch_delivered_qty(data, name)
+
+ def make_stock_reservation_entry(self, row):
fields = [
"item_code",
"warehouse",
@@ -1309,9 +1371,11 @@ class StockReservation:
for row_field in fields:
sre.set(row_field, row.get(row_field))
- sre.available_qty = reserved_qty
- sre.reserved_qty = reserved_qty
- sre.voucher_qty = row.required_qty
+ sre.available_qty = row.get("qty_to_reserve")
+ sre.reserved_qty = row.get("qty_to_reserve")
+ sre.voucher_qty = row.get("qty_to_reserve")
+
+ against_row = row.get("against_row")
sre.from_voucher_no = against_row.voucher_no
sre.from_voucher_detail_no = against_row.voucher_detail_no
sre.from_voucher_type = against_row.voucher_type
@@ -1319,26 +1383,68 @@ class StockReservation:
sre.has_serial_no = against_row.has_serial_no
sre.has_batch_no = against_row.has_batch_no
- bundles = [against_row.name]
- if row.serial_and_batch_bundles:
- bundles = row.serial_and_batch_bundles
+ if row.serial_nos:
+ for serial_no in row.serial_nos:
+ batch_no = None
+ if row.batches:
+ batch_no = frappe.db.get_value("Serial No", serial_no, "batch_no")
+
+ sre.append(
+ "sb_entries",
+ {"serial_no": serial_no, "warehouse": row.warehouse, "batch_no": batch_no, "qty": 1},
+ )
+
+ elif row.batches:
+ for batch_no, qty in row.batches.items():
+ sre.append(
+ "sb_entries",
+ {"batch_no": batch_no, "warehouse": row.warehouse, "qty": qty},
+ )
- self.set_serial_batch(sre, bundles)
sre.save()
sre.submit()
def get_reserved_entries(self, doctype, docnames):
- filters = {
- "docstatus": 1,
- "status": ("not in", ["Delivered", "Cancelled", "Draft"]),
- "voucher_type": doctype,
- "voucher_no": docnames,
- }
+ if isinstance(docnames, str):
+ docnames = [docnames]
- if isinstance(docnames, list):
- filters["voucher_no"] = ("in", docnames)
+ sre = frappe.qb.DocType("Stock Reservation Entry")
+ sabb_entry = frappe.qb.DocType("Serial and Batch Entry")
- return frappe.get_all("Stock Reservation Entry", fields=["*"], filters=filters)
+ query = (
+ frappe.qb.from_(sre)
+ .left_join(sabb_entry)
+ .on(sre.name == sabb_entry.parent)
+ .select(
+ sre.name,
+ sre.item_code,
+ sre.warehouse,
+ sre.voucher_type,
+ sre.voucher_no,
+ sre.voucher_detail_no,
+ sre.reserved_qty,
+ sre.delivered_qty,
+ sre.transferred_qty,
+ sre.consumed_qty,
+ sre.has_serial_no,
+ sre.has_batch_no,
+ sre.reservation_based_on,
+ sabb_entry.serial_no,
+ sabb_entry.batch_no,
+ sabb_entry.qty.as_("sabb_qty"),
+ sabb_entry.delivered_qty.as_("sabb_delivered_qty"),
+ )
+ .where(
+ (sre.docstatus == 1)
+ & (sre.status.notin(["Delivered", "Cancelled", "Draft", "Closed"]))
+ & (sre.voucher_type == doctype)
+ & (sre.voucher_no.isin(docnames))
+ )
+ .orderby(sre.creation)
+ .orderby(sabb_entry.idx)
+ )
+
+ return query.run(as_dict=True)
def get_items_to_reserve(self, docnames, from_doctype, to_doctype):
field = frappe.scrub(from_doctype)
@@ -1688,3 +1794,26 @@ def get_stock_reservation_entries_for_voucher(
query = query.where(sre.status.notin(["Delivered", "Cancelled"]))
return query.run(as_dict=True)
+
+
+def update_serial_batch_delivered_qty(row, name, is_cancelled=False):
+ if row.serial_nos:
+ doctype = frappe.qb.DocType("Serial and Batch Entry")
+ query = (
+ frappe.qb.update(doctype)
+ .set(doctype.delivered_qty, (doctype.delivered_qty + (1 if not is_cancelled else -1)))
+ .where((doctype.parent == name) & (doctype.serial_no.isin(row.serial_nos)))
+ )
+
+ query.run()
+
+ elif row.batches:
+ for batch_no, qty in row.batches.items():
+ doctype = frappe.qb.DocType("Serial and Batch Entry")
+ query = (
+ frappe.qb.update(doctype)
+ .set(doctype.delivered_qty, (doctype.delivered_qty + (qty if not is_cancelled else -1 * qty)))
+ .where((doctype.parent == name) & (doctype.batch_no == batch_no))
+ )
+
+ query.run()
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py
index 6d2e9021920..bb5aaebf5f2 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.py
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.py
@@ -121,7 +121,11 @@ class StockSettings(Document):
)
def cant_change_valuation_method(self):
- previous_valuation_method = self.get_doc_before_save().get("valuation_method")
+ doc_before_save = self.get_doc_before_save()
+ if not doc_before_save:
+ return
+
+ previous_valuation_method = doc_before_save.get("valuation_method")
if previous_valuation_method and previous_valuation_method != self.valuation_method:
# check if there are any stock ledger entries against items
diff --git a/erpnext/stock/report/available_serial_no/available_serial_no.py b/erpnext/stock/report/available_serial_no/available_serial_no.py
index 6911b979ae4..c7fd27020c2 100644
--- a/erpnext/stock/report/available_serial_no/available_serial_no.py
+++ b/erpnext/stock/report/available_serial_no/available_serial_no.py
@@ -19,21 +19,17 @@ def execute(filters=None):
columns = get_columns(filters)
items = get_items(filters)
sl_entries = get_stock_ledger_entries(filters, items)
+
+ if not sl_entries:
+ return columns, []
+
item_details = get_item_details(items, sl_entries, False)
-
- opening_row = get_opening_balance_data(filters, columns, sl_entries)
-
+ opening_row = get_opening_balance(filters, columns, sl_entries)
precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
data = process_stock_ledger_entries(sl_entries, item_details, opening_row, precision)
-
return columns, data
-def get_opening_balance_data(filters, columns, sl_entries):
- opening_row = get_opening_balance(filters, columns, sl_entries)
- return opening_row
-
-
def process_stock_ledger_entries(sl_entries, item_details, opening_row, precision):
data = []
diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py
index 87d94f642ab..713161e3f8a 100644
--- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py
+++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py
@@ -57,6 +57,11 @@ def execute(filters=None):
flt(qty_dict.in_qty, float_precision),
flt(qty_dict.out_qty, float_precision),
flt(qty_dict.bal_qty, float_precision),
+ flt(
+ (qty_dict.bal_value / qty_dict.bal_qty) if qty_dict.bal_qty else 0,
+ float_precision,
+ ),
+ flt(qty_dict.bal_value, float_precision),
item_map[item]["stock_uom"],
]
)
@@ -69,14 +74,16 @@ def get_columns(filters):
columns = [
_("Item") + ":Link/Item:100",
- _("Item Name") + "::150",
- _("Description") + "::150",
+ _("Item Name") + "::120",
+ _("Description") + "::90",
_("Warehouse") + ":Link/Warehouse:100",
_("Batch") + ":Link/Batch:100",
_("Opening Qty") + ":Float:90",
_("In Qty") + ":Float:80",
_("Out Qty") + ":Float:80",
- _("Balance Qty") + ":Float:90",
+ _("Balance Qty") + ":Float:120",
+ _("Valuation Rate") + ":Float:120",
+ _("Balance Value") + ":Currency:120",
_("UOM") + "::90",
]
@@ -132,6 +139,7 @@ def get_stock_ledger_entries_for_batch_no(filters):
sle.batch_no,
sle.posting_date,
fn.Sum(sle.actual_qty).as_("actual_qty"),
+ fn.Sum(sle.stock_value_difference).as_("stock_value_difference"),
)
.where(
(sle.docstatus < 2)
@@ -179,6 +187,7 @@ def get_stock_ledger_entries_for_batch_bundle(filters):
batch_package.batch_no,
sle.posting_date,
fn.Sum(batch_package.qty).as_("actual_qty"),
+ fn.Sum(batch_package.stock_value_difference).as_("stock_value_difference"),
)
.where(
(sle.docstatus < 2)
@@ -222,7 +231,10 @@ def get_item_warehouse_batch_map(filters, float_precision):
for d in sle:
iwb_map.setdefault(d.item_code, {}).setdefault(d.warehouse, {}).setdefault(
- d.batch_no, frappe._dict({"opening_qty": 0.0, "in_qty": 0.0, "out_qty": 0.0, "bal_qty": 0.0})
+ d.batch_no,
+ frappe._dict(
+ {"opening_qty": 0.0, "in_qty": 0.0, "out_qty": 0.0, "bal_qty": 0.0, "bal_value": 0.0}
+ ),
)
qty_dict = iwb_map[d.item_code][d.warehouse][d.batch_no]
if d.posting_date < from_date:
@@ -238,6 +250,7 @@ def get_item_warehouse_batch_map(filters, float_precision):
)
qty_dict.bal_qty = flt(qty_dict.bal_qty, float_precision) + flt(d.actual_qty, float_precision)
+ qty_dict.bal_value += flt(d.stock_value_difference, float_precision)
return iwb_map
diff --git a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py
index 9b4520064d6..3193ba3de51 100644
--- a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py
+++ b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py
@@ -5,6 +5,7 @@
import frappe
from frappe import _
from frappe.utils import flt, today
+from frappe.utils.nestedset import get_descendants_of
from pypika.terms import ExistsCriterion
from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_pos_reserved_qty
@@ -21,6 +22,10 @@ def execute(filters=None):
columns = get_columns()
bin_list = get_bin_list(filters)
item_map = get_item_map(filters.get("item_code"), include_uom)
+ item_groups = []
+ if filters.get("item_group"):
+ item_groups.append(filters.item_group)
+ item_groups.extend(get_descendants_of("Item Group", filters.item_group))
warehouse_company = {}
data = []
@@ -40,7 +45,7 @@ def execute(filters=None):
if filters.brand and filters.brand != item.brand:
continue
- elif filters.item_group and filters.item_group != item.item_group:
+ elif item_groups and item.item_group not in item_groups:
continue
elif filters.company and filters.company != company:
diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py
index 312f2799955..758fae6ab97 100644
--- a/erpnext/stock/serial_batch_bundle.py
+++ b/erpnext/stock/serial_batch_bundle.py
@@ -285,7 +285,7 @@ class SerialBatchBundle:
frappe.throw(_(msg))
def delink_serial_and_batch_bundle(self):
- if self.is_pos_transaction():
+ if self.is_pos_or_asset_repair_transaction():
return
update_values = {
@@ -338,21 +338,29 @@ class SerialBatchBundle:
self.cancel_serial_and_batch_bundle()
def cancel_serial_and_batch_bundle(self):
- if self.is_pos_transaction():
+ if self.is_pos_or_asset_repair_transaction():
return
doc = frappe.get_cached_doc("Serial and Batch Bundle", self.sle.serial_and_batch_bundle)
if doc.docstatus == 1:
doc.cancel()
- def is_pos_transaction(self):
+ def is_pos_or_asset_repair_transaction(self):
+ voucher_type = frappe.get_cached_value(
+ "Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "voucher_type"
+ )
+
if (
self.sle.voucher_type == "Sales Invoice"
and self.sle.serial_and_batch_bundle
- and frappe.get_cached_value(
- "Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "voucher_type"
- )
- == "POS Invoice"
+ and voucher_type == "POS Invoice"
+ ):
+ return True
+
+ if (
+ self.sle.voucher_type == "Stock Entry"
+ and self.sle.serial_and_batch_bundle
+ and voucher_type == "Asset Repair"
):
return True
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 8030d806bcf..fb90e37fa98 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -923,6 +923,7 @@ class update_entries_after:
)
sle.doctype = "Stock Ledger Entry"
+ sle.modified = now()
frappe.get_doc(sle).db_update()
if not self.args.get("sle_id") or (
@@ -1258,12 +1259,19 @@ class update_entries_after:
def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
- if sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and frappe.get_cached_value(
- sle.voucher_type, sle.voucher_no, "is_internal_supplier"
- ):
- frappe.db.set_value(
- f"{sle.voucher_type} Item", sle.voucher_detail_no, "valuation_rate", sle.outgoing_rate
+ if sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]:
+ details = frappe.get_cached_value(
+ sle.voucher_type,
+ sle.voucher_no,
+ ["is_internal_supplier", "is_return", "return_against"],
+ as_dict=True,
)
+ if details.is_internal_supplier or (details.is_return and not details.return_against):
+ rate = outgoing_rate if details.is_return else sle.outgoing_rate
+
+ frappe.db.set_value(
+ f"{sle.voucher_type} Item", sle.voucher_detail_no, "valuation_rate", rate
+ )
else:
frappe.db.set_value(
"Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
diff --git a/erpnext/templates/includes/footer/footer_extension.html b/erpnext/templates/includes/footer/footer_extension.html
deleted file mode 100644
index 11e0adaa2ee..00000000000
--- a/erpnext/templates/includes/footer/footer_extension.html
+++ /dev/null
@@ -1,42 +0,0 @@
-{% if not hide_footer_signup %}
-
-
-
-
-{% endif %}
diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py
index 731e94723d5..65309e69923 100644
--- a/erpnext/utilities/transaction_base.py
+++ b/erpnext/utilities/transaction_base.py
@@ -68,7 +68,7 @@ class TransactionBase(StatusUpdater):
frappe.throw(_("Invalid reference {0} {1}").format(reference_doctype, reference_name))
for field, condition in fields:
- if prevdoc_values[field] is not None and field not in self.exclude_fields:
+ if prevdoc_values[field] not in [None, ""] and field not in self.exclude_fields:
self.validate_value(field, condition, prevdoc_values[field], doc)
def get_prev_doc_reference_details(self, reference_names, reference_doctype, fields):
diff --git a/pyproject.toml b/pyproject.toml
index 5fb1a0ad272..489682cdb75 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -9,16 +9,15 @@ readme = "README.md"
dynamic = ["version"]
dependencies = [
# Core dependencies
- "pycountry~=24.6.1",
- "Unidecode~=1.3.6",
+ "Unidecode~=1.4.0",
"barcodenumber~=0.5.0",
"rapidfuzz~=3.12.2",
- "holidays~=0.28",
+ "holidays~=0.75",
# integration dependencies
- "googlemaps",
+ "googlemaps~=4.10.0",
"plaid-python~=7.2.1",
- "python-youtube~=0.8.0",
+ "python-youtube~=0.9.7",
# Not used directly - required by PyQRCode for PNG generation
"pypng~=0.20220715.0",