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): def validate_inter_company_accounts(self):
if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference: 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") account_currency = frappe.get_cached_value("Company", self.company, "default_currency")
previous_account_currency = frappe.get_cached_value("Company", doc.company, "default_currency") previous_account_currency = frappe.get_cached_value("Company", doc.company, "default_currency")
if account_currency == previous_account_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")) frappe.throw(_("Total Credit/ Debit Amount should be same as linked Journal Entry"))
def validate_depr_entry_voucher_type(self): 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 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_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.paid_amount = paid_amount
pe.received_amount = received_amount pe.received_amount = received_amount
pe.letter_head = doc.get("letter_head") 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 dt = ref_doc.doctype
if dt in ["Sales Order", "Purchase Order"]: 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"]: elif dt in ["Sales Invoice", "Purchase Invoice"]:
if ( if (
dt == "Sales Invoice" dt == "Sales Invoice"

View File

@@ -2,6 +2,7 @@
# For license information, please see license.txt # For license information, please see license.txt
import hashlib
import json import json
import frappe import frappe
@@ -302,10 +303,17 @@ class POSInvoiceMergeLog(Document):
accounting_dimensions = get_checks_for_pl_and_bs_accounts() accounting_dimensions = get_checks_for_pl_and_bs_accounts()
accounting_dimensions_fields = [d.fieldname for d in accounting_dimensions] accounting_dimensions_fields = [d.fieldname for d in accounting_dimensions]
dimension_values = frappe.db.get_value( 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: 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): if not dimension_value and (dimension.mandatory_for_pl or dimension.mandatory_for_bs):
frappe.throw( frappe.throw(
@@ -317,6 +325,14 @@ class POSInvoiceMergeLog(Document):
invoice.set(dimension.fieldname, dimension_value) 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": if self.merge_invoices_based_on == "Customer Group":
invoice.flags.ignore_pos_profile = True invoice.flags.ignore_pos_profile = True
invoice.pos_profile = "" invoice.pos_profile = ""
@@ -336,7 +352,7 @@ class POSInvoiceMergeLog(Document):
for doc in invoice_docs: for doc in invoice_docs:
doc.load_from_db() doc.load_from_db()
inv = sales_invoice inv = sales_invoice
if doc.is_return: if doc.is_return and credit_notes:
for key, value in credit_notes.items(): for key, value in credit_notes.items():
if doc.name in value: if doc.name in value:
inv = key inv = key
@@ -446,9 +462,34 @@ def get_invoice_customer_map(pos_invoices):
pos_invoice_customer_map.setdefault(customer, []) pos_invoice_customer_map.setdefault(customer, [])
pos_invoice_customer_map[customer].append(invoice) 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 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): def consolidate_pos_invoices(pos_invoices=None, closing_entry=None):
invoices = pos_invoices or (closing_entry and closing_entry.get("pos_transactions")) invoices = pos_invoices or (closing_entry and closing_entry.get("pos_transactions"))
if frappe.flags.in_test and not invoices: 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): def create_merge_logs(invoice_by_customer, closing_entry=None):
try: try:
for customer, invoices in invoice_by_customer.items(): for customer, invoices_acc_dim in invoice_by_customer.items():
for _invoices in split_invoices(invoices): for invoices in invoices_acc_dim.values():
merge_log = frappe.new_doc("POS Invoice Merge Log") for _invoices in split_invoices(invoices):
merge_log.posting_date = ( merge_log = frappe.new_doc("POS Invoice Merge Log")
getdate(closing_entry.get("posting_date")) if closing_entry else nowdate() 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.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.customer = customer
merge_log.set("pos_invoices", _invoices) merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None
merge_log.save(ignore_permissions=True) merge_log.set("pos_invoices", _invoices)
merge_log.submit() merge_log.save(ignore_permissions=True)
merge_log.submit()
if closing_entry: if closing_entry:
closing_entry.set_status(update=True, status="Submitted") closing_entry.set_status(update=True, status="Submitted")
closing_entry.db_set("error_message", "") closing_entry.db_set("error_message", "")

View File

@@ -455,3 +455,58 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
frappe.set_user("Administrator") frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`") 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_mobile",
"contact_email", "contact_email",
"company_shipping_address_section", "company_shipping_address_section",
"shipping_address", "dispatch_address",
"dispatch_address_display",
"column_break_126", "column_break_126",
"shipping_address",
"shipping_address_display", "shipping_address_display",
"company_billing_address_section", "company_billing_address_section",
"billing_address", "billing_address",
@@ -1546,7 +1548,7 @@
{ {
"fieldname": "company_shipping_address_section", "fieldname": "company_shipping_address_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Company Shipping Address" "label": "Shipping Address"
}, },
{ {
"fieldname": "column_break_126", "fieldname": "column_break_126",
@@ -1627,13 +1629,28 @@
"fieldname": "update_outstanding_for_self", "fieldname": "update_outstanding_for_self",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Update Outstanding for Self" "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", "icon": "fa fa-file-text",
"idx": 204, "idx": 204,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2025-01-14 11:39:04.564610", "modified": "2025-04-09 16:49:22.175081",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice", "name": "Purchase Invoice",
@@ -1688,6 +1705,7 @@
"write": 1 "write": 1
} }
], ],
"row_format": "Dynamic",
"search_fields": "posting_date, supplier, bill_no, base_grand_total, outstanding_amount", "search_fields": "posting_date, supplier, bill_no, base_grand_total, outstanding_amount",
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",

View File

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

View File

@@ -1348,7 +1348,7 @@ class SalesInvoice(SellingController):
) )
for item in self.get("items"): 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 # Do not book income for transfer within same company
if self.is_internal_transfer(): if self.is_internal_transfer():
continue continue
@@ -2298,7 +2298,10 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
# Invert Addresses # Invert Addresses
update_address(target_doc, "supplier_address", "address_display", source_doc.company_address) update_address(target_doc, "supplier_address", "address_display", source_doc.company_address)
update_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( update_address(
target_doc, "billing_address", "billing_address_display", source_doc.customer_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, party_address=None,
company_address=None, company_address=None,
shipping_address=None, shipping_address=None,
dispatch_address=None,
pos_profile=None, pos_profile=None,
): ):
if not party: if not party:
@@ -92,6 +93,7 @@ def get_party_details(
party_address, party_address,
company_address, company_address,
shipping_address, shipping_address,
dispatch_address,
pos_profile, pos_profile,
) )
@@ -111,6 +113,7 @@ def _get_party_details(
party_address=None, party_address=None,
company_address=None, company_address=None,
shipping_address=None, shipping_address=None,
dispatch_address=None,
pos_profile=None, pos_profile=None,
): ):
party_details = frappe._dict( party_details = frappe._dict(
@@ -134,6 +137,7 @@ def _get_party_details(
party_address, party_address,
company_address, company_address,
shipping_address, shipping_address,
dispatch_address,
ignore_permissions=ignore_permissions, ignore_permissions=ignore_permissions,
) )
set_contact_details(party_details, party, party_type) set_contact_details(party_details, party, party_type)
@@ -191,34 +195,51 @@ def set_address_details(
party_address=None, party_address=None,
company_address=None, company_address=None,
shipping_address=None, shipping_address=None,
dispatch_address=None,
*, *,
ignore_permissions=False, ignore_permissions=False,
): ):
billing_address_field = ( # party_billing
party_billing_field = (
"customer_address" if party_type in ["Lead", "Prospect"] else party_type.lower() + "_address" "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: if doctype:
party_details.update( 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: if company_address:
party_details.company_address = company_address party_details.company_address = company_address
else: else:
@@ -256,22 +277,20 @@ def set_address_details(
**get_fetch_values(doctype, "shipping_address", party_details.billing_address), **get_fetch_values(doctype, "shipping_address", party_details.billing_address),
) )
party_address, shipping_address = ( party_billing, party_shipping = (
party_details.get(billing_address_field), party_details.get(party_billing_field),
party_details.shipping_address_name, party_details.get(party_shipping_field),
) )
party_details["tax_category"] = get_address_tax_category( party_details["tax_category"] = get_address_tax_category(
party.get("tax_category"), party.get("tax_category"), party_billing, party_shipping
party_address,
shipping_address if party_type != "Supplier" else party_address,
) )
if doctype in TRANSACTION_TYPES: if doctype in TRANSACTION_TYPES:
with temporary_flag("company", company): with temporary_flag("company", company):
get_regional_address_details(party_details, doctype, company) get_regional_address_details(party_details, doctype, company)
return party_address, shipping_address return party_billing, party_shipping
@erpnext.allow_regional @erpnext.allow_regional

View File

@@ -658,10 +658,6 @@ frappe.ui.form.on("Asset", {
} else { } else {
frm.set_value("purchase_invoice_item", data.purchase_invoice_item); 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", "fieldname": "total_asset_cost",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Total Asset Cost", "label": "Total Asset Cost",
"no_copy": 1,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"read_only": 1 "read_only": 1
}, },
@@ -520,6 +521,7 @@
"fieldname": "additional_asset_cost", "fieldname": "additional_asset_cost",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Additional Asset Cost", "label": "Additional Asset Cost",
"no_copy": 1,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"read_only": 1 "read_only": 1
}, },
@@ -593,7 +595,7 @@
"link_fieldname": "target_asset" "link_fieldname": "target_asset"
} }
], ],
"modified": "2025-04-15 16:33:17.189524", "modified": "2025-04-24 15:31:47.373274",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset", "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}")) frappe.throw(_(f"Selected {doctype} does not contain the Item Code {item_code}"))
first_item = matching_items[0] first_item = matching_items[0]
is_multiple_items = len(matching_items) > 1
return { return {
"company": purchase_doc.company, "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, "asset_quantity": first_item.qty,
"cost_center": first_item.cost_center or purchase_doc.get("cost_center"), "cost_center": first_item.cost_center or purchase_doc.get("cost_center"),
"asset_location": first_item.get("asset_location"), "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_receipt_item": first_item.name if doctype == "Purchase Receipt" else None,
"purchase_invoice_item": first_item.name if doctype == "Purchase Invoice" else None, "purchase_invoice_item": first_item.name if doctype == "Purchase Invoice" else None,
} }

View File

@@ -152,6 +152,9 @@ class AssetMovement(Document):
""", """,
args, args,
) )
self.validate_movement_cancellation(d, latest_movement_entry)
if latest_movement_entry: if latest_movement_entry:
current_location = latest_movement_entry[0][0] current_location = latest_movement_entry[0][0]
current_employee = latest_movement_entry[0][1] current_employee = latest_movement_entry[0][1]
@@ -179,3 +182,12 @@ class AssetMovement(Document):
d.asset, d.asset,
_("Asset issued to Employee {0}").format(get_link_to_form("Employee", current_employee)), _("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() movement1.cancel()
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location") 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): def create_asset_movement(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

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

View File

@@ -109,8 +109,10 @@
"contact_mobile", "contact_mobile",
"contact_email", "contact_email",
"shipping_address_section", "shipping_address_section",
"shipping_address", "dispatch_address",
"dispatch_address_display",
"column_break_99", "column_break_99",
"shipping_address",
"shipping_address_display", "shipping_address_display",
"company_billing_address_section", "company_billing_address_section",
"billing_address", "billing_address",
@@ -1269,13 +1271,28 @@
"fieldtype": "Tab Break", "fieldtype": "Tab Break",
"label": "Connections", "label": "Connections",
"show_dashboard": 1 "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", "icon": "fa fa-file-text",
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2024-03-20 16:03:31.611808", "modified": "2025-04-09 16:54:08.836106",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order", "name": "Purchase Order",
@@ -1322,6 +1339,7 @@
"write": 1 "write": 1
} }
], ],
"row_format": "Dynamic",
"search_fields": "status, transaction_date, supplier, grand_total", "search_fields": "status, transaction_date, supplier, grand_total",
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",

View File

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

View File

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

View File

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

View File

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

View File

@@ -98,7 +98,29 @@ class BuyingController(SubcontractingController):
item.from_warehouse, item.from_warehouse,
type_of_transaction="Outward", type_of_transaction="Outward",
do_not_submit=True, 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): def set_rate_for_standalone_debit_note(self):
if self.get("is_return") and self.get("update_stock") and not self.return_against: 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, company=self.company,
party_address=self.get("supplier_address"), party_address=self.get("supplier_address"),
shipping_address=self.get("shipping_address"), shipping_address=self.get("shipping_address"),
dispatch_address=self.get("dispatch_address"),
company_address=self.get("billing_address"), company_address=self.get("billing_address"),
fetch_payment_terms_template=not self.get("ignore_default_payment_terms_template"), fetch_payment_terms_template=not self.get("ignore_default_payment_terms_template"),
ignore_permissions=self.flags.ignore_permissions, ignore_permissions=self.flags.ignore_permissions,
@@ -238,6 +261,7 @@ class BuyingController(SubcontractingController):
address_dict = { address_dict = {
"supplier_address": "address_display", "supplier_address": "address_display",
"shipping_address": "shipping_address_display", "shipping_address": "shipping_address_display",
"dispatch_address": "dispatch_address_display",
"billing_address": "billing_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" "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): def set_missing_values(source, target):
doc = frappe.get_doc(target) doc = frappe.get_doc(target)
doc.is_return = 1 doc.is_return = 1

View File

@@ -811,7 +811,7 @@ class StockController(AccountsController):
) )
def make_package_for_transfer( 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( return make_bundle_for_material_transfer(
is_new=self.is_new(), is_new=self.is_new(),
@@ -822,6 +822,7 @@ class StockController(AccountsController):
warehouse=warehouse, warehouse=warehouse,
type_of_transaction=type_of_transaction, type_of_transaction=type_of_transaction,
do_not_submit=do_not_submit, do_not_submit=do_not_submit,
qty=qty,
) )
def get_sl_entries(self, d, args): def get_sl_entries(self, d, args):
@@ -1047,6 +1048,16 @@ class StockController(AccountsController):
def validate_qi_presence(self, row): def validate_qi_presence(self, row):
"""Check if QI is present on row level. Warn on save and stop on submit if missing.""" """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: if not row.quality_inspection:
msg = _("Row #{0}: Quality Inspection is required for Item {1}").format( msg = _("Row #{0}: Quality Inspection is required for Item {1}").format(
row.idx, frappe.bold(row.item_code) row.idx, frappe.bold(row.item_code)
@@ -1805,15 +1816,20 @@ def make_bundle_for_material_transfer(**kwargs):
kwargs.type_of_transaction = "Inward" kwargs.type_of_transaction = "Inward"
bundle_doc = frappe.copy_doc(bundle_doc) bundle_doc = frappe.copy_doc(bundle_doc)
bundle_doc.docstatus = 0
bundle_doc.warehouse = kwargs.warehouse bundle_doc.warehouse = kwargs.warehouse
bundle_doc.type_of_transaction = kwargs.type_of_transaction bundle_doc.type_of_transaction = kwargs.type_of_transaction
bundle_doc.voucher_type = kwargs.voucher_type 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.voucher_no = "" if kwargs.is_new or kwargs.docstatus == 2 else kwargs.voucher_no
bundle_doc.is_cancelled = 0 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: for row in bundle_doc.entries:
row.is_outward = 0 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) row.stock_value_difference = abs(row.stock_value_difference)
if kwargs.type_of_transaction == "Outward": if kwargs.type_of_transaction == "Outward":
row.qty *= -1 row.qty *= -1

View File

@@ -705,6 +705,12 @@ class JobCard(Document):
bold("Job Card"), get_link_to_form("Job Card", self.name) 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") precision = self.precision("total_completed_qty")
total_completed_qty = flt( total_completed_qty = flt(

View File

@@ -1768,6 +1768,7 @@ def get_sub_assembly_items(
continue continue
else: else:
stock_qty = stock_qty - _bin_dict.projected_qty stock_qty = stock_qty - _bin_dict.projected_qty
sub_assembly_items.append(d.item_code)
elif warehouse: elif warehouse:
bin_details.setdefault(d.item_code, get_bin_details(d, company, for_warehouse=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) { hours: function (frm, cdt, cdn) {
calculate_end_time(frm, cdt, cdn); calculate_end_time(frm, cdt, cdn);
update_billing_hours(frm, cdt, cdn);
calculate_billing_costing_amount(frm, cdt, cdn); calculate_billing_costing_amount(frm, cdt, cdn);
calculate_time_and_amount(frm); calculate_time_and_amount(frm);
}, },

View File

@@ -54,6 +54,12 @@ erpnext.buying = {
return erpnext.queries.company_address_query(this.frm.doc) 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) { setup_queries(doc, cdt, cdn) {
@@ -295,6 +301,12 @@ erpnext.buying = {
"shipping_address_display", true); "shipping_address_display", true);
} }
dispatch_address(){
var me = this;
erpnext.utils.get_address_display(this.frm, "dispatch_address",
"dispatch_address_display", true);
}
billing_address() { billing_address() {
erpnext.utils.get_address_display(this.frm, "billing_address", erpnext.utils.get_address_display(this.frm, "billing_address",
"billing_address_display", true); "billing_address_display", true);

View File

@@ -792,6 +792,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
return; return;
} }
if (item.serial_no) {
item.use_serial_batch_fields = 1
}
if (item && item.serial_no) { if (item && item.serial_no) {
if (!item.item_code) { if (!item.item_code) {
this.frm.trigger("item_code", cdt, cdn); 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_conversion_factor(item) {
// toggle read only property for conversion factor field if the uom and stock uom are same // 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) { 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) { batch_no(frm, cdt, cdn) {
let row = locals[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); var params = this._get_args(row);
params.batch_no = row.batch_no; params.batch_no = row.batch_no;
params.uom = row.uom; 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) { if (!args.shipping_address && frm.doc.shipping_address) {
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")) { if (frappe.meta.get_docfield(frm.doc.doctype, "taxes")) {

View File

@@ -85,9 +85,10 @@ class Employee(NestedSet):
self.reset_employee_emails_cache() self.reset_employee_emails_cache()
def update_user_permissions(self): def update_user_permissions(self):
if not has_permission("User Permission", ptype="write") or ( if not self.has_value_changed("user_id") and not self.has_value_changed("create_user_permission"):
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 return
employee_user_permission_exists = frappe.db.exists( 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": _("Low")},
{"doctype": "Issue Priority", "name": _("Medium")}, {"doctype": "Issue Priority", "name": _("Medium")},
{"doctype": "Issue Priority", "name": _("High")}, {"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": "Customer", "account_type": "Receivable"},
{"doctype": "Party Type", "party_type": "Supplier", "account_type": "Payable"}, {"doctype": "Party Type", "party_type": "Supplier", "account_type": "Payable"},
{"doctype": "Party Type", "party_type": "Employee", "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 # Invert the address on target doc creation
update_address(target_doc, "supplier_address", "address_display", source_doc.company_address) update_address(target_doc, "supplier_address", "address_display", source_doc.company_address)
update_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( update_address(
target_doc, "billing_address", "billing_address_display", source_doc.customer_address target_doc, "billing_address", "billing_address_display", source_doc.customer_address

View File

@@ -970,6 +970,11 @@ class Item(Document):
changed_fields = [ changed_fields = [
field for field in restricted_fields if cstr(self.get(field)) != cstr(values.get(field)) 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: if not changed_fields:
return return

View File

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

View File

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

View File

@@ -69,6 +69,8 @@ class PurchaseReceipt(BuyingController):
currency: DF.Link currency: DF.Link
disable_rounded_total: DF.Check disable_rounded_total: DF.Check
discount_amount: DF.Currency discount_amount: DF.Currency
dispatch_address: DF.Link | None
dispatch_address_display: DF.TextEditor | None
grand_total: DF.Currency grand_total: DF.Currency
group_same_items: DF.Check group_same_items: DF.Check
ignore_pricing_rule: 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 from pypika import functions as fn
import erpnext import erpnext
import erpnext.controllers
import erpnext.controllers.status_updater
from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.buying.doctype.supplier.test_supplier import create_supplier from erpnext.buying.doctype.supplier.test_supplier import create_supplier
from erpnext.controllers.buying_controller import QtyMismatchError from erpnext.controllers.buying_controller import QtyMismatchError
@@ -4129,6 +4131,59 @@ class TestPurchaseReceipt(FrappeTestCase):
self.assertTrue(sles) 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(): def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier 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() self.get_item_specification_details()
def on_update(self): def on_update(self):
if ( action_if_qi_in_draft = frappe.db.get_single_value(
frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_not_submitted") "Stock Settings", "action_if_quality_inspection_is_not_submitted"
== "Warn" )
):
if not action_if_qi_in_draft or action_if_qi_in_draft == "Warn":
self.update_qc_reference() self.update_qc_reference()
def on_submit(self): def on_submit(self):

View File

@@ -162,7 +162,7 @@ frappe.ui.form.on("Shipment", {
args: { contact: contact_name }, args: { contact: contact_name },
callback: function (r) { callback: function (r) {
if (r.message) { 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") { if (contact_type == "Delivery") {
frm.set_value("delivery_contact_name", ""); frm.set_value("delivery_contact_name", "");
frm.set_value("delivery_contact", ""); frm.set_value("delivery_contact", "");

View File

@@ -950,6 +950,15 @@ frappe.ui.form.on("Stock Entry Detail", {
}, },
batch_no(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: "",
});
}
validate_sample_quantity(frm, cdt, cdn); validate_sample_quantity(frm, cdt, cdn);
}, },
@@ -1074,6 +1083,13 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
serial_no(doc, cdt, cdn) { serial_no(doc, cdt, cdn) {
var item = frappe.get_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) { if (item?.serial_no) {
// Replace all occurences of comma with line feed // Replace all occurences of comma with line feed
item.serial_no = item.serial_no.replace(/,/g, "\n"); 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); frm.events.set_valuation_rate_and_qty(frm, cdt, cdn);
}, },
batch_no: function (frm, cdt, cdn) { batch_no(frm, cdt, cdn) {
frm.events.set_valuation_rate_and_qty(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) { qty: function (frm, cdt, cdn) {
@@ -310,6 +318,11 @@ frappe.ui.form.on("Stock Reconciliation Item", {
var child = locals[cdt][cdn]; var child = locals[cdt][cdn];
if (child.serial_no) { 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"); const serial_nos = child.serial_no.trim().split("\n");
frappe.model.set_value(cdt, cdn, "qty", serial_nos.length); 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) 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): def make_adjustment_entry(self, row, sl_entries):
from erpnext.stock.stock_ledger import get_stock_value_difference from erpnext.stock.stock_ledger import get_stock_value_difference

View File

@@ -10,14 +10,14 @@
"is_standard": "Yes", "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\"]]}", "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, "letterhead": null,
"modified": "2024-09-26 13:07:23.451182", "modified": "2025-04-24 13:07:23.451182",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Serial No Service Contract Expiry", "name": "Serial No Warranty Expiry",
"owner": "Administrator", "owner": "Administrator",
"prepared_report": 0, "prepared_report": 0,
"ref_doctype": "Serial No", "ref_doctype": "Serial No",
"report_name": "Serial No Service Contract Expiry", "report_name": "Serial No Warranty Expiry",
"report_type": "Report Builder", "report_type": "Report Builder",
"roles": [ "roles": [
{ {

View File

@@ -31,8 +31,8 @@ rfq = class rfq {
var me = this; var me = this;
$('.rfq-items').on("change", ".rfq-qty", function(){ $('.rfq-items').on("change", ".rfq-qty", function(){
me.idx = parseFloat($(this).attr('data-idx')); me.idx = parseFloat($(this).attr('data-idx'));
me.qty = parseFloat($(this).val()) || 0; me.qty = parseFloat(flt($(this).val())) || 0;
me.rate = parseFloat($(repl('.rfq-rate[data-idx=%(idx)s]',{'idx': me.idx})).val()); me.rate = parseFloat(flt($(repl('.rfq-rate[data-idx=%(idx)s]',{'idx': me.idx})).val()));
me.update_qty_rate(); me.update_qty_rate();
$(this).val(format_number(me.qty, doc.number_format, 2)); $(this).val(format_number(me.qty, doc.number_format, 2));
}) })
@@ -42,8 +42,8 @@ rfq = class rfq {
var me = this; var me = this;
$(".rfq-items").on("change", ".rfq-rate", function(){ $(".rfq-items").on("change", ".rfq-rate", function(){
me.idx = parseFloat($(this).attr('data-idx')); me.idx = parseFloat($(this).attr('data-idx'));
me.rate = parseFloat($(this).val()) || 0; me.rate = parseFloat(flt($(this).val())) || 0;
me.qty = parseFloat($(repl('.rfq-qty[data-idx=%(idx)s]',{'idx': me.idx})).val()); me.qty = parseFloat(flt($(repl('.rfq-qty[data-idx=%(idx)s]',{'idx': me.idx})).val()));
me.update_qty_rate(); me.update_qty_rate();
$(this).val(format_number(me.rate, doc.number_format, 2)); $(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"], 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.page.set_primary_action(__("Rename"), function () {
frm.get_field("rename_log").$wrapper.html("<p>Renaming...</p>");
frappe.call({ frappe.call({
method: "erpnext.utilities.doctype.rename_tool.rename_tool.upload", method: "erpnext.utilities.doctype.rename_tool.rename_tool.upload",
args: { args: {
select_doctype: frm.doc.select_doctype, select_doctype: frm.doc.select_doctype,
}, },
callback: function (r) { freeze: true,
let html = r.message.join("<br>"); 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) { frm.trigger("render_overview");
r.exc = frappe.utils.parse_json(r.exc); },
if (Array.isArray(r.exc)) { error: function (r) {
html += "<br>" + r.exc.join("<br>"); 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")) 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],
)