Merge pull request #49227 from frappe/version-15-hotfix

chore: release v15
This commit is contained in:
ruthra kumar
2025-08-19 17:25:00 +05:30
committed by GitHub
65 changed files with 1155 additions and 263 deletions

View File

@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Entire Repository
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false

View File

@@ -7,6 +7,7 @@
"engine": "InnoDB",
"field_order": [
"accounting_dimension",
"fieldname",
"disabled",
"column_break_2",
"company",
@@ -90,11 +91,17 @@
"fieldname": "apply_restriction_on_values",
"fieldtype": "Check",
"label": "Apply restriction on dimension values"
},
{
"fieldname": "fieldname",
"fieldtype": "Data",
"hidden": 1,
"label": "Fieldname"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-06-07 14:59:41.869117",
"modified": "2025-08-08 14:13:22.203011",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting Dimension Filter",
@@ -139,8 +146,8 @@
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -17,17 +17,16 @@ class AccountingDimensionFilter(Document):
from frappe.types import DF
from erpnext.accounts.doctype.allowed_dimension.allowed_dimension import AllowedDimension
from erpnext.accounts.doctype.applicable_on_account.applicable_on_account import (
ApplicableOnAccount,
)
from erpnext.accounts.doctype.applicable_on_account.applicable_on_account import ApplicableOnAccount
accounting_dimension: DF.Literal
accounting_dimension: DF.Literal[None]
accounts: DF.Table[ApplicableOnAccount]
allow_or_restrict: DF.Literal["Allow", "Restrict"]
apply_restriction_on_values: DF.Check
company: DF.Link
dimensions: DF.Table[AllowedDimension]
disabled: DF.Check
fieldname: DF.Data | None
# end: auto-generated types
def before_save(self):
@@ -37,6 +36,10 @@ class AccountingDimensionFilter(Document):
self.set("dimensions", [])
def validate(self):
self.fieldname = frappe.db.get_value(
"Accounting Dimension", {"document_type": self.accounting_dimension}, "fieldname"
) or frappe.scrub(self.accounting_dimension) # scrub to handle default accounting dimension
self.validate_applicable_accounts()
def validate_applicable_accounts(self):
@@ -72,7 +75,7 @@ def get_dimension_filter_map():
"""
SELECT
a.applicable_on_account, d.dimension_value, p.accounting_dimension,
p.allow_or_restrict, a.is_mandatory
p.allow_or_restrict, p.fieldname, a.is_mandatory
FROM
`tabApplicable On Account` a,
`tabAccounting Dimension Filter` p
@@ -87,8 +90,6 @@ def get_dimension_filter_map():
dimension_filter_map = {}
for f in filters:
f.fieldname = scrub(f.accounting_dimension)
build_map(
dimension_filter_map,
f.fieldname,

View File

@@ -462,9 +462,8 @@ def unset_existing_data(company):
"Sales Taxes and Charges Template",
"Purchase Taxes and Charges Template",
]:
frappe.db.sql(
f'''delete from `tab{doctype}` where `company`="%s"''' % (company) # nosec
)
dt = frappe.qb.DocType(doctype)
frappe.qb.from_(dt).where(dt.company == company).delete().run()
def set_default_accounts(company):

View File

@@ -11,6 +11,7 @@
-> Resolves dunning automatically
"""
import json
import frappe
@@ -156,40 +157,66 @@ class Dunning(AccountsController):
]
def resolve_dunning(doc, state):
"""
Check if all payments have been made and resolve dunning, if yes. Called
when a Payment Entry is submitted.
"""
for reference in doc.references:
# Consider partial and full payments:
# Submitting full payment: outstanding_amount will be 0
# Submitting 1st partial payment: outstanding_amount will be the pending installment
# Cancelling full payment: outstanding_amount will revert to total amount
# Cancelling last partial payment: outstanding_amount will revert to pending amount
submit_condition = reference.outstanding_amount < reference.total_amount
cancel_condition = reference.outstanding_amount <= reference.total_amount
def update_linked_dunnings(doc, previous_outstanding_amount):
if (
doc.doctype != "Sales Invoice"
or doc.is_return
or previous_outstanding_amount == doc.outstanding_amount
):
return
if reference.reference_doctype == "Sales Invoice" and (
submit_condition if doc.docstatus == 1 else cancel_condition
):
state = "Resolved" if doc.docstatus == 2 else "Unresolved"
dunnings = get_linked_dunnings_as_per_state(reference.reference_name, state)
to_resolve = doc.outstanding_amount < previous_outstanding_amount
state = "Unresolved" if to_resolve else "Resolved"
dunnings = get_linked_dunnings_as_per_state(doc.name, state)
if not dunnings:
return
for dunning in dunnings:
resolve = True
dunning = frappe.get_doc("Dunning", dunning.get("name"))
for overdue_payment in dunning.overdue_payments:
outstanding_inv = frappe.get_value(
"Sales Invoice", overdue_payment.sales_invoice, "outstanding_amount"
)
outstanding_ps = frappe.get_value(
"Payment Schedule", overdue_payment.payment_schedule, "outstanding"
)
resolve = False if (outstanding_ps > 0 and outstanding_inv > 0) else True
dunnings = [frappe.get_doc("Dunning", dunning.name) for dunning in dunnings]
invoices = set()
payment_schedule_ids = set()
dunning.status = "Resolved" if resolve else "Unresolved"
dunning.save()
for dunning in dunnings:
for overdue_payment in dunning.overdue_payments:
invoices.add(overdue_payment.sales_invoice)
if overdue_payment.payment_schedule:
payment_schedule_ids.add(overdue_payment.payment_schedule)
invoice_outstanding_amounts = dict(
frappe.get_all(
"Sales Invoice",
filters={"name": ["in", list(invoices)]},
fields=["name", "outstanding_amount"],
as_list=True,
)
)
ps_outstanding_amounts = (
dict(
frappe.get_all(
"Payment Schedule",
filters={"name": ["in", list(payment_schedule_ids)]},
fields=["name", "outstanding"],
as_list=True,
)
)
if payment_schedule_ids
else {}
)
for dunning in dunnings:
has_outstanding = False
for overdue_payment in dunning.overdue_payments:
invoice_outstanding = invoice_outstanding_amounts[overdue_payment.sales_invoice]
ps_outstanding = ps_outstanding_amounts.get(overdue_payment.payment_schedule, 0)
has_outstanding = invoice_outstanding > 0 and ps_outstanding > 0
if has_outstanding:
break
new_status = "Resolved" if not has_outstanding else "Unresolved"
if dunning.status != new_status:
dunning.status = new_status
dunning.save()
def get_linked_dunnings_as_per_state(sales_invoice, state):

View File

@@ -139,6 +139,64 @@ class TestDunning(FrappeTestCase):
self.assertEqual(sales_invoice.status, "Overdue")
self.assertEqual(dunning.status, "Unresolved")
def test_dunning_resolution_from_credit_note(self):
"""
Test that dunning is resolved when a credit note is issued against the original invoice.
"""
sales_invoice = create_sales_invoice_against_cost_center(
posting_date=add_days(today(), -10), qty=1, rate=100
)
dunning = create_dunning_from_sales_invoice(sales_invoice.name)
dunning.submit()
self.assertEqual(dunning.status, "Unresolved")
credit_note = frappe.copy_doc(sales_invoice)
credit_note.is_return = 1
credit_note.return_against = sales_invoice.name
credit_note.update_outstanding_for_self = 0
for item in credit_note.items:
item.qty = -item.qty
credit_note.save()
credit_note.submit()
dunning.reload()
self.assertEqual(dunning.status, "Resolved")
credit_note.cancel()
dunning.reload()
self.assertEqual(dunning.status, "Unresolved")
def test_dunning_not_affected_by_standalone_credit_note(self):
"""
Test that dunning is NOT resolved when a credit note has update_outstanding_for_self checked.
"""
sales_invoice = create_sales_invoice_against_cost_center(
posting_date=add_days(today(), -10), qty=1, rate=100
)
dunning = create_dunning_from_sales_invoice(sales_invoice.name)
dunning.submit()
self.assertEqual(dunning.status, "Unresolved")
credit_note = frappe.copy_doc(sales_invoice)
credit_note.is_return = 1
credit_note.return_against = sales_invoice.name
credit_note.update_outstanding_for_self = 1
for item in credit_note.items:
item.qty = -item.qty
credit_note.save()
credit_note = frappe.get_doc("Sales Invoice", credit_note.name)
credit_note.submit()
dunning.reload()
self.assertEqual(dunning.status, "Unresolved")
def create_dunning(overdue_days, dunning_type_name=None):
posting_date = add_days(today(), -1 * overdue_days)

View File

@@ -5,6 +5,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.query_builder.functions import Sum
from frappe.utils import flt, today
@@ -45,22 +46,30 @@ def get_loyalty_details(
if not expiry_date:
expiry_date = today()
condition = ""
if company:
condition = " and company=%s " % frappe.db.escape(company)
if not include_expired_entry:
condition += " and expiry_date>='%s' " % expiry_date
LoyaltyPointEntry = frappe.qb.DocType("Loyalty Point Entry")
loyalty_point_details = frappe.db.sql(
f"""select sum(loyalty_points) as loyalty_points,
sum(purchase_amount) as total_spent from `tabLoyalty Point Entry`
where customer=%s and loyalty_program=%s and posting_date <= %s
{condition}
group by customer""",
(customer, loyalty_program, expiry_date),
as_dict=1,
query = (
frappe.qb.from_(LoyaltyPointEntry)
.select(
Sum(LoyaltyPointEntry.loyalty_points).as_("loyalty_points"),
Sum(LoyaltyPointEntry.purchase_amount).as_("total_spent"),
)
.where(
(LoyaltyPointEntry.customer == customer)
& (LoyaltyPointEntry.loyalty_program == loyalty_program)
& (LoyaltyPointEntry.posting_date <= expiry_date)
)
.groupby(LoyaltyPointEntry.customer)
)
if company:
query = query.where(LoyaltyPointEntry.company == company)
if not include_expired_entry:
query = query.where(LoyaltyPointEntry.expiry_date >= expiry_date)
loyalty_point_details = query.run(as_dict=True)
if loyalty_point_details:
return loyalty_point_details[0]
else:

View File

@@ -118,9 +118,9 @@ class PaymentEntry(AccountsController):
if self.difference_amount:
frappe.throw(_("Difference Amount must be zero"))
self.update_payment_requests()
self.update_payment_schedule()
self.make_gl_entries()
self.update_outstanding_amounts()
self.update_payment_schedule()
self.set_status()
def validate_for_repost(self):
@@ -221,10 +221,10 @@ class PaymentEntry(AccountsController):
)
super().on_cancel()
self.update_payment_requests(cancel=True)
self.update_payment_schedule(cancel=1)
self.make_gl_entries(cancel=1)
self.update_outstanding_amounts()
self.delink_advance_entry_references()
self.update_payment_schedule(cancel=1)
self.set_status()
def update_payment_requests(self, cancel=False):

View File

@@ -55,6 +55,16 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
});
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
if (this.frm.doc.pos_profile) {
frappe.db
.get_value("POS Profile", this.frm.doc.pos_profile, "disable_grand_total_to_default_mop")
.then((r) => {
if (!r.exc) {
this.frm.skip_default_payment = r.message.disable_grand_total_to_default_mop;
}
});
}
}
onload_post_render(frm) {
@@ -113,6 +123,7 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
this.frm.meta.default_print_format = r.message.print_format || "";
this.frm.doc.campaign = r.message.campaign;
this.frm.allow_print_before_pay = r.message.allow_print_before_pay;
this.frm.skip_default_payment = r.message.skip_default_payment;
}
this.frm.script_manager.trigger("update_stock");
this.calculate_taxes_and_totals();

View File

@@ -62,6 +62,7 @@
"items_section",
"update_stock",
"scan_barcode",
"last_scanned_warehouse",
"items",
"pricing_rule_details",
"pricing_rules",
@@ -1569,6 +1570,13 @@
"label": "Company Contact Person",
"options": "Contact",
"print_hide": 1
},
{
"depends_on": "eval: doc.last_scanned_warehouse",
"fieldname": "last_scanned_warehouse",
"fieldtype": "Data",
"is_virtual": 1,
"label": "Last Scanned Warehouse"
}
],
"icon": "fa fa-file-text",

View File

@@ -216,6 +216,7 @@ class POSInvoice(SalesInvoice):
self.validate_loyalty_transaction()
self.validate_company_with_pos_company()
self.validate_full_payment()
self.update_packing_list()
if self.coupon_code:
from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code
@@ -367,9 +368,9 @@ class POSInvoice(SalesInvoice):
)
elif is_stock_item and flt(available_stock) < flt(d.stock_qty):
frappe.throw(
_(
"Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}."
).format(d.idx, item_code, warehouse, available_stock),
_("Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}.").format(
d.idx, item_code, warehouse
),
title=_("Item Unavailable"),
)
@@ -680,6 +681,7 @@ class POSInvoice(SalesInvoice):
"print_format": print_format,
"campaign": profile.get("campaign"),
"allow_print_before_pay": profile.get("allow_print_before_pay"),
"skip_default_payment": profile.get("disable_grand_total_to_default_mop"),
}
@frappe.whitelist()
@@ -774,10 +776,8 @@ def get_bundle_availability(bundle_item_code, warehouse):
bundle_bin_qty = 1000000
for item in product_bundle.items:
item_bin_qty = get_bin_qty(item.item_code, warehouse)
item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse)
available_qty = item_bin_qty - item_pos_reserved_qty
max_available_bundles = available_qty / item.qty
max_available_bundles = item_bin_qty / item.qty
if bundle_bin_qty > max_available_bundles and frappe.get_value(
"Item", item.item_code, "is_stock_item"
):
@@ -800,13 +800,49 @@ def get_bin_qty(item_code, warehouse):
def get_pos_reserved_qty(item_code, warehouse):
"""
Calculate total quantity reserved for the given item and warehouse.
Includes:
- Direct sales of the item in submitted POS Invoices
- Sales of the item as a component of a Product Bundle
Excludes consolidated invoices (already merged into Sales Invoices via
POS Closing Entry). Used to reflect near real-time availability in the
POS UI and to prevent overselling while multiple sessions may be active.
"""
pinv_item_reserved_qty = get_pos_reserved_qty_from_table("POS Invoice Item", item_code, warehouse)
packed_item_reserved_qty = get_pos_reserved_qty_from_table("Packed Item", item_code, warehouse)
reserved_qty = pinv_item_reserved_qty + packed_item_reserved_qty
return reserved_qty
def get_pos_reserved_qty_from_table(child_table, item_code, warehouse):
"""
Get the total reserved quantity for a given item in POS Invoices
from a specific child table.
Args:
child_table (str): Name of the child table to query
(e.g., "POS Invoice Item", "Packed Item").
item_code (str): The Item Code to filter by.
warehouse (str): The Warehouse to filter by.
Returns:
float: The total reserved quantity for the item in the given
warehouse from submitted, unconsolidated POS Invoices.
"""
p_inv = frappe.qb.DocType("POS Invoice")
p_item = frappe.qb.DocType("POS Invoice Item")
p_item = frappe.qb.DocType(child_table)
qty_column = "qty" if child_table == "Packed Item" else "stock_qty"
reserved_qty = (
frappe.qb.from_(p_inv)
.from_(p_item)
.select(Sum(p_item.stock_qty).as_("stock_qty"))
.select(Sum(p_item[qty_column]).as_("stock_qty"))
.where(
(p_inv.name == p_item.parent)
& (IfNull(p_inv.consolidated_invoice, "") == "")

View File

@@ -153,6 +153,9 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
per_billed: ["<", 99.99],
company: me.frm.doc.company,
},
allow_child_item_selection: true,
child_fieldname: "items",
child_columns: ["item_code", "item_name", "qty", "amount", "billed_amt"],
});
},
__("Get Items From")
@@ -175,6 +178,9 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
company: me.frm.doc.company,
is_return: 0,
},
allow_child_item_selection: true,
child_fieldname: "items",
child_columns: ["item_code", "item_name", "qty", "amount", "billed_amt"],
});
},
__("Get Items From")

View File

@@ -47,6 +47,7 @@
"ignore_pricing_rule",
"sec_warehouse",
"scan_barcode",
"last_scanned_warehouse",
"col_break_warehouse",
"update_stock",
"set_warehouse",
@@ -1644,6 +1645,13 @@
"label": "Select Dispatch Address ",
"options": "Address",
"print_hide": 1
},
{
"depends_on": "eval: doc.last_scanned_warehouse",
"fieldname": "last_scanned_warehouse",
"fieldtype": "Data",
"is_virtual": 1,
"label": "Last Scanned Warehouse"
}
],
"grid_page_length": 50,

View File

@@ -2,6 +2,8 @@
# License: GNU General Public License v3. See license.txt
import json
import frappe
from frappe import _, qb, throw
from frappe.model.mapper import get_mapped_doc
@@ -2070,7 +2072,12 @@ def make_inter_company_sales_invoice(source_name, target_doc=None):
@frappe.whitelist()
def make_purchase_receipt(source_name, target_doc=None):
def make_purchase_receipt(source_name, target_doc=None, args=None):
if args is None:
args = {}
if isinstance(args, str):
args = json.loads(args)
def update_item(obj, target, source_parent):
target.qty = flt(obj.qty) - flt(obj.received_qty)
target.received_qty = flt(obj.qty) - flt(obj.received_qty)
@@ -2080,6 +2087,11 @@ def make_purchase_receipt(source_name, target_doc=None):
(flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) * flt(source_parent.conversion_rate)
)
def select_item(d):
filtered_items = args.get("filtered_children", [])
child_filter = d.name in filtered_items if filtered_items else True
return child_filter
doc = get_mapped_doc(
"Purchase Invoice",
source_name,
@@ -2103,7 +2115,7 @@ def make_purchase_receipt(source_name, target_doc=None):
"wip_composite_asset": "wip_composite_asset",
},
"postprocess": update_item,
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty),
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) and select_item(doc),
},
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges"},
},

View File

@@ -58,6 +58,13 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
me.frm.script_manager.trigger("is_pos");
me.frm.refresh_fields();
frappe.db
.get_value("POS Profile", this.frm.doc.pos_profile, "disable_grand_total_to_default_mop")
.then((r) => {
if (!r.exc) {
me.frm.skip_default_payment = r.message.disable_grand_total_to_default_mop;
}
});
}
erpnext.queries.setup_warehouse_query(this.frm);
}
@@ -259,6 +266,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
per_billed: ["<", 99.99],
company: me.frm.doc.company,
},
allow_child_item_selection: true,
child_fieldname: "items",
child_columns: ["item_code", "item_name", "qty", "amount", "billed_amt"],
});
},
__("Get Items From")
@@ -288,6 +298,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
status: ["!=", "Lost"],
company: me.frm.doc.company,
},
allow_child_item_selection: true,
child_fieldname: "items",
child_columns: ["item_code", "item_name", "qty", "rate", "amount"],
});
},
__("Get Items From")
@@ -319,6 +332,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
filters: filters,
};
},
allow_child_item_selection: true,
child_fieldname: "items",
child_columns: ["item_code", "item_name", "qty", "amount", "billed_amt"],
});
},
__("Get Items From")
@@ -497,8 +513,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
},
callback: function (r) {
if (!r.exc) {
if (r.message && r.message.print_format) {
if (r.message) {
me.frm.pos_print_format = r.message.print_format;
me.frm.skip_default_payment = r.message.skip_default_payment;
}
me.frm.trigger("update_stock");
if (me.frm.doc.taxes_and_charges) {

View File

@@ -45,6 +45,7 @@
"items_section",
"scan_barcode",
"update_stock",
"last_scanned_warehouse",
"column_break_39",
"set_warehouse",
"set_target_warehouse",
@@ -2177,6 +2178,13 @@
"label": "Company Contact Person",
"options": "Contact",
"print_hide": 1
},
{
"depends_on": "eval: doc.last_scanned_warehouse",
"fieldname": "last_scanned_warehouse",
"fieldtype": "Data",
"is_virtual": 1,
"label": "Last Scanned Warehouse"
}
],
"grid_page_length": 50,

View File

@@ -700,6 +700,7 @@ class SalesInvoice(SellingController):
"allow_edit_discount": pos.get("allow_user_to_edit_discount"),
"campaign": pos.get("campaign"),
"allow_print_before_pay": pos.get("allow_print_before_pay"),
"skip_default_payment": pos.get("disable_grand_total_to_default_mop"),
}
def update_time_sheet(self, sales_invoice):

View File

@@ -25,17 +25,25 @@ def get_group_by_asset_category_data(filters):
asset_categories = get_asset_categories_for_grouped_by_category(filters)
assets = get_assets_for_grouped_by_category(filters)
asset_value_adjustment_map = get_asset_value_adjustment_map_by_category(filters)
for asset_category in asset_categories:
row = frappe._dict()
row.update(asset_category)
adjustments = asset_value_adjustment_map.get(asset_category.get("asset_category"), {})
row.adjustment_before_from_date = flt(adjustments.get("adjustment_before_from_date", 0))
row.adjustment_till_to_date = flt(adjustments.get("adjustment_till_to_date", 0))
row.adjustment_during_period = row.adjustment_till_to_date - row.adjustment_before_from_date
row.value_as_on_from_date += row.adjustment_before_from_date
row.value_as_on_to_date = (
flt(row.value_as_on_from_date)
+ flt(row.value_of_new_purchase)
- flt(row.value_of_sold_asset)
- flt(row.value_of_scrapped_asset)
- flt(row.value_of_capitalized_asset)
+ flt(row.adjustment_during_period)
)
row.update(
@@ -229,26 +237,93 @@ def get_assets_for_grouped_by_category(filters):
)
def get_asset_value_adjustment_map_by_category(filters):
asset_value_adjustments = frappe.db.sql(
"""
SELECT
a.asset_category AS asset_category,
IFNULL(
SUM(
CASE
WHEN gle.posting_date < %(from_date)s
AND (a.disposal_date IS NULL OR a.disposal_date >= %(from_date)s)
THEN gle.debit - gle.credit
ELSE 0
END
),
0) AS value_adjustment_before_from_date,
IFNULL(
SUM(
CASE
WHEN gle.posting_date <= %(to_date)s
AND (a.disposal_date IS NULL OR a.disposal_date >= %(to_date)s)
THEN gle.debit - gle.credit
ELSE 0
END
),
0) AS value_adjustment_till_to_date
FROM `tabGL Entry` gle
JOIN `tabAsset` a ON gle.against_voucher = a.name
JOIN `tabAsset Category Account` aca
ON aca.parent = a.asset_category
AND aca.company_name = %(company)s
WHERE gle.is_cancelled = 0
AND a.docstatus = 1
AND a.company = %(company)s
AND a.purchase_date <= %(to_date)s
AND gle.account = aca.fixed_asset_account
GROUP BY a.asset_category
""",
{"from_date": filters.from_date, "to_date": filters.to_date, "company": filters.company},
as_dict=1,
)
category_value_adjustment_map = {}
for r in asset_value_adjustments:
category_value_adjustment_map[r["asset_category"]] = {
"adjustment_before_from_date": flt(r.get("value_adjustment_before_from_date", 0)),
"adjustment_till_to_date": flt(r.get("value_adjustment_till_to_date", 0)),
}
return category_value_adjustment_map
def get_group_by_asset_data(filters):
data = []
asset_details = get_asset_details_for_grouped_by_category(filters)
assets = get_assets_for_grouped_by_asset(filters)
asset_value_adjustment_map = get_asset_value_adjustment_map(filters)
for asset_detail in asset_details:
row = frappe._dict()
row.update(asset_detail)
row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", "")))
adjustments = asset_value_adjustment_map.get(
asset_detail.get("name", ""),
{
"adjustment_before_from_date": 0.0,
"adjustment_till_to_date": 0.0,
},
)
row.adjustment_before_from_date = adjustments["adjustment_before_from_date"]
row.adjustment_till_to_date = adjustments["adjustment_till_to_date"]
row.adjustment_during_period = flt(row.adjustment_till_to_date) - flt(row.adjustment_before_from_date)
row.value_as_on_from_date += row.adjustment_before_from_date
row.value_as_on_to_date = (
flt(row.value_as_on_from_date)
+ flt(row.value_of_new_purchase)
- flt(row.value_of_sold_asset)
- flt(row.value_of_scrapped_asset)
- flt(row.value_of_capitalized_asset)
+ flt(row.adjustment_during_period)
)
row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", "")))
row.accumulated_depreciation_as_on_to_date = (
flt(row.accumulated_depreciation_as_on_from_date)
+ flt(row.depreciation_amount_during_the_period)
@@ -432,6 +507,59 @@ def get_assets_for_grouped_by_asset(filters):
)
def get_asset_value_adjustment_map(filters):
asset_with_value_adjustments = frappe.db.sql(
"""
SELECT
a.name AS asset,
IFNULL(
SUM(
CASE
WHEN gle.posting_date < %(from_date)s
AND (a.disposal_date IS NULL OR a.disposal_date >= %(from_date)s)
THEN gle.debit - gle.credit
ELSE 0
END
),
0) AS value_adjustment_before_from_date,
IFNULL(
SUM(
CASE
WHEN gle.posting_date <= %(to_date)s
AND (a.disposal_date IS NULL OR a.disposal_date >= %(to_date)s)
THEN gle.debit - gle.credit
ELSE 0
END
),
0) AS value_adjustment_till_to_date
FROM `tabGL Entry` gle
JOIN `tabAsset` a ON gle.against_voucher = a.name
JOIN `tabAsset Category Account` aca
ON aca.parent = a.asset_category
AND aca.company_name = %(company)s
WHERE gle.is_cancelled = 0
AND a.docstatus = 1
AND a.company = %(company)s
AND a.purchase_date <= %(to_date)s
AND gle.account = aca.fixed_asset_account
GROUP BY a.name
""",
{"from_date": filters.from_date, "to_date": filters.to_date, "company": filters.company},
as_dict=1,
)
asset_value_adjustment_map = {}
for r in asset_with_value_adjustments:
asset_value_adjustment_map[r["asset"]] = {
"adjustment_before_from_date": flt(r.get("value_adjustment_before_from_date", 0)),
"adjustment_till_to_date": flt(r.get("value_adjustment_till_to_date", 0)),
}
return asset_value_adjustment_map
def get_columns(filters):
columns = []

View File

@@ -1,29 +1,34 @@
{
"add_total_row": 0,
"apply_user_permissions": 1,
"creation": "2013-12-06 13:22:23",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 3,
"is_standard": "Yes",
"modified": "2017-02-24 20:17:51.995451",
"modified_by": "Administrator",
"module": "Accounts",
"name": "General Ledger",
"owner": "Administrator",
"ref_doctype": "GL Entry",
"report_name": "General Ledger",
"report_type": "Script Report",
"add_total_row": 1,
"add_translate_data": 0,
"columns": [],
"creation": "2013-12-06 13:22:23",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 3,
"is_standard": "Yes",
"letterhead": null,
"modified": "2025-08-13 12:47:27.645023",
"modified_by": "Administrator",
"module": "Accounts",
"name": "General Ledger",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "GL Entry",
"report_name": "General Ledger",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts User"
},
},
{
"role": "Accounts Manager"
},
},
{
"role": "Auditor"
}
]
}
],
"timeout": 0
}

View File

@@ -355,7 +355,13 @@ def apply_conditions(query, si, sii, sip, filters, additional_conditions=None):
query = query.where(si.posting_date <= filters.get("to_date"))
if filters.get("mode_of_payment"):
query = query.where(sip.mode_of_payment == filters.get("mode_of_payment"))
subquery = (
frappe.qb.from_(sip)
.select(sip.parent)
.where(sip.mode_of_payment == filters.get("mode_of_payment"))
.groupby(sip.parent)
)
query = query.where(si.name.isin(subquery))
if filters.get("warehouse"):
if frappe.db.get_value("Warehouse", filters.get("warehouse"), "is_group"):
@@ -424,8 +430,6 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
frappe.qb.from_(si)
.join(sii)
.on(si.name == sii.parent)
.left_join(sip)
.on(sip.parent == si.name)
.left_join(item)
.on(sii.item_code == item.name)
.select(
@@ -465,7 +469,6 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
si.update_stock,
sii.uom,
sii.qty,
sip.mode_of_payment,
)
.where(si.docstatus == 1)
.where(sii.parenttype == doctype)

View File

@@ -1906,6 +1906,8 @@ def create_payment_ledger_entry(
def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, party):
from erpnext.accounts.doctype.dunning.dunning import update_linked_dunnings
if not voucher_type or not voucher_no:
return
@@ -1938,6 +1940,7 @@ def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, pa
):
outstanding = voucher_outstanding[0]
ref_doc = frappe.get_doc(voucher_type, voucher_no)
previous_outstanding_amount = ref_doc.outstanding_amount
outstanding_amount = flt(
outstanding["outstanding_in_account_currency"], ref_doc.precision("outstanding_amount")
)
@@ -1951,6 +1954,7 @@ def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, pa
outstanding_amount,
)
update_linked_dunnings(ref_doc, previous_outstanding_amount)
ref_doc.set_status(update=True)
ref_doc.notify_update()

View File

@@ -122,6 +122,7 @@ class Asset(AccountsController):
def validate(self):
self.validate_category()
self.validate_precision()
self.validate_linked_purchase_docs()
self.set_purchase_doc_row_item()
self.validate_asset_values()
self.validate_asset_and_reference()
@@ -409,6 +410,21 @@ class Asset(AccountsController):
if self.available_for_use_date and getdate(self.available_for_use_date) < getdate(self.purchase_date):
frappe.throw(_("Available-for-use Date should be after purchase date"))
def validate_linked_purchase_docs(self):
for doctype_field, doctype_name in [
("purchase_receipt", "Purchase Receipt"),
("purchase_invoice", "Purchase Invoice"),
]:
linked_doc = getattr(self, doctype_field, None)
if linked_doc:
docstatus = frappe.db.get_value(doctype_name, linked_doc, "docstatus")
if docstatus == 0:
frappe.throw(
_("{0} is still in Draft. Please submit it before saving the Asset.").format(
get_link_to_form(doctype_name, linked_doc)
)
)
def validate_gross_and_purchase_amount(self):
if self.is_existing_asset:
return
@@ -1083,7 +1099,7 @@ def get_asset_account(account_name, asset=None, asset_category=None, company=Non
def make_journal_entry(asset_name):
asset = frappe.get_doc("Asset", asset_name)
(
_,
fixed_asset_account,
accumulated_depreciation_account,
depreciation_expense_account,
) = get_depreciation_accounts(asset.asset_category, asset.company)

View File

@@ -463,8 +463,8 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
if (internal) {
let button_label =
me.frm.doc.company === me.frm.doc.represents_company
? "Internal Sales Order"
: "Inter Company Sales Order";
? __("Internal Sales Order")
: __("Inter Company Sales Order");
me.frm.add_custom_button(
button_label,

View File

@@ -41,8 +41,9 @@
"ignore_pricing_rule",
"before_items_section",
"scan_barcode",
"set_from_warehouse",
"last_scanned_warehouse",
"items_col_break",
"set_from_warehouse",
"set_warehouse",
"items_section",
"items",
@@ -1294,6 +1295,13 @@
"hidden": 1,
"label": "Has Unit Price Items",
"no_copy": 1
},
{
"depends_on": "eval: doc.last_scanned_warehouse",
"fieldname": "last_scanned_warehouse",
"fieldtype": "Data",
"is_virtual": 1,
"label": "Last Scanned Warehouse"
}
],
"grid_page_length": 50,
@@ -1301,7 +1309,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2025-04-09 16:54:08.836106",
"modified": "2025-07-31 17:19:40.816883",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",

View File

@@ -724,7 +724,12 @@ def set_missing_values(source, target):
@frappe.whitelist()
def make_purchase_receipt(source_name, target_doc=None):
def make_purchase_receipt(source_name, target_doc=None, args=None):
if args is None:
args = {}
if isinstance(args, str):
args = json.loads(args)
has_unit_price_items = frappe.db.get_value("Purchase Order", source_name, "has_unit_price_items")
def is_unit_price_row(source):
@@ -738,6 +743,11 @@ def make_purchase_receipt(source_name, target_doc=None):
(flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) * flt(source_parent.conversion_rate)
)
def select_item(d):
filtered_items = args.get("filtered_children", [])
child_filter = d.name in filtered_items if filtered_items else True
return child_filter
doc = get_mapped_doc(
"Purchase Order",
source_name,
@@ -765,7 +775,8 @@ def make_purchase_receipt(source_name, target_doc=None):
"condition": lambda doc: (
True if is_unit_price_row(doc) else abs(doc.received_qty) < abs(doc.qty)
)
and doc.delivered_by_supplier != 1,
and doc.delivered_by_supplier != 1
and select_item(doc),
},
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True},
},
@@ -777,8 +788,8 @@ def make_purchase_receipt(source_name, target_doc=None):
@frappe.whitelist()
def make_purchase_invoice(source_name, target_doc=None):
return get_mapped_purchase_invoice(source_name, target_doc)
def make_purchase_invoice(source_name, target_doc=None, args=None):
return get_mapped_purchase_invoice(source_name, target_doc, args=args)
@frappe.whitelist()
@@ -792,7 +803,12 @@ def make_purchase_invoice_from_portal(purchase_order_name):
frappe.response.location = "/purchase-invoices/" + doc.name
def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions=False):
def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions=False, args=None):
if args is None:
args = {}
if isinstance(args, str):
args = json.loads(args)
def postprocess(source, target):
target.flags.ignore_permissions = ignore_permissions
set_missing_values(source, target)
@@ -832,6 +848,11 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
or item_group.get("buying_cost_center")
)
def select_item(d):
filtered_items = args.get("filtered_children", [])
child_filter = d.name in filtered_items if filtered_items else True
return child_filter
fields = {
"Purchase Order": {
"doctype": "Purchase Invoice",
@@ -854,7 +875,8 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
"wip_composite_asset": "wip_composite_asset",
},
"postprocess": update_item,
"condition": lambda doc: (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)),
"condition": lambda doc: (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount))
and select_item(doc),
},
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True},
}

View File

@@ -9,6 +9,7 @@ from frappe import _
from frappe.core.doctype.communication.email import make
from frappe.desk.form.load import get_attachments
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder import Order
from frappe.utils import get_url
from frappe.utils.print_format import download_pdf
from frappe.utils.user import get_user_fullname
@@ -582,35 +583,32 @@ def get_supplier_tag():
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_rfq_containing_supplier(doctype, txt, searchfield, start, page_len, filters):
conditions = ""
if txt:
conditions += "and rfq.name like '%%" + txt + "%%' "
rfq = frappe.qb.DocType("Request for Quotation")
rfq_supplier = frappe.qb.DocType("Request for Quotation Supplier")
if filters.get("transaction_date"):
conditions += "and rfq.transaction_date = '{}'".format(filters.get("transaction_date"))
rfq_data = frappe.db.sql(
f"""
select
distinct rfq.name, rfq.transaction_date,
rfq.company
from
`tabRequest for Quotation` rfq, `tabRequest for Quotation Supplier` rfq_supplier
where
rfq.name = rfq_supplier.parent
and rfq_supplier.supplier = %(supplier)s
and rfq.docstatus = 1
and rfq.company = %(company)s
{conditions}
order by rfq.transaction_date ASC
limit %(page_len)s offset %(start)s """,
{
"page_len": page_len,
"start": start,
"company": filters.get("company"),
"supplier": filters.get("supplier"),
},
as_dict=1,
query = (
frappe.qb.from_(rfq)
.from_(rfq_supplier)
.select(rfq.name)
.distinct()
.select(rfq.transaction_date, rfq.company)
.where(
(rfq.name == rfq_supplier.parent)
& (rfq_supplier.supplier == filters.get("supplier"))
& (rfq.docstatus == 1)
& (rfq.company == filters.get("company"))
)
.orderby(rfq.transaction_date, order=Order.asc)
.limit(page_len)
.offset(start)
)
if txt:
query = query.where(rfq.name.like(f"%%{txt}%%"))
if filters.get("transaction_date"):
query = query.where(rfq.transaction_date == filters.get("transaction_date"))
rfq_data = query.run(as_dict=1)
return rfq_data

View File

@@ -583,21 +583,27 @@ def get_account_list(doctype, txt, searchfield, start, page_len, filters):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_blanket_orders(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql(
"""select distinct bo.name, bo.blanket_order_type, bo.to_date
from `tabBlanket Order` bo, `tabBlanket Order Item` boi
where
boi.parent = bo.name
and boi.item_code = {item_code}
and bo.blanket_order_type = '{blanket_order_type}'
and bo.company = {company}
and bo.docstatus = 1""".format(
item_code=frappe.db.escape(filters.get("item")),
blanket_order_type=filters.get("blanket_order_type"),
company=frappe.db.escape(filters.get("company")),
bo = frappe.qb.DocType("Blanket Order")
bo_item = frappe.qb.DocType("Blanket Order Item")
blanket_orders = (
frappe.qb.from_(bo)
.from_(bo_item)
.select(bo.name)
.distinct()
.select(bo.blanket_order_type, bo.to_date)
.where(
(bo_item.parent == bo.name)
& (bo_item.item_code == filters.get("item"))
& (bo.blanket_order_type == filters.get("blanket_order_type"))
& (bo.company == filters.get("company"))
& (bo.docstatus == 1)
)
.run()
)
return blanket_orders
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
@@ -615,7 +621,7 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters):
if filters.get("company"):
condition += "and tabAccount.company = %(company)s"
condition += f"and tabAccount.disabled = {filters.get('disabled', 0)}"
condition += " and tabAccount.disabled = %(disabled)s"
return frappe.db.sql(
f"""select tabAccount.name from `tabAccount`
@@ -625,7 +631,11 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters):
and tabAccount.`{searchfield}` LIKE %(txt)s
{condition} {get_match_cond(doctype)}
order by idx desc, name""",
{"txt": "%" + txt + "%", "company": filters.get("company", "")},
{
"txt": "%" + txt + "%",
"company": filters.get("company", ""),
"disabled": cint(filters.get("disabled", 0)),
},
)

View File

@@ -1854,6 +1854,7 @@ def make_bundle_for_material_transfer(**kwargs):
row.warehouse = kwargs.warehouse
bundle_doc.set_incoming_rate()
bundle_doc.calculate_qty_and_amount()
bundle_doc.flags.ignore_permissions = True
bundle_doc.flags.ignore_validate = True

View File

@@ -363,7 +363,9 @@ doc_events = {
"erpnext.regional.create_transaction_log",
"erpnext.regional.italy.utils.sales_invoice_on_submit",
],
"on_cancel": ["erpnext.regional.italy.utils.sales_invoice_on_cancel"],
"on_cancel": [
"erpnext.regional.italy.utils.sales_invoice_on_cancel",
],
"on_trash": "erpnext.regional.check_deletion_permission",
},
"Purchase Invoice": {
@@ -375,9 +377,7 @@ doc_events = {
"Payment Entry": {
"on_submit": [
"erpnext.regional.create_transaction_log",
"erpnext.accounts.doctype.dunning.dunning.resolve_dunning",
],
"on_cancel": ["erpnext.accounts.doctype.dunning.dunning.resolve_dunning"],
"on_trash": "erpnext.regional.check_deletion_permission",
},
"Address": {

View File

@@ -420,3 +420,4 @@ erpnext.patches.v15_0.repost_gl_entries_with_no_account_subcontracting #2025-08-
execute:frappe.db.set_single_value("Accounts Settings", "fetch_valuation_rate_for_internal_transaction", 1)
erpnext.patches.v15_0.add_company_payment_gateway_account
erpnext.patches.v15_0.update_uae_zero_rated_fetch
erpnext.patches.v15_0.update_fieldname_in_accounting_dimension_filter

View File

@@ -48,6 +48,7 @@ def execute():
dunning.validate()
dunning.flags.ignore_validate_update_after_submit = True
dunning.flags.ignore_links = True
dunning.save()
# Reverse entries only if dunning is submitted and not resolved

View File

@@ -0,0 +1,36 @@
import frappe
from frappe.query_builder import DocType
def execute():
default_accounting_dimension()
ADF = DocType("Accounting Dimension Filter")
AD = DocType("Accounting Dimension")
accounting_dimension_filter = (
frappe.qb.from_(ADF)
.join(AD)
.on(AD.document_type == ADF.accounting_dimension)
.select(ADF.name, AD.fieldname, ADF.accounting_dimension)
).run(as_dict=True)
for doc in accounting_dimension_filter:
value = doc.fieldname or frappe.scrub(doc.accounting_dimension)
frappe.db.set_value(
"Accounting Dimension Filter",
doc.name,
"fieldname",
value,
update_modified=False,
)
def default_accounting_dimension():
ADF = DocType("Accounting Dimension Filter")
for dim in ("Cost Center", "Project"):
(
frappe.qb.update(ADF)
.set(ADF.fieldname, frappe.scrub(dim))
.where(ADF.accounting_dimension == dim)
.run()
)

View File

@@ -342,12 +342,16 @@ def get_projectwise_timesheet_data(project=None, parent=None, from_time=None, to
@frappe.whitelist()
def get_timesheet_detail_rate(timelog, currency):
timelog_detail = frappe.db.sql(
f"""SELECT tsd.billing_amount as billing_amount,
ts.currency as currency FROM `tabTimesheet Detail` tsd
INNER JOIN `tabTimesheet` ts ON ts.name=tsd.parent
WHERE tsd.name = '{timelog}'""",
as_dict=1,
ts = frappe.qb.DocType("Timesheet")
ts_detail = frappe.qb.DocType("Timesheet Detail")
timelog_detail = (
frappe.qb.from_(ts_detail)
.inner_join(ts)
.on(ts.name == ts_detail.parent)
.select(ts_detail.billing_amount.as_("billing_amount"), ts.currency.as_("currency"))
.where(ts_detail.name == timelog)
.run(as_dict=1)
)[0]
if timelog_detail.currency:

View File

@@ -931,8 +931,11 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
set_default_payment(total_amount_to_pay, update_paid_amount) {
var me = this;
var payment_status = true;
if(this.frm.doc.is_pos && (update_paid_amount===undefined || update_paid_amount)) {
if (
this.frm.doc.is_pos
&& !cint(this.frm.skip_default_payment)
&& (update_paid_amount===undefined || update_paid_amount)
) {
$.each(this.frm.doc['payments'] || [], function(index, data) {
if(data.default && payment_status && total_amount_to_pay > 0) {
let base_amount, amount;

View File

@@ -6,6 +6,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
setup() {
super.setup();
let me = this;
this.barcode_scanner = new erpnext.utils.BarcodeScanner({ frm: this.frm });
this.set_fields_onload_for_line_item();
this.frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"];
@@ -473,8 +474,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
scan_barcode() {
frappe.flags.dialog_set = false;
const barcode_scanner = new erpnext.utils.BarcodeScanner({frm:this.frm});
barcode_scanner.process_scan();
this.barcode_scanner.process_scan();
}
barcode(doc, cdt, cdn) {
@@ -923,8 +923,15 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
return;
}
var party_type = frappe.meta.has_field(me.frm.doc.doctype, "customer") ? "Customer" : "Supplier";
var party_name = me.frm.doc[party_type.toLowerCase()];
var party_type, party_name;
if( me.frm.doc.doctype == "Quotation" && me.frm.doc.quotation_to == "Customer"){
party_type = "Customer",
party_name = me.frm.doc.party_name
}
else{
party_type = frappe.meta.has_field(me.frm.doc.doctype, "customer") ? "Customer" : "Supplier";
party_name = me.frm.doc[party_type.toLowerCase()];
}
if (party_name) {
frappe.call({
method: "frappe.client.get_value",

View File

@@ -12,6 +12,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
this.batch_no_field = opts.batch_no_field || "batch_no";
this.uom_field = opts.uom_field || "uom";
this.qty_field = opts.qty_field || "qty";
this.warehouse_field = opts.warehouse_field || "warehouse";
// field name on row which defines max quantity to be scanned e.g. picklist
this.max_qty_field = opts.max_qty_field;
// scanner won't add a new row if this flag is set.
@@ -20,7 +21,6 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
this.prompt_qty = opts.prompt_qty;
this.items_table_name = opts.items_table_name || "items";
this.items_table = this.frm.doc[this.items_table_name];
// optional sound name to play when scan either fails or passes.
// see https://frappeframework.com/docs/v14/user/en/python-api/hooks#sounds
@@ -34,8 +34,10 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
// batch_no: "LOT12", // present if batch was scanned
// serial_no: "987XYZ", // present if serial no was scanned
// uom: "Kg", // present if barcode UOM is different from default
// warehouse: "Store-001", // present if warehouse was found (location-first scanning)
// }
this.scan_api = opts.scan_api || "erpnext.stock.utils.scan_barcode";
this.has_last_scanned_warehouse = frappe.meta.has_field(this.frm.doctype, "last_scanned_warehouse");
}
process_scan() {
@@ -50,14 +52,31 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
this.scan_api_call(input, (r) => {
const data = r && r.message;
if (!data || Object.keys(data).length === 0) {
this.show_alert(__("Cannot find Item with this Barcode"), "red");
if (
!data ||
Object.keys(data).length === 0 ||
(data.warehouse && !this.has_last_scanned_warehouse)
) {
this.show_alert(
this.has_last_scanned_warehouse
? __("Cannot find Item or Warehouse with this Barcode")
: __("Cannot find Item with this Barcode"),
"red"
);
this.clean_up();
this.play_fail_sound();
reject();
return;
}
// Handle warehouse scanning
if (data.warehouse) {
this.handle_warehouse_scan(data);
this.play_success_sound();
resolve();
return;
}
me.update_table(data)
.then((row) => {
this.play_success_sound();
@@ -77,6 +96,10 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
method: this.scan_api,
args: {
search_value: input,
ctx: {
set_warehouse: this.frm.doc.set_warehouse,
company: this.frm.doc.company,
},
},
})
.then((r) => {
@@ -89,11 +112,14 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
let cur_grid = this.frm.fields_dict[this.items_table_name].grid;
frappe.flags.trigger_from_barcode_scanner = true;
const { item_code, barcode, batch_no, serial_no, uom } = data;
const { item_code, barcode, batch_no, serial_no, uom, default_warehouse } = data;
let row = this.get_row_to_modify_on_scan(item_code, batch_no, uom, barcode);
const warehouse = this.has_last_scanned_warehouse
? this.frm.doc.last_scanned_warehouse || default_warehouse
: null;
this.is_new_row = false;
let row = this.get_row_to_modify_on_scan(item_code, batch_no, uom, barcode, warehouse);
const is_new_row = !row?.item_code;
if (!row) {
if (this.dont_allow_new_row) {
this.show_alert(__("Maximum quantity scanned for item {0}.", [item_code]), "red");
@@ -101,7 +127,6 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
reject();
return;
}
this.is_new_row = true;
// add new row if new item/batch is scanned
row = frappe.model.add_child(this.frm.doc, cur_grid.doctype, this.items_table_name);
@@ -120,12 +145,13 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
() => this.set_selector_trigger_flag(data),
() =>
this.set_item(row, item_code, barcode, batch_no, serial_no).then((qty) => {
this.show_scan_message(row.idx, row.item_code, qty);
this.show_scan_message(row.idx, !is_new_row, qty);
}),
() => this.set_barcode_uom(row, uom),
() => this.set_serial_no(row, serial_no),
() => this.set_batch_no(row, batch_no),
() => this.set_barcode(row, barcode),
() => this.set_warehouse(row, warehouse),
() => this.clean_up(),
() => this.revert_selector_flag(),
() => resolve(row),
@@ -386,9 +412,17 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
}
}
show_scan_message(idx, exist = null, qty = 1) {
async set_warehouse(row, warehouse) {
const warehouse_field = this.get_warehouse_field();
if (warehouse && frappe.meta.has_field(row.doctype, warehouse_field)) {
await frappe.model.set_value(row.doctype, row.name, warehouse_field, warehouse);
}
}
show_scan_message(idx, is_existing_row = false, qty = 1) {
// show new row or qty increase toast
if (exist) {
if (is_existing_row) {
this.show_alert(__("Row #{0}: Qty increased by {1}", [idx, qty]), "green");
} else {
this.show_alert(__("Row #{0}: Item added", [idx]), "green");
@@ -404,13 +438,16 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
return is_duplicate;
}
get_row_to_modify_on_scan(item_code, batch_no, uom, barcode) {
get_row_to_modify_on_scan(item_code, batch_no, uom, barcode, warehouse) {
let cur_grid = this.frm.fields_dict[this.items_table_name].grid;
// Check if batch is scanned and table has batch no field
let is_batch_no_scan = batch_no && frappe.meta.has_field(cur_grid.doctype, this.batch_no_field);
let check_max_qty = this.max_qty_field && frappe.meta.has_field(cur_grid.doctype, this.max_qty_field);
const warehouse_field = this.get_warehouse_field();
let has_warehouse_field = frappe.meta.has_field(cur_grid.doctype, warehouse_field);
const matching_row = (row) => {
const item_match = row.item_code == item_code;
const batch_match = !row[this.batch_no_field] || row[this.batch_no_field] == batch_no;
@@ -418,20 +455,94 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
const qty_in_limit = flt(row[this.qty_field]) < flt(row[this.max_qty_field]);
const item_scanned = row.has_item_scanned;
let warehouse_match = true;
if (has_warehouse_field) {
if (warehouse) {
warehouse_match = row[warehouse_field] === warehouse;
} else {
warehouse_match = !row[warehouse_field];
}
}
return (
item_match &&
uom_match &&
warehouse_match &&
!item_scanned &&
(!is_batch_no_scan || batch_match) &&
(!check_max_qty || qty_in_limit)
);
};
return this.items_table.find(matching_row) || this.get_existing_blank_row();
const items_table = this.frm.doc[this.items_table_name] || [];
return items_table.find(matching_row) || items_table.find((d) => !d.item_code);
}
get_existing_blank_row() {
return this.items_table.find((d) => !d.item_code);
setup_last_scanned_warehouse() {
this.frm.set_df_property("last_scanned_warehouse", "options", "Warehouse");
this.frm.set_df_property("last_scanned_warehouse", "fieldtype", "Link");
this.frm.set_df_property("last_scanned_warehouse", "formatter", function (value, df, options, doc) {
const link_formatter = frappe.form.get_formatter(df.fieldtype);
const link_value = link_formatter(value, df, options, doc);
if (!value) {
return link_value;
}
const clear_btn = `
<a class="btn-clear-last-scanned-warehouse" title="${__("Clear Last Scanned Warehouse")}">
${frappe.utils.icon("close", "xs", "es-icon")}
</a>
`;
return link_value + clear_btn;
});
this.frm.$wrapper.on("click", ".btn-clear-last-scanned-warehouse", (e) => {
e.preventDefault();
e.stopPropagation();
this.clear_warehouse_context();
});
}
handle_warehouse_scan(data) {
const warehouse = data.warehouse;
const warehouse_field = this.get_warehouse_field();
const warehouse_field_label = frappe.meta.get_label(this.items_table_name, warehouse_field);
if (!this.last_scanned_warehouse_initialized) {
this.setup_last_scanned_warehouse();
this.last_scanned_warehouse_initialized = true;
}
this.frm.set_value("last_scanned_warehouse", warehouse);
this.show_alert(
__("{0} will be set as the {1} in subsequently scanned items", [
__(warehouse).bold(),
__(warehouse_field_label).bold(),
]),
"green",
6
);
}
clear_warehouse_context() {
this.frm.set_value("last_scanned_warehouse", null);
this.show_alert(
__(
"The last scanned warehouse has been cleared and won't be set in the subsequently scanned items"
),
"blue",
6
);
}
get_warehouse_field() {
if (typeof this.warehouse_field === "function") {
return this.warehouse_field(this.frm.doc);
}
return this.warehouse_field;
}
play_success_sound() {

View File

@@ -593,3 +593,11 @@ body[data-route="pos"] {
.frappe-control[data-fieldname="other_charges_calculation"] .ql-editor {
white-space: normal;
}
.btn-clear-last-scanned-warehouse {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
z-index: 1;
}

View File

@@ -32,6 +32,7 @@ class ProductBundle(Document):
def validate(self):
self.validate_main_item()
self.validate_child_items()
self.validate_child_items_qty_non_zero()
from erpnext.utilities.transaction_base import validate_uom_is_integer
validate_uom_is_integer(self, "uom", "qty")
@@ -88,6 +89,15 @@ class ProductBundle(Document):
).format(item.idx, frappe.bold(item.item_code))
)
def validate_child_items_qty_non_zero(self):
for item in self.items:
if item.qty <= 0:
frappe.throw(
_(
"Row #{0}: Quantity cannot be a non-positive number. Please increase the quantity or remove the Item {1}"
).format(item.idx, frappe.bold(item.item_code))
)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs

View File

@@ -33,6 +33,7 @@
"ignore_pricing_rule",
"items_section",
"scan_barcode",
"last_scanned_warehouse",
"items",
"sec_break23",
"total_qty",
@@ -1094,13 +1095,20 @@
"hidden": 1,
"label": "Has Unit Price Items",
"no_copy": 1
},
{
"depends_on": "eval: doc.last_scanned_warehouse",
"fieldname": "last_scanned_warehouse",
"fieldtype": "Data",
"is_virtual": 1,
"label": "Last Scanned Warehouse"
}
],
"icon": "fa fa-shopping-cart",
"idx": 82,
"is_submittable": 1,
"links": [],
"modified": "2025-05-27 16:04:39.208077",
"modified": "2025-07-31 17:23:48.875382",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation",
@@ -1199,4 +1207,4 @@
"states": [],
"timeline_field": "party_name",
"title_field": "title"
}
}

View File

@@ -2,6 +2,8 @@
# License: GNU General Public License v3. See license.txt
import json
import frappe
from frappe import _
from frappe.model.mapper import get_mapped_doc
@@ -347,7 +349,7 @@ def get_list_context(context=None):
@frappe.whitelist()
def make_sales_order(source_name: str, target_doc=None):
def make_sales_order(source_name: str, target_doc=None, args=None):
if not frappe.db.get_singles_value(
"Selling Settings", "allow_sales_order_creation_for_expired_quotation"
):
@@ -359,10 +361,15 @@ def make_sales_order(source_name: str, target_doc=None):
):
frappe.throw(_("Validity period of this quotation has ended."))
return _make_sales_order(source_name, target_doc)
return _make_sales_order(source_name, target_doc, args=args)
def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
def _make_sales_order(source_name, target_doc=None, ignore_permissions=False, args=None):
if args is None:
args = {}
if isinstance(args, str):
args = json.loads(args)
customer = _make_customer(source_name, ignore_permissions)
ordered_items = get_ordered_items(source_name)
@@ -430,6 +437,11 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
# Simple row
return True
def select_item(d):
filtered_items = args.get("filtered_children", [])
child_filter = d.name in filtered_items if filtered_items else True
return child_filter
doclist = get_mapped_doc(
"Quotation",
source_name,
@@ -439,7 +451,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
"doctype": "Sales Order Item",
"field_map": {"parent": "prevdoc_docname", "name": "quotation_item"},
"postprocess": update_item,
"condition": can_map_row,
"condition": lambda d: can_map_row(d) and select_item(d),
},
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True},
"Sales Team": {"doctype": "Sales Team", "add_if_empty": True},
@@ -476,11 +488,16 @@ def set_expired_status():
@frappe.whitelist()
def make_sales_invoice(source_name, target_doc=None):
return _make_sales_invoice(source_name, target_doc)
def make_sales_invoice(source_name, target_doc=None, args=None):
return _make_sales_invoice(source_name, target_doc, args=args)
def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, args=None):
if args is None:
args = {}
if isinstance(args, str):
args = json.loads(args)
customer = _make_customer(source_name, ignore_permissions)
def set_missing_values(source, target):
@@ -496,6 +513,11 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
target.cost_center = None
target.stock_qty = flt(obj.qty) * flt(obj.conversion_factor)
def select_item(d):
filtered_items = args.get("filtered_children", [])
child_filter = d.name in filtered_items if filtered_items else True
return child_filter
doclist = get_mapped_doc(
"Quotation",
source_name,
@@ -504,7 +526,7 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
"Quotation Item": {
"doctype": "Sales Invoice Item",
"postprocess": update_item,
"condition": lambda row: not row.is_alternative,
"condition": lambda row: not row.is_alternative and select_item(row),
},
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True},
"Sales Team": {"doctype": "Sales Team", "add_if_empty": True},

View File

@@ -738,8 +738,8 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
if (internal) {
let button_label =
me.frm.doc.company === me.frm.doc.represents_company
? "Internal Purchase Order"
: "Inter Company Purchase Order";
? __("Internal Purchase Order")
: __("Inter Company Purchase Order");
me.frm.add_custom_button(
button_label,
@@ -793,6 +793,9 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
docstatus: 1,
status: ["!=", "Lost"],
},
allow_child_item_selection: true,
child_fieldname: "items",
child_columns: ["item_code", "item_name", "qty", "rate", "amount"],
});
setTimeout(() => {

View File

@@ -41,6 +41,7 @@
"ignore_pricing_rule",
"sec_warehouse",
"scan_barcode",
"last_scanned_warehouse",
"column_break_28",
"set_warehouse",
"reserve_stock",
@@ -1657,6 +1658,13 @@
"hidden": 1,
"label": "Has Unit Price Items",
"no_copy": 1
},
{
"depends_on": "eval: doc.last_scanned_warehouse",
"fieldname": "last_scanned_warehouse",
"fieldtype": "Data",
"is_virtual": 1,
"label": "Last Scanned Warehouse"
}
],
"icon": "fa fa-file-text",

View File

@@ -977,6 +977,11 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None):
def is_unit_price_row(source):
return has_unit_price_items and source.qty == 0
def select_item(d):
filtered_items = kwargs.get("filtered_children", [])
child_filter = d.name in filtered_items if filtered_items else True
return child_filter
def set_missing_values(source, target):
if kwargs.get("ignore_pricing_rule"):
# Skip pricing rule when the dn is creating from the pick list
@@ -1042,7 +1047,7 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None):
"name": "so_detail",
"parent": "against_sales_order",
},
"condition": condition,
"condition": lambda d: condition(d) and select_item(d),
"postprocess": update_item,
}
@@ -1098,7 +1103,12 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None):
@frappe.whitelist()
def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, args=None):
if args is None:
args = {}
if isinstance(args, str):
args = json.loads(args)
# 0 qty is accepted, as the qty is uncertain for some items
has_unit_price_items = frappe.db.get_value("Sales Order", source_name, "has_unit_price_items")
@@ -1158,6 +1168,11 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
if cost_center:
target.cost_center = cost_center
def select_item(d):
filtered_items = args.get("filtered_children", [])
child_filter = d.name in filtered_items if filtered_items else True
return child_filter
doclist = get_mapped_doc(
"Sales Order",
source_name,
@@ -1182,7 +1197,8 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
True
if is_unit_price_row(doc)
else (doc.qty and (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)))
),
)
and select_item(doc),
},
"Sales Taxes and Charges": {
"doctype": "Sales Taxes and Charges",

View File

@@ -342,7 +342,6 @@ erpnext.PointOfSale.Payment = class {
}
render_payment_section() {
this.remove_grand_total_from_default_mop();
this.render_payment_mode_dom();
this.make_invoice_fields_control();
this.update_totals_section();

View File

@@ -480,14 +480,19 @@ def install_defaults(args=None): # nosemgrep
create_bank_account(args)
def set_global_defaults(args):
def set_global_defaults(kwargs):
global_defaults = frappe.get_doc("Global Defaults", "Global Defaults")
company = frappe.db.get_value(
"Company",
{"company_name": kwargs.get("company_name")},
"name",
)
global_defaults.update(
{
"default_currency": args.get("currency"),
"default_company": args.get("company_name"),
"country": args.get("country"),
"default_currency": kwargs.get("currency"),
"default_company": company,
"country": kwargs.get("country"),
}
)

View File

@@ -182,6 +182,9 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends (
company: me.frm.doc.company,
project: me.frm.doc.project || undefined,
},
allow_child_item_selection: true,
child_fieldname: "items",
child_columns: ["item_code", "item_name", "qty", "delivered_qty"],
});
},
__("Get Items From")
@@ -231,6 +234,9 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends (
},
get_query_method: "erpnext.stock.doctype.pick_list.pick_list.get_pick_list_query",
size: "extra-large",
allow_child_item_selection: true,
child_fieldname: "locations",
child_columns: ["item_code", "item_name", "stock_qty", "delivered_qty"],
});
},
__("Get Items From")

View File

@@ -38,6 +38,7 @@
"ignore_pricing_rule",
"items_section",
"scan_barcode",
"last_scanned_warehouse",
"col_break_warehouse",
"set_warehouse",
"set_target_warehouse",
@@ -1390,6 +1391,13 @@
"label": "Company Contact Person",
"options": "Contact",
"print_hide": 1
},
{
"depends_on": "eval: doc.last_scanned_warehouse",
"fieldname": "last_scanned_warehouse",
"fieldtype": "Data",
"is_virtual": 1,
"label": "Last Scanned Warehouse"
}
],
"icon": "fa fa-truck",

View File

@@ -2,6 +2,8 @@
# License: GNU General Public License v3. See license.txt
import json
import frappe
from frappe import _
from frappe.contacts.doctype.address.address import get_company_address
@@ -824,6 +826,11 @@ def get_returned_qty_map(delivery_note):
@frappe.whitelist()
def make_sales_invoice(source_name, target_doc=None, args=None):
if args is None:
args = {}
if isinstance(args, str):
args = json.loads(args)
doc = frappe.get_doc("Delivery Note", source_name)
to_make_invoice_qty_map = {}
@@ -875,6 +882,11 @@ def make_sales_invoice(source_name, target_doc=None, args=None):
return pending_qty
def select_item(d):
filtered_items = args.get("filtered_children", [])
child_filter = d.name in filtered_items if filtered_items else True
return child_filter
doc = get_mapped_doc(
"Delivery Note",
source_name,
@@ -897,6 +909,7 @@ def make_sales_invoice(source_name, target_doc=None, args=None):
"filter": lambda d: get_pending_qty(d) <= 0
if not doc.get("is_return")
else get_pending_qty(d) > 0,
"condition": select_item,
},
"Sales Taxes and Charges": {
"doctype": "Sales Taxes and Charges",

View File

@@ -674,13 +674,13 @@ def prepare_data_for_internal_transfer():
company = "_Test Company with perpetual inventory"
customer = create_internal_customer(
"_Test Internal Customer 3",
"_Test Internal Customer 2",
company,
company,
)
supplier = create_internal_supplier(
"_Test Internal Supplier 3",
"_Test Internal Supplier 2",
company,
company,
)

View File

@@ -20,9 +20,9 @@
"amended_from",
"warehouse_section",
"scan_barcode",
"column_break_13",
"set_from_warehouse",
"last_scanned_warehouse",
"column_break5",
"set_from_warehouse",
"set_warehouse",
"items_section",
"items",
@@ -350,22 +350,25 @@
"fieldname": "column_break_35",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_13",
"fieldtype": "Column Break"
},
{
"fieldname": "buying_price_list",
"fieldtype": "Link",
"label": "Price List",
"options": "Price List"
},
{
"depends_on": "eval: doc.last_scanned_warehouse",
"fieldname": "last_scanned_warehouse",
"fieldtype": "Data",
"is_virtual": 1,
"label": "Last Scanned Warehouse"
}
],
"icon": "fa fa-ticket",
"idx": 70,
"is_submittable": 1,
"links": [],
"modified": "2025-07-28 15:13:49.000037",
"modified": "2025-07-31 17:19:01.166208",
"modified_by": "Administrator",
"module": "Stock",
"name": "Material Request",

View File

@@ -11,6 +11,7 @@ import frappe
import frappe.defaults
from frappe import _, msgprint
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder import Order
from frappe.query_builder.functions import Sum
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, new_line_sep, nowdate
@@ -555,39 +556,44 @@ def get_items_based_on_default_supplier(supplier):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_material_requests_based_on_supplier(doctype, txt, searchfield, start, page_len, filters):
conditions = ""
if txt:
conditions += "and mr.name like '%%" + txt + "%%' "
if filters.get("transaction_date"):
date = filters.get("transaction_date")[1]
conditions += f"and mr.transaction_date between '{date[0]}' and '{date[1]}' "
supplier = filters.get("supplier")
supplier_items = get_items_based_on_default_supplier(supplier)
if not supplier_items:
frappe.throw(_("{0} is not the default supplier for any items.").format(supplier))
material_requests = frappe.db.sql(
"""select distinct mr.name, transaction_date,company
from `tabMaterial Request` mr, `tabMaterial Request Item` mr_item
where mr.name = mr_item.parent
and mr_item.item_code in ({})
and mr.material_request_type = 'Purchase'
and mr.per_ordered < 99.99
and mr.docstatus = 1
and mr.status != 'Stopped'
and mr.company = %s
{}
order by mr_item.item_code ASC
limit {} offset {} """.format(
", ".join(["%s"] * len(supplier_items)), conditions, cint(page_len), cint(start)
),
(*tuple(supplier_items), filters.get("company")),
as_dict=1,
mr = frappe.qb.DocType("Material Request")
mr_item = frappe.qb.DocType("Material Request Item")
query = (
frappe.qb.from_(mr)
.from_(mr_item)
.select(mr.name)
.distinct()
.select(mr.transaction_date, mr.company)
.where(
(mr.name == mr_item.parent)
& (mr_item.item_code.isin(supplier_items))
& (mr.material_request_type == "Purchase")
& (mr.per_ordered < 99.99)
& (mr.docstatus == 1)
& (mr.status != "Stopped")
& (mr.company == filters.get("company"))
)
.orderby(mr_item.item_code, order=Order.asc)
.limit(cint(page_len))
.offset(cint(start))
)
if txt:
query = query.where(mr.name.like(f"%%{txt}%%"))
if filters.get("transaction_date"):
date = filters.get("transaction_date")[1]
query = query.where(mr.transaction_date[date[0] : date[1]])
material_requests = query.run(as_dict=True)
return material_requests

View File

@@ -1252,11 +1252,16 @@ def create_dn_wo_so(pick_list, delivery_note=None):
@frappe.whitelist()
def create_dn_for_pick_lists(source_name, target_doc=None, kwargs=None):
"""Get Items from Multiple Pick Lists and create a Delivery Note for filtered customer"""
if kwargs is None:
kwargs = {}
if isinstance(kwargs, str):
kwargs = json.loads(kwargs)
pick_list = frappe.get_doc("Pick List", source_name)
validate_item_locations(pick_list)
sales_order_arg = kwargs.get("sales_order") if kwargs else None
customer_arg = kwargs.get("customer") if kwargs else None
sales_order_arg = kwargs.get("sales_order")
customer_arg = kwargs.get("customer")
if sales_order_arg:
sales_orders = {sales_order_arg}
@@ -1270,7 +1275,7 @@ def create_dn_for_pick_lists(source_name, target_doc=None, kwargs=None):
pluck="name",
)
delivery_note = create_dn_from_so(pick_list, sales_orders, delivery_note=target_doc)
delivery_note = create_dn_from_so(pick_list, sales_orders, delivery_note=target_doc, kwargs=kwargs)
if not sales_order_arg and not all(item.sales_order for item in pick_list.locations):
if isinstance(delivery_note, str):
@@ -1296,10 +1301,15 @@ def create_dn_with_so(sales_dict, pick_list):
return delivery_note
def create_dn_from_so(pick_list, sales_order_list, delivery_note=None):
def create_dn_from_so(pick_list, sales_order_list, delivery_note=None, kwargs=None):
if not sales_order_list:
return delivery_note
def select_item(d):
filtered_items = kwargs.get("filtered_children", [])
child_filter = d.name in filtered_items if filtered_items else True
return child_filter
item_table_mapper = {
"doctype": "Delivery Note Item",
"field_map": {
@@ -1307,7 +1317,9 @@ def create_dn_from_so(pick_list, sales_order_list, delivery_note=None):
"name": "so_detail",
"parent": "against_sales_order",
},
"condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier != 1,
"condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty)
and doc.delivered_by_supplier != 1
and select_item(doc),
}
kwargs = {"skip_item_mapping": True, "ignore_pricing_rule": pick_list.ignore_pricing_rule}

View File

@@ -150,7 +150,11 @@ frappe.ui.form.on("Purchase Receipt", {
docstatus: 1,
per_received: ["<", 100],
company: frm.doc.company,
update_stock: 0,
},
allow_child_item_selection: true,
child_fieldname: "items",
child_columns: ["item_code", "item_name", "qty", "received_qty"],
});
},
__("Get Items From")
@@ -255,6 +259,9 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend
per_received: ["<", 99.99],
company: me.frm.doc.company,
},
allow_child_item_selection: true,
child_fieldname: "items",
child_columns: ["item_code", "item_name", "qty", "received_qty"],
});
},
__("Get Items From")

View File

@@ -40,6 +40,7 @@
"ignore_pricing_rule",
"sec_warehouse",
"scan_barcode",
"last_scanned_warehouse",
"column_break_31",
"set_warehouse",
"set_from_warehouse",
@@ -1285,6 +1286,13 @@
"label": "Dispatch Address",
"print_hide": 1,
"read_only": 1
},
{
"depends_on": "eval: doc.last_scanned_warehouse",
"fieldname": "last_scanned_warehouse",
"fieldtype": "Data",
"is_virtual": 1,
"label": "Last Scanned Warehouse"
}
],
"grid_page_length": 50,

View File

@@ -2,6 +2,8 @@
# License: GNU General Public License v3. See license.txt
import json
import frappe
from frappe import _, throw
from frappe.desk.notifications import clear_doctype_notifications
@@ -1225,6 +1227,11 @@ def get_item_wise_returned_qty(pr_doc):
@frappe.whitelist()
def make_purchase_invoice(source_name, target_doc=None, args=None):
if args is None:
args = {}
if isinstance(args, str):
args = json.loads(args)
from erpnext.accounts.party import get_payment_terms_template
doc = frappe.get_doc("Purchase Receipt", source_name)
@@ -1279,6 +1286,11 @@ def make_purchase_invoice(source_name, target_doc=None, args=None):
return pending_qty, returned_qty
def select_item(d):
filtered_items = args.get("filtered_children", [])
child_filter = d.name in filtered_items if filtered_items else True
return child_filter
doclist = get_mapped_doc(
"Purchase Receipt",
source_name,
@@ -1308,9 +1320,10 @@ def make_purchase_invoice(source_name, target_doc=None, args=None):
"wip_composite_asset": "wip_composite_asset",
},
"postprocess": update_item,
"filter": lambda d: get_pending_qty(d)[0] <= 0
if not doc.get("is_return")
else get_pending_qty(d)[0] > 0,
"filter": lambda d: (
get_pending_qty(d)[0] <= 0 if not doc.get("is_return") else get_pending_qty(d)[0] > 0
)
and select_item(d),
},
"Purchase Taxes and Charges": {
"doctype": "Purchase Taxes and Charges",

View File

@@ -131,7 +131,12 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None):
at_capacity, rules = get_ordered_putaway_rules(item_code, company, source_warehouse=source_warehouse)
if not rules:
warehouse = source_warehouse or item.get("warehouse")
warehouse = (
(source_warehouse or item.get("warehouse"))
if not item.get("t_warehouse")
else item.get("t_warehouse")
)
if at_capacity:
# rules available, but no free space
items_not_accomodated.append([item_code, pending_qty])

View File

@@ -1003,6 +1003,13 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
setup() {
var me = this;
this.barcode_scanner = new erpnext.utils.BarcodeScanner({
frm: this.frm,
warehouse_field: (doc) => {
return doc.purpose === "Material Transfer" ? "t_warehouse" : "s_warehouse";
},
});
this.setup_posting_date_time_check();
this.frm.fields_dict.bom_no.get_query = function () {
@@ -1130,8 +1137,7 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
scan_barcode() {
frappe.flags.dialog_set = false;
const barcode_scanner = new erpnext.utils.BarcodeScanner({ frm: this.frm });
barcode_scanner.process_scan();
this.barcode_scanner.process_scan();
}
on_submit() {

View File

@@ -50,6 +50,7 @@
"target_address_display",
"sb0",
"scan_barcode",
"last_scanned_warehouse",
"items_section",
"items",
"get_stock_and_rate",
@@ -691,6 +692,13 @@
"fieldtype": "Tab Break",
"label": "Connections",
"show_dashboard": 1
},
{
"depends_on": "eval: doc.last_scanned_warehouse",
"fieldname": "last_scanned_warehouse",
"fieldtype": "Data",
"is_virtual": 1,
"label": "Last Scanned Warehouse"
}
],
"icon": "fa fa-file-text",

View File

@@ -2020,6 +2020,70 @@ class TestStockEntry(FrappeTestCase):
self.assertEqual(se.items[0].basic_rate, 300)
def test_batch_item_additional_cost_for_material_transfer_entry(self):
item_code = "_Test Batch Item Additional Cost MTE"
make_item(
item_code,
{
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_naming_series": "BT-MTE.#####",
},
)
se = make_stock_entry(
item_code=item_code,
target="_Test Warehouse - _TC",
qty=2,
basic_rate=100,
use_serial_batch_fields=1,
)
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
se = make_stock_entry(
item_code=item_code,
source="_Test Warehouse - _TC",
target="_Test Warehouse 1 - _TC",
batch_no=batch_no,
use_serial_batch_fields=1,
qty=2,
purpose="Material Transfer",
do_not_save=True,
)
se.append(
"additional_costs",
{
"cost_center": "Main - _TC",
"amount": 50,
"expense_account": "Stock Adjustment - _TC",
"description": "Test Additional Cost",
},
)
se.save()
self.assertEqual(se.additional_costs[0].amount, 50)
self.assertEqual(se.items[0].basic_rate, 100)
self.assertEqual(se.items[0].valuation_rate, 125)
se.submit()
self.assertEqual(se.items[0].basic_rate, 100)
self.assertEqual(se.items[0].valuation_rate, 125)
incoming_rate = frappe.db.get_value(
"Stock Ledger Entry",
{
"item_code": item_code,
"warehouse": "_Test Warehouse 1 - _TC",
"voucher_type": "Stock Entry",
"voucher_no": se.name,
},
"incoming_rate",
)
self.assertEqual(incoming_rate, 125.0)
def make_serialized_item(**args):
args = frappe._dict(args)

View File

@@ -7,6 +7,7 @@ frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on("Stock Reconciliation", {
setup(frm) {
frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"];
frm.barcode_scanner = new erpnext.utils.BarcodeScanner({ frm });
},
onload: function (frm) {
@@ -96,8 +97,7 @@ frappe.ui.form.on("Stock Reconciliation", {
},
scan_barcode: function (frm) {
const barcode_scanner = new erpnext.utils.BarcodeScanner({ frm: frm });
barcode_scanner.process_scan();
frm.barcode_scanner.process_scan();
},
scan_mode: function (frm) {

View File

@@ -18,6 +18,7 @@
"set_warehouse",
"section_break_22",
"scan_barcode",
"last_scanned_warehouse",
"column_break_12",
"scan_mode",
"sb9",
@@ -178,6 +179,13 @@
{
"fieldname": "column_break_12",
"fieldtype": "Column Break"
},
{
"depends_on": "eval: doc.last_scanned_warehouse",
"fieldname": "last_scanned_warehouse",
"fieldtype": "Data",
"is_virtual": 1,
"label": "Last Scanned Warehouse"
}
],
"icon": "fa fa-upload-alt",

View File

@@ -185,7 +185,21 @@ class SerialBatchBundle:
}
if self.sle.actual_qty < 0 and self.is_material_transfer():
values_to_update["valuation_rate"] = flt(sn_doc.avg_rate)
basic_rate = flt(sn_doc.avg_rate)
ste_detail = frappe.db.get_value(
"Stock Entry Detail",
self.sle.voucher_detail_no,
["additional_cost", "transfer_qty"],
as_dict=True,
)
additional_cost = 0.0
if ste_detail:
additional_cost = flt(ste_detail.additional_cost) / flt(ste_detail.transfer_qty)
values_to_update["basic_rate"] = basic_rate
values_to_update["valuation_rate"] = basic_rate + additional_cost
if not frappe.db.get_single_value(
"Stock Settings", "do_not_update_serial_batch_on_creation_of_auto_bundle"

View File

@@ -81,3 +81,44 @@ class TestStockUtilities(FrappeTestCase, StockTestMixin):
self.assertEqual(serial_scan["serial_no"], serial.name)
self.assertEqual(serial_scan["has_batch_no"], 0)
self.assertEqual(serial_scan["has_serial_no"], 1)
def test_barcode_scanning_of_warehouse(self):
warehouse = frappe.get_doc(
{
"doctype": "Warehouse",
"warehouse_name": "Test Warehouse for Barcode",
"company": "_Test Company",
}
).insert()
warehouse_2 = frappe.get_doc(
{
"doctype": "Warehouse",
"warehouse_name": "Test Warehouse for Barcode 2",
"company": "_Test Company",
}
).insert()
warehouse_scan = scan_barcode(warehouse.name)
self.assertEqual(warehouse_scan["warehouse"], warehouse.name)
item_with_warehouse = self.make_item(
properties={
"item_defaults": [{"company": "_Test Company", "default_warehouse": warehouse.name}],
"barcodes": [{"barcode": "w12345"}],
}
)
item_scan = scan_barcode("w12345")
self.assertEqual(item_scan["item_code"], item_with_warehouse.name)
self.assertEqual(item_scan.get("default_warehouse"), None)
ctx = {"company": "_Test Company"}
item_scan_with_ctx = scan_barcode("w12345", ctx=ctx)
self.assertEqual(item_scan_with_ctx["item_code"], item_with_warehouse.name)
self.assertEqual(item_scan_with_ctx["default_warehouse"], warehouse.name)
ctx = {"company": "_Test Company", "set_warehouse": warehouse_2.name}
item_scan_with_ctx = scan_barcode("w12345", ctx=ctx)
self.assertEqual(item_scan_with_ctx["item_code"], item_with_warehouse.name)
self.assertEqual(item_scan_with_ctx["default_warehouse"], warehouse_2.name)

View File

@@ -126,8 +126,9 @@ def get_stock_balance(
extra_cond = ""
if inventory_dimensions_dict:
for field, value in inventory_dimensions_dict.items():
column = frappe.utils.sanitize_column(field)
args[field] = value
extra_cond += f" and {field} = %({field})s"
extra_cond += f" and {column} = %({field})s"
last_entry = get_previous_sle(args, extra_cond=extra_cond)
@@ -584,13 +585,24 @@ def check_pending_reposting(posting_date: str, throw_error: bool = True) -> bool
@frappe.whitelist()
def scan_barcode(search_value: str) -> BarcodeScanResult:
def scan_barcode(search_value: str, ctx: dict | str | None = None) -> BarcodeScanResult:
def set_cache(data: BarcodeScanResult):
frappe.cache().set_value(f"erpnext:barcode_scan:{search_value}", data, expires_in_sec=120)
_update_item_info(data, ctx)
def get_cache() -> BarcodeScanResult | None:
if data := frappe.cache().get_value(f"erpnext:barcode_scan:{search_value}"):
return data
data = frappe.cache().get_value(f"erpnext:barcode_scan:{search_value}")
if not data:
return
_update_item_info(data, ctx)
return data
if ctx is None:
ctx = frappe._dict()
else:
ctx = frappe.parse_json(ctx)
if scan_data := get_cache():
return scan_data
@@ -603,7 +615,6 @@ def scan_barcode(search_value: str) -> BarcodeScanResult:
as_dict=True,
)
if barcode_data:
_update_item_info(barcode_data)
set_cache(barcode_data)
return barcode_data
@@ -615,7 +626,6 @@ def scan_barcode(search_value: str) -> BarcodeScanResult:
as_dict=True,
)
if serial_no_data:
_update_item_info(serial_no_data)
set_cache(serial_no_data)
return serial_no_data
@@ -634,22 +644,36 @@ def scan_barcode(search_value: str) -> BarcodeScanResult:
).format(search_value, batch_no_data.item_code)
)
_update_item_info(batch_no_data)
set_cache(batch_no_data)
return batch_no_data
warehouse = frappe.get_cached_value("Warehouse", search_value, ("name", "disabled"), as_dict=True)
if warehouse and not warehouse.disabled:
warehouse_data = {"warehouse": warehouse.name}
set_cache(warehouse_data)
return warehouse_data
return {}
def _update_item_info(scan_result: dict[str, str | None]) -> dict[str, str | None]:
if item_code := scan_result.get("item_code"):
if item_info := frappe.get_cached_value(
"Item",
item_code,
["has_batch_no", "has_serial_no"],
as_dict=True,
):
scan_result.update(item_info)
def _update_item_info(scan_result: dict[str, str | None], ctx: dict | None = None) -> dict[str, str | None]:
from erpnext.stock.get_item_details import get_item_warehouse
item_code = scan_result.get("item_code")
if not item_code:
return scan_result
if item_info := frappe.get_cached_value(
"Item",
item_code,
("has_batch_no", "has_serial_no"),
as_dict=True,
):
scan_result.update(item_info)
if ctx and (warehouse := get_item_warehouse(frappe._dict(name=item_code), ctx, overwrite_warehouse=True)):
scan_result["default_warehouse"] = warehouse
return scan_result

View File

@@ -152,6 +152,7 @@
"width": "100px"
},
{
"default": "Now",
"description": "Time at which materials were received",
"fieldname": "posting_time",
"fieldtype": "Time",