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

chore: release v15
This commit is contained in:
ruthra kumar
2025-04-29 18:40:46 +05:30
committed by GitHub
46 changed files with 606 additions and 134 deletions

View File

@@ -250,11 +250,20 @@ class JournalEntry(AccountsController):
def validate_inter_company_accounts(self):
if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
doc = frappe.get_doc("Journal Entry", self.inter_company_journal_entry_reference)
doc = frappe.db.get_value(
"Journal Entry",
self.inter_company_journal_entry_reference,
["company", "total_debit", "total_credit"],
as_dict=True,
)
account_currency = frappe.get_cached_value("Company", self.company, "default_currency")
previous_account_currency = frappe.get_cached_value("Company", doc.company, "default_currency")
if account_currency == previous_account_currency:
if self.total_credit != doc.total_debit or self.total_debit != doc.total_credit:
credit_precision = self.precision("total_credit")
debit_precision = self.precision("total_debit")
if (flt(self.total_credit, credit_precision) != flt(doc.total_debit, debit_precision)) or (
flt(self.total_debit, debit_precision) != flt(doc.total_credit, credit_precision)
):
frappe.throw(_("Total Credit/ Debit Amount should be same as linked Journal Entry"))
def validate_depr_entry_voucher_type(self):

View File

@@ -2941,6 +2941,8 @@ def get_payment_entry(
party_account_currency if payment_type == "Receive" else bank.account_currency
)
pe.paid_to_account_currency = party_account_currency if payment_type == "Pay" else bank.account_currency
pe.paid_from_account_type = frappe.db.get_value("Account", pe.paid_from, "account_type")
pe.paid_to_account_type = frappe.db.get_value("Account", pe.paid_to, "account_type")
pe.paid_amount = paid_amount
pe.received_amount = received_amount
pe.letter_head = doc.get("letter_head")

View File

@@ -670,7 +670,12 @@ def get_amount(ref_doc, payment_account=None):
dt = ref_doc.doctype
if dt in ["Sales Order", "Purchase Order"]:
grand_total = (flt(ref_doc.rounded_total) or flt(ref_doc.grand_total)) - ref_doc.advance_paid
advance_amount = flt(ref_doc.advance_paid)
if ref_doc.party_account_currency != ref_doc.currency:
advance_amount = flt(flt(ref_doc.advance_paid) / ref_doc.conversion_rate)
grand_total = (flt(ref_doc.rounded_total) or flt(ref_doc.grand_total)) - advance_amount
elif dt in ["Sales Invoice", "Purchase Invoice"]:
if (
dt == "Sales Invoice"

View File

@@ -2,6 +2,7 @@
# For license information, please see license.txt
import hashlib
import json
import frappe
@@ -302,10 +303,17 @@ class POSInvoiceMergeLog(Document):
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
accounting_dimensions_fields = [d.fieldname for d in accounting_dimensions]
dimension_values = frappe.db.get_value(
"POS Profile", {"name": invoice.pos_profile}, accounting_dimensions_fields, as_dict=1
"POS Profile",
{"name": invoice.pos_profile},
[*accounting_dimensions_fields, "cost_center", "project"],
as_dict=1,
)
for dimension in accounting_dimensions:
dimension_value = dimension_values.get(dimension.fieldname)
dimension_value = (
data[0].get(dimension.fieldname)
if data[0].get(dimension.fieldname)
else dimension_values.get(dimension.fieldname)
)
if not dimension_value and (dimension.mandatory_for_pl or dimension.mandatory_for_bs):
frappe.throw(
@@ -317,6 +325,14 @@ class POSInvoiceMergeLog(Document):
invoice.set(dimension.fieldname, dimension_value)
invoice.set(
"cost_center",
data[0].get("cost_center") if data[0].get("cost_center") else dimension_values.get("cost_center"),
)
invoice.set(
"project", data[0].get("project") if data[0].get("project") else dimension_values.get("project")
)
if self.merge_invoices_based_on == "Customer Group":
invoice.flags.ignore_pos_profile = True
invoice.pos_profile = ""
@@ -336,7 +352,7 @@ class POSInvoiceMergeLog(Document):
for doc in invoice_docs:
doc.load_from_db()
inv = sales_invoice
if doc.is_return:
if doc.is_return and credit_notes:
for key, value in credit_notes.items():
if doc.name in value:
inv = key
@@ -446,9 +462,34 @@ def get_invoice_customer_map(pos_invoices):
pos_invoice_customer_map.setdefault(customer, [])
pos_invoice_customer_map[customer].append(invoice)
for customer, invoices in pos_invoice_customer_map.items():
pos_invoice_customer_map[customer] = split_invoices_by_accounting_dimension(invoices)
return pos_invoice_customer_map
def split_invoices_by_accounting_dimension(pos_invoices):
# pos_invoices = {
# {'dim_field1': 'dim_field1_value1', 'dim_field2': 'dim_field2_value1'}: [],
# {'dim_field1': 'dim_field1_value2', 'dim_field2': 'dim_field2_value1'}: []
# }
pos_invoice_accounting_dimensions_map = {}
for invoice in pos_invoices:
dimension_fields = [d.fieldname for d in get_checks_for_pl_and_bs_accounts()]
accounting_dimensions = frappe.db.get_value(
"POS Invoice", invoice.pos_invoice, [*dimension_fields, "cost_center", "project"], as_dict=1
)
accounting_dimensions_dic_hash = hashlib.sha256(
json.dumps(accounting_dimensions).encode()
).hexdigest()
pos_invoice_accounting_dimensions_map.setdefault(accounting_dimensions_dic_hash, [])
pos_invoice_accounting_dimensions_map[accounting_dimensions_dic_hash].append(invoice)
return pos_invoice_accounting_dimensions_map
def consolidate_pos_invoices(pos_invoices=None, closing_entry=None):
invoices = pos_invoices or (closing_entry and closing_entry.get("pos_transactions"))
if frappe.flags.in_test and not invoices:
@@ -532,20 +573,21 @@ def split_invoices(invoices):
def create_merge_logs(invoice_by_customer, closing_entry=None):
try:
for customer, invoices in invoice_by_customer.items():
for _invoices in split_invoices(invoices):
merge_log = frappe.new_doc("POS Invoice Merge Log")
merge_log.posting_date = (
getdate(closing_entry.get("posting_date")) if closing_entry else nowdate()
)
merge_log.posting_time = (
get_time(closing_entry.get("posting_time")) if closing_entry else nowtime()
)
merge_log.customer = customer
merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None
merge_log.set("pos_invoices", _invoices)
merge_log.save(ignore_permissions=True)
merge_log.submit()
for customer, invoices_acc_dim in invoice_by_customer.items():
for invoices in invoices_acc_dim.values():
for _invoices in split_invoices(invoices):
merge_log = frappe.new_doc("POS Invoice Merge Log")
merge_log.posting_date = (
getdate(closing_entry.get("posting_date")) if closing_entry else nowdate()
)
merge_log.posting_time = (
get_time(closing_entry.get("posting_time")) if closing_entry else nowtime()
)
merge_log.customer = customer
merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None
merge_log.set("pos_invoices", _invoices)
merge_log.save(ignore_permissions=True)
merge_log.submit()
if closing_entry:
closing_entry.set_status(update=True, status="Submitted")
closing_entry.db_set("error_message", "")

View File

@@ -455,3 +455,58 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`")
def test_separate_consolidated_invoice_for_different_accounting_dimensions(self):
"""
Creating 3 POS Invoices where first POS Invoice has different Cost Center than the other two.
Consolidate the Invoices.
Check whether the first POS Invoice is consolidated with a separate Sales Invoice than the other two.
Check whether the second and third POS Invoice are consolidated with the same Sales Invoice.
"""
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
frappe.db.sql("delete from `tabPOS Invoice`")
create_cost_center(cost_center_name="_Test POS Cost Center 1", is_group=0)
create_cost_center(cost_center_name="_Test POS Cost Center 2", is_group=0)
try:
test_user, pos_profile = init_user_and_profile()
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300})
pos_inv.cost_center = "_Test POS Cost Center 1 - _TC"
pos_inv.save()
pos_inv.submit()
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
pos_inv.cost_center = "_Test POS Cost Center 2 - _TC"
pos_inv2.save()
pos_inv2.submit()
pos_inv3 = create_pos_invoice(rate=2300, do_not_submit=1)
pos_inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300})
pos_inv.cost_center = "_Test POS Cost Center 2 - _TC"
pos_inv3.save()
pos_inv3.submit()
consolidate_pos_invoices()
pos_inv.load_from_db()
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice))
pos_inv2.load_from_db()
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv2.consolidated_invoice))
self.assertFalse(pos_inv.consolidated_invoice == pos_inv3.consolidated_invoice)
pos_inv3.load_from_db()
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice))
self.assertTrue(pos_inv2.consolidated_invoice == pos_inv3.consolidated_invoice)
finally:
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`")

View File

@@ -143,8 +143,10 @@
"contact_mobile",
"contact_email",
"company_shipping_address_section",
"shipping_address",
"dispatch_address",
"dispatch_address_display",
"column_break_126",
"shipping_address",
"shipping_address_display",
"company_billing_address_section",
"billing_address",
@@ -1546,7 +1548,7 @@
{
"fieldname": "company_shipping_address_section",
"fieldtype": "Section Break",
"label": "Company Shipping Address"
"label": "Shipping Address"
},
{
"fieldname": "column_break_126",
@@ -1627,13 +1629,28 @@
"fieldname": "update_outstanding_for_self",
"fieldtype": "Check",
"label": "Update Outstanding for Self"
},
{
"fieldname": "dispatch_address_display",
"fieldtype": "Text Editor",
"label": "Dispatch Address",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "dispatch_address",
"fieldtype": "Link",
"label": "Select Dispatch Address ",
"options": "Address",
"print_hide": 1
}
],
"grid_page_length": 50,
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2025-01-14 11:39:04.564610",
"modified": "2025-04-09 16:49:22.175081",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
@@ -1688,6 +1705,7 @@
"write": 1
}
],
"row_format": "Dynamic",
"search_fields": "posting_date, supplier, bill_no, base_grand_total, outstanding_amount",
"show_name_in_global_search": 1,
"sort_field": "modified",

View File

@@ -117,6 +117,8 @@ class PurchaseInvoice(BuyingController):
currency: DF.Link | None
disable_rounded_total: DF.Check
discount_amount: DF.Currency
dispatch_address: DF.Link | None
dispatch_address_display: DF.TextEditor | None
due_date: DF.Date | None
from_date: DF.Date | None
grand_total: DF.Currency

View File

@@ -1348,7 +1348,7 @@ class SalesInvoice(SellingController):
)
for item in self.get("items"):
if flt(item.base_net_amount, item.precision("base_net_amount")):
if flt(item.base_net_amount, item.precision("base_net_amount")) or item.is_fixed_asset:
# Do not book income for transfer within same company
if self.is_internal_transfer():
continue
@@ -2298,7 +2298,10 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
# Invert Addresses
update_address(target_doc, "supplier_address", "address_display", source_doc.company_address)
update_address(
target_doc, "shipping_address", "shipping_address_display", source_doc.customer_address
target_doc, "dispatch_address", "dispatch_address_display", source_doc.dispatch_address_name
)
update_address(
target_doc, "shipping_address", "shipping_address_display", source_doc.shipping_address_name
)
update_address(
target_doc, "billing_address", "billing_address_display", source_doc.customer_address

View File

@@ -71,6 +71,7 @@ def get_party_details(
party_address=None,
company_address=None,
shipping_address=None,
dispatch_address=None,
pos_profile=None,
):
if not party:
@@ -92,6 +93,7 @@ def get_party_details(
party_address,
company_address,
shipping_address,
dispatch_address,
pos_profile,
)
@@ -111,6 +113,7 @@ def _get_party_details(
party_address=None,
company_address=None,
shipping_address=None,
dispatch_address=None,
pos_profile=None,
):
party_details = frappe._dict(
@@ -134,6 +137,7 @@ def _get_party_details(
party_address,
company_address,
shipping_address,
dispatch_address,
ignore_permissions=ignore_permissions,
)
set_contact_details(party_details, party, party_type)
@@ -191,34 +195,51 @@ def set_address_details(
party_address=None,
company_address=None,
shipping_address=None,
dispatch_address=None,
*,
ignore_permissions=False,
):
billing_address_field = (
# party_billing
party_billing_field = (
"customer_address" if party_type in ["Lead", "Prospect"] else party_type.lower() + "_address"
)
party_details[billing_address_field] = party_address or get_default_address(party_type, party.name)
party_details[party_billing_field] = party_address or get_default_address(party_type, party.name)
if doctype:
party_details.update(
get_fetch_values(doctype, billing_address_field, party_details[billing_address_field])
get_fetch_values(doctype, party_billing_field, party_details[party_billing_field])
)
# address display
party_details.address_display = render_address(
party_details[billing_address_field], check_permissions=not ignore_permissions
)
# shipping address
if party_type in ["Customer", "Lead"]:
party_details.shipping_address_name = shipping_address or get_party_shipping_address(
party_type, party.name
)
party_details.shipping_address = render_address(
party_details["shipping_address_name"], check_permissions=not ignore_permissions
)
if doctype:
party_details.update(
get_fetch_values(doctype, "shipping_address_name", party_details.shipping_address_name)
)
party_details.address_display = render_address(
party_details[party_billing_field], check_permissions=not ignore_permissions
)
# party_shipping
if party_type in ["Customer", "Lead"]:
party_shipping_field = "shipping_address_name"
party_shipping_display = "shipping_address"
default_shipping = shipping_address
else:
# Supplier
party_shipping_field = "dispatch_address"
party_shipping_display = "dispatch_address_display"
default_shipping = dispatch_address
party_details[party_shipping_field] = default_shipping or get_party_shipping_address(
party_type, party.name
)
party_details[party_shipping_display] = render_address(
party_details[party_shipping_field], check_permissions=not ignore_permissions
)
if doctype:
party_details.update(
get_fetch_values(doctype, party_shipping_field, party_details[party_shipping_field])
)
# company_address
if company_address:
party_details.company_address = company_address
else:
@@ -256,22 +277,20 @@ def set_address_details(
**get_fetch_values(doctype, "shipping_address", party_details.billing_address),
)
party_address, shipping_address = (
party_details.get(billing_address_field),
party_details.shipping_address_name,
party_billing, party_shipping = (
party_details.get(party_billing_field),
party_details.get(party_shipping_field),
)
party_details["tax_category"] = get_address_tax_category(
party.get("tax_category"),
party_address,
shipping_address if party_type != "Supplier" else party_address,
party.get("tax_category"), party_billing, party_shipping
)
if doctype in TRANSACTION_TYPES:
with temporary_flag("company", company):
get_regional_address_details(party_details, doctype, company)
return party_address, shipping_address
return party_billing, party_shipping
@erpnext.allow_regional

View File

@@ -658,10 +658,6 @@ frappe.ui.form.on("Asset", {
} else {
frm.set_value("purchase_invoice_item", data.purchase_invoice_item);
}
let is_editable = !data.is_multiple_items; // if multiple items, then fields should be read-only
frm.set_df_property("gross_purchase_amount", "read_only", is_editable);
frm.set_df_property("asset_quantity", "read_only", is_editable);
}
},
});

View File

@@ -512,6 +512,7 @@
"fieldname": "total_asset_cost",
"fieldtype": "Currency",
"label": "Total Asset Cost",
"no_copy": 1,
"options": "Company:company:default_currency",
"read_only": 1
},
@@ -520,6 +521,7 @@
"fieldname": "additional_asset_cost",
"fieldtype": "Currency",
"label": "Additional Asset Cost",
"no_copy": 1,
"options": "Company:company:default_currency",
"read_only": 1
},
@@ -593,7 +595,7 @@
"link_fieldname": "target_asset"
}
],
"modified": "2025-04-15 16:33:17.189524",
"modified": "2025-04-24 15:31:47.373274",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",

View File

@@ -1169,7 +1169,6 @@ def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype):
frappe.throw(_(f"Selected {doctype} does not contain the Item Code {item_code}"))
first_item = matching_items[0]
is_multiple_items = len(matching_items) > 1
return {
"company": purchase_doc.company,
@@ -1178,7 +1177,6 @@ def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype):
"asset_quantity": first_item.qty,
"cost_center": first_item.cost_center or purchase_doc.get("cost_center"),
"asset_location": first_item.get("asset_location"),
"is_multiple_items": is_multiple_items,
"purchase_receipt_item": first_item.name if doctype == "Purchase Receipt" else None,
"purchase_invoice_item": first_item.name if doctype == "Purchase Invoice" else None,
}

View File

@@ -152,6 +152,9 @@ class AssetMovement(Document):
""",
args,
)
self.validate_movement_cancellation(d, latest_movement_entry)
if latest_movement_entry:
current_location = latest_movement_entry[0][0]
current_employee = latest_movement_entry[0][1]
@@ -179,3 +182,12 @@ class AssetMovement(Document):
d.asset,
_("Asset issued to Employee {0}").format(get_link_to_form("Employee", current_employee)),
)
def validate_movement_cancellation(self, row, latest_movement_entry):
asset_doc = frappe.get_doc("Asset", row.asset)
if not latest_movement_entry and asset_doc.docstatus == 1:
frappe.throw(
_(
"Asset {0} has only one movement record. Please create another movement before deleting this one to maintain asset tracking."
).format(row.asset)
)

View File

@@ -147,6 +147,45 @@ class TestAssetMovement(unittest.TestCase):
movement1.cancel()
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location")
def test_last_movement_cancellation_validation(self):
pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location")
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name")
asset = frappe.get_doc("Asset", asset_name)
asset.calculate_depreciation = 1
asset.available_for_use_date = "2020-06-06"
asset.purchase_date = "2020-06-06"
asset.append(
"finance_books",
{
"expected_value_after_useful_life": 10000,
"next_depreciation_date": "2020-12-31",
"depreciation_method": "Straight Line",
"total_number_of_depreciations": 3,
"frequency_of_depreciation": 10,
},
)
if asset.docstatus == 0:
asset.submit()
AssetMovement = frappe.qb.DocType("Asset Movement")
AssetMovementItem = frappe.qb.DocType("Asset Movement Item")
asset_movement = (
frappe.qb.from_(AssetMovement)
.join(AssetMovementItem)
.on(AssetMovementItem.parent == AssetMovement.name)
.select(AssetMovement.name)
.where(
(AssetMovementItem.asset == asset.name)
& (AssetMovement.company == asset.company)
& (AssetMovement.docstatus == 1)
)
).run(as_dict=True)
asset_movement_doc = frappe.get_doc("Asset Movement", asset_movement[0].name)
self.assertRaises(frappe.ValidationError, asset_movement_doc.cancel)
def create_asset_movement(**args):
args = frappe._dict(args)

View File

@@ -98,9 +98,11 @@ class AssetRepair(AccountsController):
self.increase_asset_value()
total_repair_cost = self.get_total_value_of_stock_consumed()
if self.capitalize_repair_cost:
self.asset_doc.total_asset_cost += self.repair_cost
self.asset_doc.additional_asset_cost += self.repair_cost
total_repair_cost += self.repair_cost
self.asset_doc.total_asset_cost += total_repair_cost
self.asset_doc.additional_asset_cost += total_repair_cost
if self.get("stock_consumption"):
self.check_for_stock_items_and_warehouse()
@@ -139,9 +141,11 @@ class AssetRepair(AccountsController):
self.decrease_asset_value()
total_repair_cost = self.get_total_value_of_stock_consumed()
if self.capitalize_repair_cost:
self.asset_doc.total_asset_cost -= self.repair_cost
self.asset_doc.additional_asset_cost -= self.repair_cost
total_repair_cost += self.repair_cost
self.asset_doc.total_asset_cost -= total_repair_cost
self.asset_doc.additional_asset_cost -= total_repair_cost
if self.get("capitalize_repair_cost"):
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")

View File

@@ -109,8 +109,10 @@
"contact_mobile",
"contact_email",
"shipping_address_section",
"shipping_address",
"dispatch_address",
"dispatch_address_display",
"column_break_99",
"shipping_address",
"shipping_address_display",
"company_billing_address_section",
"billing_address",
@@ -1269,13 +1271,28 @@
"fieldtype": "Tab Break",
"label": "Connections",
"show_dashboard": 1
},
{
"fieldname": "dispatch_address",
"fieldtype": "Link",
"label": "Dispatch Address",
"options": "Address",
"print_hide": 1
},
{
"fieldname": "dispatch_address_display",
"fieldtype": "Text Editor",
"label": "Dispatch Address Details",
"print_hide": 1,
"read_only": 1
}
],
"grid_page_length": 50,
"icon": "fa fa-file-text",
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2024-03-20 16:03:31.611808",
"modified": "2025-04-09 16:54:08.836106",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",
@@ -1322,6 +1339,7 @@
"write": 1
}
],
"row_format": "Dynamic",
"search_fields": "status, transaction_date, supplier, grand_total",
"show_name_in_global_search": 1,
"sort_field": "modified",

View File

@@ -92,6 +92,8 @@ class PurchaseOrder(BuyingController):
customer_name: DF.Data | None
disable_rounded_total: DF.Check
discount_amount: DF.Currency
dispatch_address: DF.Link | None
dispatch_address_display: DF.TextEditor | None
from_date: DF.Date | None
grand_total: DF.Currency
group_same_items: DF.Check

View File

@@ -234,6 +234,7 @@ def make_purchase_order(source_name, target_doc=None):
{
"Supplier Quotation": {
"doctype": "Purchase Order",
"field_no_map": ["transaction_date"],
"validation": {
"docstatus": ["=", 1],
},

View File

@@ -4,6 +4,7 @@
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, today
class TestPurchaseOrder(FrappeTestCase):
@@ -25,7 +26,7 @@ class TestPurchaseOrder(FrappeTestCase):
for doc in po.get("items"):
if doc.get("item_code"):
doc.set("schedule_date", "2013-04-12")
doc.set("schedule_date", add_days(today(), 1))
po.insert()

View File

@@ -83,19 +83,11 @@ def prepare_data(supplier_quotation_data, filters):
supplier_qty_price_map = {}
group_by_field = "supplier_name" if filters.get("group_by") == "Group by Supplier" else "item_code"
company_currency = frappe.db.get_default("currency")
float_precision = cint(frappe.db.get_default("float_precision")) or 2
for data in supplier_quotation_data:
group = data.get(group_by_field) # get item or supplier value for this row
supplier_currency = frappe.db.get_value("Supplier", data.get("supplier_name"), "default_currency")
if supplier_currency:
exchange_rate = get_exchange_rate(supplier_currency, company_currency)
else:
exchange_rate = 1
row = {
"item_code": ""
if group_by_field == "item_code"
@@ -103,7 +95,7 @@ def prepare_data(supplier_quotation_data, filters):
"supplier_name": "" if group_by_field == "supplier_name" else data.get("supplier_name"),
"quotation": data.get("parent"),
"qty": data.get("qty"),
"price": flt(data.get("amount") * exchange_rate, float_precision),
"price": flt(data.get("amount"), float_precision),
"uom": data.get("uom"),
"price_list_currency": data.get("price_list_currency"),
"currency": data.get("currency"),
@@ -209,6 +201,13 @@ def get_columns(filters):
columns = [
{"fieldname": "uom", "label": _("UOM"), "fieldtype": "Link", "options": "UOM", "width": 90},
{"fieldname": "qty", "label": _("Quantity"), "fieldtype": "Float", "width": 80},
{
"fieldname": "stock_uom",
"label": _("Stock UOM"),
"fieldtype": "Link",
"options": "UOM",
"width": 90,
},
{
"fieldname": "currency",
"label": _("Currency"),
@@ -223,13 +222,6 @@ def get_columns(filters):
"options": "currency",
"width": 110,
},
{
"fieldname": "stock_uom",
"label": _("Stock UOM"),
"fieldtype": "Link",
"options": "UOM",
"width": 90,
},
{
"fieldname": "price_per_unit",
"label": _("Price per Unit (Stock UOM)"),

View File

@@ -98,7 +98,29 @@ class BuyingController(SubcontractingController):
item.from_warehouse,
type_of_transaction="Outward",
do_not_submit=True,
qty=item.qty,
)
elif (
not self.is_new()
and item.serial_and_batch_bundle
and next(
(
old_item
for old_item in self.get_doc_before_save().items
if old_item.name == item.name and old_item.qty != item.qty
),
None,
)
and len(
sabe := frappe.get_all(
"Serial and Batch Entry",
filters={"parent": item.serial_and_batch_bundle, "serial_no": ["is", "not set"]},
pluck="name",
)
)
== 1
):
frappe.set_value("Serial and Batch Entry", sabe[0], "qty", item.qty)
def set_rate_for_standalone_debit_note(self):
if self.get("is_return") and self.get("update_stock") and not self.return_against:
@@ -141,6 +163,7 @@ class BuyingController(SubcontractingController):
company=self.company,
party_address=self.get("supplier_address"),
shipping_address=self.get("shipping_address"),
dispatch_address=self.get("dispatch_address"),
company_address=self.get("billing_address"),
fetch_payment_terms_template=not self.get("ignore_default_payment_terms_template"),
ignore_permissions=self.flags.ignore_permissions,
@@ -238,6 +261,7 @@ class BuyingController(SubcontractingController):
address_dict = {
"supplier_address": "address_display",
"shipping_address": "shipping_address_display",
"dispatch_address": "dispatch_address_display",
"billing_address": "billing_address_display",
}

View File

@@ -347,6 +347,16 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
"Company", company, "default_warehouse_for_sales_return"
)
if doctype == "Sales Invoice":
inv_is_consolidated, inv_is_pos = frappe.db.get_value(
"Sales Invoice", source_name, ["is_consolidated", "is_pos"]
)
if inv_is_consolidated and inv_is_pos:
frappe.throw(
_("Cannot create return for consolidated invoice {0}.").format(source_name),
title=_("Cannot Create Return"),
)
def set_missing_values(source, target):
doc = frappe.get_doc(target)
doc.is_return = 1

View File

@@ -811,7 +811,7 @@ class StockController(AccountsController):
)
def make_package_for_transfer(
self, serial_and_batch_bundle, warehouse, type_of_transaction=None, do_not_submit=None
self, serial_and_batch_bundle, warehouse, type_of_transaction=None, do_not_submit=None, qty=0
):
return make_bundle_for_material_transfer(
is_new=self.is_new(),
@@ -822,6 +822,7 @@ class StockController(AccountsController):
warehouse=warehouse,
type_of_transaction=type_of_transaction,
do_not_submit=do_not_submit,
qty=qty,
)
def get_sl_entries(self, d, args):
@@ -1047,6 +1048,16 @@ class StockController(AccountsController):
def validate_qi_presence(self, row):
"""Check if QI is present on row level. Warn on save and stop on submit if missing."""
if self.doctype in [
"Purchase Receipt",
"Purchase Invoice",
"Sales Invoice",
"Delivery Note",
] and frappe.db.get_single_value(
"Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery"
):
return
if not row.quality_inspection:
msg = _("Row #{0}: Quality Inspection is required for Item {1}").format(
row.idx, frappe.bold(row.item_code)
@@ -1805,15 +1816,20 @@ def make_bundle_for_material_transfer(**kwargs):
kwargs.type_of_transaction = "Inward"
bundle_doc = frappe.copy_doc(bundle_doc)
bundle_doc.docstatus = 0
bundle_doc.warehouse = kwargs.warehouse
bundle_doc.type_of_transaction = kwargs.type_of_transaction
bundle_doc.voucher_type = kwargs.voucher_type
bundle_doc.voucher_no = "" if kwargs.is_new or kwargs.docstatus == 2 else kwargs.voucher_no
bundle_doc.is_cancelled = 0
qty = 0
if len(bundle_doc.entries) == 1 and kwargs.qty < bundle_doc.total_qty and not bundle_doc.has_serial_no:
qty = kwargs.qty
for row in bundle_doc.entries:
row.is_outward = 0
row.qty = abs(row.qty)
row.qty = abs(qty or row.qty)
row.stock_value_difference = abs(row.stock_value_difference)
if kwargs.type_of_transaction == "Outward":
row.qty *= -1

View File

@@ -705,6 +705,12 @@ class JobCard(Document):
bold("Job Card"), get_link_to_form("Job Card", self.name)
)
)
else:
for row in self.time_logs:
if not row.from_time or not row.to_time:
frappe.throw(
_("Row #{0}: From Time and To Time fields are required").format(row.idx),
)
precision = self.precision("total_completed_qty")
total_completed_qty = flt(

View File

@@ -1768,6 +1768,7 @@ def get_sub_assembly_items(
continue
else:
stock_qty = stock_qty - _bin_dict.projected_qty
sub_assembly_items.append(d.item_code)
elif warehouse:
bin_details.setdefault(d.item_code, get_bin_details(d, company, for_warehouse=warehouse))

View File

@@ -296,6 +296,7 @@ frappe.ui.form.on("Timesheet Detail", {
hours: function (frm, cdt, cdn) {
calculate_end_time(frm, cdt, cdn);
update_billing_hours(frm, cdt, cdn);
calculate_billing_costing_amount(frm, cdt, cdn);
calculate_time_and_amount(frm);
},

View File

@@ -54,6 +54,12 @@ erpnext.buying = {
return erpnext.queries.company_address_query(this.frm.doc)
});
}
if(this.frm.get_field('dispatch_address')) {
this.frm.set_query("dispatch_address", () => {
return erpnext.queries.address_query(this.frm.doc);
});
}
}
setup_queries(doc, cdt, cdn) {
@@ -295,6 +301,12 @@ erpnext.buying = {
"shipping_address_display", true);
}
dispatch_address(){
var me = this;
erpnext.utils.get_address_display(this.frm, "dispatch_address",
"dispatch_address_display", true);
}
billing_address() {
erpnext.utils.get_address_display(this.frm, "billing_address",
"billing_address_display", true);

View File

@@ -792,6 +792,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
return;
}
if (item.serial_no) {
item.use_serial_batch_fields = 1
}
if (item && item.serial_no) {
if (!item.item_code) {
this.frm.trigger("item_code", cdt, cdn);
@@ -1355,13 +1359,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
}
batch_no(doc, cdt, cdn) {
let item = frappe.get_doc(cdt, cdn);
if (!this.is_a_mapped_document(item)) {
this.apply_price_list(item, true);
}
}
toggle_conversion_factor(item) {
// toggle read only property for conversion factor field if the uom and stock uom are same
if(this.frm.get_field('items').grid.fields_map.conversion_factor) {
@@ -1587,7 +1584,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
batch_no(frm, cdt, cdn) {
let row = locals[cdt][cdn];
if (row.use_serial_batch_fields && row.batch_no) {
if (row.batch_no) {
row.use_serial_batch_fields = 1
}
if (row.batch_no) {
var params = this._get_args(row);
params.batch_no = row.batch_no;
params.uom = row.uom;

View File

@@ -71,6 +71,10 @@ erpnext.utils.get_party_details = function (frm, method, args, callback) {
if (!args.shipping_address && frm.doc.shipping_address) {
args.shipping_address = frm.doc.shipping_address;
}
if (!args.dispatch_address && frm.doc.dispatch_address) {
args.dispatch_address = frm.doc.dispatch_address;
}
}
if (frappe.meta.get_docfield(frm.doc.doctype, "taxes")) {

View File

@@ -85,9 +85,10 @@ class Employee(NestedSet):
self.reset_employee_emails_cache()
def update_user_permissions(self):
if not has_permission("User Permission", ptype="write") or (
not self.has_value_changed("user_id") and not self.has_value_changed("create_user_permission")
):
if not self.has_value_changed("user_id") and not self.has_value_changed("create_user_permission"):
return
if not has_permission("User Permission", ptype="write", raise_exception=False):
return
employee_user_permission_exists = frappe.db.exists(

View File

@@ -266,8 +266,6 @@ def install(country=None):
{"doctype": "Issue Priority", "name": _("Low")},
{"doctype": "Issue Priority", "name": _("Medium")},
{"doctype": "Issue Priority", "name": _("High")},
{"doctype": "Email Account", "email_id": "sales@example.com", "append_to": "Opportunity"},
{"doctype": "Email Account", "email_id": "support@example.com", "append_to": "Issue"},
{"doctype": "Party Type", "party_type": "Customer", "account_type": "Receivable"},
{"doctype": "Party Type", "party_type": "Supplier", "account_type": "Payable"},
{"doctype": "Party Type", "party_type": "Employee", "account_type": "Payable"},

View File

@@ -1163,7 +1163,10 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
# Invert the address on target doc creation
update_address(target_doc, "supplier_address", "address_display", source_doc.company_address)
update_address(
target_doc, "shipping_address", "shipping_address_display", source_doc.customer_address
target_doc, "dispatch_address", "dispatch_address_display", source_doc.dispatch_address_name
)
update_address(
target_doc, "shipping_address", "shipping_address_display", source_doc.shipping_address_name
)
update_address(
target_doc, "billing_address", "billing_address_display", source_doc.customer_address

View File

@@ -970,6 +970,11 @@ class Item(Document):
changed_fields = [
field for field in restricted_fields if cstr(self.get(field)) != cstr(values.get(field))
]
# Allow to change valuation method from FIFO to Moving Average not vice versa
if self.valuation_method == "Moving Average" and "valuation_method" in changed_fields:
changed_fields.remove("valuation_method")
if not changed_fields:
return

View File

@@ -7,7 +7,7 @@ from frappe import _
from frappe.model.document import Document
from frappe.model.meta import get_field_precision
from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt
from frappe.utils import cint, flt
import erpnext
from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals
@@ -268,14 +268,24 @@ class LandedCostVoucher(Document):
)
docs = frappe.db.get_all(
"Asset",
filters={receipt_document_type: item.receipt_document, "item_code": item.item_code},
fields=["name", "docstatus"],
filters={
receipt_document_type: item.receipt_document,
"item_code": item.item_code,
"docstatus": ["!=", 2],
},
fields=["name", "docstatus", "asset_quantity"],
)
if not docs or len(docs) < item.qty:
total_asset_qty = sum((cint(d.asset_quantity)) for d in docs)
if not docs or total_asset_qty < item.qty:
frappe.throw(
_(
"There are only {0} asset created or linked to {1}. Please create or link {2} Assets with respective document."
).format(len(docs), item.receipt_document, item.qty)
"For item <b>{0}</b>, only <b>{1}</b> asset have been created or linked to <b>{2}</b>. "
"Please create or link <b>{3}</b> more asset with the respective document."
).format(
item.item_code, total_asset_qty, item.receipt_document, item.qty - total_asset_qty
)
)
if docs:
for d in docs:

View File

@@ -112,8 +112,10 @@
"contact_mobile",
"contact_email",
"section_break_98",
"shipping_address",
"dispatch_address",
"dispatch_address_display",
"column_break_100",
"shipping_address",
"shipping_address_display",
"billing_address_section",
"billing_address",
@@ -1198,7 +1200,7 @@
{
"fieldname": "section_break_98",
"fieldtype": "Section Break",
"label": "Company Shipping Address"
"label": "Shipping Address"
},
{
"fieldname": "billing_address_section",
@@ -1267,13 +1269,28 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "dispatch_address",
"fieldtype": "Link",
"label": "Dispatch Address Template",
"options": "Address",
"print_hide": 1
},
{
"fieldname": "dispatch_address_display",
"fieldtype": "Text Editor",
"label": "Dispatch Address",
"print_hide": 1,
"read_only": 1
}
],
"grid_page_length": 50,
"icon": "fa fa-truck",
"idx": 261,
"is_submittable": 1,
"links": [],
"modified": "2024-11-13 16:55:14.129055",
"modified": "2025-04-09 16:52:19.323878",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt",
@@ -1334,6 +1351,7 @@
"write": 1
}
],
"row_format": "Dynamic",
"search_fields": "status, posting_date, supplier",
"show_name_in_global_search": 1,
"sort_field": "modified",

View File

@@ -69,6 +69,8 @@ class PurchaseReceipt(BuyingController):
currency: DF.Link
disable_rounded_total: DF.Check
discount_amount: DF.Currency
dispatch_address: DF.Link | None
dispatch_address_display: DF.TextEditor | None
grand_total: DF.Currency
group_same_items: DF.Check
ignore_pricing_rule: DF.Check

View File

@@ -7,6 +7,8 @@ from frappe.utils import add_days, cint, cstr, flt, get_datetime, getdate, nowti
from pypika import functions as fn
import erpnext
import erpnext.controllers
import erpnext.controllers.status_updater
from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
from erpnext.controllers.buying_controller import QtyMismatchError
@@ -4129,6 +4131,59 @@ class TestPurchaseReceipt(FrappeTestCase):
self.assertTrue(sles)
def test_internal_pr_qty_change_only_single_batch(self):
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
prepare_data_for_internal_transfer()
def get_sabb_qty(sabb):
return frappe.get_value("Serial and Batch Bundle", sabb, "total_qty")
item = make_item("Item with only Batch", {"has_batch_no": 1})
item.create_new_batch = 1
item.save()
make_purchase_receipt(
item_code=item.item_code,
qty=10,
rate=100,
company="_Test Company with perpetual inventory",
warehouse="Stores - TCP1",
)
dn = create_delivery_note(
item_code=item.item_code,
qty=10,
rate=100,
company="_Test Company with perpetual inventory",
customer="_Test Internal Customer 2",
cost_center="Main - TCP1",
warehouse="Stores - TCP1",
target_warehouse="Work In Progress - TCP1",
)
pr = make_inter_company_purchase_receipt(dn.name)
pr.items[0].warehouse = "Stores - TCP1"
pr.items[0].qty = 8
pr.save()
# Test 1 - Check if SABB qty is changed on first save
self.assertEqual(abs(get_sabb_qty(pr.items[0].serial_and_batch_bundle)), 8)
pr.items[0].qty = 6
pr.items[0].received_qty = 6
pr.save()
# Test 2 - Check if SABB qty is changed when saved again
self.assertEqual(abs(get_sabb_qty(pr.items[0].serial_and_batch_bundle)), 6)
pr.items[0].qty = 12
pr.items[0].received_qty = 12
# Test 3 - OverAllowanceError should be thrown as qty is greater than qty in DN
self.assertRaises(erpnext.controllers.status_updater.OverAllowanceError, pr.submit)
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

@@ -203,10 +203,11 @@ class QualityInspection(Document):
self.get_item_specification_details()
def on_update(self):
if (
frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_not_submitted")
== "Warn"
):
action_if_qi_in_draft = frappe.db.get_single_value(
"Stock Settings", "action_if_quality_inspection_is_not_submitted"
)
if not action_if_qi_in_draft or action_if_qi_in_draft == "Warn":
self.update_qc_reference()
def on_submit(self):

View File

@@ -162,7 +162,7 @@ frappe.ui.form.on("Shipment", {
args: { contact: contact_name },
callback: function (r) {
if (r.message) {
if (!(r.message.contact_email && (r.message.contact_phone || r.message.contact_mobile))) {
if (!(r.message.contact_email || r.message.contact_phone || r.message.contact_mobile)) {
if (contact_type == "Delivery") {
frm.set_value("delivery_contact_name", "");
frm.set_value("delivery_contact", "");

View File

@@ -950,6 +950,15 @@ frappe.ui.form.on("Stock Entry Detail", {
},
batch_no(frm, cdt, cdn) {
let row = locals[cdt][cdn];
if (row.batch_no) {
frappe.model.set_value(cdt, cdn, {
use_serial_batch_fields: 1,
serial_and_batch_bundle: "",
});
}
validate_sample_quantity(frm, cdt, cdn);
},
@@ -1074,6 +1083,13 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
serial_no(doc, cdt, cdn) {
var item = frappe.get_doc(cdt, cdn);
if (item.serial_no) {
frappe.model.set_value(cdt, cdn, {
use_serial_batch_fields: 1,
serial_and_batch_bundle: "",
});
}
if (item?.serial_no) {
// Replace all occurences of comma with line feed
item.serial_no = item.serial_no.replace(/,/g, "\n");

View File

@@ -289,8 +289,16 @@ frappe.ui.form.on("Stock Reconciliation Item", {
frm.events.set_valuation_rate_and_qty(frm, cdt, cdn);
},
batch_no: function (frm, cdt, cdn) {
frm.events.set_valuation_rate_and_qty(frm, cdt, cdn);
batch_no(frm, cdt, cdn) {
let row = locals[cdt][cdn];
if (row.batch_no) {
frappe.model.set_value(cdt, cdn, {
use_serial_batch_fields: 1,
serial_and_batch_bundle: "",
});
frm.events.set_valuation_rate_and_qty(frm, cdt, cdn);
}
},
qty: function (frm, cdt, cdn) {
@@ -310,6 +318,11 @@ frappe.ui.form.on("Stock Reconciliation Item", {
var child = locals[cdt][cdn];
if (child.serial_no) {
frappe.model.set_value(cdt, cdn, {
use_serial_batch_fields: 1,
serial_and_batch_bundle: "",
});
const serial_nos = child.serial_no.trim().split("\n");
frappe.model.set_value(cdt, cdn, "qty", serial_nos.length);
}

View File

@@ -726,6 +726,12 @@ class StockReconciliation(StockController):
)
self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock)
elif self.docstatus == 1:
frappe.throw(
_(
"No stock ledger entries were created. Please set the quantity or valuation rate for the items properly and try again."
)
)
def make_adjustment_entry(self, row, sl_entries):
from erpnext.stock.stock_ledger import get_stock_value_difference

View File

@@ -10,14 +10,14 @@
"is_standard": "Yes",
"json": "{\"add_total_row\": 0, \"sort_by\": \"Serial No.modified\", \"sort_order\": \"desc\", \"sort_by_next\": null, \"filters\": [[\"Serial No\", \"warehouse\", \"=\", \"\"]], \"sort_order_next\": \"desc\", \"columns\": [[\"name\", \"Serial No\"], [\"item_code\", \"Serial No\"], [\"amc_expiry_date\", \"Serial No\"], [\"maintenance_status\", \"Serial No\"],[\"item_name\", \"Serial No\"], [\"description\", \"Serial No\"], [\"item_group\", \"Serial No\"], [\"brand\", \"Serial No\"]]}",
"letterhead": null,
"modified": "2024-09-26 13:07:23.451182",
"modified": "2025-04-24 13:07:23.451182",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial No Service Contract Expiry",
"name": "Serial No Warranty Expiry",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Serial No",
"report_name": "Serial No Service Contract Expiry",
"report_name": "Serial No Warranty Expiry",
"report_type": "Report Builder",
"roles": [
{

View File

@@ -31,8 +31,8 @@ rfq = class rfq {
var me = this;
$('.rfq-items').on("change", ".rfq-qty", function(){
me.idx = parseFloat($(this).attr('data-idx'));
me.qty = parseFloat($(this).val()) || 0;
me.rate = parseFloat($(repl('.rfq-rate[data-idx=%(idx)s]',{'idx': me.idx})).val());
me.qty = parseFloat(flt($(this).val())) || 0;
me.rate = parseFloat(flt($(repl('.rfq-rate[data-idx=%(idx)s]',{'idx': me.idx})).val()));
me.update_qty_rate();
$(this).val(format_number(me.qty, doc.number_format, 2));
})
@@ -42,8 +42,8 @@ rfq = class rfq {
var me = this;
$(".rfq-items").on("change", ".rfq-rate", function(){
me.idx = parseFloat($(this).attr('data-idx'));
me.rate = parseFloat($(this).val()) || 0;
me.qty = parseFloat($(repl('.rfq-qty[data-idx=%(idx)s]',{'idx': me.idx})).val());
me.rate = parseFloat(flt($(this).val())) || 0;
me.qty = parseFloat(flt($(repl('.rfq-qty[data-idx=%(idx)s]',{'idx': me.idx})).val()));
me.update_qty_rate();
$(this).val(format_number(me.rate, doc.number_format, 2));
})

View File

@@ -18,29 +18,70 @@ frappe.ui.form.on("Rename Tool", {
allowed_file_types: [".csv"],
},
};
if (!frm.doc.file_to_rename) {
frm.get_field("rename_log").$wrapper.html("");
}
frm.trigger("render_overview");
frm.page.set_primary_action(__("Rename"), function () {
frm.get_field("rename_log").$wrapper.html("<p>Renaming...</p>");
frappe.call({
method: "erpnext.utilities.doctype.rename_tool.rename_tool.upload",
args: {
select_doctype: frm.doc.select_doctype,
},
callback: function (r) {
let html = r.message.join("<br>");
freeze: true,
freeze_message: __("Scheduling..."),
callback: function () {
frappe.msgprint({
message: __("Rename jobs for doctype {0} have been enqueued.", [
frm.doc.select_doctype,
]),
alert: true,
indicator: "green",
});
frm.set_value("select_doctype", "");
frm.set_value("file_to_rename", "");
if (r.exc) {
r.exc = frappe.utils.parse_json(r.exc);
if (Array.isArray(r.exc)) {
html += "<br>" + r.exc.join("<br>");
}
}
frm.trigger("render_overview");
},
error: function (r) {
frappe.msgprint({
message: __("Rename jobs for doctype {0} have not been enqueued.", [
frm.doc.select_doctype,
]),
alert: true,
indicator: "red",
});
frm.get_field("rename_log").$wrapper.html(html);
frm.trigger("render_overview");
},
});
});
},
render_overview: function (frm) {
frappe.db
.get_list("RQ Job", { filters: { status: ["in", ["started", "queued", "finished", "failed"]] } })
.then((jobs) => {
let counts = {
started: 0,
queued: 0,
finished: 0,
failed: 0,
};
for (const job of jobs) {
if (job.job_name !== "frappe.model.rename_doc.bulk_rename") {
continue;
}
counts[job.status]++;
}
frm.get_field("rename_log").$wrapper.html(`
<p><strong>${__("Bulk Rename Jobs")}</a></strong></p>
<p><a href="/app/rq-job?queue=long&status=queued">${__("Queued")}: ${counts.queued}</a></p>
<p><a href="/app/rq-job?queue=long&status=started">${__("Started")}: ${counts.started}</a></p>
<p><a href="/app/rq-job?queue=long&status=finished">${__("Finished")}: ${counts.finished}</a></p>
<p><a href="/app/rq-job?queue=long&status=failed">${__("Failed")}: ${counts.failed}</a></p>
`);
});
},
});

View File

@@ -45,4 +45,11 @@ def upload(select_doctype=None, rows=None):
rows = read_csv_content_from_attached_file(frappe.get_doc("Rename Tool", "Rename Tool"))
return bulk_rename(select_doctype, rows=rows)
# bulk rename allows only 500 rows at a time, so we created one job per 500 rows
for i in range(0, len(rows), 500):
frappe.enqueue(
method=bulk_rename,
queue="long",
doctype=select_doctype,
rows=rows[i : i + 500],
)