Merge branch 'develop' into FIX-36704
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/hu_chart_of_accounts_for_microenterprises_with_account_number.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/hu_chart_of_accounts_for_microenterprises_with_account_number.json
index 2cd6c0f..4013bb0 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/hu_chart_of_accounts_for_microenterprises_with_account_number.json
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/hu_chart_of_accounts_for_microenterprises_with_account_number.json
@@ -1,4 +1,6 @@
{
+ "country_code": "hu",
+ "name": "Hungary - Chart of Accounts for Microenterprises",
"tree": {
"SZ\u00c1MLAOSZT\u00c1LY BEFEKTETETT ESZK\u00d6Z\u00d6K": {
"account_number": 1,
@@ -1651,4 +1653,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
index 7e2f763..c2ddb39 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
@@ -424,7 +424,9 @@
vouchers = json.loads(vouchers)
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
transaction.add_payment_entries(vouchers)
- return frappe.get_doc("Bank Transaction", bank_transaction_name)
+ transaction.save()
+
+ return transaction
@frappe.whitelist()
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json
index b32022e..0328d51 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json
@@ -13,6 +13,7 @@
"status",
"bank_account",
"company",
+ "amended_from",
"section_break_4",
"deposit",
"withdrawal",
@@ -25,10 +26,10 @@
"transaction_id",
"transaction_type",
"section_break_14",
+ "column_break_oufv",
"payment_entries",
"section_break_18",
"allocated_amount",
- "amended_from",
"column_break_17",
"unallocated_amount",
"party_section",
@@ -138,10 +139,12 @@
"fieldtype": "Section Break"
},
{
+ "allow_on_submit": 1,
"fieldname": "allocated_amount",
"fieldtype": "Currency",
"label": "Allocated Amount",
- "options": "currency"
+ "options": "currency",
+ "read_only": 1
},
{
"fieldname": "amended_from",
@@ -157,10 +160,12 @@
"fieldtype": "Column Break"
},
{
+ "allow_on_submit": 1,
"fieldname": "unallocated_amount",
"fieldtype": "Currency",
"label": "Unallocated Amount",
- "options": "currency"
+ "options": "currency",
+ "read_only": 1
},
{
"fieldname": "party_section",
@@ -225,11 +230,15 @@
"fieldname": "bank_party_account_number",
"fieldtype": "Data",
"label": "Party Account No. (Bank Statement)"
+ },
+ {
+ "fieldname": "column_break_oufv",
+ "fieldtype": "Column Break"
}
],
"is_submittable": 1,
"links": [],
- "modified": "2023-06-06 13:58:12.821411",
+ "modified": "2023-11-18 18:32:47.203694",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Transaction",
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
index 4649d23..51c823a 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
@@ -2,78 +2,73 @@
# For license information, please see license.txt
import frappe
+from frappe import _
from frappe.utils import flt
from erpnext.controllers.status_updater import StatusUpdater
class BankTransaction(StatusUpdater):
- def after_insert(self):
- self.unallocated_amount = abs(flt(self.withdrawal) - flt(self.deposit))
+ def before_validate(self):
+ self.update_allocated_amount()
- def on_submit(self):
- self.clear_linked_payment_entries()
+ def validate(self):
+ self.validate_duplicate_references()
+
+ def validate_duplicate_references(self):
+ """Make sure the same voucher is not allocated twice within the same Bank Transaction"""
+ if not self.payment_entries:
+ return
+
+ pe = []
+ for row in self.payment_entries:
+ reference = (row.payment_document, row.payment_entry)
+ if reference in pe:
+ frappe.throw(
+ _("{0} {1} is allocated twice in this Bank Transaction").format(
+ row.payment_document, row.payment_entry
+ )
+ )
+ pe.append(reference)
+
+ def update_allocated_amount(self):
+ self.allocated_amount = (
+ sum(p.allocated_amount for p in self.payment_entries) if self.payment_entries else 0.0
+ )
+ self.unallocated_amount = abs(flt(self.withdrawal) - flt(self.deposit)) - self.allocated_amount
+
+ def before_submit(self):
+ self.allocate_payment_entries()
self.set_status()
if frappe.db.get_single_value("Accounts Settings", "enable_party_matching"):
self.auto_set_party()
- _saving_flag = False
-
- # nosemgrep: frappe-semgrep-rules.rules.frappe-modifying-but-not-comitting
- def on_update_after_submit(self):
- "Run on save(). Avoid recursion caused by multiple saves"
- if not self._saving_flag:
- self._saving_flag = True
- self.clear_linked_payment_entries()
- self.update_allocations()
- self._saving_flag = False
+ def before_update_after_submit(self):
+ self.validate_duplicate_references()
+ self.allocate_payment_entries()
+ self.update_allocated_amount()
def on_cancel(self):
- self.clear_linked_payment_entries(for_cancel=True)
- self.set_status(update=True)
+ for payment_entry in self.payment_entries:
+ self.clear_linked_payment_entry(payment_entry, for_cancel=True)
- def update_allocations(self):
- "The doctype does not allow modifications after submission, so write to the db direct"
- if self.payment_entries:
- allocated_amount = sum(p.allocated_amount for p in self.payment_entries)
- else:
- allocated_amount = 0.0
-
- amount = abs(flt(self.withdrawal) - flt(self.deposit))
- self.db_set("allocated_amount", flt(allocated_amount))
- self.db_set("unallocated_amount", amount - flt(allocated_amount))
- self.reload()
self.set_status(update=True)
def add_payment_entries(self, vouchers):
"Add the vouchers with zero allocation. Save() will perform the allocations and clearance"
if 0.0 >= self.unallocated_amount:
- frappe.throw(frappe._("Bank Transaction {0} is already fully reconciled").format(self.name))
+ frappe.throw(_("Bank Transaction {0} is already fully reconciled").format(self.name))
- added = False
for voucher in vouchers:
- # Can't add same voucher twice
- found = False
- for pe in self.payment_entries:
- if (
- pe.payment_document == voucher["payment_doctype"]
- and pe.payment_entry == voucher["payment_name"]
- ):
- found = True
-
- if not found:
- pe = {
+ self.append(
+ "payment_entries",
+ {
"payment_document": voucher["payment_doctype"],
"payment_entry": voucher["payment_name"],
"allocated_amount": 0.0, # Temporary
- }
- child = self.append("payment_entries", pe)
- added = True
-
- # runs on_update_after_submit
- if added:
- self.save()
+ },
+ )
def allocate_payment_entries(self):
"""Refactored from bank reconciliation tool.
@@ -90,6 +85,7 @@
- clear means: set the latest transaction date as clearance date
"""
remaining_amount = self.unallocated_amount
+ to_remove = []
for payment_entry in self.payment_entries:
if payment_entry.allocated_amount == 0.0:
unallocated_amount, should_clear, latest_transaction = get_clearance_details(
@@ -99,49 +95,39 @@
if 0.0 == unallocated_amount:
if should_clear:
latest_transaction.clear_linked_payment_entry(payment_entry)
- self.db_delete_payment_entry(payment_entry)
+ to_remove.append(payment_entry)
elif remaining_amount <= 0.0:
- self.db_delete_payment_entry(payment_entry)
+ to_remove.append(payment_entry)
- elif 0.0 < unallocated_amount and unallocated_amount <= remaining_amount:
- payment_entry.db_set("allocated_amount", unallocated_amount)
+ elif 0.0 < unallocated_amount <= remaining_amount:
+ payment_entry.allocated_amount = unallocated_amount
remaining_amount -= unallocated_amount
if should_clear:
latest_transaction.clear_linked_payment_entry(payment_entry)
- elif 0.0 < unallocated_amount and unallocated_amount > remaining_amount:
- payment_entry.db_set("allocated_amount", remaining_amount)
+ elif 0.0 < unallocated_amount:
+ payment_entry.allocated_amount = remaining_amount
remaining_amount = 0.0
elif 0.0 > unallocated_amount:
- self.db_delete_payment_entry(payment_entry)
- frappe.throw(frappe._("Voucher {0} is over-allocated by {1}").format(unallocated_amount))
+ frappe.throw(_("Voucher {0} is over-allocated by {1}").format(unallocated_amount))
- self.reload()
-
- def db_delete_payment_entry(self, payment_entry):
- frappe.db.delete("Bank Transaction Payments", {"name": payment_entry.name})
+ for payment_entry in to_remove:
+ self.remove(to_remove)
@frappe.whitelist()
def remove_payment_entries(self):
for payment_entry in self.payment_entries:
self.remove_payment_entry(payment_entry)
- # runs on_update_after_submit
- self.save()
+
+ self.save() # runs before_update_after_submit
def remove_payment_entry(self, payment_entry):
"Clear payment entry and clearance"
self.clear_linked_payment_entry(payment_entry, for_cancel=True)
self.remove(payment_entry)
- def clear_linked_payment_entries(self, for_cancel=False):
- if for_cancel:
- for payment_entry in self.payment_entries:
- self.clear_linked_payment_entry(payment_entry, for_cancel)
- else:
- self.allocate_payment_entries()
-
def clear_linked_payment_entry(self, payment_entry, for_cancel=False):
clearance_date = None if for_cancel else self.date
set_voucher_clearance(
@@ -162,11 +148,10 @@
deposit=self.deposit,
).match()
- if result:
- party_type, party = result
- frappe.db.set_value(
- "Bank Transaction", self.name, field={"party_type": party_type, "party": party}
- )
+ if not result:
+ return
+
+ self.party_type, self.party = result
@frappe.whitelist()
@@ -198,9 +183,7 @@
if gle["gl_account"] == gl_bank_account:
if gle["amount"] <= 0.0:
frappe.throw(
- frappe._("Voucher {0} value is broken: {1}").format(
- payment_entry.payment_entry, gle["amount"]
- )
+ _("Voucher {0} value is broken: {1}").format(payment_entry.payment_entry, gle["amount"])
)
unmatched_gles -= 1
@@ -221,7 +204,7 @@
def get_related_bank_gl_entries(doctype, docname):
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
- result = frappe.db.sql(
+ return frappe.db.sql(
"""
SELECT
ABS(gle.credit_in_account_currency - gle.debit_in_account_currency) AS amount,
@@ -239,7 +222,6 @@
dict(doctype=doctype, docname=docname),
as_dict=True,
)
- return result
def get_total_allocated_amount(doctype, docname):
@@ -365,6 +347,7 @@
if clearance_date:
vouchers = [{"payment_doctype": "Bank Transaction", "payment_name": self.name}]
bt.add_payment_entries(vouchers)
+ bt.save()
else:
for pe in bt.payment_entries:
if pe.payment_document == self.doctype and pe.payment_entry == self.name:
diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
index 5a1c139..1e64eee 100644
--- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
+++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
@@ -113,7 +113,7 @@
if as_dict:
data.append({frappe.scrub(header): row[index] for index, header in enumerate(headers)})
else:
- if not row[1]:
+ if not row[1] and len(row) > 1:
row[1] = row[0]
row[3] = row[2]
data.append(row)
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json
index 2eb54a5..906760e 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.json
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json
@@ -548,8 +548,16 @@
"icon": "fa fa-file-text",
"idx": 176,
"is_submittable": 1,
- "links": [],
- "modified": "2023-08-10 14:32:22.366895",
+ "links": [
+ {
+ "is_child_table": 1,
+ "link_doctype": "Bank Transaction Payments",
+ "link_fieldname": "payment_entry",
+ "parent_doctype": "Bank Transaction",
+ "table_fieldname": "payment_entries"
+ }
+ ],
+ "modified": "2023-11-23 12:11:04.128015",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index 85ef6f7..0ad20c3 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -508,7 +508,7 @@
).format(d.reference_name, d.account)
)
else:
- dr_or_cr = "debit" if d.credit > 0 else "credit"
+ dr_or_cr = "debit" if flt(d.credit) > 0 else "credit"
valid = False
for jvd in against_entries:
if flt(jvd[dr_or_cr]) > 0:
@@ -868,7 +868,7 @@
party_account_currency = d.account_currency
elif frappe.get_cached_value("Account", d.account, "account_type") in ["Bank", "Cash"]:
- bank_amount += d.debit_in_account_currency or d.credit_in_account_currency
+ bank_amount += flt(d.debit_in_account_currency) or flt(d.credit_in_account_currency)
bank_account_currency = d.account_currency
if party_type and pay_to_recd_from:
diff --git a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json
index 3ba8cea..3132fe9 100644
--- a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json
+++ b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json
@@ -203,7 +203,8 @@
"fieldtype": "Select",
"label": "Reference Type",
"no_copy": 1,
- "options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry"
+ "options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry",
+ "search_index": 1
},
{
"fieldname": "reference_name",
@@ -211,7 +212,8 @@
"in_list_view": 1,
"label": "Reference Name",
"no_copy": 1,
- "options": "reference_type"
+ "options": "reference_type",
+ "search_index": 1
},
{
"depends_on": "eval:doc.reference_type&&!in_list(doc.reference_type, ['Expense Claim', 'Asset', 'Employee Loan', 'Employee Advance'])",
@@ -278,13 +280,14 @@
"fieldtype": "Data",
"hidden": 1,
"label": "Reference Detail No",
- "no_copy": 1
+ "no_copy": 1,
+ "search_index": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2023-06-16 14:11:13.507807",
+ "modified": "2023-11-23 11:44:25.841187",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry Account",
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json
index 4d50a35..aa18156 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.json
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json
@@ -750,8 +750,16 @@
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
- "links": [],
- "modified": "2023-11-08 21:51:03.482709",
+ "links": [
+ {
+ "is_child_table": 1,
+ "link_doctype": "Bank Transaction Payments",
+ "link_fieldname": "payment_entry",
+ "parent_doctype": "Bank Transaction",
+ "table_fieldname": "payment_entries"
+ }
+ ],
+ "modified": "2023-11-23 12:07:20.887885",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json
index b88791d..ccb9e64 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json
@@ -212,9 +212,10 @@
],
"hide_toolbar": 1,
"icon": "icon-resize-horizontal",
+ "is_virtual": 1,
"issingle": 1,
"links": [],
- "modified": "2023-08-15 05:35:50.109290",
+ "modified": "2023-11-17 17:33:55.701726",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation",
@@ -239,6 +240,5 @@
],
"sort_field": "modified",
"sort_order": "DESC",
- "states": [],
- "track_changes": 1
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index 43167be..6673e8d 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -29,6 +29,58 @@
self.accounting_dimension_filter_conditions = []
self.ple_posting_date_filter = []
+ def load_from_db(self):
+ # 'modified' attribute is required for `run_doc_method` to work properly.
+ doc_dict = frappe._dict(
+ {
+ "modified": None,
+ "company": None,
+ "party": None,
+ "party_type": None,
+ "receivable_payable_account": None,
+ "default_advance_account": None,
+ "from_invoice_date": None,
+ "to_invoice_date": None,
+ "invoice_limit": 50,
+ "from_payment_date": None,
+ "to_payment_date": None,
+ "payment_limit": 50,
+ "minimum_invoice_amount": None,
+ "minimum_payment_amount": None,
+ "maximum_invoice_amount": None,
+ "maximum_payment_amount": None,
+ "bank_cash_account": None,
+ "cost_center": None,
+ "payment_name": None,
+ "invoice_name": None,
+ }
+ )
+ super(Document, self).__init__(doc_dict)
+
+ def save(self):
+ return
+
+ @staticmethod
+ def get_list(args):
+ pass
+
+ @staticmethod
+ def get_count(args):
+ pass
+
+ @staticmethod
+ def get_stats(args):
+ pass
+
+ def db_insert(self, *args, **kwargs):
+ pass
+
+ def db_update(self, *args, **kwargs):
+ pass
+
+ def delete(self):
+ pass
+
@frappe.whitelist()
def get_unreconciled_entries(self):
self.get_nonreconciled_payment_entries()
diff --git a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json
index 5b8556e..491c678 100644
--- a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json
+++ b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json
@@ -159,9 +159,10 @@
"label": "Difference Posting Date"
}
],
+ "is_virtual": 1,
"istable": 1,
"links": [],
- "modified": "2023-10-23 10:44:56.066303",
+ "modified": "2023-11-17 17:33:38.612615",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation Allocation",
diff --git a/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json b/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json
index c4dbd7e..7c9d49e 100644
--- a/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json
+++ b/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json
@@ -71,9 +71,10 @@
"label": "Exchange Rate"
}
],
+ "is_virtual": 1,
"istable": 1,
"links": [],
- "modified": "2022-11-08 18:18:02.502149",
+ "modified": "2023-11-17 17:33:45.455166",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation Invoice",
diff --git a/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json b/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json
index 17f3900..d199236 100644
--- a/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json
+++ b/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json
@@ -107,9 +107,10 @@
"options": "Cost Center"
}
],
+ "is_virtual": 1,
"istable": 1,
"links": [],
- "modified": "2023-09-03 07:43:29.965353",
+ "modified": "2023-11-17 17:33:34.818530",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation Payment",
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index e36e97b..9091a77 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -556,7 +556,7 @@
return bin_qty - pos_sales_qty, is_stock_item
else:
is_stock_item = True
- if frappe.db.exists("Product Bundle", item_code):
+ if frappe.db.exists("Product Bundle", {"name": item_code, "disabled": 0}):
return get_bundle_availability(item_code, warehouse), is_stock_item
else:
is_stock_item = False
diff --git a/erpnext/accounts/doctype/process_subscription/process_subscription.py b/erpnext/accounts/doctype/process_subscription/process_subscription.py
index 99269d6..0aa9970 100644
--- a/erpnext/accounts/doctype/process_subscription/process_subscription.py
+++ b/erpnext/accounts/doctype/process_subscription/process_subscription.py
@@ -17,11 +17,10 @@
def create_subscription_process(
- subscription: str | None, posting_date: Union[str, datetime.date] | None
+ subscription: str | None = None, posting_date: Union[str, datetime.date] | None = None
):
"""Create a new Process Subscription document"""
doc = frappe.new_doc("Process Subscription")
doc.subscription = subscription
doc.posting_date = getdate(posting_date)
- doc.insert(ignore_permissions=True)
doc.submit()
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index d7c2361..c6ae937 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -527,7 +527,11 @@
if self.update_stock == 1:
self.repost_future_sle_and_gle()
- self.update_project()
+ if (
+ frappe.db.get_single_value("Buying Settings", "project_update_frequency") == "Each Transaction"
+ ):
+ self.update_project()
+
update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference)
self.update_advance_tax_references()
@@ -1262,7 +1266,10 @@
if self.update_stock == 1:
self.repost_future_sle_and_gle()
- self.update_project()
+ if (
+ frappe.db.get_single_value("Buying Settings", "project_update_frequency") == "Each Transaction"
+ ):
+ self.update_project()
self.db_set("status", "Cancelled")
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
@@ -1281,13 +1288,21 @@
self.update_advance_tax_references(cancel=1)
def update_project(self):
- project_list = []
+ projects = frappe._dict()
for d in self.items:
- if d.project and d.project not in project_list:
- project = frappe.get_doc("Project", d.project)
- project.update_purchase_costing()
- project.db_update()
- project_list.append(d.project)
+ if d.project:
+ if self.docstatus == 1:
+ projects[d.project] = projects.get(d.project, 0) + d.base_net_amount
+ elif self.docstatus == 2:
+ projects[d.project] = projects.get(d.project, 0) - d.base_net_amount
+
+ pj = frappe.qb.DocType("Project")
+ for proj, value in projects.items():
+ res = (
+ frappe.qb.from_(pj).select(pj.total_purchase_cost).where(pj.name == proj).for_update().run()
+ )
+ current_purchase_cost = res and res[0][0] or 0
+ frappe.db.set_value("Project", proj, "total_purchase_cost", current_purchase_cost + value)
def validate_supplier_invoice(self):
if self.bill_date:
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index d167783..f209487 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -1615,7 +1615,8 @@
"hide_seconds": 1,
"label": "Inter Company Invoice Reference",
"options": "Purchase Invoice",
- "read_only": 1
+ "read_only": 1,
+ "search_index": 1
},
{
"fieldname": "customer_group",
@@ -2173,7 +2174,7 @@
"link_fieldname": "consolidated_invoice"
}
],
- "modified": "2023-11-20 11:51:43.555197",
+ "modified": "2023-11-23 16:56:29.679499",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py
index 3cf7d28..a3d8c23 100644
--- a/erpnext/accounts/doctype/subscription/subscription.py
+++ b/erpnext/accounts/doctype/subscription/subscription.py
@@ -676,7 +676,7 @@
def process_all(
- subscription: str | None, posting_date: Optional["DateTimeLikeObject"] = None
+ subscription: str | None = None, posting_date: Optional["DateTimeLikeObject"] = None
) -> None:
"""
Task to updates the status of all `Subscription` apart from those that are cancelled
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index 0e62ad6..7948e5f 100644
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -285,8 +285,8 @@
must_consider = False
if self.filters.get("for_revaluation_journals"):
- if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) or (
- (abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision)
+ if (abs(row.outstanding) > 0.0 / 10**self.currency_precision) or (
+ (abs(row.outstanding_in_account_currency) > 0.0 / 10**self.currency_precision)
):
must_consider = True
else:
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 7d91309..9d32a03 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -183,6 +183,7 @@
cost_center=None,
ignore_account_permission=False,
account_type=None,
+ start_date=None,
):
if not account and frappe.form_dict.get("account"):
account = frappe.form_dict.get("account")
@@ -196,6 +197,8 @@
cost_center = frappe.form_dict.get("cost_center")
cond = ["is_cancelled=0"]
+ if start_date:
+ cond.append("posting_date >= %s" % frappe.db.escape(cstr(start_date)))
if date:
cond.append("posting_date <= %s" % frappe.db.escape(cstr(date)))
else:
@@ -1833,6 +1836,28 @@
Table("outstanding").amount_in_account_currency >= self.max_outstanding
)
+ if self.limit and self.get_invoices:
+ outstanding_vouchers = (
+ qb.from_(ple)
+ .select(
+ ple.against_voucher_no.as_("voucher_no"),
+ Sum(ple.amount_in_account_currency).as_("amount_in_account_currency"),
+ )
+ .where(ple.delinked == 0)
+ .where(Criterion.all(filter_on_against_voucher_no))
+ .where(Criterion.all(self.common_filter))
+ .groupby(ple.against_voucher_type, ple.against_voucher_no, ple.party_type, ple.party)
+ .orderby(ple.posting_date, ple.voucher_no)
+ .having(qb.Field("amount_in_account_currency") > 0)
+ .limit(self.limit)
+ .run()
+ )
+ if outstanding_vouchers:
+ filter_on_voucher_no.append(ple.voucher_no.isin([x[0] for x in outstanding_vouchers]))
+ filter_on_against_voucher_no.append(
+ ple.against_voucher_no.isin([x[0] for x in outstanding_vouchers])
+ )
+
# build query for voucher amount
query_voucher_amount = (
qb.from_(ple)
diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py
index 84a428c..66930c0 100644
--- a/erpnext/assets/doctype/asset/depreciation.py
+++ b/erpnext/assets/doctype/asset/depreciation.py
@@ -509,6 +509,9 @@
def depreciate_asset(asset_doc, date, notes):
+ if not asset_doc.calculate_depreciation:
+ return
+
asset_doc.flags.ignore_validate_update_after_submit = True
make_new_active_asset_depr_schedules_and_cancel_current_ones(
@@ -521,6 +524,9 @@
def reset_depreciation_schedule(asset_doc, date, notes):
+ if not asset_doc.calculate_depreciation:
+ return
+
asset_doc.flags.ignore_validate_update_after_submit = True
make_new_active_asset_depr_schedules_and_cancel_current_ones(
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index 9e3ec6f..ca2b980 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -149,12 +149,7 @@
("Creditors - _TC", 0.0, 100000.0),
)
- gle = frappe.db.sql(
- """select account, debit, credit from `tabGL Entry`
- where voucher_type='Purchase Invoice' and voucher_no = %s
- order by account""",
- pi.name,
- )
+ gle = get_gl_entries("Purchase Invoice", pi.name)
self.assertSequenceEqual(gle, expected_gle)
pi.cancel()
@@ -264,12 +259,7 @@
),
)
- gle = frappe.db.sql(
- """select account, debit, credit from `tabGL Entry`
- where voucher_type='Journal Entry' and voucher_no = %s
- order by account""",
- asset.journal_entry_for_scrap,
- )
+ gle = get_gl_entries("Journal Entry", asset.journal_entry_for_scrap)
self.assertSequenceEqual(gle, expected_gle)
restore_asset(asset.name)
@@ -345,13 +335,7 @@
("Debtors - _TC", 25000.0, 0.0),
)
- gle = frappe.db.sql(
- """select account, debit, credit from `tabGL Entry`
- where voucher_type='Sales Invoice' and voucher_no = %s
- order by account""",
- si.name,
- )
-
+ gle = get_gl_entries("Sales Invoice", si.name)
self.assertSequenceEqual(gle, expected_gle)
si.cancel()
@@ -425,13 +409,7 @@
("Debtors - _TC", 40000.0, 0.0),
)
- gle = frappe.db.sql(
- """select account, debit, credit from `tabGL Entry`
- where voucher_type='Sales Invoice' and voucher_no = %s
- order by account""",
- si.name,
- )
-
+ gle = get_gl_entries("Sales Invoice", si.name)
self.assertSequenceEqual(gle, expected_gle)
def test_asset_with_maintenance_required_status_after_sale(self):
@@ -572,13 +550,7 @@
("CWIP Account - _TC", 5250.0, 0.0),
)
- pr_gle = frappe.db.sql(
- """select account, debit, credit from `tabGL Entry`
- where voucher_type='Purchase Receipt' and voucher_no = %s
- order by account""",
- pr.name,
- )
-
+ pr_gle = get_gl_entries("Purchase Receipt", pr.name)
self.assertSequenceEqual(pr_gle, expected_gle)
pi = make_invoice(pr.name)
@@ -591,13 +563,7 @@
("Creditors - _TC", 0.0, 5500.0),
)
- pi_gle = frappe.db.sql(
- """select account, debit, credit from `tabGL Entry`
- where voucher_type='Purchase Invoice' and voucher_no = %s
- order by account""",
- pi.name,
- )
-
+ pi_gle = get_gl_entries("Purchase Invoice", pi.name)
self.assertSequenceEqual(pi_gle, expected_gle)
asset = frappe.db.get_value("Asset", {"purchase_receipt": pr.name, "docstatus": 0}, "name")
@@ -624,13 +590,7 @@
expected_gle = (("_Test Fixed Asset - _TC", 5250.0, 0.0), ("CWIP Account - _TC", 0.0, 5250.0))
- gle = frappe.db.sql(
- """select account, debit, credit from `tabGL Entry`
- where voucher_type='Asset' and voucher_no = %s
- order by account""",
- asset_doc.name,
- )
-
+ gle = get_gl_entries("Asset", asset_doc.name)
self.assertSequenceEqual(gle, expected_gle)
def test_asset_cwip_toggling_cases(self):
@@ -653,10 +613,7 @@
asset_doc.available_for_use_date = nowdate()
asset_doc.calculate_depreciation = 0
asset_doc.submit()
- gle = frappe.db.sql(
- """select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""",
- asset_doc.name,
- )
+ gle = get_gl_entries("Asset", asset_doc.name)
self.assertFalse(gle)
# case 1 -- PR with cwip disabled, Asset with cwip enabled
@@ -670,10 +627,7 @@
asset_doc.available_for_use_date = nowdate()
asset_doc.calculate_depreciation = 0
asset_doc.submit()
- gle = frappe.db.sql(
- """select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""",
- asset_doc.name,
- )
+ gle = get_gl_entries("Asset", asset_doc.name)
self.assertFalse(gle)
# case 2 -- PR with cwip enabled, Asset with cwip disabled
@@ -686,10 +640,7 @@
asset_doc.available_for_use_date = nowdate()
asset_doc.calculate_depreciation = 0
asset_doc.submit()
- gle = frappe.db.sql(
- """select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""",
- asset_doc.name,
- )
+ gle = get_gl_entries("Asset", asset_doc.name)
self.assertTrue(gle)
# case 3 -- PI with cwip disabled, Asset with cwip enabled
@@ -702,10 +653,7 @@
asset_doc.available_for_use_date = nowdate()
asset_doc.calculate_depreciation = 0
asset_doc.submit()
- gle = frappe.db.sql(
- """select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""",
- asset_doc.name,
- )
+ gle = get_gl_entries("Asset", asset_doc.name)
self.assertFalse(gle)
# case 4 -- PI with cwip enabled, Asset with cwip disabled
@@ -718,10 +666,7 @@
asset_doc.available_for_use_date = nowdate()
asset_doc.calculate_depreciation = 0
asset_doc.submit()
- gle = frappe.db.sql(
- """select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""",
- asset_doc.name,
- )
+ gle = get_gl_entries("Asset", asset_doc.name)
self.assertTrue(gle)
frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", cwip)
@@ -1701,6 +1646,30 @@
self.assertRaises(frappe.ValidationError, jv.insert)
+ def test_multi_currency_asset_pr_creation(self):
+ pr = make_purchase_receipt(
+ item_code="Macbook Pro",
+ qty=1,
+ rate=100.0,
+ location="Test Location",
+ supplier="_Test Supplier USD",
+ currency="USD",
+ )
+
+ pr.submit()
+ self.assertTrue(get_gl_entries("Purchase Receipt", pr.name))
+
+
+def get_gl_entries(doctype, docname):
+ gl_entry = frappe.qb.DocType("GL Entry")
+ return (
+ frappe.qb.from_(gl_entry)
+ .select(gl_entry.account, gl_entry.debit, gl_entry.credit)
+ .where((gl_entry.voucher_type == doctype) & (gl_entry.voucher_no == docname))
+ .orderby(gl_entry.account)
+ .run()
+ )
+
def create_asset_data():
if not frappe.db.exists("Asset Category", "Computers"):
diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json
index eba9f24..3f8559e 100644
--- a/erpnext/buying/doctype/buying_settings/buying_settings.json
+++ b/erpnext/buying/doctype/buying_settings/buying_settings.json
@@ -17,6 +17,7 @@
"po_required",
"pr_required",
"blanket_order_allowance",
+ "project_update_frequency",
"column_break_12",
"maintain_same_rate",
"set_landed_cost_based_on_purchase_invoice_rate",
@@ -198,6 +199,14 @@
"fieldname": "auto_create_purchase_receipt",
"fieldtype": "Check",
"label": "Auto Create Purchase Receipt"
+ },
+ {
+ "default": "Each Transaction",
+ "description": "How often should Project be updated of Total Purchase Cost ?",
+ "fieldname": "project_update_frequency",
+ "fieldtype": "Select",
+ "label": "Update frequency of Project",
+ "options": "Each Transaction\nManual"
}
],
"icon": "fa fa-cog",
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index 55c01e8..0f8574c 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -16,7 +16,7 @@
make_purchase_invoice as make_pi_from_po,
)
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
-from erpnext.controllers.accounts_controller import update_child_qty_rate
+from erpnext.controllers.accounts_controller import InvalidQtyError, update_child_qty_rate
from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.material_request.material_request import make_purchase_order
@@ -27,6 +27,21 @@
class TestPurchaseOrder(FrappeTestCase):
+ def test_purchase_order_qty(self):
+ po = create_purchase_order(qty=1, do_not_save=True)
+ po.append(
+ "items",
+ {
+ "item_code": "_Test Item",
+ "qty": -1,
+ "rate": 10,
+ },
+ )
+ self.assertRaises(frappe.NonNegativeError, po.save)
+
+ po.items[1].qty = 0
+ self.assertRaises(InvalidQtyError, po.save)
+
def test_make_purchase_receipt(self):
po = create_purchase_order(do_not_submit=True)
self.assertRaises(frappe.ValidationError, make_purchase_receipt, po.name)
diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
index 2d706f4..98c1b38 100644
--- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
+++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
@@ -214,6 +214,7 @@
"fieldtype": "Float",
"in_list_view": 1,
"label": "Quantity",
+ "non_negative": 1,
"oldfieldname": "qty",
"oldfieldtype": "Currency",
"print_width": "60px",
@@ -917,7 +918,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2023-11-14 18:34:27.267382",
+ "modified": "2023-11-24 13:24:41.298416",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",
diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py
index 31bf439..b052f56 100644
--- a/erpnext/buying/doctype/supplier/supplier.py
+++ b/erpnext/buying/doctype/supplier/supplier.py
@@ -165,16 +165,17 @@
@frappe.validate_and_sanitize_search_inputs
def get_supplier_primary_contact(doctype, txt, searchfield, start, page_len, filters):
supplier = filters.get("supplier")
- return frappe.db.sql(
- """
- SELECT
- `tabContact`.name from `tabContact`,
- `tabDynamic Link`
- WHERE
- `tabContact`.name = `tabDynamic Link`.parent
- and `tabDynamic Link`.link_name = %(supplier)s
- and `tabDynamic Link`.link_doctype = 'Supplier'
- and `tabContact`.name like %(txt)s
- """,
- {"supplier": supplier, "txt": "%%%s%%" % txt},
- )
+ contact = frappe.qb.DocType("Contact")
+ dynamic_link = frappe.qb.DocType("Dynamic Link")
+
+ return (
+ frappe.qb.from_(contact)
+ .join(dynamic_link)
+ .on(contact.name == dynamic_link.parent)
+ .select(contact.name, contact.email_id)
+ .where(
+ (dynamic_link.link_name == supplier)
+ & (dynamic_link.link_doctype == "Supplier")
+ & (contact.name.like("%{0}%".format(txt)))
+ )
+ ).run(as_dict=False)
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index d12d50d..f551133 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -71,6 +71,10 @@
pass
+class InvalidQtyError(frappe.ValidationError):
+ pass
+
+
force_item_fields = (
"item_group",
"brand",
@@ -625,6 +629,7 @@
args["doctype"] = self.doctype
args["name"] = self.name
+ args["child_doctype"] = item.doctype
args["child_docname"] = item.name
args["ignore_pricing_rule"] = (
self.ignore_pricing_rule if hasattr(self, "ignore_pricing_rule") else 0
@@ -910,10 +915,16 @@
return flt(args.get(field, 0) / self.get("conversion_rate", 1))
def validate_qty_is_not_zero(self):
- if self.doctype != "Purchase Receipt":
- for item in self.items:
- if not item.qty:
- frappe.throw(_("Item quantity can not be zero"))
+ if self.doctype == "Purchase Receipt":
+ return
+
+ for item in self.items:
+ if not flt(item.qty):
+ frappe.throw(
+ msg=_("Row #{0}: Item quantity cannot be zero").format(item.idx),
+ title=_("Invalid Quantity"),
+ exc=InvalidQtyError,
+ )
def validate_account_currency(self, account, account_currency=None):
valid_currency = [self.company_currency]
@@ -3141,16 +3152,19 @@
conv_fac_precision = child_item.precision("conversion_factor") or 2
qty_precision = child_item.precision("qty") or 2
- if flt(child_item.billed_amt, rate_precision) > flt(
- flt(d.get("rate"), rate_precision) * flt(d.get("qty"), qty_precision), rate_precision
- ):
+ # Amount cannot be lesser than billed amount, except for negative amounts
+ row_rate = flt(d.get("rate"), rate_precision)
+ amount_below_billed_amt = flt(child_item.billed_amt, rate_precision) > flt(
+ row_rate * flt(d.get("qty"), qty_precision), rate_precision
+ )
+ if amount_below_billed_amt and row_rate > 0.0:
frappe.throw(
_("Row #{0}: Cannot set Rate if amount is greater than billed amount for Item {1}.").format(
child_item.idx, child_item.item_code
)
)
else:
- child_item.rate = flt(d.get("rate"), rate_precision)
+ child_item.rate = row_rate
if d.get("conversion_factor"):
if child_item.stock_uom == child_item.uom:
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index d34fbeb..5575a24 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -350,11 +350,12 @@
return il
def has_product_bundle(self, item_code):
- return frappe.db.sql(
- """select name from `tabProduct Bundle`
- where new_item_code=%s and docstatus != 2""",
- item_code,
- )
+ product_bundle = frappe.qb.DocType("Product Bundle")
+ return (
+ frappe.qb.from_(product_bundle)
+ .select(product_bundle.name)
+ .where((product_bundle.new_item_code == item_code) & (product_bundle.disabled == 0))
+ ).run()
def get_already_delivered_qty(self, current_docname, so, so_detail):
delivered_via_dn = frappe.db.sql(
diff --git a/erpnext/crm/doctype/competitor/competitor.json b/erpnext/crm/doctype/competitor/competitor.json
index 280441f..fd6da23 100644
--- a/erpnext/crm/doctype/competitor/competitor.json
+++ b/erpnext/crm/doctype/competitor/competitor.json
@@ -29,8 +29,16 @@
}
],
"index_web_pages_for_search": 1,
- "links": [],
- "modified": "2021-10-21 12:43:59.106807",
+ "links": [
+ {
+ "is_child_table": 1,
+ "link_doctype": "Competitor Detail",
+ "link_fieldname": "competitor",
+ "parent_doctype": "Quotation",
+ "table_fieldname": "competitors"
+ }
+ ],
+ "modified": "2023-11-23 19:33:54.284279",
"modified_by": "Administrator",
"module": "CRM",
"name": "Competitor",
@@ -64,5 +72,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index c6ab6f1..857471f 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -419,7 +419,6 @@
"erpnext.projects.doctype.project.project.collect_project_status",
],
"hourly_long": [
- "erpnext.accounts.doctype.process_subscription.process_subscription.create_subscription_process",
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries",
"erpnext.utilities.bulk_transaction.retry",
],
@@ -450,6 +449,7 @@
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly",
],
"daily_long": [
+ "erpnext.accounts.doctype.process_subscription.process_subscription.create_subscription_process",
"erpnext.setup.doctype.email_digest.email_digest.send",
"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.auto_update_latest_price_in_all_boms",
"erpnext.crm.utils.open_leads_opportunities_based_on_todays_event",
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index db6bc80..f303531 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -185,7 +185,8 @@
# override capacity for employee
production_capacity = 1
- if time_logs and production_capacity > len(time_logs):
+ overlap_count = self.get_overlap_count(time_logs)
+ if time_logs and production_capacity > overlap_count:
return {}
if self.workstation_type and time_logs:
@@ -195,6 +196,37 @@
return time_logs[-1]
+ @staticmethod
+ def get_overlap_count(time_logs):
+ count = 1
+
+ # Check overlap exists or not between the overlapping time logs with the current Job Card
+ for idx, row in enumerate(time_logs):
+ next_idx = idx
+ if idx + 1 < len(time_logs):
+ next_idx = idx + 1
+ next_row = time_logs[next_idx]
+ if row.name == next_row.name:
+ continue
+
+ if (
+ (
+ get_datetime(next_row.from_time) >= get_datetime(row.from_time)
+ and get_datetime(next_row.from_time) <= get_datetime(row.to_time)
+ )
+ or (
+ get_datetime(next_row.to_time) >= get_datetime(row.from_time)
+ and get_datetime(next_row.to_time) <= get_datetime(row.to_time)
+ )
+ or (
+ get_datetime(next_row.from_time) <= get_datetime(row.from_time)
+ and get_datetime(next_row.to_time) >= get_datetime(row.to_time)
+ )
+ ):
+ count += 1
+
+ return count
+
def get_time_logs(self, args, doctype, check_next_available_slot=False):
jc = frappe.qb.DocType("Job Card")
jctl = frappe.qb.DocType(doctype)
@@ -211,7 +243,14 @@
query = (
frappe.qb.from_(jctl)
.from_(jc)
- .select(jc.name.as_("name"), jctl.from_time, jctl.to_time, jc.workstation, jc.workstation_type)
+ .select(
+ jc.name.as_("name"),
+ jctl.name.as_("row_name"),
+ jctl.from_time,
+ jctl.to_time,
+ jc.workstation,
+ jc.workstation_type,
+ )
.where(
(jctl.parent == jc.name)
& (Criterion.any(time_conditions))
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index 0ae7657..e2c8f07 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -921,6 +921,20 @@
"Test RM Item 2 for Scrap Item Test",
]
+ from_time = add_days(now(), -1)
+ job_cards = frappe.get_all(
+ "Job Card Time Log",
+ fields=["distinct parent as name", "docstatus"],
+ filters={"from_time": (">", from_time)},
+ order_by="creation asc",
+ )
+
+ for job_card in job_cards:
+ if job_card.docstatus == 1:
+ frappe.get_doc("Job Card", job_card.name).cancel()
+
+ frappe.delete_doc("Job Card Time Log", job_card.name)
+
company = "_Test Company with perpetual inventory"
for item_code in items:
create_item(
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 0badab5..a73502d 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -259,6 +259,7 @@
erpnext.patches.v15_0.saudi_depreciation_warning
erpnext.patches.v15_0.delete_saudi_doctypes
erpnext.patches.v14_0.show_loan_management_deprecation_warning
+erpnext.patches.v14_0.clear_reconciliation_values_from_singles
execute:frappe.rename_doc("Report", "TDS Payable Monthly", "Tax Withholding Details", force=True)
[post_model_sync]
@@ -344,13 +345,12 @@
erpnext.patches.v15_0.update_sre_from_voucher_details
erpnext.patches.v14_0.rename_over_order_allowance_field
erpnext.patches.v14_0.migrate_delivery_stop_lock_field
-execute:frappe.db.set_single_value("Payment Reconciliation", "invoice_limit", 50)
-execute:frappe.db.set_single_value("Payment Reconciliation", "payment_limit", 50)
erpnext.patches.v14_0.add_default_for_repost_settings
erpnext.patches.v15_0.rename_daily_depreciation_to_depreciation_amount_based_on_num_days_in_month
erpnext.patches.v15_0.rename_depreciation_amount_based_on_num_days_in_month_to_daily_prorata_based
erpnext.patches.v15_0.set_reserved_stock_in_bin
erpnext.patches.v14_0.create_accounting_dimensions_in_supplier_quotation
erpnext.patches.v14_0.update_zero_asset_quantity_field
+execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency", "Each Transaction")
# below migration patch should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
diff --git a/erpnext/patches/v14_0/clear_reconciliation_values_from_singles.py b/erpnext/patches/v14_0/clear_reconciliation_values_from_singles.py
new file mode 100644
index 0000000..c1f5b60
--- /dev/null
+++ b/erpnext/patches/v14_0/clear_reconciliation_values_from_singles.py
@@ -0,0 +1,17 @@
+from frappe import qb
+
+
+def execute():
+ """
+ Clear `tabSingles` and Payment Reconciliation tables of values
+ """
+ singles = qb.DocType("Singles")
+ qb.from_(singles).delete().where(singles.doctype == "Payment Reconciliation").run()
+ doctypes = [
+ "Payment Reconciliation Invoice",
+ "Payment Reconciliation Payment",
+ "Payment Reconciliation Allocation",
+ ]
+ for x in doctypes:
+ dt = qb.DocType(x)
+ qb.from_(dt).delete().run()
diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js
index f366f77..2dac399 100644
--- a/erpnext/projects/doctype/project/project.js
+++ b/erpnext/projects/doctype/project/project.js
@@ -68,6 +68,10 @@
frm.events.create_duplicate(frm);
}, __("Actions"));
+ frm.add_custom_button(__('Update Total Purchase Cost'), () => {
+ frm.events.update_total_purchase_cost(frm);
+ }, __("Actions"));
+
frm.trigger("set_project_status_button");
@@ -92,6 +96,22 @@
},
+ update_total_purchase_cost: function(frm) {
+ frappe.call({
+ method: "erpnext.projects.doctype.project.project.recalculate_project_total_purchase_cost",
+ args: {project: frm.doc.name},
+ freeze: true,
+ freeze_message: __('Recalculating Purchase Cost against this Project...'),
+ callback: function(r) {
+ if (r && !r.exc) {
+ frappe.msgprint(__('Total Purchase Cost has been updated'));
+ frm.refresh();
+ }
+ }
+
+ });
+ },
+
set_project_status_button: function(frm) {
frm.add_custom_button(__('Set Project Status'), () => {
let d = new frappe.ui.Dialog({
diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py
index e9aed1a..4f2e395 100644
--- a/erpnext/projects/doctype/project/project.py
+++ b/erpnext/projects/doctype/project/project.py
@@ -4,11 +4,11 @@
import frappe
from email_reply_parser import EmailReplyParser
-from frappe import _
+from frappe import _, qb
from frappe.desk.reportview import get_match_cond
from frappe.model.document import Document
from frappe.query_builder import Interval
-from frappe.query_builder.functions import Count, CurDate, Date, UnixTimestamp
+from frappe.query_builder.functions import Count, CurDate, Date, Sum, UnixTimestamp
from frappe.utils import add_days, flt, get_datetime, get_time, get_url, nowtime, today
from frappe.utils.user import is_website_user
@@ -249,12 +249,7 @@
self.per_gross_margin = (self.gross_margin / flt(self.total_billed_amount)) * 100
def update_purchase_costing(self):
- total_purchase_cost = frappe.db.sql(
- """select sum(base_net_amount)
- from `tabPurchase Invoice Item` where project = %s and docstatus=1""",
- self.name,
- )
-
+ total_purchase_cost = calculate_total_purchase_cost(self.name)
self.total_purchase_cost = total_purchase_cost and total_purchase_cost[0][0] or 0
def update_sales_amount(self):
@@ -695,3 +690,29 @@
def get_users_email(doc):
return [d.email for d in doc.users if frappe.db.get_value("User", d.user, "enabled")]
+
+
+def calculate_total_purchase_cost(project: str | None = None):
+ if project:
+ pitem = qb.DocType("Purchase Invoice Item")
+ frappe.qb.DocType("Purchase Invoice Item")
+ total_purchase_cost = (
+ qb.from_(pitem)
+ .select(Sum(pitem.base_net_amount))
+ .where((pitem.project == project) & (pitem.docstatus == 1))
+ .run(as_list=True)
+ )
+ return total_purchase_cost
+ return None
+
+
+@frappe.whitelist()
+def recalculate_project_total_purchase_cost(project: str | None = None):
+ if project:
+ total_purchase_cost = calculate_total_purchase_cost(project)
+ frappe.db.set_value(
+ "Project",
+ project,
+ "total_purchase_cost",
+ (total_purchase_cost and total_purchase_cost[0][0] or 0),
+ )
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 2c40f49..6dc24fa 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -512,6 +512,7 @@
cost_center: item.cost_center,
tax_category: me.frm.doc.tax_category,
item_tax_template: item.item_tax_template,
+ child_doctype: item.doctype,
child_docname: item.name,
is_old_subcontracting_flow: me.frm.doc.is_old_subcontracting_flow,
}
diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.json b/erpnext/selling/doctype/product_bundle/product_bundle.json
index 56155fb..c4f21b6 100644
--- a/erpnext/selling/doctype/product_bundle/product_bundle.json
+++ b/erpnext/selling/doctype/product_bundle/product_bundle.json
@@ -1,315 +1,119 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 1,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2013-06-20 11:53:21",
- "custom": 0,
- "description": "Aggregate group of **Items** into another **Item**. This is useful if you are bundling a certain **Items** into a package and you maintain stock of the packed **Items** and not the aggregate **Item**. \n\nThe package **Item** will have \"Is Stock Item\" as \"No\" and \"Is Sales Item\" as \"Yes\".\n\nFor Example: If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.\n\nNote: BOM = Bill of Materials",
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 0,
+ "actions": [],
+ "allow_import": 1,
+ "creation": "2013-06-20 11:53:21",
+ "description": "Aggregate group of **Items** into another **Item**. This is useful if you are bundling a certain **Items** into a package and you maintain stock of the packed **Items** and not the aggregate **Item**. \n\nThe package **Item** will have \"Is Stock Item\" as \"No\" and \"Is Sales Item\" as \"Yes\".\n\nFor Example: If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.\n\nNote: BOM = Bill of Materials",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "basic_section",
+ "new_item_code",
+ "description",
+ "column_break_eonk",
+ "disabled",
+ "item_section",
+ "items",
+ "section_break_4",
+ "about"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "basic_section",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "basic_section",
+ "fieldtype": "Section Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "description": "",
- "fieldname": "new_item_code",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 1,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Parent Item",
- "length": 0,
- "no_copy": 1,
- "oldfieldname": "new_item_code",
- "oldfieldtype": "Data",
- "options": "Item",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "new_item_code",
+ "fieldtype": "Link",
+ "in_global_search": 1,
+ "in_list_view": 1,
+ "label": "Parent Item",
+ "no_copy": 1,
+ "oldfieldname": "new_item_code",
+ "oldfieldtype": "Data",
+ "options": "Item",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "description",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Description",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "description",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Description"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "description": "List items that form the package.",
- "fieldname": "item_section",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Items",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "description": "List items that form the package.",
+ "fieldname": "item_section",
+ "fieldtype": "Section Break",
+ "label": "Items"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "items",
- "fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Items",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "sales_bom_items",
- "oldfieldtype": "Table",
- "options": "Product Bundle Item",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "items",
+ "fieldtype": "Table",
+ "label": "Items",
+ "oldfieldname": "sales_bom_items",
+ "oldfieldtype": "Table",
+ "options": "Product Bundle Item",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_4",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "section_break_4",
+ "fieldtype": "Section Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "about",
- "fieldtype": "HTML",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "",
- "length": 0,
- "no_copy": 0,
- "options": "<h3>About Product Bundle</h3>\n\n<p>Aggregate group of <b>Items</b> into another <b>Item</b>. This is useful if you are bundling a certain <b>Items</b> into a package and you maintain stock of the packed <b>Items</b> and not the aggregate <b>Item</b>.</p>\n<p>The package <b>Item</b> will have <code>Is Stock Item</code> as <b>No</b> and <code>Is Sales Item</code> as <b>Yes</b>.</p>\n<h4>Example:</h4>\n<p>If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.</p>",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "fieldname": "about",
+ "fieldtype": "HTML",
+ "options": "<h3>About Product Bundle</h3>\n\n<p>Aggregate group of <b>Items</b> into another <b>Item</b>. This is useful if you are bundling a certain <b>Items</b> into a package and you maintain stock of the packed <b>Items</b> and not the aggregate <b>Item</b>.</p>\n<p>The package <b>Item</b> will have <code>Is Stock Item</code> as <b>No</b> and <code>Is Sales Item</code> as <b>Yes</b>.</p>\n<h4>Example:</h4>\n<p>If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.</p>"
+ },
+ {
+ "default": "0",
+ "fieldname": "disabled",
+ "fieldtype": "Check",
+ "label": "Disabled"
+ },
+ {
+ "fieldname": "column_break_eonk",
+ "fieldtype": "Column Break"
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "icon": "fa fa-sitemap",
- "idx": 1,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2020-09-18 17:26:09.703215",
- "modified_by": "Administrator",
- "module": "Selling",
- "name": "Product Bundle",
- "owner": "Administrator",
+ ],
+ "icon": "fa fa-sitemap",
+ "idx": 1,
+ "links": [],
+ "modified": "2023-11-22 15:20:46.805114",
+ "modified_by": "Administrator",
+ "module": "Selling",
+ "name": "Product Bundle",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Stock Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Stock Manager",
+ "share": 1,
"write": 1
- },
+ },
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Stock User",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
- },
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Stock User"
+ },
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Sales User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Sales User",
+ "share": 1,
"write": 1
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_order": "ASC",
- "track_changes": 0,
- "track_seen": 0
+ ],
+ "sort_field": "modified",
+ "sort_order": "ASC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.py b/erpnext/selling/doctype/product_bundle/product_bundle.py
index ac83c0f..3d4ffeb 100644
--- a/erpnext/selling/doctype/product_bundle/product_bundle.py
+++ b/erpnext/selling/doctype/product_bundle/product_bundle.py
@@ -59,10 +59,12 @@
"""Validates, main Item is not a stock item"""
if frappe.db.get_value("Item", self.new_item_code, "is_stock_item"):
frappe.throw(_("Parent Item {0} must not be a Stock Item").format(self.new_item_code))
+ if frappe.db.get_value("Item", self.new_item_code, "is_fixed_asset"):
+ frappe.throw(_("Parent Item {0} must not be a Fixed Asset").format(self.new_item_code))
def validate_child_items(self):
for item in self.items:
- if frappe.db.exists("Product Bundle", item.item_code):
+ if frappe.db.exists("Product Bundle", {"name": item.item_code, "disabled": 0}):
frappe.throw(
_(
"Row #{0}: Child Item should not be a Product Bundle. Please remove Item {1} and Save"
@@ -73,12 +75,20 @@
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_new_item_code(doctype, txt, searchfield, start, page_len, filters):
- from erpnext.controllers.queries import get_match_cond
+ product_bundles = frappe.db.get_list("Product Bundle", {"disabled": 0}, pluck="name")
- return frappe.db.sql(
- """select name, item_name, description from tabItem
- where is_stock_item=0 and name not in (select name from `tabProduct Bundle`)
- and %s like %s %s limit %s offset %s"""
- % (searchfield, "%s", get_match_cond(doctype), "%s", "%s"),
- ("%%%s%%" % txt, page_len, start),
+ item = frappe.qb.DocType("Item")
+ query = (
+ frappe.qb.from_(item)
+ .select(item.item_code, item.item_name)
+ .where(
+ (item.is_stock_item == 0) & (item.is_fixed_asset == 0) & (item[searchfield].like(f"%{txt}%"))
+ )
+ .limit(page_len)
+ .offset(start)
)
+
+ if product_bundles:
+ query = query.where(item.name.notin(product_bundles))
+
+ return query.run()
diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index 3ad18da..97b214e 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -214,13 +214,12 @@
label: __("Items to Reserve"),
allow_bulk_edit: false,
cannot_add_rows: true,
- cannot_delete_rows: true,
data: [],
fields: [
{
- fieldname: "name",
+ fieldname: "sales_order_item",
fieldtype: "Data",
- label: __("Name"),
+ label: __("Sales Order Item"),
reqd: 1,
read_only: 1,
},
@@ -260,7 +259,7 @@
],
primary_action_label: __("Reserve Stock"),
primary_action: () => {
- var data = {items: dialog.fields_dict.items.grid.get_selected_children()};
+ var data = {items: dialog.fields_dict.items.grid.data};
if (data.items && data.items.length > 0) {
frappe.call({
@@ -278,9 +277,6 @@
}
});
}
- else {
- frappe.msgprint(__("Please select items to reserve."));
- }
dialog.hide();
},
@@ -292,7 +288,7 @@
if (unreserved_qty > 0) {
dialog.fields_dict.items.df.data.push({
- 'name': item.name,
+ 'sales_order_item': item.name,
'item_code': item.item_code,
'warehouse': item.warehouse,
'qty_to_reserve': (unreserved_qty / flt(item.conversion_factor))
@@ -308,7 +304,7 @@
cancel_stock_reservation_entries(frm) {
const dialog = new frappe.ui.Dialog({
title: __("Stock Unreservation"),
- size: "large",
+ size: "extra-large",
fields: [
{
fieldname: "sr_entries",
@@ -316,14 +312,13 @@
label: __("Reserved Stock"),
allow_bulk_edit: false,
cannot_add_rows: true,
- cannot_delete_rows: true,
in_place_edit: true,
data: [],
fields: [
{
- fieldname: "name",
+ fieldname: "sre",
fieldtype: "Link",
- label: __("SRE"),
+ label: __("Stock Reservation Entry"),
options: "Stock Reservation Entry",
reqd: 1,
read_only: 1,
@@ -360,14 +355,14 @@
],
primary_action_label: __("Unreserve Stock"),
primary_action: () => {
- var data = {sr_entries: dialog.fields_dict.sr_entries.grid.get_selected_children()};
+ var data = {sr_entries: dialog.fields_dict.sr_entries.grid.data};
if (data.sr_entries && data.sr_entries.length > 0) {
frappe.call({
doc: frm.doc,
method: "cancel_stock_reservation_entries",
args: {
- sre_list: data.sr_entries,
+ sre_list: data.sr_entries.map(item => item.sre),
},
freeze: true,
freeze_message: __('Unreserving Stock...'),
@@ -377,9 +372,6 @@
}
});
}
- else {
- frappe.msgprint(__("Please select items to unreserve."));
- }
dialog.hide();
},
@@ -396,7 +388,7 @@
r.message.forEach(sre => {
if (flt(sre.reserved_qty) > flt(sre.delivered_qty)) {
dialog.fields_dict.sr_entries.df.data.push({
- 'name': sre.name,
+ 'sre': sre.name,
'item_code': sre.item_code,
'warehouse': sre.warehouse,
'qty': (flt(sre.reserved_qty) - flt(sre.delivered_qty))
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index a97198a..a23599b 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -688,7 +688,9 @@
"Sales Order Item": {
"doctype": "Material Request Item",
"field_map": {"name": "sales_order_item", "parent": "sales_order"},
- "condition": lambda item: not frappe.db.exists("Product Bundle", item.item_code)
+ "condition": lambda item: not frappe.db.exists(
+ "Product Bundle", {"name": item.item_code, "disabled": 0}
+ )
and get_remaining_qty(item) > 0,
"postprocess": update_item,
},
@@ -1309,7 +1311,7 @@
def is_product_bundle(item_code):
- return frappe.db.exists("Product Bundle", item_code)
+ return frappe.db.exists("Product Bundle", {"name": item_code, "disabled": 0})
@frappe.whitelist()
@@ -1521,7 +1523,7 @@
product_bundle_parents = [
pb.new_item_code
for pb in frappe.get_all(
- "Product Bundle", {"new_item_code": ["in", item_codes]}, ["new_item_code"]
+ "Product Bundle", {"new_item_code": ["in", item_codes], "disabled": 0}, ["new_item_code"]
)
]
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index d8b5878..a518597 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -51,6 +51,35 @@
def tearDown(self):
frappe.set_user("Administrator")
+ def test_sales_order_with_negative_rate(self):
+ """
+ Test if negative rate is allowed in Sales Order via doc submission and update items
+ """
+ so = make_sales_order(qty=1, rate=100, do_not_save=True)
+ so.append("items", {"item_code": "_Test Item", "qty": 1, "rate": -10})
+ so.save()
+ so.submit()
+
+ first_item = so.get("items")[0]
+ second_item = so.get("items")[1]
+ trans_item = json.dumps(
+ [
+ {
+ "item_code": first_item.item_code,
+ "rate": first_item.rate,
+ "qty": first_item.qty,
+ "docname": first_item.name,
+ },
+ {
+ "item_code": second_item.item_code,
+ "rate": -20,
+ "qty": second_item.qty,
+ "docname": second_item.name,
+ },
+ ]
+ )
+ update_child_qty_rate("Sales Order", trans_item, so.name)
+
def test_make_material_request(self):
so = make_sales_order(do_not_submit=True)
diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
index b4f7300..d4ccfc4 100644
--- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json
+++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
@@ -200,6 +200,7 @@
"fieldtype": "Float",
"in_list_view": 1,
"label": "Quantity",
+ "non_negative": 1,
"oldfieldname": "qty",
"oldfieldtype": "Currency",
"print_width": "100px",
@@ -895,7 +896,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2023-11-14 18:37:12.787893",
+ "modified": "2023-11-24 13:24:55.756320",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order Item",
diff --git a/erpnext/selling/report/lost_quotations/__init__.py b/erpnext/selling/report/lost_quotations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/selling/report/lost_quotations/__init__.py
diff --git a/erpnext/selling/report/lost_quotations/lost_quotations.js b/erpnext/selling/report/lost_quotations/lost_quotations.js
new file mode 100644
index 0000000..78e76cb
--- /dev/null
+++ b/erpnext/selling/report/lost_quotations/lost_quotations.js
@@ -0,0 +1,40 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.query_reports["Lost Quotations"] = {
+ filters: [
+ {
+ fieldname: "company",
+ label: __("Company"),
+ fieldtype: "Link",
+ options: "Company",
+ default: frappe.defaults.get_user_default("Company"),
+ },
+ {
+ label: "Timespan",
+ fieldtype: "Select",
+ fieldname: "timespan",
+ options: [
+ "Last Week",
+ "Last Month",
+ "Last Quarter",
+ "Last 6 months",
+ "Last Year",
+ "This Week",
+ "This Month",
+ "This Quarter",
+ "This Year",
+ ],
+ default: "This Year",
+ reqd: 1,
+ },
+ {
+ fieldname: "group_by",
+ label: __("Group By"),
+ fieldtype: "Select",
+ options: ["Lost Reason", "Competitor"],
+ default: "Lost Reason",
+ reqd: 1,
+ },
+ ],
+};
diff --git a/erpnext/selling/report/lost_quotations/lost_quotations.json b/erpnext/selling/report/lost_quotations/lost_quotations.json
new file mode 100644
index 0000000..8915bab
--- /dev/null
+++ b/erpnext/selling/report/lost_quotations/lost_quotations.json
@@ -0,0 +1,30 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2023-11-23 18:00:19.141922",
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "letter_head": null,
+ "letterhead": null,
+ "modified": "2023-11-23 19:27:28.854108",
+ "modified_by": "Administrator",
+ "module": "Selling",
+ "name": "Lost Quotations",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Quotation",
+ "report_name": "Lost Quotations",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Sales User"
+ },
+ {
+ "role": "Sales Manager"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/selling/report/lost_quotations/lost_quotations.py b/erpnext/selling/report/lost_quotations/lost_quotations.py
new file mode 100644
index 0000000..7c0bfbd
--- /dev/null
+++ b/erpnext/selling/report/lost_quotations/lost_quotations.py
@@ -0,0 +1,98 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from typing import Literal
+
+import frappe
+from frappe import _
+from frappe.model.docstatus import DocStatus
+from frappe.query_builder.functions import Coalesce, Count, Round, Sum
+from frappe.utils.data import get_timespan_date_range
+
+
+def execute(filters=None):
+ columns = get_columns(filters.get("group_by"))
+ from_date, to_date = get_timespan_date_range(filters.get("timespan").lower())
+ data = get_data(filters.get("company"), from_date, to_date, filters.get("group_by"))
+ return columns, data
+
+
+def get_columns(group_by: Literal["Lost Reason", "Competitor"]):
+ return [
+ {
+ "fieldname": "lost_reason" if group_by == "Lost Reason" else "competitor",
+ "label": _("Lost Reason") if group_by == "Lost Reason" else _("Competitor"),
+ "fieldtype": "Link",
+ "options": "Quotation Lost Reason" if group_by == "Lost Reason" else "Competitor",
+ "width": 200,
+ },
+ {
+ "filedname": "lost_quotations",
+ "label": _("Lost Quotations"),
+ "fieldtype": "Int",
+ "width": 150,
+ },
+ {
+ "filedname": "lost_quotations_pct",
+ "label": _("Lost Quotations %"),
+ "fieldtype": "Percent",
+ "width": 200,
+ },
+ {
+ "fieldname": "lost_value",
+ "label": _("Lost Value"),
+ "fieldtype": "Currency",
+ "width": 150,
+ },
+ {
+ "filedname": "lost_value_pct",
+ "label": _("Lost Value %"),
+ "fieldtype": "Percent",
+ "width": 200,
+ },
+ ]
+
+
+def get_data(
+ company: str, from_date: str, to_date: str, group_by: Literal["Lost Reason", "Competitor"]
+):
+ """Return quotation value grouped by lost reason or competitor"""
+ if group_by == "Lost Reason":
+ fieldname = "lost_reason"
+ dimension = frappe.qb.DocType("Quotation Lost Reason Detail")
+ elif group_by == "Competitor":
+ fieldname = "competitor"
+ dimension = frappe.qb.DocType("Competitor Detail")
+ else:
+ frappe.throw(_("Invalid Group By"))
+
+ q = frappe.qb.DocType("Quotation")
+
+ lost_quotation_condition = (
+ (q.status == "Lost")
+ & (q.docstatus == DocStatus.submitted())
+ & (q.transaction_date >= from_date)
+ & (q.transaction_date <= to_date)
+ & (q.company == company)
+ )
+
+ from_lost_quotations = frappe.qb.from_(q).where(lost_quotation_condition)
+ total_quotations = from_lost_quotations.select(Count(q.name))
+ total_value = from_lost_quotations.select(Sum(q.base_net_total))
+
+ query = (
+ frappe.qb.from_(q)
+ .select(
+ Coalesce(dimension[fieldname], _("Not Specified")),
+ Count(q.name).distinct(),
+ Round((Count(q.name).distinct() / total_quotations * 100), 2),
+ Sum(q.base_net_total),
+ Round((Sum(q.base_net_total) / total_value * 100), 2),
+ )
+ .left_join(dimension)
+ .on(dimension.parent == q.name)
+ .where(lost_quotation_condition)
+ .groupby(dimension[fieldname])
+ )
+
+ return query.run()
diff --git a/erpnext/setup/doctype/email_digest/email_digest.py b/erpnext/setup/doctype/email_digest/email_digest.py
index 4fc20e6..6ed44ff 100644
--- a/erpnext/setup/doctype/email_digest/email_digest.py
+++ b/erpnext/setup/doctype/email_digest/email_digest.py
@@ -382,9 +382,10 @@
"""Get income to date"""
balance = 0.0
count = 0
+ fy_start_date = get_fiscal_year(self.future_to_date)[1]
for account in self.get_root_type_accounts(root_type):
- balance += get_balance_on(account, date=self.future_to_date)
+ balance += get_balance_on(account, date=self.future_to_date, start_date=fy_start_date)
count += get_count_on(account, fieldname, date=self.future_to_date)
if fieldname == "income":
diff --git a/erpnext/setup/doctype/quotation_lost_reason/quotation_lost_reason.json b/erpnext/setup/doctype/quotation_lost_reason/quotation_lost_reason.json
index 5d778ee..0eae08e 100644
--- a/erpnext/setup/doctype/quotation_lost_reason/quotation_lost_reason.json
+++ b/erpnext/setup/doctype/quotation_lost_reason/quotation_lost_reason.json
@@ -1,83 +1,58 @@
{
- "allow_copy": 0,
- "allow_import": 1,
- "allow_rename": 0,
- "autoname": "field:order_lost_reason",
- "beta": 0,
- "creation": "2013-01-10 16:34:24",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Setup",
- "editable_grid": 0,
+ "actions": [],
+ "allow_import": 1,
+ "autoname": "field:order_lost_reason",
+ "creation": "2013-01-10 16:34:24",
+ "doctype": "DocType",
+ "document_type": "Setup",
+ "engine": "InnoDB",
+ "field_order": [
+ "order_lost_reason"
+ ],
"fields": [
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "fieldname": "order_lost_reason",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 1,
- "label": "Quotation Lost Reason",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "order_lost_reason",
- "oldfieldtype": "Data",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "fieldname": "order_lost_reason",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Quotation Lost Reason",
+ "oldfieldname": "order_lost_reason",
+ "oldfieldtype": "Data",
+ "reqd": 1,
+ "unique": 1
}
- ],
- "hide_heading": 0,
- "hide_toolbar": 0,
- "icon": "fa fa-flag",
- "idx": 1,
- "image_view": 0,
- "in_create": 0,
-
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2016-07-25 05:24:25.533953",
- "modified_by": "Administrator",
- "module": "Setup",
- "name": "Quotation Lost Reason",
- "owner": "Administrator",
+ ],
+ "icon": "fa fa-flag",
+ "idx": 1,
+ "links": [
+ {
+ "is_child_table": 1,
+ "link_doctype": "Quotation Lost Reason Detail",
+ "link_fieldname": "lost_reason",
+ "parent_doctype": "Quotation",
+ "table_fieldname": "lost_reasons"
+ }
+ ],
+ "modified": "2023-11-23 19:31:02.743353",
+ "modified_by": "Administrator",
+ "module": "Setup",
+ "name": "Quotation Lost Reason",
+ "naming_rule": "By fieldname",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Sales Master Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Sales Master Manager",
+ "share": 1,
"write": 1
}
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "track_seen": 0
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 66dd33a..f240136 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -615,7 +615,7 @@
items_list = [item.item_code for item in self.items]
return frappe.db.get_all(
"Product Bundle",
- filters={"new_item_code": ["in", items_list]},
+ filters={"new_item_code": ["in", items_list], "disabled": 0},
pluck="name",
)
@@ -938,7 +938,7 @@
},
"postprocess": update_item,
"condition": lambda item: (
- not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code})
+ not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code, "disabled": 0})
and flt(item.packed_qty) < flt(item.qty)
),
},
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index d8935fe..cb34497 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -512,8 +512,12 @@
def validate_duplicate_product_bundles_before_merge(self, old_name, new_name):
"Block merge if both old and new items have product bundles."
- old_bundle = frappe.get_value("Product Bundle", filters={"new_item_code": old_name})
- new_bundle = frappe.get_value("Product Bundle", filters={"new_item_code": new_name})
+ old_bundle = frappe.get_value(
+ "Product Bundle", filters={"new_item_code": old_name, "disabled": 0}
+ )
+ new_bundle = frappe.get_value(
+ "Product Bundle", filters={"new_item_code": new_name, "disabled": 0}
+ )
if old_bundle and new_bundle:
bundle_link = get_link_to_form("Product Bundle", old_bundle)
diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py
index a9e9ad1..35701c9 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.py
+++ b/erpnext/stock/doctype/packed_item/packed_item.py
@@ -55,7 +55,7 @@
def is_product_bundle(item_code: str) -> bool:
- return bool(frappe.db.exists("Product Bundle", {"new_item_code": item_code}))
+ return bool(frappe.db.exists("Product Bundle", {"new_item_code": item_code, "disabled": 0}))
def get_indexed_packed_items_table(doc):
@@ -111,7 +111,7 @@
product_bundle_item.uom,
product_bundle_item.description,
)
- .where(product_bundle.new_item_code == item_code)
+ .where((product_bundle.new_item_code == item_code) & (product_bundle.disabled == 0))
.orderby(product_bundle_item.idx)
)
return query.run(as_dict=True)
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index ed20209..e7f6204 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -233,7 +233,7 @@
for location in self.locations:
if location.warehouse and location.sales_order and location.sales_order_item:
item_details = {
- "name": location.sales_order_item,
+ "sales_order_item": location.sales_order_item,
"item_code": location.item_code,
"warehouse": location.warehouse,
"qty_to_reserve": (flt(location.picked_qty) - flt(location.stock_reserved_qty)),
@@ -368,7 +368,9 @@
frappe.throw("Row #{0}: Item Code is Mandatory".format(item.idx))
if not cint(
frappe.get_cached_value("Item", item.item_code, "is_stock_item")
- ) and not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}):
+ ) and not frappe.db.exists(
+ "Product Bundle", {"new_item_code": item.item_code, "disabled": 0}
+ ):
continue
item_code = item.item_code
reference = item.sales_order_item or item.material_request_item
@@ -507,7 +509,9 @@
# bundle_item_code: Dict[component, qty]
product_bundle_qty_map = {}
for bundle_item_code in bundles:
- bundle = frappe.get_last_doc("Product Bundle", {"new_item_code": bundle_item_code})
+ bundle = frappe.get_last_doc(
+ "Product Bundle", {"new_item_code": bundle_item_code, "disabled": 0}
+ )
product_bundle_qty_map[bundle_item_code] = {item.item_code: item.qty for item in bundle.items}
return product_bundle_qty_map
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index a5940f0..8647528 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -571,7 +571,7 @@
)
stock_value_diff = (
- flt(d.net_amount)
+ flt(d.base_net_amount)
+ flt(d.item_tax_amount / self.conversion_rate)
+ flt(d.landed_cost_voucher_amount)
)
@@ -781,7 +781,7 @@
for item in self.items:
if item.sales_order and item.sales_order_item:
item_details = {
- "name": item.sales_order_item,
+ "sales_order_item": item.sales_order_item,
"item_code": item.item_code,
"warehouse": item.warehouse,
"qty_to_reserve": item.stock_qty,
diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
index 0954282..cbfa4e0 100644
--- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
+++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
@@ -869,7 +869,7 @@
items = []
if items_details:
for item in items_details:
- so_item = frappe.get_doc("Sales Order Item", item.get("name"))
+ so_item = frappe.get_doc("Sales Order Item", item.get("sales_order_item"))
so_item.warehouse = item.get("warehouse")
so_item.qty_to_reserve = (
flt(item.get("qty_to_reserve"))
@@ -1053,12 +1053,14 @@
from_voucher_type: Literal["Pick List", "Purchase Receipt"] = None,
from_voucher_no: str = None,
from_voucher_detail_no: str = None,
- sre_list: list[dict] = None,
+ sre_list: list = None,
notify: bool = True,
) -> None:
"""Cancel Stock Reservation Entries."""
if not sre_list:
+ sre_list = {}
+
if voucher_type and voucher_no:
sre_list = get_stock_reservation_entries_for_voucher(
voucher_type, voucher_no, voucher_detail_no, fields=["name"]
@@ -1082,9 +1084,11 @@
sre_list = query.run(as_dict=True)
+ sre_list = [d.name for d in sre_list]
+
if sre_list:
for sre in sre_list:
- frappe.get_doc("Stock Reservation Entry", sre["name"]).cancel()
+ frappe.get_doc("Stock Reservation Entry", sre).cancel()
if notify:
msg = _("Stock Reservation Entries Cancelled")
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index c766cab..dfeb1ee 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -8,6 +8,7 @@
from frappe import _, throw
from frappe.model import child_table_fields, default_fields
from frappe.model.meta import get_field_precision
+from frappe.model.utils import get_fetch_values
from frappe.query_builder.functions import IfNull, Sum
from frappe.utils import add_days, add_months, cint, cstr, flt, getdate
@@ -149,7 +150,7 @@
def set_valuation_rate(out, args):
- if frappe.db.exists("Product Bundle", args.item_code, cache=True):
+ if frappe.db.exists("Product Bundle", {"name": args.item_code, "disabled": 0}, cache=True):
valuation_rate = 0.0
bundled_items = frappe.get_doc("Product Bundle", args.item_code)
@@ -571,6 +572,9 @@
item_tax_template = _get_item_tax_template(args, item_group_doc.taxes, out)
item_group = item_group_doc.parent_item_group
+ if args.child_doctype and item_tax_template:
+ out.update(get_fetch_values(args.child_doctype, "item_tax_template", item_tax_template))
+
def _get_item_tax_template(args, taxes, out=None, for_validate=False):
if out is None:
diff --git a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py
index b1da3ec..416cf48 100644
--- a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py
+++ b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py
@@ -166,4 +166,4 @@
if entries:
entries = ", ".join(entries)
- frappe.msgprint(_(f"Reposting entries created: {entries}"))
+ frappe.msgprint(_("Reposting entries created: {0}").format(entries))
diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py
index 7eba35d..b083614 100644
--- a/erpnext/utilities/transaction_base.py
+++ b/erpnext/utilities/transaction_base.py
@@ -98,6 +98,7 @@
"Selling Settings", "None", ["maintain_same_rate_action", "role_to_override_stop_action"]
)
+ stop_actions = []
for ref_dt, ref_dn_field, ref_link_field in ref_details:
reference_names = [d.get(ref_link_field) for d in self.get("items") if d.get(ref_link_field)]
reference_details = self.get_reference_details(reference_names, ref_dt + " Item")
@@ -108,7 +109,7 @@
if abs(flt(d.rate - ref_rate, d.precision("rate"))) >= 0.01:
if action == "Stop":
if role_allowed_to_override not in frappe.get_roles():
- frappe.throw(
+ stop_actions.append(
_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format(
d.idx, ref_dt, d.get(ref_dn_field), d.rate, ref_rate
)
@@ -121,6 +122,8 @@
title=_("Warning"),
indicator="orange",
)
+ if stop_actions:
+ frappe.throw(stop_actions, as_list=True)
def get_reference_details(self, reference_names, reference_doctype):
return frappe._dict(
diff --git a/erpnext/www/all-products/__init__.py b/erpnext/www/all-products/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/www/all-products/__init__.py
diff --git a/erpnext/www/shop-by-category/__init__.py b/erpnext/www/shop-by-category/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/www/shop-by-category/__init__.py