mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-30 18:34:48 +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):
|
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):
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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", "")
|
||||||
|
|||||||
@@ -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`")
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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)"),
|
||||||
|
|||||||
@@ -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",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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")) {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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"},
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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", "");
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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));
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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],
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user