mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-21 07:38:29 +00:00
Merge pull request #47328 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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", "")
|
||||
|
||||
@@ -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`")
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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],
|
||||
},
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)"),
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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", "");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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));
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
`);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user